mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-06 16:24:33 -04:00
fix: use ENOENT check instead of which.sync for command-not-found on Windows (#11004)
* fix: use ENOENT check instead of which.sync for command-not-found on Windows On Windows, `which.sync()` only checks if a command exists in PATH, not whether it actually executed successfully. This caused false "Command not found" errors when a command exists but exits with a non-zero code. Use the same `spawn ENOENT` check across all platforms, which is reliable thanks to cross-spawn used by execa. Closes #11000 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve prependPaths against exec prefix for correct Windows command lookup The previous ENOENT-only approach doesn't work on Windows because execa 9.x uses cross-spawn only for command parsing, not spawning. This means cross-spawn's ENOENT hook (hookChildProcess) never fires, and non-existent commands wrapped as `cmd.exe /c <command>` exit with code 1 instead of emitting ENOENT. Restore the which.sync fallback for Windows, but fix the original #11000 bug by resolving relative prependPaths (like ./node_modules/.bin) against the exec prefix instead of relying on process.cwd(). This ensures correct path resolution in --filter contexts where the command runs in a different package directory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: zubeyralmaho <zubeyralmaho@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
6
.changeset/fix-windows-exec-command-not-found.md
Normal file
6
.changeset/fix-windows-exec-command-not-found.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/exec.commands": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fixed false "Command not found" error on Windows when the command exists but exits with a non-zero exit code [#11000](https://github.com/pnpm/pnpm/issues/11000).
|
||||
@@ -296,7 +296,7 @@ export async function handler (
|
||||
result[prefix].status = 'passed'
|
||||
result[prefix].duration = getExecutionDuration(startTime)
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
if (isErrorCommandNotFound(params[0], err, prependPaths)) {
|
||||
if (isErrorCommandNotFound(params[0], err, prefix, prependPaths)) {
|
||||
err.message = `Command "${params[0]}" not found`
|
||||
err.hint = await createExecCommandNotFoundHint(params[0], {
|
||||
implicitlyFellbackFromRun: opts.implicitlyFellbackFromRun ?? false,
|
||||
@@ -394,19 +394,20 @@ interface CommandError extends Error {
|
||||
shortMessage: string
|
||||
}
|
||||
|
||||
function isErrorCommandNotFound (command: string, error: CommandError, prependPaths: string[]): boolean {
|
||||
// Mac/Linux
|
||||
if (process.platform === 'linux' || process.platform === 'darwin') {
|
||||
return error.originalMessage === `spawn ${command} ENOENT`
|
||||
function isErrorCommandNotFound (command: string, error: CommandError, prefix: string, prependPaths: string[]): boolean {
|
||||
if (error.originalMessage === `spawn ${command} ENOENT`) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Windows
|
||||
// On Windows, execa 9.x uses cross-spawn only for command parsing (not spawning),
|
||||
// so cross-spawn's ENOENT hook never fires. Non-existent commands get wrapped as
|
||||
// `cmd.exe /c <command>` which exits with code 1 instead of emitting ENOENT.
|
||||
// Fall back to checking if the command exists in PATH, resolving relative paths
|
||||
// against the exec prefix to correctly handle --filter contexts.
|
||||
if (process.platform === 'win32') {
|
||||
const { value: path } = prependDirsToPath(prependPaths)
|
||||
return !which.sync(command, {
|
||||
nothrow: true,
|
||||
path,
|
||||
})
|
||||
const absolutePrependPaths = prependPaths.map(p => path.resolve(prefix, p))
|
||||
const { value: searchPath } = prependDirsToPath(absolutePrependPaths)
|
||||
return !which.sync(command, { nothrow: true, path: searchPath })
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user