From 7df00bc3db392afa5211c60fb16c1237741c2bb3 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Tue, 24 Mar 2026 15:57:06 +0100 Subject: [PATCH] fix: use ENOENT check instead of which.sync for command-not-found on Windows (#11004) Cherry-picked from main (e9318ce974). --- .../fix-windows-exec-command-not-found.md | 6 +++++ .../src/exec.ts | 23 ++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 .changeset/fix-windows-exec-command-not-found.md diff --git a/.changeset/fix-windows-exec-command-not-found.md b/.changeset/fix-windows-exec-command-not-found.md new file mode 100644 index 0000000000..f9e818f500 --- /dev/null +++ b/.changeset/fix-windows-exec-command-not-found.md @@ -0,0 +1,6 @@ +--- +"@pnpm/plugin-commands-script-runners": patch +"pnpm": patch +--- + +Fixed false "Command not found" errors on Windows when a command exists in PATH but exits with a non-zero code. Also fixed path resolution for `--filter` contexts where the command runs in a different package directory. diff --git a/exec/plugin-commands-script-runners/src/exec.ts b/exec/plugin-commands-script-runners/src/exec.ts index 3f66fc5283..694d8965c0 100644 --- a/exec/plugin-commands-script-runners/src/exec.ts +++ b/exec/plugin-commands-script-runners/src/exec.ts @@ -304,7 +304,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, @@ -398,19 +398,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 ` 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