/** * Text rendering for `gjc gc` reports. JSON output is produced directly in * `gc-runtime.ts`; this module owns the human-readable grouped report. */ import type { GcRecord, GcReport, GcStore } from "./gc-runtime"; import { GC_STORES } from "./gc-runtime"; const STORE_HEADINGS: Record = { harness_leases: "Harness owner leases", file_locks: "Config file-locks", tmux_sessions: "Tmux sessions", registry_entries: "Harness-root registry entries", local_roots: "Session local roots", }; function actionLabel(record: GcRecord): string { switch (record.action) { case "would_remove": return "would remove"; case "removed": return "removed"; case "remove_failed": return `remove failed${record.error ? `: ${record.error}` : ""}`; case "skipped": return `skipped: ${record.reason}`; default: return "keep"; } } function renderRecord(record: GcRecord): string { const target = record.path ?? record.id; const pid = record.pid !== undefined ? ` pid=${record.pid}` : ""; const pidStatus = record.pid_status ? ` (${record.pid_status})` : ""; const note = record.detail ? ` — ${record.detail}` : ""; return ` [${actionLabel(record)}] ${target}${pid}${pidStatus} :: ${record.status} — ${record.reason}${note}`; } export function buildGcReportText(report: GcReport): string { const lines: string[] = []; if (report.operation === "repair_session_index") { lines.push("gjc gc — session-index repair (other stores are report-only)"); } else { lines.push(report.dry_run ? "gjc gc — dry run (no changes made; pass --prune to remove)" : "gjc gc — prune"); } lines.push(""); for (const store of GC_STORES) { const records = report.stores[store]; lines.push(`${STORE_HEADINGS[store]} (${records.length})`); if (records.length === 0) { lines.push(" (none)"); } else { for (const record of records) lines.push(renderRecord(record)); } lines.push(""); } if (report.session_index) { const index = report.session_index; lines.push(`Session index: ${index.status}; valid prefix sequence=${index.valid_prefix_seq}`); if (index.quarantine_path) lines.push(` Quarantined suffix: ${index.quarantine_path}`); if (index.reason) lines.push(` ${index.reason}`); if (index.status === "corrupt") lines.push(" Run `gjc gc --repair-session-index` to quarantine the corrupt suffix."); if (index.status === "unsupported") lines.push(" Upgrade GJC before attempting a repair; no index data was changed."); if (index.status === "repaired") lines.push(" Restart or re-register hosts whose only registration was in the quarantined suffix."); lines.push(""); } if (report.session_scope) { const scope = report.session_scope; const mib = (bytes: number) => (bytes / (1024 * 1024)).toFixed(1); const headline = scope.status === "over_limit" ? "Session scope is OVER the managed budget — new sessions in this directory will fail to start" : "Session scope is approaching the managed budget"; lines.push(headline); lines.push( ` ${mib(scope.total_bytes)} MiB of ${mib(scope.limit_bytes)} MiB across ${scope.entries} entries` + `${scope.truncated ? " (walk truncated; totals are a floor)" : ""}`, ); lines.push(` ${scope.path}`); lines.push(" gc does not reclaim session records; move stale session directories out of the scope by hand."); lines.push(""); } if (report.warnings.length > 0) { lines.push(`Warnings (${report.warnings.length})`); for (const warning of report.warnings) lines.push(` [${warning.store}/${warning.scope}] ${warning.message}`); lines.push(""); } if (report.errors.length > 0) { lines.push(`Errors (${report.errors.length})`); for (const err of report.errors) lines.push(` [${err.store}/${err.scope}] ${err.message}`); lines.push(""); } const c = report.counts; lines.push( `Summary: discovered=${c.discovered} stale=${c.stale} alive=${c.alive} eperm=${c.eperm} unknown=${c.unknown} ` + `terminal_lifecycle=${c.terminal_lifecycle} unclassified=${c.unclassified} ` + `${report.dry_run ? `would_remove=${c.would_remove}` : `removed=${c.removed} failed=${c.failed}`} ` + `errors=${c.errors} warnings=${report.warnings.length}`, ); lines.push(""); return `${lines.join("\n")}`; }