mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 01:25:33 +08:00
feat(settings): self-update first chain, switch hermes to native installer
Promote anchored upgrades to "<bin> update || <package fallback>" for the
tools whose CLI knows how to self-update safely, and replace pip-based
hermes install/update with the official NousResearch installer scripts.
Frontend learns to detect "update reported success but version did not
change" so silent upstream no-ops surface as a soft warning.
Backend (src-tauri/src/commands/misc.rs):
- New prefers_official_update(tool, shell) per-platform allow-list:
POSIX {claude, codex, opencode, openclaw}. Windows drops opencode
pending anomalyco/opencode#17295 (its silent `upgrade` may prompt for
install-source detection and deadlock our lifecycle). gemini is absent
from both because `gemini update` is not implemented upstream
(google-gemini/gemini-cli#18618 / #16122 OPEN).
- New chain_update_commands joins primary/fallback per shell: POSIX
`||`, Windows `|| call` — batch transfers control to the `||` RHS
without `call`, skipping subsequent tools in multi-tool actions.
- New LifecycleCommandShell {Posix, WindowsBatch} threads the target
shell through tool_action_shell_command_for_shell, replacing the old
hermes-only WSL substitution hack with a uniform mechanism.
- anchored_command_from_paths refactored: hermes anchors to
`<bin> update`; tools in prefers_official_update chain self-update
with the package-manager command as fallback; brew formulae remain
brew-managed (never self-update what Homebrew owns); others use the
prior npm/volta/bun/pnpm anchoring unchanged. Mirror on Windows.
- Hermes commands switch from pip to the official scripts. POSIX/WSL
runs `bash -c 'tmp=\$(mktemp) && curl -fsSL .../install.sh -o \$tmp
&& bash \$tmp; status=\$?; rm -f \$tmp; exit \$status'`. The mktemp +
two-step download avoids `curl | bash` because WSL sub-shells inherit
neither outer `set -o pipefail` nor a guaranteed bash (default may be
dash/ash). Windows uses `powershell -NoProfile -ExecutionPolicy Bypass
-EncodedCommand <b64>` with a hand-rolled UTF-16 LE base64 encoder
for `irm .../install.ps1 | iex`, hiding the PowerShell pipe from
cmd.exe. Pip is abandoned because macOS system python3 is often 3.9
while hermes-agent requires >=3.11, and the pyenv `python` shim may
not exist — pip errors then misclassify as "command not found".
Frontend (src/components/settings/AboutSection.tsx):
- New versionUnchangedAfterUpdate four-AND short-circuit detects when
the upgrade command succeeded, the tool still reports a version, but
that version equals the pre-upgrade version while a different
latest_version is known. Guards against upstream no-op updaters that
return exit 0 without applying anything (openai/codex#21897).
- Soft-failure shape gains kind?: "notRunnable" | "versionUnchanged"
so the warning toast title can disambiguate the two cases.
- Hermes install command snippet swapped from `python3 -m pip ...` to
the official curl-bash one-liner shown to users.
i18n (zh / en / ja):
- New settings.toolActionVersionUnchangedTitle and
settings.toolActionVersionUnchanged, synchronized across all locales.
Tests (src-tauri/src/commands/misc.rs):
- Anchored upgrade assertions updated to `<bin> update || <pkg>` shape
across nvm / volta / bun / homebrew-npm-global / spaces / fnm
branches; brew formula explicitly does NOT chain self-update.
- New hermes anchor tests replace hermes_has_no_npm_anchor on both
POSIX and Windows. WSL hermes tests rewritten for the mktemp+bash
flow with `hermes update` fallback.
- Reverse-lock tests: gemini static fallback must NOT contain
`gemini update`; opencode Windows static fallback must NOT contain
`opencode upgrade`; hermes commands must NOT contain
`python`/`pip`/`powershell`/forbidden pipes.
Validated:
- cargo check --tests: clean
- pnpm typecheck: 0 errors
- cargo fmt: clean
This commit is contained in:
@@ -115,7 +115,7 @@ npm i -g opencode-ai@latest
|
||||
# OpenClaw
|
||||
npm i -g openclaw@latest
|
||||
# Hermes
|
||||
python3 -m pip install --upgrade "hermes-agent[web]"`;
|
||||
curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash`;
|
||||
|
||||
const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
|
||||
claude: "Claude Code",
|
||||
@@ -474,15 +474,23 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
|
||||
// 逐工具串行执行:每个工具独立成败、独立刷新版本,一个失败不会连坐
|
||||
// 后续工具(后端把整批拼成单脚本 + set -e,会在首个失败处中止整批)。
|
||||
// soft=true 表示"命令成功执行但版本探不到"(装上却跑不起来),
|
||||
// soft=true 表示"命令成功执行但结果仍需用户介入"(版本没变/装上却跑不起来),
|
||||
// 与命令本身报错(soft=false)区别对待:前者不算硬失败,toast 降级为 warning。
|
||||
const failures: { toolName: ToolName; detail: string; soft: boolean }[] =
|
||||
[];
|
||||
const failures: {
|
||||
toolName: ToolName;
|
||||
detail: string;
|
||||
soft: boolean;
|
||||
kind?: "notRunnable" | "versionUnchanged";
|
||||
}[] = [];
|
||||
let succeeded = 0;
|
||||
|
||||
for (const toolName of toolNames) {
|
||||
setToolActions((prev) => ({ ...prev, [toolName]: action }));
|
||||
try {
|
||||
const previousTool = toolVersionByName.get(toolName);
|
||||
const previousVersion = previousTool?.version ?? null;
|
||||
const previousLatestVersion = previousTool?.latest_version ?? null;
|
||||
|
||||
await settingsApi.runToolLifecycleAction(
|
||||
[toolName],
|
||||
action,
|
||||
@@ -495,18 +503,46 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
);
|
||||
const tool = refreshed.find((t) => t.name === toolName);
|
||||
if (tool?.version) {
|
||||
succeeded += 1;
|
||||
// 升级成功后无条件补诊:版本没变多半被另一处遮蔽,版本变了另一处也可能仍在,
|
||||
// 两种都要刷新冲突展示(diagnoseToolSilently 无冲突时会自动清旧)。
|
||||
if (action === "update") {
|
||||
const latestVersion = tool.latest_version ?? previousLatestVersion;
|
||||
const versionUnchangedAfterUpdate =
|
||||
action === "update" &&
|
||||
Boolean(previousVersion) &&
|
||||
tool.version === previousVersion &&
|
||||
Boolean(latestVersion) &&
|
||||
tool.version !== latestVersion;
|
||||
|
||||
if (versionUnchangedAfterUpdate) {
|
||||
// 有些上游 updater 会在未实际改动版本时仍返回 0。这里用刷新后的
|
||||
// 当前版本 + latest_version 再确认一次,避免给用户误报升级成功。
|
||||
failures.push({
|
||||
toolName,
|
||||
detail: t("settings.toolActionVersionUnchanged", {
|
||||
version: tool.version,
|
||||
latest: latestVersion ?? t("common.unknown"),
|
||||
}),
|
||||
soft: true,
|
||||
kind: "versionUnchanged",
|
||||
});
|
||||
void diagnoseToolSilently(toolName);
|
||||
} else {
|
||||
succeeded += 1;
|
||||
// 升级成功后无条件补诊:版本没变多半被另一处遮蔽,版本变了另一处也可能仍在,
|
||||
// 两种都要刷新冲突展示(diagnoseToolSilently 无冲突时会自动清旧)。
|
||||
if (action === "update") {
|
||||
void diagnoseToolSilently(toolName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 命令退出码为 0、但刷新后仍探不到版本:多半是"装上了却跑不起来"
|
||||
// (如 openclaw 要求更高的 Node 版本)。refreshToolVersions 的 merge 已把
|
||||
// version 置空并写入后端 error,这里只需归类为软失败并展示原因。
|
||||
const detail = tool?.error?.trim() || t("settings.toolNotRunnable");
|
||||
failures.push({ toolName, detail, soft: true });
|
||||
failures.push({
|
||||
toolName,
|
||||
detail,
|
||||
soft: true,
|
||||
kind: "notRunnable",
|
||||
});
|
||||
// 装了却跑不起来同样可能源于多处安装,自动诊断帮用户定位。
|
||||
void diagnoseToolSilently(toolName);
|
||||
}
|
||||
@@ -560,13 +596,22 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
: failures[0]?.detail;
|
||||
|
||||
const hardFailures = failures.filter((f) => !f.soft);
|
||||
const allSoftVersionUnchanged =
|
||||
failures.length > 0 &&
|
||||
failures.every((f) => f.soft && f.kind === "versionUnchanged");
|
||||
|
||||
if (succeeded === 0 && hardFailures.length === 0) {
|
||||
// 命令均成功执行、但工具都探不到版本(装上却跑不起来)→ 降级为 warning 并解释原因。
|
||||
toast.warning(t("settings.toolActionInstalledNotRunnable"), {
|
||||
description: failureDescription || undefined,
|
||||
closeButton: true,
|
||||
});
|
||||
// 命令均成功执行、但结果需要用户介入(版本没变 / 装上却跑不起来)
|
||||
// → 降级为 warning 并解释原因。
|
||||
toast.warning(
|
||||
allSoftVersionUnchanged
|
||||
? t("settings.toolActionVersionUnchangedTitle")
|
||||
: t("settings.toolActionInstalledNotRunnable"),
|
||||
{
|
||||
description: failureDescription || undefined,
|
||||
closeButton: true,
|
||||
},
|
||||
);
|
||||
} else if (succeeded === 0) {
|
||||
toast.error(t("settings.toolActionFailed"), {
|
||||
description: failureDescription || undefined,
|
||||
@@ -584,7 +629,13 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
);
|
||||
}
|
||||
},
|
||||
[t, wslShellByTool, refreshToolVersions, diagnoseToolSilently],
|
||||
[
|
||||
t,
|
||||
wslShellByTool,
|
||||
toolVersionByName,
|
||||
refreshToolVersions,
|
||||
diagnoseToolSilently,
|
||||
],
|
||||
);
|
||||
|
||||
// 升级/安装的统一入口锁。所有动作在入口处先登记 preflight、出口处解锁;
|
||||
|
||||
Reference in New Issue
Block a user