use anyhow::Result;
use colored::Colorize;
use std::path::Path;
use trusty_common::claude_config::{
default_settings_max_depth, discover_claude_settings, mcp_server_entry, patch_mcp_server,
};
const MCP_SERVER_KEY: &str = "trusty-search";
pub fn handle_setup() -> Result<()> {
println!(
"{} Setting up trusty-search for Claude Code…\n",
"·".dimmed()
);
let home =
dirs::home_dir().ok_or_else(|| anyhow::anyhow!("could not resolve home directory"))?;
println!(
"{} Scanning for Claude settings under {}…",
"·".dimmed(),
home.display()
);
let entry = mcp_server_entry(MCP_SERVER_KEY, &["serve"]);
let files = discover_claude_settings(&home, default_settings_max_depth());
let changed = if files.is_empty() {
let fallback = home.join(".claude").join("settings.json");
println!(
"{} No Claude settings files found. Creating {}…",
"·".dimmed(),
fallback.display()
);
patch_one_with_report(&fallback, &entry, true)?
} else {
println!(
"{} Found {} settings file(s). Patching each…",
"·".dimmed(),
files.len()
);
let mut n = 0usize;
for path in &files {
n += patch_one_with_report(path, &entry, false)?;
}
n
};
println!();
if changed > 0 {
println!(
"{} Setup complete — updated {} settings file{}.",
"✓".green(),
changed,
if changed == 1 { "" } else { "s" }
);
} else {
println!(
"{} Setup complete — all settings files already configured.",
"✓".green()
);
}
println!(
" Restart Claude Code (or reload MCP servers) to pick up `{}`.",
MCP_SERVER_KEY.cyan()
);
Ok(())
}
fn patch_one_with_report(path: &Path, entry: &serde_json::Value, fresh: bool) -> Result<usize> {
match patch_mcp_server(path, MCP_SERVER_KEY, entry) {
Ok(true) => {
let label = if fresh { "+ created" } else { "✓ added" };
println!(" {} {}", label.green(), path.display());
Ok(1)
}
Ok(false) => {
println!(
" {} {} {}",
"↻".cyan(),
path.display().to_string().dimmed(),
"(already configured)".dimmed()
);
Ok(0)
}
Err(e) => {
eprintln!(
" {} {} {}",
"✗".red(),
path.display(),
format!("({e})").red()
);
Ok(0)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn setup_creates_fallback_settings_file() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("settings.json");
let entry = mcp_server_entry(MCP_SERVER_KEY, &["serve"]);
let n = patch_one_with_report(&path, &entry, true).expect("patch ok");
assert_eq!(n, 1);
let v: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(v["mcpServers"][MCP_SERVER_KEY]["command"], "trusty-search");
assert_eq!(v["mcpServers"][MCP_SERVER_KEY]["args"][0], "serve");
}
#[test]
fn setup_is_idempotent() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("settings.json");
let entry = mcp_server_entry(MCP_SERVER_KEY, &["serve"]);
std::fs::write(
&path,
serde_json::to_string_pretty(&json!({
"mcpServers": { MCP_SERVER_KEY: entry }
}))
.unwrap(),
)
.unwrap();
let before = std::fs::read_to_string(&path).unwrap();
let n = patch_one_with_report(&path, &entry, false).expect("patch ok");
assert_eq!(n, 0, "no-op when already configured");
let after = std::fs::read_to_string(&path).unwrap();
assert_eq!(before, after, "file must not change on no-op");
}
#[test]
fn setup_preserves_unrelated_keys() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("settings.json");
let seed = json!({
"theme": "light",
"mcpServers": {
"other": { "command": "other", "args": [] }
}
});
std::fs::write(&path, serde_json::to_string_pretty(&seed).unwrap()).unwrap();
let entry = mcp_server_entry(MCP_SERVER_KEY, &["serve"]);
let n = patch_one_with_report(&path, &entry, false).expect("patch ok");
assert_eq!(n, 1);
let v: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(v["theme"], "light");
let servers = v["mcpServers"].as_object().unwrap();
assert!(servers.contains_key("other"));
assert!(servers.contains_key(MCP_SERVER_KEY));
}
}