sandlot/src/commands/vm.ts

149 lines
4.8 KiB
TypeScript

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<string, string> = { 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<string, string> = { idle: `${dim}${reset}`, active: `${cyan}${reset}`, dirty: `${yellow}${reset}`, saved: `${green}${reset}` }
const branchColors: Record<string, string> = { 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")
})
}