Skip to main content

atomcode_core/uninstall/
mod.rs

1//! Uninstall flow shared between the `atomcode uninstall` subcommand
2//! and `scripts/uninstall.sh` / `uninstall.ps1`.
3//!
4//! Spec: docs/superpowers/specs/2026-05-08-uninstall-design.md
5
6pub mod actions;
7pub mod paths;
8pub mod scan;
9
10use std::path::PathBuf;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum Group {
14    /// Binary + PATH edit. Required: declining = abort.
15    Binary,
16    /// Credentials & global config (auth.toml, mcp.json, config.toml, ATOMCODE.md).
17    Credentials,
18    /// Local state & extensions (history, telemetry, plugins, commands, skills, staged).
19    State,
20}
21
22#[derive(Debug, Clone)]
23pub struct Item {
24    pub group: Group,
25    pub path: PathBuf,
26    pub size_bytes: u64,
27    pub note: &'static str,
28    pub needs_privilege: bool,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct Decisions {
33    pub binary: bool,
34    pub credentials: bool,
35    pub state: bool,
36}
37
38impl Decisions {
39    pub const DEFAULTS: Self = Self {
40        binary: true,
41        credentials: false,
42        state: true,
43    };
44    pub const PURGE: Self = Self {
45        binary: true,
46        credentials: true,
47        state: true,
48    };
49    pub const KEEP_DATA: Self = Self {
50        binary: true,
51        credentials: false,
52        state: false,
53    };
54}
55
56// ── Outcome ───────────────────────────────────────────────────────────────────
57
58#[derive(Debug, Default)]
59pub struct Outcome {
60    pub removed: Vec<PathBuf>,
61    pub kept: Vec<PathBuf>,
62    pub failed: Vec<(PathBuf, String)>,
63    pub backups: Vec<PathBuf>,
64}
65
66// ── ExecuteContext ────────────────────────────────────────────────────────────
67
68#[derive(Default)]
69pub struct ExecuteContext {
70    /// Pairs of (rc file path, install prefix) to clean.
71    pub rc_files: Vec<(PathBuf, String)>,
72    /// Literal install dir string for Windows registry filter (e.g., "%LOCALAPPDATA%\\AtomCode").
73    #[cfg(windows)]
74    pub windows_install_dir_literal: Option<String>,
75    /// Expanded install dir string for Windows registry filter (e.g., "C:\\Users\\theo\\AppData\\Local\\AtomCode").
76    #[cfg(windows)]
77    pub windows_install_dir_expanded: Option<String>,
78}
79
80// ── execute() ────────────────────────────────────────────────────────────────
81
82/// Execute the uninstall plan in spec-prescribed order:
83/// 1) State (least load-bearing)
84/// 2) Credentials
85/// 3) rmdir $ATOMCODE_HOME if empty
86/// 4) PATH cleanup (rc / Windows User PATH)
87/// 5) Binary self-update artifacts
88/// 6) Self-delete (last)
89pub fn execute(
90    plan: &scan::Plan,
91    decisions: Decisions,
92    self_delete: &dyn actions::SelfDeleteStrategy,
93    ctx: Option<ExecuteContext>,
94) -> std::io::Result<Outcome> {
95    let mut out = Outcome::default();
96
97    let want = |g: Group| -> bool {
98        match g {
99            Group::Binary => decisions.binary,
100            Group::Credentials => decisions.credentials,
101            Group::State => decisions.state,
102        }
103    };
104
105    // --- 1. State ---
106    if want(Group::State) {
107        for it in plan.items.iter().filter(|i| i.group == Group::State) {
108            match actions::remove_path(&it.path, it.needs_privilege) {
109                Ok(()) => out.removed.push(it.path.clone()),
110                Err(e) => out.failed.push((it.path.clone(), e.to_string())),
111            }
112        }
113    } else {
114        for it in plan.items.iter().filter(|i| i.group == Group::State) {
115            out.kept.push(it.path.clone());
116        }
117    }
118
119    // --- 2. Credentials ---
120    if want(Group::Credentials) {
121        for it in plan.items.iter().filter(|i| i.group == Group::Credentials) {
122            match actions::remove_path(&it.path, it.needs_privilege) {
123                Ok(()) => out.removed.push(it.path.clone()),
124                Err(e) => out.failed.push((it.path.clone(), e.to_string())),
125            }
126        }
127    } else {
128        for it in plan.items.iter().filter(|i| i.group == Group::Credentials) {
129            out.kept.push(it.path.clone());
130        }
131    }
132
133    // --- 3. rmdir $ATOMCODE_HOME/ if empty ---
134    if plan.atomcode_dir.exists() {
135        let _ = std::fs::remove_dir(&plan.atomcode_dir); // ignore non-empty error
136    }
137
138    // --- 4. PATH cleanup ---
139    if decisions.binary {
140        if let Some(c) = ctx.as_ref() {
141            for (rc, prefix) in &c.rc_files {
142                match actions::apply_unix_path_cleanup(rc, prefix) {
143                    Ok(r) if r.modified => {
144                        out.removed.push(rc.clone());
145                        if let Some(b) = r.backup_path {
146                            out.backups.push(b);
147                        }
148                    }
149                    Ok(_) => {}
150                    Err(e) => out.failed.push((rc.clone(), e.to_string())),
151                }
152            }
153            #[cfg(windows)]
154            if let (Some(lit), Some(exp)) = (
155                c.windows_install_dir_literal.as_ref(),
156                c.windows_install_dir_expanded.as_ref(),
157            ) {
158                match actions::apply_windows_path_cleanup(lit, exp) {
159                    Ok(true) => out.removed.push(PathBuf::from("HKCU\\Environment\\Path")),
160                    Ok(false) => {}
161                    Err(e) => out
162                        .failed
163                        .push((PathBuf::from("HKCU\\Environment\\Path"), e.to_string())),
164                }
165            }
166        }
167    }
168
169    // --- 5. Binary group EXCEPT the binary itself ---
170    if decisions.binary {
171        for it in plan
172            .items
173            .iter()
174            .filter(|i| i.group == Group::Binary && i.path != plan.binary_path)
175        {
176            match actions::remove_path(&it.path, it.needs_privilege) {
177                Ok(()) => out.removed.push(it.path.clone()),
178                Err(e) => out.failed.push((it.path.clone(), e.to_string())),
179            }
180        }
181        // --- 6. Self-delete (last) ---
182        match self_delete.run(&plan.binary_path) {
183            Ok(()) => out.removed.push(plan.binary_path.clone()),
184            Err(e) => out.failed.push((plan.binary_path.clone(), e.to_string())),
185        }
186    } else {
187        for it in plan.items.iter().filter(|i| i.group == Group::Binary) {
188            out.kept.push(it.path.clone());
189        }
190    }
191
192    Ok(out)
193}