Skip to content

Commit ab74cb3

Browse files
Merge pull request #246 from ScriptedAlchemy/feat/upgrade-refreshes-plugins
feat(upgrade): refresh all configured agent integrations during upgrade
2 parents 85de4da + d789f84 commit ab74cb3

9 files changed

Lines changed: 553 additions & 91 deletions

File tree

src/agent_cmd.rs

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -420,24 +420,20 @@ pub(crate) async fn handle_reinstall_command() -> tracedecay::errors::Result<()>
420420
eprintln!("No installed agents found. Run `tracedecay install` first.");
421421
} else {
422422
let agents = user_cfg.installed_agents.clone();
423-
let project_path = std::env::current_dir().ok();
424423
eprintln!(
425424
"Reinstalling {} agent(s): {}",
426425
agents.len(),
427426
agents.join(", ")
428427
);
429-
for id in &agents {
430-
let ag = tracedecay::agents::get_integration(id)?;
431-
let ctx = tracedecay::agents::InstallContext {
432-
home: home.clone(),
433-
tracedecay_bin: tracedecay_bin.clone(),
434-
tool_permissions: tracedecay::agents::expected_tool_perms(),
435-
profile: None,
436-
project_root: None,
437-
dashboard: true,
438-
};
439-
ag.install(&ctx)?;
440-
ag.post_install(project_path.as_deref()).await;
428+
let results = reinstall_agent_integrations(&agents, &home, &tracedecay_bin).await;
429+
let failed: Vec<String> = results
430+
.iter()
431+
.filter_map(|(id, result)| result.as_ref().err().map(|_| id.clone()))
432+
.collect();
433+
if !failed.is_empty() {
434+
return Err(tracedecay::errors::TraceDecayError::Config {
435+
message: format!("failed to reinstall agent(s): {}", failed.join(", ")),
436+
});
441437
}
442438
eprintln!("\x1b[32m✔\x1b[0m All agents reinstalled");
443439
user_cfg.last_installed_version = env!("CARGO_PKG_VERSION").to_string();
@@ -446,6 +442,56 @@ pub(crate) async fn handle_reinstall_command() -> tracedecay::errors::Result<()>
446442
Ok(())
447443
}
448444

445+
/// Re-runs `install()` + `post_install()` for each tracked agent id, returning
446+
/// only the ids that resolve to a real integration paired with their install
447+
/// result.
448+
///
449+
/// An id that does NOT resolve to an integration (a later release renamed or
450+
/// removed it, or a typo landed in `installed_agents`) is SKIPPED, not failed:
451+
/// it is logged as a warning and left out of the returned results entirely.
452+
/// Gating version-marker advancement on such an id would wedge the reinstall
453+
/// loop forever — `migrate_installed_agents` only ever adds ids, never prunes,
454+
/// so a stale id would never resolve and the markers would never advance. Only
455+
/// genuine `install()` failures are reported as `Err` so they still gate
456+
/// markers.
457+
pub(crate) async fn reinstall_agent_integrations(
458+
agent_ids: &[String],
459+
home: &Path,
460+
tracedecay_bin: &str,
461+
) -> Vec<(String, tracedecay::errors::Result<()>)> {
462+
let project_path = std::env::current_dir().ok();
463+
let mut results = Vec::new();
464+
for id in agent_ids {
465+
let ag = match tracedecay::agents::get_integration(id) {
466+
Ok(ag) => ag,
467+
Err(_) => {
468+
eprintln!(
469+
" \x1b[33mwarning:\x1b[0m skipping unknown tracked agent id \"{id}\" \
470+
(no such integration); it will not gate the version-marker refresh."
471+
);
472+
continue;
473+
}
474+
};
475+
let ctx = tracedecay::agents::InstallContext {
476+
home: home.to_path_buf(),
477+
tracedecay_bin: tracedecay_bin.to_string(),
478+
tool_permissions: tracedecay::agents::expected_tool_perms(),
479+
profile: None,
480+
project_root: None,
481+
dashboard: true,
482+
};
483+
let result = match ag.install(&ctx) {
484+
Ok(()) => {
485+
ag.post_install(project_path.as_deref()).await;
486+
Ok(())
487+
}
488+
Err(e) => Err(e),
489+
};
490+
results.push((id.clone(), result));
491+
}
492+
results
493+
}
494+
449495
pub(crate) async fn handle_uninstall_command(
450496
agent: Option<String>,
451497
profile: Option<String>,

src/cli.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,9 @@ pub enum Commands {
312312
/// Skip the post-update health pass (safe repairs + doctor summary)
313313
#[arg(long)]
314314
no_heal: bool,
315+
/// Skip refreshing already-configured agent integrations
316+
#[arg(long)]
317+
no_reinstall: bool,
315318
},
316319
/// Refresh generated plugins and the daemon, even when already up to date
317320
///
@@ -323,13 +326,19 @@ pub enum Commands {
323326
/// Skip the post-update health pass (safe repairs + doctor summary)
324327
#[arg(long)]
325328
no_heal: bool,
329+
/// Skip refreshing already-configured agent integrations
330+
#[arg(long)]
331+
no_reinstall: bool,
326332
},
327333
/// Refresh plugins and daemon after the binary has been updated.
328334
#[command(name = "post-update", hide = true)]
329335
PostUpdate {
330336
/// Skip the post-update health pass (safe repairs + doctor summary)
331337
#[arg(long)]
332338
no_heal: bool,
339+
/// Skip refreshing already-configured agent integrations
340+
#[arg(long)]
341+
no_reinstall: bool,
333342
},
334343
/// Show or switch the update channel (stable or beta)
335344
#[command(long_about = CHANNEL_LONG_ABOUT, after_help = CHANNEL_AFTER_HELP)]

src/cli/help.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ pub(crate) const UPGRADE_AFTER_HELP: &str = "\
219219
Examples:
220220
tracedecay upgrade Install the newest release
221221
tracedecay upgrade --no-heal Skip the post-update health pass
222+
tracedecay upgrade --no-reinstall Skip refreshing configured agents
223+
224+
After a real install, upgrade re-runs install for every configured agent
225+
integration so a separate `tracedecay reinstall` is not needed. --no-reinstall
226+
skips that refresh; --no-heal (independent) skips only the health pass.
222227
223228
Related: tracedecay update (refresh even when current), tracedecay channel
224229
(switch stable/beta).";
@@ -227,6 +232,11 @@ pub(crate) const UPDATE_AFTER_HELP: &str = "\
227232
Examples:
228233
tracedecay update Upgrade if needed, then refresh
229234
tracedecay update --no-heal Skip the post-update health pass
235+
tracedecay update --no-reinstall Skip refreshing configured agents
236+
237+
Update re-runs install for every configured agent integration so a separate
238+
`tracedecay reinstall` is not needed. --no-reinstall skips that refresh;
239+
--no-heal (independent) skips only the health pass.
230240
231241
Related: tracedecay upgrade (refresh only after a real install),
232242
tracedecay update-plugin (plugins only), tracedecay channel.";

src/cli/parse_tests.rs

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,17 @@ fn update_upgrade_and_update_plugin_parse_to_distinct_commands() {
220220

221221
assert!(matches!(
222222
update.command,
223-
Some(Commands::Update { no_heal: false })
223+
Some(Commands::Update {
224+
no_heal: false,
225+
no_reinstall: false
226+
})
224227
));
225228
assert!(matches!(
226229
upgrade.command,
227-
Some(Commands::Upgrade { no_heal: false })
230+
Some(Commands::Upgrade {
231+
no_heal: false,
232+
no_reinstall: false
233+
})
228234
));
229235
assert!(matches!(
230236
update_plugin.command,
@@ -243,15 +249,24 @@ fn update_and_post_update_parse_no_heal_flag() {
243249

244250
assert!(matches!(
245251
update.command,
246-
Some(Commands::Update { no_heal: true })
252+
Some(Commands::Update {
253+
no_heal: true,
254+
no_reinstall: false
255+
})
247256
));
248257
assert!(matches!(
249258
post_update.command,
250-
Some(Commands::PostUpdate { no_heal: true })
259+
Some(Commands::PostUpdate {
260+
no_heal: true,
261+
no_reinstall: false
262+
})
251263
));
252264
assert!(matches!(
253265
post_update_default.command,
254-
Some(Commands::PostUpdate { no_heal: false })
266+
Some(Commands::PostUpdate {
267+
no_heal: false,
268+
no_reinstall: false
269+
})
255270
));
256271
}
257272

@@ -264,11 +279,60 @@ fn upgrade_parses_no_heal_flag() {
264279

265280
assert!(matches!(
266281
upgrade.command,
267-
Some(Commands::Upgrade { no_heal: true })
282+
Some(Commands::Upgrade {
283+
no_heal: true,
284+
no_reinstall: false
285+
})
268286
));
269287
assert!(matches!(
270288
upgrade_default.command,
271-
Some(Commands::Upgrade { no_heal: false })
289+
Some(Commands::Upgrade {
290+
no_heal: false,
291+
no_reinstall: false
292+
})
293+
));
294+
}
295+
296+
#[test]
297+
fn upgrade_update_and_post_update_parse_no_reinstall_flag() {
298+
let upgrade = Cli::try_parse_from(["tracedecay", "upgrade", "--no-reinstall"])
299+
.expect("upgrade --no-reinstall should parse");
300+
let update = Cli::try_parse_from(["tracedecay", "update", "--no-reinstall"])
301+
.expect("update --no-reinstall should parse");
302+
let post_update = Cli::try_parse_from(["tracedecay", "post-update", "--no-reinstall"])
303+
.expect("post-update --no-reinstall should parse");
304+
305+
assert!(matches!(
306+
upgrade.command,
307+
Some(Commands::Upgrade {
308+
no_heal: false,
309+
no_reinstall: true
310+
})
311+
));
312+
assert!(matches!(
313+
update.command,
314+
Some(Commands::Update {
315+
no_heal: false,
316+
no_reinstall: true
317+
})
318+
));
319+
assert!(matches!(
320+
post_update.command,
321+
Some(Commands::PostUpdate {
322+
no_heal: false,
323+
no_reinstall: true
324+
})
325+
));
326+
327+
// --no-heal and --no-reinstall are independent and may combine.
328+
let both = Cli::try_parse_from(["tracedecay", "upgrade", "--no-heal", "--no-reinstall"])
329+
.expect("upgrade --no-heal --no-reinstall should parse");
330+
assert!(matches!(
331+
both.command,
332+
Some(Commands::Upgrade {
333+
no_heal: true,
334+
no_reinstall: true
335+
})
272336
));
273337
}
274338

src/main.rs

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ async fn run(cli: Cli) -> tracedecay::errors::Result<()> {
205205
};
206206

207207
maybe_run_extract_worker(&command);
208-
run_startup_preamble(&command);
208+
run_startup_preamble(&command).await;
209209
dispatch_command(command).await
210210
}
211211

@@ -219,7 +219,7 @@ fn maybe_run_extract_worker(command: &Commands) {
219219
}
220220
}
221221

222-
fn run_startup_preamble(command: &Commands) {
222+
async fn run_startup_preamble(command: &Commands) {
223223
let skip_startup_maintenance = should_skip_startup_maintenance(command);
224224
let skip_agent_install_maintenance = should_skip_agent_install_maintenance(command);
225225

@@ -257,7 +257,7 @@ fn run_startup_preamble(command: &Commands) {
257257
// Best-effort check: warn if install needs re-running.
258258
if !skip_agent_install_maintenance {
259259
tracedecay::agents::claude::check_install_stale();
260-
maybe_run_silent_reinstall(&mut user_config);
260+
maybe_run_silent_reinstall(&mut user_config).await;
261261
}
262262
}
263263

@@ -311,12 +311,12 @@ fn silent_reinstall_action(
311311
}
312312
}
313313

314-
fn maybe_run_silent_reinstall(user_config: &mut tracedecay::user_config::UserConfig) {
314+
async fn maybe_run_silent_reinstall(user_config: &mut tracedecay::user_config::UserConfig) {
315315
// Silent reinstall: re-run install for every tracked agent so permissions,
316316
// hooks, and MCP config stay in sync with the new binary.
317317
let running = env!("CARGO_PKG_VERSION");
318318
match silent_reinstall_action(user_config, running) {
319-
SilentReinstallAction::Reinstall => run_silent_reinstall(user_config, running),
319+
SilentReinstallAction::Reinstall => run_silent_reinstall(user_config, running).await,
320320
SilentReinstallAction::AdvanceMarker => {
321321
user_config.previous_version = running.to_string();
322322
user_config.save();
@@ -325,8 +325,13 @@ fn maybe_run_silent_reinstall(user_config: &mut tracedecay::user_config::UserCon
325325
}
326326
}
327327

328-
fn run_silent_reinstall(user_config: &mut tracedecay::user_config::UserConfig, running: &str) {
329-
if update_cmd::reinstall_tracked_agents(user_config) {
328+
async fn run_silent_reinstall(
329+
user_config: &mut tracedecay::user_config::UserConfig,
330+
running: &str,
331+
) {
332+
if let update_cmd::ReinstallOutcome::AllOk =
333+
update_cmd::reinstall_tracked_agents(user_config).await
334+
{
330335
user_config.mark_version_installed(running);
331336
user_config.save();
332337
}
@@ -561,14 +566,23 @@ async fn dispatch_command(command: Commands) -> tracedecay::errors::Result<()> {
561566
print!("{}", tracedecay::daemon::service_status(&socket_path));
562567
}
563568
},
564-
Commands::Upgrade { no_heal } => {
565-
update_cmd::run_upgrade_command(no_heal)?;
569+
Commands::Upgrade {
570+
no_heal,
571+
no_reinstall,
572+
} => {
573+
update_cmd::run_upgrade_command(no_heal, no_reinstall)?;
566574
}
567-
Commands::Update { no_heal } => {
568-
update_cmd::run_update_command(no_heal)?;
575+
Commands::Update {
576+
no_heal,
577+
no_reinstall,
578+
} => {
579+
update_cmd::run_update_command(no_heal, no_reinstall)?;
569580
}
570-
Commands::PostUpdate { no_heal } => {
571-
update_cmd::run_post_update_tasks(no_heal).await?;
581+
Commands::PostUpdate {
582+
no_heal,
583+
no_reinstall,
584+
} => {
585+
update_cmd::run_post_update_tasks(no_heal, no_reinstall).await?;
572586
}
573587
Commands::Channel { channel } => match channel {
574588
Some(target) => {

0 commit comments

Comments
 (0)