From 3ba27c80b4cec2c2f29e42562f12fc9930210197 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 20 Mar 2026 10:33:42 -0700 Subject: [PATCH] Simplify list command and inline resolveAllStatuses Remove generic helper that obscured a three-line call site, flatten try/catch nesting in backfillPrompts, and push results directly in loadAll instead of collecting intermediate objects. Also export normalizePath for use in actionRemove and drop redundant `acquired` flag from the lock loop. Co-Authored-By: Claude Opus 4.6 --- src/commands/list.ts | 55 ++++++++++++++++++-------------------------- src/state.ts | 29 ++++++++--------------- 2 files changed, 32 insertions(+), 52 deletions(-) diff --git a/src/commands/list.ts b/src/commands/list.ts index e8f8206..85fc1d1 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -63,20 +63,9 @@ async function resolveStatus( } } -async function resolveAllStatuses( - sessions: T[], - vmRunning: boolean, - keyFn: (s: T) => string, -): Promise> { - const results = await Promise.all(sessions.map(s => resolveStatus(s, vmRunning))) - const statuses = Object.fromEntries(sessions.map((s, i) => [keyFn(s), results[i]])) - await clearStaleReviews(sessions, results) - return statuses -} - /** Clear in_review flags for sessions where Claude is no longer active. */ -async function clearStaleReviews( - sessions: T[], +async function clearStaleReviews( + sessions: state.GlobalSession[], results: string[], ) { const staleByRepo = new Map() @@ -101,25 +90,22 @@ async function backfillPrompts(sessions: { worktree: string; prompt?: string }[] if (!vmRunning) return const needsPrompt = sessions.filter(s => !s.prompt) if (needsPrompt.length === 0) return - try { - const result = await vm.exec(homedir() + "/.sandlot", "cat /home/ubuntu/.claude/history.jsonl 2>/dev/null") - if (result.exitCode === 0 && result.stdout) { - const entries = result.stdout.split("\n").filter(Boolean).map(line => { - try { return JSON.parse(line) } catch { return null } - }).filter(Boolean) + const result = await vm.exec(homedir() + "/.sandlot", "cat /home/ubuntu/.claude/history.jsonl 2>/dev/null").catch(() => null) + if (!result || result.exitCode !== 0 || !result.stdout) return - // Later entries overwrite earlier ones so the most recent prompt wins - const byProject = new Map() - for (const e of entries) { - if (e.project && e.display) byProject.set(e.project, e.display) - } + const entries = result.stdout.split("\n").filter(Boolean).map(line => { + try { return JSON.parse(line) } catch { return null } + }).filter(Boolean) - for (const s of needsPrompt) { - const display = byProject.get(vm.containerPath(s.worktree)) - if (display) s.prompt = display - } - } - } catch {} + const byProject = new Map() + for (const e of entries) { + if (e.project && e.display) byProject.set(e.project, e.display) + } + + for (const s of needsPrompt) { + const display = byProject.get(vm.containerPath(s.worktree)) + if (display) s.prompt = display + } } // ── Commands ───────────────────────────────────────────────────────── @@ -149,7 +135,9 @@ export async function action(opts: { json?: boolean; all?: boolean; add?: string // Key by repoRoot/branch to avoid collisions across repos with the same basename const keyFn = (s: state.GlobalSession) => `${s.repoRoot}/${s.branch}` - const statuses = await resolveAllStatuses(sessions, vmRunning, keyFn) + const results = await Promise.all(sessions.map(s => resolveStatus(s, vmRunning))) + const statuses = Object.fromEntries(sessions.map((s, i) => [keyFn(s), results[i]])) + await clearStaleReviews(sessions, results) if (opts.json) { const withStatus = sessions.map(s => ({ ...s, status: statuses[keyFn(s)] })) @@ -189,6 +177,7 @@ async function actionAdd(dir: string) { } async function actionRemove(dir: string) { + const resolved = state.normalizePath(dir) let removed = false try { removed = await state.unregisterProject(dir) @@ -196,8 +185,8 @@ async function actionRemove(dir: string) { die("Could not acquire registry lock") } if (removed) { - console.log(`${red}-${reset} ${dir}`) + console.log(`${red}-${reset} ${resolved}`) } else { - die(`Project not found in registry: ${dir}`) + die(`Project not found in registry: ${resolved}`) } } diff --git a/src/state.ts b/src/state.ts index 36b1c22..9969ee5 100644 --- a/src/state.ts +++ b/src/state.ts @@ -66,11 +66,9 @@ const GLOBAL_STATE_PATH = join(GLOBAL_DIR, "registry.json") async function withGlobalLock(fn: () => Promise): Promise { const lockPath = join(GLOBAL_DIR, "registry.lock") await mkdir(GLOBAL_DIR, { recursive: true }) - let acquired = false for (let i = 0; i < 20; i++) { try { await mkdir(lockPath) - acquired = true break } catch { // If the lock is older than 5 minutes, assume it's stale (crashed process) @@ -86,7 +84,6 @@ async function withGlobalLock(fn: () => Promise): Promise { await Bun.sleep(50) } } - if (!acquired) throw new Error("Could not acquire registry lock") try { return await fn() } finally { @@ -113,7 +110,7 @@ async function saveGlobal(gs: GlobalState): Promise { await rename(tmpPath, GLOBAL_STATE_PATH) } -function normalizePath(dir: string): string { +export function normalizePath(dir: string): string { return resolve(dir.replace(/^~(?=\/|$)/, homedir())) } @@ -198,31 +195,25 @@ export async function loadAll(): Promise { const all: GlobalSession[] = [] const stale: string[] = [] - const results = await Promise.all(gs.projects.map(async (project) => { + await Promise.all(gs.projects.map(async (project) => { let st: State try { st = await load(project) } catch { - return { project, stale: true } as const + // Corrupt state.json — skip but don't prune (self-heals on next session activity) + return } if (Object.keys(st.sessions).length === 0) { - const file = Bun.file(join(project, ".sandlot", "state.json")) - if (!(await file.exists())) { - return { project, stale: true } as const + if (!(await Bun.file(join(project, ".sandlot", "state.json")).exists())) { + stale.push(project) + return } } const repo = basename(project) - const sessions = Object.values(st.sessions).map(s => ({ ...s, repo, repoRoot: project })) - return { project, stale: false, sessions } as const - })) - - for (const r of results) { - if (r.stale) { - stale.push(r.project) - } else if ('sessions' in r) { - all.push(...r.sessions) + for (const s of Object.values(st.sessions)) { + all.push({ ...s, repo, repoRoot: project }) } - } + })) // Prune projects whose state files no longer exist if (stale.length > 0) {