use anyhow::Result;
use std::fs;
use crate::commands::hook;
use crate::config::Registry;
use crate::output;
pub fn run(deep: bool, yes: bool) -> Result<()> {
output::print_header(if deep {
"dev-prune Deep Uninstaller (Full Purge)"
} else {
"dev-prune Light Uninstaller"
});
let registry = Registry::load().ok();
if deep && !yes {
use std::io::{IsTerminal, Write};
let repo_count = registry.as_ref().map(|r| r.repo_count()).unwrap_or(0);
output::print_warning(&format!(
"This deletes the global config directory (including prune history) and \
removes `.devprune.json` from {repo_count} registered repositories."
));
if !std::io::stdin().is_terminal() {
anyhow::bail!("Refusing to deep-uninstall without confirmation. Re-run with `--yes`.");
}
print!("Continue? [y/N]: ");
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !matches!(input.trim().to_lowercase().as_str(), "y" | "yes") {
output::print_info("Deep uninstall cancelled.");
return Ok(());
}
}
let mut left_behind: Vec<String> = Vec::new();
output::print_info("Removing background daemon scheduler...");
if let Err(e) = crate::daemon::uninstall_daemon() {
output::print_error(&format!("Background scheduler: {e:#}"));
left_behind.push("the background scheduler".to_string());
}
output::print_info("Removing global Git auto-registration hooks...");
if let Err(e) = hook::run_uninstall() {
output::print_error(&format!("Git hooks: {e:#}"));
left_behind.push("the global Git hooks".to_string());
}
if let Ok(current_exe) = std::env::current_exe() {
if let Some(parent) = current_exe.parent() {
#[cfg(windows)]
let alias = parent.join("devp.exe");
#[cfg(not(windows))]
let alias = parent.join("devp");
if alias.exists() && alias != current_exe {
let _ = fs::remove_file(alias);
}
}
}
crate::commands::icon::unregister_file_type();
if deep {
if let Some(reg) = registry {
for repo_path in reg.repositories.keys() {
let cfg_file = repo_path.join(crate::constants::PER_REPO_CONFIG_FILE);
if cfg_file.exists() {
let _ = fs::remove_file(cfg_file);
}
}
}
if let Ok(config_dir) = Registry::config_dir() {
if config_dir.exists() {
match fs::remove_dir_all(&config_dir) {
Ok(()) => output::print_info("Removed global configuration directory."),
Err(e) => {
let running_inside =
std::env::current_exe().is_ok_and(|exe| exe.starts_with(&config_dir));
let hint = if running_inside {
" The running binary lives inside it — delete the directory \
by hand once this command exits."
} else {
""
};
output::print_error(&format!(
"Could not remove {}: {e}.{hint}",
output::clean_path(&config_dir)
));
left_behind.push("the global configuration directory".to_string());
}
}
}
}
if left_behind.is_empty() {
output::print_success(
"Deep uninstall complete: All configurations, background daemons, and registry files purged.",
);
}
output::print_warning(
"Running dev-prune again would reinstall the hooks and the scheduler — a \
machine with no config directory looks like a fresh install.",
);
print_binary_removal_hint();
} else {
crate::setup::suppress_next_auto_setup();
if left_behind.is_empty() {
output::print_success(
"Light uninstall complete: Background daemon and Git hooks removed. Configuration preserved for future reinstall.",
);
}
output::print_info("Put them back at any time with `devp setup`.");
output::print_info(
"The next upgrade will reinstall them. To keep them off permanently: \
`devp config set auto_setup false`.",
);
}
if !left_behind.is_empty() {
anyhow::bail!("Uninstall finished, but {} is still installed.", {
left_behind.join(" and ")
});
}
Ok(())
}
fn print_binary_removal_hint() {
let Ok(exe) = std::env::current_exe() else {
return;
};
let mut targets = vec![output::clean_path(&exe)];
if let Some(parent) = exe.parent() {
for stem in ["dev-prune", "devp"] {
let name = if cfg!(windows) {
format!("{stem}.exe")
} else {
stem.to_string()
};
let twin = parent.join(name);
if twin.exists() && twin != exe {
targets.push(output::clean_path(&twin));
}
}
}
output::print_info(&format!(
"To finish, delete {}: {}",
if targets.len() > 1 {
"the binaries"
} else {
"the binary"
},
targets.join(" and ")
));
}