1import type { Database } from "bun:sqlite";2import { inflateSync } from "node:zlib";3import { mirrorState } from "./store.ts";45export type Save = {6 id: string;7 parent: string;8 message: string;9 created_at: number;10 machine: string;11 agent: string;12 session: string;13 is_checkpoint: boolean;14};1516export type FileEntry = { path: string; hash: string; executable: boolean; size: number };1718export type RepoStats = { saves: number; lines: number; language: string; updated: number | null };1920const SAVE_COLUMNS = `id, COALESCE(parent, '') AS parent, message, created_at, machine,21 COALESCE(agent, '') AS agent, COALESCE(session, '') AS session, is_checkpoint = 1 AS is_checkpoint`;22const ID_PREFIX = /^[0-9a-f]{6,12}$/;232425const NOT_CHECKPOINT = "is_checkpoint = 0";2627export function headId(db: Database): string | null {28 return mirrorState(db, "head");29}303132export function history(db: Database, limit: number, offset: number): Save[] {33 return db34 .query<Save, [number, number]>(35 `SELECT ${SAVE_COLUMNS} FROM saves WHERE ${NOT_CHECKPOINT} ORDER BY rowid DESC LIMIT ? OFFSET ?`,36 )37 .all(limit, offset)38 .map(asSave);39}404142export function timeline(db: Database): Save[] {43 return db44 .query<Save, []>(`SELECT ${SAVE_COLUMNS} FROM saves WHERE ${NOT_CHECKPOINT} ORDER BY rowid`)45 .all()46 .map(asSave);47}484950export function saveByPrefix(db: Database, prefix: string): Save | null {51 if (!ID_PREFIX.test(prefix)) return null;52 const rows = db53 .query<Save, [string]>(`SELECT ${SAVE_COLUMNS} FROM saves WHERE id LIKE ? || '%' LIMIT 2`)54 .all(prefix);55 return rows.length === 1 ? asSave(rows[0] as Save) : null;56}5758export function filesAt(db: Database, saveId: string): FileEntry[] {59 return db60 .query<FileEntry, [string]>(61 `SELECT f.path, f.hash, f.executable = 1 AS executable, o.size FROM save_files f62 JOIN objects o ON o.hash = f.hash WHERE f.save_id = ? ORDER BY f.path`,63 )64 .all(saveId)65 .map((file) => ({ ...file, executable: Boolean(file.executable) }));66}6768export function fileAt(db: Database, saveId: string, path: string): FileEntry | null {69 const file = db70 .query<FileEntry, [string, string]>(71 `SELECT f.path, f.hash, f.executable = 1 AS executable, o.size FROM save_files f72 JOIN objects o ON o.hash = f.hash WHERE f.save_id = ? AND f.path = ?`,73 )74 .get(saveId, path);75 return file ? { ...file, executable: Boolean(file.executable) } : null;76}7778export function objectBytes(db: Database, hash: string): Uint8Array<ArrayBuffer> | null {79 const row = db80 .query<{ data: Uint8Array }, [string]>("SELECT data FROM objects WHERE hash = ?")81 .get(hash);82 return row ? new Uint8Array(inflateSync(row.data)) : null;83}8485export function repoStats(db: Database): RepoStats {86 const row = db87 .query<{ saves: number; updated: number | null }, []>(88 `SELECT COUNT(*) AS saves, MAX(created_at) AS updated FROM saves WHERE ${NOT_CHECKPOINT}`,89 )90 .get() ?? { saves: 0, updated: null };91 return {92 saves: row.saves,93 updated: row.updated,94 lines: Number(mirrorState(db, "lines") ?? 0),95 language: mirrorState(db, "language") ?? "",96 };97}9899function asSave(row: Save): Save {100 return { ...row, is_checkpoint: Boolean(row.is_checkpoint) };101}