fix: support .cmd/.bat shims on Windows in exec() (#446)#469
Conversation
…profullstack#454) Replace destructuring assignment with validation to avoid TS18048 under noUncheckedIndexedAccess.
…llstack#452) Use ../../../../tsconfig.base.json instead of ../../tsconfig.base.json which incorrectly pointed to packages/tsconfig.base.json.
…k#446) Detect .cmd/.bat extensions on Windows and spawn via cmd.exe with shell:true.
Greptile SummaryThis PR fixes Windows
Confidence Score: 3/5The core exec() change has a logic error that will cause every .cmd/.bat invocation on Windows to go through cmd.exe twice, which may silently produce wrong exit codes or broken argument quoting. The tsconfig and DNS changes are safe. The Windows shim path in exec.ts both manually sets the executable to 'cmd' with /d /s /c args and also enables shell:true, which instructs Node.js to wrap the command in cmd.exe a second time. This double-wrapping is the primary change introduced by this PR and affects every .cmd/.bat call on Windows. packages/core/src/exec.ts needs the most attention — the simultaneous use of manual cmd.exe invocation and shell:true should be resolved before merging. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["exec(cmd, args, opts)"] --> B{isWin && cmd ends with .cmd/.bat?}
B -- No --> C["spawn(cmd, args, opts)"]
B -- Yes --> D["spawn('cmd', ['/d','/s','/c', cmd, ...args], {shell:true})"]
D --> E["Node.js shell:true wraps again\ncmd.exe /d /s /c cmd /d /s /c foo.cmd args"]
E --> F["⚠️ Double cmd.exe invocation"]
C --> G["Process runs normally"]
style F fill:#ffcccc,stroke:#cc0000
style G fill:#ccffcc,stroke:#009900
Reviews (1): Last reviewed commit: "fix: support .cmd/.bat shims on Windows ..." | Re-trigger Greptile |
| const child = spawn(needsShell ? 'cmd' : cmd, needsShell ? ['/d', '/s', '/c', cmd, ...args] : args, { | ||
| cwd: opts.cwd, | ||
| env: { ...process.env, ...extraEnv }, | ||
| stdio: ['ignore', 'pipe', 'pipe'], | ||
| ...(needsShell ? { shell: true } : {}), | ||
| }); |
There was a problem hiding this comment.
Double-wrapping through
cmd.exe
When needsShell is true, the code manually sets the executable to 'cmd' and prepends /d /s /c to the args, but it also passes shell: true. On Windows, shell: true causes Node.js to wrap the command as cmd.exe /d /s /c "<exe> <args>" — so the actual invocation becomes cmd.exe /d /s /c cmd /d /s /c foo.cmd <args>, spawning cmd.exe twice. Choose one approach: either manually invoke cmd without shell: true, or keep shell: true and just pass the original cmd/args.
| const child = spawn(needsShell ? 'cmd' : cmd, needsShell ? ['/d', '/s', '/c', cmd, ...args] : args, { | |
| cwd: opts.cwd, | |
| env: { ...process.env, ...extraEnv }, | |
| stdio: ['ignore', 'pipe', 'pipe'], | |
| ...(needsShell ? { shell: true } : {}), | |
| }); | |
| const child = spawn(needsShell ? 'cmd' : cmd, needsShell ? ['/d', '/s', '/c', cmd, ...args] : args, { | |
| cwd: opts.cwd, | |
| env: { ...process.env, ...extraEnv }, | |
| stdio: ['ignore', 'pipe', 'pipe'], | |
| }); |
| const child = spawn(cmd, args, { | ||
| // On Windows, .cmd/.bat shims must be spawned with shell:true or via cmd.exe | ||
| const isWin = process.platform === 'win32'; | ||
| const needsShell = isWin && (cmd.endsWith('.cmd') || cmd.endsWith('.bat')); |
There was a problem hiding this comment.
Case-insensitive extension matching needed on Windows
Windows file systems are case-insensitive, so shims named foo.CMD or foo.BAT (uppercase) will not be detected, and spawning them without the shell wrapper will fail with ENOENT. Lowercasing before the check avoids this edge case.
| const needsShell = isWin && (cmd.endsWith('.cmd') || cmd.endsWith('.bat')); | |
| const needsShell = isWin && (cmd.toLowerCase().endsWith('.cmd') || cmd.toLowerCase().endsWith('.bat')); |
| if (existing.length === 0) return; | ||
| const first = existing[0]; | ||
| if (!first) return; |
There was a problem hiding this comment.
Redundant null guard after a length check
existing.length === 0 is checked on the line immediately above, so when execution reaches const first = existing[0], the array is guaranteed to be non-empty and first can never be undefined. The if (!first) return; guard is dead code and can be removed.
| if (existing.length === 0) return; | |
| const first = existing[0]; | |
| if (!first) return; | |
| if (existing.length === 0) return; | |
| const first = existing[0]; |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
Detect
.cmd/.batextensions on Windows and spawn viacmd.exewithshell:true.Fixes #446