Skip to content

fix(cli): resolve npm wrappers to npm-cli.js - #7544

Closed
hogeheer499-commits wants to merge 2 commits into
QwenLM:mainfrom
hogeheer499-commits:fix/mise-npm-cli-wrapper
Closed

fix(cli): resolve npm wrappers to npm-cli.js#7544
hogeheer499-commits wants to merge 2 commits into
QwenLM:mainfrom
hogeheer499-commits:fix/mise-npm-cli-wrapper

Conversation

@hogeheer499-commits

Copy link
Copy Markdown
Contributor

What this PR does

The update checker now ignores a non-JavaScript npm wrapper next to the Node executable and uses the conventional npm-cli.js path instead. Normal Node installations whose adjacent npm symlink resolves to JavaScript keep the existing behavior.

Why it's needed

Version managers such as mise can place a shell wrapper at bin/npm. Passing that wrapper to Node produces a syntax error, so startup checks and /update incorrectly report a registry failure even when npm is reachable.

Reviewer Test Plan

How to verify

Resolve the npm CLI path with a Node executable whose adjacent npm entry resolves to a shell wrapper. It should return the conventional lib/node_modules/npm/bin/npm-cli.js path. An adjacent entry that resolves to npm-cli.js should still be preferred.

Evidence (Before & After)

Before: the reproduced mise-style layout selected /usr/bin/bash as the npm CLI. After: the focused regression and all 33 installation-info tests pass, and the resolver selects npm-cli.js.

Tested on

OS Status
🍏 macOS ⚠️ not tested
🪟 Windows N/A
🐧 Linux ✅ tested

Environment (optional)

Node.js 22.22.1 with the repository lockfile dependencies.

Risk & Scope

  • Main risk or tradeoff: custom npm entry points without a .js suffix now use the standard fallback path.
  • Not validated / out of scope: an end-to-end mise installation on macOS.
  • Breaking changes / migration notes: none.

Linked Issues

Fixes #7543

中文说明

本 PR 的修改

更新检查器现在会忽略 Node 可执行文件旁边的非 JavaScript npm 包装脚本,改用常规的 npm-cli.js 路径。对于相邻 npm 符号链接解析到 JavaScript 文件的普通 Node 安装,现有行为保持不变。

修改原因

mise 等版本管理器可能会在 bin/npm 放置 shell 包装脚本。将该脚本传给 Node 会触发语法错误,因此即使 npm 仓库可访问,启动检查和 /update 也会错误地报告仓库故障。

审阅者测试计划

验证方法

使用一个相邻 npm 条目解析到 shell 包装脚本的 Node 可执行文件来解析 npm CLI 路径。结果应为常规的 lib/node_modules/npm/bin/npm-cli.js 路径。解析到 npm-cli.js 的相邻条目仍应优先使用。

修改前后证据

修改前:复现的 mise 风格布局错误地选择 /usr/bin/bash 作为 npm CLI。修改后:针对性回归测试和全部 33 个安装信息测试通过,解析器选择 npm-cli.js

测试环境

操作系统 状态
macOS ⚠️ 未测试
Windows N/A
Linux ✅ 已测试

Node.js 22.22.1,使用仓库锁文件依赖。

风险与范围

  • 主要风险或权衡:没有 .js 后缀的自定义 npm 入口现在会使用标准回退路径。
  • 未验证 / 范围外:macOS 上完整的 mise 端到端安装。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

Fixes #7543

@hogeheer499-commits
hogeheer499-commits marked this pull request as ready for review July 23, 2026 02:07
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: observed bug with a clear reproduction — issue #7543 documents mise placing a bash wrapper at bin/npm, which getNpmCliPath resolves via realpathSync and hands to Node, producing SyntaxError: Unexpected identifier 'pipefail'. The update check then misclassifies this as a registry failure. Confirmed real.

Direction: aligned — update checking is a core feature and this fixes it for mise (and potentially asdf/proto) users. The issue was already triaged and a community PR was explicitly welcomed.

Size: not applicable (no core paths touched — packages/cli/src/utils/ only; 22 production lines, 11 test lines).

Approach: the scope is exactly right. Validate the resolved path ends with .js, fall back to the conventional npm-cli.js location otherwise. This is the minimal fix the issue suggested, no unrelated changes, no scope creep.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:已观测到的 bug,有明确复现——issue #7543 记录了 mise 在 bin/npm 放置 bash 包装脚本,getNpmCliPath 通过 realpathSync 解析后交给 Node,触发 SyntaxError: Unexpected identifier 'pipefail'。更新检查随后将其误判为仓库故障。确认为真实问题。

方向:对齐——更新检查是核心功能,此修复惠及 mise(以及可能的 asdf/proto)用户。issue 已被分类,明确欢迎社区 PR。

规模:不适用(未触及核心路径——仅 packages/cli/src/utils/;22 行生产代码,11 行测试代码)。

方案:范围恰好。验证解析后的路径以 .js 结尾,否则回退到标准 npm-cli.js 位置。这正是 issue 建议的最小修复,无无关改动,无范围蔓延。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at 9bbf62438526482992796c2378bccfcf27dc395d · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: given the problem (mise's bash wrapper at bin/npm passes realpathSync and gets handed to Node as a CLI path), the minimal fix is to check whether the resolved path ends with .js before returning it, and fall back to the conventional <prefix>/lib/node_modules/npm/bin/npm-cli.js otherwise. One new test for the wrapper case.

Comparison with the diff: the PR does exactly this — no more, no less. The npmCliPath fallback is hoisted into a variable (computed after the Windows early return, so no wasted work on win32), the .endsWith('.js') guard is a one-liner, and the catch branch reuses the same variable instead of re-computing the path inline. Clean.

No critical blockers. No convention violations. The existing comment block explaining the non-async call site is preserved. The new test covers the mise wrapper scenario; the 32 existing tests cover the unchanged paths.

Real-Scenario Testing

tmux is not available on this CI runner, so I ran a standalone before/after test with a simulated mise-style layout (/tmp/mise-test/prefix/bin/npm is a bash wrapper with set -euo pipefail, mimicking mise's shim):

Simulated mise layout: /tmp/mise-test/prefix/bin/npm is a bash wrapper
Expected: /tmp/mise-test/prefix/lib/node_modules/npm/bin/npm-cli.js

BEFORE (main): /tmp/mise-test/prefix/bin/npm
  ❌ BUG: returns non-.js path (bash wrapper → SyntaxError in Node)

AFTER  (PR):   /tmp/mise-test/prefix/lib/node_modules/npm/bin/npm-cli.js
  ✅ returns .js path

--- Normal symlink case (stock Node install) ---
BEFORE: /usr/lib/node_modules_22/npm/bin/npm-cli.js
AFTER:  /usr/lib/node_modules_22/npm/bin/npm-cli.js
  ✅ same result (no regression)

Unit tests with PR code: 33/33 passed (32 existing + 1 new regression test).

中文说明

代码审查

独立方案: 针对问题(mise 的 bash 包装脚本通过 realpathSync 解析后被当作 CLI 路径传给 Node),最小修复是在返回前检查解析后的路径是否以 .js 结尾,否则回退到标准 <prefix>/lib/node_modules/npm/bin/npm-cli.js。为包装脚本场景添加一个测试。

与 diff 对比: PR 完全按照这个方案实现——不多不少。npmCliPath 回退路径被提升为变量(在 Windows 提前返回之后计算,win32 不做无用功),.endsWith('.js') 守卫是一行代码,catch 分支复用同一变量而非重新计算路径。干净。

无关键阻塞项。无规范违反。现有的解释非异步调用点的注释块被保留。新测试覆盖了 mise 包装脚本场景;32 个现有测试覆盖了未变更的路径。

真实场景测试

此 CI 运行器无 tmux,因此使用模拟的 mise 风格布局运行了独立的 before/after 测试(/tmp/mise-test/prefix/bin/npm 是包含 set -euo pipefail 的 bash 包装脚本,模拟 mise 的 shim):

模拟 mise 布局:/tmp/mise-test/prefix/bin/npm 是 bash 包装脚本
预期:/tmp/mise-test/prefix/lib/node_modules/npm/bin/npm-cli.js

修改前 (main): /tmp/mise-test/prefix/bin/npm
  ❌ BUG:返回非 .js 路径(bash 包装脚本 → Node 中触发 SyntaxError)

修改后 (PR):   /tmp/mise-test/prefix/lib/node_modules/npm/bin/npm-cli.js
  ✅ 返回 .js 路径

--- 正常符号链接场景(标准 Node 安装)---
修改前: /usr/lib/node_modules_22/npm/bin/npm-cli.js
修改后: /usr/lib/node_modules_22/npm/bin/npm-cli.js
  ✅ 结果相同(无回归)

PR 代码单元测试:33/33 通过(32 个现有 + 1 个新回归测试)。

Qwen Code · qwen3.8-max-preview

Reviewed at 9bbf62438526482992796c2378bccfcf27dc395d · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — textbook minimal bugfix: observed bug, clear reproduction, one-line guard, no scope creep.

This is exactly the kind of PR that's easy to review and easy to maintain. The problem is real (mise users get a bogus "registry error" on every startup), the fix is the smallest thing that works (.endsWith('.js') check with the existing fallback path), and the before/after confirms it resolves the issue without regressing normal Node installs. The test covers the new path, the existing 32 tests cover everything else, and the diff carries nothing unrelated.

My independent proposal was identical to what the PR does — no simpler path exists. If I had to maintain this in six months, I'd thank the author: one guard condition, one hoisted variable for DRY, done.

中文说明

置信度:5/5 — 教科书式的最小修复:已观测到的 bug、明确的复现、一行守卫、无范围蔓延。

这正是容易审查、容易维护的 PR。问题是真实的(mise 用户每次启动都看到虚假的"仓库错误"),修复是最小的可行方案(.endsWith('.js') 检查加现有回退路径),before/after 确认修复了问题且不影响正常 Node 安装。测试覆盖了新路径,现有 32 个测试覆盖了其余一切,diff 不夹带无关改动。

我的独立方案与 PR 完全一致——不存在更简路径。如果六个月后维护这段代码,我会感谢作者:一个守卫条件、一个提升的变量消除重复、完事。

Qwen Code · qwen3.8-max-preview

Reviewed at 9bbf62438526482992796c2378bccfcf27dc395d · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@gwinthis

Copy link
Copy Markdown
Collaborator

Review & Local Verification Report

代码审查

修复逻辑正确——检测 realpathSync 结果是否为 .js 文件,否则回退到标准 npm-cli.js 路径。

注意: 本 PR 与 #7545 解决同一问题(版本管理器 npm wrapper)。#7545 的测试覆盖更全面(4 cases vs 1 case),建议以 #7545 为准。

结论

代码逻辑正确,但建议关闭本 PR,合并 #7545

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — no blockers. Suggestions are inline.

— qwen3.7-max via Qwen Code /review

Comment on lines +56 to +58
describe('getNpmCliPath', () => {
it('falls back to npm-cli.js when adjacent npm is a wrapper', () => {
mockedRealPathSync.mockReturnValue('/prefix/bin/npm');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The .endsWith('.js') ternary has two branches, but only the fallback (wrapper) branch is tested. There is no test for the happy path where realpathSync resolves to a .js file — the standard npm install case.

Failure scenario: if a future change removes the .endsWith('.js') guard (e.g., simplifies to return npmCliPath), this test still passes because it asserts the fallback value. On a standard npm install where the symlink resolves to a real .js file at a non-standard location, the function would silently return the wrong path with no test to catch it.

Suggested change
describe('getNpmCliPath', () => {
it('falls back to npm-cli.js when adjacent npm is a wrapper', () => {
mockedRealPathSync.mockReturnValue('/prefix/bin/npm');
describe('getNpmCliPath', () => {
it('returns the resolved path when adjacent npm is a real symlink', () => {
mockedRealPathSync.mockReturnValue(
'/prefix/lib/node_modules/npm/bin/npm-cli.js',
);
expect(getNpmCliPath('/prefix/bin/node', 'linux')).toBe(
'/prefix/lib/node_modules/npm/bin/npm-cli.js',
);
});
it('falls back to npm-cli.js when adjacent npm is a wrapper', () => {
mockedRealPathSync.mockReturnValue('/prefix/bin/npm');
expect(getNpmCliPath('/prefix/bin/node', 'linux')).toBe(
'/prefix/lib/node_modules/npm/bin/npm-cli.js',
);
});
});

— qwen3.7-max via Qwen Code /review

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found. LGTM! ✅

— qwen3.8-max-preview via Qwen Code /review

it('falls back to npm-cli.js when adjacent npm is a wrapper', () => {
mockedRealPathSync.mockReturnValue('/prefix/bin/npm');

expect(getNpmCliPath('/prefix/bin/node', 'linux')).toBe(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] This Linux-path test is host-path-dependent and will fail when the test suite runs on Windows. getNpmCliPath('/prefix/bin/node', 'linux') uses the host's path.join, so on Windows it returns a backslash-separated path rather than the asserted POSIX path. Use platform-specific path handling in the implementation or make the expectation host-aware.

— codex-cli 0.144.6 (gpt-5.6-sol) via Qwen Code /review

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review complete. One P1 finding posted inline — Linux-path test is host-path-dependent and will fail on Windows. Implementation looks correct otherwise.

— codex-cli 0.144.6 (gpt-5.6-sol) via Qwen Code /review

@yiliang114

Copy link
Copy Markdown
Collaborator

Thanks for jumping on this so quickly! The fix is spot-on — clean one-liner guard, hoisted fallback variable, exactly the minimal change needed. Really appreciate you getting it out within hours of the issue landing.

We ended up going with #7545 since it covers the same logic with a few extra regression tests (symlink happy path, ENOENT, Windows), but your PR was equally correct and the before/after mise simulation was a nice touch. Hope to see more contributions from you! 🙏

@yiliang114 yiliang114 closed this Jul 23, 2026
@hogeheer499-commits

Copy link
Copy Markdown
Contributor Author

Thanks for the kind feedback! I completely understand going with #7545 for the broader test coverage. I really appreciate you taking the time to review it, and I'll keep an eye out for similar issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

getNpmCliPath returns mise bash wrapper instead of npm-cli.js, breaking update check

5 participants