import { spawn } from "node:child_process"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { type UniDeskConfig } from "./config"; import { type DebugDispatchCommand, isDebugDispatchCommand } from "./debug"; import { summarizeMicroserviceHealthResponse, summarizeMicroserviceObservation, summarizeMicroserviceProxyResponse } from "./microservices"; import { parseNetworkPerfOptions, runNetworkPerf } from "./network-perf"; import { buildWindowsPowerShellInvocation, formatSshFailureHint, formatSshRuntimeTimeoutHint, formatSshRuntimeTimingHint, normalizeSshOperationArgs, parseSshInvocation, remoteCommandForRoute, sshFailureHint, sshRouteSeparatorCompatibilityHint, sshRoutePayloadCwd, sshRuntimeTimeoutHint, sshRuntimeTimeoutMs, sshRuntimeTimingHint, wrapSshRemoteCommand, type SshCaptureResult, } from "./ssh"; import { isSshFileTransferOperation, runSshFileTransferOperation, type SshRemoteCommandExecutor } from "./ssh-file-transfer"; import { runApplyPatchV2, type ApplyPatchV2Executor } from "./apply-patch-v2"; import { codexJudgeQueryAsync, codexOutputQueryAsync, codexPrPreflightQueryAsync, codexQueuesQueryAsync, codexTaskQueryAsync, codexTasksQueryAsync, codexUnreadTriageAsync } from "./code-queue"; import { runDecisionCenterCommandAsync } from "./decision-center"; import { artifactRegistryReadonlyResultFromCommand, buildArtifactRegistryReadonlyProbe, parseArtifactRegistryOptions, } from "./artifact-registry"; import { runCiPublishBackendCoreDryRunPreflight, runCiPublishUserServiceDryRunPreflight } from "./ci"; export interface RemoteCliOptions { host: string | null; user: string; port: number; projectRoot: string; identityFile: string | null; transport: "auto" | "frontend" | "ssh"; args: string[]; } export type RemoteFailureClassification = "auth-missing" | "remote-proxy-missing" | "provider-unreachable" | "local-docker-required"; export interface AutoRemoteCiPublishPlan { enabled: boolean; host: string | null; reason: string; failureClassification: RemoteFailureClassification | null; transport: "frontend"; command: string | null; } interface FrontendSession { baseUrl: string; cookie: string; sshClientToken: string | null; } interface FetchJsonResult { ok: boolean; status?: number; body?: unknown; error?: string; responseHeaders?: Record; responseTruncated?: boolean; responseBytesRead?: number; responseContentLength?: string | null; } class RemoteCliFailure extends Error { readonly failureClassification: RemoteFailureClassification; readonly runnerDisposition = "infra-blocked"; readonly detail: unknown; constructor(failureClassification: RemoteFailureClassification, message: string, detail?: unknown) { super(message); this.name = "RemoteCliFailure"; this.failureClassification = failureClassification; this.detail = detail ?? null; } } const hostOptions = new Set(["--main-server-ip", "--main-server", "--server"]); const userOptions = new Set(["--main-server-user", "--server-user"]); const portOptions = new Set(["--main-server-port", "--server-port"]); const rootOptions = new Set(["--main-server-root", "--server-root"]); const keyOptions = new Set(["--main-server-key", "--server-key"]); const transportOptions = new Set(["--main-server-transport", "--server-transport"]); const remoteSshInputChunkBytes = 32 * 1024; function positivePort(raw: string, option: string): number { const value = Number(raw); if (!Number.isInteger(value) || value <= 0 || value > 65535) throw new Error(`${option} must be a TCP port from 1 to 65535`); return value; } function requiredValue(argv: string[], index: number, option: string): string { const value = argv[index + 1]; if (value === undefined || value.length === 0) throw new Error(`${option} requires a non-empty value`); return value; } function transportValue(raw: string, option: string): RemoteCliOptions["transport"] { if (raw === "auto" || raw === "frontend" || raw === "ssh") return raw; throw new Error(`${option} must be one of: auto, frontend, ssh`); } function truthyDisabled(raw: string | undefined): boolean { if (raw === undefined) return false; const normalized = raw.trim().toLowerCase(); return normalized === "0" || normalized === "false" || normalized === "off" || normalized === "disabled"; } function normalizeRemoteHostHint(raw: string | undefined): string | null { const value = raw?.trim() ?? ""; if (value.length === 0) return null; if (value === "localhost" || value === "127.0.0.1" || value === "::1") return null; return value.replace(/\/+$/u, ""); } function remoteHttpClientMode(env: NodeJS.ProcessEnv = process.env): "curl" | "fetch" { const explicit = env.UNIDESK_REMOTE_HTTP_CLIENT?.trim().toLowerCase(); if (explicit === "fetch") return "fetch"; if (explicit === "curl") return "curl"; return isCodeQueueRunnerEnv(env) ? "curl" : "fetch"; } function isCodeQueueRunnerEnv(env: NodeJS.ProcessEnv): boolean { return Boolean(env.CODE_QUEUE_SERVICE_ROLE || env.CODE_QUEUE_INSTANCE_ID || env.CODE_QUEUE_DEV_CONTAINER_MASTER_HOST || env.KUBERNETES_SERVICE_HOST); } export function autoRemoteCiPublishUserServiceDryRunPlan( config: UniDeskConfig, args: string[], env: NodeJS.ProcessEnv = process.env, ): AutoRemoteCiPublishPlan { const [top, sub] = args; const isPublishDryRun = top === "ci" && (sub === "publish-user-service" || sub === "publish-backend-core") && args.includes("--dry-run"); if (!isPublishDryRun) { return { enabled: false, host: null, reason: "not ci publish-user-service/publish-backend-core --dry-run", failureClassification: null, transport: "frontend", command: null }; } if (truthyDisabled(env.UNIDESK_CI_PUBLISH_AUTO_REMOTE)) { return { enabled: false, host: null, reason: "UNIDESK_CI_PUBLISH_AUTO_REMOTE disables automatic remote preflight", failureClassification: "local-docker-required", transport: "frontend", command: null }; } const explicitHost = normalizeRemoteHostHint(env.CODE_QUEUE_DEV_CONTAINER_MASTER_HOST) ?? normalizeRemoteHostHint(env.UNIDESK_MAIN_SERVER_IP) ?? normalizeRemoteHostHint(env.UNIDESK_MAIN_SERVER_HOST); const host = explicitHost ?? (isCodeQueueRunnerEnv(env) ? normalizeRemoteHostHint(config.network.publicHost) : null); if (host === null) { return { enabled: false, host: null, reason: "no remote main-server frontend host was detected for this runner", failureClassification: "local-docker-required", transport: "frontend", command: null }; } return { enabled: true, host, reason: explicitHost === null ? "Code Queue runner environment detected; using config.network.publicHost as the frontend control-plane" : "runner remote main-server frontend host detected", failureClassification: null, transport: "frontend", command: ["bun", "scripts/cli.ts", "--main-server-ip", host, ...args].join(" "), }; } export function extractRemoteCliOptions(argv: string[]): RemoteCliOptions { const rest: string[] = []; const options: RemoteCliOptions = { host: null, user: "root", port: 22, projectRoot: "/root/unidesk", identityFile: null, transport: "auto", args: rest, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index] ?? ""; if (arg === "--") { if (rest.length === 0) { rest.push(...argv.slice(index + 1)); break; } rest.push(arg); continue; } if (hostOptions.has(arg)) { options.host = requiredValue(argv, index, arg); index += 1; continue; } if (userOptions.has(arg)) { options.user = requiredValue(argv, index, arg); index += 1; continue; } if (portOptions.has(arg)) { options.port = positivePort(requiredValue(argv, index, arg), arg); index += 1; continue; } if (rootOptions.has(arg)) { options.projectRoot = requiredValue(argv, index, arg); index += 1; continue; } if (keyOptions.has(arg)) { options.identityFile = requiredValue(argv, index, arg); index += 1; continue; } if (transportOptions.has(arg)) { options.transport = transportValue(requiredValue(argv, index, arg), arg); index += 1; continue; } rest.push(arg); } return options; } function shellQuote(value: string): string { return `'${value.replace(/'/g, `'\\''`)}'`; } async function runRemoteCliOverSsh(options: RemoteCliOptions): Promise { if (options.host === null) throw new Error("runRemoteCli requires --main-server-ip or --server"); const remoteArgs = options.args.length === 0 ? ["help"] : options.args; const remoteCommand = [ "cd", shellQuote(options.projectRoot), "&&", "exec", "bun", "scripts/cli.ts", ...remoteArgs.map(shellQuote), ].join(" "); const sshArgs = [ "-p", String(options.port), "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-o", "ServerAliveInterval=20", "-o", "ServerAliveCountMax=3", ]; if (options.identityFile !== null) sshArgs.push("-i", options.identityFile); sshArgs.push(`${options.user}@${options.host}`, remoteCommand); const child = spawn("ssh", sshArgs, { stdio: ["inherit", "inherit", "inherit"], }); return await new Promise((resolve) => { child.on("error", (error) => { process.stderr.write(`unidesk remote cli failed to start ssh: ${error.message}\n`); resolve(127); }); child.on("close", (code) => resolve(code ?? 255)); }); } function emitRemoteJson(command: string, data: unknown, ok = true): void { process.stdout.write(`${JSON.stringify({ ok, command, data }, null, 2)}\n`); } function emitRemoteError(command: string, error: unknown): void { const payload = error instanceof Error ? { name: error.name, message: error.message, stack: error.stack ?? null } : { message: String(error) }; const classification = error instanceof RemoteCliFailure ? { failureClassification: error.failureClassification, runnerDisposition: error.runnerDisposition, retryable: error.failureClassification !== "auth-missing", detail: error.detail, recoveryHints: remoteFailureRecoveryHints(error.failureClassification), } : null; process.stdout.write(`${JSON.stringify({ ok: false, command, ...(classification === null ? {} : classification), error: payload, }, null, 2)}\n`); } function remoteFailureRecoveryHints(classification: RemoteFailureClassification): string[] { if (classification === "auth-missing") { return [ "verify config.json frontend auth.username/auth.password against the remote main-server frontend login", "retry the same command through --main-server-ip after credentials are restored", ]; } if (classification === "remote-proxy-missing") { return [ "verify the remote frontend is reachable on the configured public frontend port", "verify frontend can proxy /api to backend-core before retrying artifact publish preflight", ]; } if (classification === "provider-unreachable") { return [ "verify backend-core /api/dispatch can create D601 host.ssh tasks", "verify the D601 provider-gateway host.ssh capability and artifact registry health", ]; } return [ "run this read-only preflight with --main-server-ip from runner environments without local backend-core/database containers", ]; } function commandName(args: string[]): string { return args.join(" ") || "help"; } function frontendBaseUrl(host: string, config: UniDeskConfig): string { if (host.startsWith("http://") || host.startsWith("https://")) return host.replace(/\/+$/, ""); if (/:\d+$/.test(host)) return `http://${host}`; return `http://${host}:${config.network.frontend.port}`; } async function readJson(url: string, init?: RequestInit, timeoutMs = 8000, maxResponseBytes = 5_000_000): Promise { if (remoteHttpClientMode() === "curl") return readJsonWithCurl(url, init, timeoutMs, maxResponseBytes); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetch(url, { ...init, signal: controller.signal }); const reader = res.body?.getReader(); const chunks: Uint8Array[] = []; let bytes = 0; let responseTruncated = false; if (reader !== undefined) { while (true) { const { done, value } = await reader.read(); if (done) break; if (bytes + value.byteLength > maxResponseBytes) { const keep = Math.max(0, maxResponseBytes - bytes); if (keep > 0) { chunks.push(value.slice(0, keep)); bytes += keep; } responseTruncated = true; try { await reader.cancel(); } catch { // Ignore cancel failures after the bounded preview has been collected. } break; } chunks.push(value); bytes += value.byteLength; } } const buffer = new Uint8Array(bytes); let offset = 0; for (const chunk of chunks) { buffer.set(chunk, offset); offset += chunk.byteLength; } const text = new TextDecoder().decode(buffer); let body: unknown = null; try { body = text.length > 0 && !responseTruncated ? JSON.parse(text) : null; } catch { body = { text }; } if (responseTruncated) { body = { _unideskResponseTruncated: true, maxResponseBytes, bytesRead: bytes, contentLength: res.headers.get("content-length"), textPreview: text }; } return { ok: res.ok, status: res.status, body, responseHeaders: responseHeadersRecord(res.headers), responseTruncated, responseBytesRead: bytes, responseContentLength: res.headers.get("content-length") }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) }; } finally { clearTimeout(timer); } } function responseHeadersRecord(headers: Headers): Record { const record: Record = {}; headers.forEach((value, key) => { record[key.toLowerCase()] = value; }); return record; } function requestHeaders(init?: RequestInit): Array<[string, string]> { const headers = new Headers(init?.headers); const output: Array<[string, string]> = []; headers.forEach((value, key) => output.push([key, value])); return output; } async function runCurl(args: string[], timeoutMs: number): Promise<{ status: number | null; stdout: string; stderr: string; timedOut: boolean; error?: string }> { const child = spawn("curl", args, { stdio: ["ignore", "pipe", "pipe"] }); const stdoutChunks: Buffer[] = []; const stderrChunks: Buffer[] = []; let timedOut = false; const timer = setTimeout(() => { timedOut = true; child.kill("SIGTERM"); }, timeoutMs + 1000); child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk))); child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk))); return await new Promise((resolve) => { child.on("error", (error) => { clearTimeout(timer); resolve({ status: null, stdout: "", stderr: "", timedOut, error: error.message }); }); child.on("close", (status) => { clearTimeout(timer); resolve({ status, stdout: Buffer.concat(stdoutChunks).toString("utf8"), stderr: Buffer.concat(stderrChunks).toString("utf8"), timedOut, }); }); }); } function parseCurlResponseHeaders(raw: string): Record { const blocks = raw.split(/\r?\n\r?\n/u).map((block) => block.trim()).filter(Boolean); const latest = blocks.at(-1) ?? ""; const headers: Record = {}; for (const line of latest.split(/\r?\n/u).slice(1)) { const splitAt = line.indexOf(":"); if (splitAt <= 0) continue; headers[line.slice(0, splitAt).trim().toLowerCase()] = line.slice(splitAt + 1).trim(); } return headers; } function decodeBoundedBody(buffer: Buffer, maxResponseBytes: number): { text: string; truncated: boolean; bytesRead: number } { if (buffer.byteLength <= maxResponseBytes) return { text: buffer.toString("utf8"), truncated: false, bytesRead: buffer.byteLength }; return { text: buffer.subarray(0, maxResponseBytes).toString("utf8"), truncated: true, bytesRead: maxResponseBytes }; } async function readJsonWithCurl(url: string, init?: RequestInit, timeoutMs = 8000, maxResponseBytes = 5_000_000): Promise { const dir = await mkdtemp(path.join(tmpdir(), "unidesk-remote-http-")); const headersFile = path.join(dir, "headers.txt"); const bodyFile = path.join(dir, "body.bin"); const requestBodyFile = path.join(dir, "request-body.bin"); try { const method = init?.method ?? (init?.body === undefined ? "GET" : "POST"); const args = [ "-sS", "--max-time", String(Math.max(1, Math.ceil(timeoutMs / 1000))), "-D", headersFile, "-o", bodyFile, "-w", "%{http_code}", "-X", method, ]; for (const [key, value] of requestHeaders(init)) args.push("-H", `${key}: ${value}`); if (init?.body !== undefined) { await writeFile(requestBodyFile, typeof init.body === "string" ? init.body : String(init.body)); args.push("--data-binary", `@${requestBodyFile}`); } args.push(url); const curl = await runCurl(args, timeoutMs); if (curl.status !== 0) { const curlError = curl.error ?? curl.stderr.trim(); return { ok: false, status: curl.stdout.trim().match(/^\d{3}$/u) ? Number(curl.stdout.trim()) : undefined, error: curl.timedOut ? `curl timed out after ${timeoutMs}ms` : (curlError.length > 0 ? curlError : `curl exited with ${curl.status}`), }; } const status = Number(curl.stdout.trim()); const headersText = await readFile(headersFile, "utf8").catch(() => ""); const responseHeaders = parseCurlResponseHeaders(headersText); const bodyBuffer = await readFile(bodyFile).catch(() => Buffer.alloc(0)); const decoded = decodeBoundedBody(bodyBuffer, maxResponseBytes); let body: unknown = null; try { body = decoded.text.length > 0 && !decoded.truncated ? JSON.parse(decoded.text) : null; } catch { body = { text: decoded.text }; } if (decoded.truncated) { body = { _unideskResponseTruncated: true, maxResponseBytes, bytesRead: decoded.bytesRead, contentLength: responseHeaders["content-length"] ?? null, textPreview: decoded.text, }; } return { ok: status >= 200 && status < 300, status, body, responseHeaders, responseTruncated: decoded.truncated, responseBytesRead: decoded.bytesRead, responseContentLength: responseHeaders["content-length"] ?? null, }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) }; } finally { await rm(dir, { recursive: true, force: true }).catch(() => undefined); } } async function loginFrontend(host: string, config: UniDeskConfig): Promise { const baseUrl = frontendBaseUrl(host, config); const res = await readJson(`${baseUrl}/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ username: config.auth.username, password: config.auth.password }), }, 8_000, 120_000); if (!res.ok) { const failureClassification: RemoteFailureClassification = res.status === 401 || res.status === 403 ? "auth-missing" : "remote-proxy-missing"; throw new RemoteCliFailure(failureClassification, `frontend login failed via ${baseUrl}: status=${res.status ?? "unknown"} body=${JSON.stringify(res.body ?? res.error).slice(0, 300)}`, { baseUrl, status: res.status ?? null }); } const cookie = res.responseHeaders?.["set-cookie"]?.split(";")[0] ?? ""; if (cookie.length === 0) throw new RemoteCliFailure("auth-missing", `frontend login via ${baseUrl} did not return a session cookie`, { baseUrl, status: res.status ?? null }); return { baseUrl, cookie, sshClientToken: null }; } function sshClientTokenFromEnv(env: NodeJS.ProcessEnv = process.env): string | null { const token = env.UNIDESK_SSH_CLIENT_TOKEN?.trim() ?? ""; return token.length > 0 ? token : null; } function scopedSshFrontendSession(host: string, config: UniDeskConfig, token: string): FrontendSession { return { baseUrl: frontendBaseUrl(host, config), cookie: "", sshClientToken: token }; } async function frontendJson(session: FrontendSession, path: string, init?: RequestInit, timeoutMs = 8000, maxResponseBytes = 5_000_000): Promise { const headers = new Headers(init?.headers); headers.set("cookie", session.cookie); if (init?.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json"); return readJson(`${session.baseUrl}${path}`, { ...init, headers }, timeoutMs, maxResponseBytes); } function stringOption(args: string[], 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 numberOption(args: string[], name: string, defaultValue: number): number { const raw = stringOption(args, name); if (raw === undefined) return defaultValue; const value = Number(raw); if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`); return value; } function cappedNumberOption(args: string[], name: string, defaultValue: number, maxValue: number): number { return Math.min(numberOption(args, name, defaultValue), maxValue); } function jsonOption(args: string[], name: string): Record | undefined { const raw = stringOption(args, 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(args: string[], command: DebugDispatchCommand): Record { const explicit = jsonOption(args, "--payload-json") ?? {}; if (command === "provider.upgrade") { return { source: "cli-remote", mode: stringOption(args, "--mode") ?? stringOption(args, "--upgrade-mode") ?? "plan", ...explicit }; } if (command === "host.ssh") { const sshCommand = stringOption(args, "--ssh-command"); return { source: "cli-remote", mode: sshCommand === undefined ? "probe" : "exec", ...(sshCommand === undefined ? {} : { command: sshCommand }), ...(stringOption(args, "--cwd") === undefined ? {} : { cwd: stringOption(args, "--cwd") }), ...(args.includes("--timeout-ms") ? { timeoutMs: numberOption(args, "--timeout-ms", 8000) } : {}), ...explicit, }; } return { source: "cli-remote", ...explicit }; } function summarizeSystemStatus(response: FetchJsonResult): FetchJsonResult { const body = response.body as { systemStatuses?: Array>; ok?: boolean } | null; const systemStatuses = (body?.systemStatuses ?? []).map((item) => { const current = (item.current ?? {}) as Record; const history = Array.isArray(item.history) ? item.history : []; return { providerId: item.providerId, name: item.name, nodeStatus: item.nodeStatus, updatedAt: item.updatedAt, current: item.current === null || item.current === undefined ? null : { ok: current.ok, collectedAt: current.collectedAt, cpu: current.cpu, memory: current.memory, disk: current.disk, }, historyPreview: history.slice(-8), historyCount: history.length, }; }); return { ...response, body: { ok: body?.ok === true, systemStatuses } }; } function summarizeDockerStatus(response: FetchJsonResult): FetchJsonResult { const body = response.body as { dockerStatuses?: Array>; ok?: boolean } | null; const dockerStatuses = (body?.dockerStatuses ?? []).map((item) => { const status = (item.dockerStatus ?? {}) as Record; const containers = Array.isArray(status.containers) ? status.containers : []; return { providerId: item.providerId, name: item.name, nodeStatus: item.nodeStatus, updatedAt: item.updatedAt, dockerStatus: { ok: status.ok, socketPresent: status.socketPresent, collectedAt: status.collectedAt, counts: status.counts, daemon: status.daemon, containersPreview: containers.slice(0, 8), }, }; }); return { ...response, body: { ok: body?.ok === true, dockerStatuses } }; } async function waitForFrontendTask(session: FrontendSession, taskId: string, timeoutMs: number): Promise { const started = Date.now(); let latest: unknown = null; while (Date.now() - started < timeoutMs) { latest = await frontendJson(session, `/api/tasks/${encodeURIComponent(taskId)}`, undefined, 8000, 5_000_000); const body = latest as { body?: { task?: { id?: string; status?: string; result?: unknown } } }; const task = body.body?.task; if (task?.status === "succeeded" || task?.status === "failed") return { ok: true, task }; await Bun.sleep(500); } return { ok: false, timeoutMs, latest }; } async function remoteHealth(session: FrontendSession, config: UniDeskConfig): Promise { return { transport: "frontend", frontendPublic: await readJson(`${session.baseUrl}/health`), providerIngressPublic: await readJson(`http://${new URL(session.baseUrl).hostname}:${config.network.providerIngress.port}/health`), overviewInternal: await frontendJson(session, "/api/overview"), nodesInternal: await frontendJson(session, "/api/nodes"), systemStatusInternal: summarizeSystemStatus(await frontendJson(session, "/api/nodes/system-status?limit=24")), dockerStatusInternal: summarizeDockerStatus(await frontendJson(session, "/api/nodes/docker-status")), publicExposureBoundary: { coreHostPort: { port: config.network.core.port, expected: "not-exposed" }, databaseHostPort: { port: config.network.database.port, expected: "restricted-to-code-queue-provider" }, oaEventFlowHostPort: { port: 4255, expected: "restricted-to-code-queue-provider" }, }, }; } async function remoteDebugDispatch(session: FrontendSession, config: UniDeskConfig, args: string[]): Promise { const third = args[2]; const fourth = args[3]; const providerId = isDebugDispatchCommand(third) ? config.providerGateway.id : third ?? config.providerGateway.id; const commandArg = isDebugDispatchCommand(third) ? third : fourth; const dispatchCommand = isDebugDispatchCommand(commandArg) ? commandArg : "docker.ps"; const dispatch = await frontendJson(session, "/api/dispatch", { method: "POST", body: JSON.stringify({ providerId, command: dispatchCommand, payload: dispatchPayload(args, dispatchCommand) }), }); const taskId = (dispatch as { body?: { taskId?: string } }).body?.taskId ?? ""; const waitMs = numberOption(args, "--wait-ms", 0); const wait = waitMs > 0 && taskId.length > 0 ? await waitForFrontendTask(session, taskId, waitMs) : null; return { transport: "frontend", dispatch, wait }; } async function remoteDebugTask(session: FrontendSession, args: string[]): Promise { const taskId = args[2] ?? "latest"; const tasksResponse = await frontendJson(session, "/api/tasks?limit=100"); const tasks = (tasksResponse as { body?: { tasks?: Array<{ id?: string }> } }).body?.tasks ?? []; const task = taskId === "latest" ? tasks[0] : tasks.find((item) => item.id === taskId); return { transport: "frontend", tasksResponse, taskId, task: task ?? null }; } export function summarizeRemoteMicroserviceResponse(action: string, id: string, response: unknown, args: string[]): unknown { const optionArgs = args.slice(3); if (action === "health") return summarizeMicroserviceHealthResponse(response, optionArgs, id); if (action === "status" || action === "diagnostics") return summarizeMicroserviceObservation(action, id, response, optionArgs); return response; } async function remoteMicroservice(session: FrontendSession, args: string[]): Promise { const action = args[1] ?? "list"; const id = args[2]; const path = args[3]; if (action === "list") { return { transport: "frontend", response: await frontendJson(session, "/api/microservices", undefined, 12_000) }; } if ((action === "status" || action === "health" || action === "diagnostics" || action === "tunnel-self-test") && id !== undefined) { const response = await frontendJson(session, `/api/microservices/${encodeURIComponent(id)}/${action}`, undefined, 18_000); return { transport: "frontend", response: summarizeRemoteMicroserviceResponse(action, id, response, args), }; } if (action === "proxy" && id !== undefined && path !== undefined && path.startsWith("/")) { const full = args.includes("--full"); const raw = args.includes("--raw"); const maxBodyBytes = full ? numberOption(args, "--max-body-bytes", 5_000_000) : cappedNumberOption(args, "--max-body-bytes", raw ? 120_000 : 60_000, 500_000); const maxResponseBytes = full ? Math.min(Math.max(maxBodyBytes, 120_000), 5_000_000) : Math.min(Math.max(maxBodyBytes * 3, 240_000), 1_500_000); const response = await frontendJson(session, `/api/microservices/${encodeURIComponent(id)}/proxy${path}`, undefined, 24_000, maxResponseBytes); return { transport: "frontend", response: summarizeMicroserviceProxyResponse(response, args), }; } throw new Error("remote microservice command must be: microservice list | status | health | diagnostics | tunnel-self-test | proxy "); } function commandResultFromFrontendTask(command: string[], task: { status?: string; result?: Record } | undefined) { const result = task?.result ?? {}; const stdout = typeof result.stdout === "string" ? result.stdout : ""; const stderr = typeof result.stderr === "string" ? result.stderr : ""; return { command, cwd: ".", exitCode: typeof result.exitCode === "number" ? result.exitCode : null, stdout, stderr, signal: null, timedOut: result.timedOut === true || (task?.status !== "succeeded" && /timeout|timed out/iu.test(stderr)), }; } async function dispatchHostSshJson( session: FrontendSession, providerId: string, command: string, timeoutMs: number, waitMs = Math.max(timeoutMs + 5000, 20_000), ): Promise<{ dispatch: FetchJsonResult; wait: unknown; task?: { status?: string; result?: Record }; taskId: string | null }> { const dispatch = await frontendJson(session, "/api/dispatch", { method: "POST", body: JSON.stringify({ providerId, command: "host.ssh", payload: { source: "cli-remote-artifact-registry", mode: "exec", command, timeoutMs }, }), }); const taskId = (dispatch as { body?: { taskId?: string } }).body?.taskId ?? ""; const wait = taskId.length > 0 ? await waitForFrontendTask(session, taskId, waitMs) : null; const task = (wait as { task?: { status?: string; result?: Record } } | null)?.task; return { dispatch, wait, task, taskId: taskId.length > 0 ? taskId : null }; } async function remoteArtifactRegistry(session: FrontendSession, args: string[]): Promise { const action = args[1] ?? "status"; if (action !== "status" && action !== "health") { throw new Error("remote frontend transport supports artifact-registry status|health only; use main-server SSH for mutating install/deploy commands"); } const options = parseArtifactRegistryOptions(args.slice(2)); const probe = buildArtifactRegistryReadonlyProbe(action, options); const dispatched = await dispatchHostSshJson(session, probe.providerId, probe.script, probe.timeoutMs); const command = ["frontend", "/api/dispatch", probe.providerId, "host.ssh", action]; const result = commandResultFromFrontendTask(command, dispatched.task); const registryResult = artifactRegistryReadonlyResultFromCommand(probe, result); const dispatchBody = dispatched.dispatch.body as Record | null | undefined; const dispatchError = typeof dispatchBody?.error === "string" ? dispatchBody.error : null; return { transport: "frontend", readonly: true, dispatch: dispatched.dispatch, wait: dispatched.wait, result: dispatched.taskId === null ? { ok: false, readonly: true, installed: false, healthy: false, decision: "infra-blocked", retryable: true, runnerDisposition: "infra-blocked", failureClassification: "control-plane-missing", recommendedAction: "restore the remote frontend/backend-core dispatch control plane, then rerun artifact-registry status|health", healthyScopes: [], failedScopes: ["control-plane-missing", "backend-core-api"], runtimeApiHealthy: false, channels: [ { channel: "backend-core-api", ok: false, requiredFor: "frontend /api/dispatch backend-core session creation", detail: dispatched.dispatch }, { channel: "provider-dispatch", ok: false, requiredFor: "host.ssh task creation", detail: dispatched.dispatch }, ], controlPlane: { transport: "remote-frontend", failureClassification: "control-plane-missing", dispatchStatus: dispatched.dispatch.status ?? null, dispatchError, }, registry: registryResult, } : registryResult, }; } async function remoteCi(session: FrontendSession, config: UniDeskConfig, args: string[]): Promise { const action = args[1] ?? "status"; if ((action !== "publish-user-service" && action !== "publish-backend-core") || !args.includes("--dry-run")) { throw new Error("remote frontend transport supports only ci publish-user-service --dry-run and ci publish-backend-core --dry-run preflight; real CI publication must run from the controlled main-server CLI after preflight is ready"); } const transport = { kind: "remote-frontend" as const, remoteHost: session.baseUrl, coreFetch: (path: string, init?: { method?: string; body?: unknown; maxResponseBytes?: number }) => frontendJson(session, path, init === undefined ? undefined : { method: init.method, body: init.body === undefined ? undefined : JSON.stringify(init.body), }, 12_000, init?.maxResponseBytes ?? 500_000), dispatchHostSsh: async (command: string, waitMs: number, remoteTimeoutMs: number) => { const dispatched = await dispatchHostSshJson(session, "D601", command, remoteTimeoutMs, waitMs); const task = dispatched.task; const result = task?.result ?? {}; return { ok: task?.status === "succeeded" && (typeof result.exitCode !== "number" || result.exitCode === 0) && dispatched.taskId !== null, taskId: dispatched.taskId, status: task?.status ?? null, stdout: typeof result.stdout === "string" ? result.stdout : "", stderr: typeof result.stderr === "string" ? result.stderr : "", exitCode: typeof result.exitCode === "number" ? result.exitCode : null, raw: task ?? dispatched.wait ?? dispatched.dispatch, }; }, commandCwd: ".", artifactRegistryCommand: (probe: ReturnType) => ["frontend", "/api/dispatch", probe.providerId, "host.ssh", probe.action, dispatchedTaskShape(probe.remoteCommandShape)], }; return { transport: "frontend", readonly: true, result: action === "publish-backend-core" ? await runCiPublishBackendCoreDryRunPreflight(config, args.slice(1), transport) : await runCiPublishUserServiceDryRunPreflight(config, args.slice(1), transport), }; } function dispatchedTaskShape(remoteCommandShape: string): string { return remoteCommandShape.length > 0 ? remoteCommandShape : "host.ssh artifact-registry readonly probe"; } async function remoteCodeQueue(session: FrontendSession, args: string[]): Promise { const action = args[1] ?? "task"; if (action !== "task" && action !== "summary" && action !== "show" && action !== "tasks" && action !== "overview" && action !== "unread" && action !== "terminal-unread" && action !== "queues" && action !== "queue-list" && action !== "output" && action !== "judge" && action !== "pr-preflight" && action !== "runtime-preflight") { throw new Error("remote codex command must be: codex task , codex tasks, codex unread, codex queues, codex output , codex judge --attempt N, or codex pr-preflight [--remote]"); } const taskId = args[2]; if ((action === "task" || action === "summary" || action === "show" || action === "output" || action === "judge") && (taskId === undefined || taskId.length === 0)) { throw new Error(`codex ${action} requires task id`); } const requiredTaskId = taskId ?? ""; const fetcher = (path: string, init?: { method?: string; body?: unknown }): Promise => { const requestInit = init === undefined ? undefined : { method: init.method, body: init.body === undefined ? undefined : JSON.stringify(init.body), }; return frontendJson(session, path, requestInit, action === "judge" ? 130_000 : 24_000); }; return { transport: "frontend", result: action === "tasks" || action === "overview" ? await codexTasksQueryAsync(args.slice(1), fetcher) : action === "unread" || action === "terminal-unread" ? await codexUnreadTriageAsync(args.slice(2), fetcher) : action === "queues" || action === "queue-list" ? await codexQueuesQueryAsync(args.slice(2), fetcher) : action === "output" ? await codexOutputQueryAsync(requiredTaskId, args.slice(3), fetcher) : action === "pr-preflight" || action === "runtime-preflight" ? await codexPrPreflightQueryAsync(args.slice(1), fetcher) : action === "judge" ? await codexJudgeQueryAsync(requiredTaskId, args.slice(3), fetcher) : await codexTaskQueryAsync(requiredTaskId, args.slice(3), fetcher), }; } async function remoteNetworkPerf(options: RemoteCliOptions, config: UniDeskConfig, args: string[]): Promise { if (options.host === null) throw new Error("network perf requires --main-server-ip when using remote frontend transport"); return { transport: "frontend", result: await runNetworkPerf(parseNetworkPerfOptions(config, args.slice(2), options.host)), }; } function frontendSshWebSocketUrl(session: FrontendSession): string { const url = new URL("/ws/ssh", session.baseUrl); url.protocol = url.protocol === "https:" ? "wss:" : "ws:"; return url.toString(); } function webSocketDataText(data: unknown): string { if (typeof data === "string") return data; if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8"); if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8"); return String(data); } function openFrontendSshWebSocket(session: FrontendSession): WebSocket { const WebSocketWithHeaders = WebSocket as unknown as new ( url: string, options?: { headers?: Record }, ) => WebSocket; const headers = session.sshClientToken === null ? { cookie: session.cookie } : { authorization: `Bearer ${session.sshClientToken}` }; return new WebSocketWithHeaders(frontendSshWebSocketUrl(session), { headers }); } async function runRemoteSshWebSocket( session: FrontendSession, invocation: ReturnType, ): Promise { const parsed = invocation.parsed; const startedAtMs = Date.now(); const size = { cols: Number(process.stdout.columns) > 0 ? Number(process.stdout.columns) : 100, rows: Number(process.stdout.rows) > 0 ? Number(process.stdout.rows) : 30, }; const openTimeoutMs = Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000)); const runtimeTimeoutMs = sshRuntimeTimeoutMs(); const payload = { providerId: invocation.providerId, command: wrapSshRemoteCommand(parsed.remoteCommand, parsed.requiredHelpers), cwd: sshRoutePayloadCwd(invocation.route), tty: parsed.remoteCommand === null, stdinEotOnEnd: parsed.remoteCommand !== null, openTimeoutMs, runtimeTimeoutMs, cols: size.cols, rows: size.rows, }; const ws = openFrontendSshWebSocket(session); let exitCode = 255; let settled = false; let canSend = false; let sessionReady = false; const pending: string[] = []; const pendingSessionMessages: string[] = []; const send = (value: unknown): void => { const text = JSON.stringify(value); if (!canSend || ws.readyState !== WebSocket.OPEN) { pending.push(text); return; } ws.send(text); }; const flush = (): void => { while (pending.length > 0 && ws.readyState === WebSocket.OPEN) ws.send(pending.shift()!); }; const sendInput = (value: Buffer | string): void => { sendWhenSessionReady({ type: "ssh.input", data: Buffer.from(value).toString("base64"), encoding: "base64" }); }; const sendInputChunked = (value: string): void => { const buffer = Buffer.from(value, "utf8"); for (let offset = 0; offset < buffer.length; offset += remoteSshInputChunkBytes) { sendInput(buffer.subarray(offset, Math.min(buffer.length, offset + remoteSshInputChunkBytes))); } }; const sendWhenSessionReady = (value: unknown): void => { const text = JSON.stringify(value); if (!sessionReady || ws.readyState !== WebSocket.OPEN) { pendingSessionMessages.push(text); return; } ws.send(text); }; const flushSessionMessages = (): void => { if (!sessionReady || ws.readyState !== WebSocket.OPEN) return; while (pendingSessionMessages.length > 0) ws.send(pendingSessionMessages.shift()!); }; return await new Promise((resolve) => { const rawMode = parsed.remoteCommand === null && process.stdin.isTTY && typeof process.stdin.setRawMode === "function"; let timedOut = false; const openTimer = setTimeout(() => { if (sessionReady || settled) return; process.stderr.write("unidesk remote frontend ssh bridge timed out waiting for provider session\n"); exitCode = 255; try { ws.close(); } catch { // Ignore close failures while resolving the timeout path. } }, openTimeoutMs); const runtimeTimer = setTimeout(() => { if (settled) return; timedOut = true; exitCode = 124; process.stderr.write(formatSshRuntimeTimeoutHint(sshRuntimeTimeoutHint({ invocation, transport: "frontend-websocket", timeoutMs: runtimeTimeoutMs, }))); try { ws.close(); } catch { // Ignore close failures while resolving the timeout path. } finish(124); }, runtimeTimeoutMs); const restore = (): void => { clearTimeout(openTimer); clearTimeout(runtimeTimer); process.stdin.off("data", onStdinData); process.stdin.off("end", onStdinEnd); if (rawMode) process.stdin.setRawMode(false); }; const finish = (code: number): void => { if (settled) return; settled = true; restore(); const hint = timedOut ? null : sshFailureHint(invocation.providerId, parsed, code, ""); if (hint !== null) process.stderr.write(formatSshFailureHint(hint)); const timingHint = formatSshRuntimeTimingHint(sshRuntimeTimingHint({ invocation, transport: "frontend-websocket", exitCode: code, startedAtMs, })); if (timingHint) process.stderr.write(timingHint); resolve(code); }; const onStdinData = (chunk: Buffer): void => { sendInput(chunk); }; const onStdinEnd = (): void => { if (parsed.stdinSuffix) sendInput(parsed.stdinSuffix); if (payload.stdinEotOnEnd === true) sendInput(Buffer.from([4])); sendWhenSessionReady({ type: "ssh.eof" }); }; ws.addEventListener("open", () => { canSend = true; send({ type: "ssh.open", ...payload }); flush(); }); ws.addEventListener("message", (event) => { const text = webSocketDataText(event.data); let message: Record; try { message = JSON.parse(text) as Record; } catch { process.stderr.write(`${text}\n`); return; } if (message.type === "ssh.dispatched") return; if (message.type === "ssh.opened") { sessionReady = true; clearTimeout(openTimer); flushSessionMessages(); return; } if (message.type === "ssh.data") { const chunk = Buffer.from(String(message.data ?? ""), message.encoding === "base64" ? "base64" : "utf8"); if (message.stream === "stderr") process.stderr.write(chunk); else process.stdout.write(chunk); return; } if (message.type === "ssh.error") { process.stderr.write(`${String(message.message || "ssh bridge error")}\n`); exitCode = 255; ws.close(); return; } if (message.type === "ssh.exit") { exitCode = Number.isInteger(message.exitCode) ? Number(message.exitCode) : 255; ws.close(); } }); ws.addEventListener("close", () => finish(exitCode)); ws.addEventListener("error", () => { process.stderr.write("unidesk remote frontend ssh bridge websocket error\n"); finish(255); }); if (rawMode) process.stdin.setRawMode(true); process.stdin.resume(); if (parsed.stdinPrefix) sendInput(parsed.stdinPrefix); process.stdin.on("data", onStdinData); process.stdin.on("end", onStdinEnd); }); } async function runRemoteSshWebSocketCapture( session: FrontendSession, invocation: ReturnType, command: string[], input?: string, ): Promise { const remoteCommand = remoteCommandForRoute(invocation.route, command, { stdin: input !== undefined }); return await runRemoteSshWebSocketCaptureRemoteCommand(session, invocation, remoteCommand, input); } async function runRemoteSshWebSocketCaptureRemoteCommand( session: FrontendSession, invocation: ReturnType, remoteCommand: string, input?: string, ): Promise { const captureInvocation = { ...invocation, parsed: { ...invocation.parsed, remoteCommand, requiresStdin: input !== undefined, invocationKind: "helper" as const }, }; const startedAtMs = Date.now(); const size = { cols: Number(process.stdout.columns) > 0 ? Number(process.stdout.columns) : 100, rows: Number(process.stdout.rows) > 0 ? Number(process.stdout.rows) : 30, }; const runtimeTimeoutMs = sshRuntimeTimeoutMs(); const payload = { providerId: invocation.providerId, command: wrapSshRemoteCommand(remoteCommand), cwd: sshRoutePayloadCwd(invocation.route), tty: false, stdinEotOnEnd: false, openTimeoutMs: Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000)), runtimeTimeoutMs, cols: size.cols, rows: size.rows, }; const ws = openFrontendSshWebSocket(session); let exitCode = 255; let settled = false; let canSend = false; let sessionReady = false; let stdout = ""; let stderr = ""; const pending: string[] = []; const pendingSessionMessages: string[] = []; const send = (value: unknown): void => { const text = JSON.stringify(value); if (!canSend || ws.readyState !== WebSocket.OPEN) { pending.push(text); return; } ws.send(text); }; const sendWhenSessionReady = (value: unknown): void => { const text = JSON.stringify(value); if (!sessionReady || ws.readyState !== WebSocket.OPEN) { pendingSessionMessages.push(text); return; } ws.send(text); }; const sendInput = (value: Buffer | string): void => { sendWhenSessionReady({ type: "ssh.input", data: Buffer.from(value).toString("base64"), encoding: "base64" }); }; const flush = (): void => { while (pending.length > 0 && ws.readyState === WebSocket.OPEN) ws.send(pending.shift()!); }; const flushSessionMessages = (): void => { if (!sessionReady || ws.readyState !== WebSocket.OPEN) return; while (pendingSessionMessages.length > 0) ws.send(pendingSessionMessages.shift()!); }; return await new Promise((resolve) => { let killTimer: ReturnType | null = null; const finish = (code: number): void => { if (settled) return; settled = true; clearTimeout(openTimer); clearTimeout(runtimeTimer); if (killTimer !== null) clearTimeout(killTimer); const timingHint = formatSshRuntimeTimingHint(sshRuntimeTimingHint({ invocation: captureInvocation, transport: "frontend-websocket", exitCode: code, startedAtMs, })); if (timingHint) stderr += timingHint; resolve({ exitCode: code, stdout, stderr }); }; const openTimer = setTimeout(() => { if (sessionReady || settled) return; stderr += "unidesk remote frontend ssh bridge timed out waiting for provider session\n"; exitCode = 255; try { ws.close(); } catch { // Ignore. } }, payload.openTimeoutMs); const runtimeTimer = setTimeout(() => { if (settled) return; exitCode = 124; stderr += formatSshRuntimeTimeoutHint(sshRuntimeTimeoutHint({ invocation: captureInvocation, transport: "frontend-websocket", timeoutMs: runtimeTimeoutMs, })); try { ws.close(); } catch { // Ignore. } killTimer = setTimeout(() => finish(124), 2000); finish(124); }, runtimeTimeoutMs); ws.addEventListener("open", () => { canSend = true; send({ type: "ssh.open", ...payload }); flush(); }); ws.addEventListener("message", (event) => { const text = webSocketDataText(event.data); let message: Record; try { message = JSON.parse(text) as Record; } catch { stderr += `${text}\n`; return; } if (message.type === "ssh.dispatched") return; if (message.type === "ssh.opened") { sessionReady = true; clearTimeout(openTimer); if (input !== undefined) sendInputChunked(input); sendWhenSessionReady({ type: "ssh.eof" }); flushSessionMessages(); return; } if (message.type === "ssh.data") { const chunk = Buffer.from(String(message.data ?? ""), message.encoding === "base64" ? "base64" : "utf8").toString("utf8"); if (message.stream === "stderr") stderr += chunk; else stdout += chunk; return; } if (message.type === "ssh.error") { stderr += `${String(message.message || "ssh bridge error")}\n`; exitCode = 255; ws.close(); return; } if (message.type === "ssh.exit") { exitCode = Number.isInteger(message.exitCode) ? Number(message.exitCode) : 255; ws.close(); } }); ws.addEventListener("close", () => finish(exitCode)); ws.addEventListener("error", () => { stderr += "unidesk remote frontend ssh bridge websocket error\n"; finish(255); }); }); } export function remoteSshFrontendPlanForTest(target: string, args: string[]): Record { const invocation = parseSshInvocation(target, args); return { transport: "frontend-websocket", providerId: invocation.providerId, route: invocation.route, remoteCommand: invocation.parsed.remoteCommand, wrappedRemoteCommand: wrapSshRemoteCommand(invocation.parsed.remoteCommand, invocation.parsed.requiredHelpers), requiresStdin: invocation.parsed.requiresStdin, invocationKind: invocation.parsed.invocationKind, payloadCwd: sshRoutePayloadCwd(invocation.route) ?? null, }; } async function runRemoteSshOverFrontend(session: FrontendSession, target: string | undefined, args: string[]): Promise { if (!target) throw new Error("remote ssh requires a route, for example: bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh D601 hostname"); const normalizedArgs = normalizeSshOperationArgs(args); process.stderr.write(sshRouteSeparatorCompatibilityHint(args, normalizedArgs)); const invocation = parseSshInvocation(target, normalizedArgs); if (isSshFileTransferOperation(normalizedArgs)) { const executor: SshRemoteCommandExecutor = { runRemoteCommand: (remoteCommand, input) => runRemoteSshWebSocketCaptureRemoteCommand(session, invocation, remoteCommand, input), }; return await runSshFileTransferOperation(invocation, normalizedArgs, executor, { buildRouteCommand: remoteCommandForRoute, buildWindowsPowerShellCommand: buildWindowsPowerShellInvocation, }); } if ((normalizedArgs[0] ?? "") === "apply-patch") { const executor: ApplyPatchV2Executor = { run: (command, input) => runRemoteSshWebSocketCapture(session, invocation, command, input), }; return await runApplyPatchV2({ executor, stdin: process.stdin, stdout: process.stdout, stderr: process.stderr, argv: normalizedArgs.slice(1) }); } return runRemoteSshWebSocket(session, invocation); } async function runRemoteCliOverFrontend(options: RemoteCliOptions, config: UniDeskConfig): Promise { if (options.host === null) throw new Error("runRemoteCli requires --main-server-ip or --server"); const args = options.args.length === 0 ? ["help"] : options.args; const name = commandName(args); try { const [top, sub] = args; const scopedSshToken = top === "ssh" ? sshClientTokenFromEnv() : null; const session = scopedSshToken === null ? await loginFrontend(options.host, config) : scopedSshFrontendSession(options.host, config, scopedSshToken); if (top === "help" || top === "--help" || top === "-h") { emitRemoteJson(name, { transport: "frontend", baseUrl: session.baseUrl, commands: ["debug health", "debug dispatch", "debug task", "ssh ", "ssh skills", "artifact-registry status|health", "ci publish-user-service --dry-run", "ci publish-backend-core --dry-run", "microservice list", "microservice status ", "microservice health ", "microservice diagnostics ", "microservice tunnel-self-test ", "microservice proxy ", "decision upload ", "decision list", "decision show ", "codex task ", "codex tasks", "codex unread", "codex queues", "codex judge --attempt N", "codex pr-preflight [--remote]", "network perf"], }); return 0; } if (top === "debug" && sub === "health") { emitRemoteJson(name, await remoteHealth(session, config)); return 0; } if (top === "debug" && sub === "dispatch") { emitRemoteJson(name, await remoteDebugDispatch(session, config, args)); return 0; } if (top === "debug" && sub === "task") { emitRemoteJson(name, await remoteDebugTask(session, args)); return 0; } if (top === "microservice") { emitRemoteJson(name, await remoteMicroservice(session, args)); return 0; } if (top === "artifact-registry") { emitRemoteJson(name, await remoteArtifactRegistry(session, args)); return 0; } if (top === "ci") { emitRemoteJson(name, await remoteCi(session, config, args)); return 0; } if (top === "decision" || top === "decision-center") { const fetcher = (path: string, init?: { method?: string; body?: unknown }): Promise => { const requestInit = init === undefined ? undefined : { method: init.method, body: init.body === undefined ? undefined : JSON.stringify(init.body), }; return frontendJson(session, path, requestInit, 30_000); }; const result = await runDecisionCenterCommandAsync(config, args.slice(1), fetcher); const ok = typeof result !== "object" || result === null || !("ok" in result) || (result as { ok?: unknown }).ok !== false; emitRemoteJson(name, result, ok); return ok ? 0 : 1; } if (top === "codex") { emitRemoteJson(name, await remoteCodeQueue(session, args)); return 0; } if (top === "network" && sub === "perf") { emitRemoteJson(name, await remoteNetworkPerf(options, config, args)); return 0; } if (top === "ssh") { return await runRemoteSshOverFrontend(session, sub, args.slice(2)); } throw new Error(`remote frontend transport does not support command: ${name}`); } catch (error) { emitRemoteError(name, error); return 1; } } export async function runRemoteCli(options: RemoteCliOptions, config: UniDeskConfig): Promise { if (options.host === null) throw new Error("runRemoteCli requires --main-server-ip or --server"); const useSsh = options.transport === "ssh" || (options.transport === "auto" && options.identityFile !== null); if (useSsh) return runRemoteCliOverSsh(options); return runRemoteCliOverFrontend(options, config); }