import { runCommand } from "./command"; import { type UniDeskConfig, repoRoot } from "./config"; export const dispatchCommands = ["docker.ps", "provider.upgrade", "host.ssh", "microservice.http", "echo"] as const; export type DebugDispatchCommand = typeof dispatchCommands[number]; export function isDebugDispatchCommand(value: unknown): value is DebugDispatchCommand { return dispatchCommands.includes(value as DebugDispatchCommand); } async function readJson(url: string, init?: RequestInit): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 5000); try { const res = await fetch(url, { ...init, signal: controller.signal }); const text = await res.text(); return { ok: res.ok, status: res.status, body: text.length > 0 ? JSON.parse(text) : null }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) }; } finally { clearTimeout(timer); } } function coreFetchCommand(path: string, init?: { method?: string; body?: unknown }): string[] { const method = init?.method ?? "GET"; const url = `http://127.0.0.1:8080${path}`; const body = init?.body === undefined ? "" : JSON.stringify(init.body); const script = [ "set -euo pipefail", "if command -v backend-core >/dev/null 2>&1; then", ` exec backend-core --fetch-json ${shellQuote(url)} --method ${shellQuote(method)}${body.length > 0 ? ` --body-json ${shellQuote(body)}` : ""}`, "fi", `url=${shellQuote(url)}`, `method=${shellQuote(method)}`, `body=${shellQuote(body)}`, "export url method body", "bun -e 'const url=process.env.url; const method=process.env.method; const body=process.env.body; fetch(url,{method,body:body?body:undefined,headers:body?{\"content-type\":\"application/json\"}:undefined}).then(async r=>{const text=await r.text(); console.log(JSON.stringify({ok:r.ok,status:r.status,body:text?JSON.parse(text):null})); process.exit(r.ok?0:1);}).catch(e=>{console.error(e); process.exit(1);})'", ].join("\n"); return ["docker", "exec", "unidesk-backend-core", "sh", "-lc", script]; } function shellQuote(value: string): string { return `'${value.replace(/'/g, `'\\''`)}'`; } function coreInternalFetch(path: string, init?: { method?: string; body?: unknown }): unknown { const command = coreFetchCommand(path, init); const result = runCommand(command, repoRoot); if (result.exitCode !== 0) { return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) }; } try { return JSON.parse(result.stdout.trim()) as unknown; } catch { return { ok: true, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) }; } } function coreDockerStatusSummary(): unknown { const result = runCommand(coreFetchCommand("/api/nodes/docker-status"), repoRoot); if (result.exitCode !== 0) { return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) }; } try { const parsed = JSON.parse(result.stdout.trim()) as { ok?: boolean; status?: number; body?: { dockerStatuses?: Array> } }; const dockerStatuses = (parsed.body?.dockerStatuses || []).map((item) => { const status = (item.dockerStatus || {}) as Record; 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: Array.isArray(status.containers) ? status.containers.slice(0, 8) : [], }, }; }); return { ok: parsed.ok, status: parsed.status, body: { ok: parsed.body !== undefined, dockerStatuses } }; } catch { return { ok: true, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) }; } } function coreSystemStatusSummary(): unknown { const result = runCommand(coreFetchCommand("/api/nodes/system-status?limit=24"), repoRoot); if (result.exitCode !== 0) { return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) }; } try { const parsed = JSON.parse(result.stdout.trim()) as { ok?: boolean; status?: number; body?: { systemStatuses?: Array> } }; const systemStatuses = (parsed.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 ? { ok: current.ok, collectedAt: current.collectedAt, cpu: current.cpu, memory: current.memory, disk: current.disk } : null, historyPreview: history.slice(-8), historyCount: history.length, }; }); return { ok: parsed.ok, status: parsed.status, body: { ok: parsed.body !== undefined, systemStatuses } }; } catch { return { ok: true, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) }; } } export async function debugHealth(config: UniDeskConfig): Promise { return { coreInternal: await coreInternalFetch("/health"), overviewInternal: await coreInternalFetch("/api/overview"), nodesInternal: await coreInternalFetch("/api/nodes"), systemStatusInternal: coreSystemStatusSummary(), dockerStatusInternal: coreDockerStatusSummary(), frontendPublic: await readJson(`http://127.0.0.1:${config.network.frontend.port}/health`), providerIngressPublic: await readJson(`http://127.0.0.1:${config.network.providerIngress.port}/health`), 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 waitForTask(taskId: string, timeoutMs: number): Promise { const started = Date.now(); let latest: unknown = null; while (Date.now() - started < timeoutMs) { latest = coreInternalFetch("/api/tasks?limit=100"); const tasks = (latest as { body?: { tasks?: Array<{ id?: string; status?: string; result?: unknown }> } }).body?.tasks ?? []; const task = tasks.find((item) => item.id === taskId); if (task?.status === "succeeded" || task?.status === "failed") return { ok: true, task }; await Bun.sleep(500); } return { ok: false, timeoutMs, latest }; } export async function debugTask(_config: UniDeskConfig, taskId: string): Promise { const tasksResponse = coreInternalFetch("/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 { tasksResponse, taskId, task: task ?? null }; } export async function debugDispatch( config: UniDeskConfig, providerId: string, command: DebugDispatchCommand, payload?: Record, waitMs = 0, ): Promise { const dispatchPayload = payload ?? (command === "provider.upgrade" ? { source: "cli-debug", mode: "plan" } : { source: "cli-debug" }); const dispatch = coreInternalFetch("/api/dispatch", { method: "POST", body: { providerId, command, payload: dispatchPayload }, }); const taskId = (dispatch as { body?: { taskId?: string } }).body?.taskId ?? ""; const wait = waitMs > 0 && taskId.length > 0 ? await waitForTask(taskId, waitMs) : null; return { dispatch, wait }; }