import type { Database } from "bun:sqlite"; import { inflateSync } from "node:zlib"; import { mirrorState } from "./store.ts"; export type Save = { id: string; parent: string; message: string; created_at: number; machine: string; agent: string; session: string; is_checkpoint: boolean; }; export type FileEntry = { path: string; hash: string; executable: boolean; size: number }; export type RepoStats = { saves: number; lines: number; language: string; updated: number | null }; const SAVE_COLUMNS = `id, COALESCE(parent, '') AS parent, message, created_at, machine, COALESCE(agent, '') AS agent, COALESCE(session, '') AS session, is_checkpoint = 1 AS is_checkpoint`; const ID_PREFIX = /^[0-9a-f]{6,12}$/; // Checkpoints are working marks, not history (§21.4): every list and count below leaves them out. const NOT_CHECKPOINT = "is_checkpoint = 0"; export function headId(db: Database): string | null { return mirrorState(db, "head"); } /** A page of saves, newest first. Rows land oldest first, so rowid is the order of the line. */ export function history(db: Database, limit: number, offset: number): Save[] { return db .query( `SELECT ${SAVE_COLUMNS} FROM saves WHERE ${NOT_CHECKPOINT} ORDER BY rowid DESC LIMIT ? OFFSET ?`, ) .all(limit, offset) .map(asSave); } /** Every save, oldest first, for the timeline. */ export function timeline(db: Database): Save[] { return db .query(`SELECT ${SAVE_COLUMNS} FROM saves WHERE ${NOT_CHECKPOINT} ORDER BY rowid`) .all() .map(asSave); } /** The save a unique prefix of 6–12 hex characters names (§6.1), or null when none or several do. */ export function saveByPrefix(db: Database, prefix: string): Save | null { if (!ID_PREFIX.test(prefix)) return null; const rows = db .query(`SELECT ${SAVE_COLUMNS} FROM saves WHERE id LIKE ? || '%' LIMIT 2`) .all(prefix); return rows.length === 1 ? asSave(rows[0] as Save) : null; } export function filesAt(db: Database, saveId: string): FileEntry[] { return db .query( `SELECT f.path, f.hash, f.executable = 1 AS executable, o.size FROM save_files f JOIN objects o ON o.hash = f.hash WHERE f.save_id = ? ORDER BY f.path`, ) .all(saveId) .map((file) => ({ ...file, executable: Boolean(file.executable) })); } export function fileAt(db: Database, saveId: string, path: string): FileEntry | null { const file = db .query( `SELECT f.path, f.hash, f.executable = 1 AS executable, o.size FROM save_files f JOIN objects o ON o.hash = f.hash WHERE f.save_id = ? AND f.path = ?`, ) .get(saveId, path); return file ? { ...file, executable: Boolean(file.executable) } : null; } export function objectBytes(db: Database, hash: string): Uint8Array | null { const row = db .query<{ data: Uint8Array }, [string]>("SELECT data FROM objects WHERE hash = ?") .get(hash); return row ? new Uint8Array(inflateSync(row.data)) : null; } export function repoStats(db: Database): RepoStats { const row = db .query<{ saves: number; updated: number | null }, []>( `SELECT COUNT(*) AS saves, MAX(created_at) AS updated FROM saves WHERE ${NOT_CHECKPOINT}`, ) .get() ?? { saves: 0, updated: null }; return { saves: row.saves, updated: row.updated, lines: Number(mirrorState(db, "lines") ?? 0), language: mirrorState(db, "language") ?? "", }; } function asSave(row: Save): Save { return { ...row, is_checkpoint: Boolean(row.is_checkpoint) }; }