Skip to content

fix(cli): stop treating version-manager npm shims as npm-cli.js - #7545

Merged
yiliang114 merged 3 commits into
QwenLM:mainfrom
nerdalytics:fix/get-npm-cli-path-mise-wrapper
Jul 23, 2026
Merged

fix(cli): stop treating version-manager npm shims as npm-cli.js#7545
yiliang114 merged 3 commits into
QwenLM:mainfrom
nerdalytics:fix/get-npm-cli-path-mise-wrapper

Conversation

@nerdalytics

Copy link
Copy Markdown
Contributor

What this PR does

The update check resolves the npm CLI path before spawning a Node process to query the registry. It now validates that the resolved path points to a JavaScript file. When a version manager has replaced the npm binary with a shell wrapper, the conventional npm-cli.js location is used instead. Four unit tests cover the symlink, shell-wrapper, missing-file, and Windows cases.

Why it's needed

Users who manage Node through mise see "registry error" on every startup and every /update attempt. The registry is reachable and the package resolves correctly from the command line. The error is misleading because the actual failure is a SyntaxError from Node trying to parse a bash script as JavaScript — the error classifier doesn't recognize it as a network or timeout issue, so it falls through to the default "registry" label. The same mechanism affects any version manager that wraps bin/npm with a non-JS script (asdf, proto).

Reviewer Test Plan

How to verify

Run the unit tests for the affected module:

npx vitest run packages/cli/src/utils/installationInfo.test.ts

All 36 tests pass (32 existing + 4 new). The new tests mock realpathSync to return a non-.js path (simulating mise's bash shim) and assert the fallback npm-cli.js path is returned instead.

Evidence (Before & After)

N/A — non-UI change.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment

Unit tests only (vitest run).

Risk & Scope

  • Main risk or tradeoff: minimal — the fallback path (<prefix>/lib/node_modules/npm/bin/npm-cli.js) is the same one already used when realpathSync throws. The only behavioral change is that a non-.js resolved path now triggers the fallback instead of being returned as-is.
  • Not validated / out of scope: asdf and proto installations (same mechanism, not available locally for manual testing).
  • Breaking changes / migration notes: none.

Linked Issues

Closes #7543

中文说明

此 PR 做了什么

更新检查在生成 Node 进程查询 registry 之前会解析 npm CLI 路径。现在它会验证解析后的路径是否指向 JavaScript 文件。当版本管理器将 npm 二进制文件替换为 shell 包装脚本时,将改用标准的 npm-cli.js 路径。新增四个单元测试,分别覆盖符号链接、shell 包装脚本、文件不存在和 Windows 四种情况。

为什么需要

通过 mise 管理 Node 的用户在每次启动和每次执行 /update 时都会看到 "registry error"。registry 可以正常访问,包也能通过命令行正确解析。这个错误具有误导性,因为实际的失败是 Node 试图将 bash 脚本当作 JavaScript 解析时抛出的 SyntaxError——错误分类器无法将其识别为网络或超时问题,因此回退到默认的 "registry" 标签。同样的机制会影响任何用非 JS 脚本包装 bin/npm 的版本管理器(asdf、proto)。

审查者测试计划

如何验证

运行受影响模块的单元测试:

npx vitest run packages/cli/src/utils/installationInfo.test.ts

全部 36 个测试通过(32 个已有 + 4 个新增)。新测试 mock realpathSync 返回非 .js 路径(模拟 mise 的 bash shim),并断言返回的是回退的 npm-cli.js 路径。

证据(修改前后对比)

N/A — 非 UI 变更。

测试平台

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境

仅单元测试(vitest run)。

风险与范围

  • 主要风险或权衡:极小——回退路径(<prefix>/lib/node_modules/npm/bin/npm-cli.js)与 realpathSync 抛出异常时已使用的路径相同。唯一的行为变化是:当解析后的路径不是 .js 文件时,现在会触发回退,而不是原样返回。
  • 未验证 / 超出范围:asdf 和 proto 安装环境(机制相同,但本地无法手动测试)。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

Closes #7543

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 QwenLM#7543
@nerdalytics

Copy link
Copy Markdown
Contributor Author

Both PRs fix the same root cause with the same approach: validate the resolved path before handing it to Node. The code changes are functionally identical.

The difference is test coverage. #7544 adds one test — the wrapper case that triggers the bug. This PR adds four, covering the paths that the fix touches but the bug doesn't exercise:

  • Symlink resolves to .js — the normal case on stock Node installs. Without this test, a future change to the validation logic could silently break every standard installation and the suite wouldn't catch it.
  • Adjacent npm missing entirely — the ENOENT path. The fix refactored the catch block to use a shared variable; this test confirms the fallback still fires when there is no adjacent file at all.
  • Windows — the early return that bypasses the validation entirely. Untested on fix(cli): resolve npm wrappers to npm-cli.js #7544, which was verified on Linux only.

#7544 was tested on Linux. This PR was tested on macOS, where the bug was found and where mise is the primary Node version manager.

Either PR fixes the user-facing issue. The question for maintainers is whether the additional regression coverage justifies merging this one instead, or cherry-picking the extra tests onto #7544.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: confirmed observed bug — issue #7543 documents mise users hitting "registry error" on every startup, with the exact SyntaxError: Unexpected identifier 'pipefail' traced to getNpmCliPath returning a bash wrapper path. Clear reproduction, not theoretical.

Direction: aligned — the update check is core functionality and this fixes a real breakage for version-manager users. No CHANGELOG reference needed; the bug is self-evident from the issue.

Size: not applicable (no core paths touched — packages/cli/src/utils/ only).

Approach: the scope is exactly right. One guard clause, one shared fallback variable, four focused tests. Nothing to cut.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:已确认的实际 bug——issue #7543 记录了 mise 用户每次启动都遇到 "registry error",根因是 getNpmCliPath 返回了 bash 包装脚本路径,导致 SyntaxError: Unexpected identifier 'pipefail'。有明确复现,非理论性问题。

方向:对齐——更新检查是核心功能,此修复解决了版本管理器用户的实际故障。

规模:不适用(未触及核心路径——仅 packages/cli/src/utils/)。

方案:范围恰到好处。一个守卫子句、一个共享回退变量、四个聚焦测试。无需删减。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at 4fd4805d32e28cab7dc64c8c932a11a369b5a0d9 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Clean, minimal fix. The approach is exactly what I'd propose independently: hoist the fallback npm-cli.js path into a shared const, add a .endsWith('.js') guard on the realpathSync result, and fall through to the conventional path when a version manager has substituted a shell wrapper. The catch block reuses the same const — no behavioral change for the existing ENOENT path.

Both downstream callers (updateCheck.ts and managed-npm-update.ts) pass the result to node <path>, so returning a .js file is the correct invariant. No signature change, no new exports beyond the already-exported getNpmCliPath.

The four new tests cover the right cases: normal symlink → .js, mise bash shim → fallback, ENOENT → fallback, and Windows early-return. They follow the existing mock patterns in the file (mockedRealPathSync).

No critical blockers. No convention violations.

Real-Scenario Testing

Non-UI change — verified via unit tests and headless CLI smoke test.

Unit tests (36/36 pass, including 4 new)

 ✓ src/utils/installationInfo.test.ts (36 tests) 13ms

 Test Files  1 passed (1)
      Tests  36 passed (36)
   Duration  8.72s

Headless CLI smoke test (PR build)

$ node dist/cli.js -p 'say hello' --output-format text 2>&1 | head -20
Warning: QWEN_HOME points to "/home/github-runner/actions-runner-25/_work/_temp/qwen-home" but no settings.json was found there. Existing config remains at "/home/github-runner/.qwen" — OAuth tokens,
settings, memory, extensions, and skills are not auto-migrated. Copy them manually if you want them to apply at the new location.
Hello! How can I help you today?

CLI starts cleanly, no update-check crash. The QWEN_HOME warning is a CI environment artifact, unrelated to this PR.

Note: cannot reproduce the mise scenario on this machine (no mise installed), so the before/after evidence is the unit tests mocking realpathSync to return a non-.js path. The issue (#7543) provides the real-world reproduction.

中文说明

代码审查

干净、最小化的修复。方案与我独立提出的完全一致:将回退的 npm-cli.js 路径提取为共享常量,对 realpathSync 的结果添加 .endsWith('.js') 守卫,当版本管理器用 shell 包装脚本替代了符号链接时回退到标准路径。catch 块复用同一常量——现有的 ENOENT 路径行为不变。

两个下游调用方(updateCheck.tsmanaged-npm-update.ts)都将结果传给 node <path>,因此返回 .js 文件是正确的不变量。无签名变更,无新增导出。

四个新测试覆盖了正确的场景:正常符号链接 → .js、mise bash shim → 回退、ENOENT → 回退、Windows 提前返回。遵循文件中现有的 mock 模式。

无关键阻塞项。无规范违反。

实际场景测试

非 UI 变更——通过单元测试和无头 CLI 冒烟测试验证。

单元测试 36/36 全部通过(含 4 个新增)。无头 CLI 启动正常,无更新检查崩溃。本机未安装 mise,无法复现真实场景,issue #7543 提供了真实世界的复现。

Qwen Code · qwen3.8-max-preview

Reviewed at 4fd4805d32e28cab7dc64c8c932a11a369b5a0d9 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — confirmed bug, minimal fix, clean tests, no reservations.

This is the textbook small bugfix: one guard clause, one shared const, four tests. The issue (#7543) has a clear reproduction with the exact SyntaxError: Unexpected identifier 'pipefail', and the fix addresses the root cause directly — validate the resolved path is a .js file before handing it to node. The fallback path is the same one already used for the ENOENT case, so there's no new failure mode.

The diff carries nothing beyond what the goal needs. No drive-by refactors, no scope creep. If I had to maintain this in six months, I'd thank the author.

中文说明

置信度:5/5 — 已确认的 bug,最小化修复,测试完整,无保留意见。

这是一个教科书式的小修复:一个守卫子句、一个共享常量、四个测试。issue #7543 有清晰的复现(SyntaxError: Unexpected identifier 'pipefail'),修复直接针对根因——在将路径交给 node 之前验证是否为 .js 文件。回退路径与 ENOENT 场景已使用的路径相同,不引入新的失败模式。diff 不包含目标之外的任何改动。

Qwen Code · qwen3.8-max-preview

Reviewed at 4fd4805d32e28cab7dc64c8c932a11a369b5a0d9 · 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. ✅

@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.

No issues found. LGTM! ✅

— qwen3.7-max via Qwen Code /review

@gwinthis

Copy link
Copy Markdown
Collaborator

Review & Local Verification Report

代码审查

设计评价:完备的防御性修复。 本 PR 解决了版本管理器(mise/asdf/proto)将 bin/npm 替换为 bash wrapper 而非 npm-cli.js symlink 的问题。

修复策略:

const resolved = fs.realpathSync(adjacentNpm);
if (resolved.endsWith('.js')) return resolved;  // 真正的 npm-cli.js
return npmCliJs;  // 回退到标准路径

亮点:

  • 4 个测试用例覆盖:正常 .js 解析、mise wrapper 回退、ENOENT 回退、Windows 路径
  • 注释清晰解释了 mise 的目录结构和 wrapper 行为
  • .endsWith('.js') 检查简单有效——npm-cli.js 是唯一合法的 .js 入口

注意: 本 PR 与 #7544 解决同一问题,但本 PR 测试更完备(4 cases vs 1 case),注释更详细。

结论

LGTM。 推荐本 PR 而非 #7544(测试覆盖更全面)。

@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.

No issues found. LGTM! ✅

— qwen3.7-max 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.

LGTM. Minimal fix, 4 tests covering all branches, no scope creep. Merging this over #7544 and #7591.

@yiliang114
yiliang114 added this pull request to the merge queue Jul 23, 2026
Merged via the queue into QwenLM:main with commit f9a0356 Jul 23, 2026
8 checks passed
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

4 participants