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.
104 lines
2.9 KiB
TypeScript
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
|
|
}
|