diff --git a/cmd/hwlab-device-pod/main.test.ts b/cmd/hwlab-device-pod/main.test.ts index 107d201a..c4d5bd45 100644 --- a/cmd/hwlab-device-pod/main.test.ts +++ b/cmd/hwlab-device-pod/main.test.ts @@ -222,7 +222,8 @@ test("device pod executor dispatches internal jobs through cloud-api gateway ada assert.equal(dispatches[0].internalService, "hwlab-device-pod"); assert.equal(dispatches[0].body.params.gatewaySessionId, "gws_devicepod_test"); assert.equal(dispatches[0].body.params.input.cwd, "F:\\Work\\Project"); - assert.match(dispatches[0].body.params.input.command, /^node -e /u); + assert.match(dispatches[0].body.params.input.command, /^node tools\\device-host-cli\.mjs --profile-json-b64 /u); + assert.equal(dispatches[0].body.params.input.command.includes("node -e"), false); } finally { await service.stop(); await new Promise((resolve, reject) => cloudApi.close((error) => (error ? reject(error) : resolve()))); @@ -269,15 +270,14 @@ test("device-pod executor maps v0.1 CLI options to device-host-cli argv", async assert.equal(jobResponse.status, 202); await waitForJobStatus(service.port, "device-pod-test", "job_options_test", "completed"); const command = dispatchedBody.params.input.command; - const encoded = command.match(/"([A-Za-z0-9+/=]+)"$/u)[1]; + assert.equal(command.includes("node -e"), false); + assert.match(command, /^node tools\/device-host-cli\.mjs --profile-json-b64 /u); + assert.match(command, / --pod-id device-pod-test debug-probe download start /u); + assert.match(command, / --capture-uart uart\/1 --capture-duration-ms 8000 --port COM4 --baud-rate 921600$/u); + const encoded = command.match(/--profile-json-b64 ([A-Za-z0-9+/=]+)/u)[1]; const decoded = JSON.parse(Buffer.from(encoded, "base64").toString("utf8")); - assert.deepEqual(decoded.args, [ - "debug-probe", "download", "start", - "--capture-uart", "uart/1", - "--capture-duration-ms", "8000", - "--port", "COM4", - "--baud-rate", "921600" - ]); + assert.equal(decoded.devicePodId, "device-pod-test"); + assert.equal(decoded.route.gatewaySessionId, "gws_test"); } finally { await service.stop(); await new Promise((resolve, reject) => cloudApi.close((error) => (error ? reject(error) : resolve()))); diff --git a/cmd/hwlab-device-pod/main.ts b/cmd/hwlab-device-pod/main.ts index d94d6568..278f5644 100644 --- a/cmd/hwlab-device-pod/main.ts +++ b/cmd/hwlab-device-pod/main.ts @@ -383,13 +383,44 @@ function deviceHostCommand(profile, job) { const payload = { devicePodId: profile.devicePodId || job.devicePodId, profile, - profilePath: `.device-pod/.runtime/${job.id}.json`, hostCli: profile.route?.hostCli || "node tools/device-host-cli.mjs", args }; - const runner = "const cp=require('node:child_process'),fs=require('node:fs'),path=require('node:path');const p=JSON.parse(Buffer.from(process.argv[1],'base64').toString('utf8'));fs.mkdirSync(path.dirname(p.profilePath),{recursive:true});fs.writeFileSync(p.profilePath,JSON.stringify(p.profile,null,2)+'\\n','utf8');function split(s){const out=[];let q=null,b='';for(const ch of String(s)){if(q){if(ch===q)q=null;else b+=ch}else if(ch.charCodeAt(0)===39||ch.charCodeAt(0)===34)q=ch;else if(/\\s/u.test(ch)){if(b){out.push(b);b=''}}else b+=ch}if(b)out.push(b);return out}const parts=split(p.hostCli||'node tools/device-host-cli.mjs');const child=cp.spawn(parts[0],parts.slice(1).concat(['--profile',p.profilePath,'--pod-id',p.devicePodId],p.args),{stdio:'inherit',shell:false});child.on('exit',(c)=>process.exit(c??0));child.on('error',(e)=>{console.error(e.message);process.exit(127)});"; - const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64"); - return `node -e "${runner}" "${encoded}"`; + const profileJsonB64 = Buffer.from(JSON.stringify(payload.profile), "utf8").toString("base64"); + return shellJoin([...splitCommand(payload.hostCli), "--profile-json-b64", profileJsonB64, "--pod-id", payload.devicePodId, ...payload.args]); +} + +function splitCommand(value) { + const out = []; + let quote = null; + let buffer = ""; + for (const char of String(value || "")) { + if (quote) { + if (char === quote) quote = null; + else buffer += char; + } else if (char === "'" || char === '"') { + quote = char; + } else if (/\s/u.test(char)) { + if (buffer) { + out.push(buffer); + buffer = ""; + } + } else { + buffer += char; + } + } + if (buffer) out.push(buffer); + return out.length ? out : ["node", "tools/device-host-cli.mjs"]; +} + +function shellJoin(parts) { + return parts.map(shellQuote).join(" "); +} + +function shellQuote(value) { + const text = String(value ?? ""); + if (/^[A-Za-z0-9_./:=@\\-]+$/u.test(text)) return text; + return `"${text.replace(/(["\\])/gu, "\\$1")}"`; } function deviceHostArgs(intent, args = {}) { diff --git a/skills/device-pod-cli/assets/device-host-cli.mjs b/skills/device-pod-cli/assets/device-host-cli.mjs index dbfeb128..a42f3c4c 100644 --- a/skills/device-pod-cli/assets/device-host-cli.mjs +++ b/skills/device-pod-cli/assets/device-host-cli.mjs @@ -20,6 +20,11 @@ function parseGlobalCli(argv) { i += 1; } else if (item.startsWith('--profile=')) { options.profile = cleanArg(item.slice('--profile='.length)); + } else if (item === '--profile-json-b64') { + options.profileJsonB64 = cleanArg(argv[i + 1]); + i += 1; + } else if (item.startsWith('--profile-json-b64=')) { + options.profileJsonB64 = cleanArg(item.slice('--profile-json-b64='.length)); } else if (item === '--pod-id') { options.podId = cleanArg(argv[i + 1]); i += 1; @@ -93,6 +98,7 @@ function profilePath(root = workspaceRoot(), podId = selectedPodId()) { function loadProfile() { const root = workspaceRoot(); + if (GLOBAL_CLI.options.profileJsonB64) return loadInlineProfile(root); const requestedProfile = GLOBAL_CLI.options.profile || process.env.DEVICE_POD_PROFILE || ''; const file = requestedProfile ? (path.isAbsolute(requestedProfile) ? requestedProfile : path.resolve(root, requestedProfile)) : profilePath(root); if (!fs.existsSync(file)) throw new Error(`device-pod profile not found: ${file}`); @@ -105,6 +111,18 @@ function loadProfile() { return profile; } +function loadInlineProfile(root) { + const profile = JSON.parse(Buffer.from(GLOBAL_CLI.options.profileJsonB64, 'base64').toString('utf8')); + const actualPodId = profile.devicePodId || profile.podId; + const requestedPodId = GLOBAL_CLI.options.podId || process.env.DEVICE_POD_ID || ''; + if (requestedPodId && actualPodId && actualPodId !== requestedPodId) throw new Error(`device-pod profile mismatch: requested ${requestedPodId}, profile ${actualPodId}`); + const runtimePath = path.join(devicePodDir(root), '.runtime', `${actualPodId || requestedPodId || DEFAULT_POD_ID}.json`); + writeJson(runtimePath, profile); + profile.__profilePath = runtimePath; + profile.__workspaceRoot = path.resolve(profile.workspaceRoot || root); + return profile; +} + function resolveWorkspacePath(profile, rel = '.') { const root = profile.__workspaceRoot; const raw = String(rel || '.').replace(/^\/+/, '');