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 match fs::remove_dir_all(&config_dir) {
105 Ok(()) => output::print_info("Removed global configuration directory."),
106 Err(e) => {
107 let running_inside =
112 std::env::current_exe().is_ok_and(|exe| exe.starts_with(&config_dir));
113 let hint = if running_inside {
114 " The running binary lives inside it — delete the directory \
115 by hand once this command exits."
116 } else {
117 ""
118 };
119 output::print_error(&format!(
120 "Could not remove {}: {e}.{hint}",
121 output::clean_path(&config_dir)
122 ));
123 left_behind.push("the global configuration directory".to_string());
124 }
125 }
126 }
127 }
128
129 if left_behind.is_empty() {
130 output::print_success(
131 "Deep uninstall complete: All configurations, background daemons, and registry files purged.",
132 );
133 }
134 output::print_warning(
139 "Running dev-prune again would reinstall the hooks and the scheduler — a \
140 machine with no config directory looks like a fresh install.",
141 );
142 print_binary_removal_hint();
143 } else {
144 crate::setup::suppress_next_auto_setup();
147 if left_behind.is_empty() {
148 output::print_success(
149 "Light uninstall complete: Background daemon and Git hooks removed. Configuration preserved for future reinstall.",
150 );
151 }
152 output::print_info("Put them back at any time with `devp setup`.");
153 output::print_info(
155 "The next upgrade will reinstall them. To keep them off permanently: \
156 `devp config set auto_setup false`.",
157 );
158 }
159
160 if !left_behind.is_empty() {
161 anyhow::bail!("Uninstall finished, but {} is still installed.", {
162 left_behind.join(" and ")
163 });
164 }
165
166 Ok(())
167}
168
169fn print_binary_removal_hint() {
175 let Ok(exe) = std::env::current_exe() else {
176 return;
177 };
178 let mut targets = vec![output::clean_path(&exe)];
179 if let Some(parent) = exe.parent() {
180 for stem in ["dev-prune", "devp"] {
181 let name = if cfg!(windows) {
182 format!("{stem}.exe")
183 } else {
184 stem.to_string()
185 };
186 let twin = parent.join(name);
187 if twin.exists() && twin != exe {
188 targets.push(output::clean_path(&twin));
189 }
190 }
191 }
192 output::print_info(&format!(
193 "To finish, delete {}: {}",
194 if targets.len() > 1 {
195 "the binaries"
196 } else {
197 "the binary"
198 },
199 targets.join(" and ")
200 ));
201}