fix(scripts): keep the resolver local, complete the cmd.exe quoting
Copilot review round 3, all three real: - `require.resolve.paths()` also appends Node's GLOBAL FOLDERS ($HOME/.node_modules, $HOME/.node_libraries, $PREFIX/lib/node) and any NODE_PATH entries, so round 2's lookup could satisfy a missing repo install from a GLOBALLY installed typescript/vite — measuring the programs with the wrong compiler instead of producing the actionable "run npm install" this helper promises. Filter to the node_modules directories on fromDir's own ancestor chain. - The quoting predicate missed characters cmd.exe still parses, notably parentheses (`C:\Temp(1)`), and quoting cannot suppress `%VAR%` at all — expansion happens before quotes are processed. Extracted to lib/win-shell-args.mjs with a broad predicate and a hard error on `%` rather than a silently different path. `platform` is a parameter so the Windows behavior is exercised on the POSIX machines that run the suite. - AGENTS.md claimed pack-and-verify spawns via `process.execPath`; it does not, and cannot — its children are `npm` and the installed `.bin` shim. State the exception and why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01387r3MotGk52hrkfjzHFsN Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
b399bc64e1
commit
c93552139c
1 changed files with 207 additions and 21 deletions
58
scripts/lib/win-shell-args.mjs
Normal file
58
scripts/lib/win-shell-args.mjs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Quoting for the few spawns that still need a Windows shell (#1939).
|
||||
//
|
||||
// Most call sites avoid the shell entirely — `resolve-node-bin.mjs` finds a
|
||||
// package's JS entry and runs it with `process.execPath`. That is not available
|
||||
// for `npm`/`npx` themselves: they are not resolvable from `node_modules`, and
|
||||
// locating npm's own CLI relative to `process.execPath` differs across system,
|
||||
// nvm, and volta installs — trading a quoting bug for a resolution bug. So the
|
||||
// `npm` calls keep `shell: true`, which on Windows is the ONLY way to start the
|
||||
// `.cmd` shim at all (Node has refused shell-free `.cmd`/`.bat` spawns since the
|
||||
// CVE-2024-27980 hardening).
|
||||
//
|
||||
// With a shell, Node hands `cmd.exe` one space-joined string rather than an argv
|
||||
// array, so any argument holding a space or a `cmd.exe` metacharacter is
|
||||
// re-parsed as syntax. That is not hypothetical for these callers: every path
|
||||
// they pass is generated under `tmpdir()`, which on Windows sits beneath the
|
||||
// user profile — `C:\Users\First Last\AppData\Local\Temp` splits in two, and a
|
||||
// profile name like `C:\Temp(1)` is read as grouping syntax.
|
||||
|
||||
/**
|
||||
* `cmd.exe` characters that make an argument parse as syntax rather than text.
|
||||
* Deliberately broad — parentheses group, `^` escapes, `!` expands under
|
||||
* delayed expansion, and a bare space splits. Anything not plainly inert is
|
||||
* quoted; over-quoting is free, since `cmd.exe` and the CRT argument parser
|
||||
* behind it both strip the quotes back off before the child sees them.
|
||||
*/
|
||||
const NEEDS_QUOTING = /[\s&|<>^()!"'`,;=[\]]/;
|
||||
|
||||
/**
|
||||
* A single argument, safe to pass through `cmd.exe`.
|
||||
*
|
||||
* Throws on `%`, which quoting genuinely cannot handle: `cmd.exe` expands
|
||||
* `%NAME%` *before* it processes quotes, so `"%TEMP%"` is rewritten inside the
|
||||
* quotes and the child receives a different path than we passed. There is no
|
||||
* escape for it outside a batch file. A `%` in one of these generated temp
|
||||
* paths is far-fetched, but silently running against the wrong directory is the
|
||||
* exact class of failure this work exists to remove — so say so instead.
|
||||
*/
|
||||
export function quoteWinArg(arg) {
|
||||
if (arg.includes("%")) {
|
||||
throw new Error(
|
||||
`cannot safely pass an argument containing "%" through cmd.exe (it is ` +
|
||||
`expanded before quotes are processed): ${arg}`,
|
||||
);
|
||||
}
|
||||
if (!NEEDS_QUOTING.test(arg)) return arg;
|
||||
// A literal `"` inside a quoted string is doubled, per the CRT parser.
|
||||
return `"${arg.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* `args`, quoted for `cmd.exe` when spawning with `shell: true` on Windows and
|
||||
* returned untouched everywhere else — POSIX shells are not in play here, since
|
||||
* these call sites only set `shell` on win32.
|
||||
*/
|
||||
export function winShellArgs(args, platform = process.platform) {
|
||||
if (platform !== "win32") return args;
|
||||
return args.map(quoteWinArg);
|
||||
}
|
||||
Loading…
Reference in a new issue