import { readConfig } from "./src/config"; import { debugDispatch, debugHealth, debugTask, isDebugDispatchCommand, type DebugDispatchCommand } from "./src/debug"; import { isRebuildableService, rebuildService, stackLogs, stackStatus, startStack, stopStack } from "./src/docker"; import { parseE2ERunOptions, runE2E } from "./src/e2e"; import { emitError, emitJson } from "./src/output"; import { jobWithTail, listJobs, readJob, runJob } from "./src/jobs"; import { parseCheckOptions, runChecks } from "./src/check"; import { runSsh } from "./src/ssh"; import { extractRemoteCliOptions, runRemoteCli } from "./src/remote"; import { runMicroserviceCommand } from "./src/microservices"; import { runCodeQueueCommand } from "./src/code-queue"; import { runDecisionCenterCommand } from "./src/decision-center"; import { runCodeQueueDeployCompatCommand, runDeployCommand } from "./src/deploy"; import { runProviderCommand } from "./src/provider-attach"; import { runScheduleCommand } from "./src/schedules"; import { parseNetworkPerfOptions, runNetworkPerf } from "./src/network-perf"; const remoteOptions = extractRemoteCliOptions(process.argv.slice(2)); const args = remoteOptions.args; const commandName = args.join(" ") || "help"; function help(): unknown { return { entry: "bun scripts/cli.ts", output: "json", commands: [ { command: "help", description: "List supported commands." }, { command: "--main-server-ip ", description: "Run selected commands through the public frontend API; use --main-server-key only for legacy SSH transport." }, { command: "config show", description: "Validate and print config.json as the single source of truth." }, { command: "check [--full|--files|--scripts-typecheck|--components|--compose|--logs]", description: "Run the lightweight default syntax/config gate; opt into file, type, Compose, or policy checks explicitly." }, { command: "server start", description: "Fire-and-forget build/start for database, backend-core, frontend, provider gateway, and managed main-server user services." }, { 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: "server rebuild ", description: "Build first, then serialize, force-recreate, and validate one Compose service." }, { command: "provider attach [--master-server URL] [--up] [--force]", description: "Generate the minimal external provider-gateway env/compose bundle; only master server URL and provider id are required." }, { command: "ssh [ssh-like args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge with built-in remote helper tools in PATH." }, { command: "ssh apply-patch [tool args...] < patch.diff", description: "Invoke the injected remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." }, { command: "ssh py [script-args...] < script.py", description: "Run remote Python from local stdin through SSH passthrough without nested shell quoting; extra args become script argv." }, { command: "ssh skills [--scope all|wsl|windows] [--limit N]", description: "Discover WSL/Linux and, for WSL providers, Windows skill directories in one SSH passthrough call." }, { command: "ssh find [--max-depth N] [--type d|f|l] [--contains TEXT] [--iname PATTERN] [--limit N] [--sort]", description: "Run a structured remote find command without nested shell quoting or parentheses." }, { command: "ssh glob [--root DIR] [--pattern PATTERN] [--contains TEXT] [--type any|f|d] [--limit N] [--sort]", description: "Run remote glob matching through the injected helper without shell glob expansion." }, { command: "ssh argv [args...]", description: "Run a remote command with each argv token shell-quoted by UniDesk before SSH passthrough." }, { command: "microservice list", description: "List UniDesk-managed user services and their provider/runtime mapping." }, { command: "microservice status ", description: "Show one user service config, repository reference, backend mapping, and runtime status." }, { command: "microservice health ", description: "Probe one user service through backend-core -> provider-gateway HTTP proxy." }, { command: "microservice proxy [--method GET|POST|PUT|PATCH|DELETE] [--body-json JSON|--body-file path|--body-stdin] [--raw] [--max-body-bytes N]", description: "Access a private user-service backend path through the same frontend-only proxy used by WebUI; JSON request bodies are supported for controlled write/debug endpoints." }, { command: "decision upload [--title text] [--type meeting|decision] [--level G0|G1|G2|G3|P0|P1|P2|P3|none] [--status active|blocked|parked|done] [--linked-goal-id id] [--evidence url]", description: "Upload a meeting note or decision record through backend-core -> decision-center user-service proxy." }, { command: "decision list [--type ...] [--status ...] [--level ...] [--linked-goal-id id] [--limit N]", description: "List Decision Center records through the user-service proxy." }, { command: "decision show ", description: "Show one Decision Center record." }, { command: "deploy check|plan|apply [--file deploy.json] [--service id] [--dry-run] [--force]", description: "Reconcile services from a repo+commit manifest using target-side build and live commit verification." }, { command: "schedule list|get|runs|run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run supports --wait-ms N." }, { command: "schedule upsert-pgdata-backup [--time HH:MM] [--remote-base /SERVER_DATA/UNIDESK_PG_DATA]", description: "Create or update the daily PGDATA physical backup task that uploads monthly rotated archives to Baidu Netdisk." }, { command: "codex deploy [--provider-id D601] [--timeout-ms N]", description: "Compatibility wrapper for deploy apply --service code-queue with a temporary repo+commit manifest." }, { command: "codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]", description: "Submit a Code Queue task through backend-core -> code-queue proxy; --dry-run shows the structured request without enqueueing." }, { command: "codex task [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", description: "Fetch a compact Code Queue task summary; trace rows are opt-in and paged with next/previous commands to avoid output explosion." }, { command: "codex output [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]", description: "Fetch paged raw Code Queue output records by seq when a trace row has omitted command/output text." }, { command: "codex judge --attempt N [--dry-run] [--include-prompt]", description: "Replay one stored Code Queue attempt through the same judge context builder and MiniMax judge call path used by the live queue worker." }, { command: "codex interrupt|cancel ", description: "Request interrupt for a running Code Queue task, or cancel a queued/retry_wait task, through the same private proxy." }, { command: "codex (queues | queue create | queue merge --into | move --queue )", description: "List/create/merge Code Queue lanes and move a queued task; merge preserves task queue time order and deletes the source queue record." }, { 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, nodes, system/Docker status, frontend, provider ingress, and public boundary." }, { command: "debug dispatch [providerId] [docker.ps|provider.upgrade|host.ssh|microservice.http|echo] [--wait-ms N]", description: "Submit a real internal-core dispatch request for CLI debugging." }, { command: "debug task ", description: "Read a dispatched task record from internal core for CLI debugging." }, { command: "network perf [--service code-queue --path /api/tasks/overview?limit=30 --count N --concurrency N --label before|after]", description: "Benchmark frontend -> backend-core -> provider/adapter user-service networking and report latency/proxy-mode distributions." }, { command: "e2e run [--only pattern[,pattern...]] [--skip pattern[,pattern...]]", description: "Run selected public/internal/Playwright E2E checks; use --only for focused iteration and rerun without filters for final regression." }, ], }; } 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 stringOption(name: string): string | undefined { const index = args.indexOf(name); if (index === -1) return undefined; const raw = args[index + 1]; if (raw === undefined || raw.length === 0) throw new Error(`${name} requires a non-empty value`); return raw; } function jsonOption(name: string): Record | undefined { const raw = stringOption(name); if (raw === undefined) return undefined; const parsed = JSON.parse(raw) as unknown; if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${name} must be a JSON object`); return parsed as Record; } function dispatchPayload(command: DebugDispatchCommand): Record { const explicit = jsonOption("--payload-json") ?? {}; if (command === "provider.upgrade") { return { source: "cli-debug", mode: stringOption("--mode") ?? stringOption("--upgrade-mode") ?? "plan", ...explicit }; } if (command === "host.ssh") { const sshCommand = stringOption("--ssh-command"); return { source: "cli-debug", mode: sshCommand === undefined ? "probe" : "exec", ...(sshCommand === undefined ? {} : { command: sshCommand }), ...(stringOption("--cwd") === undefined ? {} : { cwd: stringOption("--cwd") }), ...(args.includes("--timeout-ms") ? { timeoutMs: numberOption("--timeout-ms", 8000) } : {}), ...explicit, }; } return { source: "cli-debug", ...explicit }; } function latestJobId(): string { const jobs = listJobs(); if (jobs.length === 0) throw new Error("No jobs found"); return jobs[0].id; } async function main(): Promise { if (remoteOptions.host !== null) { process.exitCode = await runRemoteCli(remoteOptions, readConfig()); return; } 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 === "ssh") { const exitCode = await runSsh(config, sub ?? "", args.slice(2)); process.exitCode = exitCode; return; } if (top === "config" && sub === "show") { emitJson(commandName, { config }); return; } if (top === "check") { const result = runChecks(config, parseCheckOptions(args.slice(1))); 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 (sub === "rebuild") { if (!isRebuildableService(third)) { throw new Error("server rebuild requires one of: backend-core, frontend, provider-gateway, todo-note, code-queue-mgr, project-manager, baidu-netdisk, oa-event-flow"); } emitJson(commandName, rebuildService(config, third)); return; } } if (top === "microservice") { emitJson(commandName, await runMicroserviceCommand(config, args.slice(1))); return; } if (top === "decision" || top === "decision-center") { emitJson(commandName, await runDecisionCenterCommand(config, args.slice(1))); return; } if (top === "deploy") { const result = await runDeployCommand(config, args.slice(1)); const ok = (result as { ok?: unknown }).ok !== false; emitJson(commandName, result, ok); if (!ok) process.exitCode = 1; return; } if (top === "provider") { emitJson(commandName, await runProviderCommand(config, args.slice(1))); return; } if (top === "schedule") { emitJson(commandName, await runScheduleCommand(config, args.slice(1))); return; } if (top === "codex") { if (sub === "deploy") { const result = await runCodeQueueDeployCompatCommand(config, args.slice(2)); const ok = (result as { ok?: unknown }).ok !== false; emitJson(commandName, result, ok); if (!ok) process.exitCode = 1; return; } emitJson(commandName, await runCodeQueueCommand(config, args.slice(1))); 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 = isDebugDispatchCommand(third) ? config.providerGateway.id : third ?? config.providerGateway.id; const commandArg = isDebugDispatchCommand(third) ? third : fourth; const dispatchCommand = isDebugDispatchCommand(commandArg) ? commandArg : "docker.ps"; emitJson(commandName, await debugDispatch(config, providerId, dispatchCommand, dispatchPayload(dispatchCommand), numberOption("--wait-ms", 0))); return; } if (sub === "task") { emitJson(commandName, await debugTask(config, third ?? "latest")); return; } } if (top === "network" && sub === "perf") { emitJson(commandName, await runNetworkPerf(parseNetworkPerfOptions(config, args.slice(2)))); return; } if (top === "e2e" && sub === "run") { const result = await runE2E(config, parseE2ERunOptions(args.slice(2))); 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; });