feat: render 71freq hwpod preinstall config
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
version: 1
|
||||
kind: HwlabGatewayProfileConfig
|
||||
metadata:
|
||||
name: constart-71freq-d601-v03
|
||||
spec: PJ2026-01010305
|
||||
implementationRef: draft-2026-06-26-71freq-v03-hwpod-preinstall
|
||||
|
||||
gateway:
|
||||
profile:
|
||||
id: constart-71freq-d601-v03
|
||||
node: D601
|
||||
lane: v03
|
||||
cloudUrl: https://hwlab.pikapython.com
|
||||
apiUrl: https://hwlab.pikapython.com
|
||||
gatewayId: gw-d601-constart-71freq
|
||||
sessionId: gws_D601_71_FREQ
|
||||
resourceId: constart-71freq-c
|
||||
capabilityId: cap_constart_71freq_hwpod
|
||||
hwpodId: constart-71freq-c
|
||||
nodeId: node-d601-f103-v2
|
||||
nodeOps:
|
||||
route: /v1/hwpod-node-ops
|
||||
serviceUrl: http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667/v1/hwpod-node-ops
|
||||
publicUrl: https://hwlab.pikapython.com/v1/hwpod-node-ops
|
||||
websocketUrl: wss://hwlab.pikapython.com/v1/hwpod-node/ws
|
||||
managedRun:
|
||||
mode: windows-scheduled-task
|
||||
runtimeRoot: "C:\\Users\\liang\\hwpod-node-runtime"
|
||||
taskName: HWLAB-HWPOD-Node-WS
|
||||
periodicTaskName: hwpod-node-ws-periodic
|
||||
startCommand: "C:\\Users\\liang\\hwpod-node-runtime\\start-node-ws.cmd"
|
||||
statusCommand: "C:\\Users\\liang\\hwpod-node-runtime\\status-node-ws.cmd"
|
||||
processPattern: "tools\\hwpod-node.ts connect"
|
||||
secretRefs:
|
||||
- purpose: gatewayAuthToken
|
||||
sourceRef: hwlab/d601-v03-gateway.env
|
||||
sourceKey: HWLAB_GATEWAY_TOKEN
|
||||
targetKey: HWLAB_GATEWAY_TOKEN
|
||||
migratedFrom:
|
||||
- "F:\\Work\\ConStart\\.device-pod\\.runtime\\D601-71-FREQ.json"
|
||||
@@ -0,0 +1,113 @@
|
||||
version: 1
|
||||
kind: HwlabHwpodPreinstallConfig
|
||||
metadata:
|
||||
name: constart-71freq-c
|
||||
spec: PJ2026-01010305
|
||||
implementationRef: draft-2026-06-26-71freq-v03-hwpod-preinstall
|
||||
|
||||
hwpodPreinstall:
|
||||
hwpodId: constart-71freq-c
|
||||
sourceRef:
|
||||
spec: config/hwlab-hwpod-preinstalls/constart-71freq-c.yaml#hwpodPreinstall.specDocument
|
||||
metadata: config/hwlab-hwpod-preinstalls/constart-71freq-c.yaml#hwpodPreinstall.metadataSidecar
|
||||
metadataRef: config/hwlab-hwpod-preinstalls/constart-71freq-c.yaml#hwpodPreinstall.metadataSidecar
|
||||
targetDevice:
|
||||
board: ConStart 71-FREQ Controller
|
||||
mcu: STM32H723ZGTx
|
||||
flashBase: "0x08000000"
|
||||
nodeBinding:
|
||||
hwlabNode: D601
|
||||
lane: v03
|
||||
nodeId: node-d601-f103-v2
|
||||
nodeType: pc-host
|
||||
workspaceRootRef: "F:\\Work\\ConStart"
|
||||
projectRoot: projects/71-00075-11
|
||||
toolchain:
|
||||
name: keil-mdk
|
||||
keilProject: projects/71-00075-11/FirmWare/MDK-ARM/FREQ_Controller_FW.uvprojx
|
||||
keilTarget: FREQ_Controller_FW
|
||||
hexPath: projects/71-00075-11/FirmWare/MDK-ARM/FREQ_Controller_FW/FREQ_Controller_FW.hex
|
||||
mapPath: projects/71-00075-11/FirmWare/MDK-ARM/FREQ_Controller_FW/FREQ_Controller_FW.map
|
||||
keilCliPath: "C:\\Users\\liang\\.agents\\skills\\keil\\keil-cli.py"
|
||||
uv4Path: "C:\\Keil_v5\\UV4\\UV4.exe"
|
||||
debugProbe:
|
||||
id: debug-probe
|
||||
type: cmsis-dap
|
||||
adapter: keil
|
||||
probeUid: 3FD750C63E342E24
|
||||
probeName: MicroLink CMSIS-DAP
|
||||
programBackend: keil-headless
|
||||
autoBindUvoptx: true
|
||||
uart:
|
||||
id: uart/1
|
||||
scope: external
|
||||
port: COM4
|
||||
baudRate: 921600
|
||||
encoding: utf8
|
||||
boardComm:
|
||||
endpoints:
|
||||
- id: hwpod-node-ops
|
||||
kind: hwpod-node-ops
|
||||
endpointRef: config/hwlab-gateway/constart-71freq-d601-v03.yaml#gateway.profile.nodeOps.publicUrl
|
||||
ioProbe:
|
||||
uart:
|
||||
id: uart/1
|
||||
port: COM4
|
||||
baudrate: 921600
|
||||
runtimeMount:
|
||||
namespace: hwlab-v03
|
||||
configMapName: hwlab-v03-hwpod-preinstalled-specs
|
||||
specKey: constart-71freq-c.yaml
|
||||
metadataKey: constart-71freq-c.meta.json
|
||||
mountPath: /etc/hwlab/hwpod-specs
|
||||
envKey: HWLAB_HWPOD_SPEC_REGISTRY_DIRS
|
||||
rolloutTarget:
|
||||
kind: Deployment
|
||||
name: hwlab-cloud-api
|
||||
container: hwlab-cloud-api
|
||||
specDocument:
|
||||
apiVersion: hwlab.dev/v0alpha1
|
||||
kind: Hwpod
|
||||
metadata:
|
||||
uid: CONSTART-71FREQ-C
|
||||
name: constart-71freq-c
|
||||
spec:
|
||||
targetDevice:
|
||||
board: ConStart 71-FREQ Controller
|
||||
mcu: STM32H723ZGTx
|
||||
flashBase: "0x08000000"
|
||||
workspace:
|
||||
path: "F:\\Work\\ConStart"
|
||||
toolchain: keil-mdk
|
||||
projectRoot: projects/71-00075-11
|
||||
keilProject: projects/71-00075-11/FirmWare/MDK-ARM/FREQ_Controller_FW.uvprojx
|
||||
keilTarget: FREQ_Controller_FW
|
||||
hexPath: projects/71-00075-11/FirmWare/MDK-ARM/FREQ_Controller_FW/FREQ_Controller_FW.hex
|
||||
mapPath: projects/71-00075-11/FirmWare/MDK-ARM/FREQ_Controller_FW/FREQ_Controller_FW.map
|
||||
keilCliPath: "C:\\Users\\liang\\.agents\\skills\\keil\\keil-cli.py"
|
||||
debugProbe:
|
||||
type: cmsis-dap
|
||||
adapter: keil
|
||||
probeUid: 3FD750C63E342E24
|
||||
probeName: MicroLink CMSIS-DAP
|
||||
programBackend: keil-headless
|
||||
autoBindUvoptx: true
|
||||
ioProbe:
|
||||
uart:
|
||||
id: uart/1
|
||||
port: COM4
|
||||
baudrate: 921600
|
||||
nodeBinding:
|
||||
nodeId: node-d601-f103-v2
|
||||
nodeType: pc-host
|
||||
metadataSidecar:
|
||||
contractVersion: hwpod-spec-registry-v1
|
||||
source:
|
||||
kind: preinstalled-yaml-first-spec
|
||||
migratedFrom:
|
||||
- "F:\\Work\\ConStart\\.device-pod\\.runtime\\D601-71-FREQ.json"
|
||||
- "F:\\Work\\ConStart\\projects\\71-00075-11\\.device-pod\\device-pod-71-00075-11.json"
|
||||
workspaceRoot: "F:\\Work\\ConStart"
|
||||
projectRoot: projects/71-00075-11
|
||||
verificationIssue: pikasTech/HWLAB#2183
|
||||
verifiedAt: 2026-06-26
|
||||
@@ -150,6 +150,12 @@ lanes:
|
||||
queryRetryMaxAttempts: 5
|
||||
queryRetryInitialDelayMs: 250
|
||||
queryRetryMaxDelayMs: 5000
|
||||
hwpodPreinstall:
|
||||
enabled: true
|
||||
configRefs:
|
||||
preinstall: config/hwlab-hwpod-preinstalls/constart-71freq-c.yaml#hwpodPreinstall
|
||||
projectManagementSource: config/hwlab-project-management/constart-71freq-mdtodo.yaml#projectManagement.sources[0]
|
||||
gatewayProfile: config/hwlab-gateway/constart-71freq-d601-v03.yaml#gateway.profile
|
||||
webProbe:
|
||||
browserProxyMode: direct
|
||||
defaultOrigin:
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
version: 1
|
||||
kind: HwlabProjectManagementSourceConfig
|
||||
metadata:
|
||||
name: constart-71freq-mdtodo
|
||||
spec: PJ2026-010404
|
||||
implementationRef: draft-2026-06-25-p0-mdtodo-web-active-editing-hwpod-source
|
||||
|
||||
projectManagement:
|
||||
sources:
|
||||
- sourceId: constart-71freq-mdtodo
|
||||
sourceKind: hwpod-workspace
|
||||
displayName: 71-FREQ MDTODO
|
||||
projectId: project_constart_71freq
|
||||
hwpodId: constart-71freq-c
|
||||
nodeId: node-d601-f103-v2
|
||||
workspaceRootRef: "F:\\Work\\ConStart"
|
||||
mdtodoRootRef: docs/MDTODO
|
||||
maxFiles: 300
|
||||
focusFiles:
|
||||
- 20260419_频率判断.md
|
||||
- 20260609_频率判断_用户反馈.md
|
||||
- details/
|
||||
capabilities:
|
||||
read: true
|
||||
write: true
|
||||
reindex: true
|
||||
launchWorkbench: true
|
||||
hwpodNodeOpsUrlConfigRef: config/hwlab-gateway/constart-71freq-d601-v03.yaml#gateway.profile.nodeOps.serviceUrl
|
||||
runtimeEnv:
|
||||
envKey: HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_URL
|
||||
targetServiceId: hwlab-project-management
|
||||
rolloutTarget:
|
||||
kind: Deployment
|
||||
namespace: hwlab-v03
|
||||
name: hwlab-project-management
|
||||
container: hwlab-project-management
|
||||
redaction:
|
||||
rawMarkdown: true
|
||||
hostPath: true
|
||||
valuesRedacted: true
|
||||
+7
-1
@@ -200,11 +200,17 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
const [top, sub, third, fourth] = args;
|
||||
if (top === undefined || top === "help" || top === "--help" || top === "-h") {
|
||||
if (top === undefined || top === "--help" || top === "-h") {
|
||||
emitJson(commandName, rootHelp());
|
||||
return;
|
||||
}
|
||||
|
||||
if (top === "help") {
|
||||
const namespaceHelp = args.length > 1 ? await staticNamespaceHelp([...args.slice(1), "--help"]) : null;
|
||||
emitJson(commandName, namespaceHelp ?? rootHelp());
|
||||
return;
|
||||
}
|
||||
|
||||
if (top === "ssh" && (sub === undefined || isHelpToken(sub) || (isHelpToken(third) && args.length === 3))) {
|
||||
emitJson(commandName, sshHelp());
|
||||
return;
|
||||
|
||||
+7
-3
@@ -60,7 +60,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "git github-push-fallback [--repo owner/name] [--branch branch] [--host-name host-or-ip] [--confirm]", description: "Plan or execute a one-shot GitHub push through ssh.github.com:443 without editing remotes; use only for reviewed DNS/port-22 push fallback." },
|
||||
{ command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run", description: "Host Codex commander skeleton contract, no-daemon smoke plan, and dry-run approval preview without live bridges or message sends." },
|
||||
{ command: "web-probe run|script|observe|sentinel --node <node> --lane <lane>", description: "Run YAML-selected HWLAB Web probes, long observe/analyze sessions, project-management MDTODO commands, and Web sentinel control through the single top-level web-probe entrypoint." },
|
||||
{ command: "hwlab nodes control-plane|git-mirror|secret|test-accounts --node <node> --lane <lane>", description: "Manage HWLAB node/lane runtime prerequisites, including D601 YAML-declared k3s infra/tools-image/Argo bootstrap, redacted test-account preparation, and G14 v0.3+ runtime lanes, with the node identity passed as data." },
|
||||
{ command: "hwlab nodes control-plane|git-mirror|hwpod-preinstall|secret|test-accounts --node <node> --lane <lane>", description: "Manage HWLAB node/lane runtime prerequisites, including D601 YAML-declared k3s infra/tools-image/Argo bootstrap, HWPOD preinstall configRefs, redacted test-account preparation, and G14 v0.3+ runtime lanes, with the node identity passed as data." },
|
||||
{ command: "hwlab g14 monitor-prs | hwlab g14 control-plane status|apply|trigger-current|runtime-migration|cleanup-runs|cleanup-released-pvs | hwlab g14 git-mirror status|apply|sync|flush | hwlab g14 tools-image status|build", description: "Start the legacy G14 PR monitor, run bounded v0.2 Tekton/Argo control-plane, manual PipelineRun trigger, runtime migration, CI workspace retention, manual devops-infra git mirror/relay maintenance, or fixed HWLAB CI tools image actions; long confirmed trigger/sync/flush actions return async jobs by default." },
|
||||
{ command: "agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send|control-plane|git-mirror", description: "Use AgentRun v0.1 resource primitives with low-noise human output by default; session follow-up uses send only and the server decides internal steer vs turn." },
|
||||
{ command: "platform-infra sub2api|langbot|n8n|wechat-archive ...", description: "Deploy platform-infra services such as Sub2API, LangBot and n8n, manage YAML-controlled public FRP/Caddy exposure and WeChat archive workflows, and inspect status/logs without printing secrets." },
|
||||
@@ -663,7 +663,7 @@ function platformInfraHelpSummary(): unknown {
|
||||
|
||||
function hwlabNodeHelpSummary(): unknown {
|
||||
return {
|
||||
command: "hwlab nodes control-plane|git-mirror|observability|secret|test-accounts --node <node> --lane <lane>",
|
||||
command: "hwlab nodes control-plane|git-mirror|hwpod-preinstall|observability|secret|test-accounts --node <node> --lane <lane>",
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/cli.ts hwlab nodes control-plane infra plan --node D601 --lane v03",
|
||||
@@ -672,12 +672,13 @@ function hwlabNodeHelpSummary(): unknown {
|
||||
"bun scripts/cli.ts hwlab nodes control-plane infra argo status --node D601 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes control-plane status --node G14 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes git-mirror status --node G14 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes hwpod-preinstall plan --node D601 --lane v03 --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes observability performance-summary --node D601 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name <secret>",
|
||||
"bun scripts/cli.ts hwlab nodes test-accounts status --node D601 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes test-accounts sync --node D601 --lane v03 --confirm",
|
||||
],
|
||||
description: "Operate HWLAB node/lane runtime prerequisites with node and lane passed as data. The infra subcommand manages YAML-controlled node-local CI/CD, git-mirror, public Dockerfile tools image, and declarative Argo CD prerequisites for D601 v03 while keeping cross-node work semi-automatic; observability reads runtime metrics and authenticated Web Performance summaries; test-accounts prepares UniDesk YAML-declared admin/test account API keys with redacted sourceRef/fingerprint output. Web probe commands moved to top-level `bun scripts/cli.ts web-probe`.",
|
||||
description: "Operate HWLAB node/lane runtime prerequisites with node and lane passed as data. The infra subcommand manages YAML-controlled node-local CI/CD, git-mirror, public Dockerfile tools image, and declarative Argo CD prerequisites for D601 v03 while keeping cross-node work semi-automatic; hwpod-preinstall renders D601/v03 71-FREQ HWPOD/MDTODO/gateway configRefs without runtime mutation; observability reads runtime metrics and authenticated Web Performance summaries; test-accounts prepares UniDesk YAML-declared admin/test account API keys with redacted sourceRef/fingerprint output. Web probe commands moved to top-level `bun scripts/cli.ts web-probe`.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -771,6 +772,9 @@ export async function staticNamespaceHelp(args: string[]): Promise<unknown | nul
|
||||
if (top === "hwlab" && (sub === "node" || sub === "nodes") && args[2] === "test-accounts") {
|
||||
return loadHelp(async () => (await import("./hwlab-test-accounts")).hwlabTestAccountsHelp(), hwlabNodeHelpSummary());
|
||||
}
|
||||
if (top === "hwlab" && (sub === "node" || sub === "nodes") && args[2] === "hwpod-preinstall") {
|
||||
return loadHelp(async () => (await import("./hwlab-node-hwpod-preinstall")).hwlabNodeHwpodPreinstallHelp(), hwlabNodeHelpSummary());
|
||||
}
|
||||
if (top === "hwlab" && (sub === "node" || sub === "nodes")) return loadHelp(async () => (await import("./hwlab-node")).hwlabNodeHelp(), hwlabNodeHelpSummary());
|
||||
if (top === "hwlab" && sub === "g14") return loadHelp(async () => (await import("./hwlab-g14")).hwlabG14Help(), hwlabG14HelpSummary());
|
||||
if (top === "hwlab") return loadHelp(async () => (await import("./hwlab-cd")).hwlabHelp(), hwlabHelpSummary());
|
||||
|
||||
@@ -12,6 +12,7 @@ export function hwlabNodeHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts hwlab nodes control-plane infra plan --node D601 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes control-plane status --node D601 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes git-mirror status --node G14 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes hwpod-preinstall plan --node D601 --lane v03 --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name <secret>",
|
||||
"bun scripts/cli.ts hwlab nodes test-accounts status --node D601 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes observability performance-summary --node D601 --lane v03",
|
||||
@@ -20,6 +21,7 @@ export function hwlabNodeHelp(): Record<string, unknown> {
|
||||
actions: {
|
||||
"control-plane": "YAML-first node-local CI/CD, git-mirror, public exposure, runtime-image, Argo and PipelineRun operations.",
|
||||
"git-mirror": "Inspect or operate the selected node/lane source mirror.",
|
||||
"hwpod-preinstall": "Render YAML-first HWPOD preinstall configRefs, runtime mount targets, PM MDTODO source, and gateway profile status.",
|
||||
secret: "Inspect and sync YAML-declared runtime Secrets without printing secret values.",
|
||||
"test-accounts": "Prepare YAML-declared HWLAB admin/test account API keys with redacted sourceRef/fingerprint output.",
|
||||
observability: "Read runtime metrics and authenticated Web Performance summaries.",
|
||||
|
||||
@@ -0,0 +1,612 @@
|
||||
// SPEC: PJ2026-01010305 71FREQ预装 draft-2026-06-26-71freq-v03-hwpod-preinstall.
|
||||
// Responsibility: Redacted YAML configRef graph and plan/status rendering for D601 71-FREQ HWPOD preinstall.
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { rootPath } from "./config";
|
||||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "./hwlab-node-lanes";
|
||||
import { assertLane, assertNodeId, requiredOption } from "./hwlab-node/utils";
|
||||
import { assertKnownOptions } from "./hwlab-node/web-probe-observe";
|
||||
import type { RenderedCliResult } from "./output";
|
||||
|
||||
type HwpodPreinstallAction = "plan" | "status";
|
||||
type HwpodPreinstallConfigRefKey = "preinstall" | "projectManagementSource" | "gatewayProfile";
|
||||
|
||||
interface HwpodPreinstallOptions {
|
||||
readonly action: HwpodPreinstallAction;
|
||||
readonly node: string;
|
||||
readonly lane: string;
|
||||
readonly dryRun: boolean;
|
||||
}
|
||||
|
||||
interface HwpodPreinstallRoot {
|
||||
readonly present: boolean;
|
||||
readonly enabled: boolean;
|
||||
readonly rootPath: string;
|
||||
readonly configRefs: Partial<Record<HwpodPreinstallConfigRefKey, string>>;
|
||||
readonly error: string | null;
|
||||
}
|
||||
|
||||
interface HwpodPreinstallConfigPlan {
|
||||
readonly ok: boolean;
|
||||
readonly command: string;
|
||||
readonly status: "ready" | "blocked" | "disabled";
|
||||
readonly node: string;
|
||||
readonly lane: string;
|
||||
readonly rootPath: string;
|
||||
readonly enabled: boolean;
|
||||
readonly refs: readonly HwpodPreinstallConfigRefStatus[];
|
||||
readonly conflicts: readonly string[];
|
||||
readonly artifacts: readonly HwpodPreinstallArtifactRow[];
|
||||
readonly next: Record<string, string>;
|
||||
readonly valuesRedacted: true;
|
||||
}
|
||||
|
||||
interface HwpodPreinstallConfigRefStatus {
|
||||
readonly key: HwpodPreinstallConfigRefKey;
|
||||
readonly ref: string;
|
||||
readonly file: string;
|
||||
readonly path: string;
|
||||
readonly present: boolean;
|
||||
readonly targetPresent: boolean;
|
||||
readonly targetKind: "object" | "array" | "scalar" | "null" | "missing";
|
||||
readonly sha256: string | null;
|
||||
readonly byteCount: number | null;
|
||||
readonly missingFields: readonly string[];
|
||||
readonly conflicts: readonly string[];
|
||||
readonly summary: string;
|
||||
readonly error: string | null;
|
||||
}
|
||||
|
||||
interface InternalConfigRefStatus extends HwpodPreinstallConfigRefStatus {
|
||||
readonly target: unknown;
|
||||
}
|
||||
|
||||
interface RequiredTargetShape {
|
||||
readonly kind: "object";
|
||||
readonly requiredPaths: readonly string[];
|
||||
}
|
||||
|
||||
interface HwpodPreinstallArtifactRow {
|
||||
readonly kind: string;
|
||||
readonly name: string;
|
||||
readonly target: string;
|
||||
readonly detail: string;
|
||||
}
|
||||
|
||||
const CONFIG_REF_KEYS = ["preinstall", "projectManagementSource", "gatewayProfile"] as const satisfies readonly HwpodPreinstallConfigRefKey[];
|
||||
|
||||
const REQUIRED_TARGET_SHAPES: Record<HwpodPreinstallConfigRefKey, RequiredTargetShape> = {
|
||||
preinstall: {
|
||||
kind: "object",
|
||||
requiredPaths: [
|
||||
"hwpodId",
|
||||
"sourceRef.spec",
|
||||
"metadataRef",
|
||||
"targetDevice.board",
|
||||
"targetDevice.mcu",
|
||||
"nodeBinding.hwlabNode",
|
||||
"nodeBinding.lane",
|
||||
"nodeBinding.nodeId",
|
||||
"workspaceRootRef",
|
||||
"projectRoot",
|
||||
"toolchain.name",
|
||||
"toolchain.keilProject",
|
||||
"toolchain.keilTarget",
|
||||
"toolchain.keilCliPath",
|
||||
"debugProbe.type",
|
||||
"debugProbe.probeUid",
|
||||
"uart.id",
|
||||
"uart.port",
|
||||
"uart.baudRate",
|
||||
"boardComm.endpoints[0].id",
|
||||
"ioProbe.uart.id",
|
||||
"runtimeMount.namespace",
|
||||
"runtimeMount.configMapName",
|
||||
"runtimeMount.mountPath",
|
||||
"runtimeMount.envKey",
|
||||
"runtimeMount.rolloutTarget.name",
|
||||
"specDocument.metadata.name",
|
||||
"specDocument.spec.workspace.keilProject",
|
||||
"metadataSidecar.contractVersion",
|
||||
],
|
||||
},
|
||||
projectManagementSource: {
|
||||
kind: "object",
|
||||
requiredPaths: [
|
||||
"sourceId",
|
||||
"sourceKind",
|
||||
"projectId",
|
||||
"hwpodId",
|
||||
"nodeId",
|
||||
"workspaceRootRef",
|
||||
"mdtodoRootRef",
|
||||
"focusFiles[0]",
|
||||
"hwpodNodeOpsUrlConfigRef",
|
||||
"runtimeEnv.envKey",
|
||||
"runtimeEnv.rolloutTarget.name",
|
||||
],
|
||||
},
|
||||
gatewayProfile: {
|
||||
kind: "object",
|
||||
requiredPaths: [
|
||||
"cloudUrl",
|
||||
"gatewayId",
|
||||
"sessionId",
|
||||
"resourceId",
|
||||
"capabilityId",
|
||||
"hwpodId",
|
||||
"nodeId",
|
||||
"nodeOps.serviceUrl",
|
||||
"nodeOps.publicUrl",
|
||||
"managedRun.mode",
|
||||
"managedRun.statusCommand",
|
||||
"secretRefs[0].sourceRef",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function hwlabNodeHwpodPreinstallHelp(): Record<string, unknown> {
|
||||
return {
|
||||
ok: true,
|
||||
command: "hwlab nodes hwpod-preinstall",
|
||||
description: "Render the YAML-first HWPOD preinstall configRef graph for a selected node/lane without mutating runtime.",
|
||||
configPath: hwlabRuntimeLaneConfigPath(),
|
||||
examples: [
|
||||
"bun scripts/cli.ts hwlab nodes hwpod-preinstall plan --node D601 --lane v03 --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes hwpod-preinstall status --node D601 --lane v03",
|
||||
],
|
||||
actions: {
|
||||
plan: "Validate D601/v03 hwpodPreinstall configRefs, cross-check ids, and render runtime artifact targets.",
|
||||
status: "Render the same redacted graph for the selected target as a read-only status view.",
|
||||
},
|
||||
notes: [
|
||||
"Root hwpodPreinstall must be declared under the selected lane target; this command does not use global defaults.",
|
||||
"Secret values, full YAML objects, host file content, and raw Markdown are not printed.",
|
||||
"Runtime apply/rollout is a later P3 step; plan/status are P2 visibility and friction-removal commands.",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export async function runHwlabNodeHwpodPreinstallCommand(args: string[]): Promise<RenderedCliResult | Record<string, unknown>> {
|
||||
if (args.length === 0 || args.includes("--help") || args.includes("-h") || args[0] === "help") return hwlabNodeHwpodPreinstallHelp();
|
||||
const options = parseHwpodPreinstallOptions(args);
|
||||
const spec = hwlabRuntimeLaneSpecForNode(options.lane, options.node);
|
||||
return withHwpodPreinstallRendered(hwpodPreinstallConfigPlan(spec, options.action, options.dryRun));
|
||||
}
|
||||
|
||||
function parseHwpodPreinstallOptions(args: string[]): HwpodPreinstallOptions {
|
||||
const [actionRaw] = args;
|
||||
if (actionRaw !== "plan" && actionRaw !== "status") {
|
||||
throw new Error("hwpod-preinstall usage: hwpod-preinstall plan|status --node NODE --lane vNN [--dry-run]");
|
||||
}
|
||||
assertKnownOptions(args, new Set(["--node", "--lane"]), new Set(["--dry-run", "--full"]));
|
||||
const node = requiredOption(args, "--node");
|
||||
assertNodeId(node);
|
||||
const lane = requiredOption(args, "--lane");
|
||||
assertLane(lane);
|
||||
if (!isHwlabRuntimeLane(lane)) throw new Error(`hwpod-preinstall only supports HWLAB runtime lanes, got ${lane}`);
|
||||
return { action: actionRaw, node, lane, dryRun: args.includes("--dry-run") };
|
||||
}
|
||||
|
||||
function hwpodPreinstallConfigPlan(spec: HwlabRuntimeLaneSpec, action: HwpodPreinstallAction, dryRun: boolean): HwpodPreinstallConfigPlan {
|
||||
const root = readHwpodPreinstallRoot(spec);
|
||||
const command = `hwlab nodes hwpod-preinstall ${action} --node ${spec.nodeId} --lane ${spec.lane}${dryRun ? " --dry-run" : ""}`;
|
||||
const refs = root.present && root.enabled ? CONFIG_REF_KEYS.map((key) => readConfigRef(key, root.configRefs[key] ?? "")) : [];
|
||||
const rootConflicts = root.error === null ? [] : [root.error];
|
||||
const conflicts = root.enabled ? [...rootConflicts, ...crossReferenceConflicts(spec, refs)] : rootConflicts;
|
||||
const refBlocked = refs.some((ref) => !ref.present || !ref.targetPresent || ref.missingFields.length > 0 || ref.conflicts.length > 0 || ref.error !== null);
|
||||
const ok = root.present && root.enabled && !refBlocked && conflicts.length === 0;
|
||||
return {
|
||||
ok,
|
||||
command,
|
||||
status: root.enabled ? ok ? "ready" : "blocked" : "disabled",
|
||||
node: spec.nodeId,
|
||||
lane: spec.lane,
|
||||
rootPath: root.rootPath,
|
||||
enabled: root.enabled,
|
||||
refs: refs.map(stripInternalTarget),
|
||||
conflicts,
|
||||
artifacts: runtimeArtifactRows(refs),
|
||||
next: nextCommands(spec.nodeId, spec.lane),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function withHwpodPreinstallRendered(value: HwpodPreinstallConfigPlan): RenderedCliResult {
|
||||
return {
|
||||
ok: value.ok,
|
||||
command: value.command,
|
||||
contentType: "text/plain",
|
||||
renderedText: renderHwpodPreinstallPlan(value),
|
||||
};
|
||||
}
|
||||
|
||||
function readHwpodPreinstallRoot(spec: HwlabRuntimeLaneSpec): HwpodPreinstallRoot {
|
||||
const rootFragment = `lanes.${spec.lane}.targets.${spec.nodeId}.hwpodPreinstall`;
|
||||
const root = `config/hwlab-node-lanes.yaml#${rootFragment}`;
|
||||
try {
|
||||
const text = readFileSync(rootPath(hwlabRuntimeLaneConfigPath()), "utf8");
|
||||
const doc = Bun.YAML.parse(text) as unknown;
|
||||
const target = valueAtPath(doc, rootFragment);
|
||||
if (!isRecord(target)) {
|
||||
return { present: false, enabled: false, rootPath: root, configRefs: {}, error: `${root} must be declared on the selected node/lane target` };
|
||||
}
|
||||
const enabled = target.enabled === true;
|
||||
const refs = isRecord(target.configRefs) ? target.configRefs : {};
|
||||
const configRefs: Partial<Record<HwpodPreinstallConfigRefKey, string>> = {};
|
||||
for (const key of CONFIG_REF_KEYS) {
|
||||
const value = refs[key];
|
||||
if (typeof value === "string" && value.length > 0) configRefs[key] = value;
|
||||
}
|
||||
const missingKeys = CONFIG_REF_KEYS.filter((key) => configRefs[key] === undefined);
|
||||
return {
|
||||
present: true,
|
||||
enabled,
|
||||
rootPath: root,
|
||||
configRefs,
|
||||
error: missingKeys.length === 0 ? null : `${root}.configRefs missing ${missingKeys.join(",")}`,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return { present: false, enabled: false, rootPath: root, configRefs: {}, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
function readConfigRef(key: HwpodPreinstallConfigRefKey, ref: string): InternalConfigRefStatus {
|
||||
const parsed = parseConfigRef(ref);
|
||||
if (parsed.error !== null) return emptyRefStatus(key, ref, parsed.file, parsed.path, parsed.error);
|
||||
const absPath = rootPath(parsed.file);
|
||||
if (!existsSync(absPath)) return emptyRefStatus(key, ref, parsed.file, parsed.path, `${parsed.file} does not exist`);
|
||||
try {
|
||||
const text = readFileSync(absPath, "utf8");
|
||||
const sha256 = `sha256:${createHash("sha256").update(text).digest("hex")}`;
|
||||
const doc = Bun.YAML.parse(text) as unknown;
|
||||
const target = valueAtPath(doc, parsed.path);
|
||||
const targetKind = target === undefined ? "missing" : targetKindOf(target);
|
||||
const missingFields = target === undefined ? ["target"] : missingFieldsForTarget(key, target);
|
||||
return {
|
||||
key,
|
||||
ref,
|
||||
file: parsed.file,
|
||||
path: parsed.path,
|
||||
present: true,
|
||||
targetPresent: target !== undefined,
|
||||
targetKind,
|
||||
sha256,
|
||||
byteCount: Buffer.byteLength(text),
|
||||
missingFields,
|
||||
conflicts: [],
|
||||
summary: summarizeTarget(key, target),
|
||||
error: null,
|
||||
target,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return emptyRefStatus(key, ref, parsed.file, parsed.path, message);
|
||||
}
|
||||
}
|
||||
|
||||
function parseConfigRef(ref: string): { readonly file: string; readonly path: string; readonly error: string | null } {
|
||||
const [file, path, extra] = ref.split("#");
|
||||
if (extra !== undefined || file === undefined || path === undefined || file.length === 0 || path.length === 0) {
|
||||
return { file: file ?? "", path: path ?? "", error: "configRef must use path/to/file.yaml#object.path" };
|
||||
}
|
||||
if (file.startsWith("/") || file.includes("\0") || file.includes("..") || !file.startsWith("config/") || !file.endsWith(".yaml")) {
|
||||
return { file, path, error: "configRef file must be repo-relative config/*.yaml without .." };
|
||||
}
|
||||
return { file, path, error: null };
|
||||
}
|
||||
|
||||
function emptyRefStatus(key: HwpodPreinstallConfigRefKey, ref: string, file: string, path: string, error: string): InternalConfigRefStatus {
|
||||
return {
|
||||
key,
|
||||
ref,
|
||||
file,
|
||||
path,
|
||||
present: false,
|
||||
targetPresent: false,
|
||||
targetKind: "missing",
|
||||
sha256: null,
|
||||
byteCount: null,
|
||||
missingFields: ["target"],
|
||||
conflicts: [],
|
||||
summary: "-",
|
||||
error,
|
||||
target: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function missingFieldsForTarget(key: HwpodPreinstallConfigRefKey, target: unknown): string[] {
|
||||
const shape = REQUIRED_TARGET_SHAPES[key];
|
||||
if (!isRecord(target)) return [`expected ${shape.kind}`];
|
||||
return shape.requiredPaths.filter((path) => valueAtPath(target, path) === undefined);
|
||||
}
|
||||
|
||||
function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly InternalConfigRefStatus[]): string[] {
|
||||
const byKey = new Map(refs.map((ref) => [ref.key, ref]));
|
||||
const preinstall = recordTarget(byKey.get("preinstall"));
|
||||
const pm = recordTarget(byKey.get("projectManagementSource"));
|
||||
const gateway = recordTarget(byKey.get("gatewayProfile"));
|
||||
const conflicts: string[] = [];
|
||||
|
||||
if (preinstall !== null) {
|
||||
requireEquals(conflicts, byKey.get("preinstall"), "nodeBinding.hwlabNode", spec.nodeId, `selected node ${spec.nodeId}`);
|
||||
requireEquals(conflicts, byKey.get("preinstall"), "nodeBinding.lane", spec.lane, `selected lane ${spec.lane}`);
|
||||
requireEquals(conflicts, byKey.get("preinstall"), "runtimeMount.namespace", spec.runtimeNamespace, `selected namespace ${spec.runtimeNamespace}`);
|
||||
requireSelfEquals(conflicts, byKey.get("preinstall"), "hwpodId", "specDocument.metadata.name");
|
||||
requireSelfEquals(conflicts, byKey.get("preinstall"), "workspaceRootRef", "specDocument.spec.workspace.path");
|
||||
requireSelfEquals(conflicts, byKey.get("preinstall"), "toolchain.keilProject", "specDocument.spec.workspace.keilProject");
|
||||
requireSelfEquals(conflicts, byKey.get("preinstall"), "toolchain.keilTarget", "specDocument.spec.workspace.keilTarget");
|
||||
requireSelfEquals(conflicts, byKey.get("preinstall"), "nodeBinding.nodeId", "specDocument.spec.nodeBinding.nodeId");
|
||||
}
|
||||
|
||||
const hwpodId = stringAt(preinstall, "hwpodId");
|
||||
const nodeId = stringAt(preinstall, "nodeBinding.nodeId");
|
||||
const workspaceRootRef = stringAt(preinstall, "workspaceRootRef");
|
||||
if (pm !== null) {
|
||||
requireEquals(conflicts, byKey.get("projectManagementSource"), "sourceKind", "hwpod-workspace", "required sourceKind hwpod-workspace");
|
||||
if (hwpodId !== null) requireEquals(conflicts, byKey.get("projectManagementSource"), "hwpodId", hwpodId, `preinstall hwpodId ${hwpodId}`);
|
||||
if (nodeId !== null) requireEquals(conflicts, byKey.get("projectManagementSource"), "nodeId", nodeId, `preinstall nodeId ${nodeId}`);
|
||||
if (workspaceRootRef !== null) requireEquals(conflicts, byKey.get("projectManagementSource"), "workspaceRootRef", workspaceRootRef, "preinstall workspaceRootRef");
|
||||
}
|
||||
if (gateway !== null) {
|
||||
requireEquals(conflicts, byKey.get("gatewayProfile"), "node", spec.nodeId, `selected node ${spec.nodeId}`);
|
||||
requireEquals(conflicts, byKey.get("gatewayProfile"), "lane", spec.lane, `selected lane ${spec.lane}`);
|
||||
if (hwpodId !== null) {
|
||||
requireEquals(conflicts, byKey.get("gatewayProfile"), "hwpodId", hwpodId, `preinstall hwpodId ${hwpodId}`);
|
||||
requireEquals(conflicts, byKey.get("gatewayProfile"), "resourceId", hwpodId, `preinstall hwpodId ${hwpodId}`);
|
||||
}
|
||||
if (nodeId !== null) requireEquals(conflicts, byKey.get("gatewayProfile"), "nodeId", nodeId, `preinstall nodeId ${nodeId}`);
|
||||
}
|
||||
const pmOpsRef = stringAt(pm, "hwpodNodeOpsUrlConfigRef");
|
||||
const gatewayRef = byKey.get("gatewayProfile");
|
||||
if (pmOpsRef !== null && gatewayRef !== undefined) {
|
||||
const expected = `${gatewayRef.file}#${gatewayRef.path}.nodeOps.serviceUrl`;
|
||||
if (pmOpsRef !== expected) {
|
||||
conflicts.push(`${byKey.get("projectManagementSource")?.file}#${byKey.get("projectManagementSource")?.path}.hwpodNodeOpsUrlConfigRef=${pmOpsRef} does not match gateway nodeOps serviceUrl ref ${expected}`);
|
||||
}
|
||||
}
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
function runtimeArtifactRows(refs: readonly InternalConfigRefStatus[]): HwpodPreinstallArtifactRow[] {
|
||||
const byKey = new Map(refs.map((ref) => [ref.key, ref]));
|
||||
const preinstall = recordTarget(byKey.get("preinstall"));
|
||||
const pm = recordTarget(byKey.get("projectManagementSource"));
|
||||
const gateway = recordTarget(byKey.get("gatewayProfile"));
|
||||
return [
|
||||
...(preinstall === null
|
||||
? []
|
||||
: [
|
||||
{
|
||||
kind: "ConfigMap",
|
||||
name: textAt(preinstall, "runtimeMount.configMapName"),
|
||||
target: textAt(preinstall, "runtimeMount.namespace"),
|
||||
detail: `mount=${textAt(preinstall, "runtimeMount.mountPath")} env=${textAt(preinstall, "runtimeMount.envKey")} rollout=${textAt(preinstall, "runtimeMount.rolloutTarget.name")}`,
|
||||
},
|
||||
{
|
||||
kind: "HWPOD",
|
||||
name: textAt(preinstall, "hwpodId"),
|
||||
target: textAt(preinstall, "nodeBinding.nodeId"),
|
||||
detail: `keil=${textAt(preinstall, "toolchain.keilTarget")} uart=${textAt(preinstall, "uart.port")}/${textAt(preinstall, "uart.baudRate")}`,
|
||||
},
|
||||
]),
|
||||
...(pm === null
|
||||
? []
|
||||
: [
|
||||
{
|
||||
kind: "PM source",
|
||||
name: textAt(pm, "sourceId"),
|
||||
target: textAt(pm, "projectId"),
|
||||
detail: `root=${textAt(pm, "mdtodoRootRef")} env=${textAt(pm, "runtimeEnv.envKey")} focusFiles=${arrayAt(pm, "focusFiles").length}`,
|
||||
},
|
||||
]),
|
||||
...(gateway === null
|
||||
? []
|
||||
: [
|
||||
{
|
||||
kind: "Gateway",
|
||||
name: textAt(gateway, "gatewayId"),
|
||||
target: textAt(gateway, "resourceId"),
|
||||
detail: `mode=${textAt(gateway, "managedRun.mode")} status=${lastPathSegment(textAt(gateway, "managedRun.statusCommand"))}`,
|
||||
},
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
function requireEquals(conflicts: string[], ref: InternalConfigRefStatus | undefined, path: string, expected: string, expectedLabel: string): void {
|
||||
if (ref === undefined) return;
|
||||
const actual = stringAt(ref.target, path);
|
||||
if (actual !== null && actual !== expected) {
|
||||
conflicts.push(`${ref.file}#${ref.path}.${path}=${actual} does not match ${expectedLabel}`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireSelfEquals(conflicts: string[], ref: InternalConfigRefStatus | undefined, firstPath: string, secondPath: string): void {
|
||||
if (ref === undefined) return;
|
||||
const first = stringAt(ref.target, firstPath);
|
||||
const second = stringAt(ref.target, secondPath);
|
||||
if (first !== null && second !== null && first !== second) {
|
||||
conflicts.push(`${ref.file}#${ref.path}.${firstPath}=${first} does not match ${secondPath}=${second}`);
|
||||
}
|
||||
}
|
||||
|
||||
function stripInternalTarget(ref: InternalConfigRefStatus): HwpodPreinstallConfigRefStatus {
|
||||
return {
|
||||
key: ref.key,
|
||||
ref: ref.ref,
|
||||
file: ref.file,
|
||||
path: ref.path,
|
||||
present: ref.present,
|
||||
targetPresent: ref.targetPresent,
|
||||
targetKind: ref.targetKind,
|
||||
sha256: ref.sha256,
|
||||
byteCount: ref.byteCount,
|
||||
missingFields: ref.missingFields,
|
||||
conflicts: ref.conflicts,
|
||||
summary: ref.summary,
|
||||
error: ref.error,
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeTarget(key: HwpodPreinstallConfigRefKey, target: unknown): string {
|
||||
if (target === undefined) return "target=missing";
|
||||
if (!isRecord(target)) return `kind=${targetKindOf(target)}`;
|
||||
if (key === "preinstall") {
|
||||
return `hwpod=${textAt(target, "hwpodId")} keil=${textAt(target, "toolchain.keilTarget")} uart=${textAt(target, "uart.port")}/${textAt(target, "uart.baudRate")} board=${textAt(target, "targetDevice.board")}`;
|
||||
}
|
||||
if (key === "projectManagementSource") {
|
||||
return `source=${textAt(target, "sourceId")} project=${textAt(target, "projectId")} root=${textAt(target, "mdtodoRootRef")} focusFiles=${arrayAt(target, "focusFiles").length}`;
|
||||
}
|
||||
if (key === "gatewayProfile") {
|
||||
return `gateway=${textAt(target, "gatewayId")} resource=${textAt(target, "resourceId")} mode=${textAt(target, "managedRun.mode")} cloud=${textAt(target, "cloudUrl")}`;
|
||||
}
|
||||
return `keys=${Object.keys(target).length}`;
|
||||
}
|
||||
|
||||
function renderHwpodPreinstallPlan(value: HwpodPreinstallConfigPlan): string {
|
||||
const refSummary = value.refs.length === 0
|
||||
? ["CONFIG REFS", " none (root disabled or not declared)"]
|
||||
: [
|
||||
hwpodTable(
|
||||
["KEY", "PRESENT", "TARGET", "TYPE", "HASH", "MISSING", "SUMMARY"],
|
||||
value.refs.map((ref) => [
|
||||
ref.key,
|
||||
ref.present,
|
||||
ref.targetPresent,
|
||||
ref.targetKind,
|
||||
ref.sha256 === null ? "-" : `${ref.sha256.slice(0, 19)}...`,
|
||||
ref.missingFields.length === 0 ? "-" : short(ref.missingFields.join(","), 52),
|
||||
short(ref.summary, 120),
|
||||
]),
|
||||
),
|
||||
"",
|
||||
hwpodTable(
|
||||
["KEY", "FILE", "PATH", "BYTES"],
|
||||
value.refs.map((ref) => [ref.key, ref.file, ref.path, ref.byteCount ?? "-"]),
|
||||
),
|
||||
];
|
||||
const artifactSummary = value.artifacts.length === 0
|
||||
? ["ARTIFACTS", " none"]
|
||||
: [
|
||||
hwpodTable(
|
||||
["KIND", "NAME", "TARGET", "DETAIL"],
|
||||
value.artifacts.map((item) => [item.kind, item.name, item.target, short(item.detail, 120)]),
|
||||
),
|
||||
];
|
||||
const blocked = value.ok ? [] : [
|
||||
"",
|
||||
"Blocked detail:",
|
||||
hwpodTable(["KIND", "VALUE"], [
|
||||
...value.conflicts.map((item) => ["conflict", short(item, 150)]),
|
||||
...value.refs.flatMap((ref) => [
|
||||
...(ref.error === null ? [] : [[`${ref.key}.error`, short(ref.error, 150)]]),
|
||||
...(ref.missingFields.length === 0 ? [] : [[`${ref.key}.missing`, short(ref.missingFields.join(","), 150)]]),
|
||||
...(ref.conflicts.length === 0 ? [] : [[`${ref.key}.conflict`, short(ref.conflicts.join(" | "), 150)]]),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
return [
|
||||
`hwlab nodes hwpod-preinstall ${commandAction(value.command)} (${value.status})`,
|
||||
"",
|
||||
hwpodTable(["NODE", "LANE", "ENABLED", "OK", "ROOT"], [[value.node, value.lane, value.enabled, value.ok, value.rootPath]]),
|
||||
"",
|
||||
...refSummary,
|
||||
"",
|
||||
...artifactSummary,
|
||||
...blocked,
|
||||
"",
|
||||
"NEXT",
|
||||
` plan: ${value.next.plan}`,
|
||||
` status: ${value.next.status}`,
|
||||
` deploy: ${value.next.deploy}`,
|
||||
"DISCLOSURE",
|
||||
" valuesRedacted=true; secret values, raw Markdown, host file content, and full YAML objects are not printed.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function nextCommands(node: string, lane: string): Record<string, string> {
|
||||
return {
|
||||
plan: `bun scripts/cli.ts hwlab nodes hwpod-preinstall plan --node ${node} --lane ${lane} --dry-run`,
|
||||
status: `bun scripts/cli.ts hwlab nodes hwpod-preinstall status --node ${node} --lane ${lane}`,
|
||||
deploy: `P3: apply the rendered ConfigMap/env/gateway profile through controlled node/lane rollout for ${node}/${lane}`,
|
||||
};
|
||||
}
|
||||
|
||||
function valueAtPath(value: unknown, path: string): unknown {
|
||||
let current: unknown = value;
|
||||
for (const segment of path.split(".")) {
|
||||
if (segment.length === 0) return undefined;
|
||||
const match = /^(?:([A-Za-z0-9_-]+))?(?:\[(\d+)\])?$/u.exec(segment);
|
||||
if (match === null) return undefined;
|
||||
if (match[1] !== undefined) {
|
||||
if (!isRecord(current)) return undefined;
|
||||
current = current[match[1]];
|
||||
}
|
||||
if (match[2] !== undefined) {
|
||||
if (!Array.isArray(current)) return undefined;
|
||||
current = current[Number(match[2])];
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function stringAt(value: unknown, path: string): string | null {
|
||||
const found = valueAtPath(value, path);
|
||||
return typeof found === "string" && found.length > 0 ? found : null;
|
||||
}
|
||||
|
||||
function textAt(value: unknown, path: string): string {
|
||||
const found = valueAtPath(value, path);
|
||||
if (typeof found === "string") return found;
|
||||
if (typeof found === "number" || typeof found === "boolean") return String(found);
|
||||
return "-";
|
||||
}
|
||||
|
||||
function arrayAt(value: unknown, path: string): unknown[] {
|
||||
const found = valueAtPath(value, path);
|
||||
return Array.isArray(found) ? found : [];
|
||||
}
|
||||
|
||||
function recordTarget(ref: InternalConfigRefStatus | undefined): Record<string, unknown> | null {
|
||||
return ref !== undefined && isRecord(ref.target) ? ref.target : null;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function targetKindOf(value: unknown): "object" | "array" | "scalar" | "null" {
|
||||
if (value === null) return "null";
|
||||
if (Array.isArray(value)) return "array";
|
||||
if (isRecord(value)) return "object";
|
||||
return "scalar";
|
||||
}
|
||||
|
||||
function commandAction(command: string): string {
|
||||
return command.includes(" status ") ? "status" : "plan";
|
||||
}
|
||||
|
||||
function hwpodTable(headers: string[], rows: unknown[][]): string {
|
||||
const normalized = [headers, ...rows.map((row) => row.map((cell) => hwpodText(cell)))];
|
||||
const widths = headers.map((_, index) => Math.max(...normalized.map((row) => hwpodText(row[index] ?? "").length)));
|
||||
return normalized.map((row) => row.map((cell, index) => hwpodText(cell).padEnd(widths[index])).join(" ").trimEnd()).join("\n");
|
||||
}
|
||||
|
||||
function hwpodText(value: unknown): string {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
if (typeof value === "boolean") return value ? "true" : "false";
|
||||
return String(value).replace(/\s+/gu, " ").trim();
|
||||
}
|
||||
|
||||
function short(value: string, maxLength: number): string {
|
||||
if (value.length <= maxLength) return value;
|
||||
if (maxLength <= 1) return value.slice(0, maxLength);
|
||||
return `${value.slice(0, maxLength - 1)}~`;
|
||||
}
|
||||
|
||||
function lastPathSegment(value: string): string {
|
||||
const normalized = value.replace(/\\/gu, "/");
|
||||
const segment = normalized.split("/").filter(Boolean).pop();
|
||||
return segment ?? value;
|
||||
}
|
||||
@@ -419,6 +419,10 @@ export async function runHwlabNodeCommand(_config: Config, args: string[]): Prom
|
||||
const { runHwlabTestAccountsCommand } = await import("../hwlab-test-accounts");
|
||||
return runHwlabTestAccountsCommand(args.slice(1));
|
||||
}
|
||||
if (domain === "hwpod-preinstall") {
|
||||
const { runHwlabNodeHwpodPreinstallCommand } = await import("../hwlab-node-hwpod-preinstall");
|
||||
return runHwlabNodeHwpodPreinstallCommand(args.slice(1));
|
||||
}
|
||||
if (domain === "web-probe") {
|
||||
return legacyHwlabNodeWebProbeUnsupported(args.slice(1));
|
||||
}
|
||||
@@ -431,7 +435,7 @@ export async function runHwlabNodeCommand(_config: Config, args: string[]): Prom
|
||||
return runNodeDelegatedDomain(_config, domain, args.slice(1));
|
||||
}
|
||||
if (domain !== "secret") {
|
||||
return { ok: false, command: `hwlab nodes ${domain ?? ""}`.trim(), message: "supported commands: hwlab nodes control-plane, hwlab nodes git-mirror, hwlab nodes observability, hwlab nodes secret, hwlab nodes test-accounts. web-probe moved to top-level: bun scripts/cli.ts web-probe --help" };
|
||||
return { ok: false, command: `hwlab nodes ${domain ?? ""}`.trim(), message: "supported commands: hwlab nodes control-plane, hwlab nodes git-mirror, hwlab nodes hwpod-preinstall, hwlab nodes observability, hwlab nodes secret, hwlab nodes test-accounts. web-probe moved to top-level: bun scripts/cli.ts web-probe --help" };
|
||||
}
|
||||
const options = parseSecretOptions(args.slice(1));
|
||||
return runNodeSecret(options);
|
||||
|
||||
Reference in New Issue
Block a user