1use std::path::{Path, PathBuf};
2
3use super::paths::{
4 augment_cli_settings_path, augment_vscode_mcp_path, claude_mcp_json_path, cline_mcp_path,
5 qoder_all_mcp_paths, qoderwork_mcp_path, roo_mcp_path, vscode_mcp_path, zed_config_dir,
6 zed_settings_path,
7};
8use super::types::{ConfigType, EditorTarget};
9
10pub fn build_targets(home: &Path) -> Vec<EditorTarget> {
11 #[cfg(windows)]
12 let opencode_cfg = if let Ok(appdata) = std::env::var("APPDATA") {
13 PathBuf::from(appdata)
14 .join("opencode")
15 .join("opencode.json")
16 } else {
17 home.join(".config/opencode/opencode.json")
18 };
19 #[cfg(not(windows))]
20 let opencode_cfg = home.join(".config/opencode/opencode.json");
21
22 #[cfg(windows)]
23 let opencode_detect = opencode_cfg
24 .parent()
25 .map(|p| p.to_path_buf())
26 .unwrap_or_else(|| home.join(".config/opencode"));
27 #[cfg(not(windows))]
28 let opencode_detect = home.join(".config/opencode");
29
30 let mut targets = vec![
31 EditorTarget {
32 name: "Cursor",
33 agent_key: "cursor".to_string(),
34 config_path: home.join(".cursor/mcp.json"),
35 detect_path: home.join(".cursor"),
36 config_type: ConfigType::McpJson,
37 },
38 EditorTarget {
39 name: "Claude Code",
40 agent_key: "claude".to_string(),
41 config_path: claude_mcp_json_path(home),
42 detect_path: detect_claude_path(),
43 config_type: ConfigType::McpJson,
44 },
45 EditorTarget {
46 name: "Augment CLI",
47 agent_key: "augment".to_string(),
48 config_path: augment_cli_settings_path(home),
49 detect_path: detect_augment_path(home),
50 config_type: ConfigType::McpJson,
51 },
52 EditorTarget {
53 name: "Augment (VS Code)",
54 agent_key: "augment".to_string(),
55 config_path: augment_vscode_mcp_path(home),
56 detect_path: detect_augment_vscode_path(home),
57 config_type: ConfigType::AugmentVsCode,
58 },
59 EditorTarget {
60 name: "Windsurf",
61 agent_key: "windsurf".to_string(),
62 config_path: home.join(".codeium/windsurf/mcp_config.json"),
63 detect_path: home.join(".codeium/windsurf"),
64 config_type: ConfigType::McpJson,
65 },
66 EditorTarget {
67 name: "Codex CLI",
68 agent_key: "codex".to_string(),
69 config_path: crate::core::home::resolve_codex_dir()
70 .unwrap_or_else(|| home.join(".codex"))
71 .join("config.toml"),
72 detect_path: detect_codex_path(home),
73 config_type: ConfigType::Codex,
74 },
75 EditorTarget {
76 name: "Gemini CLI",
77 agent_key: "gemini".to_string(),
78 config_path: home.join(".gemini/settings.json"),
79 detect_path: home.join(".gemini"),
80 config_type: ConfigType::GeminiSettings,
81 },
82 EditorTarget {
83 name: "Antigravity IDE",
84 agent_key: "antigravity".to_string(),
85 config_path: home.join(".gemini/antigravity/mcp_config.json"),
86 detect_path: home.join(".gemini/antigravity"),
87 config_type: ConfigType::McpJson,
88 },
89 EditorTarget {
90 name: "Antigravity CLI",
91 agent_key: "antigravity-cli".to_string(),
92 config_path: home.join(".gemini/antigravity-cli/mcp_config.json"),
93 detect_path: home.join(".gemini/antigravity-cli"),
94 config_type: ConfigType::McpJson,
95 },
96 EditorTarget {
97 name: "Zed",
98 agent_key: "zed".to_string(),
99 config_path: zed_settings_path(home),
100 detect_path: zed_config_dir(home),
101 config_type: ConfigType::Zed,
102 },
103 EditorTarget {
104 name: "VS Code",
105 agent_key: "vscode".to_string(),
106 config_path: vscode_mcp_path(),
107 detect_path: detect_vscode_path(),
108 config_type: ConfigType::VsCodeMcp,
109 },
110 EditorTarget {
111 name: "Copilot CLI",
112 agent_key: "copilot".to_string(),
113 config_path: home.join(".copilot/mcp-config.json"),
114 detect_path: home.join(".copilot"),
115 config_type: ConfigType::CopilotCli,
116 },
117 EditorTarget {
118 name: "OpenCode",
119 agent_key: "opencode".to_string(),
120 config_path: opencode_cfg,
121 detect_path: opencode_detect,
122 config_type: ConfigType::OpenCode,
123 },
124 EditorTarget {
125 name: "Qwen Code",
126 agent_key: "qwen".to_string(),
127 config_path: home.join(".qwen/settings.json"),
128 detect_path: home.join(".qwen"),
129 config_type: ConfigType::McpJson,
130 },
131 EditorTarget {
132 name: "Trae",
133 agent_key: "trae".to_string(),
134 config_path: home.join(".trae/mcp.json"),
135 detect_path: home.join(".trae"),
136 config_type: ConfigType::McpJson,
137 },
138 EditorTarget {
139 name: "Amazon Q Developer",
140 agent_key: "amazonq".to_string(),
141 config_path: home.join(".aws/amazonq/default.json"),
142 detect_path: home.join(".aws/amazonq"),
143 config_type: ConfigType::McpJson,
144 },
145 EditorTarget {
146 name: "JetBrains IDEs",
147 agent_key: "jetbrains".to_string(),
148 config_path: home.join(".jb-mcp.json"),
149 detect_path: detect_jetbrains_path(home),
150 config_type: ConfigType::JetBrains,
151 },
152 EditorTarget {
153 name: "Cline",
154 agent_key: "cline".to_string(),
155 config_path: cline_mcp_path(),
156 detect_path: detect_cline_path(),
157 config_type: ConfigType::McpJson,
158 },
159 EditorTarget {
160 name: "Roo Code",
161 agent_key: "roo".to_string(),
162 config_path: roo_mcp_path(),
163 detect_path: detect_roo_path(),
164 config_type: ConfigType::McpJson,
165 },
166 EditorTarget {
167 name: "AWS Kiro",
168 agent_key: "kiro".to_string(),
169 config_path: home.join(".kiro/settings/mcp.json"),
170 detect_path: home.join(".kiro"),
171 config_type: ConfigType::McpJson,
172 },
173 EditorTarget {
174 name: "Verdent",
175 agent_key: "verdent".to_string(),
176 config_path: home.join(".verdent/mcp.json"),
177 detect_path: home.join(".verdent"),
178 config_type: ConfigType::McpJson,
179 },
180 EditorTarget {
181 name: "Crush",
182 agent_key: "crush".to_string(),
183 config_path: home.join(".config/crush/crush.json"),
184 detect_path: home.join(".config/crush"),
185 config_type: ConfigType::Crush,
186 },
187 EditorTarget {
188 name: "Pi Coding Agent",
189 agent_key: "pi".to_string(),
190 config_path: home.join(".pi/agent/mcp.json"),
191 detect_path: home.join(".pi/agent"),
192 config_type: ConfigType::McpJson,
193 },
194 EditorTarget {
195 name: "Amp",
196 agent_key: "amp".to_string(),
197 config_path: home.join(".config/amp/settings.json"),
198 detect_path: home.join(".config/amp"),
199 config_type: ConfigType::Amp,
200 },
201 EditorTarget {
202 name: "QoderWork",
203 agent_key: "qoderwork".to_string(),
204 config_path: qoderwork_mcp_path(home),
205 detect_path: detect_qoderwork_path(home),
206 config_type: ConfigType::McpJson,
207 },
208 EditorTarget {
209 name: "Hermes Agent",
210 agent_key: "hermes".to_string(),
211 config_path: home.join(".hermes/config.yaml"),
212 detect_path: home.join(".hermes"),
213 config_type: ConfigType::HermesYaml,
214 },
215 EditorTarget {
216 name: "Aider",
217 agent_key: "aider".to_string(),
218 config_path: home.join(".aider/mcp.json"),
219 detect_path: home.join(".aider"),
220 config_type: ConfigType::McpJson,
221 },
222 EditorTarget {
223 name: "Continue",
224 agent_key: "continue".to_string(),
225 config_path: home.join(".continue/mcp.json"),
226 detect_path: home.join(".continue"),
227 config_type: ConfigType::McpJson,
228 },
229 EditorTarget {
230 name: "Neovim (mcphub.nvim)",
231 agent_key: "neovim".to_string(),
232 config_path: home.join(".config/mcphub/servers.json"),
233 detect_path: home.join(".config/nvim"),
234 config_type: ConfigType::McpJson,
235 },
236 EditorTarget {
237 name: "Emacs (mcp.el)",
238 agent_key: "emacs".to_string(),
239 config_path: home.join(".emacs.d/mcp.json"),
240 detect_path: home.join(".emacs.d"),
241 config_type: ConfigType::McpJson,
242 },
243 EditorTarget {
244 name: "Sublime Text",
245 agent_key: "sublime".to_string(),
246 config_path: detect_sublime_mcp_path(home),
247 detect_path: detect_sublime_path(home),
248 config_type: ConfigType::McpJson,
249 },
250 EditorTarget {
251 name: "OpenClaw",
252 agent_key: "openclaw".to_string(),
253 config_path: home.join(".openclaw/openclaw.json"),
254 detect_path: home.join(".openclaw"),
255 config_type: ConfigType::McpJson,
256 },
257 ];
258
259 targets.extend(
260 qoder_all_mcp_paths(home)
261 .into_iter()
262 .map(|config_path| EditorTarget {
263 name: "Qoder",
264 agent_key: "qoder".to_string(),
265 config_path,
266 detect_path: detect_qoder_path(home),
267 config_type: ConfigType::QoderSettings,
268 }),
269 );
270
271 targets
272}
273
274fn detect_qoder_path(home: &Path) -> PathBuf {
275 let qoder_dir = home.join(".qoder");
276 if qoder_dir.exists() {
277 return qoder_dir;
278 }
279 #[cfg(target_os = "macos")]
280 {
281 let app_dir = home.join("Library/Application Support/Qoder");
282 if app_dir.exists() {
283 return app_dir;
284 }
285 }
286 #[cfg(target_os = "windows")]
287 {
288 if let Ok(appdata) = std::env::var("APPDATA") {
289 let app_dir = PathBuf::from(appdata).join("Qoder");
290 if app_dir.exists() {
291 return app_dir;
292 }
293 }
294 }
295 PathBuf::from("/nonexistent")
296}
297
298fn detect_qoderwork_path(home: &Path) -> PathBuf {
299 let dir = home.join(".qoderwork");
300 if dir.exists() {
301 return dir;
302 }
303 #[cfg(target_os = "windows")]
304 {
305 if let Ok(appdata) = std::env::var("APPDATA") {
306 let app_dir = PathBuf::from(appdata).join("QoderWork");
307 if app_dir.exists() {
308 return app_dir;
309 }
310 }
311 }
312 PathBuf::from("/nonexistent")
313}
314
315fn detect_sublime_path(home: &Path) -> PathBuf {
316 #[cfg(target_os = "macos")]
317 {
318 let app_dir = home.join("Library/Application Support/Sublime Text");
319 if app_dir.exists() {
320 return app_dir;
321 }
322 }
323 let xdg_dir = home.join(".config/sublime-text");
324 if xdg_dir.exists() {
325 return xdg_dir;
326 }
327 #[cfg(target_os = "windows")]
328 {
329 if let Ok(appdata) = std::env::var("APPDATA") {
330 let app_dir = PathBuf::from(appdata).join("Sublime Text");
331 if app_dir.exists() {
332 return app_dir;
333 }
334 }
335 }
336 PathBuf::from("/nonexistent")
337}
338
339fn detect_sublime_mcp_path(home: &Path) -> PathBuf {
340 #[cfg(target_os = "macos")]
341 {
342 let app_dir = home.join("Library/Application Support/Sublime Text/Packages/User/mcp.json");
343 if app_dir.parent().is_some_and(std::path::Path::exists) {
344 return app_dir;
345 }
346 }
347 home.join(".config/sublime-text/mcp.json")
348}
349
350pub fn detect_claude_path() -> PathBuf {
351 let which_cmd = if cfg!(windows) { "where" } else { "which" };
352 if let Ok(output) = std::process::Command::new(which_cmd).arg("claude").output() {
353 if output.status.success() {
354 return PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
355 }
356 }
357 if let Ok(dir) = std::env::var("CLAUDE_CONFIG_DIR") {
358 let dir = dir.trim();
359 if !dir.is_empty() {
360 let p = PathBuf::from(dir);
361 if p.exists() {
362 return p;
363 }
364 }
365 }
366 if let Some(home) = dirs::home_dir() {
367 let claude_json = claude_mcp_json_path(&home);
368 if claude_json.exists() {
369 return claude_json;
370 }
371 }
372 PathBuf::from("/nonexistent")
373}
374
375pub fn detect_augment_path(home: &Path) -> PathBuf {
376 let which_cmd = if cfg!(windows) { "where" } else { "which" };
377 if let Ok(output) = std::process::Command::new(which_cmd).arg("auggie").output() {
378 if output.status.success() {
379 return PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
380 }
381 }
382 let augment_dir = home.join(".augment");
383 if augment_dir.exists() {
384 return augment_dir;
385 }
386 PathBuf::from("/nonexistent")
387}
388
389pub fn detect_augment_vscode_path(home: &Path) -> PathBuf {
409 let mcp_path = augment_vscode_mcp_path(home);
410 if mcp_path.exists() {
411 return mcp_path;
412 }
413 let extension_state = mcp_path
414 .parent()
415 .and_then(|p| p.parent())
416 .map(Path::to_path_buf);
417 if let Some(path) = extension_state {
418 if path.exists() {
419 return path;
420 }
421 }
422 if detect_extension_installed(home, "augment.vscode-augment") {
423 return mcp_path;
424 }
425 PathBuf::from("/nonexistent")
426}
427
428fn detect_extension_installed(home: &Path, extension_id: &str) -> bool {
429 #[cfg(target_os = "macos")]
430 {
431 if home
432 .join(format!(
433 "Library/Application Support/Code/User/globalStorage/{extension_id}"
434 ))
435 .exists()
436 {
437 return true;
438 }
439 }
440 #[cfg(target_os = "linux")]
441 {
442 if home
443 .join(format!(".config/Code/User/globalStorage/{extension_id}"))
444 .exists()
445 {
446 return true;
447 }
448 }
449 #[cfg(target_os = "windows")]
450 {
451 if let Ok(appdata) = std::env::var("APPDATA") {
452 if PathBuf::from(appdata)
453 .join(format!("Code/User/globalStorage/{extension_id}"))
454 .exists()
455 {
456 return true;
457 }
458 }
459 let _ = home;
460 }
461 false
462}
463
464pub fn detect_codex_path(home: &Path) -> PathBuf {
465 let codex_dir = crate::core::home::resolve_codex_dir().unwrap_or_else(|| home.join(".codex"));
466 if codex_dir.exists() {
467 return codex_dir;
468 }
469 let which_cmd = if cfg!(windows) { "where" } else { "which" };
470 if let Ok(output) = std::process::Command::new(which_cmd).arg("codex").output() {
471 if output.status.success() {
472 return codex_dir;
473 }
474 }
475 PathBuf::from("/nonexistent")
476}
477
478pub fn detect_vscode_path() -> PathBuf {
479 #[cfg(target_os = "macos")]
480 {
481 if let Some(home) = dirs::home_dir() {
482 let vscode = home.join("Library/Application Support/Code/User/settings.json");
483 if vscode.exists() {
484 return vscode;
485 }
486 }
487 }
488 #[cfg(target_os = "linux")]
489 {
490 if let Some(home) = dirs::home_dir() {
491 let vscode = home.join(".config/Code/User/settings.json");
492 if vscode.exists() {
493 return vscode;
494 }
495 }
496 }
497 #[cfg(target_os = "windows")]
498 {
499 if let Ok(appdata) = std::env::var("APPDATA") {
500 let vscode = PathBuf::from(appdata).join("Code/User/settings.json");
501 if vscode.exists() {
502 return vscode;
503 }
504 }
505 }
506 let which_cmd = if cfg!(windows) { "where" } else { "which" };
507 if let Ok(output) = std::process::Command::new(which_cmd).arg("code").output() {
508 if output.status.success() {
509 return PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
510 }
511 }
512 PathBuf::from("/nonexistent")
513}
514
515pub fn detect_jetbrains_path(home: &Path) -> PathBuf {
516 #[cfg(target_os = "macos")]
517 {
518 let lib = home.join("Library/Application Support/JetBrains");
519 if lib.exists() {
520 return lib;
521 }
522 }
523 #[cfg(target_os = "linux")]
524 {
525 let cfg = home.join(".config/JetBrains");
526 if cfg.exists() {
527 return cfg;
528 }
529 }
530 #[cfg(target_os = "windows")]
531 {
532 if let Ok(appdata) = std::env::var("APPDATA") {
533 let jb = std::path::PathBuf::from(appdata).join("JetBrains");
534 if jb.exists() {
535 return jb;
536 }
537 }
538 if let Ok(local) = std::env::var("LOCALAPPDATA") {
539 let jb = std::path::PathBuf::from(local).join("JetBrains");
540 if jb.exists() {
541 return jb;
542 }
543 }
544 }
545 if home.join(".jb-mcp.json").exists() {
546 return home.join(".jb-mcp.json");
547 }
548 PathBuf::from("/nonexistent")
549}
550
551#[allow(unreachable_code)]
552pub fn detect_cline_path() -> PathBuf {
553 #[cfg(target_os = "windows")]
554 {
555 if let Ok(appdata) = std::env::var("APPDATA") {
556 let p = PathBuf::from(appdata).join("Code/User/globalStorage/saoudrizwan.claude-dev");
557 if p.exists() {
558 return p;
559 }
560 }
561 return PathBuf::from("/nonexistent");
562 }
563
564 let Some(home) = dirs::home_dir() else {
565 return PathBuf::from("/nonexistent");
566 };
567 #[cfg(target_os = "macos")]
568 {
569 let p =
570 home.join("Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev");
571 if p.exists() {
572 return p;
573 }
574 }
575 #[cfg(target_os = "linux")]
576 {
577 let p = home.join(".config/Code/User/globalStorage/saoudrizwan.claude-dev");
578 if p.exists() {
579 return p;
580 }
581 }
582 PathBuf::from("/nonexistent")
583}
584
585#[allow(unreachable_code)]
586pub fn detect_roo_path() -> PathBuf {
587 #[cfg(target_os = "windows")]
588 {
589 if let Ok(appdata) = std::env::var("APPDATA") {
590 let p =
591 PathBuf::from(appdata).join("Code/User/globalStorage/rooveterinaryinc.roo-cline");
592 if p.exists() {
593 return p;
594 }
595 }
596 return PathBuf::from("/nonexistent");
597 }
598
599 let Some(home) = dirs::home_dir() else {
600 return PathBuf::from("/nonexistent");
601 };
602 #[cfg(target_os = "macos")]
603 {
604 let p = home
605 .join("Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline");
606 if p.exists() {
607 return p;
608 }
609 }
610 #[cfg(target_os = "linux")]
611 {
612 let p = home.join(".config/Code/User/globalStorage/rooveterinaryinc.roo-cline");
613 if p.exists() {
614 return p;
615 }
616 }
617 PathBuf::from("/nonexistent")
618}
619
620#[cfg(test)]
621mod augment_tests {
622 use super::*;
623 use crate::core::editor_registry::writers::{
624 remove_lean_ctx_server, write_config_with_options, WriteAction, WriteOptions,
625 };
626
627 #[test]
628 fn build_targets_includes_augment_cli_entry() {
629 let home = Path::new("/home/tester");
630 let target = build_targets(home)
631 .into_iter()
632 .find(|t| t.agent_key == "augment")
633 .expect("augment target should be registered");
634 assert_eq!(target.name, "Augment CLI");
635 assert_eq!(target.config_path, home.join(".augment/settings.json"));
636 assert!(matches!(target.config_type, ConfigType::McpJson));
637 }
638
639 #[test]
640 fn build_targets_includes_augment_vscode_entry() {
641 let home = Path::new("/home/tester");
642 let target = build_targets(home)
643 .into_iter()
644 .find(|t| t.name == "Augment (VS Code)")
645 .expect("augment vscode target should be registered");
646 assert_eq!(target.agent_key, "augment");
647 assert_eq!(target.config_path, augment_vscode_mcp_path(home));
648 assert!(matches!(target.config_type, ConfigType::AugmentVsCode));
649 }
650
651 #[test]
656 fn mcp_json_writer_round_trip_at_augment_settings_path_preserves_other_servers() {
657 let tmp = tempfile::tempdir().expect("tempdir");
658 let cfg = tmp.path().join(".augment").join("settings.json");
659 std::fs::create_dir_all(cfg.parent().unwrap()).unwrap();
660 std::fs::write(
661 &cfg,
662 r#"{ "mcpServers": { "other": { "command": "other-bin", "args": [] } } }"#,
663 )
664 .unwrap();
665
666 let target = EditorTarget {
667 name: "Augment CLI",
668 agent_key: "augment".to_string(),
669 config_path: cfg.clone(),
670 detect_path: PathBuf::from("/nonexistent"),
671 config_type: ConfigType::McpJson,
672 };
673
674 let install =
675 write_config_with_options(&target, "/usr/local/bin/lean-ctx", WriteOptions::default())
676 .expect("install");
677 assert!(matches!(
678 install.action,
679 WriteAction::Created | WriteAction::Updated
680 ));
681 let json: serde_json::Value =
682 serde_json::from_str(&std::fs::read_to_string(&cfg).unwrap()).unwrap();
683 assert_eq!(json["mcpServers"]["other"]["command"], "other-bin");
684 assert_eq!(
685 json["mcpServers"]["lean-ctx"]["command"],
686 "/usr/local/bin/lean-ctx"
687 );
688
689 let uninstall =
690 remove_lean_ctx_server(&target, WriteOptions::default()).expect("uninstall");
691 assert!(matches!(uninstall.action, WriteAction::Updated));
692 let json: serde_json::Value =
693 serde_json::from_str(&std::fs::read_to_string(&cfg).unwrap()).unwrap();
694 assert!(json["mcpServers"].get("lean-ctx").is_none());
695 assert_eq!(json["mcpServers"]["other"]["command"], "other-bin");
696 }
697}
698
699#[cfg(all(test, target_os = "macos"))]
700mod tests {
701 use super::*;
702
703 #[test]
704 #[cfg(target_os = "macos")]
705 fn build_targets_includes_all_qoder_macos_mcp_locations() {
706 let home = Path::new("/Users/tester");
707 let qoder_paths: Vec<_> = build_targets(home)
708 .into_iter()
709 .filter(|target| target.agent_key == "qoder")
710 .map(|target| target.config_path)
711 .collect();
712
713 assert_eq!(
714 qoder_paths,
715 vec![
716 home.join(".qoder/mcp.json"),
717 home.join("Library/Application Support/Qoder/User/mcp.json"),
718 home.join("Library/Application Support/Qoder/SharedClientCache/mcp.json"),
719 ]
720 );
721 }
722}