Files
CodeNomad/src/lib/storage.ts
2025-11-17 01:38:53 +00:00

163 lines
4.7 KiB
TypeScript

import type { Preferences, RecentFolder, OpenCodeBinary } from "../stores/preferences"
export interface ConfigData {
preferences: Preferences
recentFolders: RecentFolder[]
opencodeBinaries: OpenCodeBinary[]
theme?: "light" | "dark" | "system"
}
export interface InstanceData {
messageHistory: string[]
}
export class FileStorage {
private configPath: string | undefined
private instancesDir: string | undefined
private configChangeListeners: Set<() => void> = new Set()
private initialized = false
constructor() {
this.initialize()
}
private async initialize() {
if (this.initialized) return
this.configPath = await window.electronAPI.getConfigPath()
this.instancesDir = await window.electronAPI.getInstancesDir()
// Listen for config changes from other instances
window.electronAPI.onConfigChanged(() => {
this.configChangeListeners.forEach((listener) => listener())
})
this.initialized = true
}
private async ensureInitialized() {
if (!this.initialized) {
await this.initialize()
}
}
private parseConfig(content: string): ConfigData {
const trimmed = content.trim()
try {
return JSON.parse(trimmed)
} catch (error) {
const firstBrace = trimmed.indexOf("{")
const lastBrace = trimmed.lastIndexOf("}")
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
const sanitized = trimmed.slice(firstBrace, lastBrace + 1)
if (sanitized.length !== trimmed.length) {
console.warn("Config file contained trailing data; attempting recovery")
}
try {
return JSON.parse(sanitized)
} catch {
// Fall through to rethrow original error below
}
}
throw error
}
}
// Config operations
async loadConfig(): Promise<ConfigData> {
await this.ensureInitialized()
try {
const content = await window.electronAPI.readConfigFile()
return this.parseConfig(content)
} catch (error) {
console.warn("Failed to load config, using defaults:", error)
return {
preferences: {
showThinkingBlocks: false,
environmentVariables: {},
modelRecents: [],
agentModelSelections: {},
diffViewMode: "split",
toolOutputExpansion: "expanded",
diagnosticsExpansion: "expanded",
},
recentFolders: [],
opencodeBinaries: [],
}
}
}
async saveConfig(config: ConfigData): Promise<void> {
await this.ensureInitialized()
try {
await window.electronAPI.writeConfigFile(JSON.stringify(config, null, 2))
} catch (error) {
console.error("Failed to save config:", error)
throw error
}
}
// Instance operations
async loadInstanceData(instanceId: string): Promise<InstanceData> {
await this.ensureInitialized()
try {
const filename = this.instanceIdToFilename(instanceId)
const content = await window.electronAPI.readInstanceFile(filename)
return JSON.parse(content)
} catch (error) {
console.warn(`Failed to load instance data for ${instanceId}, using defaults:`, error)
return {
messageHistory: [],
}
}
}
async saveInstanceData(instanceId: string, data: InstanceData): Promise<void> {
await this.ensureInitialized()
try {
const filename = this.instanceIdToFilename(instanceId)
await window.electronAPI.writeInstanceFile(filename, JSON.stringify(data, null, 2))
} catch (error) {
console.error(`Failed to save instance data for ${instanceId}:`, error)
throw error
}
}
async deleteInstanceData(instanceId: string): Promise<void> {
await this.ensureInitialized()
try {
const filename = this.instanceIdToFilename(instanceId)
await window.electronAPI.deleteInstanceFile(filename)
} catch (error) {
console.error(`Failed to delete instance data for ${instanceId}:`, error)
throw error
}
}
// Convert folder path to safe filename
private instanceIdToFilename(instanceId: string): string {
// Convert folder path to safe filename
// Replace path separators and other invalid characters
return instanceId
.replace(/[\\/]/g, "_") // Replace path separators
.replace(/[^a-zA-Z0-9_.-]/g, "_") // Replace other invalid chars
.replace(/_{2,}/g, "_") // Replace multiple underscores with single
.replace(/^_|_$/g, "") // Remove leading/trailing underscores
.toLowerCase()
}
// Config change listeners
onConfigChanged(listener: () => void): () => void {
this.configChangeListeners.add(listener)
return () => this.configChangeListeners.delete(listener)
}
}
// Singleton instance
export const storage = new FileStorage()