Allow users to configure container memory limit (default 32G) via `sandlot config memory <value>` instead of hardcoding it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
244 lines
8.4 KiB
TypeScript
Executable File
244 lines
8.4 KiB
TypeScript
Executable File
#!/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"
|
|
import { action as initAction } from "./commands/init.ts"
|
|
import { action as cdAction } from "./commands/cd.ts"
|
|
import { action as configAction } from "./commands/config.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", () => {
|
|
const versionParts = pkg.version.split('.')
|
|
console.log(`v${versionParts.at(-1)}`)
|
|
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 <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)
|
|
|
|
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("Open an existing Claude session")
|
|
.action(openAction)
|
|
|
|
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")
|
|
.description("Remove a session (alias for close)")
|
|
.action((branch: string, opts: { force?: boolean }) => closeAction(branch, opts))
|
|
|
|
program
|
|
.command("checkout")
|
|
.alias("co")
|
|
.argument("<branch>", "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>", "branch name")
|
|
.description("Show uncommitted changes, or full branch diff vs main")
|
|
.action(diffAction)
|
|
|
|
program
|
|
.command("log")
|
|
.argument("<branch>", "branch name")
|
|
.description("Show commits on a branch that are not on main")
|
|
.action(logAction)
|
|
|
|
program
|
|
.command("show")
|
|
.argument("<branch>", "branch name")
|
|
.description("Show the prompt and full diff for a branch")
|
|
.action(showAction)
|
|
|
|
program
|
|
.command("web")
|
|
.argument("<branch>", "branch name")
|
|
.description("Open the branch diff in a web browser")
|
|
.action(webAction)
|
|
|
|
program
|
|
.command("save")
|
|
.argument("<branch>", "branch name")
|
|
.argument("[message]", "commit message (AI-generated if omitted)")
|
|
.description("Stage all changes and commit")
|
|
.action(saveAction)
|
|
|
|
program
|
|
.command("merge")
|
|
.argument("<branch>", "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>", "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>", "branch name")
|
|
.description("Rebase a branch onto the latest main")
|
|
.action(rebaseAction)
|
|
|
|
program
|
|
.command("review")
|
|
.argument("<branch>", "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>", "branch name")
|
|
.argument("<file>", "file path relative to worktree root")
|
|
.description("Open a file from a session in $EDITOR")
|
|
.action(editAction)
|
|
|
|
program
|
|
.command("dir")
|
|
.argument("<branch>", "branch name")
|
|
.description("Print the worktree path for a session")
|
|
.action(dirAction)
|
|
|
|
program
|
|
.command("cd")
|
|
.argument("<branch>", "branch name")
|
|
.description("Change to a branch's worktree directory")
|
|
.action(cdAction)
|
|
|
|
// ── Admin ───────────────────────────────────────────────────────────
|
|
|
|
program.commandsGroup("Admin Commands:")
|
|
|
|
program
|
|
.command("config")
|
|
.argument("[args...]", "key [value]")
|
|
.description("Get or set configuration (e.g. sandlot config memory 16G)")
|
|
.action(configAction)
|
|
|
|
program
|
|
.command("cleanup")
|
|
.description("Remove stale sessions whose worktrees no longer exist")
|
|
.action(cleanupAction)
|
|
|
|
registerVmCommands(program)
|
|
|
|
program
|
|
.command("upgrade")
|
|
.description("Upgrade sandlot to the latest version")
|
|
.action(async () => {
|
|
const result = await Bun.spawn(["bun", "install", "-g", "@because/sandlot@latest"], {
|
|
stdin: "inherit",
|
|
stdout: "inherit",
|
|
stderr: "inherit",
|
|
}).exited
|
|
process.exit(result)
|
|
})
|
|
|
|
program
|
|
.command("version")
|
|
.description("Print the version number")
|
|
.action(() => {
|
|
const versionParts = pkg.version.split('.')
|
|
console.log(`v${versionParts.at(-1)}`)
|
|
})
|
|
|
|
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))
|
|
|
|
program
|
|
.command("init")
|
|
.argument("<shell>", "shell type (fish, bash, zsh)")
|
|
.description("Print shell init script (eval in your shell config)")
|
|
.action((shell: string) => initAction(program, shell))
|
|
|
|
// ── 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)
|
|
})
|