From 4fd4805d32e28cab7dc64c8c932a11a369b5a0d9 Mon Sep 17 00:00:00 2001 From: nerdalytics <97166791+nerdalytics@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:42:43 +0200 Subject: [PATCH] fix(cli): stop treating version-manager npm shims as npm-cli.js mise replaces bin/npm with a bash wrapper. getNpmCliPath resolved it without checking whether it was actually a JS file, so runGlobalNpm spawned `node /path/to/bash-script` and the resulting SyntaxError surfaced as a misleading "registry error" on every update check. Closes #7543 --- .../cli/src/utils/installationInfo.test.ts | 61 +++++++++++++++++++ packages/cli/src/utils/installationInfo.ts | 27 +++++--- 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/utils/installationInfo.test.ts b/packages/cli/src/utils/installationInfo.test.ts index 7a8ee4cee66..81580d387c3 100644 --- a/packages/cli/src/utils/installationInfo.test.ts +++ b/packages/cli/src/utils/installationInfo.test.ts @@ -8,6 +8,7 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { formatUpdateInstructions, getInstallationInfo, + getNpmCliPath, PackageManager, resolveUpdateCommand, } from './installationInfo.js'; @@ -740,3 +741,63 @@ describe('formatUpdateInstructions', () => { ).toEqual(['Running via npx, update not applicable.']); }); }); + +describe('getNpmCliPath', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('returns the resolved path when adjacent npm is a .js file', () => { + const nodePath = '/usr/local/bin/node'; + mockedRealPathSync.mockReturnValue( + '/usr/local/lib/node_modules/npm/bin/npm-cli.js', + ); + + const result = getNpmCliPath(nodePath, 'linux'); + + expect(mockedRealPathSync).toHaveBeenCalledWith('/usr/local/bin/npm'); + expect(result).toBe('/usr/local/lib/node_modules/npm/bin/npm-cli.js'); + }); + + it('falls back to npm-cli.js when adjacent npm is a non-.js wrapper (e.g. mise bash shim)', () => { + // mise installs node to ~/.local/share/mise/installs/node// on + // both Intel and Apple Silicon Macs (and Linux). Its npm shim is a bash + // script, not a symlink to npm-cli.js. + const nodePath = + '/Users/dev/.local/share/mise/installs/node/26.5.0/bin/node'; + mockedRealPathSync.mockReturnValue( + '/Users/dev/.local/share/mise/installs/node/26.5.0/bin/npm', + ); + + const result = getNpmCliPath(nodePath, 'darwin'); + + expect(mockedRealPathSync).toHaveBeenCalledWith( + '/Users/dev/.local/share/mise/installs/node/26.5.0/bin/npm', + ); + expect(result).toBe( + '/Users/dev/.local/share/mise/installs/node/26.5.0/lib/node_modules/npm/bin/npm-cli.js', + ); + }); + + it('falls back to npm-cli.js when adjacent npm does not exist', () => { + const nodePath = '/usr/local/bin/node'; + mockedRealPathSync.mockImplementation(() => { + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); + }); + + const result = getNpmCliPath(nodePath, 'linux'); + + expect(result).toBe('/usr/local/lib/node_modules/npm/bin/npm-cli.js'); + }); + + it('returns win32 npm-cli.js path on Windows', () => { + const nodePath = 'C:\\Program Files\\nodejs\\node.exe'; + + const result = getNpmCliPath(nodePath, 'win32'); + + expect(result).toBe( + 'C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js', + ); + expect(mockedRealPathSync).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/cli/src/utils/installationInfo.ts b/packages/cli/src/utils/installationInfo.ts index f1c424b2a35..0817194efc3 100644 --- a/packages/cli/src/utils/installationInfo.ts +++ b/packages/cli/src/utils/installationInfo.ts @@ -41,19 +41,26 @@ export function getNpmCliPath( // instead of throwing synchronously — getNpmCliPath is called from a // non-async site (handleAutoUpdate), and a returned best-effort path lets the // downstream spawn surface any failure through its 'error' handler. + // + // Node version managers (mise, asdf, proto) may replace bin/npm with a shell + // wrapper instead of a symlink to npm-cli.js. Validate the resolved path is a + // .js file before returning it; otherwise use the conventional fallback. + const npmCliJs = path.join( + path.dirname(nodePath), + '..', + 'lib', + 'node_modules', + 'npm', + 'bin', + 'npm-cli.js', + ); const adjacentNpm = path.join(path.dirname(nodePath), 'npm'); try { - return fs.realpathSync(adjacentNpm); + const resolved = fs.realpathSync(adjacentNpm); + if (resolved.endsWith('.js')) return resolved; + return npmCliJs; } catch { - return path.join( - path.dirname(nodePath), - '..', - 'lib', - 'node_modules', - 'npm', - 'bin', - 'npm-cli.js', - ); + return npmCliJs; } }