import type { UniDeskConfig } from "./config"; import { renderMachine } from "./cicd-render"; import type { RenderedCliResult } from "./output"; import { getPlatformInfraPipelinesAsCodeNodeStatus } from "./platform-infra-pipelines-as-code"; interface NodeStatusOptions { nodeId: string | null; targetId: string | null; full: boolean; raw: boolean; output: "text" | "json"; } export function cicdNodeStatusHelp(): Record { return { command: "cicd status --node ", description: "Read a node-level CI/CD closeout summary for every Pipelines-as-Code consumer on the node.", usage: [ "bun scripts/cli.ts cicd status --node NC01", "bun scripts/cli.ts cicd status --node NC01 --full", "bun scripts/cli.ts cicd status --node NC01 --json", ], output: "compact text by default; --json/--raw returns machine payload", source: "config/platform-infra/pipelines-as-code.yaml consumers filtered by node", }; } export async function runCicdNodeStatusCommand(config: UniDeskConfig | null, args: string[]): Promise { if (args[0] === undefined || args.some(isHelpToken)) { return renderMachine("cicd status", cicdNodeStatusHelp(), "json"); } const options = parseNodeStatusOptions(args); if (config === null) throw new Error("cicd status requires UniDesk config; run through bun scripts/cli.ts cicd status --node "); const result = await getPlatformInfraPipelinesAsCodeNodeStatus(config, { targetId: options.targetId, nodeId: options.nodeId, full: options.full, raw: options.raw, }); if (options.output === "json" || options.raw) return renderMachine("cicd status", result, "json", result.ok !== false); return renderNodeStatus(result, options.full); } function parseNodeStatusOptions(args: string[]): NodeStatusOptions { let nodeId: string | null = null; let targetId: string | null = null; let full = false; let raw = false; let output: "text" | "json" = "text"; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "status") continue; if (arg === "--node" || arg === "--target") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`); if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${arg} must be a simple node id`); if (arg === "--node") nodeId = value; else targetId = value; index += 1; } else if (arg === "--full") { full = true; } else if (arg === "--raw" || arg === "-o=json" || (arg === "-o" && args[index + 1] === "json")) { raw = true; full = true; output = "json"; if (arg === "-o") index += 1; } else if (arg === "--json") { output = "json"; } else if (isHelpToken(arg)) { output = "json"; } else { throw new Error(`unsupported cicd status option: ${arg}`); } } if (nodeId === null && targetId === null) throw new Error("cicd status requires --node "); return { nodeId, targetId, full, raw, output }; } function renderNodeStatus(result: Record, full: boolean): RenderedCliResult { const target = record(result.target); const summary = record(result.summary); const consumers = arrayRecords(result.consumers); const rows = consumers.map((row) => [ stringValue(row.consumer), boolText(row.ready), stringValue(row.pipelineStatus), `${stringValue(row.durationSeconds)}s`, short(stringValue(row.sourceCommit)), short(stringValue(row.gitopsCommit)), `${stringValue(record(row.argo).sync)}/${stringValue(record(row.argo).health)}`, runtimeText(record(row.runtime)), stringValue(row.reason), ]); const detailRows = consumers.map((row) => [ stringValue(row.consumer), stringValue(row.latestPipelineRun), short(stringValue(row.digest), 18), stringValue(row.imageStatus), compactLine(stringValue(record(row.diagnostics).hint)), ]); const lines = [ "CI/CD NODE STATUS", ...table(["NODE", "TARGET", "READY", "READY_COUNT", "BLOCKED", "ELAPSED_MS"], [[ stringValue(result.node), stringValue(target.id), boolText(summary.ready), `${stringValue(summary.readyCount)}/${stringValue(summary.total)}`, stringValue(summary.blockedCount), stringValue(summary.elapsedMs), ]]), "", "CONSUMERS", ...(rows.length === 0 ? ["-"] : table(["CONSUMER", "READY", "PIPELINE", "DUR", "SOURCE", "GITOPS", "ARGO", "RUNTIME", "REASON"], rows)), "", ...(full ? [ "DETAIL", ...(detailRows.length === 0 ? ["-"] : table(["CONSUMER", "PIPELINERUN", "DIGEST", "IMAGE_STATUS", "HINT"], detailRows)), "", ] : []), "NEXT", ` refresh: ${stringValue(record(result.next).status)}`, ` history: ${stringValue(record(result.next).history)}`, ` closeout: ${stringValue(record(result.next).closeout)}`, ]; return { ok: result.ok !== false, command: "cicd status", renderedText: lines.join("\n"), contentType: "text/plain" }; } function isHelpToken(value: string | undefined): boolean { return value === "help" || value === "--help" || value === "-h"; } function record(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; } function arrayRecords(value: unknown): Record[] { return Array.isArray(value) ? value.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item)) as Record[] : []; } function stringValue(value: unknown, fallback = "-"): string { if (typeof value === "string" && value.length > 0) return value; if (typeof value === "number" || typeof value === "boolean") return String(value); return fallback; } function boolText(value: unknown): string { return value === true ? "true" : "false"; } function short(value: string, length = 12): string { if (value === "-" || value.length <= length) return value; return value.slice(0, length); } function runtimeText(runtime: Record): string { const ready = `${stringValue(runtime.readyReplicas)}/${stringValue(runtime.replicas)}`; const digest = short(stringValue(runtime.digest), 18); return digest === "-" ? ready : `${ready}:${digest}`; } function compactLine(value: string): string { const trimmed = value.replace(/\s+/gu, " ").trim(); return trimmed.length > 0 ? trimmed.slice(0, 120) : "-"; } function table(headers: string[], rows: string[][]): string[] { const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length))); const format = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index])).join(" "); return [format(headers), format(headers.map((header, index) => "-".repeat(Math.max(header.length, widths[index])))), ...rows.map(format)]; }