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_data_dir(&home);
20
21    println!();
22
23    if removed_any {
24        println!("  ──────────────────────────────────");
25        println!("  lean-ctx configuration removed.\n");
26    } else {
27        println!("  Nothing to remove — lean-ctx was not configured.\n");
28    }
29
30    print_binary_removal_instructions();
31}
32
33fn remove_shell_hook(home: &Path) -> bool {
34    let shell = std::env::var("SHELL").unwrap_or_default();
35    let mut removed = false;
36
37    let rc_files: Vec<PathBuf> = vec![
38        home.join(".zshrc"),
39        home.join(".bashrc"),
40        home.join(".config/fish/config.fish"),
41        #[cfg(windows)]
42        home.join("Documents/PowerShell/Microsoft.PowerShell_profile.ps1"),
43    ];
44
45    for rc in &rc_files {
46        if !rc.exists() {
47            continue;
48        }
49        let content = match fs::read_to_string(rc) {
50            Ok(c) => c,
51            Err(_) => continue,
52        };
53        if !content.contains("lean-ctx") {
54            continue;
55        }
56
57        let cleaned = remove_lean_ctx_block(&content);
58        if cleaned.trim() != content.trim() {
59            if let Err(e) = fs::write(rc, &cleaned) {
60                eprintln!("  ✗ Failed to update {}: {}", rc.display(), e);
61            } else {
62                let short = shorten(rc, home);
63                println!("  ✓ Shell hook removed from {short}");
64                removed = true;
65            }
66        }
67    }
68
69    if !removed && !shell.is_empty() {
70        println!("  · No shell hook found");
71    }
72
73    removed
74}
75
76fn remove_mcp_configs(home: &Path) -> bool {
77    let configs: Vec<(&str, PathBuf)> = vec![
78        ("Cursor", home.join(".cursor/mcp.json")),
79        ("Claude Code", home.join(".claude.json")),
80        ("Windsurf", home.join(".codeium/windsurf/mcp_config.json")),
81        ("Gemini CLI", home.join(".gemini/settings/mcp.json")),
82        (
83            "Antigravity",
84            home.join(".gemini/antigravity/mcp_config.json"),
85        ),
86        ("Codex CLI", home.join(".codex/config.toml")),
87        ("OpenCode", home.join(".config/opencode/opencode.json")),
88        ("Qwen Code", home.join(".qwen/mcp.json")),
89        ("Trae", home.join(".trae/mcp.json")),
90        ("Amazon Q Developer", home.join(".aws/amazonq/mcp.json")),
91        ("JetBrains IDEs", home.join(".jb-mcp.json")),
92    ];
93
94    let mut removed = false;
95
96    for (name, path) in &configs {
97        if !path.exists() {
98            continue;
99        }
100        let content = match fs::read_to_string(path) {
101            Ok(c) => c,
102            Err(_) => continue,
103        };
104        if !content.contains("lean-ctx") {
105            continue;
106        }
107
108        if let Some(cleaned) = remove_lean_ctx_from_json(&content) {
109            if let Err(e) = fs::write(path, &cleaned) {
110                eprintln!("  ✗ Failed to update {} config: {}", name, e);
111            } else {
112                println!("  ✓ MCP config removed from {name}");
113                removed = true;
114            }
115        }
116    }
117
118    let zed_path = zed_settings_path(home);
119    if zed_path.exists() {
120        if let Ok(content) = fs::read_to_string(&zed_path) {
121            if content.contains("lean-ctx") {
122                println!(
123                    "  ⚠ Zed: manually remove lean-ctx from {}",
124                    shorten(&zed_path, home)
125                );
126            }
127        }
128    }
129
130    let vscode_path = vscode_mcp_path();
131    if vscode_path.exists() {
132        if let Ok(content) = fs::read_to_string(&vscode_path) {
133            if content.contains("lean-ctx") {
134                if let Some(cleaned) = remove_lean_ctx_from_json(&content) {
135                    if let Err(e) = fs::write(&vscode_path, &cleaned) {
136                        eprintln!("  ✗ Failed to update VS Code config: {e}");
137                    } else {
138                        println!("  ✓ MCP config removed from VS Code / Copilot");
139                        removed = true;
140                    }
141                }
142            }
143        }
144    }
145
146    removed
147}
148
149fn remove_data_dir(home: &Path) -> bool {
150    let data_dir = home.join(".lean-ctx");
151    if !data_dir.exists() {
152        println!("  · No data directory found");
153        return false;
154    }
155
156    match fs::remove_dir_all(&data_dir) {
157        Ok(_) => {
158            println!("  ✓ Data directory removed (~/.lean-ctx/)");
159            true
160        }
161        Err(e) => {
162            eprintln!("  ✗ Failed to remove ~/.lean-ctx/: {e}");
163            false
164        }
165    }
166}
167
168fn print_binary_removal_instructions() {
169    let binary_path = std::env::current_exe()
170        .map(|p| p.display().to_string())
171        .unwrap_or_else(|_| "lean-ctx".to_string());
172
173    println!("  To complete uninstallation, remove the binary:\n");
174
175    if binary_path.contains(".cargo") {
176        println!("    cargo uninstall lean-ctx\n");
177    } else if binary_path.contains("homebrew") || binary_path.contains("Cellar") {
178        println!("    brew uninstall lean-ctx\n");
179    } else {
180        println!("    rm {binary_path}\n");
181    }
182
183    println!("  Then restart your shell.\n");
184}
185
186fn remove_lean_ctx_block(content: &str) -> String {
187    if content.contains("# lean-ctx shell hook — end") {
188        return remove_lean_ctx_block_by_marker(content);
189    }
190    remove_lean_ctx_block_legacy(content)
191}
192
193fn remove_lean_ctx_block_by_marker(content: &str) -> String {
194    let mut result = String::new();
195    let mut in_block = false;
196
197    for line in content.lines() {
198        if !in_block && line.contains("lean-ctx shell hook") && !line.contains("end") {
199            in_block = true;
200            continue;
201        }
202        if in_block {
203            if line.trim() == "# lean-ctx shell hook — end" {
204                in_block = false;
205            }
206            continue;
207        }
208        result.push_str(line);
209        result.push('\n');
210    }
211    result
212}
213
214fn remove_lean_ctx_block_legacy(content: &str) -> String {
215    let mut result = String::new();
216    let mut in_block = false;
217
218    for line in content.lines() {
219        if line.contains("lean-ctx shell hook") {
220            in_block = true;
221            continue;
222        }
223        if in_block {
224            if line.trim() == "fi" || line.trim() == "end" || line.trim().is_empty() {
225                if line.trim() == "fi" || line.trim() == "end" {
226                    in_block = false;
227                }
228                continue;
229            }
230            if !line.starts_with("alias ") && !line.starts_with('\t') && !line.starts_with("if ") {
231                in_block = false;
232                result.push_str(line);
233                result.push('\n');
234            }
235            continue;
236        }
237        result.push_str(line);
238        result.push('\n');
239    }
240    result
241}
242
243fn remove_lean_ctx_from_json(content: &str) -> Option<String> {
244    let mut parsed: serde_json::Value = serde_json::from_str(content).ok()?;
245
246    let modified =
247        if let Some(servers) = parsed.get_mut("mcpServers").and_then(|s| s.as_object_mut()) {
248            servers.remove("lean-ctx").is_some()
249        } else {
250            false
251        };
252
253    if modified {
254        Some(serde_json::to_string_pretty(&parsed).ok()? + "\n")
255    } else {
256        None
257    }
258}
259
260fn shorten(path: &Path, home: &Path) -> String {
261    match path.strip_prefix(home) {
262        Ok(rel) => format!("~/{}", rel.display()),
263        Err(_) => path.display().to_string(),
264    }
265}
266
267fn zed_settings_path(home: &Path) -> PathBuf {
268    if cfg!(target_os = "macos") {
269        home.join("Library/Application Support/Zed/settings.json")
270    } else {
271        home.join(".config/zed/settings.json")
272    }
273}
274
275fn vscode_mcp_path() -> PathBuf {
276    if cfg!(target_os = "macos") {
277        dirs::home_dir()
278            .unwrap_or_default()
279            .join("Library/Application Support/Code/User/settings.json")
280    } else if cfg!(target_os = "windows") {
281        dirs::home_dir()
282            .unwrap_or_default()
283            .join("AppData/Roaming/Code/User/settings.json")
284    } else {
285        dirs::home_dir()
286            .unwrap_or_default()
287            .join(".config/Code/User/settings.json")
288    }
289}