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