use anyhow::Context as _;
use colored::Colorize;
use trusty_mpm::core::binary_reinstall::{ReinstallRoute, reinstall_route};
use trusty_mpm::core::reinstall::{AssetCounts, ReinstallReport, reinstall_assets};
pub(crate) async fn run(args: crate::cli::ReinstallArgs) -> anyhow::Result<()> {
let crate::cli::ReinstallArgs { force, binary, yes } = args;
let paths = trusty_mpm::core::paths::FrameworkPaths::default();
let managed = super::managed_root::resolve_managed_paths(None)
.context("could not resolve the standalone managed root")?;
let project_dir = std::env::current_dir().ok();
let backup_root = backup_root(&paths);
println!(
"Redeploying bundled agents and skills (source: {})",
paths.framework.display()
);
let report = reinstall_assets(
&paths,
&managed.claude_config_dir,
project_dir.as_deref(),
force,
&backup_root,
);
print_report(&report);
if binary {
reinstall_binary(yes).await?;
} else {
println!(
"\n{}",
"The binary was not touched — pass `--binary` to reinstall it too.".dimmed()
);
}
if report.has_failures() {
anyhow::bail!("one or more destinations could not be fully redeployed (see above)");
}
Ok(())
}
fn backup_root(paths: &trusty_mpm::core::paths::FrameworkPaths) -> std::path::PathBuf {
let stamp = chrono::Utc::now().format("%Y%m%d-%H%M%S");
paths.root.join(format!("backup-reinstall-{stamp}"))
}
fn print_report(report: &ReinstallReport) {
for note in &report.notes {
eprintln!(" {} {note}", "!".yellow());
}
println!(
" agent source: {} skill source: {}",
refreshed(report.agent_source_refreshed),
refreshed(report.skill_source_refreshed)
);
for tier in &report.tiers {
println!("\n{} {}", tier.label.bold(), tier.skills_dir.display());
if let Some(dir) = &tier.agents_dir {
println!(" agents {} {}", counts(&tier.agents), dir.display());
}
println!(" skills {}", counts(&tier.skills));
if tier.optional {
println!(
" {}",
"(optional destination — a failure here does not fail the command)".dimmed()
);
}
for note in &tier.notes {
println!(" {} {note}", "!".yellow());
}
}
let deployed: usize = report
.tiers
.iter()
.map(|t| t.agents.deployed + t.skills.deployed)
.sum();
let repaired: usize = report.tiers.iter().map(|t| t.agents.repaired).sum();
let shadowed: usize = report.tiers.iter().map(|t| t.skills.shadowed).sum();
println!(
"\n{deployed} file(s) written, {repaired} repaired, {shadowed} shadowed, across {} \
destination(s).",
report.tiers.len()
);
}
fn refreshed(yes: bool) -> String {
if yes {
"refreshed from this binary".green().to_string()
} else {
"already current".dimmed().to_string()
}
}
fn counts(c: &AssetCounts) -> String {
format!(
"{} deployed, {} repaired, {} preserved, {} unchanged, {} shadowed, {} failed",
c.deployed, c.repaired, c.preserved, c.unchanged, c.shadowed, c.failed
)
}
async fn reinstall_binary(yes: bool) -> anyhow::Result<()> {
let exe = std::env::current_exe().context("could not resolve the running executable")?;
let bin_name = exe
.file_name()
.and_then(|n| n.to_str())
.context("the running executable has no file name")?
.to_string();
let cargo_home = match std::env::var("CARGO_HOME") {
Ok(h) if !h.is_empty() => std::path::PathBuf::from(h),
_ => dirs::home_dir()
.context("no `$CARGO_HOME` and no home directory — cannot locate cargo's ledger")?
.join(".cargo"),
};
let ledger = std::fs::read_to_string(cargo_home.join(".crates2.json"))
.ok()
.and_then(|raw| trusty_mpm::core::binary_provenance::parse_cargo_installs(&raw));
let route = reinstall_route(
&bin_name,
env!("CARGO_PKG_VERSION"),
&exe,
&cargo_home.join("bin"),
ledger.as_deref(),
&|dir| dir.exists(),
);
match route {
ReinstallRoute::Refuse(why) => {
eprintln!("\n{} {why}", "✗".red());
anyhow::bail!("refusing to reinstall the binary: provenance is not conclusive");
}
ReinstallRoute::Registry { package } => {
println!(
"\nBinary: `{bin_name}` was installed from crates.io — checking for a newer \
{package}…"
);
if !confirm(yes, &format!("Reinstall {package} from crates.io"))? {
return Ok(());
}
match trusty_common::update::check_crates_io(&package, env!("CARGO_PKG_VERSION")).await
{
Some(info) => println!(" update available: {} → {}", info.current, info.latest),
None => println!(" already at the latest published version; reinstalling anyway"),
}
match trusty_common::update::upgrade_and_restart(&package, &bin_name).await {
Ok(Some(hint)) => println!(" {hint}"),
Ok(None) => {}
Err(e) => return Err(anyhow::anyhow!("binary reinstall failed: {e}")),
}
Ok(())
}
ReinstallRoute::Path { package, dir } => {
println!(
"\nBinary: `{bin_name}` was installed with `cargo install --path` from {}",
dir.display()
);
if !confirm(yes, &format!("Reinstall {package} from {}", dir.display()))? {
return Ok(());
}
install_from_path(&dir).await?;
trusty_common::update::verify_installed_binary(&bin_name)
.await
.map_err(|e| {
anyhow::anyhow!(
"health gate failed after reinstalling {package} from {}: {e}",
dir.display()
)
})?;
println!(" {} `{bin_name}` reinstalled and verified", "✓".green());
Ok(())
}
}
}
async fn install_from_path(dir: &std::path::Path) -> anyhow::Result<()> {
let status = tokio::process::Command::new("cargo")
.arg("install")
.arg("--path")
.arg(dir)
.arg("--locked")
.status()
.await
.with_context(|| format!("failed to run `cargo install --path {}`", dir.display()))?;
if !status.success() {
anyhow::bail!(
"`cargo install --path {} --locked` exited with {status}",
dir.display()
);
}
Ok(())
}
fn confirm(yes: bool, prompt: &str) -> anyhow::Result<bool> {
if yes {
return Ok(true);
}
use std::io::Write as _;
eprint!("{prompt}? [y/N] ");
let _ = std::io::stderr().flush();
let mut line = String::new();
std::io::stdin()
.read_line(&mut line)
.context("failed to read confirmation")?;
let answer = line.trim().to_ascii_lowercase();
if answer != "y" && answer != "yes" {
eprintln!("Binary reinstall cancelled.");
return Ok(false);
}
Ok(true)
}