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        ("Crush", home.join(".config/crush/crush.json")),
103    ];
104
105    let mut removed = false;
106
107    for (name, path) in &configs {
108        if !path.exists() {
109            continue;
110        }
111        let content = match fs::read_to_string(path) {
112            Ok(c) => c,
113            Err(_) => continue,
114        };
115        if !content.contains("lean-ctx") {
116            continue;
117        }
118
119        if let Some(cleaned) = remove_lean_ctx_from_json(&content) {
120            if let Err(e) = fs::write(path, &cleaned) {
121                eprintln!("  ✗ Failed to update {} config: {}", name, e);
122            } else {
123                println!("  ✓ MCP config removed from {name}");
124                removed = true;
125            }
126        }
127    }
128
129    let zed_path = zed_settings_path(home);
130    if zed_path.exists() {
131        if let Ok(content) = fs::read_to_string(&zed_path) {
132            if content.contains("lean-ctx") {
133                println!(
134                    "  ⚠ Zed: manually remove lean-ctx from {}",
135                    shorten(&zed_path, home)
136                );
137            }
138        }
139    }
140
141    let vscode_path = vscode_mcp_path();
142    if vscode_path.exists() {
143        if let Ok(content) = fs::read_to_string(&vscode_path) {
144            if content.contains("lean-ctx") {
145                if let Some(cleaned) = remove_lean_ctx_from_json(&content) {
146                    if let Err(e) = fs::write(&vscode_path, &cleaned) {
147                        eprintln!("  ✗ Failed to update VS Code config: {e}");
148                    } else {
149                        println!("  ✓ MCP config removed from VS Code / Copilot");
150                        removed = true;
151                    }
152                }
153            }
154        }
155    }
156
157    removed
158}
159
160fn remove_rules_files(home: &Path) -> bool {
161    let rules_files: Vec<(&str, PathBuf)> = vec![
162        ("Claude Code", home.join(".claude/CLAUDE.md")),
163        ("Cursor", home.join(".cursor/rules/lean-ctx.mdc")),
164        ("Gemini CLI", home.join(".gemini/GEMINI.md")),
165        (
166            "Gemini CLI (legacy)",
167            home.join(".gemini/rules/lean-ctx.md"),
168        ),
169        ("Codex CLI", home.join(".codex/LEAN-CTX.md")),
170        ("Codex CLI", home.join(".codex/instructions.md")),
171        ("Windsurf", home.join(".codeium/windsurf/rules/lean-ctx.md")),
172        ("Zed", home.join(".config/zed/rules/lean-ctx.md")),
173        ("Cline", home.join(".cline/rules/lean-ctx.md")),
174        ("Roo Code", home.join(".roo/rules/lean-ctx.md")),
175        ("OpenCode", home.join(".config/opencode/rules/lean-ctx.md")),
176        ("Continue", home.join(".continue/rules/lean-ctx.md")),
177        ("Aider", home.join(".aider/rules/lean-ctx.md")),
178        ("Amp", home.join(".ampcoder/rules/lean-ctx.md")),
179        ("Qwen Code", home.join(".qwen/rules/lean-ctx.md")),
180        ("Trae", home.join(".trae/rules/lean-ctx.md")),
181        (
182            "Amazon Q Developer",
183            home.join(".aws/amazonq/rules/lean-ctx.md"),
184        ),
185        ("JetBrains IDEs", home.join(".jb-rules/lean-ctx.md")),
186        (
187            "Antigravity",
188            home.join(".gemini/antigravity/rules/lean-ctx.md"),
189        ),
190        ("Pi Coding Agent", home.join(".pi/rules/lean-ctx.md")),
191        ("AWS Kiro", home.join(".kiro/rules/lean-ctx.md")),
192        ("Verdent", home.join(".verdent/rules/lean-ctx.md")),
193        ("Crush", home.join(".config/crush/rules/lean-ctx.md")),
194    ];
195
196    let mut removed = false;
197    for (name, path) in &rules_files {
198        if !path.exists() {
199            continue;
200        }
201        if let Ok(content) = fs::read_to_string(path) {
202            if content.contains("lean-ctx") {
203                if let Err(e) = fs::remove_file(path) {
204                    eprintln!("  ✗ Failed to remove {name} rules: {e}");
205                } else {
206                    println!("  ✓ Rules removed from {name}");
207                    removed = true;
208                }
209            }
210        }
211    }
212
213    if !removed {
214        println!("  · No rules files found");
215    }
216    removed
217}
218
219fn remove_hook_files(home: &Path) -> bool {
220    let hook_files: Vec<PathBuf> = vec![
221        home.join(".claude/hooks/lean-ctx-rewrite.sh"),
222        home.join(".claude/hooks/lean-ctx-redirect.sh"),
223        home.join(".cursor/hooks/lean-ctx-rewrite.sh"),
224        home.join(".cursor/hooks/lean-ctx-redirect.sh"),
225        home.join(".gemini/hooks/lean-ctx-rewrite-gemini.sh"),
226        home.join(".gemini/hooks/lean-ctx-redirect-gemini.sh"),
227        home.join(".gemini/hooks/lean-ctx-hook-gemini.sh"),
228        home.join(".codex/hooks/lean-ctx-rewrite-codex.sh"),
229    ];
230
231    let mut removed = false;
232    for path in &hook_files {
233        if path.exists() {
234            if let Err(e) = fs::remove_file(path) {
235                eprintln!("  ✗ Failed to remove hook {}: {e}", path.display());
236            } else {
237                removed = true;
238            }
239        }
240    }
241
242    if removed {
243        println!("  ✓ Hook scripts removed");
244    }
245
246    let hooks_json = home.join(".cursor/hooks.json");
247    if hooks_json.exists() {
248        if let Ok(content) = fs::read_to_string(&hooks_json) {
249            if content.contains("lean-ctx") {
250                if let Err(e) = fs::remove_file(&hooks_json) {
251                    eprintln!("  ✗ Failed to remove Cursor hooks.json: {e}");
252                } else {
253                    println!("  ✓ Cursor hooks.json removed");
254                    removed = true;
255                }
256            }
257        }
258    }
259
260    removed
261}
262
263fn remove_data_dir(home: &Path) -> bool {
264    let data_dir = home.join(".lean-ctx");
265    if !data_dir.exists() {
266        println!("  · No data directory found");
267        return false;
268    }
269
270    match fs::remove_dir_all(&data_dir) {
271        Ok(_) => {
272            println!("  ✓ Data directory removed (~/.lean-ctx/)");
273            true
274        }
275        Err(e) => {
276            eprintln!("  ✗ Failed to remove ~/.lean-ctx/: {e}");
277            false
278        }
279    }
280}
281
282fn print_binary_removal_instructions() {
283    let binary_path = std::env::current_exe()
284        .map(|p| p.display().to_string())
285        .unwrap_or_else(|_| "lean-ctx".to_string());
286
287    println!("  To complete uninstallation, remove the binary:\n");
288
289    if binary_path.contains(".cargo") {
290        println!("    cargo uninstall lean-ctx\n");
291    } else if binary_path.contains("homebrew") || binary_path.contains("Cellar") {
292        println!("    brew uninstall lean-ctx\n");
293    } else {
294        println!("    rm {binary_path}\n");
295    }
296
297    println!("  Then restart your shell.\n");
298}
299
300fn remove_lean_ctx_block(content: &str) -> String {
301    if content.contains("# lean-ctx shell hook — end") {
302        return remove_lean_ctx_block_by_marker(content);
303    }
304    remove_lean_ctx_block_legacy(content)
305}
306
307fn remove_lean_ctx_block_by_marker(content: &str) -> String {
308    let mut result = String::new();
309    let mut in_block = false;
310
311    for line in content.lines() {
312        if !in_block && line.contains("lean-ctx shell hook") && !line.contains("end") {
313            in_block = true;
314            continue;
315        }
316        if in_block {
317            if line.trim() == "# lean-ctx shell hook — end" {
318                in_block = false;
319            }
320            continue;
321        }
322        result.push_str(line);
323        result.push('\n');
324    }
325    result
326}
327
328fn remove_lean_ctx_block_legacy(content: &str) -> String {
329    let mut result = String::new();
330    let mut in_block = false;
331
332    for line in content.lines() {
333        if line.contains("lean-ctx shell hook") {
334            in_block = true;
335            continue;
336        }
337        if in_block {
338            if line.trim() == "fi" || line.trim() == "end" || line.trim().is_empty() {
339                if line.trim() == "fi" || line.trim() == "end" {
340                    in_block = false;
341                }
342                continue;
343            }
344            if !line.starts_with("alias ") && !line.starts_with('\t') && !line.starts_with("if ") {
345                in_block = false;
346                result.push_str(line);
347                result.push('\n');
348            }
349            continue;
350        }
351        result.push_str(line);
352        result.push('\n');
353    }
354    result
355}
356
357fn remove_lean_ctx_from_json(content: &str) -> Option<String> {
358    let mut parsed: serde_json::Value = serde_json::from_str(content).ok()?;
359    let mut modified = false;
360
361    if let Some(servers) = parsed.get_mut("mcpServers").and_then(|s| s.as_object_mut()) {
362        modified |= servers.remove("lean-ctx").is_some();
363    }
364
365    if let Some(servers) = parsed.get_mut("servers").and_then(|s| s.as_object_mut()) {
366        modified |= servers.remove("lean-ctx").is_some();
367    }
368
369    if modified {
370        Some(serde_json::to_string_pretty(&parsed).ok()? + "\n")
371    } else {
372        None
373    }
374}
375
376fn shorten(path: &Path, home: &Path) -> String {
377    match path.strip_prefix(home) {
378        Ok(rel) => format!("~/{}", rel.display()),
379        Err(_) => path.display().to_string(),
380    }
381}
382
383fn zed_settings_path(home: &Path) -> PathBuf {
384    if cfg!(target_os = "macos") {
385        home.join("Library/Application Support/Zed/settings.json")
386    } else {
387        home.join(".config/zed/settings.json")
388    }
389}
390
391fn vscode_mcp_path() -> PathBuf {
392    let home = dirs::home_dir().unwrap_or_default();
393    if cfg!(target_os = "macos") {
394        home.join("Library/Application Support/Code/User/mcp.json")
395    } else if cfg!(target_os = "windows") {
396        if let Ok(appdata) = std::env::var("APPDATA") {
397            return PathBuf::from(appdata).join("Code/User/mcp.json");
398        }
399        home.join("AppData/Roaming/Code/User/mcp.json")
400    } else {
401        home.join(".config/Code/User/mcp.json")
402    }
403}