import { createHash } from 'node:crypto'; import path from 'node:path'; export interface OpencodeProject { id: string; path: string; name: string; createdAt: string; updatedAt: string; lastOpenedAt: string; } export interface ProjectStoreData { projects: Record; activeProjectId: string | null; } export type ProjectStoreChange = | { type: 'upsert'; project: OpencodeProject } | { type: 'remove'; projectId: string }; export type ProjectStoreListener = (change: ProjectStoreChange) => void; export interface ProjectStorage { read(): Promise; write(data: ProjectStoreData): Promise; } export interface ProjectKeyValueStore { get(key: string): unknown; set(key: string, value: ProjectStoreData): unknown; } export type OpencodeProjectStore = ReturnType; const DEFAULT_PROJECT_STORE_KEY = 'projects'; export function normalizeProjectPath(input: string): string { const normalized = path.normalize(path.resolve(input.trim())); return process.platform === 'win32' ? normalized.toLowerCase() : normalized; } export function getProjectIdForPath(input: string): string { const hash = createHash('sha256').update(normalizeProjectPath(input)).digest('hex'); return `prj_${hash.slice(0, 16)}`; } export function createMemoryProjectStorage(initial?: ProjectStoreData): ProjectStorage { let stored = initial ? structuredClone(initial) : undefined; return { async read() { return stored ? structuredClone(stored) : undefined; }, async write(data) { stored = structuredClone(data); }, }; } export function createProjectStorageFromStore( store: ProjectKeyValueStore, key = DEFAULT_PROJECT_STORE_KEY, ): ProjectStorage { return { async read() { const data = store.get(key); return data ? structuredClone(data) as ProjectStoreData : undefined; }, async write(data) { store.set(key, structuredClone(data)); }, }; } export async function createElectronProjectStorage(): Promise { const Store = (await import('electron-store')).default; const store = new Store<{ projects?: ProjectStoreData }>({ name: 'opencode-projects', }); return createProjectStorageFromStore(store); } function createEmptyData(): ProjectStoreData { return { projects: {}, activeProjectId: null, }; } async function readData(storage: ProjectStorage): Promise { return (await storage.read()) ?? createEmptyData(); } export function createProjectStore(storage: ProjectStorage) { const listeners = new Set(); function emit(change: ProjectStoreChange): void { for (const listener of listeners) { try { listener(change); } catch { // An observer must not break project persistence for other callers. } } } async function upsertProjectRecord( input: string, options?: { activate?: boolean }, ): Promise { const data = await readData(storage); const normalizedPath = normalizeProjectPath(input); const id = getProjectIdForPath(normalizedPath); const now = new Date().toISOString(); const existing = data.projects[id]; const project: OpencodeProject = existing ? { ...existing, path: normalizedPath, name: path.basename(normalizedPath), updatedAt: now, lastOpenedAt: now, } : { id, path: normalizedPath, name: path.basename(normalizedPath), createdAt: now, updatedAt: now, lastOpenedAt: now, }; data.projects[id] = project; if (options?.activate) { data.activeProjectId = id; } await storage.write(data); emit({ type: 'upsert', project }); return project; } return { async openFolder(input: string): Promise { return await upsertProjectRecord(input, { activate: true }); }, async rememberProject(input: string): Promise { return await upsertProjectRecord(input, { activate: false }); }, async listProjects(): Promise { const data = await readData(storage); return Object.values(data.projects).sort((left, right) => right.lastOpenedAt.localeCompare(left.lastOpenedAt), ); }, async getActiveProject(): Promise { const data = await readData(storage); return data.activeProjectId ? data.projects[data.activeProjectId] ?? null : null; }, async setActiveProject(projectId: string | null): Promise { const data = await readData(storage); data.activeProjectId = projectId && data.projects[projectId] ? projectId : null; await storage.write(data); return data.activeProjectId ? data.projects[data.activeProjectId] : null; }, async removeProject(projectId: string): Promise { const data = await readData(storage); delete data.projects[projectId]; if (data.activeProjectId === projectId) { data.activeProjectId = null; } await storage.write(data); emit({ type: 'remove', projectId }); }, subscribe(listener: ProjectStoreListener): () => void { listeners.add(listener); return () => { listeners.delete(listener); }; }, }; }