import fs from 'node:fs/promises'; import path from 'node:path'; import { createHash } from 'node:crypto'; import type { GameSystemListItem } from '../../shared/ipc/contracts'; import type { SystemDefinition } from '../../shared/characterSheet/types'; import { validateSystemDefinition } from '../../shared/characterSheet/systemValidate'; import { snapshotHash } from '../../shared/characterSheet/snapshot'; export type { GameSystemListItem }; type Manifest = { systems: GameSystemListItem[]; }; export class GameSystemsStore { private readonly rootDir: string; private readonly manifestPath: string; private readonly snapshotsDir: string; private manifest: Manifest = { systems: [] }; private loaded = false; constructor(userData: string) { this.rootDir = path.join(userData, 'game-systems'); this.manifestPath = path.join(this.rootDir, 'manifest.json'); this.snapshotsDir = path.join(this.rootDir, 'snapshots'); } async ensureLoaded(): Promise { if (this.loaded) return; await fs.mkdir(this.snapshotsDir, { recursive: true }); try { const raw = await fs.readFile(this.manifestPath, 'utf8'); this.manifest = JSON.parse(raw) as Manifest; if (!Array.isArray(this.manifest.systems)) this.manifest.systems = []; } catch { this.manifest = { systems: [] }; } this.loaded = true; } async list(): Promise { await this.ensureLoaded(); return [...this.manifest.systems]; } private systemPath(id: string): string { return path.join(this.rootDir, id, 'system.json'); } async get(id: string): Promise { await this.ensureLoaded(); try { const raw = await fs.readFile(this.systemPath(id), 'utf8'); return JSON.parse(raw) as SystemDefinition; } catch { return null; } } async importSystem(system: SystemDefinition): Promise { await this.ensureLoaded(); const check = validateSystemDefinition(system); if (!check.ok) throw new Error(check.errors.map((e) => e.message).join('; ')); const dir = path.join(this.rootDir, system.id); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(this.systemPath(system.id), JSON.stringify(system, null, 2), 'utf8'); const hash = snapshotHash(system); await fs.writeFile(path.join(this.snapshotsDir, `${hash}.json`), JSON.stringify(system), 'utf8'); const item: GameSystemListItem = { id: system.id, name: system.name, edition: system.edition, schemaVersion: system.schemaVersion, revision: system.revision, }; this.manifest.systems = this.manifest.systems.filter((s) => s.id !== system.id).concat(item); await this.saveManifest(); return item; } async delete(id: string): Promise { await this.ensureLoaded(); this.manifest.systems = this.manifest.systems.filter((s) => s.id !== id); await this.saveManifest(); try { await fs.rm(path.join(this.rootDir, id), { recursive: true, force: true }); } catch { // ignore } // Caller clears player bindings; return id for convenience return [id]; } async getSnapshot(hash: string): Promise { try { const raw = await fs.readFile(path.join(this.snapshotsDir, `${hash}.json`), 'utf8'); return JSON.parse(raw) as SystemDefinition; } catch { return null; } } async bindSnapshot(systemId: string): Promise<{ systemId: string; snapshotHash: string; system: SystemDefinition }> { const system = await this.get(systemId); if (!system) throw new Error(`System not found: ${systemId}`); const hash = snapshotHash(system); const snapPath = path.join(this.snapshotsDir, `${hash}.json`); try { await fs.access(snapPath); } catch { await fs.writeFile(snapPath, JSON.stringify(system), 'utf8'); } return { systemId, snapshotHash: hash, system }; } private async saveManifest(): Promise { await fs.mkdir(this.rootDir, { recursive: true }); await fs.writeFile(this.manifestPath, JSON.stringify(this.manifest, null, 2), 'utf8'); } } /** Test helper — content hash of arbitrary buffer. */ export function sha256Hex(data: string): string { return createHash('sha256').update(data).digest('hex'); }