diff --git a/src/commands/list.ts b/src/commands/list.ts index c7897d0..cdab9f8 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -46,6 +46,7 @@ export async function action(opts: { json?: boolean }) { // Determine status for each session in parallel const statusEntries = await Promise.all( sessions.map(async (s): Promise<[string, string]> => { + if (!(await git.isValidWorktree(s.worktree))) return [s.branch, "stale"] if (await vm.isClaudeActive(s.worktree, s.branch)) return [s.branch, "active"] const dirty = await git.isDirty(s.worktree) if (dirty) return [s.branch, "dirty"] @@ -61,8 +62,8 @@ export async function action(opts: { json?: boolean }) { return } - const icons: Record = { idle: `${dim}◯${reset}`, active: `${cyan}◎${reset}`, dirty: `${yellow}◐${reset}`, saved: `${green}●${reset}` } - const branchColors: Record = { idle: dim, active: cyan, dirty: yellow, saved: green } + const icons: Record = { idle: `${dim}◯${reset}`, active: `${cyan}◎${reset}`, dirty: `${yellow}◐${reset}`, saved: `${green}●${reset}`, stale: `${red}✖${reset}` } + const branchColors: Record = { idle: dim, active: cyan, dirty: yellow, saved: green, stale: red } const branchWidth = Math.max(6, ...sessions.map((s) => s.branch.length)) const cols = process.stdout.columns || 80 const prefixWidth = branchWidth + 4 @@ -70,7 +71,7 @@ export async function action(opts: { json?: boolean }) { console.log(` ${dim}${"BRANCH".padEnd(branchWidth)} PROMPT${reset}`) for (const s of sessions) { - const prompt = (s.prompt ?? "").split("\n")[0] + const prompt = status === "stale" ? "broken worktree — close with -f to clean up" : (s.prompt ?? "").split("\n")[0] const status = statuses[s.branch] const icon = icons[status] const bc = branchColors[status] @@ -79,7 +80,9 @@ export async function action(opts: { json?: boolean }) { console.log(`${icon} ${bc}${s.branch.padEnd(branchWidth)}${reset} ${dim}${truncated}${reset}`) } - console.log(`\n${dim}◯ idle${reset} · ${cyan}◎ active${reset} · ${yellow}◐ unsaved${reset} · ${green}● saved${reset}`) + const hasStale = Object.values(statuses).includes("stale") + const legend = `${dim}◯ idle${reset} · ${cyan}◎ active${reset} · ${yellow}◐ unsaved${reset} · ${green}● saved${reset}` + console.log(`\n${legend}${hasStale ? ` · ${red}✖ stale${reset} (run ${dim}sandlot close -f ${reset} to clean up)` : ""}`) if ((await vm.status()) !== "running") { console.log(`\n${red}VM is not running.${reset}`) diff --git a/src/git.ts b/src/git.ts index 6afb717..3379a28 100644 --- a/src/git.ts +++ b/src/git.ts @@ -189,6 +189,12 @@ export async function rebaseAbort(cwd: string): Promise { } /** Check if a worktree has uncommitted changes. */ +/** Check if a worktree path is a valid git directory. */ +export async function isValidWorktree(worktreePath: string): Promise { + const result = await $`git -C ${worktreePath} rev-parse --git-dir`.nothrow().quiet() + return result.exitCode === 0 +} + export async function isDirty(worktreePath: string): Promise { const result = await $`git -C ${worktreePath} status --porcelain`.nothrow().quiet() if (result.exitCode !== 0) return false @@ -227,7 +233,8 @@ export async function fileDiff(ref1: string, ref2: string, file: string, cwd: st /** Check if a branch has commits beyond main. */ export async function hasNewCommits(worktreePath: string): Promise { - const main = await mainBranch(worktreePath) + let main: string + try { main = await mainBranch(worktreePath) } catch { return false } const result = await $`git -C ${worktreePath} rev-list ${main}..HEAD --count`.nothrow().quiet() if (result.exitCode !== 0) return false return parseInt(result.text().trim(), 10) > 0