From 49848219c1a43b167a3a0dc0a52b3dcc6eddb867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 11 Jul 2026 13:07:19 +0000 Subject: [PATCH 1/4] fix(media): resolve npx without npm_execpath on Windows --- skills-manifest.json | 8 +-- skills/media-use/audio/scripts/lib/tts.mjs | 16 ++++- .../audio/scripts/lib/tts.spawn.test.mjs | 60 +++++++------------ 3 files changed, 41 insertions(+), 43 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index a0384101e1..ae1d630366 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -6,7 +6,7 @@ "files": 144 }, "faceless-explainer": { - "hash": "b57c98179677e7a0", + "hash": "a09191a97564b114", "files": 18 }, "figma": { @@ -46,7 +46,7 @@ "files": 10 }, "media-use": { - "hash": "389942983c2c78c2", + "hash": "60839757ea4de645", "files": 122 }, "motion-graphics": { @@ -58,11 +58,11 @@ "files": 132 }, "pr-to-video": { - "hash": "2132f6f9bd474f52", + "hash": "21dcf8aa94fc1033", "files": 22 }, "product-launch-video": { - "hash": "b1b41c74a52e28f1", + "hash": "cb30ad95ba8863c7", "files": 21 }, "remotion-to-hyperframes": { diff --git a/skills/media-use/audio/scripts/lib/tts.mjs b/skills/media-use/audio/scripts/lib/tts.mjs index 1b61e58fcd..424a096cae 100644 --- a/skills/media-use/audio/scripts/lib/tts.mjs +++ b/skills/media-use/audio/scripts/lib/tts.mjs @@ -128,6 +128,17 @@ export function resolveNpxCliFromNpmExecPath( return pathExists(npxCliPath) ? npxCliPath : null; } +export function resolveNpxCliPath( + npmExecPath = process.env.npm_execpath, + nodeExecPath = process.env.npm_node_execpath || process.execPath, + pathExists = existsSync, +) { + const fromNpm = resolveNpxCliFromNpmExecPath(npmExecPath, pathExists); + if (fromNpm) return fromNpm; + const besideNode = join(dirname(nodeExecPath), "node_modules", "npm", "bin", "npx-cli.js"); + return pathExists(besideNode) ? besideNode : null; +} + export function resolveSpawnCommand( cmd, args, @@ -143,10 +154,11 @@ export function resolveSpawnCommand( // On Windows, npx resolves to npx.cmd, which Node cannot execute directly. // Avoid `shell:true` and the .cmd shim entirely by invoking npm's JS CLI with // node, preserving request-provided values as argv data instead of shell text. - const npxCliPath = resolveNpxCliFromNpmExecPath(env.npm_execpath, pathExists); + const nodeExecPath = env.npm_node_execpath || process.execPath; + const npxCliPath = resolveNpxCliPath(env.npm_execpath, nodeExecPath, pathExists); if (!npxCliPath) return null; return { - cmd: env.npm_node_execpath || process.execPath, + cmd: nodeExecPath, args: [npxCliPath, ...args.map((arg) => String(arg))], opts: { stdio: "ignore", windowsHide: true, ...opts }, }; diff --git a/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs b/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs index 9262f7395d..defae568a3 100644 --- a/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs +++ b/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import { EventEmitter } from "node:events"; import { resolveNpxCliFromNpmExecPath, + resolveNpxCliPath, resolveSpawnCommand, spawnP, _resetNpxResolutionWarnForTests, @@ -33,6 +34,12 @@ test("resolveNpxCliFromNpmExecPath finds npx-cli next to npm-cli", () => { assert.equal(resolveNpxCliFromNpmExecPath(envWithNpxCli.npm_execpath, pathExists), npxCliPath); }); +test("resolveNpxCliPath finds npx-cli beside node when npm_execpath is unset", () => { + const node = "C:/Program Files/nodejs/node.exe"; + const expected = "C:/Program Files/nodejs/node_modules/npm/bin/npx-cli.js"; + assert.equal(resolveNpxCliPath(undefined, node, (path) => path === expected), expected); +}); + test("resolveSpawnCommand routes npx through node+npx-cli on win32 without shell:true", () => { const resolved = resolveSpawnCommand( "npx", @@ -101,43 +108,22 @@ test("spawnP does not enable shell for non-npx commands even on win32", async () assert.equal(captured[0].opts.shell, undefined); }); -// Regression: win32 + npx with npm_execpath unset can't locate the npx JS CLI, -// so resolveSpawnCommand returns null and spawnP short-circuits. Previously it -// returned {status:-1} silently — every TTS line just dropped as "TTS failed - -// omitted" with no hint. Now it must surface a clear one-time diagnostic naming -// npm_execpath, while still returning {status:-1} without spawning anything. -test("spawnP surfaces a clear diagnostic (once) when npx can't be resolved on win32", async () => { +test("spawnP resolves npx beside node when npm_execpath is unset on win32", async () => { _resetNpxResolutionWarnForTests(); - const errors = []; - const originalError = console.error; - console.error = (msg) => errors.push(msg); const captured = []; - const emptyEnv = {}; // no npm_execpath - try { - const r1 = await spawnP( - "npx", - ["hyperframes", "tts"], - {}, - "win32", - fakeSpawn(captured), - emptyEnv, - () => false, - ); - const r2 = await spawnP( - "npx", - ["hyperframes", "tts"], - {}, - "win32", - fakeSpawn(captured), - emptyEnv, - () => false, - ); - assert.equal(r1.status, -1); - assert.equal(r2.status, -1); - assert.equal(captured.length, 0, "must not spawn anything when resolution fails"); - assert.equal(errors.length, 1, "diagnostic is emitted once for a batch, not per line"); - assert.match(errors[0], /npm_execpath/); - } finally { - console.error = originalError; - } + const node = "C:/Program Files/nodejs/node.exe"; + const npxCli = "C:/Program Files/nodejs/node_modules/npm/bin/npx-cli.js"; + const result = await spawnP( + "npx", + ["hyperframes", "tts"], + {}, + "win32", + fakeSpawn(captured), + { npm_node_execpath: node }, + (path) => path === npxCli, + ); + assert.equal(result.status, 0); + assert.equal(captured.length, 1); + assert.equal(captured[0].cmd, node); + assert.deepEqual(captured[0].args, [npxCli, "hyperframes", "tts"]); }); From 19d12709df7f0287fcbed0eb5a88b49a352b4912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 11 Jul 2026 21:56:10 +0000 Subject: [PATCH 2/4] test(media): preserve npx resolution diagnostics --- skills/media-use/audio/scripts/lib/tts.mjs | 16 +++++----- .../audio/scripts/lib/tts.spawn.test.mjs | 31 ++++++++++++++++++- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/skills/media-use/audio/scripts/lib/tts.mjs b/skills/media-use/audio/scripts/lib/tts.mjs index 424a096cae..b708594744 100644 --- a/skills/media-use/audio/scripts/lib/tts.mjs +++ b/skills/media-use/audio/scripts/lib/tts.mjs @@ -186,17 +186,17 @@ export function spawnP( const resolved = resolveSpawnCommand(cmd, args, opts, platform, env, pathExists); if (!resolved) { // resolveSpawnCommand only returns null for the npx-on-win32 case where - // npm_execpath isn't set (e.g. audio.mjs invoked directly with `node`, not - // through npm/npx). Without this, every call silently returns status:-1 and - // stdio:"ignore" hides why — callers just report "TTS failed - omitted" for - // every line. Surface the real reason once so it's diagnosable. + // neither npm's configured CLI nor the beside-node fallback exists. Without + // this, every call silently returns status:-1 and stdio:"ignore" hides why. if (!_warnedNpxResolution) { _warnedNpxResolution = true; + const reason = env.npm_execpath + ? `npm_execpath (${env.npm_execpath}) and the beside-node npm fallback could not be found` + : "npm_execpath is unset and the beside-node npm fallback could not be found"; console.error( - `[media-use] Cannot run "${cmd}" on Windows: npm_execpath is not set, so the ` + - `npx JS CLI can't be located. This happens when this script is run directly with ` + - `\`node\` instead of through npm/npx. Every "${cmd}" call is being skipped. ` + - `Fix: run via \`npx\`/\`npm run\`, or export npm_execpath pointing at your npm-cli.js.`, + `[media-use] Cannot run "${cmd}" on Windows: ${reason}. ` + + `Every "${cmd}" call is being skipped. Install npm with Node, or run via ` + + `\`npx\`/\`npm run\` with a valid npm_execpath.`, ); } return Promise.resolve({ status: -1 }); diff --git a/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs b/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs index defae568a3..b8e6b49a9b 100644 --- a/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs +++ b/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs @@ -37,7 +37,10 @@ test("resolveNpxCliFromNpmExecPath finds npx-cli next to npm-cli", () => { test("resolveNpxCliPath finds npx-cli beside node when npm_execpath is unset", () => { const node = "C:/Program Files/nodejs/node.exe"; const expected = "C:/Program Files/nodejs/node_modules/npm/bin/npx-cli.js"; - assert.equal(resolveNpxCliPath(undefined, node, (path) => path === expected), expected); + assert.equal( + resolveNpxCliPath(undefined, node, (path) => path === expected), + expected, + ); }); test("resolveSpawnCommand routes npx through node+npx-cli on win32 without shell:true", () => { @@ -127,3 +130,29 @@ test("spawnP resolves npx beside node when npm_execpath is unset on win32", asyn assert.equal(captured[0].cmd, node); assert.deepEqual(captured[0].args, [npxCli, "hyperframes", "tts"]); }); + +test("spawnP warns once with an accurate diagnostic when neither npx path exists", async () => { + _resetNpxResolutionWarnForTests(); + const errors = []; + const originalError = console.error; + console.error = (message) => errors.push(String(message)); + try { + const env = { npm_execpath: "C:/missing/npm-cli.js", npm_node_execpath: "C:/node/node.exe" }; + const missing = () => false; + assert.equal( + (await spawnP("npx", ["hyperframes", "tts"], {}, "win32", fakeSpawn([]), env, missing)) + .status, + -1, + ); + assert.equal( + (await spawnP("npx", ["hyperframes", "tts"], {}, "win32", fakeSpawn([]), env, missing)) + .status, + -1, + ); + } finally { + console.error = originalError; + } + assert.equal(errors.length, 1); + assert.match(errors[0], /npm_execpath \(C:\/missing\/npm-cli\.js\)/); + assert.doesNotMatch(errors[0], /npm_execpath is not set/); +}); From 76f5730cdbab93a6e213e9f2a16e2e30ea255b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 11 Jul 2026 21:57:30 +0000 Subject: [PATCH 3/4] chore: refresh skills manifest --- skills-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills-manifest.json b/skills-manifest.json index ae1d630366..393428e68f 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -46,7 +46,7 @@ "files": 10 }, "media-use": { - "hash": "60839757ea4de645", + "hash": "22918e36bf8ed15a", "files": 122 }, "motion-graphics": { From fa88b3c3cefec9be9c34d3fe898d21da55b70a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 11 Jul 2026 22:33:57 +0000 Subject: [PATCH 4/4] chore: refresh skills manifest after rebase --- skills-manifest.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 393428e68f..b4e8c78d8a 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -6,7 +6,7 @@ "files": 144 }, "faceless-explainer": { - "hash": "a09191a97564b114", + "hash": "b57c98179677e7a0", "files": 18 }, "figma": { @@ -46,7 +46,7 @@ "files": 10 }, "media-use": { - "hash": "22918e36bf8ed15a", + "hash": "e3f818afb0afbb34", "files": 122 }, "motion-graphics": { @@ -58,11 +58,11 @@ "files": 132 }, "pr-to-video": { - "hash": "21dcf8aa94fc1033", + "hash": "2132f6f9bd474f52", "files": 22 }, "product-launch-video": { - "hash": "cb30ad95ba8863c7", + "hash": "b1b41c74a52e28f1", "files": 21 }, "remotion-to-hyperframes": {