// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. options module for scripts/src/platform-infra.ts. // Moved mechanically from scripts/src/platform-infra.ts:338-453 for #903. import { createHash, randomBytes } from "node:crypto"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, isAbsolute, join } from "node:path"; import type { UniDeskConfig } from "../config"; import { rootPath } from "../config"; import { startJob } from "../jobs"; import type { RenderedCliResult } from "../output"; import { pk01CaddyMergeManagedBlocksPython, renderCaddyManagedBlock, renderSimpleReverseProxyCaddySiteBlock } from "../pk01-caddy"; import { capture, compactCapture, parseJsonOutput, prepareFrpcSecret, shQuote } from "../platform-infra-public-service"; import { yamlBooleanField, yamlFieldLabel, yamlIntegerField } from "../platform-infra-ops-library"; import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets"; import { apply, plan, status } from "./actions"; import { defaultSub2ApiTargetId, readSub2ApiConfig, validateOptions } from "./config"; import { configPath, platformInfraHelp, serviceName } from "./entry"; import { resolveTarget } from "./manifest"; import { validate } from "./policy"; export function sub2ApiHelpTargetSummary(): Record { const sub2api = readSub2ApiConfig(); const target = resolveTarget(sub2api, sub2api.defaults.targetId); return { default: sub2api.defaults.targetId, namespace: target.namespace, service: serviceName, serviceDns: `${serviceName}.${target.namespace}.svc.cluster.local:8080`, exposure: target.publicExposure?.enabled === true ? "yaml-controlled-public-exposure" : "k3s-cluster-internal-only", resourceLimits: "unset-by-policy", versionConfigPath: configPath, }; } export async function runPlatformInfraCommand(config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { const [target, action] = args; if (target === "langbot") { const { runLangBotCommand } = await import("../platform-infra-langbot"); return await runLangBotCommand(config, args.slice(1)); } if (target === "n8n") { const { runN8nCommand } = await import("../platform-infra-n8n"); return await runN8nCommand(config, args.slice(1)); } if (target === "wechat-archive") { const { runWechatArchiveCommand } = await import("../platform-infra-wechat-archive"); return await runWechatArchiveCommand(config, args.slice(1)); } if (target === "observability") { const { runPlatformObservabilityCommand } = await import("../platform-infra-observability"); return await runPlatformObservabilityCommand(config, args.slice(1)); } if (target !== "sub2api") return unsupported(args); if (action === "plan" || action === undefined) { const planArgs = args.slice(2); const result = plan(parseTargetOptions(planArgs)); return planArgs.includes("--full") || planArgs.includes("--raw") ? result : renderSub2ApiPlan(result); } if (action === "apply") return await apply(config, parseApplyOptions(args.slice(2))); if (action === "status") { const options = parseDisclosureOptions(args.slice(2)); const result = await status(config, options); return options.full || options.raw ? result : renderSub2ApiStatus(result); } if (action === "validate") return await validate(config, parseDisclosureOptions(args.slice(2))); if (action === "codex-pool") { const { runCodexPoolCommand } = await import("../platform-infra-sub2api-codex"); return await runCodexPoolCommand(config, args.slice(2)); } return unsupported(args); } export interface ApplyOptions { targetId: string; dryRun: boolean; confirm: boolean; wait: boolean; } export interface DisclosureOptions { targetId: string; full: boolean; raw: boolean; } export interface TargetOptions { targetId: string; } export interface PolicyCheck { name: string; ok: boolean; detail: string; } function renderSub2ApiPlan(result: Record): RenderedCliResult { const target = record(result.target); const config = record(record(result.config).target); const exposure = record(config.publicExposure); const egress = record(config.egressProxy); const next = record(result.next); const policy = arrayRecords(result.policy); const failedPolicy = policy.filter((item) => item.ok === false); const rows = [ ["TARGET", stringValue(target.id), "route", stringValue(target.route)], ["NAMESPACE", stringValue(target.namespace), "role", stringValue(target.role)], ["DATABASE", stringValue(config.databaseMode), "redis", stringValue(config.redisMode)], ["REPLICAS", `${stringValue(config.appReplicas)}/${stringValue(config.redisReplicas)}`, "service", stringValue(target.serviceDns)], ["PUBLIC", boolText(exposure.enabled), "url", stringValue(exposure.publicBaseUrl, "-")], ["EGRESS", boolText(egress.enabled), "sourceRef", stringValue(egress.sourceRef, "-")], ["POLICY", failedPolicy.length === 0 ? "ok" : `failed=${failedPolicy.length}`, "valuesPrinted", "false"], ]; const lines = [ "PLATFORM-INFRA SUB2API PLAN", ...table(["FIELD", "VALUE", "DETAIL", "VALUE"], rows), "", "NEXT", ` dry-run: ${stringValue(next.dryRun, `bun scripts/cli.ts platform-infra sub2api apply --target ${stringValue(target.id)} --dry-run`)}`, ` apply: ${stringValue(next.apply, `bun scripts/cli.ts platform-infra sub2api apply --target ${stringValue(target.id)} --confirm`)}`, ` status: ${stringValue(next.status, `bun scripts/cli.ts platform-infra sub2api status --target ${stringValue(target.id)}`)}`, " full: bun scripts/cli.ts platform-infra sub2api plan --target " + stringValue(target.id) + " --full", "", "Disclosure: default output is bounded; Secret and proxy source values are not printed.", ]; return rendered(result, "platform-infra sub2api plan", lines); } function renderSub2ApiStatus(result: Record): RenderedCliResult { const target = record(result.target); const summary = record(result.summary); const secret = record(summary.secret); const egress = record(summary.egressProxy); const networkPolicy = record(summary.networkPolicy); const deployments = arrayRecords(summary.deployments) .filter((item) => stringValue(item.name).startsWith("sub2api")) .map((item) => [ stringValue(item.name), boolText(item.ready), `${stringValue(item.readyReplicas, "0")}/${stringValue(item.desired, "0")}`, arrayValues(item.images).slice(0, 1).map((value) => stringValue(value)).join(",") || "-", ]); const services = arrayRecords(summary.services) .filter((item) => stringValue(item.name).startsWith("sub2api")) .map((item) => [ stringValue(item.name), stringValue(item.type), arrayRecords(item.ports).map((port) => `${stringValue(port.name, "port")}:${stringValue(port.port)}`).join(","), ]); const lines = [ "PLATFORM-INFRA SUB2API STATUS", ...table(["TARGET", "ROUTE", "NAMESPACE", "STATUS"], [[stringValue(summary.target, stringValue(target.id)), stringValue(summary.route, stringValue(target.route)), stringValue(summary.namespace, stringValue(target.namespace)), stringValue(summary.status, result.ok === false ? "failed" : "ok")]]), "", "WORKLOADS", ...(deployments.length === 0 ? ["-"] : table(["NAME", "READY", "REPLICAS", "IMAGE"], deployments)), "", "SERVICES", ...(services.length === 0 ? ["-"] : table(["NAME", "TYPE", "PORTS"], services)), "", "CHECKS", ...table(["CHECK", "VALUE", "DETAIL"], [ ["namespace", boolText(summary.namespaceExists), stringValue(summary.namespace)], ["secret", boolText(secret.ready), `name=${stringValue(secret.name, "-")} missing=${arrayValues(secret.missingKeys).length}`], ["egressProxy", boolText(egress.aligned), `enabled=${boolText(egress.enabled)} valuesPrinted=false`], ["networkPolicy", boolText(networkPolicy.ok), `required=${stringValue(networkPolicy.requiredName, "-")}`], ]), "", "NEXT", ` validate: bun scripts/cli.ts platform-infra sub2api validate --target ${stringValue(summary.target, stringValue(target.id))}`, ` full: bun scripts/cli.ts platform-infra sub2api status --target ${stringValue(summary.target, stringValue(target.id))} --full`, "Disclosure: default output is bounded; use --full or --raw for pod/service/event details.", ]; return rendered(result, "platform-infra sub2api status", lines); } function rendered(result: Record, command: string, lines: string[]): RenderedCliResult { return { ok: result.ok !== false, command, renderedText: lines.join("\n"), contentType: "text/plain", }; } 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.map(record) : []; } function arrayValues(value: unknown): unknown[] { return Array.isArray(value) ? value : []; } function stringValue(value: unknown, fallback = "-"): string { if (value === undefined || value === null || value === "") return fallback; return String(value); } function boolText(value: unknown): string { return value === true ? "yes" : value === false ? "no" : "-"; } function table(headers: string[], rows: string[][]): string[] { const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0))); const renderRow = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(" ").trimEnd(); return [renderRow(headers), ...rows.map(renderRow)]; } export function unsupported(args: string[]): Record { return { ok: false, error: "unsupported-platform-infra-command", args, help: platformInfraHelp(), }; } export function parseApplyOptions(args: string[]): ApplyOptions { const target = parseTargetOptions(args); validateOptions(args, new Set(["--dry-run", "--confirm", "--wait", "--target"])); if (args.includes("--dry-run") && args.includes("--confirm")) throw new Error("apply accepts only one of --dry-run or --confirm"); return { targetId: target.targetId, dryRun: args.includes("--dry-run") || !args.includes("--confirm"), confirm: args.includes("--confirm"), wait: args.includes("--wait"), }; } export function parseDisclosureOptions(args: string[]): DisclosureOptions { const target = parseTargetOptions(args); validateOptions(args, new Set(["--full", "--raw", "--target"])); const raw = args.includes("--raw"); return { targetId: target.targetId, full: raw || args.includes("--full"), raw }; } export function parseTargetOptions(args: string[]): TargetOptions { let targetId: string | null = null; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--target") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value"); targetId = value; index += 1; } else if (arg.startsWith("--target=")) { targetId = arg.slice("--target=".length); } } const resolvedTargetId = targetId ?? defaultSub2ApiTargetId(); if (!/^[A-Za-z0-9._-]+$/u.test(resolvedTargetId)) throw new Error("--target must be a simple target id"); return { targetId: resolvedTargetId }; }