#!/usr/bin/env bun import { Command } from "commander" import * as git from "./git.ts" import * as state from "./state.ts" import { action as newAction } from "./commands/new.ts" import { action as listAction } from "./commands/list.ts" import { action as openAction } from "./commands/open.ts" import { action as reviewAction } from "./commands/review.ts" import { action as shellAction } from "./commands/shell.ts" import { closeAction } from "./commands/close.ts" import { action as mergeAction } from "./commands/merge.ts" import { action as saveAction } from "./commands/save.ts" import { action as diffAction } from "./commands/diff.ts" import { action as showAction } from "./commands/show.ts" import { action as logAction } from "./commands/log.ts" import { action as dirAction } from "./commands/dir.ts" import { action as cleanupAction } from "./commands/cleanup.ts" import { register as registerVmCommands } from "./commands/vm.ts" import { action as completionsAction } from "./commands/completions.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 Apple Container").version(pkg.version) // ── sandlot new ────────────────────────────────────────────────────── program .command("new") .argument("[branch]", "branch name or prompt (if it contains spaces)") .argument("[prompt]", "initial prompt for Claude") .option("-p, --print ", "run Claude in non-interactive mode with -p") .option("-n, --no-save", "skip auto-save after Claude exits") .description("Create a new session and launch Claude") .action(newAction) // ── sandlot list ───────────────────────────────────────────────────── program .command("list") .description("Show all active sessions (◌ idle · ◯ working · ◎ unsaved · ● saved)") .option("--json", "Output as JSON") .action(listAction) // ── sandlot open ───────────────────────────────────────────────────── program .command("open") .argument("", "branch name") .argument("[prompt]", "initial prompt for Claude") .option("-p, --print ", "run Claude in non-interactive mode with -p") .option("-n, --no-save", "skip auto-save after Claude exits") .description("Re-enter an existing session") .action(openAction) // ── sandlot review ─────────────────────────────────────────────────── program .command("review") .argument("", "branch name") .option("-p, --print", "print the review to stdout instead of launching interactive mode") .description("Launch an interactive grumpy code review for a branch") .action(reviewAction) // ── sandlot shell ──────────────────────────────────────────────────── program .command("shell") .argument("[branch]", "branch name (omit for a plain VM shell)") .description("Open a shell in the VM (at the session's worktree if branch given)") .action(shellAction) // ── sandlot close ──────────────────────────────────────────────────── program .command("close") .argument("", "branch name") .option("-f, --force", "close even if there are unsaved changes") .description("Remove a worktree and clean up the session") .action((branch: string, opts: { force?: boolean }) => closeAction(branch, opts)) program .command("rm", { hidden: true }) .argument("", "branch name") .option("-f, --force", "close even if there are unsaved changes") .action((branch: string, opts: { force?: boolean }) => closeAction(branch, opts)) // ── sandlot merge ──────────────────────────────────────────────────── program .command("merge") .argument("", "branch name") .description("Merge a branch into main and close the session") .action(mergeAction) // ── sandlot save ───────────────────────────────────────────────────── program .command("save") .argument("", "branch name") .argument("[message]", "commit message (AI-generated if omitted)") .description("Stage all changes and commit") .action(saveAction) // ── sandlot diff ───────────────────────────────────────────────────── program .command("diff") .argument("", "branch name") .description("Show uncommitted changes, or full branch diff vs main") .action(diffAction) // ── sandlot show ───────────────────────────────────────────────────── program .command("show") .argument("", "branch name") .description("Show the prompt and full diff for a branch (for code review)") .action(showAction) // ── sandlot log ────────────────────────────────────────────────────── program .command("log") .argument("", "branch name") .description("Show commits on a branch that are not on main") .action(logAction) // ── sandlot dir ────────────────────────────────────────────────────── program .command("dir") .argument("", "branch name") .description("Print the worktree path for a session") .action(dirAction) // ── sandlot cleanup ────────────────────────────────────────────────── program .command("cleanup") .description("Remove stale sessions whose worktrees no longer exist") .action(cleanupAction) // ── sandlot vm ─────────────────────────────────────────────────────── registerVmCommands(program) // ── sandlot completions ────────────────────────────────────────────── program .command("completions") .description("Output fish shell completions") .action(() => completionsAction(program)) // ── Default: show list if sessions exist, otherwise help ───────────── const args = process.argv.slice(2) if (args.length === 0) { try { const root = await git.repoRoot() const st = await state.load(root) if (Object.keys(st.sessions).length > 0) { process.argv.push("list") } } catch {} } program.parseAsync().catch((err) => { console.error(`✖ ${err.message ?? err}`) process.exit(1) })