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_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 if !confirm("Uninstall mi6?")? {
107 eprintln!("Aborted.");
108 return Ok(());
109 }
110 println!();
111 }
112
113 if !enabled_frameworks.is_empty() {
115 print!("{} mi6 hooks... ", bold_green("Disabling"));
116 std::io::stdout().flush().ok();
117 for (adapter, _scope) in &enabled_frameworks {
118 let _ = adapter.uninstall_hooks(false, false); let _ = adapter.uninstall_hooks(true, false); let _ = adapter.uninstall_hooks(false, true); }
123 remove_claude_marketplace();
125 println!("{}", bold_green("done"));
126 }
127
128 if otel_running {
130 print!("{} OTel server... ", bold_green("Stopping"));
131 std::io::stdout().flush().ok();
132 let _ = stop_server(DEFAULT_PORT);
134 println!("{}", bold_green("done"));
135 }
136
137 print!("{} mi6 binary... ", bold_green("Uninstalling"));
139 std::io::stdout().flush().ok();
140 match &method {
141 InstallMethod::Homebrew => uninstall_homebrew()?,
142 InstallMethod::CargoRegistry | InstallMethod::CargoPath(_) => uninstall_cargo()?,
143 InstallMethod::Standalone => uninstall_standalone()?,
144 }
145 println!("{}", bold_green("done"));
146
147 if !options.keep_data
149 && let Some(ref dir) = mi6_dir
150 && dir.exists()
151 {
152 let should_delete = if options.yes {
153 true
154 } else {
155 println!();
156 eprintln!("{} {}", bold_yellow("mi6 data directory:"), dir.display());
157 confirm("Delete mi6 data (database, config, themes)?")?
158 };
159
160 if should_delete {
161 print!("{} {}... ", bold_green("Deleting"), dir.display());
162 std::io::stdout().flush().ok();
163 std::fs::remove_dir_all(dir)
164 .with_context(|| format!("failed to delete {}", dir.display()))?;
165 println!("{}", bold_green("done"));
166 } else {
167 eprintln!("Keeping mi6 data.");
168 }
169 }
170
171 println!();
172 println!("Done! mi6 has been uninstalled.");
173
174 Ok(())
175}
176
177fn get_uninstall_command(method: &InstallMethod) -> String {
179 match method {
180 InstallMethod::Homebrew => "brew uninstall mi6".to_string(),
181 InstallMethod::CargoRegistry | InstallMethod::CargoPath(_) => {
182 "cargo uninstall mi6".to_string()
183 }
184 InstallMethod::Standalone => {
185 if let Ok(exe_path) = std::env::current_exe() {
186 format!("rm {}", exe_path.display())
187 } else {
188 "(remove mi6 binary manually)".to_string()
189 }
190 }
191 }
192}
193
194fn scope_name(local: bool, settings_local: bool) -> &'static str {
196 if settings_local {
197 "settings-local"
198 } else if local {
199 "local"
200 } else {
201 "global"
202 }
203}
204
205fn find_all_enabled_frameworks() -> Vec<(&'static dyn FrameworkAdapter, &'static str)> {
209 let mut results = Vec::new();
210
211 for adapter in all_adapters() {
212 for (local, settings_local) in [(false, false), (true, false), (false, true)] {
214 if adapter.has_mi6_hooks(local, settings_local) {
215 results.push((adapter, scope_name(local, settings_local)));
216 }
217 }
218 }
219
220 results
221}
222
223fn get_mi6_dir() -> Option<PathBuf> {
225 Config::mi6_dir().ok()
226}
227
228fn remove_claude_marketplace() {
232 let Some(home) = dirs::home_dir() else {
234 return;
235 };
236 let cache_path = home.join(".mi6/claude-plugin");
237
238 if !cache_path.exists() {
240 return;
241 }
242
243 let cache_path_str = cache_path.to_string_lossy();
244
245 let _ = Command::new("claude")
248 .args(["plugin", "marketplace", "remove", &cache_path_str])
249 .output();
250}
251
252fn uninstall_homebrew() -> Result<()> {
254 let status = Command::new("brew")
255 .args(["uninstall", "mi6"])
256 .status()
257 .context("failed to run brew uninstall")?;
258
259 if !status.success() {
260 anyhow::bail!("brew uninstall failed with exit code: {:?}", status.code());
261 }
262
263 Ok(())
264}
265
266fn uninstall_cargo() -> Result<()> {
268 let status = Command::new("cargo")
269 .args(["uninstall", "mi6"])
270 .status()
271 .context("failed to run cargo uninstall")?;
272
273 if !status.success() {
274 anyhow::bail!("cargo uninstall failed with exit code: {:?}", status.code());
275 }
276
277 Ok(())
278}
279
280fn uninstall_standalone() -> Result<()> {
282 let exe_path = std::env::current_exe().context("failed to get current executable path")?;
283
284 std::fs::remove_file(&exe_path)
287 .with_context(|| format!("failed to delete {}", exe_path.display()))?;
288
289 Ok(())
290}