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}