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}