1use std::path::PathBuf;
2
3struct EditorTarget {
4 name: &'static str,
5 agent_key: &'static str,
6 config_path: PathBuf,
7 detect_path: PathBuf,
8 config_type: ConfigType,
9}
10
11enum ConfigType {
12 McpJson,
13 Zed,
14 Codex,
15 VsCodeMcp,
16 OpenCode,
17}
18
19pub fn run_setup() {
20 use crate::terminal_ui;
21
22 let home = match dirs::home_dir() {
23 Some(h) => h,
24 None => {
25 eprintln!("Cannot determine home directory");
26 std::process::exit(1);
27 }
28 };
29
30 let binary = resolve_portable_binary();
31
32 let home_str = home.to_string_lossy().to_string();
33
34 terminal_ui::print_setup_header();
35
36 terminal_ui::print_step_header(1, 5, "Shell Hook");
38 crate::cli::cmd_init(&["--global".to_string()]);
39
40 terminal_ui::print_step_header(2, 5, "AI Tool Detection");
42
43 let targets = build_targets(&home, &binary);
44 let mut newly_configured: Vec<&str> = Vec::new();
45 let mut already_configured: Vec<&str> = Vec::new();
46 let mut not_installed: Vec<&str> = Vec::new();
47 let mut errors: Vec<&str> = Vec::new();
48
49 for target in &targets {
50 let short_path = shorten_path(&target.config_path.to_string_lossy(), &home_str);
51
52 if !target.detect_path.exists() {
53 not_installed.push(target.name);
54 continue;
55 }
56
57 let has_config = target.config_path.exists()
58 && std::fs::read_to_string(&target.config_path)
59 .map(|c| c.contains("lean-ctx"))
60 .unwrap_or(false);
61
62 if has_config {
63 terminal_ui::print_status_ok(&format!(
64 "{:<20} \x1b[2m{short_path}\x1b[0m",
65 target.name
66 ));
67 already_configured.push(target.name);
68 continue;
69 }
70
71 match write_config(target, &binary) {
72 Ok(()) => {
73 terminal_ui::print_status_new(&format!(
74 "{:<20} \x1b[2m{short_path}\x1b[0m",
75 target.name
76 ));
77 newly_configured.push(target.name);
78 }
79 Err(e) => {
80 terminal_ui::print_status_warn(&format!("{}: {e}", target.name));
81 errors.push(target.name);
82 }
83 }
84 }
85
86 let total_ok = newly_configured.len() + already_configured.len();
87 if total_ok == 0 && errors.is_empty() {
88 terminal_ui::print_status_warn(
89 "No AI tools detected. Install one and re-run: lean-ctx setup",
90 );
91 }
92
93 if !not_installed.is_empty() {
94 println!(
95 " \x1b[2m○ {} not detected: {}\x1b[0m",
96 not_installed.len(),
97 not_installed.join(", ")
98 );
99 }
100
101 terminal_ui::print_step_header(3, 5, "Agent Rules");
103 let rules_result = crate::rules_inject::inject_all_rules(&home);
104 for name in &rules_result.injected {
105 terminal_ui::print_status_new(&format!("{name:<20} \x1b[2mrules injected\x1b[0m"));
106 }
107 for name in &rules_result.updated {
108 terminal_ui::print_status_new(&format!("{name:<20} \x1b[2mrules updated\x1b[0m"));
109 }
110 for name in &rules_result.already {
111 terminal_ui::print_status_ok(&format!("{name:<20} \x1b[2mrules up-to-date\x1b[0m"));
112 }
113 for err in &rules_result.errors {
114 terminal_ui::print_status_warn(err);
115 }
116 if rules_result.injected.is_empty()
117 && rules_result.updated.is_empty()
118 && rules_result.already.is_empty()
119 && rules_result.errors.is_empty()
120 {
121 terminal_ui::print_status_skip("No agent rules needed");
122 }
123
124 for target in &targets {
126 if !target.detect_path.exists() || target.agent_key.is_empty() {
127 continue;
128 }
129 crate::hooks::install_agent_hook(target.agent_key, true);
130 }
131
132 terminal_ui::print_step_header(4, 5, "Environment Check");
134 let lean_dir = home.join(".lean-ctx");
135 if !lean_dir.exists() {
136 let _ = std::fs::create_dir_all(&lean_dir);
137 terminal_ui::print_status_new("Created ~/.lean-ctx/");
138 } else {
139 terminal_ui::print_status_ok("~/.lean-ctx/ ready");
140 }
141 crate::doctor::run_compact();
142
143 terminal_ui::print_step_header(5, 5, "Help Improve lean-ctx");
145 println!(" Share anonymous compression stats to make lean-ctx better.");
146 println!(" \x1b[1mNo code, no file names, no personal data — ever.\x1b[0m");
147 println!();
148 print!(" Enable anonymous data sharing? \x1b[1m[Y/n]\x1b[0m ");
149 use std::io::Write;
150 std::io::stdout().flush().ok();
151
152 let mut input = String::new();
153 let contribute = if std::io::stdin().read_line(&mut input).is_ok() {
154 let answer = input.trim().to_lowercase();
155 answer.is_empty() || answer == "y" || answer == "yes"
156 } else {
157 false
158 };
159
160 if contribute {
161 let config_dir = home.join(".lean-ctx");
162 let _ = std::fs::create_dir_all(&config_dir);
163 let config_path = config_dir.join("config.toml");
164 let mut config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
165 if !config_content.contains("[cloud]") {
166 if !config_content.is_empty() && !config_content.ends_with('\n') {
167 config_content.push('\n');
168 }
169 config_content.push_str("\n[cloud]\ncontribute_enabled = true\n");
170 let _ = std::fs::write(&config_path, config_content);
171 }
172 terminal_ui::print_status_ok("Enabled — thank you!");
173 } else {
174 terminal_ui::print_status_skip("Skipped — enable later with: lean-ctx config");
175 }
176
177 println!();
179 println!(
180 " \x1b[1;32m✓ Setup complete!\x1b[0m \x1b[1m{}\x1b[0m configured, \x1b[2m{} already set, {} skipped\x1b[0m",
181 newly_configured.len(),
182 already_configured.len(),
183 not_installed.len()
184 );
185
186 if !errors.is_empty() {
187 println!(
188 " \x1b[33m⚠ {} error{}: {}\x1b[0m",
189 errors.len(),
190 if errors.len() != 1 { "s" } else { "" },
191 errors.join(", ")
192 );
193 }
194
195 let shell = std::env::var("SHELL").unwrap_or_default();
197 let source_cmd = if shell.contains("zsh") {
198 "source ~/.zshrc"
199 } else if shell.contains("fish") {
200 "source ~/.config/fish/config.fish"
201 } else if shell.contains("bash") {
202 "source ~/.bashrc"
203 } else {
204 "Restart your shell"
205 };
206
207 let dim = "\x1b[2m";
208 let bold = "\x1b[1m";
209 let cyan = "\x1b[36m";
210 let yellow = "\x1b[33m";
211 let rst = "\x1b[0m";
212
213 println!();
214 println!(" {bold}Next steps:{rst}");
215 println!();
216 println!(" {cyan}1.{rst} Reload your shell:");
217 println!(" {bold}{source_cmd}{rst}");
218 println!();
219
220 let mut tools_to_restart: Vec<String> =
221 newly_configured.iter().map(|s| s.to_string()).collect();
222 for name in rules_result
223 .injected
224 .iter()
225 .chain(rules_result.updated.iter())
226 {
227 if !tools_to_restart.iter().any(|t| t == name) {
228 tools_to_restart.push(name.clone());
229 }
230 }
231
232 if !tools_to_restart.is_empty() {
233 println!(" {cyan}2.{rst} {yellow}{bold}Restart your IDE / AI tool:{rst}");
234 println!(" {bold}{}{rst}", tools_to_restart.join(", "));
235 println!(
236 " {dim}The MCP connection must be re-established for changes to take effect.{rst}"
237 );
238 println!(" {dim}Close and re-open the application completely.{rst}");
239 } else if !already_configured.is_empty() {
240 println!(
241 " {cyan}2.{rst} {dim}Your tools are already configured — no restart needed.{rst}"
242 );
243 }
244
245 println!();
246 println!(
247 " {dim}After restart, lean-ctx will automatically optimize every AI interaction.{rst}"
248 );
249 println!(" {dim}Verify with:{rst} {bold}lean-ctx gain{rst}");
250
251 println!();
253 terminal_ui::print_logo_animated();
254 terminal_ui::print_command_box();
255}
256
257fn shorten_path(path: &str, home: &str) -> String {
258 if let Some(stripped) = path.strip_prefix(home) {
259 format!("~{stripped}")
260 } else {
261 path.to_string()
262 }
263}
264
265fn build_targets(home: &std::path::Path, _binary: &str) -> Vec<EditorTarget> {
266 vec![
267 EditorTarget {
268 name: "Cursor",
269 agent_key: "cursor",
270 config_path: home.join(".cursor/mcp.json"),
271 detect_path: home.join(".cursor"),
272 config_type: ConfigType::McpJson,
273 },
274 EditorTarget {
275 name: "Claude Code",
276 agent_key: "claude",
277 config_path: home.join(".claude.json"),
278 detect_path: detect_claude_path(),
279 config_type: ConfigType::McpJson,
280 },
281 EditorTarget {
282 name: "Windsurf",
283 agent_key: "windsurf",
284 config_path: home.join(".codeium/windsurf/mcp_config.json"),
285 detect_path: home.join(".codeium/windsurf"),
286 config_type: ConfigType::McpJson,
287 },
288 EditorTarget {
289 name: "Codex CLI",
290 agent_key: "codex",
291 config_path: home.join(".codex/config.toml"),
292 detect_path: detect_codex_path(home),
293 config_type: ConfigType::Codex,
294 },
295 EditorTarget {
296 name: "Gemini CLI",
297 agent_key: "gemini",
298 config_path: home.join(".gemini/settings/mcp.json"),
299 detect_path: home.join(".gemini"),
300 config_type: ConfigType::McpJson,
301 },
302 EditorTarget {
303 name: "Antigravity",
304 agent_key: "gemini",
305 config_path: home.join(".gemini/antigravity/mcp_config.json"),
306 detect_path: home.join(".gemini/antigravity"),
307 config_type: ConfigType::McpJson,
308 },
309 EditorTarget {
310 name: "Zed",
311 agent_key: "",
312 config_path: zed_settings_path(home),
313 detect_path: zed_config_dir(home),
314 config_type: ConfigType::Zed,
315 },
316 EditorTarget {
317 name: "VS Code / Copilot",
318 agent_key: "copilot",
319 config_path: vscode_mcp_path(),
320 detect_path: detect_vscode_path(),
321 config_type: ConfigType::VsCodeMcp,
322 },
323 EditorTarget {
324 name: "OpenCode",
325 agent_key: "",
326 config_path: home.join(".config/opencode/opencode.json"),
327 detect_path: home.join(".config/opencode"),
328 config_type: ConfigType::OpenCode,
329 },
330 EditorTarget {
331 name: "Qwen Code",
332 agent_key: "qwen",
333 config_path: home.join(".qwen/mcp.json"),
334 detect_path: home.join(".qwen"),
335 config_type: ConfigType::McpJson,
336 },
337 EditorTarget {
338 name: "Trae",
339 agent_key: "trae",
340 config_path: home.join(".trae/mcp.json"),
341 detect_path: home.join(".trae"),
342 config_type: ConfigType::McpJson,
343 },
344 EditorTarget {
345 name: "Amazon Q Developer",
346 agent_key: "amazonq",
347 config_path: home.join(".aws/amazonq/mcp.json"),
348 detect_path: home.join(".aws/amazonq"),
349 config_type: ConfigType::McpJson,
350 },
351 EditorTarget {
352 name: "JetBrains IDEs",
353 agent_key: "jetbrains",
354 config_path: home.join(".jb-mcp.json"),
355 detect_path: detect_jetbrains_path(home),
356 config_type: ConfigType::McpJson,
357 },
358 EditorTarget {
359 name: "Cline",
360 agent_key: "cline",
361 config_path: cline_mcp_path(),
362 detect_path: detect_cline_path(),
363 config_type: ConfigType::McpJson,
364 },
365 EditorTarget {
366 name: "Roo Code",
367 agent_key: "roo",
368 config_path: roo_mcp_path(),
369 detect_path: detect_roo_path(),
370 config_type: ConfigType::McpJson,
371 },
372 EditorTarget {
373 name: "AWS Kiro",
374 agent_key: "kiro",
375 config_path: home.join(".kiro/settings/mcp.json"),
376 detect_path: home.join(".kiro"),
377 config_type: ConfigType::McpJson,
378 },
379 EditorTarget {
380 name: "Verdent",
381 agent_key: "verdent",
382 config_path: home.join(".verdent/mcp.json"),
383 detect_path: home.join(".verdent"),
384 config_type: ConfigType::McpJson,
385 },
386 EditorTarget {
387 name: "Crush",
388 agent_key: "crush",
389 config_path: home.join(".config/crush/crush.json"),
390 detect_path: home.join(".config/crush"),
391 config_type: ConfigType::McpJson,
392 },
393 EditorTarget {
394 name: "Pi Coding Agent",
395 agent_key: "pi",
396 config_path: home.join(".pi/agent/mcp.json"),
397 detect_path: home.join(".pi/agent"),
398 config_type: ConfigType::McpJson,
399 },
400 ]
401}
402
403fn detect_claude_path() -> PathBuf {
404 if let Ok(output) = std::process::Command::new("which").arg("claude").output() {
405 if output.status.success() {
406 return PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
407 }
408 }
409 if let Some(home) = dirs::home_dir() {
410 let claude_json = home.join(".claude.json");
411 if claude_json.exists() {
412 return claude_json;
413 }
414 }
415 PathBuf::from("/nonexistent")
416}
417
418fn detect_codex_path(home: &std::path::Path) -> PathBuf {
419 let codex_dir = home.join(".codex");
420 if codex_dir.exists() {
421 return codex_dir;
422 }
423 if let Ok(output) = std::process::Command::new("which").arg("codex").output() {
424 if output.status.success() {
425 return codex_dir;
426 }
427 }
428 PathBuf::from("/nonexistent")
429}
430
431fn zed_settings_path(home: &std::path::Path) -> PathBuf {
432 if cfg!(target_os = "macos") {
433 home.join("Library/Application Support/Zed/settings.json")
434 } else {
435 home.join(".config/zed/settings.json")
436 }
437}
438
439fn zed_config_dir(home: &std::path::Path) -> PathBuf {
440 if cfg!(target_os = "macos") {
441 home.join("Library/Application Support/Zed")
442 } else {
443 home.join(".config/zed")
444 }
445}
446
447fn write_config(target: &EditorTarget, binary: &str) -> Result<(), String> {
448 if let Some(parent) = target.config_path.parent() {
449 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
450 }
451
452 match target.config_type {
453 ConfigType::McpJson => write_mcp_json(target, binary),
454 ConfigType::Zed => write_zed_config(target, binary),
455 ConfigType::Codex => write_codex_config(target, binary),
456 ConfigType::VsCodeMcp => write_vscode_mcp(target, binary),
457 ConfigType::OpenCode => write_opencode_config(target, binary),
458 }
459}
460
461fn lean_ctx_server_entry(binary: &str) -> serde_json::Value {
462 serde_json::json!({
463 "command": binary,
464 "autoApprove": [
465 "ctx_read", "ctx_shell", "ctx_search", "ctx_tree",
466 "ctx_overview", "ctx_compress", "ctx_metrics", "ctx_session",
467 "ctx_knowledge", "ctx_agent", "ctx_analyze", "ctx_benchmark",
468 "ctx_cache", "ctx_discover", "ctx_smart_read", "ctx_delta",
469 "ctx_edit", "ctx_dedup", "ctx_fill", "ctx_intent", "ctx_response",
470 "ctx_context", "ctx_graph", "ctx_wrapped", "ctx_multi_read",
471 "ctx_semantic_search", "ctx"
472 ]
473 })
474}
475
476fn write_mcp_json(target: &EditorTarget, binary: &str) -> Result<(), String> {
477 if target.config_path.exists() {
478 let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
479
480 if content.contains("lean-ctx") {
481 return Ok(());
482 }
483
484 if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
485 if let Some(obj) = json.as_object_mut() {
486 let servers = obj
487 .entry("mcpServers")
488 .or_insert_with(|| serde_json::json!({}));
489 if let Some(servers_obj) = servers.as_object_mut() {
490 servers_obj.insert("lean-ctx".to_string(), lean_ctx_server_entry(binary));
491 }
492 let formatted = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
493 std::fs::write(&target.config_path, formatted).map_err(|e| e.to_string())?;
494 return Ok(());
495 }
496 }
497 return Err(format!(
498 "Could not parse existing config at {}. Please add lean-ctx manually:\n\
499 Add to \"mcpServers\": \"lean-ctx\": {{ \"command\": \"{}\" }}",
500 target.config_path.display(),
501 binary
502 ));
503 }
504
505 let content = serde_json::to_string_pretty(&serde_json::json!({
506 "mcpServers": {
507 "lean-ctx": lean_ctx_server_entry(binary)
508 }
509 }))
510 .map_err(|e| e.to_string())?;
511
512 std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
513}
514
515fn write_zed_config(target: &EditorTarget, binary: &str) -> Result<(), String> {
516 if target.config_path.exists() {
517 let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
518
519 if content.contains("lean-ctx") {
520 return Ok(());
521 }
522
523 if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
524 if let Some(obj) = json.as_object_mut() {
525 let servers = obj
526 .entry("context_servers")
527 .or_insert_with(|| serde_json::json!({}));
528 if let Some(servers_obj) = servers.as_object_mut() {
529 servers_obj.insert(
530 "lean-ctx".to_string(),
531 serde_json::json!({
532 "source": "custom",
533 "command": binary,
534 "args": [],
535 "env": {}
536 }),
537 );
538 }
539 let formatted = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
540 std::fs::write(&target.config_path, formatted).map_err(|e| e.to_string())?;
541 return Ok(());
542 }
543 }
544 return Err(format!(
545 "Could not parse existing config at {}. Please add lean-ctx manually to \"context_servers\".",
546 target.config_path.display()
547 ));
548 }
549
550 let content = serde_json::to_string_pretty(&serde_json::json!({
551 "context_servers": {
552 "lean-ctx": {
553 "source": "custom",
554 "command": binary,
555 "args": [],
556 "env": {}
557 }
558 }
559 }))
560 .map_err(|e| e.to_string())?;
561
562 std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
563}
564
565fn write_codex_config(target: &EditorTarget, binary: &str) -> Result<(), String> {
566 if target.config_path.exists() {
567 let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
568
569 if content.contains("lean-ctx") {
570 return Ok(());
571 }
572
573 let mut new_content = content.clone();
574 if !new_content.ends_with('\n') {
575 new_content.push('\n');
576 }
577 new_content.push_str(&format!(
578 "\n[mcp_servers.lean-ctx]\ncommand = \"{}\"\nargs = []\n",
579 binary
580 ));
581 std::fs::write(&target.config_path, new_content).map_err(|e| e.to_string())?;
582 return Ok(());
583 }
584
585 let content = format!(
586 "[mcp_servers.lean-ctx]\ncommand = \"{}\"\nargs = []\n",
587 binary
588 );
589 std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
590}
591
592fn write_vscode_mcp(target: &EditorTarget, binary: &str) -> Result<(), String> {
593 if target.config_path.exists() {
594 let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
595 if content.contains("lean-ctx") {
596 return Ok(());
597 }
598 if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
599 if let Some(obj) = json.as_object_mut() {
600 let servers = obj
601 .entry("servers")
602 .or_insert_with(|| serde_json::json!({}));
603 if let Some(servers_obj) = servers.as_object_mut() {
604 servers_obj.insert(
605 "lean-ctx".to_string(),
606 serde_json::json!({ "command": binary, "args": [] }),
607 );
608 }
609 let formatted = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
610 std::fs::write(&target.config_path, formatted).map_err(|e| e.to_string())?;
611 return Ok(());
612 }
613 }
614 return Err(format!(
615 "Could not parse existing config at {}. Please add lean-ctx manually to \"servers\".",
616 target.config_path.display()
617 ));
618 }
619
620 if let Some(parent) = target.config_path.parent() {
621 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
622 }
623
624 let content = serde_json::to_string_pretty(&serde_json::json!({
625 "servers": {
626 "lean-ctx": {
627 "command": binary,
628 "args": []
629 }
630 }
631 }))
632 .map_err(|e| e.to_string())?;
633
634 std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
635}
636
637fn write_opencode_config(target: &EditorTarget, binary: &str) -> Result<(), String> {
638 if target.config_path.exists() {
639 let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
640 if content.contains("lean-ctx") {
641 return Ok(());
642 }
643 if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
644 if let Some(obj) = json.as_object_mut() {
645 let mcp = obj.entry("mcp").or_insert_with(|| serde_json::json!({}));
646 if let Some(mcp_obj) = mcp.as_object_mut() {
647 mcp_obj.insert(
648 "lean-ctx".to_string(),
649 serde_json::json!({
650 "type": "local",
651 "command": [binary],
652 "enabled": true
653 }),
654 );
655 }
656 let formatted = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
657 std::fs::write(&target.config_path, formatted).map_err(|e| e.to_string())?;
658 return Ok(());
659 }
660 }
661 return Err(format!(
662 "Could not parse existing config at {}. Please add lean-ctx manually:\n\
663 Add to the \"mcp\" section: \"lean-ctx\": {{ \"type\": \"local\", \"command\": [\"{}\"], \"enabled\": true }}",
664 target.config_path.display(),
665 binary
666 ));
667 }
668
669 if let Some(parent) = target.config_path.parent() {
670 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
671 }
672
673 let content = serde_json::to_string_pretty(&serde_json::json!({
674 "$schema": "https://opencode.ai/config.json",
675 "mcp": {
676 "lean-ctx": {
677 "type": "local",
678 "command": [binary],
679 "enabled": true
680 }
681 }
682 }))
683 .map_err(|e| e.to_string())?;
684
685 std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
686}
687
688fn detect_vscode_path() -> PathBuf {
689 #[cfg(target_os = "macos")]
690 {
691 if let Some(home) = dirs::home_dir() {
692 let vscode = home.join("Library/Application Support/Code/User/settings.json");
693 if vscode.exists() {
694 return vscode;
695 }
696 }
697 }
698 #[cfg(target_os = "linux")]
699 {
700 if let Some(home) = dirs::home_dir() {
701 let vscode = home.join(".config/Code/User/settings.json");
702 if vscode.exists() {
703 return vscode;
704 }
705 }
706 }
707 #[cfg(target_os = "windows")]
708 {
709 if let Ok(appdata) = std::env::var("APPDATA") {
710 let vscode = PathBuf::from(appdata).join("Code/User/settings.json");
711 if vscode.exists() {
712 return vscode;
713 }
714 }
715 }
716 if let Ok(output) = std::process::Command::new("which").arg("code").output() {
717 if output.status.success() {
718 return PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
719 }
720 }
721 PathBuf::from("/nonexistent")
722}
723
724fn vscode_mcp_path() -> PathBuf {
725 if let Some(home) = dirs::home_dir() {
726 #[cfg(target_os = "macos")]
727 {
728 return home.join("Library/Application Support/Code/User/mcp.json");
729 }
730 #[cfg(target_os = "linux")]
731 {
732 return home.join(".config/Code/User/mcp.json");
733 }
734 #[cfg(target_os = "windows")]
735 {
736 if let Ok(appdata) = std::env::var("APPDATA") {
737 return PathBuf::from(appdata).join("Code/User/mcp.json");
738 }
739 }
740 #[allow(unreachable_code)]
741 home.join(".config/Code/User/mcp.json")
742 } else {
743 PathBuf::from("/nonexistent")
744 }
745}
746
747fn detect_jetbrains_path(home: &std::path::Path) -> PathBuf {
748 #[cfg(target_os = "macos")]
749 {
750 let lib = home.join("Library/Application Support/JetBrains");
751 if lib.exists() {
752 return lib;
753 }
754 }
755 #[cfg(target_os = "linux")]
756 {
757 let cfg = home.join(".config/JetBrains");
758 if cfg.exists() {
759 return cfg;
760 }
761 }
762 if home.join(".jb-mcp.json").exists() {
763 return home.join(".jb-mcp.json");
764 }
765 PathBuf::from("/nonexistent")
766}
767
768fn cline_mcp_path() -> PathBuf {
769 if let Some(home) = dirs::home_dir() {
770 #[cfg(target_os = "macos")]
771 {
772 return home.join("Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
773 }
774 #[cfg(target_os = "linux")]
775 {
776 return home.join(".config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
777 }
778 #[cfg(target_os = "windows")]
779 {
780 if let Ok(appdata) = std::env::var("APPDATA") {
781 return PathBuf::from(appdata).join("Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
782 }
783 }
784 }
785 PathBuf::from("/nonexistent")
786}
787
788fn detect_cline_path() -> PathBuf {
789 if let Some(home) = dirs::home_dir() {
790 #[cfg(target_os = "macos")]
791 {
792 let p = home
793 .join("Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev");
794 if p.exists() {
795 return p;
796 }
797 }
798 #[cfg(target_os = "linux")]
799 {
800 let p = home.join(".config/Code/User/globalStorage/saoudrizwan.claude-dev");
801 if p.exists() {
802 return p;
803 }
804 }
805 }
806 PathBuf::from("/nonexistent")
807}
808
809fn roo_mcp_path() -> PathBuf {
810 if let Some(home) = dirs::home_dir() {
811 #[cfg(target_os = "macos")]
812 {
813 return home.join("Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
814 }
815 #[cfg(target_os = "linux")]
816 {
817 return home.join(".config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
818 }
819 #[cfg(target_os = "windows")]
820 {
821 if let Ok(appdata) = std::env::var("APPDATA") {
822 return PathBuf::from(appdata).join("Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
823 }
824 }
825 }
826 PathBuf::from("/nonexistent")
827}
828
829fn detect_roo_path() -> PathBuf {
830 if let Some(home) = dirs::home_dir() {
831 #[cfg(target_os = "macos")]
832 {
833 let p = home.join(
834 "Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline",
835 );
836 if p.exists() {
837 return p;
838 }
839 }
840 #[cfg(target_os = "linux")]
841 {
842 let p = home.join(".config/Code/User/globalStorage/rooveterinaryinc.roo-cline");
843 if p.exists() {
844 return p;
845 }
846 }
847 }
848 PathBuf::from("/nonexistent")
849}
850
851fn resolve_portable_binary() -> String {
852 let which_cmd = if cfg!(windows) { "where" } else { "which" };
853 if let Ok(status) = std::process::Command::new(which_cmd)
854 .arg("lean-ctx")
855 .stdout(std::process::Stdio::null())
856 .stderr(std::process::Stdio::null())
857 .status()
858 {
859 if status.success() {
860 return "lean-ctx".to_string();
861 }
862 }
863 std::env::current_exe()
864 .map(|p| p.to_string_lossy().to_string())
865 .unwrap_or_else(|_| "lean-ctx".to_string())
866}