pub mod claude;
pub mod cursor;
pub mod vscode;
pub mod zed;
use std::collections::BTreeMap;
use clap::{Arg, ArgAction};
use crate::Result;
pub fn arg_config_path() -> Arg {
Arg::new("config-path")
.long("config-path")
.value_name("PATH")
.help("Path to the editor's MCP config file (overrides per-OS default)")
}
pub fn arg_server_name() -> Arg {
Arg::new("server-name")
.long("server-name")
.value_name("NAME")
.help("Name for the MCP server (default: derived from executable name)")
}
pub fn arg_env() -> Arg {
Arg::new("env")
.long("env")
.short('e')
.value_name("KEY=VAL")
.help("Environment variable (repeatable; e.g. -e KEY1=val1 -e KEY2=val2)")
.action(ArgAction::Append)
}
pub fn arg_log_level() -> Arg {
Arg::new("log-level")
.long("log-level")
.value_name("LEVEL")
.help("Log level for the spawned MCP server (trace, debug, info, warn, error)")
}
pub fn merge_env(
default_env: &std::collections::HashMap<String, String>,
user_pairs: &[String],
) -> Result<Option<BTreeMap<String, String>>> {
let mut merged: BTreeMap<String, String> = BTreeMap::new();
for (k, v) in default_env {
merged.insert(k.clone(), v.clone());
}
for raw in user_pairs {
let (k, v) = parse_env_pair(raw)?;
merged.insert(k, v);
}
Ok(if merged.is_empty() {
None
} else {
Some(merged)
})
}
fn parse_env_pair(raw: &str) -> Result<(String, String)> {
let Some(idx) = raw.find('=') else {
return Err(crate::Error::Config(format!(
"--env value {raw:?} missing '=' separator (expected KEY=VAL)"
)));
};
let (k, v_with_eq) = raw.split_at(idx);
if k.is_empty() {
return Err(crate::Error::Config(format!(
"--env value {raw:?} has empty key (expected KEY=VAL)"
)));
}
let v = &v_with_eq[1..];
Ok((k.to_string(), v.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn merge_env_empty_returns_none() {
let default = HashMap::new();
let user: Vec<String> = Vec::new();
let result = merge_env(&default, &user).expect("merges");
assert!(result.is_none(), "empty merged map must collapse to None");
}
#[test]
fn merge_env_default_only() {
let mut default = HashMap::new();
default.insert("PATH".into(), "/usr/bin".into());
let user: Vec<String> = Vec::new();
let merged = merge_env(&default, &user)
.expect("merges")
.expect("non-empty");
assert_eq!(merged.get("PATH").map(String::as_str), Some("/usr/bin"));
assert_eq!(merged.len(), 1);
}
#[test]
fn merge_env_user_wins_on_conflict() {
let mut default = HashMap::new();
default.insert("PATH".into(), "/default".into());
default.insert("HOME".into(), "/home/default".into());
let user = vec!["PATH=/user".to_string()];
let merged = merge_env(&default, &user)
.expect("merges")
.expect("non-empty");
assert_eq!(merged.get("PATH").map(String::as_str), Some("/user"));
assert_eq!(
merged.get("HOME").map(String::as_str),
Some("/home/default")
);
}
#[test]
fn merge_env_user_only() {
let default = HashMap::new();
let user = vec!["FOO=bar".to_string()];
let merged = merge_env(&default, &user)
.expect("merges")
.expect("non-empty");
assert_eq!(merged.get("FOO").map(String::as_str), Some("bar"));
}
#[test]
fn merge_env_rejects_missing_separator() {
let default = HashMap::new();
let user = vec!["BAD".to_string()];
let err = merge_env(&default, &user).expect_err("must reject missing '='");
let msg = err.to_string();
assert!(msg.contains("missing '='"), "got: {msg}");
}
#[test]
fn merge_env_rejects_empty_key() {
let default = HashMap::new();
let user = vec!["=val".to_string()];
let err = merge_env(&default, &user).expect_err("must reject empty key");
let msg = err.to_string();
assert!(msg.contains("empty key"), "got: {msg}");
}
#[test]
fn merge_env_value_with_equals_survives() {
let default = HashMap::new();
let user = vec!["KEY=foo=bar=baz".to_string()];
let merged = merge_env(&default, &user)
.expect("merges")
.expect("non-empty");
assert_eq!(merged.get("KEY").map(String::as_str), Some("foo=bar=baz"));
}
}