// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. options module for scripts/src/platform-infra-sub2api-codex.ts. // Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:320-620 for #903. import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import type { UniDeskConfig } from "../config"; import { rootPath } from "../config"; import type { RenderedCliResult } from "../output"; import { applyPk01CaddyBlock, prepareFrpcSecret, renderFrpcManifest, type PublicServiceExposure, type PublicServiceTarget } from "../platform-infra-public-service"; import { shortSha256Fingerprint } from "../platform-infra-ops-library"; import { codexPoolSentinelSummary, codexPoolSentinelRuntimeImage, readCodexPoolSentinelConfig, renderCodexPoolSentinelManifest, type CodexPoolSentinelConfig, type CodexPoolSentinelProfileSecret, } from "../platform-infra-sub2api-codex-sentinel"; import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets"; import { runSshCommandCapture, type SshCaptureResult } from "../ssh"; import type { ConfirmOptions, DisclosureOptions, SentinelImageOptions, SentinelProbeOptions, SentinelReportOptions, SyncOptions, TraceOptions } from "./types"; import { codexPoolCleanupProbes, codexPoolConfigureLocal, codexPoolExpose, codexPoolPlan, codexPoolSentinelImage, codexPoolSentinelProbe, codexPoolSentinelReport, codexPoolSync, codexPoolTrace, codexPoolValidate } from "./actions"; import { renderCodexPoolPlan } from "./render"; import { defaultCodexPoolRuntimeTargetId } from "./runtime-target"; import { codexPoolHelp } from "./types"; export async function runCodexPoolCommand(config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { const [action = "plan"] = args; if (action === "plan") { const options = parseDisclosureOptions(args.slice(1)); const result = codexPoolPlan(options); return options.full || options.raw ? result : renderCodexPoolPlan(result); } if (action === "sync") return await codexPoolSync(config, parseSyncOptions(args.slice(1))); if (action === "validate") return await codexPoolValidate(config, parseDisclosureOptions(args.slice(1))); if (action === "trace") return await codexPoolTrace(config, parseTraceOptions(args.slice(1))); if (action === "sentinel-image") return await codexPoolSentinelImage(config, parseSentinelImageOptions(args.slice(1))); if (action === "sentinel-probe") return await codexPoolSentinelProbe(config, parseSentinelProbeOptions(args.slice(1))); if (action === "sentinel-report") return await codexPoolSentinelReport(config, parseSentinelReportOptions(args.slice(1))); if (action === "cleanup-probes") return await codexPoolCleanupProbes(config, parseConfirmOptions(args.slice(1))); if (action === "expose") return await codexPoolExpose(config, parseConfirmOptions(args.slice(1))); if (action === "configure-local") return await codexPoolConfigureLocal(config, parseConfirmOptions(args.slice(1))); return { ok: false, error: "unsupported-platform-infra-sub2api-codex-pool-command", args, help: codexPoolHelp(), }; } export function parseSyncOptions(args: string[]): SyncOptions { validateOptions(args, new Set(["--confirm", "--prune-removed", "--full", "--raw", "--target"])); const disclosure = parseDisclosureOptions(stripBooleanOptions(args, new Set(["--confirm", "--prune-removed"]))); return { ...disclosure, confirm: args.includes("--confirm"), pruneRemoved: args.includes("--prune-removed") }; } export function parseConfirmOptions(args: string[]): ConfirmOptions { validateOptions(args, new Set(["--confirm", "--full", "--raw", "--target"])); const disclosure = parseDisclosureOptions(stripBooleanOptions(args, new Set(["--confirm"]))); return { ...disclosure, confirm: args.includes("--confirm") }; } export function parseSentinelImageOptions(args: string[]): SentinelImageOptions { const [actionRaw = "status", ...rest] = args; if (actionRaw !== "status" && actionRaw !== "build") throw new Error("sentinel-image usage: status|build [--dry-run|--confirm] [--full|--raw]"); let confirm = false; let explicitDryRun = false; const disclosureArgs: string[] = []; for (let index = 0; index < rest.length; index += 1) { const arg = rest[index]!; if (arg === "--confirm") { confirm = true; continue; } if (arg === "--dry-run") { explicitDryRun = true; continue; } if (arg === "--full" || arg === "--raw" || arg === "--target" || arg.startsWith("--target=")) { disclosureArgs.push(arg); if (arg === "--target") { const value = rest[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value"); disclosureArgs.push(value); index += 1; } continue; } throw new Error(`unsupported option: ${arg}`); } if (confirm && explicitDryRun) throw new Error("sentinel-image accepts only one of --confirm or --dry-run"); const disclosure = parseDisclosureOptions(disclosureArgs); return { ...disclosure, action: actionRaw, confirm, dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm, }; } export function parseSentinelProbeOptions(args: string[]): SentinelProbeOptions { const accounts: string[] = []; const disclosureArgs: string[] = []; let confirm = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index]!; if (arg === "--confirm") { confirm = true; continue; } if (arg === "--full" || arg === "--raw" || arg === "--target" || arg.startsWith("--target=")) { disclosureArgs.push(arg); if (arg === "--target") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value"); disclosureArgs.push(value); index += 1; } continue; } if (arg === "--account") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--account requires an account name"); accounts.push(...splitAccountNames(value)); index += 1; continue; } if (arg.startsWith("--account=")) { accounts.push(...splitAccountNames(arg.slice("--account=".length))); continue; } throw new Error(`unsupported option: ${arg}`); } const uniqueAccounts = [...new Set(accounts)]; if (uniqueAccounts.length === 0) throw new Error("sentinel-probe requires --account "); for (const account of uniqueAccounts) validateSub2ApiAccountSelector(account, "--account"); const disclosure = parseDisclosureOptions(disclosureArgs); return { ...disclosure, confirm, accounts: uniqueAccounts }; } export function parseSentinelReportOptions(args: string[]): SentinelReportOptions { let events = 20; let explicitEvents = false; const disclosureArgs: string[] = []; for (let index = 0; index < args.length; index += 1) { const arg = args[index]!; if (arg === "--full" || arg === "--raw" || arg === "--target" || arg.startsWith("--target=")) { disclosureArgs.push(arg); if (arg === "--target") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value"); disclosureArgs.push(value); index += 1; } continue; } if (arg === "--events") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--events requires a positive integer"); events = readReportEventLimit(value, "--events"); explicitEvents = true; index += 1; continue; } if (arg.startsWith("--events=")) { events = readReportEventLimit(arg.slice("--events=".length), "--events"); explicitEvents = true; continue; } throw new Error(`unsupported option: ${arg}`); } const disclosure = parseDisclosureOptions(disclosureArgs); if (disclosure.full && !explicitEvents) events = 80; return { ...disclosure, events }; } export function parseTraceOptions(args: string[]): TraceOptions { let requestId: string | null = null; let since = "24h"; let tail = 20_000; let contextSeconds = 300; let showLines = false; const disclosureArgs: string[] = []; for (let index = 0; index < args.length; index += 1) { const arg = args[index]!; if (arg === "--full" || arg === "--raw" || arg === "--target" || arg.startsWith("--target=")) { disclosureArgs.push(arg); if (arg === "--target") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value"); disclosureArgs.push(value); index += 1; } continue; } if (arg === "--show-lines") { showLines = true; continue; } if (arg === "--request-id" || arg === "--id") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a request id`); requestId = value.trim(); index += 1; continue; } if (arg.startsWith("--request-id=")) { requestId = arg.slice("--request-id=".length).trim(); continue; } if (arg.startsWith("--id=")) { requestId = arg.slice("--id=".length).trim(); continue; } if (arg === "--since") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--since requires a kubectl duration such as 24h or 90m"); since = parseKubectlDuration(value); index += 1; continue; } if (arg.startsWith("--since=")) { since = parseKubectlDuration(arg.slice("--since=".length)); continue; } if (arg === "--tail") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--tail requires an integer"); tail = parseTraceLimit(value, "--tail", 100, 200_000); index += 1; continue; } if (arg.startsWith("--tail=")) { tail = parseTraceLimit(arg.slice("--tail=".length), "--tail", 100, 200_000); continue; } if (arg === "--context-seconds") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--context-seconds requires an integer"); contextSeconds = parseTraceLimit(value, "--context-seconds", 0, 3600); index += 1; continue; } if (arg.startsWith("--context-seconds=")) { contextSeconds = parseTraceLimit(arg.slice("--context-seconds=".length), "--context-seconds", 0, 3600); continue; } throw new Error(`unsupported option: ${arg}`); } if (requestId === null || requestId.length === 0) throw new Error("trace requires --request-id "); if (!/^[A-Za-z0-9_.:-]{8,128}$/u.test(requestId)) throw new Error("--request-id has an unsupported format"); const disclosure = parseDisclosureOptions(disclosureArgs); return { ...disclosure, requestId, since, tail, contextSeconds, showLines }; } export function parseKubectlDuration(raw: string): string { const value = raw.trim(); if (!/^[1-9][0-9]*(?:s|m|h)$/u.test(value)) throw new Error("--since must be a kubectl duration such as 24h, 90m, or 300s"); return value; } export function parseTraceLimit(raw: string, option: string, min: number, max: number): number { const value = Number(raw); if (!Number.isInteger(value) || value < min || value > max) throw new Error(`${option} must be an integer from ${min} to ${max}`); return value; } export function readReportEventLimit(raw: string, option: string): number { const value = Number(raw); if (!Number.isInteger(value) || value < 1 || value > 200) throw new Error(`${option} must be an integer from 1 to 200`); return value; } export function parseDisclosureOptions(args: string[]): DisclosureOptions { validateOptions(args, new Set(["--full", "--raw", "--target"])); const raw = args.includes("--raw"); return { full: raw || args.includes("--full"), raw, targetId: parseTargetId(args) }; } export function parseTargetId(args: string[]): string { 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; continue; } if (arg.startsWith("--target=")) targetId = arg.slice("--target=".length); } const resolvedTargetId = targetId ?? defaultCodexPoolRuntimeTargetId(); if (!/^[A-Za-z0-9._-]+$/u.test(resolvedTargetId)) throw new Error("--target must be a simple target id"); return resolvedTargetId; } export function splitAccountNames(value: string): string[] { return value.split(",").map((item) => item.trim()).filter(Boolean); } export function validateSub2ApiAccountSelector(value: string, option: string): void { if (value.length === 0 || value.length > 256) throw new Error(`${option} must be a non-empty account name up to 256 characters`); if (/[\r\n]/u.test(value)) throw new Error(`${option} must not contain newlines`); if (!/^[^<>"'`\\]+$/u.test(value)) throw new Error(`${option} contains unsupported characters`); } export function validateOptions(args: string[], booleanOptions: Set): void { for (let index = 0; index < args.length; index += 1) { const arg = args[index]!; if (arg === "--target") { index += 1; continue; } if (arg.startsWith("--target=") && booleanOptions.has("--target")) continue; if (booleanOptions.has(arg)) continue; throw new Error(`unsupported option: ${arg}`); } } export function stripBooleanOptions(args: string[], stripped: Set): string[] { return args.filter((arg) => !stripped.has(arg)); } export interface Sub2ApiRuntimeConfig { defaultTargetId: string; appSecretName: string; secretsRoot: string; appSourceRef: string; sentinelEnabledOnTargets: string[]; targets: Record[]; }