Skip to main content

lean_ctx/
setup.rs

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    // Step 1: Shell hook
37    terminal_ui::print_step_header(1, 5, "Shell Hook");
38    crate::cli::cmd_init(&["--global".to_string()]);
39
40    // Step 2: Editor auto-detection + configuration
41    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    // Step 3: Agent rules injection
102    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    // Legacy agent hooks
125    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    // Step 4: Data directory + diagnostics
133    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    // Step 5: Data sharing
144    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    // Summary
178    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    // Next steps
196    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    // Logo + commands
252    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}