mi6_cli/commands/
uninstall.rs

1//! Uninstall command - completely remove mi6 from the system.
2//!
3//! This command:
4//! 1. Disables mi6 hooks for all enabled frameworks
5//! 2. Optionally deletes the mi6 data directory (MI6_DIR_PATH or ~/.mi6)
6//! 3. Uninstalls the mi6 binary based on detected installation method
7
8use std::path::PathBuf;
9use std::process::Command;
10
11use anyhow::{Context, Result};
12
13use mi6_core::{
14    Config, FrameworkAdapter, FrameworkResolutionMode, all_adapters, resolve_frameworks,
15};
16
17use super::disable::disable_framework_silent;
18use super::upgrade::InstallMethod;
19use crate::display::{StderrColors, confirm};
20
21/// Options for the uninstall command
22pub struct UninstallOptions {
23    /// Skip confirmation prompts
24    pub confirm: bool,
25    /// Keep mi6 data (database, config, themes)
26    pub keep_data: bool,
27    /// Show what would happen without actually doing it
28    pub dry_run: bool,
29}
30
31/// Run the uninstall command
32pub fn run_uninstall(options: UninstallOptions) -> Result<()> {
33    let colors = StderrColors::new();
34    let method = InstallMethod::detect()?;
35
36    eprintln!("{}mi6 uninstall{}", colors.bold, colors.reset);
37    eprintln!("Detected installation method: {}", method.name());
38    eprintln!();
39
40    // Gather information about what will be done
41    let enabled_frameworks = find_enabled_frameworks();
42    let mi6_dir = get_mi6_dir();
43    let data_exists = mi6_dir.as_ref().is_some_and(|p| p.exists());
44
45    // Step 1: Show frameworks that will be disabled
46    if enabled_frameworks.is_empty() {
47        eprintln!(
48            "{}Step 1:{} No enabled frameworks found",
49            colors.cyan, colors.reset
50        );
51        eprintln!();
52    } else {
53        eprintln!("{}Step 1:{} Disable mi6 hooks", colors.cyan, colors.reset);
54        for adapter in &enabled_frameworks {
55            eprintln!("  - {}", adapter.display_name());
56        }
57        eprintln!();
58    }
59
60    // Step 2: Show data that will be deleted
61    if !options.keep_data && data_exists {
62        eprintln!("{}Step 2:{} Delete mi6 data", colors.cyan, colors.reset);
63        if let Some(ref dir) = mi6_dir
64            && dir.exists()
65        {
66            eprintln!("  - {} (database, config, themes)", dir.display());
67        }
68        eprintln!();
69    } else if options.keep_data {
70        eprintln!(
71            "{}Step 2:{} Keeping mi6 data (--keep-data)",
72            colors.cyan, colors.reset
73        );
74        eprintln!();
75    } else {
76        eprintln!(
77            "{}Step 2:{} No mi6 data to delete",
78            colors.cyan, colors.reset
79        );
80        eprintln!();
81    }
82
83    // Step 3: Show uninstall command
84    eprintln!(
85        "{}Step 3:{} Uninstall mi6 binary",
86        colors.cyan, colors.reset
87    );
88    match &method {
89        InstallMethod::Homebrew => eprintln!("  brew uninstall mi6"),
90        InstallMethod::CargoRegistry | InstallMethod::CargoPath(_) => {
91            eprintln!("  cargo uninstall mi6");
92        }
93        InstallMethod::Standalone => {
94            if let Ok(exe_path) = std::env::current_exe() {
95                eprintln!("  rm {}", exe_path.display());
96            } else {
97                eprintln!("  (remove mi6 binary manually)");
98            }
99        }
100    }
101    eprintln!();
102
103    // If dry run, stop here
104    if options.dry_run {
105        eprintln!("{}Dry run:{} No changes made.", colors.yellow, colors.reset);
106        return Ok(());
107    }
108
109    // Confirm with user (ask about data deletion before any operations)
110    if !options.confirm {
111        eprintln!(
112            "{}This will permanently uninstall mi6.{}",
113            colors.yellow, colors.reset
114        );
115
116        // If there's data and we're not keeping it, emphasize this
117        if !options.keep_data && data_exists {
118            eprintln!(
119                "{}All mi6 data (sessions, events, config) will be deleted.{}",
120                colors.yellow, colors.reset
121            );
122        }
123
124        if !confirm("Proceed with uninstall?")? {
125            eprintln!("Aborted.");
126            return Ok(());
127        }
128        eprintln!();
129    }
130
131    // Execute Step 1: Disable frameworks
132    if !enabled_frameworks.is_empty() {
133        eprint!("{}Disabling{} mi6 hooks... ", colors.green, colors.reset);
134        for adapter in &enabled_frameworks {
135            disable_framework_silent(*adapter)?;
136        }
137        eprintln!("{}done{}", colors.green, colors.reset);
138    }
139
140    // Execute Step 2: Delete data
141    if !options.keep_data
142        && let Some(ref dir) = mi6_dir
143        && dir.exists()
144    {
145        eprint!(
146            "{}Deleting{} {}... ",
147            colors.green,
148            colors.reset,
149            dir.display()
150        );
151        std::fs::remove_dir_all(dir)
152            .with_context(|| format!("failed to delete {}", dir.display()))?;
153        eprintln!("{}done{}", colors.green, colors.reset);
154    }
155
156    // Execute Step 3: Uninstall binary
157    eprint!(
158        "{}Uninstalling{} mi6 binary... ",
159        colors.green, colors.reset
160    );
161    match &method {
162        InstallMethod::Homebrew => uninstall_homebrew()?,
163        InstallMethod::CargoRegistry | InstallMethod::CargoPath(_) => uninstall_cargo()?,
164        InstallMethod::Standalone => uninstall_standalone()?,
165    }
166    eprintln!("{}done{}", colors.green, colors.reset);
167
168    eprintln!();
169    eprintln!("{}mi6 has been uninstalled.{}", colors.bold, colors.reset);
170
171    Ok(())
172}
173
174/// Find all frameworks that have mi6 enabled
175fn find_enabled_frameworks() -> Vec<&'static dyn FrameworkAdapter> {
176    // Check global config for all frameworks
177    let mode = FrameworkResolutionMode::Active {
178        local: false,
179        settings_local: false,
180    };
181
182    // Try to resolve with empty list (will find all enabled)
183    match resolve_frameworks(&[], Some(mode)) {
184        Ok(adapters) => adapters,
185        Err(_) => {
186            // Fallback: check each adapter individually
187            all_adapters()
188                .into_iter()
189                .filter(|a| a.has_mi6_hooks(false, false))
190                .collect()
191        }
192    }
193}
194
195/// Get the main mi6 data directory (MI6_DIR_PATH or ~/.mi6)
196fn get_mi6_dir() -> Option<PathBuf> {
197    Config::mi6_dir().ok()
198}
199
200/// Uninstall via Homebrew
201fn uninstall_homebrew() -> Result<()> {
202    let status = Command::new("brew")
203        .args(["uninstall", "mi6"])
204        .status()
205        .context("failed to run brew uninstall")?;
206
207    if !status.success() {
208        anyhow::bail!("brew uninstall failed with exit code: {:?}", status.code());
209    }
210
211    Ok(())
212}
213
214/// Uninstall via Cargo
215fn uninstall_cargo() -> Result<()> {
216    let status = Command::new("cargo")
217        .args(["uninstall", "mi6"])
218        .status()
219        .context("failed to run cargo uninstall")?;
220
221    if !status.success() {
222        anyhow::bail!("cargo uninstall failed with exit code: {:?}", status.code());
223    }
224
225    Ok(())
226}
227
228/// Uninstall standalone binary
229fn uninstall_standalone() -> Result<()> {
230    let exe_path = std::env::current_exe().context("failed to get current executable path")?;
231
232    // On Unix, we can delete ourselves while running
233    // On Windows, this would fail, but mi6 is primarily for Unix systems
234    std::fs::remove_file(&exe_path)
235        .with_context(|| format!("failed to delete {}", exe_path.display()))?;
236
237    Ok(())
238}