45105cd069
Auto-seeds 10 presets on first launch; visual designer with palette, drag, resize, and bricks CRUD. Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
3.0 KiB
TypeScript
107 lines
3.0 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import type { CharacterSheet, SystemDefinition } from '../shared/types.ts';
|
|
import { validateSystemDefinition } from '../shared/systemValidate.ts';
|
|
|
|
type Meta = {
|
|
systems: Record<string, SystemDefinition>;
|
|
characters: Record<string, CharacterSheet>;
|
|
};
|
|
|
|
export class AppStore {
|
|
private readonly rootDir: string;
|
|
private readonly dbPath: string;
|
|
private data: Meta = { systems: {}, characters: {} };
|
|
|
|
constructor(dbPath: string) {
|
|
// dbPath historically pointed at *.sqlite — keep parent dir, use store.json
|
|
this.rootDir = path.dirname(dbPath);
|
|
this.dbPath = path.join(this.rootDir, 'store.json');
|
|
fs.mkdirSync(this.rootDir, { recursive: true });
|
|
this.load();
|
|
}
|
|
|
|
private load(): void {
|
|
try {
|
|
const raw = fs.readFileSync(this.dbPath, 'utf8');
|
|
const parsed = JSON.parse(raw) as Meta;
|
|
this.data = {
|
|
systems: parsed.systems ?? {},
|
|
characters: parsed.characters ?? {},
|
|
};
|
|
} catch {
|
|
this.data = { systems: {}, characters: {} };
|
|
this.persist();
|
|
}
|
|
}
|
|
|
|
private persist(): void {
|
|
fs.writeFileSync(this.dbPath, JSON.stringify(this.data, null, 2), 'utf8');
|
|
}
|
|
|
|
listSystems(): Array<{ id: string; name: string; revision: number; schemaVersion: number }> {
|
|
return Object.values(this.data.systems)
|
|
.map((s) => ({
|
|
id: s.id,
|
|
name: s.name,
|
|
revision: s.revision,
|
|
schemaVersion: s.schemaVersion,
|
|
}))
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
getSystem(id: string): SystemDefinition | null {
|
|
return this.data.systems[id] ?? null;
|
|
}
|
|
|
|
upsertSystem(system: SystemDefinition): SystemDefinition {
|
|
const check = validateSystemDefinition(system);
|
|
if (!check.ok) {
|
|
throw new Error(check.errors.map((e) => e.message).join('; '));
|
|
}
|
|
const existing = this.data.systems[system.id];
|
|
const next: SystemDefinition = {
|
|
...system,
|
|
revision: existing ? existing.revision + 1 : system.revision || 1,
|
|
};
|
|
this.data.systems[next.id] = next;
|
|
this.persist();
|
|
return next;
|
|
}
|
|
|
|
deleteSystem(id: string): void {
|
|
delete this.data.systems[id];
|
|
for (const [cid, ch] of Object.entries(this.data.characters)) {
|
|
if (ch.systemId === id) delete this.data.characters[cid];
|
|
}
|
|
this.persist();
|
|
}
|
|
|
|
listCharacters(systemId?: string): Array<{ id: string; name: string; systemId: string }> {
|
|
return Object.values(this.data.characters)
|
|
.filter((c) => (systemId ? c.systemId === systemId : true))
|
|
.map((c) => ({ id: c.id, name: c.name, systemId: c.systemId }))
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
getCharacter(id: string): CharacterSheet | null {
|
|
return this.data.characters[id] ?? null;
|
|
}
|
|
|
|
upsertCharacter(character: CharacterSheet): CharacterSheet {
|
|
this.data.characters[character.id] = character;
|
|
this.persist();
|
|
return character;
|
|
}
|
|
|
|
deleteCharacter(id: string): void {
|
|
delete this.data.characters[id];
|
|
this.persist();
|
|
}
|
|
|
|
close(): void {
|
|
this.persist();
|
|
}
|
|
}
|