Skip to content

Commit fa8b532

Browse files
feat(claude): sync Claude Code plugin cache on install and update (#273)
Claude Code copies installed plugins into a versioned cache (~/.claude/plugins/cache/...) recorded in installed_plugins.json and loads from there, so refreshing the marketplace directory during `tracedecay upgrade`/`update-plugin` left running installs pinned to the stale cached version until the user manually ran `claude plugin update`. Install and update now drive Claude Code's own CLI (claude plugin install|update tracedecay@tracedecay) best-effort: temp-home installs (tests) skip it, and a missing or failing claude binary degrades to the existing restart hint. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent f65b7cf commit fa8b532

1 file changed

Lines changed: 46 additions & 0 deletions

File tree

src/agents/claude.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ impl AgentIntegration for ClaudeIntegration {
7373
crate::automation::skill_targets::SkillInstallTarget::Claude,
7474
)?;
7575
install_clean_local_config();
76+
sync_claude_plugin_cache(&ctx.home);
7677

7778
eprintln!();
7879
eprintln!("Setup complete. Next steps:");
@@ -150,6 +151,7 @@ impl AgentIntegration for ClaudeIntegration {
150151
write_json_file(&settings_path, &settings)?;
151152

152153
migrate_off_config_managed(&ctx.home);
154+
sync_claude_plugin_cache(&ctx.home);
153155

154156
Ok(UpdatePluginOutcome::Refreshed(vec![deploy_dir]))
155157
}
@@ -450,6 +452,50 @@ fn register_marketplace(home: &Path, deploy_dir: &Path) -> Result<()> {
450452
Ok(())
451453
}
452454

455+
/// Sync Claude Code's own plugin registry with the refreshed marketplace
456+
/// bundle.
457+
///
458+
/// Claude Code copies installed plugins into a versioned cache
459+
/// (`~/.claude/plugins/cache/...`) recorded in `installed_plugins.json`, so
460+
/// refreshing the marketplace directory alone leaves running installs on the
461+
/// stale cached version. Drive Claude Code's own CLI (`claude plugin
462+
/// install|update`) instead of writing its internal files, so the cache and
463+
/// registry always follow Claude Code's current contract. Best-effort: only
464+
/// runs against the real user home (temp-home installs in tests skip it),
465+
/// and a missing/failed `claude` CLI degrades to the existing restart hint.
466+
fn sync_claude_plugin_cache(home: &Path) {
467+
let is_real_home = dirs::home_dir().is_some_and(|real| real == home);
468+
if !is_real_home {
469+
return;
470+
}
471+
let registry = load_json_file(&home.join(".claude/plugins/installed_plugins.json"));
472+
let installed = registry
473+
.get("plugins")
474+
.and_then(|plugins| plugins.get(PLUGIN_IDENTIFIER))
475+
.is_some();
476+
let action = if installed { "update" } else { "install" };
477+
let output = std::process::Command::new("claude")
478+
.args(["plugin", action, PLUGIN_IDENTIFIER])
479+
.output();
480+
match output {
481+
Ok(output) if output.status.success() => {
482+
eprintln!(
483+
"\x1b[32m\u{2714}\x1b[0m Synced Claude Code plugin cache (claude plugin {action})"
484+
);
485+
}
486+
Ok(output) => {
487+
let stderr = String::from_utf8_lossy(&output.stderr);
488+
eprintln!(
489+
" Could not sync Claude Code plugin cache (claude plugin {action}): {}",
490+
stderr.trim().lines().last().unwrap_or("unknown error")
491+
);
492+
}
493+
Err(err) => {
494+
eprintln!(" Could not run claude plugin {action}: {err}");
495+
}
496+
}
497+
}
498+
453499
/// Remove the tracedecay marketplace entry from `known_marketplaces.json`,
454500
/// preserving every other marketplace. Idempotent.
455501
fn unregister_marketplace(home: &Path) -> Result<()> {

0 commit comments

Comments
 (0)