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    ]
394}
395
396fn detect_claude_path() -> PathBuf {
397    if let Ok(output) = std::process::Command::new("which").arg("claude").output() {
398        if output.status.success() {
399            return PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
400        }
401    }
402    if let Some(home) = dirs::home_dir() {
403        let claude_json = home.join(".claude.json");
404        if claude_json.exists() {
405            return claude_json;
406        }
407    }
408    PathBuf::from("/nonexistent")
409}
410
411fn detect_codex_path(home: &std::path::Path) -> PathBuf {
412    let codex_dir = home.join(".codex");
413    if codex_dir.exists() {
414        return codex_dir;
415    }
416    if let Ok(output) = std::process::Command::new("which").arg("codex").output() {
417        if output.status.success() {
418            return codex_dir;
419        }
420    }
421    PathBuf::from("/nonexistent")
422}
423
424fn zed_settings_path(home: &std::path::Path) -> PathBuf {
425    if cfg!(target_os = "macos") {
426        home.join("Library/Application Support/Zed/settings.json")
427    } else {
428        home.join(".config/zed/settings.json")
429    }
430}
431
432fn zed_config_dir(home: &std::path::Path) -> PathBuf {
433    if cfg!(target_os = "macos") {
434        home.join("Library/Application Support/Zed")
435    } else {
436        home.join(".config/zed")
437    }
438}
439
440fn write_config(target: &EditorTarget, binary: &str) -> Result<(), String> {
441    if let Some(parent) = target.config_path.parent() {
442        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
443    }
444
445    match target.config_type {
446        ConfigType::McpJson => write_mcp_json(target, binary),
447        ConfigType::Zed => write_zed_config(target, binary),
448        ConfigType::Codex => write_codex_config(target, binary),
449        ConfigType::VsCodeMcp => write_vscode_mcp(target, binary),
450        ConfigType::OpenCode => write_opencode_config(target, binary),
451    }
452}
453
454fn lean_ctx_server_entry(binary: &str) -> serde_json::Value {
455    serde_json::json!({
456        "command": binary,
457        "autoApprove": [
458            "ctx_read", "ctx_shell", "ctx_search", "ctx_tree",
459            "ctx_overview", "ctx_compress", "ctx_metrics", "ctx_session",
460            "ctx_knowledge", "ctx_agent", "ctx_analyze", "ctx_benchmark",
461            "ctx_cache", "ctx_discover", "ctx_smart_read", "ctx_delta",
462            "ctx_edit", "ctx_dedup", "ctx_fill", "ctx_intent", "ctx_response",
463            "ctx_context", "ctx_graph", "ctx_wrapped", "ctx_multi_read",
464            "ctx_semantic_search", "ctx"
465        ]
466    })
467}
468
469fn write_mcp_json(target: &EditorTarget, binary: &str) -> Result<(), String> {
470    if target.config_path.exists() {
471        let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
472
473        if content.contains("lean-ctx") {
474            return Ok(());
475        }
476
477        if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
478            if let Some(obj) = json.as_object_mut() {
479                let servers = obj
480                    .entry("mcpServers")
481                    .or_insert_with(|| serde_json::json!({}));
482                if let Some(servers_obj) = servers.as_object_mut() {
483                    servers_obj.insert("lean-ctx".to_string(), lean_ctx_server_entry(binary));
484                }
485                let formatted = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
486                std::fs::write(&target.config_path, formatted).map_err(|e| e.to_string())?;
487                return Ok(());
488            }
489        }
490        return Err(format!(
491            "Could not parse existing config at {}. Please add lean-ctx manually:\n\
492             Add to \"mcpServers\": \"lean-ctx\": {{ \"command\": \"{}\" }}",
493            target.config_path.display(),
494            binary
495        ));
496    }
497
498    let content = serde_json::to_string_pretty(&serde_json::json!({
499        "mcpServers": {
500            "lean-ctx": lean_ctx_server_entry(binary)
501        }
502    }))
503    .map_err(|e| e.to_string())?;
504
505    std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
506}
507
508fn write_zed_config(target: &EditorTarget, binary: &str) -> Result<(), String> {
509    if target.config_path.exists() {
510        let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
511
512        if content.contains("lean-ctx") {
513            return Ok(());
514        }
515
516        if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
517            if let Some(obj) = json.as_object_mut() {
518                let servers = obj
519                    .entry("context_servers")
520                    .or_insert_with(|| serde_json::json!({}));
521                if let Some(servers_obj) = servers.as_object_mut() {
522                    servers_obj.insert(
523                        "lean-ctx".to_string(),
524                        serde_json::json!({
525                            "source": "custom",
526                            "command": binary,
527                            "args": [],
528                            "env": {}
529                        }),
530                    );
531                }
532                let formatted = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
533                std::fs::write(&target.config_path, formatted).map_err(|e| e.to_string())?;
534                return Ok(());
535            }
536        }
537        return Err(format!(
538            "Could not parse existing config at {}. Please add lean-ctx manually to \"context_servers\".",
539            target.config_path.display()
540        ));
541    }
542
543    let content = serde_json::to_string_pretty(&serde_json::json!({
544        "context_servers": {
545            "lean-ctx": {
546                "source": "custom",
547                "command": binary,
548                "args": [],
549                "env": {}
550            }
551        }
552    }))
553    .map_err(|e| e.to_string())?;
554
555    std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
556}
557
558fn write_codex_config(target: &EditorTarget, binary: &str) -> Result<(), String> {
559    if target.config_path.exists() {
560        let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
561
562        if content.contains("lean-ctx") {
563            return Ok(());
564        }
565
566        let mut new_content = content.clone();
567        if !new_content.ends_with('\n') {
568            new_content.push('\n');
569        }
570        new_content.push_str(&format!(
571            "\n[mcp_servers.lean-ctx]\ncommand = \"{}\"\nargs = []\n",
572            binary
573        ));
574        std::fs::write(&target.config_path, new_content).map_err(|e| e.to_string())?;
575        return Ok(());
576    }
577
578    let content = format!(
579        "[mcp_servers.lean-ctx]\ncommand = \"{}\"\nargs = []\n",
580        binary
581    );
582    std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
583}
584
585fn write_vscode_mcp(target: &EditorTarget, binary: &str) -> Result<(), String> {
586    if target.config_path.exists() {
587        let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
588        if content.contains("lean-ctx") {
589            return Ok(());
590        }
591        if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
592            if let Some(obj) = json.as_object_mut() {
593                let servers = obj
594                    .entry("servers")
595                    .or_insert_with(|| serde_json::json!({}));
596                if let Some(servers_obj) = servers.as_object_mut() {
597                    servers_obj.insert(
598                        "lean-ctx".to_string(),
599                        serde_json::json!({ "command": binary, "args": [] }),
600                    );
601                }
602                let formatted = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
603                std::fs::write(&target.config_path, formatted).map_err(|e| e.to_string())?;
604                return Ok(());
605            }
606        }
607        return Err(format!(
608            "Could not parse existing config at {}. Please add lean-ctx manually to \"servers\".",
609            target.config_path.display()
610        ));
611    }
612
613    if let Some(parent) = target.config_path.parent() {
614        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
615    }
616
617    let content = serde_json::to_string_pretty(&serde_json::json!({
618        "servers": {
619            "lean-ctx": {
620                "command": binary,
621                "args": []
622            }
623        }
624    }))
625    .map_err(|e| e.to_string())?;
626
627    std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
628}
629
630fn write_opencode_config(target: &EditorTarget, binary: &str) -> Result<(), String> {
631    if target.config_path.exists() {
632        let content = std::fs::read_to_string(&target.config_path).map_err(|e| e.to_string())?;
633        if content.contains("lean-ctx") {
634            return Ok(());
635        }
636        if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
637            if let Some(obj) = json.as_object_mut() {
638                let mcp = obj.entry("mcp").or_insert_with(|| serde_json::json!({}));
639                if let Some(mcp_obj) = mcp.as_object_mut() {
640                    mcp_obj.insert(
641                        "lean-ctx".to_string(),
642                        serde_json::json!({
643                            "type": "local",
644                            "command": [binary],
645                            "enabled": true
646                        }),
647                    );
648                }
649                let formatted = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
650                std::fs::write(&target.config_path, formatted).map_err(|e| e.to_string())?;
651                return Ok(());
652            }
653        }
654        return Err(format!(
655            "Could not parse existing config at {}. Please add lean-ctx manually:\n\
656             Add to the \"mcp\" section: \"lean-ctx\": {{ \"type\": \"local\", \"command\": [\"{}\"], \"enabled\": true }}",
657            target.config_path.display(),
658            binary
659        ));
660    }
661
662    if let Some(parent) = target.config_path.parent() {
663        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
664    }
665
666    let content = serde_json::to_string_pretty(&serde_json::json!({
667        "$schema": "https://opencode.ai/config.json",
668        "mcp": {
669            "lean-ctx": {
670                "type": "local",
671                "command": [binary],
672                "enabled": true
673            }
674        }
675    }))
676    .map_err(|e| e.to_string())?;
677
678    std::fs::write(&target.config_path, content).map_err(|e| e.to_string())
679}
680
681fn detect_vscode_path() -> PathBuf {
682    #[cfg(target_os = "macos")]
683    {
684        if let Some(home) = dirs::home_dir() {
685            let vscode = home.join("Library/Application Support/Code/User/settings.json");
686            if vscode.exists() {
687                return vscode;
688            }
689        }
690    }
691    #[cfg(target_os = "linux")]
692    {
693        if let Some(home) = dirs::home_dir() {
694            let vscode = home.join(".config/Code/User/settings.json");
695            if vscode.exists() {
696                return vscode;
697            }
698        }
699    }
700    #[cfg(target_os = "windows")]
701    {
702        if let Ok(appdata) = std::env::var("APPDATA") {
703            let vscode = PathBuf::from(appdata).join("Code/User/settings.json");
704            if vscode.exists() {
705                return vscode;
706            }
707        }
708    }
709    if let Ok(output) = std::process::Command::new("which").arg("code").output() {
710        if output.status.success() {
711            return PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
712        }
713    }
714    PathBuf::from("/nonexistent")
715}
716
717fn vscode_mcp_path() -> PathBuf {
718    if let Some(home) = dirs::home_dir() {
719        #[cfg(target_os = "macos")]
720        {
721            return home.join("Library/Application Support/Code/User/mcp.json");
722        }
723        #[cfg(target_os = "linux")]
724        {
725            return home.join(".config/Code/User/mcp.json");
726        }
727        #[cfg(target_os = "windows")]
728        {
729            if let Ok(appdata) = std::env::var("APPDATA") {
730                return PathBuf::from(appdata).join("Code/User/mcp.json");
731            }
732        }
733        #[allow(unreachable_code)]
734        home.join(".config/Code/User/mcp.json")
735    } else {
736        PathBuf::from("/nonexistent")
737    }
738}
739
740fn detect_jetbrains_path(home: &std::path::Path) -> PathBuf {
741    #[cfg(target_os = "macos")]
742    {
743        let lib = home.join("Library/Application Support/JetBrains");
744        if lib.exists() {
745            return lib;
746        }
747    }
748    #[cfg(target_os = "linux")]
749    {
750        let cfg = home.join(".config/JetBrains");
751        if cfg.exists() {
752            return cfg;
753        }
754    }
755    if home.join(".jb-mcp.json").exists() {
756        return home.join(".jb-mcp.json");
757    }
758    PathBuf::from("/nonexistent")
759}
760
761fn cline_mcp_path() -> PathBuf {
762    if let Some(home) = dirs::home_dir() {
763        #[cfg(target_os = "macos")]
764        {
765            return home.join("Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
766        }
767        #[cfg(target_os = "linux")]
768        {
769            return home.join(".config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
770        }
771        #[cfg(target_os = "windows")]
772        {
773            if let Ok(appdata) = std::env::var("APPDATA") {
774                return PathBuf::from(appdata).join("Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
775            }
776        }
777    }
778    PathBuf::from("/nonexistent")
779}
780
781fn detect_cline_path() -> PathBuf {
782    if let Some(home) = dirs::home_dir() {
783        #[cfg(target_os = "macos")]
784        {
785            let p = home
786                .join("Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev");
787            if p.exists() {
788                return p;
789            }
790        }
791        #[cfg(target_os = "linux")]
792        {
793            let p = home.join(".config/Code/User/globalStorage/saoudrizwan.claude-dev");
794            if p.exists() {
795                return p;
796            }
797        }
798    }
799    PathBuf::from("/nonexistent")
800}
801
802fn roo_mcp_path() -> PathBuf {
803    if let Some(home) = dirs::home_dir() {
804        #[cfg(target_os = "macos")]
805        {
806            return home.join("Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
807        }
808        #[cfg(target_os = "linux")]
809        {
810            return home.join(".config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
811        }
812        #[cfg(target_os = "windows")]
813        {
814            if let Ok(appdata) = std::env::var("APPDATA") {
815                return PathBuf::from(appdata).join("Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
816            }
817        }
818    }
819    PathBuf::from("/nonexistent")
820}
821
822fn detect_roo_path() -> PathBuf {
823    if let Some(home) = dirs::home_dir() {
824        #[cfg(target_os = "macos")]
825        {
826            let p = home.join(
827                "Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline",
828            );
829            if p.exists() {
830                return p;
831            }
832        }
833        #[cfg(target_os = "linux")]
834        {
835            let p = home.join(".config/Code/User/globalStorage/rooveterinaryinc.roo-cline");
836            if p.exists() {
837                return p;
838            }
839        }
840    }
841    PathBuf::from("/nonexistent")
842}
843
844fn resolve_portable_binary() -> String {
845    let which_cmd = if cfg!(windows) { "where" } else { "which" };
846    if let Ok(status) = std::process::Command::new(which_cmd)
847        .arg("lean-ctx")
848        .stdout(std::process::Stdio::null())
849        .stderr(std::process::Stdio::null())
850        .status()
851    {
852        if status.success() {
853            return "lean-ctx".to_string();
854        }
855    }
856    std::env::current_exe()
857        .map(|p| p.to_string_lossy().to_string())
858        .unwrap_or_else(|_| "lean-ctx".to_string())
859}