Skip to main content

lean_ctx/uninstall/
mod.rs

1mod agents;
2mod parsers;
3
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use agents::{
8    remove_hook_files, remove_mcp_configs, remove_project_agent_files, remove_rules_files,
9    remove_shell_hook,
10};
11
12pub(super) fn backup_before_modify(path: &Path, dry_run: bool) {
13    if dry_run {
14        return;
15    }
16    if path.exists() {
17        let bak = bak_path_for(path);
18        let _ = fs::copy(path, &bak);
19    }
20}
21
22pub(super) fn bak_path_for(path: &Path) -> PathBuf {
23    let filename = path.file_name().unwrap_or_default().to_string_lossy();
24    path.with_file_name(format!("{filename}.lean-ctx.bak"))
25}
26
27fn cleanup_bak(path: &Path) {
28    let bak = bak_path_for(path);
29    if bak.exists() {
30        let _ = fs::remove_file(&bak);
31    }
32}
33
34pub(super) fn shorten(path: &Path, home: &Path) -> String {
35    match path.strip_prefix(home) {
36        Ok(rel) => format!("~/{}", rel.display()),
37        Err(_) => path.display().to_string(),
38    }
39}
40
41pub(super) fn copilot_instructions_path(home: &Path) -> PathBuf {
42    #[cfg(target_os = "macos")]
43    {
44        return home.join("Library/Application Support/Code/User/github-copilot-instructions.md");
45    }
46    #[cfg(target_os = "linux")]
47    {
48        return home.join(".config/Code/User/github-copilot-instructions.md");
49    }
50    #[cfg(target_os = "windows")]
51    {
52        if let Ok(appdata) = std::env::var("APPDATA") {
53            return PathBuf::from(appdata).join("Code/User/github-copilot-instructions.md");
54        }
55    }
56    #[allow(unreachable_code)]
57    home.join(".config/Code/User/github-copilot-instructions.md")
58}
59
60/// Write `content` to `path` only if not in dry-run mode.
61pub(super) fn safe_write(path: &Path, content: &str, dry_run: bool) -> Result<(), std::io::Error> {
62    if dry_run {
63        return Ok(());
64    }
65    fs::write(path, content)?;
66    // If we successfully wrote the cleaned file, the backup is no longer needed.
67    cleanup_bak(path);
68    Ok(())
69}
70
71/// Remove `path` only if not in dry-run mode.
72pub(super) fn safe_remove(path: &Path, dry_run: bool) -> Result<(), std::io::Error> {
73    if dry_run {
74        return Ok(());
75    }
76    fs::remove_file(path)?;
77    // If we successfully removed the file, also remove its backup.
78    cleanup_bak(path);
79    Ok(())
80}
81
82// ---------------------------------------------------------------------------
83// Main entry
84// ---------------------------------------------------------------------------
85
86pub fn run(dry_run: bool) {
87    let Some(home) = dirs::home_dir() else {
88        tracing::warn!("Could not determine home directory");
89        return;
90    };
91
92    if dry_run {
93        println!("\n  lean-ctx uninstall --dry-run\n  ──────────────────────────────────\n");
94        println!("  Preview mode — no files will be modified.\n");
95    } else {
96        println!("\n  lean-ctx uninstall\n  ──────────────────────────────────\n");
97    }
98
99    let mut removed_any = false;
100
101    removed_any |= remove_shell_hook(&home, dry_run);
102    if dry_run {
103        crate::proxy_setup::preview_proxy_cleanup(&home);
104    } else {
105        crate::proxy_setup::uninstall_proxy_env(&home, false);
106    }
107    removed_any |= remove_mcp_configs(&home, dry_run);
108    removed_any |= remove_rules_files(&home, dry_run);
109    removed_any |= remove_hook_files(&home, dry_run);
110    removed_any |= remove_project_agent_files(dry_run);
111
112    if !dry_run {
113        cleanup_bak_files(&home);
114    }
115
116    removed_any |= remove_data_dir(&home, dry_run);
117
118    println!();
119
120    if removed_any {
121        println!("  ──────────────────────────────────");
122        if dry_run {
123            println!(
124                "  The above changes WOULD be applied.\n  Run `lean-ctx uninstall` to execute.\n"
125            );
126        } else {
127            println!("  lean-ctx configuration removed.\n");
128        }
129    } else {
130        println!("  Nothing to remove — lean-ctx was not configured.\n");
131    }
132
133    if !dry_run {
134        print_binary_removal_instructions();
135    }
136}
137
138// ---------------------------------------------------------------------------
139// Marked block removal (for AGENTS.md, SharedMarkdown)
140// ---------------------------------------------------------------------------
141
142pub(super) fn remove_marked_block(content: &str, start: &str, end: &str) -> String {
143    let s = content.find(start);
144    let e = content.find(end);
145    match (s, e) {
146        (Some(si), Some(ei)) if ei >= si => {
147            let after_end = ei + end.len();
148            let before = &content[..si];
149            let after = &content[after_end..];
150            let mut out = String::new();
151            out.push_str(before.trim_end_matches('\n'));
152            out.push('\n');
153            if !after.trim().is_empty() {
154                out.push('\n');
155                out.push_str(after.trim_start_matches('\n'));
156            }
157            out
158        }
159        _ => content.to_string(),
160    }
161}
162
163// ---------------------------------------------------------------------------
164// Data directory
165// ---------------------------------------------------------------------------
166
167fn remove_data_dir(home: &Path, dry_run: bool) -> bool {
168    let data_dir = home.join(".lean-ctx");
169    if !data_dir.exists() {
170        println!("  · No data directory found");
171        return false;
172    }
173
174    if dry_run {
175        println!("  Would remove Data directory (~/.lean-ctx/)");
176        return true;
177    }
178
179    match fs::remove_dir_all(&data_dir) {
180        Ok(()) => {
181            println!("  ✓ Data directory removed (~/.lean-ctx/)");
182            true
183        }
184        Err(e) => {
185            tracing::warn!("Failed to remove ~/.lean-ctx/: {e}");
186            false
187        }
188    }
189}
190
191// ---------------------------------------------------------------------------
192// .bak cleanup: remove orphaned backup files after successful surgical removal
193// ---------------------------------------------------------------------------
194
195fn cleanup_bak_files(home: &Path) {
196    let dirs_to_scan: Vec<PathBuf> = vec![
197        home.join(".cursor"),
198        home.join(".claude"),
199        crate::core::editor_registry::claude_state_dir(home),
200        home.join(".gemini"),
201        home.join(".gemini/antigravity"),
202        home.join(".codex"),
203        home.join(".codeium"),
204        home.join(".codeium/windsurf"),
205        home.join(".config/opencode"),
206        home.join(".config/amp"),
207        home.join(".config/crush"),
208        home.join(".config/zed"),
209        home.join(".qwen"),
210        home.join(".trae"),
211        home.join(".aws/amazonq"),
212        home.join(".kiro"),
213        home.join(".kiro/settings"),
214        home.join(".ampcoder"),
215        home.join(".pi"),
216        home.join(".pi/agent"),
217        home.join(".hermes"),
218        home.join(".verdent"),
219        home.join(".cline"),
220        home.join(".roo"),
221        home.join(".continue"),
222        home.join(".jb-rules"),
223    ];
224
225    let mut cleaned = 0;
226    for dir in &dirs_to_scan {
227        if !dir.exists() {
228            continue;
229        }
230        if let Ok(entries) = fs::read_dir(dir) {
231            for entry in entries.flatten() {
232                let name = entry.file_name();
233                let name_str = name.to_string_lossy();
234                if name_str.ends_with(".lean-ctx.tmp") {
235                    let _ = fs::remove_file(entry.path());
236                    cleaned += 1;
237                    continue;
238                }
239                if name_str.ends_with(".lean-ctx.bak") {
240                    let original_name = name_str.trim_end_matches(".lean-ctx.bak");
241                    let original = entry.path().with_file_name(original_name);
242                    if original.exists() {
243                        // Only remove backups if the original is already clean.
244                        match fs::read_to_string(&original) {
245                            Ok(c) if !c.contains("lean-ctx") => {
246                                let _ = fs::remove_file(entry.path());
247                                cleaned += 1;
248                            }
249                            _ => {}
250                        }
251                    } else {
252                        // If the original is gone, the backup is no longer needed.
253                        let _ = fs::remove_file(entry.path());
254                        cleaned += 1;
255                    }
256                }
257            }
258        }
259    }
260
261    // Also clean shell RC backups
262    let rc_baks = [
263        home.join(".zshrc.lean-ctx.bak"),
264        home.join(".zshenv.lean-ctx.bak"),
265        home.join(".bashrc.lean-ctx.bak"),
266        home.join(".bashenv.lean-ctx.bak"),
267    ];
268    for bak in &rc_baks {
269        if bak.exists() {
270            let original_name = bak
271                .file_name()
272                .unwrap_or_default()
273                .to_string_lossy()
274                .trim_end_matches(".lean-ctx.bak")
275                .to_string();
276            let original = bak.with_file_name(original_name);
277            if original.exists() {
278                if let Ok(c) = fs::read_to_string(&original) {
279                    if !c.contains("lean-ctx") {
280                        let _ = fs::remove_file(bak);
281                        cleaned += 1;
282                    }
283                }
284            } else {
285                let _ = fs::remove_file(bak);
286                cleaned += 1;
287            }
288        }
289    }
290
291    if cleaned > 0 {
292        println!("  ✓ Cleaned up {cleaned} backup file(s)");
293    }
294}
295
296// ---------------------------------------------------------------------------
297// Binary removal instructions
298// ---------------------------------------------------------------------------
299
300fn print_binary_removal_instructions() {
301    let binary_path = std::env::current_exe()
302        .map_or_else(|_| "lean-ctx".to_string(), |p| p.display().to_string());
303
304    println!("  To complete uninstallation, remove the binary:\n");
305
306    if binary_path.contains(".cargo") {
307        println!("    cargo uninstall lean-ctx\n");
308    } else if binary_path.contains("homebrew") || binary_path.contains("Cellar") {
309        println!("    brew uninstall lean-ctx\n");
310    } else {
311        println!("    rm {binary_path}\n");
312    }
313
314    println!("  Then restart your shell.\n");
315}