From 967b3f451215fb017528299b70ff3b875a233b5c Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 17 Feb 2026 19:15:24 -0800 Subject: [PATCH] sandlot save Co-Authored-By: Claude Opus 4.6 --- src/cli.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/vm.ts | 10 ++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/cli.ts b/src/cli.ts index 7494a02..2389f1b 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -126,6 +126,58 @@ program .argument("", "branch name") .action(closeAction) +// ── sandlot save ─────────────────────────────────────────── + +program + .command("save") + .argument("", "branch name") + .description("Stage all changes and commit with an AI-generated message") + .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 worktreeAbs = join(root, session.worktree) + + const spin = spinner("Starting VM") + await vm.ensure() + + spin.text = "Staging changes" + await vm.exec(worktreeAbs, "git add .") + + const check = await vm.exec(worktreeAbs, "git diff --staged --quiet") + if (check.exitCode === 0) { + spin.fail("No changes to commit") + process.exit(1) + } + + spin.text = "Generating commit message" + const gen = await vm.exec( + worktreeAbs, + 'set -a; source ~/.env 2>/dev/null; set +a; git diff --staged | claude -p "write a short commit message summarizing these changes. output only the message, no quotes or extra text" > /tmp/sandlot_commit_msg', + ) + if (gen.exitCode !== 0) { + spin.fail("Failed to generate commit message") + if (gen.stderr) console.error(gen.stderr) + process.exit(1) + } + + spin.text = "Committing" + const commit = await vm.exec(worktreeAbs, "git commit -F /tmp/sandlot_commit_msg") + if (commit.exitCode !== 0) { + spin.fail("Commit failed") + if (commit.stderr) console.error(commit.stderr) + process.exit(1) + } + + const { stdout: msg } = await vm.exec(worktreeAbs, "cat /tmp/sandlot_commit_msg") + await vm.exec(worktreeAbs, "rm -f /tmp/sandlot_commit_msg") + spin.succeed(`Saved: ${msg}`) + }) + // ── sandlot vm ─────────────────────────────────────────────────────── const vmCmd = program.command("vm").description("Manage the sandlot VM") diff --git a/src/vm.ts b/src/vm.ts index 52308a2..672256e 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -89,6 +89,16 @@ export async function info(): Promise { await proc.exited } +/** Run a bash command in the VM at the given workdir, capturing output. */ +export async function exec(workdir: string, command: string): Promise<{ exitCode: number; stdout: string; stderr: string }> { + const result = await $`limactl shell --workdir=${workdir} ${VM_NAME} -- bash -c ${command}`.nothrow().quiet() + return { + exitCode: result.exitCode, + stdout: result.stdout.toString().trim(), + stderr: result.stderr.toString().trim(), + } +} + /** Stop the VM. */ export async function stop(): Promise { await $`limactl stop ${VM_NAME}`.nothrow().quiet()