fix: preserve keil sidecars for caserun downloads

This commit is contained in:
Codex Agent
2026-06-07 00:52:59 +08:00
parent 53aafde5bd
commit b22e4d25b8
5 changed files with 174 additions and 49 deletions
+1 -1
View File
@@ -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/<caseId>/case.json` 与同目录 `hwpod-spec.yaml`;每次运行会先在 subject repo 本地 checkout 下创建隔离 Git worktree,再把 spec 复制到 `.state/hwlab-cli/caserun/<runId>/.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/<caseId>/<runId>/` 审计产物;它不保存或替代实际固件、工程、测试样例或产品代码库。subject repo 是被开发、被测试、被编译或被下载验证的真实源码仓库,`case.json.subject` 必须明确写入 `repoLocalPath``commitId`,其中 `repoLocalPath` 是绑定 HWPOD node 上的本地 Git checkout`commitId` 是完整 40 字符 subject commit id。`case run` 准备阶段只能从 `repoLocalPath` 执行 `git worktree add --detach <worktree> <commitId>`,不得 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/<caseId>/<runId>/` 审计产物;它不保存或替代实际固件、工程、测试样例或产品代码库。subject repo 是被开发、被测试、被编译或被下载验证的真实源码仓库,`case.json.subject` 必须明确写入 `repoLocalPath``commitId`,其中 `repoLocalPath` 是绑定 HWPOD node 上的本地 Git checkout`commitId` 是完整 40 字符 subject commit id。`case run` 准备阶段只能从 `repoLocalPath` 执行 `git worktree add --detach <worktree> <commitId>`,不得 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 字段形态如下:
+17 -7
View File
@@ -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" } });
};
}
+15 -10
View File
@@ -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);
+118 -30
View File
@@ -1346,6 +1346,35 @@ function subjectFromDefinition(definition: Record<string, unknown>) {
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<string, unknown>), line });
return clean({ copied: numberOption(line.match(/copied=(\d+)/u)?.[1]), line });
}
function objectRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
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);
+23 -1
View File
@@ -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);