sandlot/src/state.ts
Chris Wanstrath d402a3f980 Derive repo name from repoRoot instead of storing it separately
Remove the redundant `repo` field from GlobalSession and compute it
via basename(repoRoot) at render time. Also fix prompt truncation
when terminal is extremely narrow, and simplify backfillPrompts to
avoid an intermediate array allocation.
2026-03-23 21:20:44 -07:00

104 lines
2.9 KiB
TypeScript

import { join, dirname } from "path"
import { readdir, rename } from "fs/promises"
import { homedir } from "os"
export interface Session {
branch: string
worktree: string
created_at: string
prompt?: string
in_review?: boolean
}
export interface State {
sessions: Record<string, Session>
}
function statePath(repoRoot: string): string {
return join(repoRoot, ".sandlot", "state.json")
}
export async function load(repoRoot: string): Promise<State> {
const path = statePath(repoRoot)
const file = Bun.file(path)
if (await file.exists()) {
return await file.json()
}
return { sessions: {} }
}
export async function save(repoRoot: string, state: State): Promise<void> {
const path = statePath(repoRoot)
const tmpPath = path + ".tmp"
const dir = join(repoRoot, ".sandlot")
await Bun.write(join(dir, ".gitkeep"), "") // ensure dir exists
await Bun.write(tmpPath, JSON.stringify(state, null, 2) + "\n")
await rename(tmpPath, path)
}
export async function getSession(repoRoot: string, branch: string): Promise<Session | undefined> {
const state = await load(repoRoot)
return state.sessions[branch]
}
export async function setSession(repoRoot: string, session: Session): Promise<void> {
const state = await load(repoRoot)
state.sessions[session.branch] = session
await save(repoRoot, state)
}
export async function removeSession(repoRoot: string, branch: string): Promise<void> {
const state = await load(repoRoot)
delete state.sessions[branch]
await save(repoRoot, state)
}
export interface GlobalSession extends Session {
repoRoot: string
}
/** Discover all sessions across all repos by scanning ~/.sandlot/ */
export async function loadAll(): Promise<GlobalSession[]> {
const sandlotDir = join(homedir(), ".sandlot")
const all: GlobalSession[] = []
let repoDirs
try {
repoDirs = await readdir(sandlotDir, { withFileTypes: true })
} catch {
return []
}
for (const entry of repoDirs) {
if (!entry.isDirectory() || entry.name.startsWith(".")) continue
const repoDir = join(sandlotDir, entry.name)
// Find the main repo root from a worktree's .git pointer
let repoRoot: string | null = null
const branchEntries = await readdir(repoDir, { withFileTypes: true }).catch(() => [])
for (const be of branchEntries) {
if (!be.isDirectory() || be.name.startsWith(".")) continue
const dotGit = await Bun.file(join(repoDir, be.name, ".git")).text().catch(() => "")
const m = dotGit.match(/^gitdir:\s*(.+)/m)
if (m) {
// gitdir: /path/to/repo/.git/worktrees/<name>
const mainGit = m[1].trim().replace(/\/worktrees\/[^/]+$/, "")
repoRoot = dirname(mainGit)
break
}
}
if (repoRoot) {
try {
const st = await load(repoRoot)
for (const session of Object.values(st.sessions)) {
all.push({ ...session, repoRoot })
}
} catch {}
}
}
return all
}