import { mkdir, readdir, rm, stat } from "node:fs/promises"; import { join, relative } from "node:path"; export const DEFAULT_LIVE_HOME = "/tmp/.dench-home"; export const DEFAULT_SNAPSHOT_DIR = "/workspace/.dench/home/snapshots"; export const DEFAULT_DEBOUNCE_MS = 2_000; export const DEFAULT_KEEP_SNAPSHOTS = 8; export const DEFAULT_STATUS_PATH = "/tmp/dench-home-sync.status.json"; export type HomeSyncArgs = { liveHome: string; snapshotDir: string; debounceMs: number; keepSnapshots: number; statusPath: string; once: boolean; }; export function parseHomeSyncArgs(argv: string[]): HomeSyncArgs { const out: Partial = {}; for (let i = 0; i < argv.length; i++) { const flag = argv[i]; const value = argv[i + 1]; switch (flag) { case "--live-home": out.liveHome = value; i++; break; case "--snapshot-dir": out.snapshotDir = value; i++; break; case "--debounce-ms": out.debounceMs = Number.parseInt(value, 10); i++; break; case "--keep-snapshots": out.keepSnapshots = Number.parseInt(value, 10); i++; break; case "--status-path": out.statusPath = value; i++; break; case "--once": out.once = true; break; default: // Ignore unknown flags so newer bootstraps can run on older CLIs. break; } } return { liveHome: out.liveHome ?? DEFAULT_LIVE_HOME, snapshotDir: out.snapshotDir ?? process.env.DENCH_HOME_SNAPSHOT_DIR?.trim() ?? DEFAULT_SNAPSHOT_DIR, debounceMs: Number.isFinite(out.debounceMs) ? Math.max(250, out.debounceMs ?? DEFAULT_DEBOUNCE_MS) : DEFAULT_DEBOUNCE_MS, keepSnapshots: Number.isFinite(out.keepSnapshots) ? Math.max(1, out.keepSnapshots ?? DEFAULT_KEEP_SNAPSHOTS) : DEFAULT_KEEP_SNAPSHOTS, statusPath: out.statusPath ?? DEFAULT_STATUS_PATH, once: out.once ?? false, }; } const EXCLUDED_HOME_PATTERNS = [ ".cache", ".cache/**", ".npm/_cacache", ".npm/_cacache/**", ".npm/_logs", ".npm/_logs/**", ".bun/install/cache", ".bun/install/cache/**", ".cargo/registry/cache", ".cargo/registry/cache/**", ".cargo/registry/index", ".cargo/registry/index/**", ".cargo/git/checkouts", ".cargo/git/checkouts/**", ".cargo/git/db", ".cargo/git/db/**", "go/pkg/mod/cache", "go/pkg/mod/cache/**", ".config/google-chrome", ".config/google-chrome/**", ".config/chromium", ".config/chromium/**", ]; export function homeSnapshotExcludes(): string[] { return [...EXCLUDED_HOME_PATTERNS]; } export function shouldIgnoreHomePath(path: string, liveHome: string): boolean { const rel = relative(liveHome, path).replaceAll("\\", "/"); if (!rel || rel === ".") return false; if (rel.startsWith("..")) return true; return EXCLUDED_HOME_PATTERNS.some((pattern) => { if (pattern.endsWith("/**")) { const prefix = pattern.slice(0, -3); return rel === prefix || rel.startsWith(`${prefix}/`); } return rel === pattern; }); } export function buildTarCreateArgs(args: { liveHome: string; archivePath: string; }): string[] { return [ "--create", "--file", args.archivePath, "--directory", args.liveHome, ...EXCLUDED_HOME_PATTERNS.map((pattern) => `--exclude=${pattern}`), ".", ]; } export function buildSnapshotName(now: Date, pid = process.pid): string { const stamp = now .toISOString() .replaceAll("-", "") .replaceAll(":", "") .replace(".", ""); return `home-${stamp}-${pid}.tar`; } export async function ensureHomeSyncDirs(args: { liveHome: string; snapshotDir: string; }): Promise { await mkdir(args.liveHome, { recursive: true }); await mkdir(args.snapshotDir, { recursive: true }); } export async function listHomeSnapshots( snapshotDir: string, ): Promise { let entries: string[]; try { entries = await readdir(snapshotDir); } catch { return []; } const snapshots = await Promise.all( entries .filter((entry) => /^home-\d{8}T\d{9}Z-\d+\.tar$/.test(entry)) .map(async (entry) => { const path = join(snapshotDir, entry); try { const info = await stat(path); return { path, mtimeMs: info.mtimeMs }; } catch { return null; } }), ); return snapshots .filter((snapshot): snapshot is { path: string; mtimeMs: number } => Boolean(snapshot), ) .sort((a, b) => b.mtimeMs - a.mtimeMs) .map((snapshot) => snapshot.path); } export async function pruneHomeSnapshots(args: { snapshotDir: string; keepSnapshots: number; }): Promise { const snapshots = await listHomeSnapshots(args.snapshotDir); const stale = snapshots.slice(args.keepSnapshots); await Promise.all(stale.map((path) => rm(path, { force: true }))); return stale; }