/** * ───────────────────────────────────────────── * SPB Git — Personal Git Platform * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : src/lib/store.mjs * Purpose : meta.json repo index + activity feed (filesystem is the database) * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ import { readFileSync, appendFileSync, existsSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { atomicWriteJSON } from './util.mjs'; const META_FILE = 'meta.json'; const ACTIVITY_FILE = 'activity.jsonl'; const ACTIVITY_MAX_READ = 500; /** * Repo metadata store backed by `data/meta.json`. * Shape: { version: 1, repos: { [name]: { description, topics, homepage, pinned, created, defaultBranch } } } */ export class MetaStore { /** @param {string} dataDir */ constructor(dataDir) { this.dataDir = dataDir; this.path = join(dataDir, META_FILE); } /** @returns {{version: number, repos: Record}} */ load() { if (!existsSync(this.path)) return { version: 1, repos: {} }; try { const parsed = JSON.parse(readFileSync(this.path, 'utf8')); if (parsed && typeof parsed.repos === 'object') return parsed; } catch { /* corrupted meta falls back to empty — bare repos remain the truth */ } return { version: 1, repos: {} }; } /** @param {object} meta */ save(meta) { atomicWriteJSON(this.path, meta); } /** * @param {string} name * @returns {object|null} */ get(name) { return this.load().repos[name] ?? null; } /** * Create or merge a repo entry. * @param {string} name * @param {object} fields * @returns {object} the updated entry */ upsert(name, fields = {}) { const meta = this.load(); const existing = meta.repos[name] ?? { description: '', topics: [], homepage: '', pinned: false, created: new Date().toISOString(), defaultBranch: 'main', }; meta.repos[name] = { ...existing, ...fields }; this.save(meta); return meta.repos[name]; } /** @param {string} name */ remove(name) { const meta = this.load(); delete meta.repos[name]; this.save(meta); } } /** * Append-only push activity feed backed by `data/activity.jsonl`. */ export class ActivityFeed { /** @param {string} dataDir */ constructor(dataDir) { this.path = join(dataDir, ACTIVITY_FILE); this.dataDir = dataDir; } /** * @param {{repo: string, ref: string, commits: number, sha: string}} event */ append(event) { mkdirSync(this.dataDir, { recursive: true }); const record = { ...event, at: new Date().toISOString() }; appendFileSync(this.path, JSON.stringify(record) + '\n'); } /** * @param {number} [limit] * @returns {object[]} newest-first events */ recent(limit = 15) { if (!existsSync(this.path)) return []; const lines = readFileSync(this.path, 'utf8').trim().split('\n'); const slice = lines.slice(-ACTIVITY_MAX_READ); const events = []; for (const line of slice) { try { events.push(JSON.parse(line)); } catch { /* skip torn line */ } } return events.reverse().slice(0, limit); } }