From 5ae081e87a9ee0dd878ddadaf50d034c99e9247c Mon Sep 17 00:00:00 2001 From: xintaofei Date: Sat, 25 Apr 2026 10:45:49 +0800 Subject: [PATCH] fix(acp): uninstall before reinstall on npx agent upgrade Plain `npm install -g @` over an existing install does not reliably re-resolve platform-specific optionalDependencies, leaving the native CLI binary missing or stale (e.g. 'Native CLI binary for darwin-x64 not found' after the claude-agent-sdk upgrade). The Upgrade button now runs npm uninstall -g first as a best-effort step, forcing npm to rebuild the dependency graph from scratch on the subsequent install. If the clean upgrade fails midway, the DB and the Settings UI resync to the actual on-disk state instead of showing a phantom version. --- src-tauri/src/commands/acp.rs | 53 ++++++++++++++++++- src-tauri/src/web/handlers/acp.rs | 3 ++ .../settings/acp-agent-settings.tsx | 22 +++++++- src/lib/api.ts | 4 +- src/lib/tauri.ts | 4 +- 5 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/commands/acp.rs b/src-tauri/src/commands/acp.rs index a2f64f9..d30bb83 100644 --- a/src-tauri/src/commands/acp.rs +++ b/src-tauri/src/commands/acp.rs @@ -2735,6 +2735,7 @@ pub async fn acp_detect_agent_local_version( pub(crate) async fn acp_prepare_npx_agent_core( agent_type: AgentType, registry_version: Option, + clean_first: bool, task_id: String, db: &AppDatabase, emitter: &EventEmitter, @@ -2759,6 +2760,30 @@ pub(crate) async fn acp_prepare_npx_agent_core( .flatten() .and_then(|m| m.installed_version); + // Best-effort uninstall before reinstall. Forces npm to re-resolve + // the dependency graph from scratch, which is required for + // platform-specific optionalDependencies (e.g. native CLI binaries + // shipped as `-darwin-x64`) to be picked up after an upgrade. + // Failures here are logged and swallowed so we still attempt the + // install — for example when nothing is currently installed. + if clean_first { + let package_name = package_name_from_spec(package); + emit_agent_install_event( + emitter, + &task_id, + AgentInstallEventKind::Log, + format!("$ npm uninstall -g {package_name} (clean reinstall)"), + ); + if let Err(e) = uninstall_npm_global_package(package).await { + emit_agent_install_event( + emitter, + &task_id, + AgentInstallEventKind::Log, + format!("(warning) uninstall step failed, continuing: {e}"), + ); + } + } + emit_agent_install_event( emitter, &task_id, @@ -2813,6 +2838,23 @@ pub(crate) async fn acp_prepare_npx_agent_core( ); } Err(e) => { + // When clean_first was true the uninstall step may already have + // succeeded by the time install failed, leaving the DB pointing at + // a version that no longer exists on disk. Resync the DB to the + // actual filesystem state so the UI doesn't mislead the user into + // thinking they can connect. + if clean_first { + let detected = detect_local_version(agent_type).await; + if let Err(sync_err) = + agent_setting_service::set_installed_version(&db.conn, agent_type, detected) + .await + { + eprintln!( + "[acp] failed to resync installed_version after clean upgrade failure: {sync_err}" + ); + } + emit_acp_agents_updated(emitter, "npx_prepare_failed", Some(agent_type)); + } emit_agent_install_event( emitter, &task_id, @@ -2829,12 +2871,21 @@ pub(crate) async fn acp_prepare_npx_agent_core( pub async fn acp_prepare_npx_agent( agent_type: AgentType, registry_version: Option, + clean_first: Option, task_id: String, db: State<'_, AppDatabase>, app: tauri::AppHandle, ) -> Result { let emitter = EventEmitter::Tauri(app); - acp_prepare_npx_agent_core(agent_type, registry_version, task_id, &db, &emitter).await + acp_prepare_npx_agent_core( + agent_type, + registry_version, + clean_first.unwrap_or(false), + task_id, + &db, + &emitter, + ) + .await } pub(crate) async fn acp_uninstall_agent_core( diff --git a/src-tauri/src/web/handlers/acp.rs b/src-tauri/src/web/handlers/acp.rs index 40bab97..80fa352 100644 --- a/src-tauri/src/web/handlers/acp.rs +++ b/src-tauri/src/web/handlers/acp.rs @@ -492,6 +492,8 @@ pub async fn acp_detect_agent_local_version( pub struct AcpPrepareNpxAgentParams { pub agent_type: AgentType, pub registry_version: Option, + #[serde(default)] + pub clean_first: bool, pub task_id: String, } @@ -504,6 +506,7 @@ pub async fn acp_prepare_npx_agent( let result = acp_commands::acp_prepare_npx_agent_core( params.agent_type, params.registry_version, + params.clean_first, params.task_id, db, &emitter, diff --git a/src/components/settings/acp-agent-settings.tsx b/src/components/settings/acp-agent-settings.tsx index 014e0e1..2bc9c59 100644 --- a/src/components/settings/acp-agent-settings.tsx +++ b/src/components/settings/acp-agent-settings.tsx @@ -3153,7 +3153,8 @@ export function AcpAgentSettings() { const installedVersion = await acpPrepareNpxAgent( agent.agent_type, agent.registry_version, - taskId + taskId, + mode === "upgrade" ) setAgents((prev) => prev.map((item) => @@ -3200,6 +3201,25 @@ export function AcpAgentSettings() { description: message, } ) + if (mode === "upgrade") { + // Clean upgrade may have removed the old install before failing — + // resync local state so the UI doesn't keep showing a phantom version. + try { + const detected = await acpDetectAgentLocalVersion(agent.agent_type) + setAgents((prev) => + prev.map((item) => + item.agent_type === agent.agent_type + ? { ...item, installed_version: detected ?? null } + : item + ) + ) + } catch (detectErr) { + console.error( + "[Settings] failed to resync installed version after upgrade failure:", + detectErr + ) + } + } throw err } finally { busyActionRef.current.delete(agent.agent_type) diff --git a/src/lib/api.ts b/src/lib/api.ts index 9298b26..ec5de4c 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -202,11 +202,13 @@ export async function acpDetectAgentLocalVersion( export async function acpPrepareNpxAgent( agentType: AgentType, registryVersion: string | null | undefined, - taskId: string + taskId: string, + cleanFirst: boolean = false ): Promise { return getTransport().call("acp_prepare_npx_agent", { agentType, registryVersion: registryVersion ?? null, + cleanFirst, taskId, }) } diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index c53be36..16c0438 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -189,11 +189,13 @@ export async function acpDetectAgentLocalVersion( export async function acpPrepareNpxAgent( agentType: AgentType, registryVersion: string | null | undefined, - taskId: string + taskId: string, + cleanFirst: boolean = false ): Promise { return invoke("acp_prepare_npx_agent", { agentType, registryVersion: registryVersion ?? null, + cleanFirst, taskId, }) }