145 lines
4.3 KiB
TypeScript
Executable File
145 lines
4.3 KiB
TypeScript
Executable File
#!/usr/bin/env bun
|
|
|
|
import { Command } from "commander"
|
|
import { join } from "path"
|
|
import * as git from "./git.ts"
|
|
import * as vm from "./vm.ts"
|
|
import * as state from "./state.ts"
|
|
import { spinner } from "./spinner.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 Lima VMs").version(pkg.version)
|
|
|
|
// ── sandlot new <branch> ──────────────────────────────────────────────
|
|
|
|
program
|
|
.command("new")
|
|
.argument("<branch>", "branch name")
|
|
.description("Create a new session and launch Claude")
|
|
.action(async (branch: string) => {
|
|
const root = await git.repoRoot()
|
|
const worktreeRel = `.sandlot/${branch}`
|
|
const worktreeAbs = join(root, worktreeRel)
|
|
|
|
const existing = await state.getSession(root, branch)
|
|
if (existing) {
|
|
console.error(`Session "${branch}" already exists. Use "sandlot open ${branch}" to re-enter it.`)
|
|
process.exit(1)
|
|
}
|
|
|
|
const spin = spinner("Creating worktree")
|
|
await git.createWorktree(branch, worktreeAbs, root)
|
|
|
|
spin.text = "Starting VM"
|
|
await vm.ensure()
|
|
spin.succeed("Session ready")
|
|
|
|
await state.setSession(root, {
|
|
branch,
|
|
worktree: worktreeRel,
|
|
created_at: new Date().toISOString(),
|
|
})
|
|
|
|
await vm.claude(worktreeAbs)
|
|
})
|
|
|
|
// ── sandlot list ──────────────────────────────────────────────────────
|
|
|
|
program
|
|
.command("list")
|
|
.description("Show all active sessions")
|
|
.action(async () => {
|
|
const root = await git.repoRoot()
|
|
const st = await state.load(root)
|
|
const sessions = Object.values(st.sessions)
|
|
|
|
if (sessions.length === 0) {
|
|
console.log("No active sessions.")
|
|
return
|
|
}
|
|
|
|
const branchWidth = Math.max(6, ...sessions.map((s) => s.branch.length))
|
|
console.log(`${"BRANCH".padEnd(branchWidth)} WORKTREE`)
|
|
for (const s of sessions) {
|
|
console.log(`${s.branch.padEnd(branchWidth)} ${s.worktree}/`)
|
|
}
|
|
})
|
|
|
|
// ── sandlot open <branch> ─────────────────────────────────────────────
|
|
|
|
program
|
|
.command("open")
|
|
.argument("<branch>", "branch name")
|
|
.description("Re-enter an existing session")
|
|
.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 spin = spinner("Starting VM")
|
|
await vm.ensure()
|
|
spin.succeed("Session ready")
|
|
|
|
await vm.claude(join(root, session.worktree))
|
|
})
|
|
|
|
// ── sandlot rm <branch> ──────────────────────────────────────────────
|
|
|
|
program
|
|
.command("rm")
|
|
.argument("<branch>", "branch name")
|
|
.description("Remove a worktree and clean up the session")
|
|
.action(async (branch: string) => {
|
|
const root = await git.repoRoot()
|
|
const session = await state.getSession(root, branch)
|
|
const worktreeRel = session?.worktree ?? `.sandlot/${branch}`
|
|
const worktreeAbs = join(root, worktreeRel)
|
|
|
|
await git.removeWorktree(worktreeAbs, root)
|
|
console.log(`Removed worktree ${worktreeRel}/`)
|
|
|
|
await git.deleteLocalBranch(branch, root)
|
|
console.log(`Deleted local branch ${branch}`)
|
|
|
|
if (session) {
|
|
await state.removeSession(root, branch)
|
|
}
|
|
})
|
|
|
|
// ── sandlot vm ───────────────────────────────────────────────────────
|
|
|
|
const vmCmd = program.command("vm").description("Manage the sandlot VM")
|
|
|
|
vmCmd
|
|
.command("status")
|
|
.description("Show VM status")
|
|
.action(async () => {
|
|
const s = await vm.status()
|
|
console.log(s)
|
|
})
|
|
|
|
vmCmd
|
|
.command("stop")
|
|
.description("Stop the VM")
|
|
.action(async () => {
|
|
await vm.stop()
|
|
console.log("VM stopped")
|
|
})
|
|
|
|
vmCmd
|
|
.command("destroy")
|
|
.description("Stop and delete the VM")
|
|
.action(async () => {
|
|
await vm.destroy()
|
|
console.log("VM destroyed")
|
|
})
|
|
|
|
program.parse()
|