583 lines
22 KiB
TypeScript
583 lines
22 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. entry module for scripts/src/hwlab-node-impl.ts.
|
|
|
|
// Moved mechanically from scripts/src/hwlab-node-impl.ts:1-439 for #903.
|
|
|
|
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
|
|
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
|
|
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-multi-web-probe-sentinel.
|
|
// Responsibility: YAML-first node/lane operations, including Workbench observability control commands.
|
|
import { createHash, randomBytes } from "node:crypto";
|
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { repoRoot, rootPath, type Config } from "../config";
|
|
import { runCommand, type CommandResult } from "../command";
|
|
import { startJob } from "../jobs";
|
|
import { classifySshTcpPoolFailure } from "../ssh";
|
|
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
|
|
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, hwlabRuntimeLaneSpecForNode, hwlabRuntimeNodeIds, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeObservabilityRecordingRuleSpec, type HwlabRuntimeObservabilitySpec, type HwlabRuntimeObservabilityWarningAlertSpec, type HwlabRuntimePublicExposureSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec, type HwlabRuntimeWebProbeProjectManagementSpec } from "../hwlab-node-lanes";
|
|
import { nodeWebProbeScriptRunnerSource } from "../hwlab-node-web-probe-runner-source";
|
|
import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source";
|
|
import { nodeWebObserveRunnerSource } from "../hwlab-node-web-observe-runner-source";
|
|
import { nodeWebObserveCollectViewNodeScript, parseNodeWebProbeObserveCollectView, type NodeWebProbeObserveCollectView } from "../hwlab-node-web-observe-collect";
|
|
import { withWebObserveCollectRendered, withWebObserveCommandRendered, withWebObserveStatusRendered } from "../hwlab-node-web-observe-render";
|
|
import { buildWebObserveWrapperForObserveOptions, webObserveWrapperStateDirFromStatus } from "../hwlab-node-web-observe-wrapper";
|
|
import { renderWebObserveWrapperContract } from "../hwlab-node-web-observe-wrapper-render";
|
|
import { runWebProbeSentinelCommand, type WebProbeSentinelOptions } from "../hwlab-node-web-sentinel-cicd";
|
|
import { hwlabNodeHelp, hwlabNodeObservabilityHelp, hwlabNodeWebProbeHelp } from "../hwlab-node-help";
|
|
import { compactWebProbeResult, compactWebProbeScriptResult } from "../hwlab-node-web-probe-summary";
|
|
import { nodeObservabilityRecordingRuleExpression, nodeObservabilityRecordingRuleSummaries, nodeObservabilityWarningAlertExpression, nodeObservabilityWarningAlertSummaries } from "../hwlab-node-observability-promql";
|
|
import { runDelegatedHwlabNodeCommand, type DelegatedNodeDomain } from "../hwlab-node-transport";
|
|
import type { RenderedCliResult } from "../output";
|
|
|
|
import { nodeRuntimeControlPlaneRun } from "./cleanup";
|
|
import { nodeRuntimeBaseImageCommand } from "./control-actions";
|
|
import { withNodeRuntimeControlPlanePlanRendered, withNodeRuntimeControlPlaneStatusFullRendered, withNodeRuntimeControlPlaneStatusRendered } from "./git-mirror";
|
|
import { nodeRuntimeControlPlanePlan, runNodeObservability } from "./observability";
|
|
import { parseNodeScopedDelegatedOptions } from "./plan";
|
|
import { parseSecretOptions, runNodePublicExposure, runNodeSecret } from "./public-exposure";
|
|
import { nodeRuntimeControlPlaneStatus } from "./render";
|
|
import { nodeRuntimeUnsupportedAction } from "./runtime-common";
|
|
import { runNodeEndpointBridge } from "./secret-scripts";
|
|
import { nodeRuntimeGitMirrorRun, nodeRuntimeGitMirrorStatus, nodeScopedFullOutput, withNodeRuntimeGitMirrorRendered } from "./status";
|
|
import { assertNodeId, positiveIntegerOption, requiredOption, stripOption } from "./utils";
|
|
import { legacyHwlabNodeWebProbeUnsupported } from "../web-probe";
|
|
import { rewriteDelegatedNodeResult, startNodeDelegatedJob } from "./web-probe";
|
|
import { assertKnownOptions } from "./web-probe-observe";
|
|
|
|
export { hwlabNodeHelp, hwlabNodeWebProbeHelp, hwlabNodeObservabilityHelp } from "../hwlab-node-help";
|
|
|
|
export type SecretAction = "status" | "ensure" | "cleanup-owned-postgres" | "cleanup-obsolete";
|
|
|
|
export type SecretPreset = "openfga" | "master-server-admin-api-key" | "bootstrap-admin" | "code-agent-provider" | "cloud-api-db" | "owned-postgres-cleanup" | "obsolete-secret-cleanup";
|
|
|
|
export type NodeRuntimeRenderLocation = "node-host" | "local";
|
|
|
|
export type WebProbeBrowserProxyMode = "auto" | "direct";
|
|
|
|
export interface NodeWebProbeRunOptions {
|
|
action: "run";
|
|
node: string;
|
|
lane: string;
|
|
url: string;
|
|
timeoutMs: number;
|
|
waitAfterSubmitMs: number;
|
|
waitMessagesMs: number;
|
|
waitAgentTerminalMs: number;
|
|
traceSampleCount: number;
|
|
traceSampleIntervalMs: number;
|
|
message: string | null;
|
|
conversationId: string | null;
|
|
freshSession: boolean;
|
|
cancelRunning: boolean;
|
|
commandTimeoutSeconds: number;
|
|
commandTimeoutAutoSeconds: number;
|
|
commandTimeoutUserProvided: boolean;
|
|
}
|
|
|
|
export interface NodeWebProbeScriptOptions {
|
|
action: "script";
|
|
node: string;
|
|
lane: string;
|
|
url: string;
|
|
timeoutMs: number;
|
|
viewport: string;
|
|
browserProxyMode: WebProbeBrowserProxyMode;
|
|
commandTimeoutSeconds: number;
|
|
scriptText: string;
|
|
scriptSource: {
|
|
kind: "stdin" | "file";
|
|
path: string | null;
|
|
byteCount: number;
|
|
sha256: string;
|
|
};
|
|
}
|
|
|
|
export interface NodeWebProbeScreenshotOptions {
|
|
action: "screenshot";
|
|
node: string;
|
|
lane: string;
|
|
url: string;
|
|
path: string | null;
|
|
viewport: string;
|
|
localDir: string;
|
|
name: string;
|
|
timeoutMs: number;
|
|
waitUntil: "load" | "domcontentloaded" | "networkidle" | "commit";
|
|
fullPage: boolean;
|
|
selector: string | null;
|
|
keepRemote: boolean;
|
|
waitTimeoutMs: number;
|
|
commandTimeoutSeconds: number;
|
|
}
|
|
|
|
export type NodeWebProbeObserveAction = "start" | "status" | "command" | "stop" | "collect" | "analyze";
|
|
|
|
export type NodeWebProbeObserveCommandType =
|
|
| "login"
|
|
| "loginAccount"
|
|
| "logout"
|
|
| "listSessions"
|
|
| "switchSessions"
|
|
| "preflight"
|
|
| "goto"
|
|
| "gotoProjectMdtodo"
|
|
| "newSession"
|
|
| "sendPrompt"
|
|
| "steer"
|
|
| "cancel"
|
|
| "selectProvider"
|
|
| "clickSession"
|
|
| "refreshCurrentSession"
|
|
| "switchAwayAndBack"
|
|
| "assertSessionInvariant"
|
|
| "selectProjectSource"
|
|
| "selectMdtodoSource"
|
|
| "selectMdtodoFile"
|
|
| "selectMdtodoTask"
|
|
| "expandMdtodoTask"
|
|
| "openMdtodoReportPreview"
|
|
| "toggleMdtodoReportFullscreen"
|
|
| "openMdtodoSourceConfig"
|
|
| "closeMdtodoSourceConfig"
|
|
| "configureMdtodoHwpodSource"
|
|
| "probeMdtodoSource"
|
|
| "reindexMdtodoSource"
|
|
| "editMdtodoTaskInline"
|
|
| "editMdtodoTaskTitle"
|
|
| "editMdtodoTaskBody"
|
|
| "toggleMdtodoTaskStatus"
|
|
| "addMdtodoRootTask"
|
|
| "addMdtodoSubTask"
|
|
| "continueMdtodoTask"
|
|
| "deleteMdtodoTask"
|
|
| "launchWorkbenchFromTask"
|
|
| "launchWorkbenchFromMdtodo"
|
|
| "screenshot"
|
|
| "mark"
|
|
| "stop";
|
|
|
|
export interface NodeWebProbeObserveOptions {
|
|
action: "observe";
|
|
observeAction: NodeWebProbeObserveAction;
|
|
id: string | null;
|
|
node: string;
|
|
lane: string;
|
|
url: string;
|
|
targetPath: string;
|
|
viewport: string;
|
|
browserProxyMode: WebProbeBrowserProxyMode;
|
|
sampleIntervalMs: number;
|
|
screenshotIntervalMs: number;
|
|
observerRefreshIntervalMs: number;
|
|
maxSamples: number;
|
|
commandTimeoutSeconds: number;
|
|
waitMs: number;
|
|
tailLines: number;
|
|
maxFiles: number;
|
|
collectView: NodeWebProbeObserveCollectView;
|
|
collectFile: string | null;
|
|
collectFinding: string | null;
|
|
collectGrep: string | null;
|
|
collectTraceId: string | null;
|
|
collectSampleSeq: number | null;
|
|
collectTimestamp: string | null;
|
|
collectTurn: number | null;
|
|
collectCommandId: string | null;
|
|
collectWindowMs: number | null;
|
|
analyzeArchivePrefix: string | null;
|
|
analyzeTailSamples: number | null;
|
|
full: boolean;
|
|
raw: boolean;
|
|
compactRaw: boolean;
|
|
stateDir: string | null;
|
|
jobId: string | null;
|
|
force: boolean;
|
|
commandType: NodeWebProbeObserveCommandType | null;
|
|
commandText: string | null;
|
|
commandPath: string | null;
|
|
commandLabel: string | null;
|
|
commandSessionId: string | null;
|
|
commandProvider: string | null;
|
|
commandAfterRound: number | null;
|
|
commandSeverity: string | null;
|
|
commandAlternateSessionStrategy: string | null;
|
|
commandExpectedSentinelRange: string | null;
|
|
commandExpectedActionWaitMs: number | null;
|
|
commandRequireComposerReady: boolean;
|
|
commandWaitProjectManagementReady: boolean;
|
|
commandFindingId: string | null;
|
|
commandBlocking: boolean | null;
|
|
commandAccountId: string | null;
|
|
commandFromAccountId: string | null;
|
|
commandToAccountId: string | null;
|
|
commandSourceId: string | null;
|
|
commandFileRef: string | null;
|
|
commandFilename: string | null;
|
|
commandTaskRef: string | null;
|
|
commandTaskId: string | null;
|
|
commandField: string | null;
|
|
commandLink: string | null;
|
|
commandTitle: string | null;
|
|
commandBody: string | null;
|
|
commandStatus: string | null;
|
|
commandHwpodId: string | null;
|
|
commandNodeId: string | null;
|
|
commandWorkspaceRoot: string | null;
|
|
commandRoot: string | null;
|
|
}
|
|
|
|
export interface NodeWebProbeSentinelOptions {
|
|
action: "sentinel";
|
|
sentinel: WebProbeSentinelOptions;
|
|
node: string;
|
|
lane: string;
|
|
}
|
|
|
|
export type NodeWebProbeOptions = NodeWebProbeRunOptions | NodeWebProbeScriptOptions | NodeWebProbeScreenshotOptions | NodeWebProbeObserveOptions | NodeWebProbeSentinelOptions;
|
|
|
|
export interface WebObserveIndexEntry {
|
|
id: string;
|
|
node: string;
|
|
lane: string;
|
|
workspace: string;
|
|
stateDir: string;
|
|
url: string;
|
|
targetPath: string;
|
|
status: string | null;
|
|
pid: number | null;
|
|
startedAt: string | null;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export type NodeObservabilityAction = "plan" | "apply" | "status" | "workbench-summary" | "performance-summary";
|
|
|
|
export interface NodeObservabilityOptions {
|
|
action: NodeObservabilityAction;
|
|
node: string;
|
|
lane: HwlabRuntimeLane;
|
|
confirm: boolean;
|
|
dryRun: boolean;
|
|
full: boolean;
|
|
timeoutSeconds: number;
|
|
spec: HwlabRuntimeLaneSpec;
|
|
}
|
|
|
|
export interface NodeRuntimeRenderResult {
|
|
readonly result: CommandResult;
|
|
readonly renderDir: string;
|
|
readonly worktreeDir: string;
|
|
readonly location: NodeRuntimeRenderLocation;
|
|
}
|
|
|
|
export interface NodeRuntimeCleanupPipelineRunRow {
|
|
name: string;
|
|
createdAt: string | null;
|
|
ageMinutes: number | null;
|
|
status: string | null;
|
|
reason: string | null;
|
|
selected?: boolean;
|
|
selectedReason?: string;
|
|
}
|
|
|
|
export interface NodeRuntimeCleanupOptions {
|
|
minAgeMinutes: number;
|
|
limit: number;
|
|
sourceCommit?: string;
|
|
pipelineRun?: string;
|
|
targetPipelineRun?: string;
|
|
includeActive: boolean;
|
|
dryRun: boolean;
|
|
}
|
|
|
|
export interface NodeSecretOptions {
|
|
action: SecretAction;
|
|
node: string;
|
|
lane: string;
|
|
name: string;
|
|
key?: string;
|
|
preset: SecretPreset;
|
|
dryRun: boolean;
|
|
confirm: boolean;
|
|
force?: boolean;
|
|
timeoutSeconds: number;
|
|
}
|
|
|
|
export interface BootstrapAdminSecretMaterial {
|
|
ok: boolean;
|
|
sourceRef: string | null;
|
|
sourceKey: string | null;
|
|
sourcePath: string | null;
|
|
sourcePresent: boolean;
|
|
sourceFingerprint: string | null;
|
|
passwordHash: string | null;
|
|
error: string | null;
|
|
}
|
|
|
|
export interface BootstrapAdminPasswordMaterial {
|
|
ok: boolean;
|
|
sourceRef: string | null;
|
|
sourceKey: string | null;
|
|
sourcePath: string | null;
|
|
sourcePresent: boolean;
|
|
sourceFingerprint: string | null;
|
|
password: string | null;
|
|
error: string | null;
|
|
}
|
|
|
|
export interface NodePublicExposureOptions {
|
|
action: "public-exposure";
|
|
node: string;
|
|
lane: HwlabRuntimeLane;
|
|
dryRun: boolean;
|
|
confirm: boolean;
|
|
timeoutSeconds: number;
|
|
spec: HwlabRuntimeLaneSpec;
|
|
}
|
|
|
|
export interface RuntimeSecretSpec {
|
|
node: string;
|
|
lane: string;
|
|
namespace: string;
|
|
platformDb: boolean;
|
|
runtimeLaneSpec?: HwlabRuntimeLaneSpec;
|
|
externalPostgres?: NonNullable<HwlabRuntimeLaneSpec["externalPostgres"]>;
|
|
platformPostgresService: string;
|
|
platformPostgresEndpointAddress?: string;
|
|
platformPostgresEndpointSlice: string;
|
|
postgresSecret: string;
|
|
postgresStatefulSet: string;
|
|
postgresAdminUser: string;
|
|
openFgaSecret: string;
|
|
openFgaDbName: string;
|
|
openFgaDbUser: string;
|
|
openFgaDbHost: string;
|
|
masterAdminApiKeySecret: string;
|
|
bootstrapAdminSecret: string;
|
|
bootstrapAdminPasswordHashKey: string;
|
|
bootstrapAdminUsername: string;
|
|
bootstrapAdminDisplayName: string;
|
|
bootstrapAdminPasswordSourceRef?: string;
|
|
bootstrapAdminPasswordSourceKey?: string;
|
|
bootstrapAdminPasswordHashTransform?: "hwlab-sha256";
|
|
bootstrapAdminSourceNamespace: string;
|
|
bootstrapAdminSourceSecret: string;
|
|
cloudApiDbSecret: string;
|
|
cloudApiDbKey: string;
|
|
cloudApiDbName: string;
|
|
cloudApiDbUser: string;
|
|
cloudApiDbHost: string;
|
|
cloudApiDeployment: string;
|
|
obsoleteHwpodDbSecret: string;
|
|
obsoleteHwpodDbName: string;
|
|
obsoleteHwpodDbUser: string;
|
|
codeAgentProviderSecret: string;
|
|
codeAgentProviderSourceNamespace: string;
|
|
codeAgentProviderSourceSecret: string;
|
|
fieldManager: string;
|
|
}
|
|
|
|
export interface NodeRuntimeGitMirrorTargetSpec {
|
|
id: string;
|
|
node: string;
|
|
lane: string;
|
|
namespace: string;
|
|
serviceReadName: string;
|
|
serviceWriteName: string;
|
|
cachePvcName: string;
|
|
cachePvcStorage: string;
|
|
cacheHostPath: string | null;
|
|
servicePort: number;
|
|
secretName: string;
|
|
syncConfigMapName: string;
|
|
syncJobPrefix: string;
|
|
flushJobPrefix: string;
|
|
toolsImage: string;
|
|
toolsImagePullPolicy: "Always" | "IfNotPresent" | "Never";
|
|
sourceRepository: string;
|
|
sourceBranch: string;
|
|
gitopsBranch: string;
|
|
egressProxy: NodeRuntimeGitMirrorEgressProxySpec;
|
|
githubTransport: NodeRuntimeGitMirrorGithubTransportSpec;
|
|
}
|
|
|
|
export type NodeRuntimeGitMirrorGithubTransportSpec =
|
|
| { mode: "ssh" }
|
|
| {
|
|
mode: "https";
|
|
username: string;
|
|
tokenSecretName: string;
|
|
tokenSecretKey: string;
|
|
tokenSourceRef: string;
|
|
tokenSourceKey: string;
|
|
};
|
|
|
|
export type NodeRuntimeGitMirrorEgressProxySpec =
|
|
| { mode: "direct"; required: false }
|
|
| {
|
|
mode: "k8s-service-cluster-ip";
|
|
clientName: string;
|
|
namespace: string;
|
|
serviceName: string;
|
|
port: number;
|
|
sourceConfigRef: string | null;
|
|
sourceRef: string;
|
|
sourceKey: string;
|
|
sourceType: "subscription-url" | "master-shadowsocks";
|
|
preferredOutbound: "vless-reality" | "hysteria2" | null;
|
|
sourceFingerprint: string | null;
|
|
noProxy: string[];
|
|
};
|
|
|
|
export const MASTER_ADMIN_API_KEY_KEY = "api-key";
|
|
|
|
export const BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY = "password-hash";
|
|
|
|
export const BOOTSTRAP_ADMIN_SOURCE_NAMESPACE = "hwlab-v02";
|
|
|
|
export const BOOTSTRAP_ADMIN_SOURCE_SECRET = "hwlab-v02-bootstrap-admin";
|
|
|
|
export const OPENFGA_AUTHN_KEY = "authn-preshared-key";
|
|
|
|
export const OPENFGA_DATASTORE_URI_KEY = "datastore-uri";
|
|
|
|
export const OPENFGA_POSTGRES_PASSWORD_KEY = "postgres-password";
|
|
|
|
export const CLOUD_API_DB_KEY = "database-url";
|
|
|
|
export const CODE_AGENT_PROVIDER_OPENAI_KEY = "openai-api-key";
|
|
|
|
export const CODE_AGENT_PROVIDER_OPENCODE_KEY = "opencode-api-key";
|
|
|
|
export const CODE_AGENT_PROVIDER_SOURCE_NAMESPACE = "hwlab-v02";
|
|
|
|
export const CODE_AGENT_PROVIDER_SOURCE_SECRET = "hwlab-v02-code-agent-provider";
|
|
|
|
export const HWLAB_CI_NAMESPACE = "hwlab-ci";
|
|
|
|
export const NODE_RUNTIME_CICD_WAIT_WARNING_SECONDS = 120;
|
|
|
|
export const NODE_RUNTIME_TRIGGER_SEVERE_WARNING_MS = NODE_RUNTIME_CICD_WAIT_WARNING_SECONDS * 1000;
|
|
|
|
export async function runHwlabNodeCommand(_config: Config, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
|
|
if (args.length === 0) return hwlabNodeHelp();
|
|
const [domain] = args;
|
|
if (domain === "control-plane" && args[1] === "infra") {
|
|
if (args.length === 2 || args.includes("--help") || args.includes("-h") || args[2] === "help") return hwlabNodeControlPlaneInfraHelp();
|
|
return runHwlabNodeControlPlaneInfra(args.slice(2));
|
|
}
|
|
if (domain === "test-accounts") {
|
|
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));
|
|
}
|
|
if (domain === "observability") {
|
|
if (args.length === 1 || args.includes("--help") || args.includes("-h") || args[1] === "help") return hwlabNodeObservabilityHelp();
|
|
return runNodeObservability(parseNodeObservabilityOptions(args.slice(1)));
|
|
}
|
|
if (args.includes("--help") || args.includes("-h")) return hwlabNodeHelp();
|
|
if (domain === "control-plane" || domain === "git-mirror") {
|
|
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 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);
|
|
}
|
|
|
|
export function parseNodeObservabilityOptions(args: string[]): NodeObservabilityOptions {
|
|
const [actionRaw] = args;
|
|
if (typeof actionRaw !== "string" || actionRaw.startsWith("--")) {
|
|
throw new Error("observability usage: observability ACTION --node NODE --lane vNN [--dry-run|--confirm]");
|
|
}
|
|
if (actionRaw !== "plan" && actionRaw !== "apply" && actionRaw !== "status" && actionRaw !== "workbench-summary" && actionRaw !== "performance-summary") {
|
|
throw new Error(`observability action must be plan, apply, status, workbench-summary, or performance-summary; got ${actionRaw}`);
|
|
}
|
|
assertKnownOptions(args, new Set(["--node", "--lane", "--timeout-seconds"]), new Set(["--dry-run", "--confirm", "--full", "--raw"]));
|
|
const node = requiredOption(args, "--node");
|
|
assertNodeId(node);
|
|
const laneRaw = requiredOption(args, "--lane");
|
|
if (!isHwlabRuntimeLane(laneRaw)) throw new Error(`--lane must be one of v02, v03; got ${laneRaw}`);
|
|
const confirm = args.includes("--confirm");
|
|
const dryRun = args.includes("--dry-run");
|
|
if (confirm && dryRun) throw new Error("observability accepts only one of --confirm or --dry-run");
|
|
if (actionRaw === "apply" && !confirm && !dryRun) throw new Error("observability apply requires --dry-run or --confirm");
|
|
if (actionRaw !== "apply" && (confirm || dryRun)) throw new Error(`observability ${actionRaw} is read-only and does not accept --confirm or --dry-run`);
|
|
return {
|
|
action: actionRaw,
|
|
node,
|
|
lane: laneRaw,
|
|
confirm,
|
|
dryRun,
|
|
full: args.includes("--full") || args.includes("--raw"),
|
|
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600),
|
|
spec: hwlabRuntimeLaneSpecForNode(laneRaw, node),
|
|
};
|
|
}
|
|
|
|
export async function runNodeDelegatedDomain(config: Config, domain: DelegatedNodeDomain, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
|
|
const scoped = parseNodeScopedDelegatedOptions(domain, args);
|
|
const defaultSpec = hwlabRuntimeLaneSpec(scoped.lane);
|
|
if (domain === "control-plane" && scoped.action === "public-exposure") {
|
|
return runNodePublicExposure({
|
|
action: "public-exposure",
|
|
node: scoped.node,
|
|
lane: scoped.lane,
|
|
dryRun: scoped.dryRun || !scoped.confirm,
|
|
confirm: scoped.confirm,
|
|
timeoutSeconds: scoped.timeoutSeconds,
|
|
spec: scoped.spec,
|
|
});
|
|
}
|
|
if (domain === "control-plane" && scoped.action === "runtime-image") {
|
|
return nodeRuntimeBaseImageCommand(scoped);
|
|
}
|
|
if (domain === "control-plane" && scoped.action === "plan") {
|
|
const result = nodeRuntimeControlPlanePlan(scoped);
|
|
return nodeScopedFullOutput(scoped) ? result : withNodeRuntimeControlPlanePlanRendered(result, scoped);
|
|
}
|
|
if (domain === "control-plane" && scoped.node !== defaultSpec.nodeId) {
|
|
if (scoped.action === "status") {
|
|
const result = nodeRuntimeControlPlaneStatus(scoped);
|
|
if (scoped.originalArgs.includes("--raw")) return result;
|
|
if (scoped.originalArgs.includes("--full")) return withNodeRuntimeControlPlaneStatusFullRendered(result, scoped);
|
|
return withNodeRuntimeControlPlaneStatusRendered(result, scoped);
|
|
}
|
|
if (scoped.action === "apply" || scoped.action === "trigger-current" || scoped.action === "refresh" || scoped.action === "sync" || scoped.action === "runtime-migration" || scoped.action === "cleanup-runs") {
|
|
if (scoped.confirm && !scoped.dryRun && !scoped.wait) return startNodeDelegatedJob(scoped);
|
|
return nodeRuntimeControlPlaneRun(scoped);
|
|
}
|
|
return nodeRuntimeUnsupportedAction(scoped);
|
|
}
|
|
if (domain === "git-mirror" && scoped.node !== defaultSpec.nodeId) {
|
|
if (scoped.action === "status") {
|
|
const result = nodeRuntimeGitMirrorStatus(scoped);
|
|
return nodeScopedFullOutput(scoped) ? result : withNodeRuntimeGitMirrorRendered(result, scoped);
|
|
}
|
|
if (scoped.action === "sync" || scoped.action === "flush") {
|
|
if (scoped.confirm && !scoped.dryRun && !scoped.wait) return startNodeDelegatedJob(scoped);
|
|
const result = nodeRuntimeGitMirrorRun(scoped);
|
|
return nodeScopedFullOutput(scoped) ? result : withNodeRuntimeGitMirrorRendered(result, scoped);
|
|
}
|
|
return nodeRuntimeUnsupportedAction(scoped);
|
|
}
|
|
if (domain === "control-plane" && scoped.action === "allow-endpoint-bridge") {
|
|
return runNodeEndpointBridge(scoped);
|
|
}
|
|
if (domain === "control-plane" && scoped.action === "trigger-current" && scoped.confirm && !scoped.dryRun && !scoped.wait) {
|
|
return startNodeDelegatedJob(scoped);
|
|
}
|
|
if (domain === "git-mirror" && (scoped.action === "sync" || scoped.action === "flush") && scoped.confirm && !scoped.dryRun && !scoped.wait) {
|
|
return startNodeDelegatedJob(scoped);
|
|
}
|
|
const delegatedArgs = stripOption(args, "--node");
|
|
const result = await runDelegatedHwlabNodeCommand(config, domain, delegatedArgs);
|
|
return rewriteDelegatedNodeResult(result, scoped);
|
|
}
|