Skip to main content

lean_ctx/core/editor_registry/
detect.rs

1use std::path::{Path, PathBuf};
2
3use super::paths::{
4    claude_mcp_json_path, cline_mcp_path, qoder_all_mcp_paths, qoderwork_mcp_path, roo_mcp_path,
5    vscode_mcp_path, zed_config_dir, zed_settings_path,
6};
7use super::types::{ConfigType, EditorTarget};
8
9pub fn build_targets(home: &Path) -> Vec<EditorTarget> {
10    #[cfg(windows)]
11    let opencode_cfg = if let Ok(appdata) = std::env::var("APPDATA") {
12        PathBuf::from(appdata)
13            .join("opencode")
14            .join("opencode.json")
15    } else {
16        home.join(".config/opencode/opencode.json")
17    };
18    #[cfg(not(windows))]
19    let opencode_cfg = home.join(".config/opencode/opencode.json");
20
21    #[cfg(windows)]
22    let opencode_detect = opencode_cfg
23        .parent()
24        .map(|p| p.to_path_buf())
25        .unwrap_or_else(|| home.join(".config/opencode"));
26    #[cfg(not(windows))]
27    let opencode_detect = home.join(".config/opencode");
28
29    let mut targets = vec![
30        EditorTarget {
31            name: "Cursor",
32            agent_key: "cursor".to_string(),
33            config_path: home.join(".cursor/mcp.json"),
34            detect_path: home.join(".cursor"),
35            config_type: ConfigType::McpJson,
36        },
37        EditorTarget {
38            name: "Claude Code",
39            agent_key: "claude".to_string(),
40            config_path: claude_mcp_json_path(home),
41            detect_path: detect_claude_path(),
42            config_type: ConfigType::McpJson,
43        },
44        EditorTarget {
45            name: "Windsurf",
46            agent_key: "windsurf".to_string(),
47            config_path: home.join(".codeium/windsurf/mcp_config.json"),
48            detect_path: home.join(".codeium/windsurf"),
49            config_type: ConfigType::McpJson,
50        },
51        EditorTarget {
52            name: "Codex CLI",
53            agent_key: "codex".to_string(),
54            config_path: crate::core::home::resolve_codex_dir()
55                .unwrap_or_else(|| home.join(".codex"))
56                .join("config.toml"),
57            detect_path: detect_codex_path(home),
58            config_type: ConfigType::Codex,
59        },
60        EditorTarget {
61            name: "Gemini CLI",
62            agent_key: "gemini".to_string(),
63            config_path: home.join(".gemini/settings.json"),
64            detect_path: home.join(".gemini"),
65            config_type: ConfigType::GeminiSettings,
66        },
67        EditorTarget {
68            name: "Antigravity",
69            agent_key: "gemini".to_string(),
70            config_path: home.join(".gemini/antigravity/mcp_config.json"),
71            detect_path: home.join(".gemini/antigravity"),
72            config_type: ConfigType::McpJson,
73        },
74        EditorTarget {
75            name: "Zed",
76            agent_key: "zed".to_string(),
77            config_path: zed_settings_path(home),
78            detect_path: zed_config_dir(home),
79            config_type: ConfigType::Zed,
80        },
81        EditorTarget {
82            name: "VS Code / Copilot",
83            agent_key: "copilot".to_string(),
84            config_path: vscode_mcp_path(),
85            detect_path: detect_vscode_path(),
86            config_type: ConfigType::VsCodeMcp,
87        },
88        EditorTarget {
89            name: "OpenCode",
90            agent_key: "opencode".to_string(),
91            config_path: opencode_cfg,
92            detect_path: opencode_detect,
93            config_type: ConfigType::OpenCode,
94        },
95        EditorTarget {
96            name: "Qwen Code",
97            agent_key: "qwen".to_string(),
98            config_path: home.join(".qwen/settings.json"),
99            detect_path: home.join(".qwen"),
100            config_type: ConfigType::McpJson,
101        },
102        EditorTarget {
103            name: "Trae",
104            agent_key: "trae".to_string(),
105            config_path: home.join(".trae/mcp.json"),
106            detect_path: home.join(".trae"),
107            config_type: ConfigType::McpJson,
108        },
109        EditorTarget {
110            name: "Amazon Q Developer",
111            agent_key: "amazonq".to_string(),
112            config_path: home.join(".aws/amazonq/default.json"),
113            detect_path: home.join(".aws/amazonq"),
114            config_type: ConfigType::McpJson,
115        },
116        EditorTarget {
117            name: "JetBrains IDEs",
118            agent_key: "jetbrains".to_string(),
119            config_path: home.join(".jb-mcp.json"),
120            detect_path: detect_jetbrains_path(home),
121            config_type: ConfigType::JetBrains,
122        },
123        EditorTarget {
124            name: "Cline",
125            agent_key: "cline".to_string(),
126            config_path: cline_mcp_path(),
127            detect_path: detect_cline_path(),
128            config_type: ConfigType::McpJson,
129        },
130        EditorTarget {
131            name: "Roo Code",
132            agent_key: "roo".to_string(),
133            config_path: roo_mcp_path(),
134            detect_path: detect_roo_path(),
135            config_type: ConfigType::McpJson,
136        },
137        EditorTarget {
138            name: "AWS Kiro",
139            agent_key: "kiro".to_string(),
140            config_path: home.join(".kiro/settings/mcp.json"),
141            detect_path: home.join(".kiro"),
142            config_type: ConfigType::McpJson,
143        },
144        EditorTarget {
145            name: "Verdent",
146            agent_key: "verdent".to_string(),
147            config_path: home.join(".verdent/mcp.json"),
148            detect_path: home.join(".verdent"),
149            config_type: ConfigType::McpJson,
150        },
151        EditorTarget {
152            name: "Crush",
153            agent_key: "crush".to_string(),
154            config_path: home.join(".config/crush/crush.json"),
155            detect_path: home.join(".config/crush"),
156            config_type: ConfigType::Crush,
157        },
158        EditorTarget {
159            name: "Pi Coding Agent",
160            agent_key: "pi".to_string(),
161            config_path: home.join(".pi/agent/mcp.json"),
162            detect_path: home.join(".pi/agent"),
163            config_type: ConfigType::McpJson,
164        },
165        EditorTarget {
166            name: "Amp",
167            agent_key: "amp".to_string(),
168            config_path: home.join(".config/amp/settings.json"),
169            detect_path: home.join(".config/amp"),
170            config_type: ConfigType::Amp,
171        },
172        EditorTarget {
173            name: "QoderWork",
174            agent_key: "qoderwork".to_string(),
175            config_path: qoderwork_mcp_path(home),
176            detect_path: detect_qoderwork_path(home),
177            config_type: ConfigType::McpJson,
178        },
179        EditorTarget {
180            name: "Hermes Agent",
181            agent_key: "hermes".to_string(),
182            config_path: home.join(".hermes/config.yaml"),
183            detect_path: home.join(".hermes"),
184            config_type: ConfigType::HermesYaml,
185        },
186        EditorTarget {
187            name: "Aider",
188            agent_key: "aider".to_string(),
189            config_path: home.join(".aider/mcp.json"),
190            detect_path: home.join(".aider"),
191            config_type: ConfigType::McpJson,
192        },
193        EditorTarget {
194            name: "Continue",
195            agent_key: "continue".to_string(),
196            config_path: home.join(".continue/mcp.json"),
197            detect_path: home.join(".continue"),
198            config_type: ConfigType::McpJson,
199        },
200        EditorTarget {
201            name: "Neovim (mcphub.nvim)",
202            agent_key: "neovim".to_string(),
203            config_path: home.join(".config/mcphub/servers.json"),
204            detect_path: home.join(".config/nvim"),
205            config_type: ConfigType::McpJson,
206        },
207        EditorTarget {
208            name: "Emacs (mcp.el)",
209            agent_key: "emacs".to_string(),
210            config_path: home.join(".emacs.d/mcp.json"),
211            detect_path: home.join(".emacs.d"),
212            config_type: ConfigType::McpJson,
213        },
214        EditorTarget {
215            name: "Sublime Text",
216            agent_key: "sublime".to_string(),
217            config_path: detect_sublime_mcp_path(home),
218            detect_path: detect_sublime_path(home),
219            config_type: ConfigType::McpJson,
220        },
221    ];
222
223    targets.extend(
224        qoder_all_mcp_paths(home)
225            .into_iter()
226            .map(|config_path| EditorTarget {
227                name: "Qoder",
228                agent_key: "qoder".to_string(),
229                config_path,
230                detect_path: detect_qoder_path(home),
231                config_type: ConfigType::QoderSettings,
232            }),
233    );
234
235    targets
236}
237
238fn detect_qoder_path(home: &Path) -> PathBuf {
239    let qoder_dir = home.join(".qoder");
240    if qoder_dir.exists() {
241        return qoder_dir;
242    }
243    #[cfg(target_os = "macos")]
244    {
245        let app_dir = home.join("Library/Application Support/Qoder");
246        if app_dir.exists() {
247            return app_dir;
248        }
249    }
250    #[cfg(target_os = "windows")]
251    {
252        if let Ok(appdata) = std::env::var("APPDATA") {
253            let app_dir = PathBuf::from(appdata).join("Qoder");
254            if app_dir.exists() {
255                return app_dir;
256            }
257        }
258    }
259    PathBuf::from("/nonexistent")
260}
261
262fn detect_qoderwork_path(home: &Path) -> PathBuf {
263    let dir = home.join(".qoderwork");
264    if dir.exists() {
265        return dir;
266    }
267    #[cfg(target_os = "windows")]
268    {
269        if let Ok(appdata) = std::env::var("APPDATA") {
270            let app_dir = PathBuf::from(appdata).join("QoderWork");
271            if app_dir.exists() {
272                return app_dir;
273            }
274        }
275    }
276    PathBuf::from("/nonexistent")
277}
278
279fn detect_sublime_path(home: &Path) -> PathBuf {
280    #[cfg(target_os = "macos")]
281    {
282        let app_dir = home.join("Library/Application Support/Sublime Text");
283        if app_dir.exists() {
284            return app_dir;
285        }
286    }
287    let xdg_dir = home.join(".config/sublime-text");
288    if xdg_dir.exists() {
289        return xdg_dir;
290    }
291    #[cfg(target_os = "windows")]
292    {
293        if let Ok(appdata) = std::env::var("APPDATA") {
294            let app_dir = PathBuf::from(appdata).join("Sublime Text");
295            if app_dir.exists() {
296                return app_dir;
297            }
298        }
299    }
300    PathBuf::from("/nonexistent")
301}
302
303fn detect_sublime_mcp_path(home: &Path) -> PathBuf {
304    #[cfg(target_os = "macos")]
305    {
306        let app_dir = home.join("Library/Application Support/Sublime Text/Packages/User/mcp.json");
307        if app_dir.parent().is_some_and(std::path::Path::exists) {
308            return app_dir;
309        }
310    }
311    home.join(".config/sublime-text/mcp.json")
312}
313
314pub fn detect_claude_path() -> PathBuf {
315    let which_cmd = if cfg!(windows) { "where" } else { "which" };
316    if let Ok(output) = std::process::Command::new(which_cmd).arg("claude").output() {
317        if output.status.success() {
318            return PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
319        }
320    }
321    if let Ok(dir) = std::env::var("CLAUDE_CONFIG_DIR") {
322        let dir = dir.trim();
323        if !dir.is_empty() {
324            let p = PathBuf::from(dir);
325            if p.exists() {
326                return p;
327            }
328        }
329    }
330    if let Some(home) = dirs::home_dir() {
331        let claude_json = claude_mcp_json_path(&home);
332        if claude_json.exists() {
333            return claude_json;
334        }
335    }
336    PathBuf::from("/nonexistent")
337}
338
339pub fn detect_codex_path(home: &Path) -> PathBuf {
340    let codex_dir = crate::core::home::resolve_codex_dir().unwrap_or_else(|| home.join(".codex"));
341    if codex_dir.exists() {
342        return codex_dir;
343    }
344    let which_cmd = if cfg!(windows) { "where" } else { "which" };
345    if let Ok(output) = std::process::Command::new(which_cmd).arg("codex").output() {
346        if output.status.success() {
347            return codex_dir;
348        }
349    }
350    PathBuf::from("/nonexistent")
351}
352
353pub fn detect_vscode_path() -> PathBuf {
354    #[cfg(target_os = "macos")]
355    {
356        if let Some(home) = dirs::home_dir() {
357            let vscode = home.join("Library/Application Support/Code/User/settings.json");
358            if vscode.exists() {
359                return vscode;
360            }
361        }
362    }
363    #[cfg(target_os = "linux")]
364    {
365        if let Some(home) = dirs::home_dir() {
366            let vscode = home.join(".config/Code/User/settings.json");
367            if vscode.exists() {
368                return vscode;
369            }
370        }
371    }
372    #[cfg(target_os = "windows")]
373    {
374        if let Ok(appdata) = std::env::var("APPDATA") {
375            let vscode = PathBuf::from(appdata).join("Code/User/settings.json");
376            if vscode.exists() {
377                return vscode;
378            }
379        }
380    }
381    let which_cmd = if cfg!(windows) { "where" } else { "which" };
382    if let Ok(output) = std::process::Command::new(which_cmd).arg("code").output() {
383        if output.status.success() {
384            return PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
385        }
386    }
387    PathBuf::from("/nonexistent")
388}
389
390pub fn detect_jetbrains_path(home: &Path) -> PathBuf {
391    #[cfg(target_os = "macos")]
392    {
393        let lib = home.join("Library/Application Support/JetBrains");
394        if lib.exists() {
395            return lib;
396        }
397    }
398    #[cfg(target_os = "linux")]
399    {
400        let cfg = home.join(".config/JetBrains");
401        if cfg.exists() {
402            return cfg;
403        }
404    }
405    #[cfg(target_os = "windows")]
406    {
407        if let Ok(appdata) = std::env::var("APPDATA") {
408            let jb = std::path::PathBuf::from(appdata).join("JetBrains");
409            if jb.exists() {
410                return jb;
411            }
412        }
413        if let Ok(local) = std::env::var("LOCALAPPDATA") {
414            let jb = std::path::PathBuf::from(local).join("JetBrains");
415            if jb.exists() {
416                return jb;
417            }
418        }
419    }
420    if home.join(".jb-mcp.json").exists() {
421        return home.join(".jb-mcp.json");
422    }
423    PathBuf::from("/nonexistent")
424}
425
426#[allow(unreachable_code)]
427pub fn detect_cline_path() -> PathBuf {
428    #[cfg(target_os = "windows")]
429    {
430        if let Ok(appdata) = std::env::var("APPDATA") {
431            let p = PathBuf::from(appdata).join("Code/User/globalStorage/saoudrizwan.claude-dev");
432            if p.exists() {
433                return p;
434            }
435        }
436        return PathBuf::from("/nonexistent");
437    }
438
439    let Some(home) = dirs::home_dir() else {
440        return PathBuf::from("/nonexistent");
441    };
442    #[cfg(target_os = "macos")]
443    {
444        let p =
445            home.join("Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev");
446        if p.exists() {
447            return p;
448        }
449    }
450    #[cfg(target_os = "linux")]
451    {
452        let p = home.join(".config/Code/User/globalStorage/saoudrizwan.claude-dev");
453        if p.exists() {
454            return p;
455        }
456    }
457    PathBuf::from("/nonexistent")
458}
459
460#[allow(unreachable_code)]
461pub fn detect_roo_path() -> PathBuf {
462    #[cfg(target_os = "windows")]
463    {
464        if let Ok(appdata) = std::env::var("APPDATA") {
465            let p =
466                PathBuf::from(appdata).join("Code/User/globalStorage/rooveterinaryinc.roo-cline");
467            if p.exists() {
468                return p;
469            }
470        }
471        return PathBuf::from("/nonexistent");
472    }
473
474    let Some(home) = dirs::home_dir() else {
475        return PathBuf::from("/nonexistent");
476    };
477    #[cfg(target_os = "macos")]
478    {
479        let p = home
480            .join("Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline");
481        if p.exists() {
482            return p;
483        }
484    }
485    #[cfg(target_os = "linux")]
486    {
487        let p = home.join(".config/Code/User/globalStorage/rooveterinaryinc.roo-cline");
488        if p.exists() {
489            return p;
490        }
491    }
492    PathBuf::from("/nonexistent")
493}
494
495#[cfg(all(test, target_os = "macos"))]
496mod tests {
497    use super::*;
498
499    #[test]
500    #[cfg(target_os = "macos")]
501    fn build_targets_includes_all_qoder_macos_mcp_locations() {
502        let home = Path::new("/Users/tester");
503        let qoder_paths: Vec<_> = build_targets(home)
504            .into_iter()
505            .filter(|target| target.agent_key == "qoder")
506            .map(|target| target.config_path)
507            .collect();
508
509        assert_eq!(
510            qoder_paths,
511            vec![
512                home.join(".qoder/mcp.json"),
513                home.join("Library/Application Support/Qoder/User/mcp.json"),
514                home.join("Library/Application Support/Qoder/SharedClientCache/mcp.json"),
515            ]
516        );
517    }
518}