548 lines
19 KiB
TypeScript
548 lines
19 KiB
TypeScript
import { readConfig } from "./src/config";
|
|
import { debugDispatch, debugHealth, debugTask, isDebugDispatchCommand, type DebugDispatchCommand } from "./src/debug";
|
|
import { isRebuildableService, rebuildService, stackLogs, stackStatus, startStack, stopStack, unsupportedRebuildService } from "./src/docker";
|
|
import { emitError, emitJson } from "./src/output";
|
|
import { cancelJob, jobWithTail, listJobs, listJobsSummary, readJob, runJob } from "./src/jobs";
|
|
import { checkHelp, parseCheckOptions, runChecks, runRecoveryGuardrailsCheck } from "./src/check";
|
|
import { runSsh } from "./src/ssh";
|
|
import { autoRemoteCiPublishUserServiceDryRunPlan, extractRemoteCliOptions, runRemoteCli } from "./src/remote";
|
|
import { runMicroserviceCommand } from "./src/microservices";
|
|
import { runCodeQueueCommand } from "./src/code-queue";
|
|
import { runDecisionCenterCommand } from "./src/decision-center";
|
|
import { deployHelp, runCodeQueueDeployCompatCommand, runDeployCommand } from "./src/deploy";
|
|
import { runProviderCommand } from "./src/provider-attach";
|
|
import { runScheduleCommand } from "./src/schedules";
|
|
import { parseNetworkPerfOptions, runNetworkPerf } from "./src/network-perf";
|
|
import { ciHelp, runCiCommand } from "./src/ci";
|
|
import { runSwapCommand } from "./src/swap";
|
|
import { runDevEnvCommand } from "./src/dev-env";
|
|
import { runArtifactRegistryCommand } from "./src/artifact-registry";
|
|
import { runAuthBrokerCommand } from "./src/auth-broker";
|
|
import { runGhCommand } from "./src/gh";
|
|
import { isGhContentRoute, runGhContentRoute } from "./src/gh-route";
|
|
import { runCommanderCommand } from "./src/commander";
|
|
import { isHelpToken, rootHelp, serverHelp, sshHelp, staticNamespaceHelp } from "./src/help";
|
|
import { runServerCleanupCommand } from "./src/server-cleanup";
|
|
import { runGcCommand } from "./src/gc";
|
|
|
|
const remoteOptions = extractRemoteCliOptions(process.argv.slice(2));
|
|
const args = remoteOptions.args;
|
|
const commandName = displayCommandName(args);
|
|
|
|
function displayCommandName(parts: string[]): string {
|
|
if (parts.length === 0) return "help";
|
|
if (parts[0] === "codex" && (parts[1] === "submit" || parts[1] === "enqueue")) {
|
|
const shown = ["codex", parts[1]];
|
|
const shownValueOptions = new Set([
|
|
"--prompt-file",
|
|
"--file",
|
|
"--queue",
|
|
"--queue-id",
|
|
"--provider",
|
|
"--provider-id",
|
|
"--cwd",
|
|
"--workdir",
|
|
"--model",
|
|
"--reasoning-effort",
|
|
"--execution-mode",
|
|
"--mode",
|
|
"--max-attempts",
|
|
"--reference-task-id",
|
|
"--reference",
|
|
"--ref",
|
|
]);
|
|
const hasPromptFile = parts.includes("--prompt-file") || parts.includes("--file");
|
|
const hasPromptStdin = parts.includes("--prompt-stdin") || parts.includes("--stdin");
|
|
const hasHelp = parts.slice(2).some(isHelpToken);
|
|
if (!hasPromptFile && !hasPromptStdin && !hasHelp) shown.push("<prompt:redacted>");
|
|
for (let index = 2; index < parts.length; index += 1) {
|
|
const part = parts[index] ?? "";
|
|
if (!part.startsWith("--")) continue;
|
|
shown.push(part);
|
|
if (shownValueOptions.has(part)) {
|
|
shown.push(parts[index + 1] ?? "<missing>");
|
|
index += 1;
|
|
}
|
|
}
|
|
return shown.join(" ");
|
|
}
|
|
if (parts[0] === "codex" && parts[1] === "steer" && parts[2] !== undefined) {
|
|
const shown = ["codex", "steer", parts[2]];
|
|
const shownValueOptions = new Set(["--prompt-file", "--file", "--retry-attempts", "--retry-delay-ms", "--steer-id", "--steerId"]);
|
|
const hasPromptFile = parts.includes("--prompt-file") || parts.includes("--file");
|
|
const hasPromptStdin = parts.includes("--prompt-stdin") || parts.includes("--stdin");
|
|
const hasHelp = parts.slice(3).some(isHelpToken);
|
|
if (!hasPromptFile && !hasPromptStdin && !hasHelp) shown.push("<prompt:redacted>");
|
|
for (let index = 3; index < parts.length; index += 1) {
|
|
const part = parts[index] ?? "";
|
|
if (!part.startsWith("--")) continue;
|
|
shown.push(part);
|
|
if (shownValueOptions.has(part)) {
|
|
shown.push(parts[index + 1] ?? "<missing>");
|
|
index += 1;
|
|
}
|
|
}
|
|
return shown.join(" ");
|
|
}
|
|
if (parts[0] === "codex" && parts[1] === "resume" && parts[2] !== undefined) {
|
|
const shown = ["codex", "resume", parts[2]];
|
|
const shownValueOptions = new Set(["--prompt-file", "--file", "--resume-id", "--resumeId"]);
|
|
const hasPromptFile = parts.includes("--prompt-file") || parts.includes("--file");
|
|
const hasPromptStdin = parts.includes("--prompt-stdin") || parts.includes("--stdin");
|
|
const hasHelp = parts.slice(3).some(isHelpToken);
|
|
if (!hasPromptFile && !hasPromptStdin && !hasHelp) shown.push("<prompt:redacted>");
|
|
for (let index = 3; index < parts.length; index += 1) {
|
|
const part = parts[index] ?? "";
|
|
if (!part.startsWith("--")) continue;
|
|
shown.push(part);
|
|
if (shownValueOptions.has(part)) {
|
|
shown.push(parts[index + 1] ?? "<missing>");
|
|
index += 1;
|
|
}
|
|
}
|
|
return shown.join(" ");
|
|
}
|
|
if (parts[0] === "commander" && parts[1] === "approval" && parts[2] === "request") {
|
|
const shown: string[] = [];
|
|
for (let index = 0; index < parts.length; index += 1) {
|
|
const part = parts[index] ?? "";
|
|
shown.push(part);
|
|
if (part === "--reason") {
|
|
shown.push("<reason:redacted>");
|
|
index += 1;
|
|
}
|
|
}
|
|
return shown.join(" ");
|
|
}
|
|
if (parts[0] === "gh") {
|
|
const shown: string[] = [];
|
|
for (let index = 0; index < parts.length; index += 1) {
|
|
const part = parts[index] ?? "";
|
|
shown.push(part);
|
|
if (part === "--body" || part === "--comment") {
|
|
shown.push("<body:redacted>");
|
|
index += 1;
|
|
}
|
|
}
|
|
return shown.join(" ");
|
|
}
|
|
return parts.join(" ");
|
|
}
|
|
|
|
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 boundedNumberOption(name: string, defaultValue: number, maxValue: number): number {
|
|
return Math.min(numberOption(name, defaultValue), maxValue);
|
|
}
|
|
|
|
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<string, unknown> | 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<string, unknown>;
|
|
}
|
|
|
|
function dispatchPayload(command: DebugDispatchCommand): Record<string, unknown> {
|
|
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 resultOk(result: unknown): boolean {
|
|
return typeof result !== "object" || result === null || !("ok" in result) || (result as { ok?: unknown }).ok !== false;
|
|
}
|
|
|
|
function latestJobId(): string {
|
|
const jobs = listJobs();
|
|
if (jobs.length === 0) throw new Error("No jobs found");
|
|
return jobs[0].id;
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
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, rootHelp());
|
|
return;
|
|
}
|
|
|
|
if (top === "ssh" && (sub === undefined || isHelpToken(sub) || (isHelpToken(third) && args.length === 3))) {
|
|
emitJson(commandName, sshHelp());
|
|
return;
|
|
}
|
|
|
|
if (top === "ssh" && isGhContentRoute(sub)) {
|
|
process.exitCode = await runGhContentRoute(sub ?? "", args.slice(2));
|
|
return;
|
|
}
|
|
|
|
if (top === "check" && isHelpToken(sub)) {
|
|
emitJson(commandName, checkHelp());
|
|
return;
|
|
}
|
|
|
|
if (top === "server" && (isHelpToken(sub) || args.slice(2).some(isHelpToken))) {
|
|
emitJson(commandName, serverHelp(isHelpToken(sub) ? undefined : sub));
|
|
return;
|
|
}
|
|
|
|
if (top === "deploy" && args.slice(1).some(isHelpToken)) {
|
|
emitJson(commandName, deployHelp(isHelpToken(sub) ? undefined : sub));
|
|
return;
|
|
}
|
|
|
|
if (top === "ci" && (isHelpToken(sub) || args.slice(1).some(isHelpToken))) {
|
|
emitJson(commandName, ciHelp());
|
|
return;
|
|
}
|
|
|
|
const namespaceHelp = await staticNamespaceHelp(args);
|
|
if (namespaceHelp !== null) {
|
|
emitJson(commandName, namespaceHelp);
|
|
return;
|
|
}
|
|
|
|
if (top === "internal" && sub === "run-job") {
|
|
if (!third) throw new Error("internal run-job requires job id");
|
|
emitJson(commandName, await runJob(third));
|
|
return;
|
|
}
|
|
|
|
if (top === "dev-env") {
|
|
const result = runDevEnvCommand(args.slice(1));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (top === "artifact-registry") {
|
|
const result = await runArtifactRegistryCommand(args.slice(1));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (top === "auth-broker") {
|
|
const result = runAuthBrokerCommand(args.slice(1));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (top === "gh") {
|
|
const result = await runGhCommand(args.slice(1));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (top === "commander") {
|
|
const result = runCommanderCommand(args.slice(1));
|
|
if (sub === "prompt-lint") {
|
|
emitJson(commandName, result, true);
|
|
return;
|
|
}
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (top === "hwlab") {
|
|
if (sub === "node" || sub === "nodes") {
|
|
const { runHwlabNodeCommand } = await import("./src/hwlab-node");
|
|
const result = await runHwlabNodeCommand(readConfig(), args.slice(2));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
if (sub === "g14") {
|
|
const { runHwlabG14Command } = await import("./src/hwlab-g14");
|
|
const result = await runHwlabG14Command(readConfig(), args.slice(2));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
const { runHwlabCdCommand } = await import("./src/hwlab-cd");
|
|
const result = await runHwlabCdCommand(args.slice(1));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (top === "agentrun") {
|
|
const { runAgentRunCommand } = await import("./src/agentrun");
|
|
const result = await runAgentRunCommand(readConfig(), args.slice(1));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (top === "platform-infra") {
|
|
const { runPlatformInfraCommand } = await import("./src/platform-infra");
|
|
const result = await runPlatformInfraCommand(readConfig(), args.slice(1));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
const config = readConfig();
|
|
const autoRemoteCiPublishPlan = autoRemoteCiPublishUserServiceDryRunPlan(config, args);
|
|
if (autoRemoteCiPublishPlan.enabled && autoRemoteCiPublishPlan.host !== null) {
|
|
process.exitCode = await runRemoteCli({
|
|
...remoteOptions,
|
|
host: autoRemoteCiPublishPlan.host,
|
|
transport: "frontend",
|
|
args,
|
|
}, config);
|
|
return;
|
|
}
|
|
|
|
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") {
|
|
if (isHelpToken(sub)) {
|
|
emitJson(commandName, checkHelp());
|
|
return;
|
|
}
|
|
if (sub === "recovery-guardrails") {
|
|
const result = runRecoveryGuardrailsCheck(config);
|
|
emitJson(commandName, result, result.ok);
|
|
if (!result.ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
const result = await runChecks(config, parseCheckOptions(args.slice(1)));
|
|
emitJson(commandName, result, result.ok);
|
|
if (!result.ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (top === "server") {
|
|
if (isHelpToken(sub) || args.slice(2).some(isHelpToken)) {
|
|
emitJson(commandName, serverHelp(isHelpToken(sub) ? undefined : sub));
|
|
return;
|
|
}
|
|
if (sub === "start") {
|
|
const result = startStack(config);
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
if (sub === "stop") {
|
|
emitJson(commandName, stopStack(config));
|
|
return;
|
|
}
|
|
if (sub === "status") {
|
|
const result = await stackStatus(config);
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
if (sub === "swap") {
|
|
const result = runSwapCommand(args.slice(2));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
if (sub === "logs") {
|
|
emitJson(commandName, stackLogs(config, boundedNumberOption("--tail-bytes", 3000, 500_000)));
|
|
return;
|
|
}
|
|
if (sub === "cleanup") {
|
|
const result = await runServerCleanupCommand(config, args.slice(2));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
if (sub === "rebuild") {
|
|
if (!isRebuildableService(third)) {
|
|
const result = unsupportedRebuildService(third);
|
|
emitJson(commandName, result, false);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
emitJson(commandName, rebuildService(config, third));
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (top === "gc") {
|
|
const result = await runGcCommand(config, args.slice(1));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (top === "microservice") {
|
|
const result = await runMicroserviceCommand(config, args.slice(1));
|
|
const ok = resultOk(result);
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (top === "decision" || top === "decision-center") {
|
|
const result = await runDecisionCenterCommand(config, args.slice(1));
|
|
const ok = resultOk(result);
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 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") {
|
|
const result = await runScheduleCommand(config, args.slice(1));
|
|
const ok = resultOk(result);
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 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;
|
|
}
|
|
const result = await runCodeQueueCommand(config, args.slice(1));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (top === "job") {
|
|
if (sub === "list") {
|
|
emitJson(commandName, listJobsSummary({ limit: boundedNumberOption("--limit", 50, 500), includeCommand: args.includes("--include-command") }));
|
|
return;
|
|
}
|
|
if (sub === "status") {
|
|
const id = third === "latest" || third === undefined ? latestJobId() : third;
|
|
emitJson(commandName, { job: jobWithTail(readJob(id), boundedNumberOption("--tail-bytes", 12000, 500_000)) });
|
|
return;
|
|
}
|
|
if (sub === "cancel") {
|
|
if (!third) throw new Error("job cancel requires job id");
|
|
emitJson(commandName, cancelJob(third));
|
|
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 === "ci") {
|
|
const result = await runCiCommand(config, args.slice(1));
|
|
const ok = (result as { ok?: unknown }).ok !== false;
|
|
emitJson(commandName, result, ok);
|
|
if (!ok) process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (top === "e2e" && sub === "run") {
|
|
const { parseE2ERunOptions, runE2E } = await import("./src/e2e");
|
|
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;
|
|
});
|