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 <noreply@anthropic.com>
This commit is contained in:
parent
e5f1de4717
commit
3ba27c80b4
|
|
@ -63,20 +63,9 @@ async function resolveStatus(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveAllStatuses<T extends { branch: string; worktree: string; in_review?: boolean; repoRoot: string }>(
|
|
||||||
sessions: T[],
|
|
||||||
vmRunning: boolean,
|
|
||||||
keyFn: (s: T) => string,
|
|
||||||
): Promise<Record<string, string>> {
|
|
||||||
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. */
|
/** Clear in_review flags for sessions where Claude is no longer active. */
|
||||||
async function clearStaleReviews<T extends { branch: string; in_review?: boolean; repoRoot: string }>(
|
async function clearStaleReviews(
|
||||||
sessions: T[],
|
sessions: state.GlobalSession[],
|
||||||
results: string[],
|
results: string[],
|
||||||
) {
|
) {
|
||||||
const staleByRepo = new Map<string, string[]>()
|
const staleByRepo = new Map<string, string[]>()
|
||||||
|
|
@ -101,14 +90,13 @@ async function backfillPrompts(sessions: { worktree: string; prompt?: string }[]
|
||||||
if (!vmRunning) return
|
if (!vmRunning) return
|
||||||
const needsPrompt = sessions.filter(s => !s.prompt)
|
const needsPrompt = sessions.filter(s => !s.prompt)
|
||||||
if (needsPrompt.length === 0) return
|
if (needsPrompt.length === 0) return
|
||||||
try {
|
const result = await vm.exec(homedir() + "/.sandlot", "cat /home/ubuntu/.claude/history.jsonl 2>/dev/null").catch(() => null)
|
||||||
const result = await vm.exec(homedir() + "/.sandlot", "cat /home/ubuntu/.claude/history.jsonl 2>/dev/null")
|
if (!result || result.exitCode !== 0 || !result.stdout) return
|
||||||
if (result.exitCode === 0 && result.stdout) {
|
|
||||||
const entries = result.stdout.split("\n").filter(Boolean).map(line => {
|
const entries = result.stdout.split("\n").filter(Boolean).map(line => {
|
||||||
try { return JSON.parse(line) } catch { return null }
|
try { return JSON.parse(line) } catch { return null }
|
||||||
}).filter(Boolean)
|
}).filter(Boolean)
|
||||||
|
|
||||||
// Later entries overwrite earlier ones so the most recent prompt wins
|
|
||||||
const byProject = new Map<string, string>()
|
const byProject = new Map<string, string>()
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
if (e.project && e.display) byProject.set(e.project, e.display)
|
if (e.project && e.display) byProject.set(e.project, e.display)
|
||||||
|
|
@ -119,8 +107,6 @@ async function backfillPrompts(sessions: { worktree: string; prompt?: string }[]
|
||||||
if (display) s.prompt = display
|
if (display) s.prompt = display
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Commands ─────────────────────────────────────────────────────────
|
// ── 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
|
// Key by repoRoot/branch to avoid collisions across repos with the same basename
|
||||||
const keyFn = (s: state.GlobalSession) => `${s.repoRoot}/${s.branch}`
|
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) {
|
if (opts.json) {
|
||||||
const withStatus = sessions.map(s => ({ ...s, status: statuses[keyFn(s)] }))
|
const withStatus = sessions.map(s => ({ ...s, status: statuses[keyFn(s)] }))
|
||||||
|
|
@ -189,6 +177,7 @@ async function actionAdd(dir: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function actionRemove(dir: string) {
|
async function actionRemove(dir: string) {
|
||||||
|
const resolved = state.normalizePath(dir)
|
||||||
let removed = false
|
let removed = false
|
||||||
try {
|
try {
|
||||||
removed = await state.unregisterProject(dir)
|
removed = await state.unregisterProject(dir)
|
||||||
|
|
@ -196,8 +185,8 @@ async function actionRemove(dir: string) {
|
||||||
die("Could not acquire registry lock")
|
die("Could not acquire registry lock")
|
||||||
}
|
}
|
||||||
if (removed) {
|
if (removed) {
|
||||||
console.log(`${red}-${reset} ${dir}`)
|
console.log(`${red}-${reset} ${resolved}`)
|
||||||
} else {
|
} else {
|
||||||
die(`Project not found in registry: ${dir}`)
|
die(`Project not found in registry: ${resolved}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
29
src/state.ts
29
src/state.ts
|
|
@ -66,11 +66,9 @@ const GLOBAL_STATE_PATH = join(GLOBAL_DIR, "registry.json")
|
||||||
async function withGlobalLock<T>(fn: () => Promise<T>): Promise<T> {
|
async function withGlobalLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
const lockPath = join(GLOBAL_DIR, "registry.lock")
|
const lockPath = join(GLOBAL_DIR, "registry.lock")
|
||||||
await mkdir(GLOBAL_DIR, { recursive: true })
|
await mkdir(GLOBAL_DIR, { recursive: true })
|
||||||
let acquired = false
|
|
||||||
for (let i = 0; i < 20; i++) {
|
for (let i = 0; i < 20; i++) {
|
||||||
try {
|
try {
|
||||||
await mkdir(lockPath)
|
await mkdir(lockPath)
|
||||||
acquired = true
|
|
||||||
break
|
break
|
||||||
} catch {
|
} catch {
|
||||||
// If the lock is older than 5 minutes, assume it's stale (crashed process)
|
// If the lock is older than 5 minutes, assume it's stale (crashed process)
|
||||||
|
|
@ -86,7 +84,6 @@ async function withGlobalLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
await Bun.sleep(50)
|
await Bun.sleep(50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!acquired) throw new Error("Could not acquire registry lock")
|
|
||||||
try {
|
try {
|
||||||
return await fn()
|
return await fn()
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -113,7 +110,7 @@ async function saveGlobal(gs: GlobalState): Promise<void> {
|
||||||
await rename(tmpPath, GLOBAL_STATE_PATH)
|
await rename(tmpPath, GLOBAL_STATE_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePath(dir: string): string {
|
export function normalizePath(dir: string): string {
|
||||||
return resolve(dir.replace(/^~(?=\/|$)/, homedir()))
|
return resolve(dir.replace(/^~(?=\/|$)/, homedir()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,32 +195,26 @@ export async function loadAll(): Promise<GlobalSession[]> {
|
||||||
const all: GlobalSession[] = []
|
const all: GlobalSession[] = []
|
||||||
const stale: string[] = []
|
const stale: string[] = []
|
||||||
|
|
||||||
const results = await Promise.all(gs.projects.map(async (project) => {
|
await Promise.all(gs.projects.map(async (project) => {
|
||||||
let st: State
|
let st: State
|
||||||
try {
|
try {
|
||||||
st = await load(project)
|
st = await load(project)
|
||||||
} catch {
|
} 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) {
|
if (Object.keys(st.sessions).length === 0) {
|
||||||
const file = Bun.file(join(project, ".sandlot", "state.json"))
|
if (!(await Bun.file(join(project, ".sandlot", "state.json")).exists())) {
|
||||||
if (!(await file.exists())) {
|
stale.push(project)
|
||||||
return { project, stale: true } as const
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const repo = basename(project)
|
const repo = basename(project)
|
||||||
const sessions = Object.values(st.sessions).map(s => ({ ...s, repo, repoRoot: project }))
|
for (const s of Object.values(st.sessions)) {
|
||||||
return { project, stale: false, sessions } as const
|
all.push({ ...s, repo, repoRoot: project })
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
for (const r of results) {
|
|
||||||
if (r.stale) {
|
|
||||||
stale.push(r.project)
|
|
||||||
} else if ('sessions' in r) {
|
|
||||||
all.push(...r.sessions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prune projects whose state files no longer exist
|
// Prune projects whose state files no longer exist
|
||||||
if (stale.length > 0) {
|
if (stale.length > 0) {
|
||||||
const staleSet = new Set(stale)
|
const staleSet = new Set(stale)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user