#!/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 ────────────────────────────────────────────── program .command("new") .argument("", "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 ───────────────────────────────────────────── program .command("open") .argument("", "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 ────────────────────────────────────────────── program .command("rm") .argument("", "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()