From a9214d36dd5143067756e3a346e7bf9ed23f0da4 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 27 May 2026 21:15:29 +0800 Subject: [PATCH] feat: add device pod cli mvp --- docs/plan/device-pod-cli-mvp.md | 6 +- docs/reference/device-pod.md | 4 +- package.json | 2 +- tools/device-pod-cli.mjs | 218 ++++++++++++++++++++++++++++++++ 4 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 tools/device-pod-cli.mjs diff --git a/docs/plan/device-pod-cli-mvp.md b/docs/plan/device-pod-cli-mvp.md index 1fafe2ec..c8cb27fb 100644 --- a/docs/plan/device-pod-cli-mvp.md +++ b/docs/plan/device-pod-cli-mvp.md @@ -32,7 +32,7 @@ MVP surface 固定为: - `debug-probe`:下载、复位、探针状态和芯片 ID。 - `io-probe`:UART、GPIO、AI/AO/DI/DO、状态采样和日志读取。 -workspace 操作采用 busybox 风格白名单,不提供 `cmd` 子命令。第一版建议只开放 `ls`、`cat`、`stat`、`rg`、`find`、`head`、`tail`、`wc`、`apply-patch`、`upload`、`download`、`build`、`clean` 和 `artifact list`。所有文件路径必须限制在 profile 声明的 workspace root 内。 +workspace 操作采用 busybox 风格白名单,不提供 `cmd` 子命令。已跑通的 CLI MVP 先开放 `ls`、`cat`、`rg`、`apply-patch` 和 `build`;`stat`、`find`、`head`、`tail`、`wc`、`upload`、`download`、`clean` 和 `artifact list` 作为后续扩展进入同一白名单模型。所有文件路径必须限制在 profile 声明的 workspace root 内。 示例: @@ -61,7 +61,7 @@ CLI 默认输出 JSON。成功和失败都必须包含 `devicePodId`、`targetId 1. 先实现 profile schema、loader、locator parser 和 JSON 输出合同,用 fake profile 在本地单元测试覆盖成功、缺 profile、坏 profile、路径越界和未知 surface。 2. 实现 workspace busybox 操作和 fake gateway adapter,先在不接硬件时证明路径裁剪、输出截断和错误结构稳定。 3. 实现 debug-probe 与 io-probe 的 adapter 接口,先用 fake `device-host-cli` 返回 chip ID、UART 样例和下载 job 状态。 -4. 在 D518 Windows 上开发 `device-host-cli`,通过 `D518:win` 做真实 DAPLink、串口或下载器 smoke;D518 侧只承载硬件上位机逻辑,不改变 G14 source truth。 +4. 在 D518 Windows 上开发自包含 `device-host-cli`,通过 `D518:win` 做真实 DAPLink、串口或下载器 smoke;Keil、串口监控、mklink 和文件编辑 skill 只能作为实现参考,不作为运行时依赖。D518 侧只承载硬件上位机逻辑,不改变 G14 source truth。 5. 在 G14 code agent pod 中开发和验证 `device-pod-cli`,code agent pod 不放完整 HWLAB 源码和 Git key;CLI 通过 skill 分发到预装位置,并在 skill 说明中写清仅适用于 HWLAB 内部 code agent。 6. fake 闭环通过后再做 G14 -> gateway -> D518 -> 硬件的最小 live smoke,然后把修复固化到源码、测试和长期参考。 @@ -78,7 +78,7 @@ CLI 默认输出 JSON。成功和失败都必须包含 `devicePodId`、`targetId MVP 通过至少需要满足: - `.device-pod/.json` 能被读取、校验、hash 并体现在每次 CLI JSON 输出中。 -- workspace `ls/cat/rg/apply-patch/build/artifact list` 在受控 root 内可用,路径越界、过大输出和未知命令有结构化失败。 +- workspace `ls/cat/rg/apply-patch/build` 在受控 root 内可用,路径越界、过大输出和未知命令有结构化失败;`artifact list` 进入后续扩展。 - `debug-probe chip-id/download/reset` 能通过 fake adapter 稳定返回 job/evidence;live smoke 至少证明一次真实 debug probe 路径可达。 - `io-probe:/uart/1 read` 与 `io-probe:/inner/... read` 在输出中明确标注外部/内部来源,不互相冒充。 - gateway 仍只是 cmd transport;CLI 不暴露任意 shell、不接收泛化 `cmd` 子命令、不绕过 profile 访问硬件。 diff --git a/docs/reference/device-pod.md b/docs/reference/device-pod.md index 7b51925a..7274d584 100644 --- a/docs/reference/device-pod.md +++ b/docs/reference/device-pod.md @@ -65,11 +65,13 @@ profile 必须只描述 target、debugInterface、projectWorkspace、ioInterface 第一版路径固定为: ```text -device-pod -> cloud-api -> gateway -> cmd -> skill/downloader -> debug probe -> target +device-pod -> cloud-api -> gateway -> cmd -> device-host-cli -> downloader/debug probe -> target ``` `debugInterface` 只暴露设备语义能力,例如 `debug.probe`、`debug.download`、`debug.reset`。不得把它退化成任意 shell 执行入口。 +`device-host-cli` 必须是用户 PC 侧自包含组件;Keil、串口、DAPLink、J-Link 或其他 skill 代码只能作为实现参考,不能成为运行时依赖。 + `debugInterface` 不承载源码编译、工程发现或工具链定义;这些属于 `projectWorkspace`。`debugInterface` 可以消费 `projectWorkspace` 产生的 artifact,例如 `.hex`、`.bin` 或 `.elf`,并负责把该 artifact 下载、校验、 diff --git a/package.json b/package.json index ea63b119..2e3746a7 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "validate": "node scripts/repo-reports-guard.mjs && node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/dev-runtime-hotfix-audit.mjs && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.mjs && node --test scripts/artifact-runtime-readiness-guard.test.mjs scripts/src/dev-runtime-hotfix-audit.test.mjs", - "check": "node scripts/repo-reports-guard.mjs && node --check scripts/repo-reports-guard.mjs && node --check scripts/src/report-paths.mjs && node --check internal/protocol/index.mjs && node --check internal/build-metadata.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/cloud-web-routes.mjs && node --check internal/dev-entrypoint/cloud-web-proxy.mjs && node --check internal/dev-entrypoint/cloud-web-proxy.test.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/code-agent-trace-store.mjs && node --check internal/cloud/codex-stdio-session.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-cloud-api/provision.mjs && node --check cmd/hwlab-cloud-api/migrate.mjs && node --check cmd/hwlab-deepseek-responses-bridge/main.mjs && node --check cmd/hwlab-deepseek-responses-bridge/main.test.mjs && node --test cmd/hwlab-deepseek-responses-bridge/main.test.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check tools/hwlab-gateway-shell.mjs && node --check tools/hwlab-gateway-tran.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.test.mjs && node --check scripts/artifact-publish.mjs && node --check scripts/g14-artifact-publish.mjs && node --check scripts/dev-runtime-base-image.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.test.mjs && node --test scripts/src/dev-runtime-hotfix-audit.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs && node --check skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check tools/hwlab-cli/lib/cli.test.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/code-agent-facts.mjs && node --check web/hwlab-cloud-web/code-agent-facts.test.mjs && node --check web/hwlab-cloud-web/code-agent-status.mjs && node --check web/hwlab-cloud-web/code-agent-status.test.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/wiring-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node cmd/hwlab-cloud-api/provision.mjs --check && node cmd/hwlab-cloud-api/migrate.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", + "check": "node scripts/repo-reports-guard.mjs && node --check scripts/repo-reports-guard.mjs && node --check scripts/src/report-paths.mjs && node --check internal/protocol/index.mjs && node --check internal/build-metadata.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/cloud-web-routes.mjs && node --check internal/dev-entrypoint/cloud-web-proxy.mjs && node --check internal/dev-entrypoint/cloud-web-proxy.test.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/code-agent-trace-store.mjs && node --check internal/cloud/codex-stdio-session.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-cloud-api/provision.mjs && node --check cmd/hwlab-cloud-api/migrate.mjs && node --check cmd/hwlab-deepseek-responses-bridge/main.mjs && node --check cmd/hwlab-deepseek-responses-bridge/main.test.mjs && node --test cmd/hwlab-deepseek-responses-bridge/main.test.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check tools/hwlab-gateway-shell.mjs && node --check tools/hwlab-gateway-tran.mjs && node --check tools/device-pod-cli.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.test.mjs && node --check scripts/artifact-publish.mjs && node --check scripts/g14-artifact-publish.mjs && node --check scripts/dev-runtime-base-image.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.test.mjs && node --test scripts/src/dev-runtime-hotfix-audit.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs && node --check skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check tools/hwlab-cli/lib/cli.test.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/code-agent-facts.mjs && node --check web/hwlab-cloud-web/code-agent-facts.test.mjs && node --check web/hwlab-cloud-web/code-agent-status.mjs && node --check web/hwlab-cloud-web/code-agent-status.test.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/wiring-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node cmd/hwlab-cloud-api/provision.mjs --check && node cmd/hwlab-cloud-api/migrate.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", "dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs", "dev-runtime-base:build": "node scripts/dev-runtime-base-image.mjs", "cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs", diff --git a/tools/device-pod-cli.mjs b/tools/device-pod-cli.mjs new file mode 100644 index 00000000..9aa49a5a --- /dev/null +++ b/tools/device-pod-cli.mjs @@ -0,0 +1,218 @@ +#!/usr/bin/env node +import { createHash, randomUUID } from 'node:crypto'; +import fs from 'node:fs'; +import http from 'node:http'; +import https from 'node:https'; +import path from 'node:path'; + +const VERSION = '0.1.0-mvp'; +const PROFILE_DIR = process.env.DEVICE_POD_PROFILE_DIR || path.resolve(process.cwd(), '.device-pod'); +const DEFAULT_TIMEOUT_MS = 30000; + +function nowIso() { return new Date().toISOString(); } +function stripSlash(value) { return String(value || '').replace(/\/+$/u, ''); } +function sha256(text) { return createHash('sha256').update(text).digest('hex'); } +function print(body, exitCode = 0) { + console.log(JSON.stringify({ generatedAt: nowIso(), cli: 'device-pod-cli', version: VERSION, ...body }, null, 2)); + process.exitCode = exitCode; +} +function fail(action, error, details = {}) { + const message = error instanceof Error ? error.message : String(error); + print({ ok: false, action, status: 'failed', blocker: classifyBlocker(message), error: message, details }, 1); +} +function classifyBlocker(message) { + if (/profile not found/u.test(message)) return 'profile-missing'; + if (/profile invalid|requires/u.test(message)) return 'profile-invalid'; + if (/approval/u.test(message)) return 'approval-required'; + if (/unsupported|invalid target selector/u.test(message)) return 'invalid-request'; + return 'operation-failed'; +} + +function parseTarget(raw) { + if (!raw || raw === '--help' || raw === 'help') return { kind: 'help' }; + if (raw === 'health') return { kind: 'health' }; + const match = String(raw).match(/^([^:]+):(workspace|debug-probe|io-probe)(?::(.*))?$/u); + if (!match) throw new Error(`invalid target selector: ${raw}`); + return { kind: 'invoke', podId: match[1], surface: match[2], path: match[3] || '' }; +} + +function parseOptions(argv) { + const out = { _: [] }; + for (let i = 0; i < argv.length; i += 1) { + const item = argv[i]; + if (!item.startsWith('--')) { out._.push(item); continue; } + const eq = item.indexOf('='); + const rawKey = eq >= 0 ? item.slice(2, eq) : item.slice(2); + const key = rawKey.replace(/-([a-z])/gu, (_, c) => c.toUpperCase()); + if (eq >= 0) { out[key] = item.slice(eq + 1); continue; } + const next = argv[i + 1]; + if (next && !next.startsWith('--')) { out[key] = next; i += 1; } + else out[key] = true; + } + return out; +} + +function resolvePodId(requested) { + if (requested) return requested; + if (process.env.DEVICE_POD_ID) return process.env.DEVICE_POD_ID; + const names = fs.existsSync(PROFILE_DIR) + ? fs.readdirSync(PROFILE_DIR).filter((name) => name.endsWith('.json')).map((name) => path.basename(name, '.json')) + : []; + if (names.length === 1) return names[0]; + throw new Error('profile invalid: pass : or set DEVICE_POD_ID when multiple/no profiles exist'); +} + +function readProfile(podId) { + const requestedPodId = podId || process.env.DEVICE_POD_ID || null; + const explicitFile = process.env.DEVICE_POD_PROFILE || null; + const devicePodId = explicitFile ? requestedPodId : resolvePodId(requestedPodId); + const file = explicitFile || path.join(PROFILE_DIR, `${devicePodId}.json`); + if (!fs.existsSync(file)) throw new Error(`profile not found: ${file}`); + const raw = fs.readFileSync(file, 'utf8'); + const profile = JSON.parse(raw); + const resolvedPodId = profile.devicePodId || profile.podId || devicePodId || path.basename(file, '.json'); + const targetId = profile.targetId || profile.target?.id || profile.deviceTarget?.targetId || profile.deviceTarget?.id; + const required = ['cloudApiUrl', 'gatewaySessionId', 'resourceId', 'capabilityId', 'hostWorkspaceRoot', 'hostCli']; + const missing = required.filter((key) => !profile[key]); + if (missing.length) throw new Error(`profile invalid: missing ${missing.join(', ')}`); + if (devicePodId && resolvedPodId !== devicePodId) throw new Error(`profile invalid: podId mismatch selector=${devicePodId} profile=${resolvedPodId}`); + if (!targetId) throw new Error('profile invalid: missing target.id'); + return { ...profile, podId: resolvedPodId, targetId, __profilePath: file, __profileHash: sha256(raw) }; +} + +function profileSummary(profile) { + return { profilePath: profile.__profilePath, profileHash: profile.__profileHash, devicePodId: profile.podId, targetId: profile.targetId }; +} +function routeSummary(profile) { + return { cloudApiUrl: profile.cloudApiUrl, gatewaySessionId: profile.gatewaySessionId, resourceId: profile.resourceId, capabilityId: profile.capabilityId, hostWorkspaceRoot: profile.hostWorkspaceRoot }; +} + +function cmdQuote(value) { + const text = String(value); + if (/^[A-Za-z0-9_./:\\-]+$/u.test(text)) return text; + return `"${text.replace(/"/g, '\\"')}"`; +} + +async function buildHostCommand(selector, parsed) { + const args = []; + const operation = parsed._[0] || (selector.surface === 'workspace' ? 'ls' : selector.surface === 'debug-probe' ? 'status' : 'read'); + const mutating = isMutating(selector.surface, operation); + if (mutating) requireApproval(parsed, `${selector.surface}.${operation}`); + + if (selector.surface === 'workspace') { + const workspacePath = selector.path.replace(/^\/+/, '') || '.'; + if (operation === 'ls' || operation === 'cat') args.push('workspace', operation, parsed._[1] ? path.posix.join(workspacePath, parsed._[1]) : workspacePath, ...parsed._.slice(2)); + else if (operation === 'rg') args.push('workspace', 'rg', parsed._[1] || '', workspacePath, ...parsed._.slice(2)); + else if (operation === 'apply-patch') args.push('workspace', 'apply-patch', workspacePath, '--patch-b64', Buffer.from(await resolvePatchText(parsed), 'utf8').toString('base64')); + else if (operation === 'build') args.push('workspace', 'build', ...(parsed._.slice(1).length ? parsed._.slice(1) : ['start'])); + else throw new Error(`unsupported workspace command: ${operation}`); + } else if (selector.surface === 'debug-probe') { + args.push('debug-probe', operation, ...parsed._.slice(1)); + } else if (selector.surface === 'io-probe') { + const probePath = selector.path.replace(/^\/+/, '') || 'uart/1'; + if (probePath.startsWith('inner/')) throw new Error(`unsupported io-probe path in MVP: ${probePath}`); + args.push('io-probe', probePath, operation, ...parsed._.slice(1)); + } else { + throw new Error(`unsupported surface: ${selector.surface}`); + } + return { operation, mutating, commandArgs: args }; +} + +function isMutating(surface, operation) { + return (surface === 'workspace' && operation === 'apply-patch') + || (surface === 'debug-probe' && (operation === 'download' || operation === 'reset')) + || (surface === 'io-probe' && operation === 'write'); +} +function requireApproval(parsed, operation) { + if (parsed.approved === true && typeof parsed.reason === 'string' && parsed.reason.trim()) return; + throw new Error(`approval required for ${operation}: pass --approved --reason `); +} + +async function resolvePatchText(parsed) { + if (typeof parsed.patch === 'string') return parsed.patch; + if (typeof parsed.patchB64 === 'string') return Buffer.from(parsed.patchB64, 'base64').toString('utf8'); + const text = await readAllStdin(); + if (!text.trim()) throw new Error('workspace apply-patch requires patch text on stdin or --patch-b64'); + return text; +} +async function readAllStdin() { + const chunks = []; + for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk)); + return Buffer.concat(chunks).toString('utf8'); +} + +async function invoke(profile, command, parsed) { + const timeoutMs = Number(parsed.timeoutMs || DEFAULT_TIMEOUT_MS); + const requestId = `req_device_pod_cli_${randomUUID()}`; + const operationId = `op_device_pod_cli_${randomUUID()}`; + const traceId = parsed.traceId || `trc_device_pod_cli_${Date.now()}`; + const response = await requestJson(`${stripSlash(profile.cloudApiUrl)}/v1/rpc/hardware.invoke.shell`, { + timeoutMs: timeoutMs + 10000, + headers: { 'x-trace-id': traceId, 'x-request-id': requestId, 'x-actor-id': 'usr_device_pod_cli', 'x-source-service-id': 'hwlab-cloud-api' }, + body: { projectId: profile.projectId || 'prj_mvp_topology', operationId, gatewaySessionId: profile.gatewaySessionId, resourceId: profile.resourceId, capabilityId: profile.capabilityId, input: { command, cwd: profile.hostWorkspaceRoot, timeoutMs } } + }); + const dispatch = response.body?.result?.dispatch || response.body?.result || {}; + const hostJson = parseMaybeJson(dispatch.stdout); + const status = response.body?.error || dispatch.exitCode !== 0 || dispatch.status === 'failed' ? 'failed' : 'succeeded'; + return { requestId, operationId, traceId, httpStatus: response.status, status, dispatch, hostJson, gatewayBody: response.body }; +} + +function requestJson(url, options = {}) { + const body = JSON.stringify(options.body || {}); + const target = new URL(url); + const client = target.protocol === 'https:' ? https : http; + const headers = { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), ...(options.headers || {}) }; + return new Promise((resolve, reject) => { + const req = client.request(target, { method: 'POST', headers }, (res) => { + let text = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { text += chunk; }); + res.on('end', () => { + try { resolve({ status: res.statusCode || 0, body: text ? JSON.parse(text) : null }); } + catch (error) { reject(new Error(`invalid JSON response from gateway API: ${error.message}; body=${text.slice(0, 500)}`)); } + }); + }); + const timer = setTimeout(() => req.destroy(new Error(`request timeout after ${options.timeoutMs || DEFAULT_TIMEOUT_MS}ms`)), options.timeoutMs || DEFAULT_TIMEOUT_MS); + req.on('error', reject); + req.on('close', () => clearTimeout(timer)); + req.write(body); + req.end(); + }); +} + +function parseMaybeJson(text) { + if (!text) return null; + try { return JSON.parse(text); } catch { return null; } +} +function compactDispatch(dispatch) { + return { status: dispatch.status || dispatch.dispatchStatus, exitCode: dispatch.exitCode, timedOut: dispatch.timedOut, durationMs: dispatch.durationMs, stdoutTruncated: dispatch.stdoutTruncated, stderrTruncated: dispatch.stderrTruncated, stderr: dispatch.stderr || '', auditId: dispatch.auditId, evidenceId: dispatch.evidenceId }; +} + +function help() { + print({ ok: true, action: 'help', scope: 'HWLAB internal code agents only; not for external UniDesk workspaces.', usage: [ + 'node tools/device-pod-cli.mjs health', + 'node tools/device-pod-cli.mjs device-pod-71-freq:workspace:/ ls', + 'node tools/device-pod-cli.mjs device-pod-71-freq:workspace:/firmware cat main.c', + 'node tools/device-pod-cli.mjs device-pod-71-freq:workspace:/ apply-patch --approved --reason "DEV edit" < patch.diff', + 'node tools/device-pod-cli.mjs device-pod-71-freq:workspace:/ build start|status ', + 'node tools/device-pod-cli.mjs device-pod-71-freq:debug-probe chip-id|status|download|reset --approved --reason "DEV smoke"', + 'node tools/device-pod-cli.mjs device-pod-71-freq:io-probe:/uart/1 ports|read|write --approved --reason "DEV io"' + ] }); +} + +async function main() { + const [rawTarget, ...argv] = process.argv.slice(2); + const selector = parseTarget(rawTarget); + if (selector.kind === 'help') return help(); + const parsed = parseOptions(argv); + const profile = readProfile(selector.podId || parsed.podId); + if (selector.kind === 'health') return print({ ok: true, action: 'health', status: 'succeeded', ...profileSummary(profile), route: routeSummary(profile) }); + const built = await buildHostCommand(selector, parsed); + const command = `${profile.hostCli} ${built.commandArgs.map(cmdQuote).join(' ')}`; + if (parsed.dryRun) return print({ ok: true, action: 'device-pod.plan', status: 'planned', selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile) }); + const result = await invoke(profile, command, parsed); + const ok = result.status === 'succeeded'; + print({ ok, action: 'device-pod.invoke', status: result.status, selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile), traceId: result.traceId, operationId: result.operationId, requestId: result.requestId, httpStatus: result.httpStatus, dispatch: parsed.full ? result.dispatch : compactDispatch(result.dispatch), hostJson: result.hostJson, evidence: { auditId: result.dispatch.auditId, evidenceId: result.dispatch.evidenceId }, gatewayBody: parsed.full ? result.gatewayBody : undefined }, ok ? 0 : 1); +} + +main().catch((error) => fail('device-pod-cli', error));