import { readFile } from "node:fs/promises"; import { startManagerServer } from "../../src/mgr/server.js"; import { MemoryAgentRunStore } from "../../src/mgr/store.js"; import { ManagerClient } from "../../src/mgr/client.js"; import { runOnce } from "../../src/runner/run-once.js"; import { renderRunnerJobDryRun } from "../../src/runner/k8s-job.js"; import { renderCodexProviderSecretPlan } from "./secret-render.js"; import type { JsonRecord, JsonValue, RunRecord } from "../../src/common/types.js"; import { AgentRunError, errorToJson } from "../../src/common/errors.js"; import type { RunnerOnceOptions } from "../../src/runner/run-once.js"; interface ParsedArgs { positional: string[]; flags: Map; } export async function runCli(argv: string[]): Promise { try { const result = await dispatch(parseArgs(argv)); print({ ok: true, data: result }); } catch (error) { const status = error instanceof AgentRunError ? error.httpStatus : 1; print({ ok: false, ...(error instanceof AgentRunError ? { failureKind: error.failureKind, message: error.message } : { failureKind: "infra-failed", message: error instanceof Error ? error.message : String(error) }), error: errorToJson(error) }); process.exitCode = status === 0 ? 1 : status > 255 ? 1 : status; } } async function dispatch(args: ParsedArgs): Promise { const [group, command, id] = args.positional; if (!group || group === "help") return help(); if (group === "server" && command === "start") return startServer(args); if (group === "server" && command === "status") return client(args).get("/health/readiness"); if (group === "backends" && command === "list") return client(args).get("/api/v1/backends"); if (group === "secrets" && command === "codex" && id === "render") return renderCodexSecret(args); if (group === "runs" && command === "create") return client(args).post("/api/v1/runs", await jsonFile(args)); if (group === "runs" && command === "show" && id) return client(args).get(`/api/v1/runs/${encodeURIComponent(id)}`); if (group === "runs" && command === "events" && id) return client(args).get(`/api/v1/runs/${encodeURIComponent(id)}/events?afterSeq=${flag(args, "after-seq", "0")}&limit=${flag(args, "limit", "100")}`); if (group === "commands" && command === "create" && id) { const body = await jsonFile(args); if (!body.type) body.type = flag(args, "type", "turn"); const idempotencyKey = optionalFlag(args, "idempotency-key"); if (idempotencyKey) body.idempotencyKey = idempotencyKey; return client(args).post(`/api/v1/runs/${encodeURIComponent(id)}/commands`, body); } if (group === "commands" && command === "show" && id) { const runId = flag(args, "run-id", ""); if (!runId) throw new AgentRunError("schema-invalid", "commands show requires --run-id", { httpStatus: 2 }); return client(args).get(`/api/v1/runs/${encodeURIComponent(runId)}/commands/${encodeURIComponent(id)}`); } if (group === "runner" && command === "start") { const runId = flag(args, "run-id", ""); if (!runId) throw new AgentRunError("schema-invalid", "runner start requires --run-id", { httpStatus: 2 }); const options: RunnerOnceOptions = { managerUrl: managerUrl(args), runId, }; const runnerId = optionalFlag(args, "runner-id"); const codexCommand = optionalFlag(args, "codex-command"); const codexHome = optionalFlag(args, "codex-home") ?? process.env.CODEX_HOME; if (runnerId) options.runnerId = runnerId; if (codexCommand) options.codexCommand = codexCommand; if (codexHome) options.codexHome = codexHome; return runOnce(options) as unknown as JsonValue; } if (group === "runner" && command === "job") return renderRunnerJob(args); throw new AgentRunError("schema-invalid", `unsupported command: ${args.positional.join(" ")}`, { httpStatus: 2 }); } async function renderRunnerJob(args: ParsedArgs): Promise { const runId = flag(args, "run-id", ""); const commandId = flag(args, "command-id", ""); if (!runId) throw new AgentRunError("schema-invalid", "runner job requires --run-id", { httpStatus: 2 }); if (!commandId) throw new AgentRunError("schema-invalid", "runner job requires --command-id", { httpStatus: 2 }); const image = optionalFlag(args, "image"); if (args.flags.get("dry-run") !== true) { const body: JsonRecord = { commandId }; if (image) body.image = image; const namespace = optionalFlag(args, "namespace"); const attemptId = optionalFlag(args, "attempt-id"); const runnerId = optionalFlag(args, "runner-id"); const sourceCommit = optionalFlag(args, "source-commit"); const runnerManagerUrl = optionalFlag(args, "runner-manager-url"); if (namespace) body.namespace = namespace; if (attemptId) body.attemptId = attemptId; if (runnerId) body.runnerId = runnerId; if (sourceCommit) body.sourceCommit = sourceCommit; if (runnerManagerUrl) body.managerUrl = runnerManagerUrl; return await client(args).post(`/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs`, body) as JsonRecord; } if (!image) throw new AgentRunError("schema-invalid", "runner job --dry-run requires --image", { httpStatus: 2 }); const run = await client(args).get(`/api/v1/runs/${encodeURIComponent(runId)}`) as RunRecord; const options = { run, commandId, image, managerUrl: managerUrl(args), namespace: optionalFlag(args, "namespace") ?? "agentrun-v01", }; const attemptId = optionalFlag(args, "attempt-id"); const runnerId = optionalFlag(args, "runner-id"); const sourceCommit = optionalFlag(args, "source-commit"); return renderRunnerJobDryRun({ ...options, ...(attemptId ? { attemptId } : {}), ...(runnerId ? { runnerId } : {}), ...(sourceCommit ? { sourceCommit } : {}), }); } async function renderCodexSecret(args: ParsedArgs): Promise { if (args.flags.get("dry-run") !== true) { throw new AgentRunError("schema-invalid", "secrets codex render requires --dry-run", { httpStatus: 2 }); } const options: Parameters[0] = { dryRun: true }; const codexHome = optionalFlag(args, "codex-home"); const authFile = optionalFlag(args, "auth-file"); const configFile = optionalFlag(args, "config-file"); const namespace = optionalFlag(args, "namespace"); const secretName = optionalFlag(args, "secret-name"); if (codexHome) options.codexHome = codexHome; if (authFile) options.authFile = authFile; if (configFile) options.configFile = configFile; if (namespace) options.namespace = namespace; if (secretName) options.secretName = secretName; return renderCodexProviderSecretPlan(options); } async function startServer(args: ParsedArgs): Promise { const port = Number(flag(args, "port", "8080")); const host = flag(args, "host", "0.0.0.0"); const storeMode = optionalFlag(args, "store") ?? process.env.AGENTRUN_STORE ?? process.env.AGENTRUN_MGR_STORE; const started = await startManagerServer({ port, host, ...(storeMode === "memory" ? { store: new MemoryAgentRunStore() } : {}) }); const database = await started.store.health(); return { serviceId: "agentrun-mgr", baseUrl: started.baseUrl, pid: process.pid, database, note: "foreground process; use Kubernetes/Tekton for v0.1 runtime" }; } function client(args: ParsedArgs): ManagerClient { return new ManagerClient(managerUrl(args)); } function managerUrl(args: ParsedArgs): string { return optionalFlag(args, "manager-url") ?? process.env.AGENTRUN_MGR_URL ?? "http://127.0.0.1:8080"; } async function jsonFile(args: ParsedArgs): Promise { const file = optionalFlag(args, "json-file"); if (!file) throw new AgentRunError("schema-invalid", "--json-file is required", { httpStatus: 2 }); const value = JSON.parse(await readFile(file, "utf8")) as unknown; if (typeof value === "object" && value !== null && !Array.isArray(value)) return value as JsonRecord; throw new AgentRunError("schema-invalid", "json file must contain an object", { httpStatus: 2 }); } function parseArgs(argv: string[]): ParsedArgs { const positional: string[] = []; const flags = new Map(); for (let index = 0; index < argv.length; index += 1) { const item = argv[index] ?? ""; if (!item.startsWith("--")) { positional.push(item); continue; } const key = item.slice(2); const next = argv[index + 1]; if (next === undefined || next.startsWith("--")) flags.set(key, true); else { flags.set(key, next); index += 1; } } return { positional, flags }; } function flag(args: ParsedArgs, name: string, fallback: string): string { const value = args.flags.get(name); return typeof value === "string" ? value : fallback; } function optionalFlag(args: ParsedArgs, name: string): string | null { const value = args.flags.get(name); return typeof value === "string" && value.length > 0 ? value : null; } function help(): JsonRecord { return { commands: [ "runs create --json-file ", "runs show ", "runs events --after-seq --limit ", "commands create --type turn --json-file ", "commands show --run-id ", "runner start --run-id ", "runner job --run-id --command-id [--image ] [--runner-manager-url ]", "runner job --dry-run --run-id --command-id --image ", "secrets codex render --dry-run [--codex-home ] [--namespace agentrun-v01] [--secret-name agentrun-v01-provider-codex]", "backends list", "server start|status", ], }; } function print(value: JsonRecord): void { process.stdout.write(`${JSON.stringify(value)}\n`); }