174 lines
7.4 KiB
TypeScript
Executable File
174 lines
7.4 KiB
TypeScript
Executable File
#!/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 <prompt>", "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>", "branch name")
|
|
.argument("[prompt]", "initial prompt for Claude")
|
|
.option("-p, --print <prompt>", "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>", "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>", "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>", "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>", "branch name")
|
|
.description("Merge a branch into main and close the session")
|
|
.action(mergeAction)
|
|
|
|
// ── sandlot save ─────────────────────────────────────────────────────
|
|
|
|
program
|
|
.command("save")
|
|
.argument("<branch>", "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>", "branch name")
|
|
.description("Show uncommitted changes, or full branch diff vs main")
|
|
.action(diffAction)
|
|
|
|
// ── sandlot show ─────────────────────────────────────────────────────
|
|
|
|
program
|
|
.command("show")
|
|
.argument("<branch>", "branch name")
|
|
.description("Show the prompt and full diff for a branch (for code review)")
|
|
.action(showAction)
|
|
|
|
// ── sandlot log ──────────────────────────────────────────────────────
|
|
|
|
program
|
|
.command("log")
|
|
.argument("<branch>", "branch name")
|
|
.description("Show commits on a branch that are not on main")
|
|
.action(logAction)
|
|
|
|
// ── sandlot dir ──────────────────────────────────────────────────────
|
|
|
|
program
|
|
.command("dir")
|
|
.argument("<branch>", "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)
|
|
})
|