diff --git a/docs/reference/spec-hwpod-harness.md b/docs/reference/spec-hwpod-harness.md index 7729f860..57f1752c 100644 --- a/docs/reference/spec-hwpod-harness.md +++ b/docs/reference/spec-hwpod-harness.md @@ -238,7 +238,7 @@ CaseRun prompt 只能描述任务、边界、允许修改的文件、必须尝 第一阶段 CaseRun 不引入常驻调度服务,由 `hwlab-cli case` 组合 case registry repo、本地 `.state`、`hwpod-cli`、`hwpod-compiler-cli`、`hwlab-api` 和在线 `hwpod-node` 完成单次运行。标准 case registry repo 是 `pikasTech/hwlab-case-registry`,本地默认 checkout 路径是 `/root/hwlab-case-registry`;历史 `pikasTech/hwpod-cases` 只作为迁移前来源。case registry repo 提供 `cases//case.json` 与同目录 `hwpod-spec.yaml`;每次运行会先在 subject repo 本地 checkout 下创建隔离 Git worktree,再把 spec 复制到 `.state/hwlab-cli/caserun//.hwlab/hwpod-spec.yaml` 并把 `spec.workspace.path` 改写到该隔离 worktree,形成 run-local spec。 -CaseRun 的 case registry repo 与 subject repo 必须分离。标准 registry repo 是 `pikasTech/hwlab-case-registry`,G14 标准 checkout 是 `/root/hwlab-case-registry`;历史 `pikasTech/hwpod-cases` 和 `/root/hwpod-cases` 只作为迁移前来源,不再作为新 CaseRun 入口或写入目标。case registry repo 只保存 `case.json`、HWPOD spec 或 spec 引用、`subject` 引用、期望结果和 `runs///` 审计产物;它不保存或替代实际固件、工程、测试样例或产品代码库。subject repo 是被开发、被测试、被编译或被下载验证的真实源码仓库,`case.json.subject` 必须明确写入 `repoLocalPath` 和 `commitId`,其中 `repoLocalPath` 是绑定 HWPOD node 上的本地 Git checkout,`commitId` 是完整 40 字符 subject commit id。`case run` 准备阶段只能从 `repoLocalPath` 执行 `git worktree add --detach `,不得 clone/fetch GitHub,也不得把 branch、tag 或当前 HEAD 作为执行依据或 fallback。`subdir` 只描述 subject repo 内的 case 关注区域,不能替代 `spec.workspace.path` 的隔离 worktree。`evidence.json` 必须记录 subject repo 本地路径、subject commit、subdir 和 run-local worktree,避免把 floating ref 或外部脏 workspace 当作可审计证据。 +CaseRun 的 case registry repo 与 subject repo 必须分离。标准 registry repo 是 `pikasTech/hwlab-case-registry`,G14 标准 checkout 是 `/root/hwlab-case-registry`;历史 `pikasTech/hwpod-cases` 和 `/root/hwpod-cases` 只作为迁移前来源,不再作为新 CaseRun 入口或写入目标。case registry repo 只保存 `case.json`、HWPOD spec 或 spec 引用、`subject` 引用、期望结果和 `runs///` 审计产物;它不保存或替代实际固件、工程、测试样例或产品代码库。subject repo 是被开发、被测试、被编译或被下载验证的真实源码仓库,`case.json.subject` 必须明确写入 `repoLocalPath` 和 `commitId`,其中 `repoLocalPath` 是绑定 HWPOD node 上的本地 Git checkout,`commitId` 是完整 40 字符 subject commit id。`case run` 准备阶段只能从 `repoLocalPath` 执行 `git worktree add --detach `,不得 clone/fetch GitHub,也不得把 branch、tag 或当前 HEAD 作为执行依据或 fallback。若 HWPOD spec 声明 Keil MDK 工程,准备阶段还必须把同名 `.uvoptx` / `.uvopt` sidecar 从 `repoLocalPath` 同步到 isolated worktree,并在 prepare 结果里暴露 copied/missing 诊断;这些 sidecar 承载 CMSIS-DAP/UV4 探头绑定、flash sequence 和 reset/run 配置,不能被 git worktree 的 tracked-file 视角静默丢失。`subdir` 只描述 subject repo 内的 case 关注区域,不能替代 `spec.workspace.path` 的隔离 worktree。`evidence.json` 必须记录 subject repo 本地路径、subject commit、subdir 和 run-local worktree,避免把 floating ref 或外部脏 workspace 当作可审计证据。 标准 `case.json` 的 subject 字段形态如下: diff --git a/tools/hwlab-cli/caserun.test.ts b/tools/hwlab-cli/caserun.test.ts index ca0ff64f..8042a156 100644 --- a/tools/hwlab-cli/caserun.test.ts +++ b/tools/hwlab-cli/caserun.test.ts @@ -38,11 +38,19 @@ test("hwlab-cli case prepare copies a case hwpod-spec into isolated run state", assert.match(runLocalSpec, /node-d601-f103-v2/u); assert.match(runLocalSpec, /path: "F:\\\\Work\\\\HWLAB-CASE-F103\\\\\.worktree\\\\caserun-run-test"/u); assert.equal(requests.length, 1); - assert.equal(requests[0].body.ops.length, 3); + assert.equal(requests[0].body.ops.length, 4); assert.equal(requests[0].body.ops[0].args.workspacePath, SUBJECT_REPO_LOCAL_PATH); assert.deepEqual(requests[0].body.ops[0].args.argv, ["cat-file", "-e", SUBJECT_COMMIT_ID]); assert.deepEqual(requests[0].body.ops[1].args.argv, ["worktree", "add", "--detach", "--force", ".worktree\\caserun-run-test", SUBJECT_COMMIT_ID]); assert.deepEqual(requests[0].body.ops[2].args.argv, ["-C", ".worktree\\caserun-run-test", "rev-parse", "HEAD"]); + assert.equal(requests[0].body.ops[3].opId, "op_04_keil_sidecars"); + assert.equal(requests[0].body.ops[3].args.command, "node"); + const sidecarInput = JSON.parse(requests[0].body.ops[3].args.argv[2]); + assert.equal(sidecarInput.sourceBase, "projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103"); + assert.equal(sidecarInput.destinationBase, ".worktree\\caserun-run-test\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103"); + assert.equal(requests[0].body.ops[3].args.commandBinding.sourceBase, "projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103"); + assert.equal(requests[0].body.ops[3].args.commandBinding.destinationBase, ".worktree\\caserun-run-test\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103"); + assert.equal(result.payload.subject.setup.keilSidecars.copied, 1); } finally { await rm(root, { recursive: true, force: true }); } @@ -700,18 +708,20 @@ async function writeJsonFile(file: string, value: any) { } function hwpodSpecText() { - return "kind: Hwpod\nmetadata:\n name: d601-f103-v2\nspec:\n nodeBinding:\n nodeId: node-d601-f103-v2\n workspace:\n path: F:\\\\Work\\\\HWLAB-CASE-F103\n targetDevice:\n board: D601-F103-V2\n debugProbe:\n type: daplink\n ioProbe:\n uart:\n port: COM9\n"; + return "kind: Hwpod\nmetadata:\n name: d601-f103-v2\nspec:\n nodeBinding:\n nodeId: node-d601-f103-v2\n workspace:\n path: F:\\\\Work\\\\HWLAB-CASE-F103\n toolchain: keil-mdk\n keilProject: projects/01_baseline/Projects/MDK-ARM/atk_f103.uvprojx\n keilTarget: USART\n targetDevice:\n board: D601-F103-V2\n debugProbe:\n type: daplink\n programBackend: keil\n probeUid: 95FFF39D3DB47E0D\n ioProbe:\n uart:\n port: COM9\n"; } function subjectWorktreeFetch(requests: any[]) { return async (url: RequestInfo | URL, init?: RequestInit) => { const body = JSON.parse(String(init?.body ?? "{}")); requests.push({ url: String(url), body }); - return new Response(JSON.stringify({ body: { results: [ - { ok: true, output: { exitCode: 0, stdout: "" } }, - { ok: true, output: { exitCode: 0, stdout: "Preparing worktree\n" } }, - { ok: true, output: { exitCode: 0, stdout: `${SUBJECT_COMMIT_ID}\n` } } - ] } }), { status: 200, headers: { "content-type": "application/json" } }); + const results = (body.ops ?? []).map((op: any, index: number) => { + if (op.opId === "op_02_subject_worktree_add") return { ok: true, output: { exitCode: 0, stdout: "Preparing worktree\n" } }; + if (op.opId === "op_03_subject_worktree_head") return { ok: true, output: { exitCode: 0, stdout: `${SUBJECT_COMMIT_ID}\n` } }; + if (op.opId === "op_04_keil_sidecars") return { ok: true, output: { exitCode: 0, stdout: "caseRunKeilSidecars {\"copied\":1,\"sourceBase\":\"projects\\\\01_baseline\\\\Projects\\\\MDK-ARM\\\\atk_f103\",\"destinationBase\":\".worktree\\\\caserun-run-test\\\\projects\\\\01_baseline\\\\Projects\\\\MDK-ARM\\\\atk_f103\"}\n" } }; + return { ok: true, output: { exitCode: 0, stdout: index === 0 ? "" : "" } }; + }); + return new Response(JSON.stringify({ body: { results } }), { status: 200, headers: { "content-type": "application/json" } }); }; } diff --git a/tools/hwpod-harness.test.ts b/tools/hwpod-harness.test.ts index a2d99ce4..bb303b7e 100644 --- a/tools/hwpod-harness.test.ts +++ b/tools/hwpod-harness.test.ts @@ -106,16 +106,21 @@ test("hwpod-compiler-cli generates Keil build and download commands from structu const download = await runHwpodCompilerCli(["compile", "--spec", specPath, "--intent", "debug.download"], { now: () => NOW }); assert.equal(download.exitCode, 0); - assert.deepEqual(download.payload.plan.ops.map((op: any) => op.op), ["cmd.run"]); - assert.equal(download.payload.plan.ops[0].args.step, "keil-flash"); - assert.equal(download.payload.plan.ops[0].args.command, "py"); - assert.deepEqual(download.payload.plan.ops[0].args.argv.slice(0, 5), ["-3", "C:\\Users\\liang\\.agents\\skills\\keil\\keil-cli.py", "flash", "-p", "F:\\Work\\D601-HWLAB\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103.uvprojx"]); - assert.deepEqual(download.payload.plan.ops[0].args.argv.slice(-8), ["-m", "daplink", "--program-backend", "keil", "-u", "3FD750C63E342E24", "-t", "USART"]); - assert.equal(download.payload.plan.ops[0].args.commandBinding.action, "download"); - assert.equal(download.payload.plan.ops[0].args.commandBinding.keilCommand, "flash"); - assert.equal(download.payload.plan.ops[0].args.commandBinding.programBackend, "keil"); - assert.equal(download.payload.plan.ops[0].args.commandBinding.autoBindUvoptx, false); - assert.match(download.payload.plan.ops[0].args.commandLine, /^py -3 C:\\Users\\liang\\\.agents\\skills\\keil\\keil-cli\.py flash -p/u); + assert.deepEqual(download.payload.plan.ops.map((op: any) => op.op), ["cmd.run", "cmd.run"]); + assert.equal(download.payload.plan.ops[0].args.step, "keil-sidecar-diagnostics"); + assert.equal(download.payload.plan.ops[0].args.command, "node"); + assert.deepEqual(JSON.parse(download.payload.plan.ops[0].args.argv[2]), { primary: "F:\\Work\\D601-HWLAB\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103.uvoptx", alternate: "F:\\Work\\D601-HWLAB\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103.uvopt" }); + assert.equal(download.payload.plan.ops[0].args.commandBinding.primary, "F:\\Work\\D601-HWLAB\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103.uvoptx"); + assert.equal(download.payload.plan.ops[0].args.commandBinding.alternate, "F:\\Work\\D601-HWLAB\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103.uvopt"); + assert.equal(download.payload.plan.ops[1].args.step, "keil-flash"); + assert.equal(download.payload.plan.ops[1].args.command, "py"); + assert.deepEqual(download.payload.plan.ops[1].args.argv.slice(0, 5), ["-3", "C:\\Users\\liang\\.agents\\skills\\keil\\keil-cli.py", "flash", "-p", "F:\\Work\\D601-HWLAB\\projects\\01_baseline\\Projects\\MDK-ARM\\atk_f103.uvprojx"]); + assert.deepEqual(download.payload.plan.ops[1].args.argv.slice(-8), ["-m", "daplink", "--program-backend", "keil", "-u", "3FD750C63E342E24", "-t", "USART"]); + assert.equal(download.payload.plan.ops[1].args.commandBinding.action, "download"); + assert.equal(download.payload.plan.ops[1].args.commandBinding.keilCommand, "flash"); + assert.equal(download.payload.plan.ops[1].args.commandBinding.programBackend, "keil"); + assert.equal(download.payload.plan.ops[1].args.commandBinding.autoBindUvoptx, false); + assert.match(download.payload.plan.ops[1].args.commandLine, /^py -3 C:\\Users\\liang\\\.agents\\skills\\keil\\keil-cli\.py flash -p/u); const jobStatus = await runHwpodCli(["job", "status", "keil_job_123", "--spec", specPath, "--dry-run"], { now: () => NOW }); assert.equal(jobStatus.exitCode, 0); diff --git a/tools/src/hwlab-caserun-lib.ts b/tools/src/hwlab-caserun-lib.ts index 5c737183..bb666e8a 100644 --- a/tools/src/hwlab-caserun-lib.ts +++ b/tools/src/hwlab-caserun-lib.ts @@ -1346,6 +1346,35 @@ function subjectFromDefinition(definition: Record) { async function requestSubjectWorktree(context: CaseContext, input: { caseId: string; runId: string; runDir: string; apiUrl: string; sourceSpecPath: string }, subject: { repoLocalPath: string; commitId: string; subdir: string }, worktreePath: string, relativeWorktreePath: string) { const document = await readHwpodSpec(input.sourceSpecPath); const hwpodId = text(document.metadata.name) || text(document.metadata.uid) || input.caseId; + const sidecarOp = keilSidecarCopyOp(document, subject, hwpodId, relativeWorktreePath); + const ops = [{ + opId: "op_01_subject_commit", + op: "cmd.run", + args: { + hwpodId, + workspacePath: subject.repoLocalPath, + command: "git", + argv: ["cat-file", "-e", subject.commitId] + } + }, { + opId: "op_02_subject_worktree_add", + op: "cmd.run", + args: { + hwpodId, + workspacePath: subject.repoLocalPath, + command: "git", + argv: ["worktree", "add", "--detach", "--force", relativeWorktreePath, subject.commitId] + } + }, { + opId: "op_03_subject_worktree_head", + op: "cmd.run", + args: { + hwpodId, + workspacePath: subject.repoLocalPath, + command: "git", + argv: ["-C", relativeWorktreePath, "rev-parse", "HEAD"] + } + }, ...(sidecarOp ? [sidecarOp] : [])]; const plan = { contractVersion: "hwpod-node-ops-v1", planId: `case_subject_${randomUUID()}`, @@ -1353,34 +1382,7 @@ async function requestSubjectWorktree(context: CaseContext, input: { caseId: str nodeId: document.spec.nodeBinding.nodeId, intent: "cmd.run", source: { compiler: "hwlab-cli.case", specPath: input.sourceSpecPath, specAuthority: "case-registry" }, - ops: [{ - opId: "op_01_subject_commit", - op: "cmd.run", - args: { - hwpodId, - workspacePath: subject.repoLocalPath, - command: "git", - argv: ["cat-file", "-e", subject.commitId] - } - }, { - opId: "op_02_subject_worktree_add", - op: "cmd.run", - args: { - hwpodId, - workspacePath: subject.repoLocalPath, - command: "git", - argv: ["worktree", "add", "--detach", "--force", relativeWorktreePath, subject.commitId] - } - }, { - opId: "op_03_subject_worktree_head", - op: "cmd.run", - args: { - hwpodId, - workspacePath: subject.repoLocalPath, - command: "git", - argv: ["-C", relativeWorktreePath, "rev-parse", "HEAD"] - } - }] + ops }; const response = await context.fetchImpl(`${input.apiUrl}/v1/hwpod-node-ops`, { method: "POST", @@ -1393,7 +1395,7 @@ async function requestSubjectWorktree(context: CaseContext, input: { caseId: str if (!status.ok) { throw cliError("subject_worktree_prepare_failed", "failed to prepare isolated subject worktree on the HWPOD node", clean({ httpStatus: response.status, nodeId: document.spec.nodeBinding.nodeId, repoLocalPath: subject.repoLocalPath, commitId: subject.commitId, worktreePath, exitCode: status.exitCode, stdout: clipText(status.stdout), stderr: clipText(status.stderr), body: compactObject(body) })); } - return clean({ httpStatus: response.status, planId: plan.planId, nodeId: document.spec.nodeBinding.nodeId, exitCode: status.exitCode, stdout: clipText(status.stdout), stderr: clipText(status.stderr) }); + return clean({ httpStatus: response.status, planId: plan.planId, nodeId: document.spec.nodeBinding.nodeId, exitCode: status.exitCode, stdout: clipText(status.stdout), stderr: clipText(status.stderr), keilSidecars: keilSidecarSummary(status.stdout) }); } function subjectWorktreeStatus(httpStatus: number, body: any, expected: { commitId: string; worktreePath: string }) { @@ -1403,11 +1405,97 @@ function subjectWorktreeStatus(httpStatus: number, body: any, expected: { commit const stdout = outputs.map((output: any) => text(output.stdout)).filter(Boolean).join("\n"); const stderr = outputs.map((output: any) => text(output.stderr)).filter(Boolean).join("\n"); const actualHead = text(outputs[2]?.stdout).split(/\r?\n/u)[0]?.toLowerCase() ?? ""; - const commandOk = httpStatus >= 200 && httpStatus < 300 && outputs.length === 3 && exitCodes.every((exitCode) => exitCode === 0); + const commandOk = httpStatus >= 200 && httpStatus < 300 && outputs.length >= 3 && exitCodes.every((exitCode) => exitCode === 0); const ok = commandOk && actualHead === expected.commitId; return { ok, exitCode: exitCodes.find((exitCode) => exitCode !== 0) ?? exitCodes[exitCodes.length - 1], stdout, stderr, actualHead, actualWorktreePath: expected.worktreePath }; } +function keilSidecarCopyOp(document: any, subject: { repoLocalPath: string }, hwpodId: string, relativeWorktreePath: string) { + const project = subjectRelativeKeilProject(document, subject.repoLocalPath); + if (!project) return null; + const sourceBase = stripUvprojx(project).replace(/\//gu, "\\"); + const destinationBase = windowsJoin(relativeWorktreePath, sourceBase); + return { + opId: "op_04_keil_sidecars", + op: "cmd.run", + args: { + hwpodId, + workspacePath: subject.repoLocalPath, + command: "node", + argv: ["-e", keilSidecarCopyNodeScript(), JSON.stringify({ sourceBase, destinationBase })], + commandBinding: { + kind: "keil-mdk-sidecar-copy", + source: "hwlab-cli.case.prepare", + sourceBase, + destinationBase, + sidecars: [".uvoptx", ".uvopt"] + } + } + }; +} + +function subjectRelativeKeilProject(document: any, repoLocalPath: string) { + const workspace = objectRecord(document.spec?.workspace); + const projectWorkspace = objectRecord(document.spec?.projectWorkspace); + const rawProject = text(workspace.keilProject ?? workspace.projectPath ?? workspace.project ?? projectWorkspace.projectPath); + if (!rawProject || !/\.uvprojx$/iu.test(rawProject)) return ""; + if (!isAbsolutePathLike(rawProject)) return rawProject.replace(/^[\\/]+/u, ""); + const repo = repoLocalPath.replace(/[\\/]+$/u, ""); + const normalizedRepo = normalizeLocalPath(repo); + const normalizedProject = normalizeLocalPath(rawProject); + if (!normalizedProject.startsWith(`${normalizedRepo}/`)) return ""; + return rawProject.slice(repo.length).replace(/^[\\/]+/u, ""); +} + +function stripUvprojx(value: string) { + return value.replace(/\.uvprojx$/iu, ""); +} + +function windowsJoin(parent: string, child: string) { + const left = parent.replace(/[\\/]+$/u, "").replace(/\//gu, "\\"); + const right = child.replace(/^[\\/]+/u, "").replace(/\//gu, "\\"); + return `${left}\\${right}`; +} + +function keilSidecarCopyNodeScript() { + return [ + "const fs=require('fs');", + "const path=require('path');", + "const input=JSON.parse(process.argv[1]||'{}');", + "const sidecars=['.uvoptx','.uvopt'];", + "const copiedFiles=[];", + "const missing=[];", + "for(const ext of sidecars){", + "const source=String(input.sourceBase||'')+ext;", + "const destination=String(input.destinationBase||'')+ext;", + "if(fs.existsSync(source)){", + "const dir=destination.includes('\\\\')?path.win32.dirname(destination):path.dirname(destination);", + "fs.mkdirSync(dir,{recursive:true});", + "fs.copyFileSync(source,destination);", + "copiedFiles.push({ext,source,destination});", + "}else{missing.push(source);}", + "}", + "console.log('caseRunKeilSidecars '+JSON.stringify({copied:copiedFiles.length,copiedFiles,missing,sourceBase:input.sourceBase,destinationBase:input.destinationBase}));" + ].join(""); +} + +function keilSidecarSummary(stdout: string) { + const line = stdout.split(/\r?\n/u).find((item) => item.includes("caseRunKeilSidecars")); + if (!line) return null; + const payload = line.replace(/^.*caseRunKeilSidecars\s*/u, ""); + const parsed = parseJsonMaybe(payload); + if (parsed && typeof parsed === "object") return clean({ ...(parsed as Record), line }); + return clean({ copied: numberOption(line.match(/copied=(\d+)/u)?.[1]), line }); +} + +function objectRecord(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; +} + +function isAbsolutePathLike(value: string) { + return /^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("/") || value.startsWith("\\\\"); +} + function subjectRelativeWorktreePath(repoLocalPath: string, worktreePath: string) { const repo = repoLocalPath.replace(/[\\/]+$/u, ""); const normalizedRepo = normalizeLocalPath(repo); diff --git a/tools/src/hwpod-harness-lib.ts b/tools/src/hwpod-harness-lib.ts index 67477a60..fc0e2bc0 100644 --- a/tools/src/hwpod-harness-lib.ts +++ b/tools/src/hwpod-harness-lib.ts @@ -531,6 +531,7 @@ function debugDownloadOps(common: any, args: any, document: any) { op: "cmd.run", args: clean({ ...opArgs, + commandBinding: entry.commandBinding ?? opArgs.commandBinding, command: entry.commandRun.command, argv: entry.commandRun.argv, commandLine: entry.command, @@ -605,6 +606,7 @@ function keilCommandForIntent(intent: "debug.build" | "debug.download", args: an const programBackend = text(args.programBackend ?? args.backend) || text(debugProbe.programBackend) || (text(debugProbe.adapter).toLowerCase() === "keil" ? "keil" : "keil"); const probeName = text(args.probeName) || text(debugProbe.probeName) || text(debugProbe.name); const autoBindUvoptx = debugProbe.autoBindUvoptx === true || debugProbe.autoBindProbe === true; + const sidecarDiagnostics = keilSidecarDiagnosticRun(project); const bindTokens = autoBindUvoptx && probeUid ? [...commonTokens, "project", "probe-binding", "set", "-p", project, ...projectTargetOption(target), "--probe-uid", probeUid, ...probeNameOption(probeName)] : []; @@ -622,6 +624,7 @@ function keilCommandForIntent(intent: "debug.build" | "debug.download", args: an ...(args.wait === true ? ["--wait"] : []) ]; const commandRuns = [ + sidecarDiagnostics, ...(bindTokens.length > 0 ? [{ step: "keil-probe-binding", command: shellCommand(bindTokens), commandRun: commandRun(bindTokens) }] : []), { step: "keil-flash", command: shellCommand(flashTokens), commandRun: commandRun(flashTokens) } ]; @@ -630,10 +633,29 @@ function keilCommandForIntent(intent: "debug.build" | "debug.download", args: an command: commandRuns.map((entry) => entry.command).join(" && "), commandRuns, timeoutMs, - binding: clean({ kind: "keil-mdk", source: "hwpod-compiler.keil-mdk", action: "download", keilCommand: "flash", project, target, keilCliPath, pythonCommand, programmer, programBackend, probeUid, probeName, autoBindUvoptx: bindTokens.length > 0, wait: args.wait === true }) + binding: clean({ kind: "keil-mdk", source: "hwpod-compiler.keil-mdk", action: "download", keilCommand: "flash", project, target, keilCliPath, pythonCommand, programmer, programBackend, probeUid, probeName, autoBindUvoptx: bindTokens.length > 0, wait: args.wait === true, keilSidecars: sidecarDiagnostics.commandBinding }) }; } +function keilSidecarDiagnosticRun(project: string) { + const base = project.replace(/\.uvprojx$/iu, ""); + const primary = `${base}.uvoptx`; + const alternate = `${base}.uvopt`; + const script = [ + "const fs=require('fs');", + "const input=JSON.parse(process.argv[1]||'{}');", + "const primary=String(input.primary||'');", + "const alternate=String(input.alternate||'');", + "let result;", + "if(fs.existsSync(primary)){result={status:'present',primary};}", + "else if(fs.existsSync(alternate)){result={status:'present',alternate};}", + "else{result={status:'missing',primary,alternate,reason:'keil_uvoptx_binding_absent'};}", + "console.log('hwpodKeilSidecar '+JSON.stringify(result));" + ].join(""); + const run = { command: "node", argv: ["-e", script, JSON.stringify({ primary, alternate })] }; + return { step: "keil-sidecar-diagnostics", command: shellCommand([run.command, ...run.argv]), commandRun: run, commandBinding: { primary, alternate } }; +} + function keilJobStatusCommand(args: any, document: any) { const workspace = objectValue(document.spec.workspace); const debugProbe = objectValue(document.spec.debugProbe);