sandlot/src/cli.ts
2026-02-17 08:52:07 -08:00

145 lines
4.3 KiB
TypeScript
Executable File

#!/usr/bin/env bun
import { Command } from "commander"
import { join } from "path"
import * as git from "./git.ts"
import * as vm from "./vm.ts"
import * as state from "./state.ts"
import { spinner } from "./spinner.ts"
const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json()
const program = new Command()
program.name("sandlot").description("Branch-based development with git worktrees and Lima VMs").version(pkg.version)
// ── sandlot new <branch> ──────────────────────────────────────────────
program
.command("new")
.argument("<branch>", "branch name")
.description("Create a new session and launch Claude")
.action(async (branch: string) => {
const root = await git.repoRoot()
const worktreeRel = `.sandlot/${branch}`
const worktreeAbs = join(root, worktreeRel)
const existing = await state.getSession(root, branch)
if (existing) {
console.error(`Session "${branch}" already exists. Use "sandlot open ${branch}" to re-enter it.`)
process.exit(1)
}
const spin = spinner("Creating worktree")
await git.createWorktree(branch, worktreeAbs, root)
spin.text = "Starting VM"
await vm.ensure()
spin.succeed("Session ready")
await state.setSession(root, {
branch,
worktree: worktreeRel,
created_at: new Date().toISOString(),
})
await vm.claude(worktreeAbs)
})
// ── sandlot list ──────────────────────────────────────────────────────
program
.command("list")
.description("Show all active sessions")
.action(async () => {
const root = await git.repoRoot()
const st = await state.load(root)
const sessions = Object.values(st.sessions)
if (sessions.length === 0) {
console.log("No active sessions.")
return
}
const branchWidth = Math.max(6, ...sessions.map((s) => s.branch.length))
console.log(`${"BRANCH".padEnd(branchWidth)} WORKTREE`)
for (const s of sessions) {
console.log(`${s.branch.padEnd(branchWidth)} ${s.worktree}/`)
}
})
// ── sandlot open <branch> ─────────────────────────────────────────────
program
.command("open")
.argument("<branch>", "branch name")
.description("Re-enter an existing session")
.action(async (branch: string) => {
const root = await git.repoRoot()
const session = await state.getSession(root, branch)
if (!session) {
console.error(`No session found for branch "${branch}".`)
process.exit(1)
}
const spin = spinner("Starting VM")
await vm.ensure()
spin.succeed("Session ready")
await vm.claude(join(root, session.worktree))
})
// ── sandlot rm <branch> ──────────────────────────────────────────────
program
.command("rm")
.argument("<branch>", "branch name")
.description("Remove a worktree and clean up the session")
.action(async (branch: string) => {
const root = await git.repoRoot()
const session = await state.getSession(root, branch)
const worktreeRel = session?.worktree ?? `.sandlot/${branch}`
const worktreeAbs = join(root, worktreeRel)
await git.removeWorktree(worktreeAbs, root)
console.log(`Removed worktree ${worktreeRel}/`)
await git.deleteLocalBranch(branch, root)
console.log(`Deleted local branch ${branch}`)
if (session) {
await state.removeSession(root, branch)
}
})
// ── sandlot vm ───────────────────────────────────────────────────────
const vmCmd = program.command("vm").description("Manage the sandlot VM")
vmCmd
.command("status")
.description("Show VM status")
.action(async () => {
const s = await vm.status()
console.log(s)
})
vmCmd
.command("stop")
.description("Stop the VM")
.action(async () => {
await vm.stop()
console.log("VM stopped")
})
vmCmd
.command("destroy")
.description("Stop and delete the VM")
.action(async () => {
await vm.destroy()
console.log("VM destroyed")
})
program.parse()