149 lines
4.8 KiB
TypeScript
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")
|
|
})
|
|
}
|