#!/usr/bin/env bun import { Command, Option } from "commander" import { yellow, reset } from "./fmt.ts" 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 { action as closeAction } from "./commands/close.ts" import { action as checkoutAction } from "./commands/checkout.ts" import { action as mergeAction } from "./commands/merge.ts" import { action as squashAction } from "./commands/squash.ts" import { action as rebaseAction } from "./commands/rebase.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 webAction } from "./commands/web.ts" import { action as logAction } from "./commands/log.ts" import { action as dirAction } from "./commands/dir.ts" import { action as editAction } from "./commands/edit.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("Sandboxed development with Claude.") .configureHelp({ styleTitle: (str) => `${yellow}${str}${reset}` }) .helpOption(false) .addOption(new Option("-h, --help").hideHelp()) .on("option:help", () => program.help()) .addOption(new Option("-V, --version").hideHelp()) .on("option:version", () => { console.log(pkg.version) process.exit(0) }) // ── Sessions ──────────────────────────────────────────────────────── program .command("list") .description("Show all active sessions") .option("--json", "Output as JSON") .action(listAction) 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) 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("Open an existing Claude session") .action(openAction) 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") .description("Remove a session (alias for close)") .action((branch: string, opts: { force?: boolean }) => closeAction(branch, opts)) program .command("checkout") .alias("co") .argument("", "branch name") .option("-f, --force", "checkout even if there are unsaved changes") .description("Close the session and check out the branch locally") .action((branch: string, opts: { force?: boolean }) => checkoutAction(branch, opts)) // ── Branch ────────────────────────────────────────────────────────── program.commandsGroup("Branch Commands:") program .command("diff") .argument("", "branch name") .description("Show uncommitted changes, or full branch diff vs main") .action(diffAction) program .command("log") .argument("", "branch name") .description("Show commits on a branch that are not on main") .action(logAction) program .command("show") .argument("", "branch name") .description("Show the prompt and full diff for a branch") .action(showAction) program .command("web") .argument("", "branch name") .description("Open the branch diff in a web browser") .action(webAction) program .command("save") .argument("", "branch name") .argument("[message]", "commit message (AI-generated if omitted)") .description("Stage all changes and commit") .action(saveAction) program .command("merge") .argument("", "branch name") .option("-f, --force", "allow merging into a non-main branch") .description("Merge a branch into main and close the session") .action((branch: string, opts: { force?: boolean }) => mergeAction(branch, opts)) program .command("squash") .argument("", "branch name") .option("-f, --force", "allow merging into a non-main branch") .description("Squash-merge a branch into main and close the session") .action((branch: string, opts: { force?: boolean }) => squashAction(branch, opts)) program .command("rebase") .argument("", "branch name") .description("Rebase a branch onto the latest main") .action(rebaseAction) program .command("review") .argument("", "branch name") .argument("[prompt]", "additional instructions to append to the review prompt") .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) program .command("shell") .argument("[branch]", "branch name (omit for a plain VM shell)") .description("Open a shell in the VM") .action(shellAction) program .command("edit") .argument("", "branch name") .argument("", "file path relative to worktree root") .description("Open a file from a session in $EDITOR") .action(editAction) program .command("dir") .argument("", "branch name") .description("Print the worktree path for a session") .action(dirAction) // ── Admin ─────────────────────────────────────────────────────────── program.commandsGroup("Admin Commands:") program .command("cleanup") .description("Remove stale sessions whose worktrees no longer exist") .action(cleanupAction) registerVmCommands(program) program .command("version") .description("Print the version number") .action(() => { console.log(pkg.version) }) program .command("completions") .option("--install", "Output a shell script that installs the completions file") .description("Output fish shell completions") .action((opts: { install?: boolean }) => completionsAction(program, opts)) // ── Default: `sandlot` → `sandlot list` ───────────────────────────── if (process.argv.length === 2) { process.argv.push("list") } program.parseAsync().catch((err) => { console.error(`✖ ${err.message ?? err}`) process.exit(1) })