mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
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:
@@ -28,7 +28,10 @@ import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { settingsApi } from "@/lib/api";
|
||||
import type { ToolInstallation } from "@/lib/api/settings";
|
||||
import type {
|
||||
ToolInstallation,
|
||||
ToolInstallationReport,
|
||||
} from "@/lib/api/settings";
|
||||
import { useUpdate } from "@/contexts/UpdateContext";
|
||||
import { relaunchApp } from "@/lib/updater";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -37,6 +40,8 @@ import appIcon from "@/assets/icons/app-icon.png";
|
||||
import { APP_ICON_MAP } from "@/config/appConfig";
|
||||
import type { AppId } from "@/lib/api/types";
|
||||
import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
import { ToolUpgradeConfirmDialog } from "./ToolUpgradeConfirmDialog";
|
||||
import { ToolInstallRow } from "./ToolInstallRow";
|
||||
|
||||
interface AboutSectionProps {
|
||||
isPortable: boolean;
|
||||
@@ -122,6 +127,12 @@ const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
|
||||
hermes: "Hermes",
|
||||
};
|
||||
|
||||
// 后端返回的 tool 是 string;这里收敛唯一的 ToolName 断言与兜底,供升级确认
|
||||
// 对话框按工具名展示(避免在 JSX 里内联 cast、且每次渲染都新建闭包)。
|
||||
function toolDisplayName(tool: string): string {
|
||||
return TOOL_DISPLAY_NAMES[tool as ToolName] ?? tool;
|
||||
}
|
||||
|
||||
const TOOL_APP_IDS: Record<ToolName, AppId> = {
|
||||
claude: "claude",
|
||||
codex: "codex",
|
||||
@@ -217,6 +228,12 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
Partial<Record<ToolName, ToolInstallation[]>>
|
||||
>({});
|
||||
const [isDiagnosingAll, setIsDiagnosingAll] = useState(false);
|
||||
// 升级前探测到「多处安装需确认」时暂存:toolNames=本次要升级的全部工具,
|
||||
// plans=其中需要确认的(≥2 处)那些。用户确认后对 toolNames 整体执行升级。
|
||||
const [pendingUpgrade, setPendingUpgrade] = useState<{
|
||||
toolNames: ToolName[];
|
||||
plans: ToolInstallationReport[];
|
||||
} | null>(null);
|
||||
|
||||
const toolVersionByName = useMemo(() => {
|
||||
return new Map(toolVersions.map((tool) => [tool.name, tool]));
|
||||
@@ -450,14 +467,22 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
// 升级后自动补诊单个工具:静默后台执行,仅在确有冲突时写入结果,
|
||||
// 无冲突不弹 toast、不报错打扰——与用户主动点的全量诊断区别对待。
|
||||
// 升级后自动补诊单个工具:静默后台执行。有冲突写入结果;无冲突则清掉该工具可能残留
|
||||
// 的过期冲突展示(外部卸载/修复后冲突可能已消失,不清会一直显示旧列表)。不弹 toast、
|
||||
// 不报错打扰——与用户主动点的全量诊断区别对待。
|
||||
const diagnoseToolSilently = useCallback(async (toolName: ToolName) => {
|
||||
try {
|
||||
const installs = await settingsApi.diagnoseToolInstallations(toolName);
|
||||
if (installs.length > 0) {
|
||||
setToolDiagnostics((prev) => ({ ...prev, [toolName]: installs }));
|
||||
}
|
||||
const [report] = await settingsApi.probeToolInstallations([toolName]);
|
||||
setToolDiagnostics((prev) => {
|
||||
if (report?.is_conflict) {
|
||||
return { ...prev, [toolName]: report.installs };
|
||||
}
|
||||
// 无冲突:清掉残留;无旧结果则返回同引用,避免无谓 re-render。
|
||||
if (!(toolName in prev)) return prev;
|
||||
const next = { ...prev };
|
||||
delete next[toolName];
|
||||
return next;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[AboutSection] Auto-diagnose failed for ${toolName}`,
|
||||
@@ -471,17 +496,14 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
const handleDiagnoseAll = useCallback(async () => {
|
||||
setIsDiagnosingAll(true);
|
||||
try {
|
||||
const entries = await Promise.all(
|
||||
TOOL_NAMES.map(
|
||||
async (name) =>
|
||||
[name, await settingsApi.diagnoseToolInstallations(name)] as const,
|
||||
),
|
||||
);
|
||||
const reports = await settingsApi.probeToolInstallations([...TOOL_NAMES]);
|
||||
const next: Partial<Record<ToolName, ToolInstallation[]>> = {};
|
||||
let conflicts = 0;
|
||||
for (const [name, installs] of entries) {
|
||||
next[name] = installs;
|
||||
if (installs.length > 0) conflicts += 1;
|
||||
for (const report of reports) {
|
||||
if (report.is_conflict) {
|
||||
next[report.tool as ToolName] = report.installs;
|
||||
conflicts += 1;
|
||||
}
|
||||
}
|
||||
setToolDiagnostics(next);
|
||||
if (conflicts === 0) {
|
||||
@@ -498,10 +520,9 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const handleRunToolAction = useCallback(
|
||||
// 实际执行安装/升级的串行循环(已通过任何必要的确认后才调用)。
|
||||
const executeRun = useCallback(
|
||||
async (toolNames: ToolName[], action: ToolLifecycleAction) => {
|
||||
if (toolNames.length === 0) return;
|
||||
|
||||
const isBatch = toolNames.length > 1;
|
||||
if (isBatch) {
|
||||
setBatchAction(action);
|
||||
@@ -517,9 +538,6 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
|
||||
for (const toolName of toolNames) {
|
||||
setToolActions((prev) => ({ ...prev, [toolName]: action }));
|
||||
// 记录执行前的版本:用于识别"升级跑完但版本没变"——这正是多处安装互相
|
||||
// 打架的头号症状(升级写入 A 处、PATH 实际仍跑 B 处),需自动触发诊断。
|
||||
const beforeVersion = toolVersionByName.get(toolName)?.version ?? null;
|
||||
try {
|
||||
await settingsApi.runToolLifecycleAction(
|
||||
[toolName],
|
||||
@@ -534,12 +552,9 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
const tool = refreshed.find((t) => t.name === toolName);
|
||||
if (tool?.version) {
|
||||
succeeded += 1;
|
||||
// 升级命令成功、版本号却没动:大概率被另一处安装遮蔽,自动诊断。
|
||||
if (
|
||||
action === "update" &&
|
||||
beforeVersion &&
|
||||
tool.version === beforeVersion
|
||||
) {
|
||||
// 升级成功后无条件补诊:版本没变多半被另一处遮蔽,版本变了另一处也可能仍在,
|
||||
// 两种都要刷新冲突展示(diagnoseToolSilently 无冲突时会自动清旧)。
|
||||
if (action === "update") {
|
||||
void diagnoseToolSilently(toolName);
|
||||
}
|
||||
} else {
|
||||
@@ -625,15 +640,46 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
t,
|
||||
wslShellByTool,
|
||||
refreshToolVersions,
|
||||
toolVersionByName,
|
||||
diagnoseToolSilently,
|
||||
],
|
||||
[t, wslShellByTool, refreshToolVersions, diagnoseToolSilently],
|
||||
);
|
||||
|
||||
// 升级前先探测安装分布:检测到多处安装(命令行只命中其中一处)时弹确认,
|
||||
// 让用户在「升级只动默认那处、其余不动」这件事上知情。install 无锚点,直接执行。
|
||||
const handleRunToolAction = useCallback(
|
||||
async (toolNames: ToolName[], action: ToolLifecycleAction) => {
|
||||
if (toolNames.length === 0) return;
|
||||
if (action === "install") {
|
||||
await executeRun(toolNames, action);
|
||||
return;
|
||||
}
|
||||
let reports: ToolInstallationReport[];
|
||||
try {
|
||||
reports = await settingsApi.probeToolInstallations(toolNames);
|
||||
} catch (error) {
|
||||
// 探测失败不应阻断升级:退回直接执行(等同旧行为)。
|
||||
console.error("[AboutSection] probeToolInstallations failed", error);
|
||||
await executeRun(toolNames, action);
|
||||
return;
|
||||
}
|
||||
const needConfirm = reports.filter((r) => r.needs_confirmation);
|
||||
if (needConfirm.length === 0) {
|
||||
await executeRun(toolNames, action);
|
||||
return;
|
||||
}
|
||||
setPendingUpgrade({ toolNames, plans: needConfirm });
|
||||
},
|
||||
[executeRun],
|
||||
);
|
||||
|
||||
const handleConfirmUpgrade = useCallback(() => {
|
||||
if (pendingUpgrade) {
|
||||
void executeRun(pendingUpgrade.toolNames, "update");
|
||||
}
|
||||
setPendingUpgrade(null);
|
||||
}, [pendingUpgrade, executeRun]);
|
||||
|
||||
const handleCancelUpgrade = useCallback(() => setPendingUpgrade(null), []);
|
||||
|
||||
const displayVersion = version ?? t("common.unknown");
|
||||
|
||||
// 任一安装/升级进行中(批量或单工具)即视为忙碌:用于禁用所有操作按钮,
|
||||
@@ -998,33 +1044,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
);
|
||||
return (
|
||||
<li key={inst.path} className="space-y-1">
|
||||
<div className="flex items-center gap-1.5 text-[10px]">
|
||||
<span className="shrink-0 rounded bg-background/80 px-1 py-0.5 font-mono text-muted-foreground">
|
||||
{inst.source}
|
||||
</span>
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate font-mono text-muted-foreground"
|
||||
title={inst.path}
|
||||
>
|
||||
{inst.path}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
inst.runnable
|
||||
? "shrink-0 font-mono text-foreground"
|
||||
: "shrink-0 text-yellow-600 dark:text-yellow-400"
|
||||
}
|
||||
>
|
||||
{inst.runnable
|
||||
? inst.version
|
||||
: t("settings.toolConflictNotRunnable")}
|
||||
</span>
|
||||
{inst.is_path_default && (
|
||||
<span className="shrink-0 rounded-full border border-primary/30 bg-primary/10 px-1 py-0.5 text-[9px] text-primary">
|
||||
{t("settings.toolConflictDefault")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ToolInstallRow inst={inst} />
|
||||
{/* 卸载建议命令:仅供复制,绝不代执行。前置红色垃圾桶图标
|
||||
明示这是卸载(破坏性)命令,避免误以为是普通信息。 */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -1132,6 +1152,14 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<ToolUpgradeConfirmDialog
|
||||
isOpen={pendingUpgrade !== null}
|
||||
plans={pendingUpgrade?.plans ?? []}
|
||||
displayName={toolDisplayName}
|
||||
onConfirm={handleConfirmUpgrade}
|
||||
onCancel={handleCancelUpgrade}
|
||||
/>
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ToolInstallation } from "@/lib/api/settings";
|
||||
|
||||
/**
|
||||
* 单处工具安装的信息行:来源徽章 + 路径 + 版本(或「无法运行」)+「默认」标记。
|
||||
* 冲突诊断列表与升级确认对话框共用,确保两处的视觉与「默认」判定始终一致。
|
||||
*/
|
||||
export function ToolInstallRow({ inst }: { inst: ToolInstallation }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-[10px]">
|
||||
<span className="shrink-0 rounded bg-background/80 px-1 py-0.5 font-mono text-muted-foreground">
|
||||
{inst.source}
|
||||
</span>
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate font-mono text-muted-foreground"
|
||||
title={inst.path}
|
||||
>
|
||||
{inst.path}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
inst.runnable
|
||||
? "shrink-0 font-mono text-foreground"
|
||||
: "shrink-0 text-yellow-600 dark:text-yellow-400"
|
||||
}
|
||||
>
|
||||
{inst.runnable ? inst.version : t("settings.toolConflictNotRunnable")}
|
||||
</span>
|
||||
{inst.is_path_default && (
|
||||
<span className="shrink-0 rounded-full border border-primary/30 bg-primary/10 px-1 py-0.5 text-[9px] text-primary">
|
||||
{t("settings.toolConflictDefault")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ToolInstallationReport } from "@/lib/api/settings";
|
||||
import { ToolInstallRow } from "./ToolInstallRow";
|
||||
|
||||
interface ToolUpgradeConfirmDialogProps {
|
||||
isOpen: boolean;
|
||||
plans: ToolInstallationReport[];
|
||||
displayName: (tool: string) => string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级前的「多处安装确认」。仅当某工具检测到 ≥2 处安装时弹出:展示命令行实际命中
|
||||
* 哪处(标「默认」= 升级目标)、各处版本,以及锚定后将执行的命令,让用户在
|
||||
* 「升级只动其中一处、其余不动」这件事上知情后再确认。单处安装不会走到这里。
|
||||
*/
|
||||
export function ToolUpgradeConfirmDialog({
|
||||
isOpen,
|
||||
plans,
|
||||
displayName,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ToolUpgradeConfirmDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onCancel();
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-md" zIndex="alert">
|
||||
<DialogHeader className="space-y-2 border-b-0 bg-transparent pb-0">
|
||||
<DialogTitle className="flex items-center gap-2 text-base font-semibold">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
||||
{t("settings.toolUpgradeConfirmTitle")}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm leading-relaxed">
|
||||
{t("settings.toolUpgradeConfirmHint")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="max-h-[50vh] space-y-3 overflow-y-auto">
|
||||
{plans.map((plan) => (
|
||||
<div
|
||||
key={plan.tool}
|
||||
className="space-y-1.5 rounded-lg border border-yellow-500/20 bg-yellow-500/5 p-2.5"
|
||||
>
|
||||
<div className="text-xs font-medium">
|
||||
{displayName(plan.tool)}
|
||||
</div>
|
||||
{!plan.anchored && (
|
||||
<div className="text-[10px] leading-snug text-yellow-600 dark:text-yellow-400">
|
||||
{t("settings.toolUpgradeUnanchoredHint")}
|
||||
</div>
|
||||
)}
|
||||
<ul className="space-y-1">
|
||||
{plan.installs.map((inst) => (
|
||||
<li key={inst.path}>
|
||||
<ToolInstallRow inst={inst} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
{t("settings.toolUpgradeWillRun")}
|
||||
</div>
|
||||
<code
|
||||
className="block truncate rounded bg-background/80 px-1.5 py-0.5 font-mono text-[10px] text-foreground"
|
||||
title={plan.command}
|
||||
>
|
||||
{plan.command}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex gap-2 border-t-0 bg-transparent pt-2 sm:justify-end">
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>
|
||||
{t("settings.toolUpgradeConfirmBtn")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -686,6 +686,11 @@
|
||||
"toolDiagnoseFailed": "診断に失敗しました",
|
||||
"toolUninstallCopyHint": "アンインストールコマンドをコピー(自動実行しません)",
|
||||
"toolUninstallCopied": "アンインストールコマンドをコピーしました",
|
||||
"toolUpgradeConfirmTitle": "アップグレード先の確認",
|
||||
"toolUpgradeConfirmHint": "複数のインストールを検出しました。今回のアップグレードはすべてを更新するわけではありません。実際に作用する箇所は下記の各項目をご確認ください。",
|
||||
"toolUpgradeWillRun": "実行内容:",
|
||||
"toolUpgradeConfirmBtn": "アップグレードを実行",
|
||||
"toolUpgradeUnanchoredHint": "コマンドラインが実際に使用する箇所を特定できません。既定のアップグレードコマンドを実行します(PATH 上の最初の npm にインストールされる場合があります)。",
|
||||
"envBadge": {
|
||||
"wsl": "WSL",
|
||||
"windows": "Win",
|
||||
|
||||
@@ -686,6 +686,11 @@
|
||||
"toolDiagnoseFailed": "诊断失败",
|
||||
"toolUninstallCopyHint": "复制卸载命令(不会自动执行)",
|
||||
"toolUninstallCopied": "卸载命令已复制",
|
||||
"toolUpgradeConfirmTitle": "确认升级位置",
|
||||
"toolUpgradeConfirmHint": "检测到多处安装。本次升级不会全部更新,具体作用的位置以下方各项为准。",
|
||||
"toolUpgradeWillRun": "将执行:",
|
||||
"toolUpgradeConfirmBtn": "确认升级",
|
||||
"toolUpgradeUnanchoredHint": "无法确定命令行实际使用哪处,将执行默认升级命令(可能装到 PATH 上第一个 npm)。",
|
||||
"envBadge": {
|
||||
"wsl": "WSL",
|
||||
"windows": "Win",
|
||||
|
||||
+16
-2
@@ -208,8 +208,12 @@ export const settingsApi = {
|
||||
});
|
||||
},
|
||||
|
||||
async diagnoseToolInstallations(tool: string): Promise<ToolInstallation[]> {
|
||||
return await invoke("diagnose_tool_installations", { tool });
|
||||
/** 探测各工具安装分布:枚举所有安装、标记冲突、生成锚定升级命令。
|
||||
* 诊断按钮、升级前确认、升级后补诊共用此命令,各取所需字段。 */
|
||||
async probeToolInstallations(
|
||||
tools: string[],
|
||||
): Promise<ToolInstallationReport[]> {
|
||||
return await invoke("probe_tool_installations", { tools });
|
||||
},
|
||||
|
||||
async getRectifierConfig(): Promise<RectifierConfig> {
|
||||
@@ -247,6 +251,16 @@ export interface ToolInstallation {
|
||||
is_path_default: boolean;
|
||||
}
|
||||
|
||||
/** 一次"探测工具安装分布"的结果。字段对应后端 ToolInstallationReport。 */
|
||||
export interface ToolInstallationReport {
|
||||
tool: string;
|
||||
installs: ToolInstallation[];
|
||||
is_conflict: boolean;
|
||||
needs_confirmation: boolean;
|
||||
command: string;
|
||||
anchored: boolean;
|
||||
}
|
||||
|
||||
export interface RectifierConfig {
|
||||
enabled: boolean;
|
||||
requestThinkingSignature: boolean;
|
||||
|
||||
Reference in New Issue
Block a user