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}