use std::path::{Path, PathBuf};
use super::{ClientSpec, MergeStrategy, Scope};
use crate::cli::{ClientKind, IdeScope};
pub(super) const CLIENT_CATALOGUE: &[CatalogueEntry] = &[
CatalogueEntry {
kind: ClientKind::ClaudeCode,
scope: Scope::Project,
strategy: MergeStrategy::McpServers,
},
CatalogueEntry {
kind: ClientKind::ClaudeCode,
scope: Scope::User,
strategy: MergeStrategy::McpServers,
},
CatalogueEntry {
kind: ClientKind::Cursor,
scope: Scope::Project,
strategy: MergeStrategy::McpServers,
},
CatalogueEntry {
kind: ClientKind::Vscode,
scope: Scope::Project,
strategy: MergeStrategy::VsCodeServers,
},
CatalogueEntry {
kind: ClientKind::Vscode,
scope: Scope::User,
strategy: MergeStrategy::VsCodeServers,
},
CatalogueEntry {
kind: ClientKind::ClaudeDesktop,
scope: Scope::User,
strategy: MergeStrategy::McpServers,
},
CatalogueEntry {
kind: ClientKind::Windsurf,
scope: Scope::User,
strategy: MergeStrategy::McpServers,
},
CatalogueEntry {
kind: ClientKind::Opencode,
scope: Scope::User,
strategy: MergeStrategy::Mcp,
},
CatalogueEntry {
kind: ClientKind::Zed,
scope: Scope::User,
strategy: MergeStrategy::ContextServers,
},
CatalogueEntry {
kind: ClientKind::Codex,
scope: Scope::User,
strategy: MergeStrategy::CodexToml,
},
CatalogueEntry {
kind: ClientKind::Gemini,
scope: Scope::User,
strategy: MergeStrategy::McpServers,
},
CatalogueEntry {
kind: ClientKind::Antigravity,
scope: Scope::User,
strategy: MergeStrategy::McpServers,
},
CatalogueEntry {
kind: ClientKind::Kiro,
scope: Scope::Project,
strategy: MergeStrategy::McpServers,
},
CatalogueEntry {
kind: ClientKind::Kiro,
scope: Scope::User,
strategy: MergeStrategy::McpServers,
},
CatalogueEntry {
kind: ClientKind::Hermes,
scope: Scope::User,
strategy: MergeStrategy::HermesYaml,
},
];
pub(super) struct CatalogueEntry {
pub(super) kind: ClientKind,
pub(super) scope: Scope,
pub(super) strategy: MergeStrategy,
}
pub(super) fn serve_args(no_watch: bool) -> Vec<String> {
if no_watch {
vec!["serve".into()]
} else {
vec!["serve".into(), "--watch".into()]
}
}
pub(super) fn project_path(kind: ClientKind, cwd: &Path) -> Option<PathBuf> {
match kind {
ClientKind::ClaudeCode => Some(cwd.join(".mcp.json")),
ClientKind::Cursor => Some(cwd.join(".cursor").join("mcp.json")),
ClientKind::Vscode => Some(cwd.join(".vscode").join("mcp.json")),
ClientKind::Kiro => Some(cwd.join(".kiro").join("settings").join("mcp.json")),
_ => None,
}
}
pub(super) fn user_path(kind: ClientKind, home: &HomeDirs) -> Option<PathBuf> {
match kind {
ClientKind::ClaudeCode => Some(home.claude_code.clone()),
ClientKind::ClaudeDesktop => Some(home.claude_desktop.clone()),
ClientKind::Codex => Some(home.codex.clone()),
ClientKind::Gemini => Some(home.gemini.clone()),
ClientKind::Opencode => Some(home.opencode.clone()),
ClientKind::Windsurf => Some(home.windsurf.clone()),
ClientKind::Zed => Some(home.zed.clone()),
ClientKind::Antigravity => Some(home.antigravity.clone()),
ClientKind::Hermes => Some(home.hermes.clone()),
ClientKind::Kiro => Some(home.kiro.clone()),
ClientKind::Vscode => Some(home.vscode.clone()),
ClientKind::Cursor => None,
}
}
pub fn build_specs(
client: Option<ClientKind>,
scope: IdeScope,
no_watch: bool,
cwd: &Path,
home_dirs: &HomeDirs,
) -> Vec<ClientSpec> {
CLIENT_CATALOGUE
.iter()
.filter(|e| match scope {
IdeScope::Project => e.scope == Scope::Project,
IdeScope::User => e.scope == Scope::User,
IdeScope::All => true,
})
.filter(|e| client.map_or(true, |c| c == e.kind))
.filter_map(|e| {
let path = match e.scope {
Scope::Project => project_path(e.kind, cwd)?,
Scope::User => user_path(e.kind, home_dirs)?,
};
Some(ClientSpec {
kind: e.kind,
scope: e.scope,
path,
strategy: e.strategy,
args: serve_args(no_watch),
})
})
.collect()
}
#[derive(Debug, Clone)]
pub struct HomeDirs {
pub claude_code: PathBuf,
pub claude_desktop: PathBuf,
pub codex: PathBuf,
pub gemini: PathBuf,
pub windsurf: PathBuf,
pub opencode: PathBuf,
pub zed: PathBuf,
pub antigravity: PathBuf,
pub hermes: PathBuf,
pub kiro: PathBuf,
pub vscode: PathBuf,
}
impl Default for HomeDirs {
fn default() -> Self {
let stub = PathBuf::from(".");
HomeDirs {
claude_code: stub.clone(),
claude_desktop: stub.clone(),
codex: stub.clone(),
gemini: stub.clone(),
windsurf: stub.clone(),
opencode: stub.clone(),
zed: stub.clone(),
antigravity: stub.clone(),
hermes: stub.clone(),
kiro: stub.clone(),
vscode: stub,
}
}
}
impl HomeDirs {
pub fn detect() -> Self {
let Some(base) = directories::BaseDirs::new() else {
return Self::default();
};
let home = base.home_dir().to_path_buf();
let xdg_config = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".config"));
let claude_desktop = if cfg!(target_os = "macos") {
home.join("Library/Application Support/Claude/claude_desktop_config.json")
} else if cfg!(target_os = "windows") {
base.config_dir().join("Claude/claude_desktop_config.json")
} else {
xdg_config.join("Claude/claude_desktop_config.json")
};
let vscode = base.config_dir().join("Code/User/mcp.json");
HomeDirs {
claude_code: home.join(".claude/settings.json"),
claude_desktop,
codex: home.join(".codex/config.toml"),
gemini: home.join(".gemini/settings.json"),
windsurf: home.join(".codeium/windsurf/mcp_config.json"),
opencode: xdg_config.join("opencode/opencode.json"),
zed: xdg_config.join("zed/settings.json"),
antigravity: home.join(".gemini/config/mcp_config.json"),
hermes: home.join(".hermes/config.yaml"),
kiro: home.join(".kiro/settings/mcp.json"),
vscode,
}
}
}
fn client_binary(kind: ClientKind) -> Option<&'static str> {
match kind {
ClientKind::ClaudeCode => Some("claude"),
ClientKind::Codex => Some("codex"),
ClientKind::Gemini => Some("gemini"),
ClientKind::Opencode => Some("opencode"),
ClientKind::Windsurf => Some("windsurf"),
ClientKind::Zed => Some("zed"),
ClientKind::Hermes => Some("hermes"),
ClientKind::Kiro => Some("kiro"),
ClientKind::ClaudeDesktop
| ClientKind::Cursor
| ClientKind::Vscode
| ClientKind::Antigravity => None,
}
}
pub(super) fn binary_in(paths: &std::ffi::OsStr, name: &str) -> bool {
let mut candidates = vec![name.to_string()];
if cfg!(windows) {
let pathext =
std::env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_string());
for ext in pathext.split(';').filter(|e| !e.is_empty()) {
candidates.push(format!("{name}{}", ext.to_ascii_lowercase()));
}
}
std::env::split_paths(paths).any(|dir| candidates.iter().any(|c| dir.join(c).is_file()))
}
pub(super) fn client_installed(
kind: ClientKind,
scope: Scope,
path: &Path,
path_env: Option<&std::ffi::OsStr>,
) -> bool {
if scope == Scope::Project {
return true;
}
if path.is_absolute() {
if let (Some(bin), Some(p)) = (client_binary(kind), path_env) {
if binary_in(p, bin) {
return true;
}
}
}
path.parent().is_some_and(Path::exists)
}
pub(super) fn client_display_name(kind: ClientKind) -> &'static str {
match kind {
ClientKind::ClaudeCode => "Claude Code",
ClientKind::ClaudeDesktop => "Claude Desktop",
ClientKind::Cursor => "Cursor",
ClientKind::Vscode => "VS Code",
ClientKind::Windsurf => "Windsurf",
ClientKind::Opencode => "OpenCode",
ClientKind::Zed => "Zed",
ClientKind::Codex => "Codex CLI",
ClientKind::Gemini => "Gemini CLI",
ClientKind::Antigravity => "Antigravity",
ClientKind::Hermes => "Hermes Agent",
ClientKind::Kiro => "Kiro",
}
}
pub(super) fn client_name(kind: ClientKind) -> &'static str {
match kind {
ClientKind::ClaudeCode => "claude-code",
ClientKind::ClaudeDesktop => "claude-desktop",
ClientKind::Codex => "codex",
ClientKind::Cursor => "cursor",
ClientKind::Gemini => "gemini",
ClientKind::Opencode => "opencode",
ClientKind::Vscode => "vscode",
ClientKind::Windsurf => "windsurf",
ClientKind::Zed => "zed",
ClientKind::Antigravity => "antigravity",
ClientKind::Hermes => "hermes",
ClientKind::Kiro => "kiro",
}
}