use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde_json::{json, Value};
use crate::cli::SetupArgs;
const EMBEDDED_SKILL: &str = include_str!("../../skills/code-rcl/SKILL.md");
fn clean_path(path: &Path) -> String {
let s = path.to_string_lossy().to_string();
if let Some(stripped) = s.strip_prefix(r"\\?\") {
stripped.to_string()
} else {
s
}
}
fn get_home_dir() -> Option<PathBuf> {
#[cfg(windows)]
{
if let Ok(profile) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(profile));
}
}
std::env::var("HOME").ok().map(PathBuf::from)
}
fn get_executable_command() -> String {
if let Ok(exe) = std::env::current_exe() {
if let Ok(canonical) = exe.canonicalize() {
return clean_path(&canonical);
}
return clean_path(&exe);
}
"code-rcl".to_string()
}
fn update_mcp_json_file(file_path: &Path, exe_cmd: &str) -> Result<()> {
let mut root: Value = if file_path.exists() {
let content = fs::read_to_string(file_path)
.with_context(|| format!("Failed to read MCP config: {}", file_path.display()))?;
serde_json::from_str(&content).unwrap_or_else(|_| json!({}))
} else {
json!({})
};
if !root.is_object() {
root = json!({});
}
let mcp_servers = root
.as_object_mut()
.unwrap()
.entry("mcpServers")
.or_insert_with(|| json!({}));
if !mcp_servers.is_object() {
*mcp_servers = json!({});
}
mcp_servers.as_object_mut().unwrap().insert(
"code-rcl".to_string(),
json!({
"command": exe_cmd,
"args": ["mcp"]
}),
);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory: {}", parent.display()))?;
}
let pretty_json = serde_json::to_string_pretty(&root)
.context("Failed to serialize MCP configuration JSON")?;
fs::write(file_path, format!("{}\n", pretty_json))
.with_context(|| format!("Failed to write MCP config: {}", file_path.display()))?;
Ok(())
}
pub fn run(args: SetupArgs) -> Result<()> {
if args.print_skill {
println!("{}", EMBEDDED_SKILL);
return Ok(());
}
println!("[code-rcl setup] Setting up AI agent integration...");
let exe_cmd = get_executable_command();
let home = get_home_dir();
if !args.mcp_only {
let mut skill_paths: Vec<(PathBuf, bool)> = Vec::new();
if let Some(custom_dir) = &args.skill_dir {
skill_paths.push((custom_dir.join("SKILL.md"), true));
} else if args.global {
if let Some(h) = &home {
skill_paths.push((
h.join(".gemini").join("config").join("skills").join("code-rcl").join("SKILL.md"),
true,
));
} else {
eprintln!("[warn] Cannot determine home directory for global skill installation; falling back to workspace.");
skill_paths.push((
PathBuf::from(".agents").join("skills").join("code-rcl").join("SKILL.md"),
true,
));
}
} else if args.workspace {
skill_paths.push((
PathBuf::from(".agents").join("skills").join("code-rcl").join("SKILL.md"),
true,
));
} else {
skill_paths.push((
PathBuf::from(".agents").join("skills").join("code-rcl").join("SKILL.md"),
true,
));
if let Some(h) = &home {
let global_skill = h.join(".gemini").join("config").join("skills").join("code-rcl").join("SKILL.md");
if global_skill.parent().map(|p| p.exists()).unwrap_or(false) {
skill_paths.push((global_skill, false));
}
}
}
for (skill_path, required) in skill_paths {
if let Some(parent) = skill_path.parent() {
if let Err(e) = fs::create_dir_all(parent) {
if required {
return Err(e).with_context(|| format!("Failed to create skill directory: {}", parent.display()));
} else {
eprintln!(" ⚠ Skipped optional skill directory: {}", parent.display());
continue;
}
}
}
match fs::write(&skill_path, EMBEDDED_SKILL) {
Ok(_) => println!(" ✓ Installed agent skill: {}", skill_path.display()),
Err(e) => {
if required {
return Err(e).with_context(|| format!("Failed to write SKILL.md to: {}", skill_path.display()));
} else {
eprintln!(" ⚠ Skipped optional skill path: {}", skill_path.display());
}
}
}
}
}
if !args.skill_only {
let target_lower = args.target.to_lowercase();
let config_gemini = target_lower == "all" || target_lower == "gemini" || target_lower == "antigravity";
let config_claude = target_lower == "all" || target_lower == "claude";
if args.global {
if config_gemini {
if let Some(h) = &home {
let global_gemini = h.join(".gemini").join("config").join("mcp_config.json");
update_mcp_json_file(&global_gemini, &exe_cmd)?;
println!(" ✓ Registered global MCP server (Antigravity/Gemini): {}", global_gemini.display());
}
}
if config_claude {
if let Some(h) = &home {
let global_claude = h.join(".claude.json");
update_mcp_json_file(&global_claude, &exe_cmd)?;
println!(" ✓ Registered global MCP server (Claude): {}", global_claude.display());
}
}
} else if args.workspace {
if config_claude {
let workspace_mcp = PathBuf::from(".mcp.json");
update_mcp_json_file(&workspace_mcp, &exe_cmd)?;
println!(" ✓ Registered workspace MCP server (Claude Code / Standard): {}", workspace_mcp.display());
}
if config_gemini {
let workspace_gemini = PathBuf::from(".agents").join("mcp_config.json");
update_mcp_json_file(&workspace_gemini, &exe_cmd)?;
println!(" ✓ Registered workspace MCP server (Antigravity/Gemini): {}", workspace_gemini.display());
}
} else {
if config_claude {
let workspace_mcp = PathBuf::from(".mcp.json");
update_mcp_json_file(&workspace_mcp, &exe_cmd)?;
println!(" ✓ Registered workspace MCP server (Claude Code / Standard): {}", workspace_mcp.display());
}
if config_gemini {
let workspace_gemini = PathBuf::from(".agents").join("mcp_config.json");
update_mcp_json_file(&workspace_gemini, &exe_cmd)?;
println!(" ✓ Registered workspace MCP server (Antigravity/Gemini): {}", workspace_gemini.display());
if let Some(h) = &home {
let global_gemini = h.join(".gemini").join("config").join("mcp_config.json");
if global_gemini.parent().map(|p| p.exists()).unwrap_or(false) {
if let Err(e) = update_mcp_json_file(&global_gemini, &exe_cmd) {
eprintln!(" ⚠ Optional global MCP config sync skipped: {}", e);
} else {
println!(" ✓ Synchronized global MCP server (Antigravity/Gemini): {}", global_gemini.display());
}
}
}
}
}
}
println!("[code-rcl setup] Setup completed successfully.");
println!(" Binary path: {}", exe_cmd);
println!(" Run `code-rcl --help` or `code-rcl mcp` for details.");
Ok(())
}