Skip to main content

lean_ctx/
uninstall.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4pub fn run() {
5    let home = match dirs::home_dir() {
6        Some(h) => h,
7        None => {
8            eprintln!("  ✗ Could not determine home directory");
9            return;
10        }
11    };
12
13    println!("\n  lean-ctx uninstall\n  ──────────────────────────────────\n");
14
15    let mut removed_any = false;
16
17    removed_any |= remove_shell_hook(&home);
18    removed_any |= remove_mcp_configs(&home);
19    removed_any |= remove_rules_files(&home);
20    removed_any |= remove_hook_files(&home);
21    removed_any |= remove_data_dir(&home);
22
23    println!();
24
25    if removed_any {
26        println!("  ──────────────────────────────────");
27        println!("  lean-ctx configuration removed.\n");
28    } else {
29        println!("  Nothing to remove — lean-ctx was not configured.\n");
30    }
31
32    print_binary_removal_instructions();
33}
34
35fn remove_shell_hook(home: &Path) -> bool {
36    let shell = std::env::var("SHELL").unwrap_or_default();
37    let mut removed = false;
38
39    let rc_files: Vec<PathBuf> = vec![
40        home.join(".zshrc"),
41        home.join(".bashrc"),
42        home.join(".config/fish/config.fish"),
43        #[cfg(windows)]
44        home.join("Documents/PowerShell/Microsoft.PowerShell_profile.ps1"),
45    ];
46
47    for rc in &rc_files {
48        if !rc.exists() {
49            continue;
50        }
51        let content = match fs::read_to_string(rc) {
52            Ok(c) => c,
53            Err(_) => continue,
54        };
55        if !content.contains("lean-ctx") {
56            continue;
57        }
58
59        let cleaned = remove_lean_ctx_block(&content);
60        if cleaned.trim() != content.trim() {
61            let bak = rc.with_extension("lean-ctx.bak");
62            let _ = fs::copy(rc, &bak);
63            if let Err(e) = fs::write(rc, &cleaned) {
64                eprintln!("  ✗ Failed to update {}: {}", rc.display(), e);
65            } else {
66                let short = shorten(rc, home);
67                println!("  ✓ Shell hook removed from {short}");
68                println!("    Backup: {}", shorten(&bak, home));
69                removed = true;
70            }
71        }
72    }
73
74    if !removed && !shell.is_empty() {
75        println!("  · No shell hook found");
76    }
77
78    removed
79}
80
81fn remove_mcp_configs(home: &Path) -> bool {
82    let configs: Vec<(&str, PathBuf)> = vec![
83        ("Cursor", home.join(".cursor/mcp.json")),
84        ("Claude Code", home.join(".claude.json")),
85        ("Windsurf", home.join(".codeium/windsurf/mcp_config.json")),
86        ("Gemini CLI", home.join(".gemini/settings/mcp.json")),
87        (
88            "Antigravity",
89            home.join(".gemini/antigravity/mcp_config.json"),
90        ),
91        ("Codex CLI", home.join(".codex/config.toml")),
92        ("OpenCode", home.join(".config/opencode/opencode.json")),
93        ("Qwen Code", home.join(".qwen/mcp.json")),
94        ("Trae", home.join(".trae/mcp.json")),
95        ("Amazon Q Developer", home.join(".aws/amazonq/mcp.json")),
96        ("JetBrains IDEs", home.join(".jb-mcp.json")),
97        ("AWS Kiro", home.join(".kiro/settings/mcp.json")),
98        ("Verdent", home.join(".verdent/mcp.json")),
99        ("OpenCode", home.join(".opencode/mcp.json")),
100        ("Aider", home.join(".aider/mcp.json")),
101        ("Amp", home.join(".amp/mcp.json")),
102    ];
103
104    let mut removed = false;
105
106    for (name, path) in &configs {
107        if !path.exists() {
108            continue;
109        }
110        let content = match fs::read_to_string(path) {
111            Ok(c) => c,
112            Err(_) => continue,
113        };
114        if !content.contains("lean-ctx") {
115            continue;
116        }
117
118        if let Some(cleaned) = remove_lean_ctx_from_json(&content) {
119            if let Err(e) = fs::write(path, &cleaned) {
120                eprintln!("  ✗ Failed to update {} config: {}", name, e);
121            } else {
122                println!("  ✓ MCP config removed from {name}");
123                removed = true;
124            }
125        }
126    }
127
128    let zed_path = zed_settings_path(home);
129    if zed_path.exists() {
130        if let Ok(content) = fs::read_to_string(&zed_path) {
131            if content.contains("lean-ctx") {
132                println!(
133                    "  ⚠ Zed: manually remove lean-ctx from {}",
134                    shorten(&zed_path, home)
135                );
136            }
137        }
138    }
139
140    let vscode_path = vscode_mcp_path();
141    if vscode_path.exists() {
142        if let Ok(content) = fs::read_to_string(&vscode_path) {
143            if content.contains("lean-ctx") {
144                if let Some(cleaned) = remove_lean_ctx_from_json(&content) {
145                    if let Err(e) = fs::write(&vscode_path, &cleaned) {
146                        eprintln!("  ✗ Failed to update VS Code config: {e}");
147                    } else {
148                        println!("  ✓ MCP config removed from VS Code / Copilot");
149                        removed = true;
150                    }
151                }
152            }
153        }
154    }
155
156    removed
157}
158
159fn remove_rules_files(home: &Path) -> bool {
160    let rules_files: Vec<(&str, PathBuf)> = vec![
161        ("Claude Code", home.join(".claude/CLAUDE.md")),
162        ("Cursor", home.join(".cursor/rules/lean-ctx.mdc")),
163        ("Gemini CLI", home.join(".gemini/GEMINI.md")),
164        (
165            "Gemini CLI (legacy)",
166            home.join(".gemini/rules/lean-ctx.md"),
167        ),
168        ("Codex CLI", home.join(".codex/LEAN-CTX.md")),
169        ("Codex CLI", home.join(".codex/instructions.md")),
170        ("Windsurf", home.join(".codeium/windsurf/rules/lean-ctx.md")),
171        ("Zed", home.join(".config/zed/rules/lean-ctx.md")),
172        ("Cline", home.join(".cline/rules/lean-ctx.md")),
173        ("Roo Code", home.join(".roo/rules/lean-ctx.md")),
174        ("OpenCode", home.join(".config/opencode/rules/lean-ctx.md")),
175        ("Continue", home.join(".continue/rules/lean-ctx.md")),
176        ("Aider", home.join(".aider/rules/lean-ctx.md")),
177        ("Amp", home.join(".ampcoder/rules/lean-ctx.md")),
178        ("Qwen Code", home.join(".qwen/rules/lean-ctx.md")),
179        ("Trae", home.join(".trae/rules/lean-ctx.md")),
180        (
181            "Amazon Q Developer",
182            home.join(".aws/amazonq/rules/lean-ctx.md"),
183        ),
184        ("JetBrains IDEs", home.join(".jb-rules/lean-ctx.md")),
185        (
186            "Antigravity",
187            home.join(".gemini/antigravity/rules/lean-ctx.md"),
188        ),
189        ("Pi Coding Agent", home.join(".pi/rules/lean-ctx.md")),
190        ("AWS Kiro", home.join(".kiro/rules/lean-ctx.md")),
191        ("Verdent", home.join(".verdent/rules/lean-ctx.md")),
192    ];
193
194    let mut removed = false;
195    for (name, path) in &rules_files {
196        if !path.exists() {
197            continue;
198        }
199        if let Ok(content) = fs::read_to_string(path) {
200            if content.contains("lean-ctx") {
201                if let Err(e) = fs::remove_file(path) {
202                    eprintln!("  ✗ Failed to remove {name} rules: {e}");
203                } else {
204                    println!("  ✓ Rules removed from {name}");
205                    removed = true;
206                }
207            }
208        }
209    }
210
211    if !removed {
212        println!("  · No rules files found");
213    }
214    removed
215}
216
217fn remove_hook_files(home: &Path) -> bool {
218    let hook_files: Vec<PathBuf> = vec![
219        home.join(".claude/hooks/lean-ctx-rewrite.sh"),
220        home.join(".claude/hooks/lean-ctx-redirect.sh"),
221        home.join(".cursor/hooks/lean-ctx-rewrite.sh"),
222        home.join(".cursor/hooks/lean-ctx-redirect.sh"),
223        home.join(".gemini/hooks/lean-ctx-rewrite-gemini.sh"),
224        home.join(".gemini/hooks/lean-ctx-redirect-gemini.sh"),
225        home.join(".gemini/hooks/lean-ctx-hook-gemini.sh"),
226        home.join(".codex/hooks/lean-ctx-rewrite-codex.sh"),
227    ];
228
229    let mut removed = false;
230    for path in &hook_files {
231        if path.exists() {
232            if let Err(e) = fs::remove_file(path) {
233                eprintln!("  ✗ Failed to remove hook {}: {e}", path.display());
234            } else {
235                removed = true;
236            }
237        }
238    }
239
240    if removed {
241        println!("  ✓ Hook scripts removed");
242    }
243
244    let hooks_json = home.join(".cursor/hooks.json");
245    if hooks_json.exists() {
246        if let Ok(content) = fs::read_to_string(&hooks_json) {
247            if content.contains("lean-ctx") {
248                if let Err(e) = fs::remove_file(&hooks_json) {
249                    eprintln!("  ✗ Failed to remove Cursor hooks.json: {e}");
250                } else {
251                    println!("  ✓ Cursor hooks.json removed");
252                    removed = true;
253                }
254            }
255        }
256    }
257
258    removed
259}
260
261fn remove_data_dir(home: &Path) -> bool {
262    let data_dir = home.join(".lean-ctx");
263    if !data_dir.exists() {
264        println!("  · No data directory found");
265        return false;
266    }
267
268    match fs::remove_dir_all(&data_dir) {
269        Ok(_) => {
270            println!("  ✓ Data directory removed (~/.lean-ctx/)");
271            true
272        }
273        Err(e) => {
274            eprintln!("  ✗ Failed to remove ~/.lean-ctx/: {e}");
275            false
276        }
277    }
278}
279
280fn print_binary_removal_instructions() {
281    let binary_path = std::env::current_exe()
282        .map(|p| p.display().to_string())
283        .unwrap_or_else(|_| "lean-ctx".to_string());
284
285    println!("  To complete uninstallation, remove the binary:\n");
286
287    if binary_path.contains(".cargo") {
288        println!("    cargo uninstall lean-ctx\n");
289    } else if binary_path.contains("homebrew") || binary_path.contains("Cellar") {
290        println!("    brew uninstall lean-ctx\n");
291    } else {
292        println!("    rm {binary_path}\n");
293    }
294
295    println!("  Then restart your shell.\n");
296}
297
298fn remove_lean_ctx_block(content: &str) -> String {
299    if content.contains("# lean-ctx shell hook — end") {
300        return remove_lean_ctx_block_by_marker(content);
301    }
302    remove_lean_ctx_block_legacy(content)
303}
304
305fn remove_lean_ctx_block_by_marker(content: &str) -> String {
306    let mut result = String::new();
307    let mut in_block = false;
308
309    for line in content.lines() {
310        if !in_block && line.contains("lean-ctx shell hook") && !line.contains("end") {
311            in_block = true;
312            continue;
313        }
314        if in_block {
315            if line.trim() == "# lean-ctx shell hook — end" {
316                in_block = false;
317            }
318            continue;
319        }
320        result.push_str(line);
321        result.push('\n');
322    }
323    result
324}
325
326fn remove_lean_ctx_block_legacy(content: &str) -> String {
327    let mut result = String::new();
328    let mut in_block = false;
329
330    for line in content.lines() {
331        if line.contains("lean-ctx shell hook") {
332            in_block = true;
333            continue;
334        }
335        if in_block {
336            if line.trim() == "fi" || line.trim() == "end" || line.trim().is_empty() {
337                if line.trim() == "fi" || line.trim() == "end" {
338                    in_block = false;
339                }
340                continue;
341            }
342            if !line.starts_with("alias ") && !line.starts_with('\t') && !line.starts_with("if ") {
343                in_block = false;
344                result.push_str(line);
345                result.push('\n');
346            }
347            continue;
348        }
349        result.push_str(line);
350        result.push('\n');
351    }
352    result
353}
354
355fn remove_lean_ctx_from_json(content: &str) -> Option<String> {
356    let mut parsed: serde_json::Value = serde_json::from_str(content).ok()?;
357    let mut modified = false;
358
359    if let Some(servers) = parsed.get_mut("mcpServers").and_then(|s| s.as_object_mut()) {
360        modified |= servers.remove("lean-ctx").is_some();
361    }
362
363    if let Some(servers) = parsed.get_mut("servers").and_then(|s| s.as_object_mut()) {
364        modified |= servers.remove("lean-ctx").is_some();
365    }
366
367    if modified {
368        Some(serde_json::to_string_pretty(&parsed).ok()? + "\n")
369    } else {
370        None
371    }
372}
373
374fn shorten(path: &Path, home: &Path) -> String {
375    match path.strip_prefix(home) {
376        Ok(rel) => format!("~/{}", rel.display()),
377        Err(_) => path.display().to_string(),
378    }
379}
380
381fn zed_settings_path(home: &Path) -> PathBuf {
382    if cfg!(target_os = "macos") {
383        home.join("Library/Application Support/Zed/settings.json")
384    } else {
385        home.join(".config/zed/settings.json")
386    }
387}
388
389fn vscode_mcp_path() -> PathBuf {
390    let home = dirs::home_dir().unwrap_or_default();
391    if cfg!(target_os = "macos") {
392        home.join("Library/Application Support/Code/User/mcp.json")
393    } else if cfg!(target_os = "windows") {
394        if let Ok(appdata) = std::env::var("APPDATA") {
395            return PathBuf::from(appdata).join("Code/User/mcp.json");
396        }
397        home.join("AppData/Roaming/Code/User/mcp.json")
398    } else {
399        home.join(".config/Code/User/mcp.json")
400    }
401}