fix(acp): uninstall before reinstall on npx agent upgrade

Plain `npm install -g <pkg>@<version>` 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.
This commit is contained in:
xintaofei
2026-04-25 10:45:49 +08:00
parent 4f0684dd04
commit 5ae081e87a
5 changed files with 82 additions and 4 deletions

View File

@@ -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<String>,
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 `<pkg>-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<String>,
clean_first: Option<bool>,
task_id: String,
db: State<'_, AppDatabase>,
app: tauri::AppHandle,
) -> Result<String, AcpError> {
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(

View File

@@ -492,6 +492,8 @@ pub async fn acp_detect_agent_local_version(
pub struct AcpPrepareNpxAgentParams {
pub agent_type: AgentType,
pub registry_version: Option<String>,
#[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,

View File

@@ -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)

View File

@@ -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<string> {
return getTransport().call("acp_prepare_npx_agent", {
agentType,
registryVersion: registryVersion ?? null,
cleanFirst,
taskId,
})
}

View File

@@ -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<string> {
return invoke("acp_prepare_npx_agent", {
agentType,
registryVersion: registryVersion ?? null,
cleanFirst,
taskId,
})
}