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::Cargo => eprintln!("  cargo uninstall mi6"),
91        InstallMethod::Standalone => {
92            if let Ok(exe_path) = std::env::current_exe() {
93                eprintln!("  rm {}", exe_path.display());
94            } else {
95                eprintln!("  (remove mi6 binary manually)");
96            }
97        }
98    }
99    eprintln!();
100
101    // If dry run, stop here
102    if options.dry_run {
103        eprintln!("{}Dry run:{} No changes made.", colors.yellow, colors.reset);
104        return Ok(());
105    }
106
107    // Confirm with user (ask about data deletion before any operations)
108    if !options.confirm {
109        eprintln!(
110            "{}This will permanently uninstall mi6.{}",
111            colors.yellow, colors.reset
112        );
113
114        // If there's data and we're not keeping it, emphasize this
115        if !options.keep_data && data_exists {
116            eprintln!(
117                "{}All mi6 data (sessions, events, config) will be deleted.{}",
118                colors.yellow, colors.reset
119            );
120        }
121
122        if !confirm("Proceed with uninstall?")? {
123            eprintln!("Aborted.");
124            return Ok(());
125        }
126        eprintln!();
127    }
128
129    // Execute Step 1: Disable frameworks
130    if !enabled_frameworks.is_empty() {
131        eprint!("{}Disabling{} mi6 hooks... ", colors.green, colors.reset);
132        for adapter in &enabled_frameworks {
133            disable_framework_silent(*adapter)?;
134        }
135        eprintln!("{}done{}", colors.green, colors.reset);
136    }
137
138    // Execute Step 2: Delete data
139    if !options.keep_data
140        && let Some(ref dir) = mi6_dir
141        && dir.exists()
142    {
143        eprint!(
144            "{}Deleting{} {}... ",
145            colors.green,
146            colors.reset,
147            dir.display()
148        );
149        std::fs::remove_dir_all(dir)
150            .with_context(|| format!("failed to delete {}", dir.display()))?;
151        eprintln!("{}done{}", colors.green, colors.reset);
152    }
153
154    // Execute Step 3: Uninstall binary
155    eprint!(
156        "{}Uninstalling{} mi6 binary... ",
157        colors.green, colors.reset
158    );
159    match method {
160        InstallMethod::Homebrew => uninstall_homebrew()?,
161        InstallMethod::Cargo => uninstall_cargo()?,
162        InstallMethod::Standalone => uninstall_standalone()?,
163    }
164    eprintln!("{}done{}", colors.green, colors.reset);
165
166    eprintln!();
167    eprintln!("{}mi6 has been uninstalled.{}", colors.bold, colors.reset);
168
169    Ok(())
170}
171
172/// Find all frameworks that have mi6 enabled
173fn find_enabled_frameworks() -> Vec<&'static dyn FrameworkAdapter> {
174    // Check global config for all frameworks
175    let mode = FrameworkResolutionMode::Active {
176        local: false,
177        settings_local: false,
178    };
179
180    // Try to resolve with empty list (will find all enabled)
181    match resolve_frameworks(&[], Some(mode)) {
182        Ok(adapters) => adapters,
183        Err(_) => {
184            // Fallback: check each adapter individually
185            all_adapters()
186                .into_iter()
187                .filter(|a| a.has_mi6_hooks(false, false))
188                .collect()
189        }
190    }
191}
192
193/// Get the main mi6 data directory (MI6_DIR_PATH or ~/.mi6)
194fn get_mi6_dir() -> Option<PathBuf> {
195    Config::mi6_dir().ok()
196}
197
198/// Uninstall via Homebrew
199fn uninstall_homebrew() -> Result<()> {
200    let status = Command::new("brew")
201        .args(["uninstall", "mi6"])
202        .status()
203        .context("failed to run brew uninstall")?;
204
205    if !status.success() {
206        anyhow::bail!("brew uninstall failed with exit code: {:?}", status.code());
207    }
208
209    Ok(())
210}
211
212/// Uninstall via Cargo
213fn uninstall_cargo() -> Result<()> {
214    let status = Command::new("cargo")
215        .args(["uninstall", "mi6"])
216        .status()
217        .context("failed to run cargo uninstall")?;
218
219    if !status.success() {
220        anyhow::bail!("cargo uninstall failed with exit code: {:?}", status.code());
221    }
222
223    Ok(())
224}
225
226/// Uninstall standalone binary
227fn uninstall_standalone() -> Result<()> {
228    let exe_path = std::env::current_exe().context("failed to get current executable path")?;
229
230    // On Unix, we can delete ourselves while running
231    // On Windows, this would fail, but mi6 is primarily for Unix systems
232    std::fs::remove_file(&exe_path)
233        .with_context(|| format!("failed to delete {}", exe_path.display()))?;
234
235    Ok(())
236}