Makelore 2.0 initial clean snapshot
This commit is contained in:
188
electron/opencode/project-store.ts
Normal file
188
electron/opencode/project-store.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
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<string, OpencodeProject>;
|
||||
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<ProjectStoreData | undefined>;
|
||||
write(data: ProjectStoreData): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ProjectKeyValueStore {
|
||||
get(key: string): unknown;
|
||||
set(key: string, value: ProjectStoreData): unknown;
|
||||
}
|
||||
|
||||
export type OpencodeProjectStore = ReturnType<typeof createProjectStore>;
|
||||
|
||||
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<ProjectStorage> {
|
||||
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<ProjectStoreData> {
|
||||
return (await storage.read()) ?? createEmptyData();
|
||||
}
|
||||
|
||||
export function createProjectStore(storage: ProjectStorage) {
|
||||
const listeners = new Set<ProjectStoreListener>();
|
||||
|
||||
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<OpencodeProject> {
|
||||
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<OpencodeProject> {
|
||||
return await upsertProjectRecord(input, { activate: true });
|
||||
},
|
||||
|
||||
async rememberProject(input: string): Promise<OpencodeProject> {
|
||||
return await upsertProjectRecord(input, { activate: false });
|
||||
},
|
||||
|
||||
async listProjects(): Promise<OpencodeProject[]> {
|
||||
const data = await readData(storage);
|
||||
return Object.values(data.projects).sort((left, right) =>
|
||||
right.lastOpenedAt.localeCompare(left.lastOpenedAt),
|
||||
);
|
||||
},
|
||||
|
||||
async getActiveProject(): Promise<OpencodeProject | null> {
|
||||
const data = await readData(storage);
|
||||
return data.activeProjectId ? data.projects[data.activeProjectId] ?? null : null;
|
||||
},
|
||||
|
||||
async setActiveProject(projectId: string | null): Promise<OpencodeProject | null> {
|
||||
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<void> {
|
||||
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);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user