dev_prune/commands/
uninstall.rs1use anyhow::Result;
13use std::fs;
14
15use crate::commands::hook;
16use crate::config::Registry;
17use crate::output;
18
19pub fn run(deep: bool, yes: bool) -> Result<()> {
20 output::print_header(if deep {
21 "dev-prune Deep Uninstaller (Full Purge)"
22 } else {
23 "dev-prune Light Uninstaller"
24 });
25
26 let registry = Registry::load().ok();
27
28 if deep && !yes {
31 use std::io::{IsTerminal, Write};
32 let repo_count = registry.as_ref().map(|r| r.repo_count()).unwrap_or(0);
33 output::print_warning(&format!(
34 "This deletes the global config directory (including prune history) and \
35 removes `.devprune.json` from {repo_count} registered repositories."
36 ));
37 if !std::io::stdin().is_terminal() {
38 anyhow::bail!("Refusing to deep-uninstall without confirmation. Re-run with `--yes`.");
39 }
40 print!("Continue? [y/N]: ");
41 std::io::stdout().flush()?;
42 let mut input = String::new();
43 std::io::stdin().read_line(&mut input)?;
44 if !matches!(input.trim().to_lowercase().as_str(), "y" | "yes") {
45 output::print_info("Deep uninstall cancelled.");
46 return Ok(());
47 }
48 }
49
50 let mut left_behind: Vec<String> = Vec::new();
54
55 output::print_info("Removing background daemon scheduler...");
57 if let Err(e) = crate::daemon::uninstall_daemon() {
58 output::print_error(&format!("Background scheduler: {e:#}"));
59 left_behind.push("the background scheduler".to_string());
60 }
61
62 output::print_info("Removing global Git auto-registration hooks...");
64 if let Err(e) = hook::run_uninstall() {
65 output::print_error(&format!("Git hooks: {e:#}"));
66 left_behind.push("the global Git hooks".to_string());
67 }
68
69 if let Ok(current_exe) = std::env::current_exe() {
76 if let Some(parent) = current_exe.parent() {
77 #[cfg(windows)]
78 let alias = parent.join("devp.exe");
79 #[cfg(not(windows))]
80 let alias = parent.join("devp");
81
82 if alias.exists() && alias != current_exe {
83 let _ = fs::remove_file(alias);
84 }
85 }
86 }
87
88 crate::commands::icon::unregister_file_type();
90
91 if deep {
92 if let Some(reg) = registry {
94 for repo_path in reg.repositories.keys() {
95 let cfg_file = repo_path.join(crate::constants::PER_REPO_CONFIG_FILE);
96 if cfg_file.exists() {
97 let _ = fs::remove_file(cfg_file);
98 }
99 }
100 }
101
102 if let Ok(config_dir) = Registry::config_dir() {
103 if config_dir.exists() {
104 let _ = fs::remove_dir_all(config_dir);
105 output::print_info("Removed global configuration directory.");
106 }
107 }
108
109 if left_behind.is_empty() {
110 output::print_success(
111 "Deep uninstall complete: All configurations, background daemons, and registry files purged.",
112 );
113 }
114 output::print_warning(
119 "Running dev-prune again would reinstall the hooks and the scheduler — a \
120 machine with no config directory looks like a fresh install.",
121 );
122 print_binary_removal_hint();
123 } else {
124 crate::setup::suppress_next_auto_setup();
127 if left_behind.is_empty() {
128 output::print_success(
129 "Light uninstall complete: Background daemon and Git hooks removed. Configuration preserved for future reinstall.",
130 );
131 }
132 output::print_info("Put them back at any time with `devp setup`.");
133 output::print_info(
135 "The next upgrade will reinstall them. To keep them off permanently: \
136 `devp config set auto_setup false`.",
137 );
138 }
139
140 if !left_behind.is_empty() {
141 anyhow::bail!("Uninstall finished, but {} is still installed.", {
142 left_behind.join(" and ")
143 });
144 }
145
146 Ok(())
147}
148
149fn print_binary_removal_hint() {
151 let Ok(exe) = std::env::current_exe() else {
152 return;
153 };
154 output::print_info(&format!(
155 "To finish, delete the binary: {}",
156 output::clean_path(&exe)
157 ));
158}