import { readConfig } from "./src/config"; import { debugDispatch, debugHealth } from "./src/debug"; import { stackLogs, stackStatus, startStack, stopStack } from "./src/docker"; import { runE2E } from "./src/e2e"; import { emitError, emitJson } from "./src/output"; import { jobWithTail, listJobs, readJob, runJob } from "./src/jobs"; import { runChecks } from "./src/check"; const args = process.argv.slice(2); const commandName = args.join(" ") || "help"; function help(): unknown { return { entry: "bun scripts/cli.ts", output: "json", commands: [ { command: "help", description: "List supported commands." }, { command: "config show", description: "Validate and print config.json as the single source of truth." }, { command: "check", description: "Run config, TypeScript, file presence, and docker-compose config checks." }, { command: "server start", description: "Fire-and-forget build/start for database, backend-core, frontend, and provider gateway." }, { command: "server stop", description: "Fire-and-forget docker-compose down for the fixed UniDesk stack." }, { command: "server status", description: "Show fixed ports, containers, service health, and public URLs." }, { command: "server logs [--tail-bytes N]", description: "Return bounded tails from file logs and docker logs." }, { command: "job list", description: "List async jobs from .state/jobs." }, { command: "job status [--tail-bytes N]", description: "Show job state with bounded stdout/stderr tails." }, { command: "debug health", description: "Probe internal core, overview, nodes, frontend, provider ingress, and public boundary." }, { command: "debug dispatch [providerId] [docker.ps|echo]", description: "Submit a real internal-core dispatch request for CLI debugging." }, { command: "e2e run", description: "Run public frontend/provider, internal core/database, and Playwright login E2E checks." }, ], }; } function numberOption(name: string, defaultValue: number): number { const index = args.indexOf(name); if (index === -1) return defaultValue; const raw = args[index + 1]; const value = Number(raw); if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`); return value; } function latestJobId(): string { const jobs = listJobs(); if (jobs.length === 0) throw new Error("No jobs found"); return jobs[0].id; } async function main(): Promise { const [top, sub, third, fourth] = args; if (top === undefined || top === "help" || top === "--help" || top === "-h") { emitJson(commandName, help()); return; } if (top === "internal" && sub === "run-job") { if (!third) throw new Error("internal run-job requires job id"); emitJson(commandName, await runJob(third)); return; } const config = readConfig(); if (top === "config" && sub === "show") { emitJson(commandName, { config }); return; } if (top === "check") { const result = runChecks(config); emitJson(commandName, result, result.ok); if (!result.ok) process.exitCode = 1; return; } if (top === "server") { if (sub === "start") { emitJson(commandName, startStack(config)); return; } if (sub === "stop") { emitJson(commandName, stopStack(config)); return; } if (sub === "status") { emitJson(commandName, await stackStatus(config)); return; } if (sub === "logs") { emitJson(commandName, stackLogs(config, numberOption("--tail-bytes", 3000))); return; } } if (top === "job") { if (sub === "list") { emitJson(commandName, { jobs: listJobs() }); return; } if (sub === "status") { const id = third === "latest" || third === undefined ? latestJobId() : third; emitJson(commandName, { job: jobWithTail(readJob(id), numberOption("--tail-bytes", 12000)) }); return; } } if (top === "debug") { if (sub === "health") { emitJson(commandName, await debugHealth(config)); return; } if (sub === "dispatch") { const providerId = third ?? config.providerGateway.id; const dispatchCommand = fourth === "docker.ps" || fourth === "echo" ? fourth : "docker.ps"; emitJson(commandName, await debugDispatch(config, providerId, dispatchCommand)); return; } } if (top === "e2e" && sub === "run") { const result = await runE2E(config); const ok = (result as { ok?: unknown }).ok === true; emitJson(commandName, result, ok); if (!ok) process.exitCode = 1; return; } throw new Error(`Unknown command: ${commandName}`); } main().catch((error) => { emitError(commandName, error); process.exitCode = 1; });