feat(settings): anchor tool upgrades to the actual install

Rework the About tool-upgrade flow so upgrades target the install the
command line is actually using, instead of running bare `npm i -g` and
landing on PATH's first npm — which used to move upgrades to the wrong
location or fail outright when the resolved bin had no sibling npm.

Anchored upgrade command (update path, non-Windows):
- claude native installer (~/.local/share/claude/versions/) -> `claude update`
- Homebrew formula (canonical path under /Cellar/) -> `brew upgrade <formula>`
- volta / bun -> `volta install` / `bun add -g`
- Node managers with sibling npm (nvm/fnm/mise/homebrew-npm) -> that
  directory's npm with `i -g <pkg>@latest`
- system / unknown sources (e.g. opencode install.sh under ~/.opencode/bin,
  ~/go/bin) -> fall back to the bare command, surfaced as anchored=false
  so the UI shows an honest "default entry not determinable" hint instead
  of pretending the upgrade is targeted.

Multi-install confirmation:
- When the probe finds >=2 installs, pop ToolUpgradeConfirmDialog showing
  each install (source badge + path + version + Default badge) and the
  resolved command before executing. Top-level text only asserts what is
  universally true ("won't update all of them; see each tool below");
  per-card text adapts to anchored vs unanchored.
- Pending upgrade state stores the full upgrade set; the dialog only
  shows the subset that needs confirmation, so non-conflicting tools in
  a batch still get upgraded after confirm.

Unified probe command:
- Merge the previous diagnose_tool_installations and plan_tool_upgrades
  into a single probe_tool_installations returning installs + is_conflict
  + needs_confirmation + command + anchored. Diagnose button, post-upgrade
  auto-diagnose, and pre-upgrade confirmation all share one IPC. The
  diagnose button's previous Promise.all of 6 IPCs is now a single call.

resolve_path_default robustness:
- The previous lines().next() was poisoned by interactive .zshrc output
  (welcome banners, p10k instant prompts, etc.), causing default-install
  detection to fail and dropping multi-install scenarios into the
  unanchored fallback even when one install was clearly the native one.
- Replaced with first_abs_path_line that scans for the first
  absolute-path line and ignores noise, mirroring how try_get_version
  already tolerates banner noise via a regex.

Diagnostic staleness fix:
- diagnoseToolSilently now clears stale diagnostics in the else branch
  when the conflict no longer holds (previously only wrote on conflict,
  never cleared, so externally resolving a conflict left the card stuck
  on a stale list until the user clicked Diagnose).
- Auto-diagnose now triggers after any successful update, not only when
  the version didn't change, so the card reflects the latest conflict
  state even when an anchored upgrade succeeds while another install
  remains.

Other:
- Shared ToolInstallRow between the conflict list and the confirmation
  dialog removes the duplicated per-install row JSX.
- Drop the new shell_quote_path helper in favor of reusing the file's
  existing POSIX-correct shell_single_quote, gated on whitespace.
- Collapse displayName lookup into a stable module-level helper to remove
  the `as ToolName` cast and avoid recreating the closure each render.
- 17 backend unit tests covering anchored command generation across each
  source, brew formula extraction, is_conflicting thresholds, default
  install resolution, and the welcome-banner scenario.
- i18n (zh/en/ja): new keys for confirmation dialog
  title/hint/will-run/confirm-button and the unanchored hint.
This commit is contained in:
Jason
2026-05-23 09:04:14 +08:00
parent ce232a1452
commit 014c82d28b
9 changed files with 741 additions and 98 deletions
+5
View File
@@ -686,6 +686,11 @@
"toolDiagnoseFailed": "Diagnosis failed",
"toolUninstallCopyHint": "Copy uninstall command (won't run it)",
"toolUninstallCopied": "Uninstall command copied",
"toolUpgradeConfirmTitle": "Confirm upgrade target",
"toolUpgradeConfirmHint": "Multiple installations detected. This upgrade won't update all of them; refer to each tool below for what it actually targets.",
"toolUpgradeWillRun": "Will run:",
"toolUpgradeConfirmBtn": "Confirm upgrade",
"toolUpgradeUnanchoredHint": "Can't determine which install your command line uses; will run the default upgrade command (may install to the first npm on PATH).",
"envBadge": {
"wsl": "WSL",
"windows": "Win",
+5
View File
@@ -686,6 +686,11 @@
"toolDiagnoseFailed": "診断に失敗しました",
"toolUninstallCopyHint": "アンインストールコマンドをコピー(自動実行しません)",
"toolUninstallCopied": "アンインストールコマンドをコピーしました",
"toolUpgradeConfirmTitle": "アップグレード先の確認",
"toolUpgradeConfirmHint": "複数のインストールを検出しました。今回のアップグレードはすべてを更新するわけではありません。実際に作用する箇所は下記の各項目をご確認ください。",
"toolUpgradeWillRun": "実行内容:",
"toolUpgradeConfirmBtn": "アップグレードを実行",
"toolUpgradeUnanchoredHint": "コマンドラインが実際に使用する箇所を特定できません。既定のアップグレードコマンドを実行します(PATH 上の最初の npm にインストールされる場合があります)。",
"envBadge": {
"wsl": "WSL",
"windows": "Win",
+5
View File
@@ -686,6 +686,11 @@
"toolDiagnoseFailed": "诊断失败",
"toolUninstallCopyHint": "复制卸载命令(不会自动执行)",
"toolUninstallCopied": "卸载命令已复制",
"toolUpgradeConfirmTitle": "确认升级位置",
"toolUpgradeConfirmHint": "检测到多处安装。本次升级不会全部更新,具体作用的位置以下方各项为准。",
"toolUpgradeWillRun": "将执行:",
"toolUpgradeConfirmBtn": "确认升级",
"toolUpgradeUnanchoredHint": "无法确定命令行实际使用哪处,将执行默认升级命令(可能装到 PATH 上第一个 npm)。",
"envBadge": {
"wsl": "WSL",
"windows": "Win",