mi6_cli/commands/
uninstall.rs1use std::io::Write;
12use std::path::PathBuf;
13use std::process::Command;
14
15use anyhow::{Context, Result};
16
17use mi6_core::{Config, FrameworkAdapter, all_adapters};
18use mi6_otel_server::lifecycle::{DEFAULT_PORT, is_mi6_server, stop_server};
19
20use super::upgrade::InstallMethod;
21use crate::display::{bold_green, bold_red, bold_white, bold_yellow, confirm};
22
23pub struct UninstallOptions {
25 pub yes: bool,
27 pub keep_data: bool,
29 pub dry_run: bool,
31}
32
33pub fn run_uninstall(options: UninstallOptions) -> Result<()> {
35 let current_version = env!("CARGO_PKG_VERSION");
36 let method = InstallMethod::detect()?;
37
38 println!("mi6 v{}", current_version);
39 println!();
40 println!("{} {}", bold_green("Installation method:"), method.name());
41 if let InstallMethod::CargoPath(path) = &method {
42 println!("{} {}", bold_green(" Source code path:"), path.display());
43 }
44 println!();
45
46 let enabled_frameworks = find_all_enabled_frameworks();
48 let mi6_dir = get_mi6_dir();
49 let data_exists = mi6_dir.as_ref().is_some_and(|p| p.exists());
50 let otel_running = is_mi6_server(DEFAULT_PORT);
51
52 println!("{}", bold_green("Uninstall steps:"));
53
54 if enabled_frameworks.is_empty() {
56 println!("{} No enabled frameworks found", bold_green("1."));
57 } else {
58 println!("{} Disable mi6 hooks", bold_green("1."));
59 for (adapter, scope) in &enabled_frameworks {
60 println!(" - {} ({})", bold_white(adapter.display_name()), scope);
61 }
62 }
63
64 if otel_running {
66 println!(
67 "{} Stop OTel server ({})",
68 bold_green("2."),
69 bold_white(&format!("port {}", DEFAULT_PORT))
70 );
71 } else {
72 println!("{} OTel server not running", bold_green("2."));
73 }
74
75 println!(
77 "{} Uninstall mi6 binary ({})",
78 bold_green("3."),
79 bold_white(get_uninstall_command(&method).as_str())
80 );
81
82 if options.keep_data {
84 println!("{} Keeping mi6 data (--keep-data)", bold_green("4."));
85 } else if data_exists {
86 if let Some(ref dir) = mi6_dir {
87 println!("{} Delete mi6 data (will prompt)", bold_green("4."));
88 println!(
89 " - {} (database, config, themes)",
90 bold_white(&dir.display().to_string())
91 );
92 }
93 } else {
94 println!("{} No mi6 data to delete", bold_green("4."));
95 }
96 println!();
97
98 if options.dry_run {
100 println!("{} No changes made.", bold_yellow("Dry run:"));
101 return Ok(());
102 }
103
104 if !options.yes {
106 eprintln!("{}", bold_red("This will permanently uninstall mi6."));
107 if !confirm("Uninstall mi6?")? {
108 eprintln!("Aborted.");
109 return Ok(());
110 }
111 println!();
112 }
113
114 if !enabled_frameworks.is_empty() {
116 print!("{} mi6 hooks... ", bold_green("Disabling"));
117 std::io::stdout().flush().ok();
118 for (adapter, _scope) in &enabled_frameworks {
119 let _ = adapter.uninstall_hooks(false, false); let _ = adapter.uninstall_hooks(true, false); let _ = adapter.uninstall_hooks(false, true); }
124 remove_claude_marketplace();
126 println!("{}", bold_green("done"));
127 }
128
129 if otel_running {
131 print!("{} OTel server... ", bold_green("Stopping"));
132 std::io::stdout().flush().ok();
133 let _ = stop_server(DEFAULT_PORT);
135 println!("{}", bold_green("done"));
136 }
137
138 print!("{} mi6 binary... ", bold_green("Uninstalling"));
140 std::io::stdout().flush().ok();
141 match &method {
142 InstallMethod::Homebrew => uninstall_homebrew()?,
143 InstallMethod::CargoRegistry | InstallMethod::CargoPath(_) => uninstall_cargo()?,
144 InstallMethod::Standalone => uninstall_standalone()?,
145 }
146 println!("{}", bold_green("done"));
147
148 if !options.keep_data
150 && let Some(ref dir) = mi6_dir
151 && dir.exists()
152 {
153 let should_delete = if options.yes {
154 true
155 } else {
156 println!();
157 eprintln!("{} {}", bold_yellow("mi6 data directory:"), dir.display());
158 confirm("Delete mi6 data (database, config, themes)?")?
159 };
160
161 if should_delete {
162 print!("{} {}... ", bold_green("Deleting"), dir.display());
163 std::io::stdout().flush().ok();
164 std::fs::remove_dir_all(dir)
165 .with_context(|| format!("failed to delete {}", dir.display()))?;
166 println!("{}", bold_green("done"));
167 } else {
168 eprintln!("Keeping mi6 data.");
169 }
170 }
171
172 println!();
173 println!("Done! mi6 has been uninstalled.");
174
175 Ok(())
176}
177
178fn get_uninstall_command(method: &InstallMethod) -> String {
180 match method {
181 InstallMethod::Homebrew => "brew uninstall mi6".to_string(),
182 InstallMethod::CargoRegistry | InstallMethod::CargoPath(_) => {
183 "cargo uninstall mi6".to_string()
184 }
185 InstallMethod::Standalone => {
186 if let Ok(exe_path) = std::env::current_exe() {
187 format!("rm {}", exe_path.display())
188 } else {
189 "(remove mi6 binary manually)".to_string()
190 }
191 }
192 }
193}
194
195fn scope_name(local: bool, settings_local: bool) -> &'static str {
197 if settings_local {
198 "settings-local"
199 } else if local {
200 "local"
201 } else {
202 "global"
203 }
204}
205
206fn find_all_enabled_frameworks() -> Vec<(&'static dyn FrameworkAdapter, &'static str)> {
210 let mut results = Vec::new();
211
212 for adapter in all_adapters() {
213 for (local, settings_local) in [(false, false), (true, false), (false, true)] {
215 if adapter.has_mi6_hooks(local, settings_local) {
216 results.push((adapter, scope_name(local, settings_local)));
217 }
218 }
219 }
220
221 results
222}
223
224fn get_mi6_dir() -> Option<PathBuf> {
226 Config::mi6_dir().ok()
227}
228
229fn remove_claude_marketplace() {
233 let Some(home) = dirs::home_dir() else {
235 return;
236 };
237 let cache_path = home.join(".mi6/claude-plugin");
238
239 if !cache_path.exists() {
241 return;
242 }
243
244 let cache_path_str = cache_path.to_string_lossy();
245
246 let _ = Command::new("claude")
249 .args(["plugin", "marketplace", "remove", &cache_path_str])
250 .output();
251}
252
253fn uninstall_homebrew() -> Result<()> {
255 let status = Command::new("brew")
256 .args(["uninstall", "mi6"])
257 .status()
258 .context("failed to run brew uninstall")?;
259
260 if !status.success() {
261 anyhow::bail!("brew uninstall failed with exit code: {:?}", status.code());
262 }
263
264 Ok(())
265}
266
267fn uninstall_cargo() -> Result<()> {
269 let status = Command::new("cargo")
270 .args(["uninstall", "mi6"])
271 .status()
272 .context("failed to run cargo uninstall")?;
273
274 if !status.success() {
275 anyhow::bail!("cargo uninstall failed with exit code: {:?}", status.code());
276 }
277
278 Ok(())
279}
280
281fn uninstall_standalone() -> Result<()> {
283 let exe_path = std::env::current_exe().context("failed to get current executable path")?;
284
285 std::fs::remove_file(&exe_path)
288 .with_context(|| format!("failed to delete {}", exe_path.display()))?;
289
290 Ok(())
291}