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::uninstall_proxy_env(&home, false);
104    }
105    removed_any |= remove_mcp_configs(&home, dry_run);
106    removed_any |= remove_rules_files(&home, dry_run);
107    removed_any |= remove_hook_files(&home, dry_run);
108    removed_any |= remove_project_agent_files(dry_run);
109
110    if !dry_run {
111        cleanup_bak_files(&home);
112    }
113
114    removed_any |= remove_data_dir(&home, dry_run);
115
116    println!();
117
118    if removed_any {
119        println!("  ──────────────────────────────────");
120        if dry_run {
121            println!(
122                "  The above changes WOULD be applied.\n  Run `lean-ctx uninstall` to execute.\n"
123            );
124        } else {
125            println!("  lean-ctx configuration removed.\n");
126        }
127    } else {
128        println!("  Nothing to remove — lean-ctx was not configured.\n");
129    }
130
131    if !dry_run {
132        print_binary_removal_instructions();
133    }
134}
135
136// ---------------------------------------------------------------------------
137// Marked block removal (for AGENTS.md, SharedMarkdown)
138// ---------------------------------------------------------------------------
139
140pub(super) fn remove_marked_block(content: &str, start: &str, end: &str) -> String {
141    let s = content.find(start);
142    let e = content.find(end);
143    match (s, e) {
144        (Some(si), Some(ei)) if ei >= si => {
145            let after_end = ei + end.len();
146            let before = &content[..si];
147            let after = &content[after_end..];
148            let mut out = String::new();
149            out.push_str(before.trim_end_matches('\n'));
150            out.push('\n');
151            if !after.trim().is_empty() {
152                out.push('\n');
153                out.push_str(after.trim_start_matches('\n'));
154            }
155            out
156        }
157        _ => content.to_string(),
158    }
159}
160
161// ---------------------------------------------------------------------------
162// Data directory
163// ---------------------------------------------------------------------------
164
165fn remove_data_dir(home: &Path, dry_run: bool) -> bool {
166    let data_dir = home.join(".lean-ctx");
167    if !data_dir.exists() {
168        println!("  · No data directory found");
169        return false;
170    }
171
172    if dry_run {
173        println!("  Would remove Data directory (~/.lean-ctx/)");
174        return true;
175    }
176
177    match fs::remove_dir_all(&data_dir) {
178        Ok(()) => {
179            println!("  ✓ Data directory removed (~/.lean-ctx/)");
180            true
181        }
182        Err(e) => {
183            tracing::warn!("Failed to remove ~/.lean-ctx/: {e}");
184            false
185        }
186    }
187}
188
189// ---------------------------------------------------------------------------
190// .bak cleanup: remove orphaned backup files after successful surgical removal
191// ---------------------------------------------------------------------------
192
193fn cleanup_bak_files(home: &Path) {
194    let dirs_to_scan: Vec<PathBuf> = vec![
195        home.join(".cursor"),
196        home.join(".claude"),
197        crate::core::editor_registry::claude_state_dir(home),
198        home.join(".gemini"),
199        home.join(".gemini/antigravity"),
200        home.join(".codex"),
201        home.join(".codeium"),
202        home.join(".codeium/windsurf"),
203        home.join(".config/opencode"),
204        home.join(".config/amp"),
205        home.join(".config/crush"),
206        home.join(".config/zed"),
207        home.join(".qwen"),
208        home.join(".trae"),
209        home.join(".aws/amazonq"),
210        home.join(".kiro"),
211        home.join(".kiro/settings"),
212        home.join(".ampcoder"),
213        home.join(".pi"),
214        home.join(".pi/agent"),
215        home.join(".hermes"),
216        home.join(".verdent"),
217        home.join(".cline"),
218        home.join(".roo"),
219        home.join(".continue"),
220        home.join(".jb-rules"),
221    ];
222
223    let mut cleaned = 0;
224    for dir in &dirs_to_scan {
225        if !dir.exists() {
226            continue;
227        }
228        if let Ok(entries) = fs::read_dir(dir) {
229            for entry in entries.flatten() {
230                let name = entry.file_name();
231                let name_str = name.to_string_lossy();
232                if name_str.ends_with(".lean-ctx.tmp") {
233                    let _ = fs::remove_file(entry.path());
234                    cleaned += 1;
235                    continue;
236                }
237                if name_str.ends_with(".lean-ctx.bak") {
238                    let original_name = name_str.trim_end_matches(".lean-ctx.bak");
239                    let original = entry.path().with_file_name(original_name);
240                    if original.exists() {
241                        // Only remove backups if the original is already clean.
242                        match fs::read_to_string(&original) {
243                            Ok(c) if !c.contains("lean-ctx") => {
244                                let _ = fs::remove_file(entry.path());
245                                cleaned += 1;
246                            }
247                            _ => {}
248                        }
249                    } else {
250                        // If the original is gone, the backup is no longer needed.
251                        let _ = fs::remove_file(entry.path());
252                        cleaned += 1;
253                    }
254                }
255            }
256        }
257    }
258
259    // Also clean shell RC backups
260    let rc_baks = [
261        home.join(".zshrc.lean-ctx.bak"),
262        home.join(".zshenv.lean-ctx.bak"),
263        home.join(".bashrc.lean-ctx.bak"),
264        home.join(".bashenv.lean-ctx.bak"),
265    ];
266    for bak in &rc_baks {
267        if bak.exists() {
268            let original_name = bak
269                .file_name()
270                .unwrap_or_default()
271                .to_string_lossy()
272                .trim_end_matches(".lean-ctx.bak")
273                .to_string();
274            let original = bak.with_file_name(original_name);
275            if original.exists() {
276                if let Ok(c) = fs::read_to_string(&original) {
277                    if !c.contains("lean-ctx") {
278                        let _ = fs::remove_file(bak);
279                        cleaned += 1;
280                    }
281                }
282            } else {
283                let _ = fs::remove_file(bak);
284                cleaned += 1;
285            }
286        }
287    }
288
289    if cleaned > 0 {
290        println!("  ✓ Cleaned up {cleaned} backup file(s)");
291    }
292}
293
294// ---------------------------------------------------------------------------
295// Binary removal instructions
296// ---------------------------------------------------------------------------
297
298fn print_binary_removal_instructions() {
299    let binary_path = std::env::current_exe()
300        .map_or_else(|_| "lean-ctx".to_string(), |p| p.display().to_string());
301
302    println!("  To complete uninstallation, remove the binary:\n");
303
304    if binary_path.contains(".cargo") {
305        println!("    cargo uninstall lean-ctx\n");
306    } else if binary_path.contains("homebrew") || binary_path.contains("Cellar") {
307        println!("    brew uninstall lean-ctx\n");
308    } else {
309        println!("    rm {binary_path}\n");
310    }
311
312    println!("  Then restart your shell.\n");
313}