From 7a6441dede1f998cdff11a91963a441d56273936 Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Tue, 7 Apr 2026 14:57:42 +0200 Subject: [PATCH] fix: prefer system npm/npx over project-local versions from node_modules When the CLI runs inside an npm script (or any context where node_modules/.bin is on PATH), getNpxBinPath() could pick up a project-local npx instead of the system one. The standalone npx package (npx@10.2.2) bundles npm@5.1.0 which is incompatible with Node 22+, causing "cb.apply is not a function" errors during Coana reachability analysis. Fix: check for npm/npx next to process.execPath (the running node binary) before falling back to PATH-based lookup. This follows the same pattern already used by findRealNpm() in @socketsecurity/registry and getAgentExecPath() in package-environment.mts. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/utils/npm-paths.mts | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/utils/npm-paths.mts b/src/utils/npm-paths.mts index 3396db5e8..6897f62d8 100755 --- a/src/utils/npm-paths.mts +++ b/src/utils/npm-paths.mts @@ -2,6 +2,7 @@ import { existsSync } from 'node:fs' import Module from 'node:module' import path from 'node:path' +import { resolveBinPathSync } from '@socketsecurity/registry/lib/bin' import { logger } from '@socketsecurity/registry/lib/logger' import constants, { NODE_MODULES, NPM } from '../constants.mts' @@ -19,6 +20,23 @@ function exitWithBinPathError(binName: string): never { throw new Error('process.exit called') } +// Find a binary next to the running node binary (process.execPath). +// This avoids picking up a project-local binary from node_modules/.bin +// on PATH, e.g. the standalone "npx" package which bundles npm@5.1.0 +// that is incompatible with Node 22+. +function findBinNextToNode(binName: string): string | undefined { + const nodeDir = path.dirname(process.execPath) + const binPath = path.join(nodeDir, binName) + if (existsSync(binPath)) { + try { + return resolveBinPathSync(binPath) + } catch { + return undefined + } + } + return undefined +} + let _npmBinPath: string | undefined export function getNpmBinPath(): string { if (_npmBinPath === undefined) { @@ -33,7 +51,14 @@ export function getNpmBinPath(): string { let _npmBinPathDetails: ReturnType | undefined function getNpmBinPathDetails(): ReturnType { if (_npmBinPathDetails === undefined) { - _npmBinPathDetails = findBinPathDetailsSync(NPM) + // First try to find npm next to the node binary to avoid picking up + // a project-local npm from node_modules/.bin on PATH. + const npmNextToNode = findBinNextToNode(NPM) + if (npmNextToNode) { + _npmBinPathDetails = { name: NPM, path: npmNextToNode, shadowed: false } + } else { + _npmBinPathDetails = findBinPathDetailsSync(NPM) + } } return _npmBinPathDetails } @@ -95,7 +120,16 @@ export function getNpxBinPath(): string { let _npxBinPathDetails: ReturnType | undefined function getNpxBinPathDetails(): ReturnType { if (_npxBinPathDetails === undefined) { - _npxBinPathDetails = findBinPathDetailsSync('npx') + // First try to find npx next to the node binary to avoid picking up + // a project-local npx from node_modules/.bin on PATH (e.g., the + // standalone npx package which bundles npm@5.1.0, incompatible + // with Node 22+). + const npxNextToNode = findBinNextToNode('npx') + if (npxNextToNode) { + _npxBinPathDetails = { name: 'npx', path: npxNextToNode, shadowed: false } + } else { + _npxBinPathDetails = findBinPathDetailsSync('npx') + } } return _npxBinPathDetails }