feat(settings): backend-generated source-aware uninstall command hints

Move uninstall hint command generation from frontend (which used a lossy
`source` string and couldn't tell brew formula from homebrew-npm-global apart)
into the Rust backend, where `brew_formula_from_path` + `infer_install_source`
together produce correct commands for each install. Conflict UI now reads
`inst.uninstall_command` directly.

Backend (src-tauri/src/commands/misc.rs):
- New `uninstall_command_from_paths` (POSIX + Windows) mirrors the
  `anchored_command_from_paths` family. POSIX branches on brew formula /
  volta / bun / pnpm / nvm|fnm|mise|homebrew / system. Windows branches on
  volta / pnpm / npm.
- New `quoted_sibling_or_bare` (POSIX) + `quoted_sibling_or_bare_with_ext`
  (Windows) deduplicate the `sibling_bin().map(quote).unwrap_or_else(bare)`
  pattern across all uninstall branches.
- New `posix_quote_for_user_shell` whitelists `[A-Za-z0-9._/+=:@-]`; any
  other character routes through `shell_single_quote`. Replaces
  `quote_path_if_spaced` on the uninstall side because the fallback chain
  is destructive (`rm -rf <pkg_dir>`), so a `;` / `$` / backtick in the
  home dir would have let the chain inject extra commands.
- New `win_quote_path_for_user_shell` (no `%` 4x escape) splits from
  `win_quote_path_for_batch`. Uninstall hints are copy-paste targets, not
  `.bat + call` payloads, so the two-round percent expansion that
  motivates the 4x escape doesn't apply.
- POSIX `nvm|fnm|mise|homebrew` branch appends physical-delete fallback
  `; [ -e <bin> ] && rm -f <bin> && rm -rf <pkg_dir>` because nvm v18.20.8
  was observed to silently no-op `npm rm -g` (exit 0 with `up to date`
  message, no filesystem change). Anchored `<node_root>/{bin,lib}` layout
  assumption documented as branch-specific.
- `ToolInstallation` gains `uninstall_command: String` and
  `uninstall_command_needs_cmd_hint: bool`. The bool is computed as
  `cfg!(target_os = "windows") && uninstall_command.contains('"')` — a
  compile-time platform gate avoids false positives from POSIX
  `shell_single_quote`'s `'"'"'` escape form, which also contains `"`.
- `enumerate_tool_installations` computes `source` once and threads it
  into `uninstall_command_from_paths`, removing a redundant
  `infer_install_source` call (review N1).

Frontend (src/components/settings/AboutSection.tsx + src/lib/api/settings.ts):
- Remove the JS helpers `buildUninstallCommand`, `dirOfPath`,
  `isWindowsPath`, `shellQuote`, `TOOL_NPM_PACKAGES`. Conflict rows now
  read `inst.uninstall_command` directly.
- Render a PowerShell-call-operator hint when
  `inst.uninstall_command_needs_cmd_hint` is true. Avoids string-match
  inference from the command (which fails on macOS user names containing
  `'`, e.g. `o'leary`, because `shell_single_quote`'s escape contains `"`).
- `ToolInstallation` TS interface gains the two new fields.

i18n (zh/en/ja):
- New key `settings.toolUninstallPwshHint` documents the PowerShell
  `&` (call operator) requirement for quoted paths.

Tests: 30 new cases under `commands::misc::tests::anchored_upgrade` cover
each POSIX branch (brew formula extraction, npm anchored with fallback,
pnpm, volta, bun, hermes), strong-quoting under `;` / `'` paths, the
fallback-only-for-anchored-branches inverse guard, plus mirror Windows
helpers for the user-shell quoter and a `path%foo%` integration case
demonstrating the literal `%` preservation contract. Total
`commands::misc` test count: 68.
This commit is contained in:
Jason
2026-05-24 18:55:35 +08:00
parent 671859748d
commit 3a77861dd9
6 changed files with 819 additions and 58 deletions
+1
View File
@@ -686,6 +686,7 @@
"toolDiagnoseFailed": "Diagnosis failed",
"toolUninstallCopyHint": "Copy uninstall command (won't run it)",
"toolUninstallCopied": "Uninstall command copied",
"toolUninstallPwshHint": "Built for Command Prompt (cmd). PowerShell users: prefix the command with `&` (call operator), otherwise the quoted path is treated as a string literal.",
"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:",
+1
View File
@@ -686,6 +686,7 @@
"toolDiagnoseFailed": "診断に失敗しました",
"toolUninstallCopyHint": "アンインストールコマンドをコピー(自動実行しません)",
"toolUninstallCopied": "アンインストールコマンドをコピーしました",
"toolUninstallPwshHint": "コマンドプロンプト (cmd) 用です。PowerShell ではコマンド先頭に `&`(呼び出し演算子)を付ける必要があります。付けないと引用符付きパスが文字列リテラルとして扱われます。",
"toolUpgradeConfirmTitle": "アップグレード先の確認",
"toolUpgradeConfirmHint": "複数のインストールを検出しました。今回のアップグレードはすべてを更新するわけではありません。実際に作用する箇所は下記の各項目をご確認ください。",
"toolUpgradeWillRun": "実行内容:",
+1
View File
@@ -686,6 +686,7 @@
"toolDiagnoseFailed": "诊断失败",
"toolUninstallCopyHint": "复制卸载命令(不会自动执行)",
"toolUninstallCopied": "卸载命令已复制",
"toolUninstallPwshHint": "适用于命令提示符 (cmd)。PowerShell 用户需在命令前加 `&`(call 运算符),否则带引号的路径会被当作字符串字面值。",
"toolUpgradeConfirmTitle": "确认升级位置",
"toolUpgradeConfirmHint": "检测到多处安装。本次升级不会全部更新,具体作用的位置以下方各项为准。",
"toolUpgradeWillRun": "将执行:",