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}