Skip to content

fix(cli): align npm update checks with global registry - #7224

Merged
wenshao merged 8 commits into
QwenLM:mainfrom
yiliang114:cx/fix-update-registry-relaunch
Jul 19, 2026
Merged

fix(cli): align npm update checks with global registry#7224
wenshao merged 8 commits into
QwenLM:mainfrom
yiliang114:cx/fix-update-registry-relaunch

Conversation

@yiliang114

@yiliang114 yiliang114 commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR makes global npm installations use the same npm configuration scope for update checks and installation.

Global installations query versions through the trusted npm CLI, matching the registry and authentication configuration used by npm install --global. Both the check and installation paths invoke the npm CLI through the active Node.js runtime without going through the shell or the current project's PATH.

If an explicit update check or installation fails, the stable launcher restarts the existing working version instead of ending the session flow. A background update requested for normal session exit preserves the existing exit behavior.

Why it's needed

Before this change, the update check could read the current project's .npmrc, while the global installation command used a different npm executable or configuration path.

This is reproducible locally: Qwen Code 0.19.12 sees 0.20.0 through the project's npmjs registry, then npm install --global uses anpm, where 0.20.0 is not available, and fails with ETARGET No matching version found.

The result is an update notification for a version that the configured installation source cannot install.

Reviewer Test Plan

How to verify

  1. Configure the project .npmrc and user npm configuration with different registries.
  2. Run the update check from an older globally installed Qwen Code version.
  3. Confirm that version discovery and global installation use the same trusted npm CLI and registry configuration.
  4. Simulate an explicit update-check or installation failure and confirm that the existing version is relaunched.
  5. Confirm that a background update requested for normal session exit does not reopen the CLI when installation cannot proceed.
  6. Confirm that npm is invoked through the active Node.js runtime and an absolute npm CLI path without a shell.

Evidence (Before & After)

Before: The locally installed Qwen Code 0.19.12 reports 0.20.0 from the project npmjs registry, while npm install --global @qwen-code/qwen-code@0.20.0 --dry-run requests anpm and fails with ETARGET.

After: The patched check identifies the installation as global npm and queries through the same trusted npm CLI used for installation. In the reproduced environment, both operations resolve 0.19.12, so Qwen Code no longer offers an update that its configured registry cannot install.

Tested on

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

Environment (optional)

macOS with Node.js 22 and npm 10. Windows and Linux invocation paths are covered by automated tests, but no live Windows or Linux global update was performed.

Risk & Scope

  • Main risk or tradeoff: Users of a private registry mirror receive an update only after that mirror synchronizes the new version. This is intentional so every offered version is installable from the configured source.
  • Not validated / out of scope: Immediate safe relaunch for background automatic updates is intentionally split into a separate PR.
  • Breaking changes / migration notes: None.

Linked Issues

Fixes #7151. Follow-up to #6874 and #6889.

中文说明

此 PR 做了什么

此 PR 确保全局 npm 安装的 Qwen Code 在检查和安装更新时使用同一个 npm 配置作用域。

全局安装现在通过可信 npm CLI 查询版本,与后续 npm install --global 使用相同的 registry 和认证配置。检查与安装都通过当前 Node.js 运行时直接执行 npm CLI,不再经过 shell 或当前项目的 PATH

如果显式更新检查或安装失败,稳定 launcher 会重新启动当前可用的旧版本,避免用户流程直接结束。正常会话退出时触发的后台更新保持原有退出语义。

为什么需要

此前更新检查可能读取当前项目的 .npmrc,而全局安装使用不同的 npm 可执行文件或配置路径。本机可以稳定复现:Qwen Code 0.19.12 通过项目 npmjs registry 看到 0.20.0,随后 npm install --global 实际访问 anpm,而 anpm 中没有 0.20.0,最终报 ETARGET No matching version found

这会产生“提示存在更新,但配置的安装源无法安装该版本”的不一致行为。

Reviewer 测试计划

如何验证

  1. 将项目 .npmrc 和用户 npm 配置为不同 registry。
  2. 从全局 npm 安装的旧版 Qwen Code 执行更新检查。
  3. 确认版本发现与全局安装使用同一个可信 npm CLI 和 registry 配置。
  4. 模拟显式更新检查或安装失败,确认旧版本被重新启动。
  5. 确认正常会话退出时触发的后台更新如果无法执行,不会重新打开 CLI。
  6. 确认 npm 通过当前 Node.js 运行时和绝对 npm CLI 路径执行,不经过 shell。

证据(Before & After)

Before:本机 Qwen Code 0.19.12 从项目 npmjs registry 提示 0.20.0,但 npm install --global @qwen-code/qwen-code@0.20.0 --dry-run 实际请求 anpm 并报 ETARGET

After:补丁识别当前安装为 global npm,并通过与安装相同的可信 npm CLI 查询版本。在复现环境中,两次操作都解析到 0.19.12,因此不会再提示配置安装源无法安装的更新。

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境

macOS、Node.js 22、npm 10。Windows 和 Linux 调用路径由自动化测试覆盖,但没有执行真实 Windows/Linux 全局更新。

风险与范围

  • 主要风险或权衡:使用私有 registry mirror 的用户只有在 mirror 同步新版本后才会收到更新提示,这是为了保证提示的版本实际可安装。
  • 未验证或范围外:后台自动更新的立即安全重启将明确拆到另一个 PR。
  • Breaking changes / migration notes:无。

关联 Issue

关联 #6874#6889 的后续修复。

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: Observed bug with a clear reproduction. Issue #7151 reports a failure during upgrade, and the PR describes the exact scenario: the update check reads the project's .npmrc (npmjs registry) while npm install --global uses a different registry (anpm), producing an ETARGET error for a version the configured source cannot install. This is a real, reproducible mismatch.

Direction: Aligned — this fixes a concrete user-facing bug in the update flow. The update check and installation must use the same registry configuration; offering an update that can't be installed is broken behavior.

Size: Not applicable — all changes are in packages/cli/src/, no core module paths touched. 208 production lines + 367 test lines across 11 files.

Approach: The scope feels right. The PR addresses the registry mismatch at both the check path (query versions through the same npm CLI used for installation) and the install path (invoke npm through the active Node.js runtime instead of a shell). The relaunchOnFailure parameter cleanly separates explicit-update vs background-update-on-exit behavior. Every change in the diff serves the stated goal — no drive-by refactors or unrelated edits.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:已观测到的 bug,有明确复现。Issue #7151 报告了升级时的错误,PR 描述了具体场景:更新检查读取项目的 .npmrc(npmjs registry),而 npm install --global 使用不同的 registry(anpm),导致 ETARGET 错误。这是一个真实的、可复现的配置不一致问题。

方向:对齐——修复更新流程中具体的用户可见 bug。更新检查和安装必须使用相同的 registry 配置;提示一个配置源无法安装的版本是错误行为。

规模:不适用——所有改动在 packages/cli/src/,未触及核心模块路径。11 个文件,208 行生产代码 + 367 行测试代码。

方案:范围合理。PR 在检查路径(通过与安装相同的 npm CLI 查询版本)和安装路径(通过当前 Node.js 运行时执行 npm 而非 shell)两处修复了 registry 不一致。relaunchOnFailure 参数清晰地区分了显式更新与后台退出时更新的行为。diff 中每个改动都服务于目标——无顺手重构或无关改动。

进入代码审查 🔍

Qwen Code · qwen3.7-max

Reviewed at 95643c2d9eeb5ce6c67ffb856febad17ab0cb3be · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: Given "update check uses project .npmrc but installation uses global npm config", I would: (1) detect global npm installation by comparing the resolved CLI path against npm root --global, (2) query versions through npm view --global which uses the same config as npm install -g, (3) invoke npm through the active Node.js runtime with an absolute path to avoid shell/PATH issues, (4) on failure, relaunch the old version for explicit updates but not for background updates.

Comparison with the diff: The PR's approach matches this proposal closely. The implementation is clean and well-structured:

  • isGlobalNpmInstallation canonicalizes process.argv[1] before matching (handles bin symlinks), excludes pnpm paths, and compares against the canonicalized npm root --global using path.relative — robust against symlinked layouts.
  • runGlobalNpm uses execFile (no shell) with process.execPath + absolute npm-cli.js path — consistent between check and install, and safe against PATH manipulation.
  • getNpmCliPath resolves the npm symlink adjacent to the node binary with a sensible fallback for split layouts. The synchronous API is justified by the non-async call site in handleAutoUpdate.
  • The relaunchOnFailure boolean cleanly threads through relaunch.tsgemini.tsxupdate-relaunch.ts, distinguishing explicit updates (relaunch old version on failure) from background updates (preserve exit semantics).
  • DI parameters on checkForUpdatesDetailed enable isolated testing without real I/O — good pattern that existing tests already leverage.

No critical blockers or AGENTS.md violations found. One non-blocking observation: handleAutoUpdate extracts npm args via updateCommand.split(' ').slice(1) — this works for all known resolveUpdateCommand outputs (npm install -g @pkg@version) but would break if a future command format introduced spaces in arguments. Fine for now.

Real-Scenario Testing

Unit tests: 125 tests pass across all 5 changed test files (updateCheck: 29, handleAutoUpdate: 26, update-relaunch: 6, relaunch: 15, gemini: 49). Typecheck clean.

CLI smoke test (built from PR code):

$ node packages/cli/dist/index.js -p 'say hello' --output-format text
Hello! How can I help you today?

Direct function verification:

$ node -e "const {getNpmCliPath} = require('./packages/cli/dist/src/utils/installationInfo.js'); ..."
npm CLI path: /usr/lib/node_modules/npm/bin/npm-cli.js
exists: true

$ node --input-type=module -e "import {isGlobalNpmInstallation, runGlobalNpm} from '...'; ..."
isGlobalNpmInstallation: false
npm root --global: /home/github-runner/.npm-global/lib/node_modules

$ node --input-type=module -e "import {fetchGlobalNpmUpdateInfo} from '...'; ..."
fetchGlobalNpmUpdateInfo: {"latest":"2.1.215","current":"1.0.0","type":"latest","name":"@anthropic-ai/claude-code"}

All three new functions work correctly in a real environment: getNpmCliPath resolves the npm CLI path, isGlobalNpmInstallation correctly returns false for a source-tree run (not a global install), runGlobalNpm executes npm through the active Node.js runtime, and fetchGlobalNpmUpdateInfo successfully queries a registry through the global npm configuration.

中文说明

代码审查

独立方案: 针对"更新检查使用项目 .npmrc 但安装使用全局 npm 配置"的问题,我的方案是:(1) 通过比较解析后的 CLI 路径与 npm root --global 来检测全局 npm 安装;(2) 通过 npm view --global 查询版本(与 npm install -g 使用相同配置);(3) 通过当前 Node.js 运行时以绝对路径执行 npm,避免 shell/PATH 问题;(4) 失败时,显式更新重启旧版本,后台更新保持退出语义。

与 diff 的比较: PR 方案与我的独立提案高度一致。实现干净、结构良好:

  • isGlobalNpmInstallation 在匹配前对 process.argv[1] 做规范化(处理 bin 符号链接),排除 pnpm 路径,使用 path.relative 与规范化的 npm root --global 比较——对符号链接布局健壮。
  • runGlobalNpm 使用 execFile(无 shell)+ process.execPath + 绝对 npm-cli.js 路径——检查与安装一致,且对 PATH 操纵安全。
  • getNpmCliPath 解析 node 二进制旁的 npm 符号链接,对分离布局有合理回退。同步 API 由 handleAutoUpdate 的非异步调用点证明合理。
  • relaunchOnFailure 布尔值清晰地贯穿 relaunch.tsgemini.tsxupdate-relaunch.ts,区分显式更新(失败时重启旧版本)与后台更新(保持退出语义)。
  • checkForUpdatesDetailed 的 DI 参数支持无真实 I/O 的隔离测试——现有测试已利用此模式。

未发现关键阻塞问题或 AGENTS.md 违规。一个非阻塞观察:handleAutoUpdate 通过 updateCommand.split(' ').slice(1) 提取 npm 参数——对所有已知 resolveUpdateCommand 输出有效,但如果未来命令格式引入带空格的参数会中断。目前没问题。

真实场景测试

单元测试:125 个测试全部通过(updateCheck: 29, handleAutoUpdate: 26, update-relaunch: 6, relaunch: 15, gemini: 49)。类型检查通过。

CLI 冒烟测试(从 PR 代码构建):正常启动并响应。三个新函数在真实环境中均正确工作。

Qwen Code · qwen3.7-max

Reviewed at 95643c2d9eeb5ce6c67ffb856febad17ab0cb3be · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — solid fix for a real bug; clean implementation with thorough tests, only non-blocking nit on the arg-splitting pattern.

This PR solves a problem I can immediately recognize as real: the update check and the update installation disagree on which registry to use, so users get offered a version they can't install. The reproduction in the PR description is concrete (0.19.12 sees 0.20.0 via project npmjs, but npm install -g hits anpm where 0.20.0 doesn't exist), and the linked issue #7151 confirms a user hit this.

The implementation matches my independent proposal almost exactly. The detection logic (isGlobalNpmInstallation) is careful — canonicalizes symlinks, excludes pnpm, uses path.relative for containment. The npm invocation (runGlobalNpm + getNpmCliPath) is consistent between check and install, uses execFile without a shell, and resolves the npm CLI path through the active Node.js runtime. The relaunchOnFailure threading is the minimal change needed to distinguish explicit vs background update semantics.

The code is straightforward to maintain. Nothing feels over-engineered — the DI parameters exist because the tests need them, the comments explain non-obvious decisions (why realpathSync, why empty stdout is treated as "no update"), and the test coverage is thorough (125 tests, all passing). If I had to maintain this in six months, I'd thank the author.

The only nit: updateCommand.split(' ').slice(1) in handleAutoUpdate is a string-parsing shortcut that works for all current command formats but is slightly fragile. Not worth blocking over.

中文说明

置信度:4/5 — 对真实 bug 的扎实修复;实现干净、测试充分,仅有参数拆分模式的非阻塞小问题。

这个 PR 解决了一个我立刻能认定为真实的问题:更新检查和更新安装对使用哪个 registry 不一致,导致用户被提示一个无法安装的版本。PR 描述中的复现具体(0.19.12 通过项目 npmjs 看到 0.20.0,但 npm install -g 访问 anpm 而 0.20.0 不存在),关联的 issue #7151 确认用户遇到了此问题。

实现与我的独立方案几乎完全一致。检测逻辑(isGlobalNpmInstallation)谨慎——规范化符号链接、排除 pnpm、使用 path.relative 做包含判断。npm 调用(runGlobalNpm + getNpmCliPath)在检查和安装间一致,使用 execFile 无 shell,通过当前 Node.js 运行时解析 npm CLI 路径。relaunchOnFailure 的传递是区分显式与后台更新语义所需的最小改动。

代码易于维护。没有过度工程——DI 参数因测试需要而存在,注释解释了非显而易见的决策,测试覆盖充分(125 个测试全部通过)。

唯一的小问题:handleAutoUpdate 中的 updateCommand.split(' ').slice(1) 是字符串解析捷径,对所有当前命令格式有效但略脆弱。不值得阻塞。

Qwen Code · qwen3.7-max

Reviewed at 95643c2d9eeb5ce6c67ffb856febad17ab0cb3be · 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. ✅

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The main flow looks right to me — routing both the version check and the install through the same npm --global CLI invocation is the right fix for the ETARGET mismatch: check and install now resolve the same registry by construction, so they can no longer disagree.

I traced the return false → true changes in update-relaunch.ts against the caller in gemini.tsx (return shouldRelaunch ? UPDATE_COMPLETE_EXIT_CODE : 0). The intent is consistent — relaunch the existing version on update/check failure instead of dropping the session — and the standalone-deferred path still correctly returns false, so the boolean stays meaningful.

One non-blocking nit: in fetchGlobalNpmUpdateInfo, type: 'latest' is hardcoded even when distTag is 'nightly'. Harmless today since the downstream only reads .latest, but it's misleading if anything starts consuming .type later — might be worth deriving it from distTag.

Nothing blocks merge.

@wenshao

wenshao commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Code Review — fix(cli): align npm update checks with global registry

Reviewed the diff against main, plus the surrounding runtime flow (update-relaunch.tsgemini.tsx onUpdateRelaunchrelaunch.ts exit-code loop → scripts/cli-entry.js launcher). Overall this is a solid, well-tested fix that correctly targets the root cause. A few points worth confirming before merge.

What it does

  • Version check now goes through the real npm CLI. For global-npm installs, the check runs npm view <pkg> dist-tags.<tag> --json --global instead of update-notifier's internal registry client, so version discovery uses the same config resolution as the subsequent npm install --global. This is the right fix for the reported ETARGET mismatch — both operations now resolve the registry identically (same cwd, same .npmrc precedence).
  • Windows npm runs shell-free. Both the check and the auto-update spawn node <npm-cli.js> … directly instead of cmd.exe /c "<command>".
  • Failed update no longer drops the user to a shell. updateBeforeRelaunch now returns true on failure paths, so the launcher relaunches the existing (working) version via exit code 44.

Strengths

  • Loop-safe by design. I traced the concern that "always return true on failure" could relaunch-loop forever. It can't: exit 44cli-entry.js relaunches with QWEN_CODE_SKIP_UPDATE_CHECK_ONCE=true, so the relaunched session skips the check and won't re-arm the update. Worst case is one extra restart per failed-update session. Good.
  • Shell-free Windows invocation is a real defense-in-depth win — the version string can no longer be interpreted by cmd.exe.
  • Clean testability via dependency injection (run, canonicalize, resolveNpmCliPath), and the subpath check in isGlobalNpmInstallation is robust (realpath-canonicalizes both sides, handles ENOENT, rejects ../absolute path.relative).
  • Low blast radius — non-global-npm installs keep the exact update-notifier path unchanged.

Points to confirm / suggestions

1. (Minor correctness) Misleading "Update installed" message on a failed update when no launcher is found.
In cli-entry.js, exit code 44 with !launcher prints:

Update installed. Restart Qwen Code to use the new version.

Previously a failed update returned false → exit 0 → this line never printed. Now a failed update returns true → exit 44 → this line prints even though updateBeforeRelaunch just wrote "Automatic update failed…" to stderr. The user sees contradictory messages. Edge-case (global-npm installs normally have qwen on PATH, so launcher is found), but worth gating the message on actual success or wording it neutrally (e.g. "Restart Qwen Code to continue.").

2. (Confirm intent + test) up-to-date / skipped now also return true.
The final return true is reached not just on failure but when checkForUpdatesDetailed() returns up-to-date or skipped — so those now trigger a relaunch too (previously false → no relaunch). This is defensible (the caller only runs in the update-requested flow, and skip-once prevents looping), but it's a behavior change beyond what the PR description covers ("if the check or install fails"), and it's untested. Suggest adding a case asserting the up-to-date return value so the intent is pinned.

3. (Maintainability) Duplicated global-npm detection.
isGlobalNpmInstallation (via npm root --global + realpath) is a second, independent mechanism from getInstallationInfo()'s path heuristics (isGlobal + packageManager === NPM). Two detectors for the same fact can drift. checkForUpdatesDetailed() has no projectRoot, so I understand why it's standalone — but a shared helper (or passing the already-computed InstallationInfo) would avoid divergence.

4. (Minor perf) Extra npm subprocesses on the check path.
A nightly global-npm check now spawns up to three npm processes (npm root --global, then npm view ×2). It's background + FETCH_TIMEOUT_MS-bounded so probably fine, but it's heavier than the previous pure-HTTP check. Worth being aware of on slow/cold-start machines.

5. (Nits)

  • handleAutoUpdate.ts: the command/commandArgs ternaries evaluate isWindows && packageManager === NPM twice — extract a const useWindowsNpm = … for readability. Also updateCommand.split(' ').slice(1) is brittle if the command format ever gains a quoted arg; today it's safe.
  • looksLikeNpmPackagePath hardcodes @qwen-code/qwen-code, while checkForUpdatesDetailed uses the dynamic packageJson.name. On a rename/fork the detector silently falls back to update-notifier. Low impact, but inconsistent.
  • fetchGlobalNpmUpdateInfo hardcodes type: 'latest' even for the nightly tag. Harmless (only .latest/.current are consumed downstream), but slightly misleading.
  • Consistency: runGlobalNpm reads process.platform while handleAutoUpdate reads os.platform(). Equivalent at runtime, just mismatched.

6. (Question) Does --global on npm view actually change registry selection?
The consistency guarantee here comes from routing through the npm CLI with the same cwd — that part is sound. But npm view --global still reads the project ./.npmrc first (global mode changes the prefix, not .npmrc precedence), so --global on a view may be a no-op for registry resolution. Not a problem (it's harmless and the fix still works), but if the intent was specifically to force the global registry, that may not be what --global does here.

Test coverage

Good — new unit tests cover the global-npm view path, pnpm exclusion, Windows shell-free invocation, symlink canonicalization, and the ENOENT guard. Gaps: the up-to-date/skipped → relaunch behavior (#2) and the !launcher failed-update message (#1) are untested.


Nothing here is blocking; #1 and #2 are the two I'd most want addressed (or explicitly confirmed as intended) before merge.

中文小结

方向正确、测试扎实。核心修复(版本检查改走真实 npm CLI,使 npm viewnpm install 使用同一套 registry 解析)能真正消除 ETARGET 不一致;Windows 不再经过 shell 是安全加分项;失败后返回 true 不会造成死循环(launcher 用 QWEN_CODE_SKIP_UPDATE_CHECK_ONCE 兜底,最多多重启一次)。

建议关注:

  1. 失败但找不到 launcher 时cli-entry.js 仍会打印 "Update installed…",与刚打印的"更新失败"矛盾(边缘场景)。
  2. up-to-date / skipped 现在也返回 true(会触发重启),超出 PR 描述范围且无测试,建议补测试确认意图。
  3. 全局 npm 检测在 installationInfo.tsupdateCheck.ts 各有一套,易漂移。
  4. 检查路径新增最多 3 个 npm 子进程(性能,可接受)。
  5. 小问题:重复三元表达式、硬编码包名、type:'latest'process.platform vs os.platform()
  6. npm view --global 是否真的改变 registry 选择存疑(--global 只改 prefix,不改 .npmrc 优先级)——不影响正确性。

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

Reviewed. Suggestions are inline.

— qwen-latest-series-invite-beta-v77 via Qwen Code /review

Comment thread packages/cli/src/utils/update-relaunch.ts
Comment thread packages/cli/src/utils/handleAutoUpdate.ts
Comment thread packages/cli/src/ui/utils/updateCheck.ts
@yiliang114 yiliang114 changed the title fix(cli): align npm update checks with global registry fix(cli): make automatic updates safe and registry-consistent Jul 19, 2026
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Validation update for 5c5b529a76:

  • 441 focused launcher, update, relaunch, sandbox, AppContainer, stream, and CLI tests passed.
  • Repository npm run typecheck, changed-file ESLint, and the full npm run build passed.
  • Independent correctness review found no remaining actionable defect.
  • Independent simplicity/Ponytail review found no unnecessary mechanism left in the relaunch path.

The three latest automated review notes are coverage-only Suggestions rather than defects in the current behavior: the no-update-command return permutations, the unchanged non-npm shell invocation branch, and the negative global-path containment branch. This PR has already exceeded the repository's review-round threshold, so I am deferring those additional test-only expansions to a focused follow-up instead of widening this update-safety change. No Critical finding remains.

@yiliang114
yiliang114 marked this pull request as draft July 19, 2026 11:24
@yiliang114 yiliang114 changed the title fix(cli): make automatic updates safe and registry-consistent fix(cli): align npm update checks with global registry Jul 19, 2026
@yiliang114
yiliang114 marked this pull request as ready for review July 19, 2026 12:02
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Follow-up hardening on this PR:

  • isGlobalNpmInstallation now realpath-resolves argv[1] before matching the npm package path (consistent with getInstallationInfo), so a bin-symlink launch no longer silently skips the global-npm update path.
  • getNpmCliPath falls back to <prefix>/lib/node_modules/npm/bin/npm-cli.js on POSIX instead of throwing when npm isn't adjacent to node, letting the spawn's error handler surface failures.

Added a regression test that a .../bin/qwen symlink launch (no node_modules in the raw path) still resolves to a global npm install.

@wenshao

wenshao commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Re-review (updated PR)

Re-reviewed after the latest push (head 47b4a63e, +389/−49, 11 files; note one commit + its revert are in the range). Focusing on the delta from my previous comment.

Prior concerns — addressed 👍

  • Where is the config saved? #2 "everything returns true" is resolved cleanly. The blanket return was replaced with a threaded relaunchOnFailure flag: relaunchOnExitCode passes true for the explicit-/update path (UPDATE_RELAUNCH_EXIT_CODE), and relaunchAppInChildProcess passes false for the background update-on-exit path. So updateBeforeRelaunch now returns success || relaunchOnFailure, which is exactly the right semantics — a user who asked to update stays in a working session on failure, while a background check that fails just exits 0 as before. Good design, and well covered by the new it.each / onUpdateRelaunch(true|false) tests.
  • pre-release: fix ci #1 "misleading Update installed message" is largely resolved as a consequence: the background path now returns false on failure → exit 0cli-entry.js never reaches the 44 branch. (Residual edge case below.)
  • Testability improved — checkForUpdatesDetailed(detectGlobalNpm, fetchGlobalNpm) injection plus the new "selects the global npm registry" integration test are nice additions.

New concern (introduced by this revision) — please look at this one

getNpmCliPath on non-Windows uses fs.realpathSync, and npm now runs through it on all platforms — this trades resilience for fragility.

installationInfo.ts:

return fs.realpathSync(path.join(path.dirname(nodePath), 'npm')); // non-win32

and handleAutoUpdate.ts now gates on packageManager === NPM (no longer isWindows && …), so the Linux/macOS auto-update path also switched from bash -c "npm install -g …" to node <resolved npm-cli.js> …. Two issues:

  1. realpathSync throws ENOENT if dirname(process.execPath)/npm doesn't exist. That happens whenever npm isn't a sibling of the running node (distro packages that split node/npm into different bin dirs, a qwen shebang resolving to a different node than the one npm ships with, etc.). When it throws:

    • in the check path (isGlobalNpmInstallation → runGlobalNpm) it rejects → checkForUpdatesDetailed returns status: 'error' → the user silently stops getting update notifications, where update-notifier (HTTP) previously worked;
    • in the install path it throws synchronously out of handleAutoUpdate → update fails.

    This is a silent functional regression on setups that used to work. I verified the happy path resolves correctly here (/usr/bin/node → /usr/lib/node_modules/npm/bin/npm-cli.js), so it's fine for standard installs — but the failure mode is silent.

  2. No consistency benefit on Unix to justify the fragility. The stated goal is that check and install use the same npm. On Unix, PATH-based npm (what the previous revision used for the check, and what bash -c used for install) already resolves identically for both — same process.env.PATH, same result. So realpathSync(dirname(node)/npm) doesn't buy consistency here; it only adds a hard dependency on the node/npm colocation layout. The valuable, non-controversial change is the Windows one (shell → node npm-cli.js).

    Suggestion: keep the strict path for Windows, but on Unix wrap it in a try/catch and fall back to plain 'npm' (PATH) — or fs.existsSync-guard before realpathSync — so a non-colocated layout degrades to the old resilient behavior instead of erroring.

Also note the Windows/Unix asymmetry: getNpmCliPath on Windows returns a constructed path without checking existence (a wrong path just yields a graceful spawn 'error' event), whereas Unix realpathSync throws eagerly. Same fragility, different failure surface per platform.

Residual minor points

  • Explicit /update + no launcher on PATH + failure still hits the !launcher branch in cli-entry.js and prints "Update installed. Restart Qwen Code…" after a failure (because that path intentionally returns true). Much narrower than before, but the message is still inaccurate in that combination.
  • relaunchOnFailure now also governs up-to-date / skipped (they fall through to return relaunchOnFailure). So /update when already current triggers a relaunch. Harmless (skip-once guards the loop) and arguably fine, but the flag name reads as "failure-only" — a one-line comment on the fall-through would help future readers.
  • PR description drift: the body still says "On Windows, both the check and installation paths invoke the trusted npm CLI…" — that's now true on all platforms. Worth updating so reviewers/users know the Unix install path changed too.
  • Test coupling: the amended handleAutoUpdate "correct package manager" test (no os.platform mock) now asserts process.execPath + /npm-cli\.js$/ on the host, so it depends on npm being colocated on the CI runner. Fine on GitHub Actions setup-node, but it's an environmental coupling that didn't exist before.
  • Still-standing nits from last round (low priority): duplicated global-npm detection (installationInfo heuristics vs isGlobalNpmInstallation), up to 3 npm subprocess spawns on the nightly check path, looksLikeNpmPackagePath hardcodes the package name, and fetchGlobalNpmUpdateInfo sets type: 'latest' even for the nightly tag.

Verdict

The relaunch-semantics rework is a real improvement and resolves my main earlier feedback. The one thing I'd change before merge is the Unix getNpmCliPath realpathSync — add a PATH fallback so it can't silently disable update checks / break auto-update on non-colocated node/npm layouts.

中文小结

本次更新总体是进步:

  • 已解决:不再一律 return true,改为透传 relaunchOnFailure——显式 /update(exit 43)传 true,后台 update-on-exit 传 false,语义正确,测试也补齐了。之前"失败后误显示 Update installed"的问题在后台路径上随之消除。

  • 本次新引入、建议重点看getNpmCliPath 在非 Windows 上用 fs.realpathSync(dirname(node)/npm),且 handleAutoUpdate 现在所有平台都走 node npm-cli.js。若 npm 与当前 node 不在同一 bin 目录,realpathSync 会抛 ENOENT:检查路径→静默 status:'error'(用户收不到更新提示,而原来 update-notifier 走 HTTP 是好的),安装路径→更新失败。而在 Unix 上,PATH 方式对"检查/安装"本就一致,realpath 并没有带来一致性收益,只增加了对 node/npm 同目录布局的硬依赖。建议:Windows 保持严格路径,Unix 加 try/catch 回退到 'npm'(PATH)。

  • 残留小问题:显式 /update + PATH 上无 launcher + 失败时仍会打印 "Update installed"(很窄);relaunchOnFailure 也影响了 up-to-date/skipped(建议加注释);PR 描述仍写"仅 Windows",实际已是全平台;改动后的测试隐式依赖 CI 上 node/npm 同目录。

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closed the current review round with commit e7f0e81.

Fixed:

  • added coverage that non-npm package-manager updates still use the shell command path;
  • added coverage that local npm installs outside the global npm root are not treated as global npm installs.

The remaining unresolved-looking update-relaunch thread was outdated on the current diff, so I resolved it without code changes.

Verification:

  • cd packages/cli && ../../node_modules/.bin/vitest run src/utils/handleAutoUpdate.test.ts src/ui/utils/updateCheck.test.ts
  • npx prettier --check packages/cli/src/utils/handleAutoUpdate.test.ts packages/cli/src/ui/utils/updateCheck.test.ts
  • git diff --check

Note: npm -w packages/cli run typecheck is blocked in this temporary worktree by broader generated package/type resolution issues outside these two test files.

@yiliang114
yiliang114 requested a review from wenshao July 19, 2026 12:42
…paths

- isGlobalNpmInstallation now realpath-resolves argv[1] before matching the
  npm package path (consistent with getInstallationInfo), so a bin-symlink
  launch no longer silently skips the global-npm update path.
- getNpmCliPath falls back to <prefix>/lib/node_modules/npm/bin/npm-cli.js on
  POSIX instead of throwing when npm is not adjacent to node.
- fetchGlobalNpmUpdateInfo treats an empty `npm view dist-tags.<tag>` response
  as no-update instead of failing the whole check, so a missing nightly tag on
  a private mirror no longer poisons the latest check.
@yiliang114

Copy link
Copy Markdown
Collaborator Author

One more edge, pushed in ee56a77: npm view <pkg> dist-tags.<tag> --json exits 0 with empty stdout when the configured registry/mirror has no version under that tag. That empty string hit JSON.parse and threw, and since checkForUpdatesDetailed fetches nightly + latest via Promise.all, a missing nightly tag on a private mirror would fail the whole check and discard the latest result too. fetchGlobalNpmUpdateInfo now treats empty output as "no newer version for this tag" (returns current), so it no longer poisons the check. Added a test for it.

@wenshao

wenshao commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Re-review (head ee56a779)

Reviewed the delta since 47b4a63e (commits e7f0e810 tests + ee56a779 hardening). This round resolves everything I raised — and includes one fix that's more than cleanup.

Addressed 👍

  1. Unix getNpmCliPath no longer throws synchronously (my main concern). It now try/catches realpathSync(dirname(node)/npm) and falls back to the conventional <prefix>/lib/node_modules/npm/bin/npm-cli.js (verified: /usr/bin/node/usr/lib/node_modules/npm/bin/npm-cli.js). So a non-colocated layout degrades to a best-effort path that the downstream spawn/execFile surfaces via its error handler, instead of failing the whole check with a synchronous throw. The comment explaining the non-async call site is spot-on.

  2. Real correctness fix: symlink-launched global installs were silently skipping the feature. Matching looksLikeNpmPackagePath now happens after canonicalize(cliPath), not on the raw process.argv[1]. I confirmed why this matters: launched via the global bin symlink, argv[1] is .../bin/qwenlooksLikeNpmPackagePath = false, so pre-fix isGlobalNpmInstallation returned false and the entire registry-consistent path fell back to update-notifier for the most common launch method. Node doesn't resolve argv[1] symlinks (unlike getInstallationInfo, which realpaths first), so canonicalizing first is required. Good catch — this one was making the PR partly inert, not just an edge case. Nicely covered by the new "resolves a bin symlink…" test.

  3. Empty dist-tag response handled. fetchGlobalNpmUpdateInfo now treats empty npm view … dist-tags.<tag> --json stdout as "no newer version" (latest = current) rather than letting JSON.parse('') throw — which previously would have failed the whole Promise.all in checkForUpdatesDetailed and discarded the other tag. This is exactly the nightly-tag-missing-on-a-private-mirror scenario. The empty-vs-error distinction (empty → no update, real npm error → still throws) is the right call, and both are now tested.

  4. Good negative-test coverage added — local (project) node_modules install not treated as global, and non-npm managers (pnpm) still routed through the shell. Both confirm the gating is tight.

Residual (all minor / non-blocking, unchanged from before)

  • The getNpmCliPath fallback is still an absolute guess rather than a PATH lookup, so a layout where npm is on PATH but in neither dirname(node)/npm nor <prefix>/lib/node_modules/npm still won't get update checks. Much narrower now and it degrades gracefully — optional to add a final 'npm' (PATH) fallback.
  • Explicit /update + no qwen launcher on PATH + failure still reaches the !launcher branch in cli-entry.js and prints "Update installed…" (that path intentionally returns true). Narrow.
  • PR description still says "On Windows, both the check and installation paths…" — the install path now goes through node npm-cli.js on all platforms; worth updating the body.
  • A one-line comment on the return relaunchOnFailure fall-through would clarify that it also covers up-to-date/skipped, since the flag name reads as failure-only.

Verdict

LGTM from my side once the PR body is updated to match the all-platforms behavior. The symlink-resolution and empty-tag fixes materially improve correctness, and the earlier robustness concern is resolved. (I traced the logic rather than executing the suite — worth a normal CI run across the three OSes given the npm-layout assumptions.)

中文小结

本轮把我之前提的问题都处理了,而且其中一处是实打实的 bug 修复:

  1. Unix 的 getNpmCliPath 不再同步抛错(我上轮的主要顾虑):realpathSync 外包了 try/catch,失败时回退到常规的 <prefix>/lib/node_modules/npm/bin/npm-cli.js,交由下游 spawn 的 error handler 处理,不再让整个检查因同步抛错而失败。
  2. 真正的正确性修复:现在先 canonicalize(cliPath) 再做 looksLikeNpmPackagePath 匹配。之前用原始 argv[1] 匹配——通过全局 bin 软链 .../bin/qwen 启动时 argv[1] 不含 node_modules 段 → 匹配失败 → 整个"registry 一致"逻辑对最常见的启动方式直接失效、回退到 update-notifier。Node 不会解析 argv[1] 软链,所以必须先 realpath。这个修复让 PR 从"部分失效"变为真正生效。
  3. 空 dist-tag 处理npm view … dist-tags.<tag> --json 输出为空时按"无新版本"处理,避免 JSON.parse('') 抛错拖垮整个 Promise.all(私有 mirror 无 nightly 的典型场景)。
  4. 新增了很好的反例测试(本地安装、pnpm 走 shell、bin 软链、空 tag)。

残留小问题(都不阻塞):fallback 仍是绝对路径猜测而非 PATH 查找;显式 /update+PATH 无 launcher+失败仍会误显示 "Update installed";PR 描述仍写"仅 Windows"(实际已全平台,建议更新);建议给 return relaunchOnFailure 的兜底加一行注释说明也覆盖了 up-to-date/skipped。

结论:更新 PR 描述后我这边 LGTM。建议三大平台都跑一遍 CI(因为涉及 npm 布局假设)。

wenshao and others added 2 commits July 19, 2026 21:23
The QwenLM#6857 timeout tests called checkForUpdatesDetailed() with the default
detectGlobalNpm (real isGlobalNpmInstallation), which runs a real async
realpath() before the timeout setTimeout is armed. Under fake timers this
races with vi.advanceTimersByTimeAsync: on a slow/loaded runner the advance
completes before the timer is scheduled, so the timeout never fires and the
test hangs until vitest's 15s limit. Seen on ubuntu-latest CI (2 of 4 hung).

Inject a synchronous `async () => false` stub so the timer is armed
deterministically and the tests isolate the timeout logic from global-npm
detection.

@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. Suggestions are inline. Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI; the PR adds Windows-specific spawn/path code (path.win32, cmd.exe branch) and the suite ran on Linux only.

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

Comment on lines +47 to +50
} catch {
return path.join(
path.dirname(nodePath),
'..',

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 non-win32 fallback branch of getNpmCliPath (taken when fs.realpathSync throws because npm isn't symlinked adjacent to node) is not exercised by any test — installationInfo.test.ts doesn't import getNpmCliPath, and the handleAutoUpdate/updateCheck tests run where npm IS adjacent to node, so only the realpathSync-success path is hit. Concrete cost: if a future refactor alters this fallback path (e.g. drops the '..' segment), no test fails — the update spawn would ENOENT on every split-layout system (node at /usr/bin/node, npm elsewhere), surfacing only as a user-facing update failure. Consider adding a unit test that mocks fs.realpathSync to throw and asserts the returned path matches the conventional <prefix>/lib/node_modules/npm/bin/npm-cli.js shape:

it('falls back to the conventional prefix layout when npm is not adjacent to node', () => {
  vi.spyOn(fs, 'realpathSync').mockImplementation(() => { throw new Error('ENOENT'); });
  expect(getNpmCliPath('/usr/bin/node', 'linux')).toBe(
    path.join('/usr', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
  );
});

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

Comment on lines +109 to +110
getNpmCliPath(process.execPath, platform),
...updateCommand.split(' ').slice(1),

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] updateCommand.split(' ').slice(1) rebuilds the npm argument array by space-splitting a string that was designed for shell execution. The same updateCommand string is also shown verbatim to the user via formatUpdateInstructions, so the human-readable command and this machine-parsed arg array share a format contract (exactly four space-free tokens) that is enforced nowhere at the point of use. Today the interpolated version is semver.gt-validated upstream so it can't contain spaces and the split is safe; but a future edit to the template in getInstallationInfo (e.g. a --prefix /some path flag, a npx npm wrapper, or any space-containing value) would silently produce wrong npm arguments — npm would fail or install the wrong target, surfaced only as the generic "Automatic update failed". The non-npm branch passes the whole string to a shell, so the two branches already diverge in how they treat the same value. Consider building the npm args directly (e.g. [getNpmCliPath(process.execPath, platform), 'install', '-g', packageName + '@' + resolvedVersion]) or returning a structured updateArgs from getInstallationInfo; at minimum, document the assumed format and its single producer here.

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

@wenshao

wenshao commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

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

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

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

Comment thread packages/cli/src/ui/AppContainer.tsx Outdated
Comment on lines +2052 to +2054
setWorkflowKeywordActive(false);
}
}, [isIdle, buffer.text, isProcessing, messageQueue.length]);

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.

[Critical] Widening this effect's dependency array from [streamingState] to [isIdle, buffer.text, isProcessing, messageQueue.length] causes setWorkflowKeywordActive(false) to fire at submission time instead of when the steered turn finishes. — Failure scenario: user submits a workflow-keyword prompt → handleSubmitAndClear clears buffer.text and enqueues the message (changing messageQueue.length) in the same batch → the next render still has isIdle === true (streamingState hasn't changed yet) → the effect re-runs on the changed deps, hits the if (isIdle) branch, and clears the indicator immediately. The old [streamingState] dep did not re-run on that render, so the indicator persisted through the turn.

Suggested change
setWorkflowKeywordActive(false);
}
}, [isIdle, buffer.text, isProcessing, messageQueue.length]);
setWorkflowKeywordActive(false);
}
}, [isIdle]);

Decouple the two responsibilities: keep flush() on the widened deps (it is idempotent), but clear workflowKeywordActive in a separate effect keyed only on isIdle (or on streamingState transitioning to Idle), matching the prior behavior.

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

Comment thread packages/cli/src/gemini.tsx Outdated
Comment on lines +300 to +303
if (process.env[SKIP_INITIAL_PROMPT_ENV_VAR] === 'true') {
if (process.env['QWEN_CODE_NO_RELAUNCH'] || process.env['SANDBOX']) {
delete process.env[SKIP_INITIAL_PROMPT_ENV_VAR];
}

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 main() function's consumption of SKIP_INITIAL_PROMPT_ENV_VAR (clearing prompt, promptInteractive, query) has no test. Entry-point propagation is tested in cli.test.ts and sandbox passthrough in sandbox.test.ts, but the actual prompt-clearing behaviour in main() is unexercised. — Concrete cost: if a future refactor drops query: undefined (or the entire block), a relaunched session would re-execute the original --query or --prompt argument despite the supervisor signalling it was already consumed. No test would catch this regression.

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

Comment thread scripts/cli-entry.js Outdated
Comment on lines +43 to +46
function withResumeSession(args, sessionId) {
const result = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i];

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] withResumeSession strips six conflicting session flag forms (--continue, -c, --resume, -r, --session-id, --sandbox-session-id, plus =-suffixed variants) before appending --resume <id>. The only test exercising this function passes ['--prompt', 'a&b'] — args with no conflicting flags — so none of the stripping logic is exercised. — Concrete cost: if the stripping of --continue or --sandbox-session-id is broken, a relaunched session would receive both --continue and --resume <id>, causing the CLI to resume the wrong session or error on conflicting flags. No test would detect this.

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

Comment on lines +25 to +28
export function getNpmCliPath(
nodePath = process.execPath,
platform = process.platform,
): string {

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] getNpmCliPath has no direct unit test. The Windows branch (path.win32.join) is only exercised through a mock in updateCheck.test.ts that replaces the function entirely. The Unix branch (fs.realpathSync) is exercised indirectly in handleAutoUpdate.test.ts via vi.importActual, but only as a side effect of the spawn-args assertion. — Concrete cost: if the Windows path construction is wrong (e.g., npm changes its layout from node_modules/npm/bin/npm-cli.js), the mocked test would still pass while the real function produces a nonexistent path, causing handleAutoUpdate to fail to spawn the update process on Windows.

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

Comment thread packages/cli/src/utils/relaunch.ts Outdated
Comment on lines 78 to 80
stdio: 'inherit',
env: newEnv,
});

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] relaunch.test.ts is inert — all 14 remaining tests pass with the IPC-based update-on-exit code reverted, so no test guards the removal of the IPC channel and message handler from relaunchAppInChildProcess. — Concrete cost: the PR removed the 'ipc' stdio entry and the child.on('message', …) handler. The surviving test only asserts onUpdateRelaunch is not called on exit 0, which is trivially true because no IPC message is ever emitted. If a future change accidentally re-introduces the IPC channel, no test will fail.

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

Comment on lines +159 to +165
if (
installInfo.isStandalone &&
installInfo.standaloneDir &&
os.platform() === 'win32'
) {
return updateStandalone();
}

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 Windows standalone interactive update path bypasses the isAutoUpdateEnabled check, downloading and staging an update even when the user has set enableAutoUpdate: false. — Failure scenario: a user on a Windows standalone installation sets enableAutoUpdate: false, runs /update. The old code routed through isAutoUpdateEnabled && canAutoUpdate, falling through to manualInstructions() when disabled. The new code places the os.platform() === 'win32' standalone check before the isAutoUpdateEnabled guard (line 166), so updateStandalone() runs unconditionally.

Suggested change
if (
installInfo.isStandalone &&
installInfo.standaloneDir &&
os.platform() === 'win32'
) {
return updateStandalone();
}
if (
isAutoUpdateEnabled &&
installInfo.isStandalone &&
installInfo.standaloneDir &&
os.platform() === 'win32'
) {
return updateStandalone();
}

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

Comment on lines +101 to +104
const [resolvedCliPath, unresolvedGlobalRoot] = await Promise.all([
canonicalize(cliPath),
runGlobalNpm(['root', '--global'], run),
]);

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] isGlobalNpmInstallation does not guard the Promise.all against ENOENT from runGlobalNpmgetNpmCliPath, even though it already guards the subsequent canonicalize(unresolvedGlobalRoot) call against the same error class. — Failure scenario: on a non-Windows host where the CLI path passes looksLikeNpmPackagePath but no npm symlink exists next to process.execPath (minimal Node.js install, broken symlink), getNpmCliPath throws ENOENT. The error propagates uncaught into checkForUpdatesDetailed's outer catch, returning { status: 'error' } — the user sees "Failed to check for updates" on every startup instead of falling back to update-notifier.

Suggested change
const [resolvedCliPath, unresolvedGlobalRoot] = await Promise.all([
canonicalize(cliPath),
runGlobalNpm(['root', '--global'], run),
]);
let resolvedCliPath: string;
let unresolvedGlobalRoot: string;
try {
[resolvedCliPath, unresolvedGlobalRoot] = await Promise.all([
canonicalize(cliPath),
runGlobalNpm(['root', '--global'], run),
]);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
throw error;
}

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

Comment thread packages/cli/src/utils/processUtils.ts Outdated
Comment on lines +48 to +50
fs.writeFileSync(
statePath,
JSON.stringify({ sessionId, skipInitialPrompt }),

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] Unguarded fs.writeFileSync in relaunchForUpdate aborts the entire update relaunch (skipping runExitCleanup() and process.exit(UPDATE_RELAUNCH_EXIT_CODE)) if the state-file write fails. The IPC-based predecessor wrapped process.send in a try/catch and degraded gracefully. — Failure scenario: the temp directory holding the state file is cleaned by an OS tmp-reaper between the supervisor's mkdtempSync and this write, or the tmp filesystem is full (ENOSPC). writeFileSync throws, process.exit(43) is never reached, the parent never sees exit code 43, and the already-staged update is not installed.

Suggested change
fs.writeFileSync(
statePath,
JSON.stringify({ sessionId, skipInitialPrompt }),
try {
fs.writeFileSync(
statePath,
JSON.stringify({ sessionId, skipInitialPrompt }),
{ encoding: 'utf8', mode: 0o600 },
);
} catch {
// State handoff failed — proceed without session resume.
}

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

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

@wenshao
wenshao added this pull request to the merge queue Jul 19, 2026
Merged via the queue into QwenLM:main with commit fa597ea Jul 19, 2026
151 checks passed
@TradingLaboratory

Copy link
Copy Markdown

Post-merge smoke test on Windows: the fix is partial — update check still throws ✕ Failed to check for updates on every cold start with v0.20.0.

Setup: clean npm install -g @qwen-code/qwen-code@latest on Windows, qwen --version reports v0.20.0. The CLI banner still shows the error every launch even after the merged changes.

What I measured (PowerShell Measure-Command):

  • npm view @qwen-code/qwen-code version returns 0.20.0 in well under 1 s.
  • npm outdated -g @qwen-code/qwen-code consistently takes ~3.0 s (~3029 ms).
  • The CLI still shows the error every launch.

Peeking at the installed bundle, the 2-second budget is still in the code with no user-side override path:

  • chunks/chunk-542SK7KV.js:8119var FETCH_TIMEOUT_MS = 2e3;, fed straight into fetchInfoWithTimeout(..., FETCH_TIMEOUT_MS, ...) at lines 8186 / 8191 / 8217.
  • Grepping the whole @qwen-code/qwen-code tree for env vars or settings.json keys that touch FETCH_TIMEOUT_MS returns nothing — not tunable from the user side.

As a workaround test, switching npm config get registry to https://registry.npmmirror.com (a full mirror of npmjs, ~150 ms from my network) makes the error vanish without disabling the check itself — confirming the timeout really is the bottleneck rather than registry reachability. The remaining gap would be a small bump of the default (e.g. 10 s) and/or exposing it via an env var / config key for users behind slower registry access.

Happy to test a follow-up PR if useful.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Thanks for the detailed repro — the timeout gap is tracked in #7049 (raise budget + soften the error styling). The registry/auth fix here was orthogonal; the follow-up for the 2s→5s bump and warning UX is already assigned.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.20.1.

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.

v.0.19.10升级到v0.19.11出现的bug

4 participants