import type { Command } from "commander" import * as vm from "../vm.ts" import * as git from "../git.ts" import * as state from "../state.ts" import { spinner } from "../spinner.ts" import { reset, dim, green, yellow, cyan, red } from "../fmt.ts" export function register(program: Command) { const vmCmd = program.command("vm").description("Manage the sandlot VM") vmCmd .command("create") .description("Create and provision the VM") .action(async () => { const spin = spinner("Creating VM") try { await vm.create((msg) => { spin.text = msg }) spin.succeed("VM created") } catch (err) { spin.fail(String((err as Error).message ?? err)) process.exit(1) } }) vmCmd .command("start") .description("Start the VM") .action(async () => { try { await vm.start() console.log("✔ VM started") } catch (err) { console.error(`✖ ${(err as Error).message ?? err}`) process.exit(1) } }) vmCmd .command("shell") .description("Open a shell in the VM") .action(async () => { await vm.ensure() await vm.shell() }) vmCmd .command("status") .description("Show VM status and all sessions across repos") .option("--json", "Output as JSON") .action(async (opts: { json?: boolean }) => { const s = await vm.status() const sessions = await state.loadAll() if (opts.json) { console.log(JSON.stringify({ vm: s, sessions }, null, 2)) return } const statusColors: Record = { running: green, stopped: yellow, missing: red } console.log(`${dim}VM:${reset} ${statusColors[s] ?? ""}${s}${reset}`) if (sessions.length === 0) { console.log(`\n${dim}No active sessions.${reset}`) return } // Determine status for each session in parallel const statusEntries = await Promise.all( sessions.map(async (sess): Promise<[string, string]> => { const key = `${sess.repo}/${sess.branch}` try { if (await vm.isClaudeActive(sess.worktree, sess.branch)) return [key, "active"] if (await git.isDirty(sess.worktree)) return [key, "dirty"] if (await git.hasNewCommits(sess.worktree)) return [key, "saved"] } catch {} return [key, "idle"] }) ) const statuses = Object.fromEntries(statusEntries) const icons: Record = { idle: `${dim}◯${reset}`, active: `${cyan}◎${reset}`, dirty: `${yellow}◐${reset}`, saved: `${green}●${reset}` } const branchColors: Record = { idle: dim, active: cyan, dirty: yellow, saved: green } const repoWidth = Math.max(4, ...sessions.map(sess => sess.repo.length)) const branchWidth = Math.max(6, ...sessions.map(sess => sess.branch.length)) const cols = process.stdout.columns || 80 const prefixWidth = repoWidth + branchWidth + 6 console.log(`\n ${dim}${"REPO".padEnd(repoWidth)} ${"BRANCH".padEnd(branchWidth)} PROMPT${reset}`) for (const sess of sessions) { const prompt = (sess.prompt ?? "").split("\n")[0] const key = `${sess.repo}/${sess.branch}` const status = statuses[key] const icon = icons[status] const bc = branchColors[status] const maxPrompt = cols - prefixWidth const truncated = maxPrompt > 3 && prompt.length > maxPrompt ? prompt.slice(0, maxPrompt - 3) + "..." : prompt console.log(`${icon} ${dim}${sess.repo.padEnd(repoWidth)}${reset} ${bc}${sess.branch.padEnd(branchWidth)}${reset} ${dim}${truncated}${reset}`) } console.log(`\n${dim}◯ idle${reset} · ${cyan}◎ active${reset} · ${yellow}◐ unsaved${reset} · ${green}● saved${reset}`) }) vmCmd .command("info") .description("Show VM system info (via neofetch)") .action(async () => { await vm.ensure() await vm.neofetch() }) vmCmd .command("stop") .description("Stop the VM") .action(async () => { const spin = spinner("Stopping VM") try { await vm.stop() spin.succeed("VM stopped") } catch (err) { spin.fail(String((err as Error).message ?? err)) process.exit(1) } }) vmCmd .command("destroy") .description("Stop and delete the VM") .action(async () => { const spin = spinner("Destroying VM") try { await vm.destroy() spin.succeed("VM destroyed") } catch (err) { spin.fail(String((err as Error).message ?? err)) process.exit(1) } }) vmCmd .command("uncache") .description("Clear the package cache (next create will re-download)") .action(async () => { const had = await vm.clearCache() console.log(had ? "✔ Package cache cleared" : "No cache to clear") }) }