use anyhow::Result;
use std::path::{Path, PathBuf};
pub fn handle(agent: Option<String>) -> Result<()> {
match agent.as_deref() {
Some(name) => uninstall_agent(name)?,
None => {
let mut removed = 0u32;
for name in SUPPORTED_AGENTS {
if is_configured(name) {
println!("Removing: {name}");
uninstall_agent(name)?;
removed += 1;
println!();
}
}
if removed == 0 {
println!("No bctx agent configurations found.");
println!("Run with --agent to remove a specific agent:");
for name in SUPPORTED_AGENTS {
println!(" bctx uninstall --agent {name}");
}
}
}
}
Ok(())
}
const SUPPORTED_AGENTS: &[&str] = &[
"claude",
"cursor",
"windsurf",
"gemini",
"zed",
"continue",
"cline",
"roo",
"copilot",
"antigravity",
"amazonq",
"kiro",
"codex",
"trae",
"zsh",
"bash",
];
fn is_configured(agent: &str) -> bool {
let home = super::home_dir();
let h = |rel: &str| PathBuf::from(&home).join(rel);
match agent {
"claude" => {
has_bctx_mcp_key(&h(".claude.json"), "mcpServers")
|| has_bctx_mcp_key(&h(".claude/mcp.json"), "mcpServers")
|| h(".claude/bctx-hook.sh").exists()
}
"cursor" => has_bctx_mcp_key(&h(".cursor/mcp.json"), "mcpServers"),
"windsurf" => has_bctx_mcp_key(&h(".codeium/windsurf/mcp_config.json"), "mcpServers"),
"gemini" => has_bctx_mcp_key(&h(".gemini/settings.json"), "mcpServers"),
"zed" => has_bctx_mcp_key(&h(".config/zed/settings.json"), "context_servers"),
"continue" => has_bctx_continue_entry(&h(".continue/config.json")),
"cline" | "roo" | "copilot" => {
#[cfg(target_os = "macos")]
{
let p = PathBuf::from(&home).join("Library/Application Support/Code/User/mcp.json");
has_bctx_mcp_key(&p, "servers")
}
#[cfg(not(target_os = "macos"))]
{
let p = h(".config/Code/User/mcp.json");
has_bctx_mcp_key(&p, "servers")
}
}
"antigravity" => has_bctx_mcp_key(&h(".gemini/antigravity/mcp_config.json"), "mcpServers"),
"amazonq" => has_bctx_mcp_key(&h(".aws/amazonq/mcp.json"), "mcpServers"),
"kiro" => has_bctx_mcp_key(&h(".kiro/settings/mcp.json"), "mcpServers"),
"codex" => has_bctx_mcp_key(&h(".codex/mcp.json"), "mcpServers"),
"trae" => {
#[cfg(target_os = "macos")]
{
let p = PathBuf::from(&home).join("Library/Application Support/Trae/User/mcp.json");
has_bctx_mcp_key(&p, "servers")
}
#[cfg(not(target_os = "macos"))]
false
}
"zsh" => shell_has_bctx_hook(&h(".zshrc")),
"bash" => shell_has_bctx_hook(&h(".bashrc")) || shell_has_bctx_hook(&h(".bash_profile")),
_ => false,
}
}
fn has_bctx_mcp_key(path: &Path, key: &str) -> bool {
let Ok(raw) = std::fs::read_to_string(path) else {
return false;
};
let Ok(cfg) = serde_json::from_str::<serde_json::Value>(&raw) else {
return false;
};
cfg[key].get("bctx").is_some()
}
fn has_bctx_continue_entry(path: &Path) -> bool {
let Ok(raw) = std::fs::read_to_string(path) else {
return false;
};
let Ok(cfg) = serde_json::from_str::<serde_json::Value>(&raw) else {
return false;
};
cfg["mcpServers"].as_array().is_some_and(|arr| {
arr.iter()
.any(|e| e.get("name").and_then(|v| v.as_str()) == Some("bctx"))
})
}
fn shell_has_bctx_hook(path: &Path) -> bool {
std::fs::read_to_string(path).is_ok_and(|s| s.contains("# bctx —"))
}
fn uninstall_agent(agent: &str) -> Result<()> {
match agent {
"claude" => uninstall_claude(),
"cursor" => uninstall_cursor(),
"windsurf" => uninstall_windsurf(),
"gemini" => uninstall_gemini(),
"zed" => uninstall_zed(),
"continue" => uninstall_continue(),
"cline" | "roo" => uninstall_vscode_extension(agent),
"copilot" => uninstall_vscode_extension("copilot"),
"antigravity" => uninstall_antigravity(),
"amazonq" => uninstall_amazonq(),
"kiro" => uninstall_kiro(),
"codex" => uninstall_codex(),
"trae" => uninstall_trae(),
"zsh" => uninstall_shell("zsh"),
"bash" => uninstall_shell("bash"),
other => {
eprintln!("bctx uninstall: unknown agent '{other}'");
eprintln!("Supported agents:");
for name in SUPPORTED_AGENTS {
eprintln!(" {name}");
}
std::process::exit(1);
}
}
}
fn uninstall_claude() -> Result<()> {
let home = super::home_dir();
let claude_dir = PathBuf::from(&home).join(".claude");
let claude_json = PathBuf::from(&home).join(".claude.json");
remove_mcp_key(&claude_json, "mcpServers")?;
let claude_mcp = claude_dir.join("mcp.json");
remove_mcp_key(&claude_mcp, "mcpServers")?;
#[cfg(target_os = "macos")]
remove_vscode_mcp_entry(&home, "Code/User")?;
let settings_path = claude_dir.join("settings.json");
if settings_path.exists() {
let raw = std::fs::read_to_string(&settings_path)?;
let mut cfg: serde_json::Value =
serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
if let Some(arr) = cfg["hooks"]["PreToolUse"].as_array_mut() {
let before = arr.len();
arr.retain(|h| {
!h["hooks"]
.as_array()
.and_then(|hs| hs.first())
.and_then(|h| h["command"].as_str())
.is_some_and(|c| c.contains("bctx-hook"))
});
if arr.len() < before {
std::fs::write(&settings_path, serde_json::to_string_pretty(&cfg)?)?;
println!(" ✓ settings.json hook entry removed");
}
}
}
let hook_path = claude_dir.join("bctx-hook.sh");
if hook_path.exists() {
std::fs::remove_file(&hook_path)?;
println!(" ✓ bctx-hook.sh removed");
}
println!(" → Restart Claude Code to deactivate.");
Ok(())
}
fn uninstall_cursor() -> Result<()> {
let home = super::home_dir();
let path = PathBuf::from(&home).join(".cursor/mcp.json");
remove_mcp_key(&path, "mcpServers")?;
#[cfg(target_os = "macos")]
remove_vscode_mcp_entry(&home, "Cursor/User")?;
println!(" → Restart Cursor to deactivate.");
Ok(())
}
fn uninstall_windsurf() -> Result<()> {
let home = super::home_dir();
let path = PathBuf::from(&home).join(".codeium/windsurf/mcp_config.json");
remove_mcp_key(&path, "mcpServers")?;
#[cfg(target_os = "macos")]
remove_vscode_mcp_entry(&home, "Windsurf/User")?;
println!(" → Restart Windsurf to deactivate.");
Ok(())
}
fn uninstall_gemini() -> Result<()> {
let home = super::home_dir();
let path = PathBuf::from(&home).join(".gemini/settings.json");
remove_mcp_key(&path, "mcpServers")?;
println!(" → Restart Gemini CLI to deactivate.");
Ok(())
}
fn uninstall_zed() -> Result<()> {
let home = super::home_dir();
let path = PathBuf::from(&home).join(".config/zed/settings.json");
remove_mcp_key(&path, "context_servers")?;
println!(" → Restart Zed to deactivate.");
Ok(())
}
fn uninstall_continue() -> Result<()> {
let home = super::home_dir();
let path = PathBuf::from(&home).join(".continue/config.json");
if !path.exists() {
return Ok(());
}
let raw = std::fs::read_to_string(&path)?;
let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
if let Some(arr) = cfg["mcpServers"].as_array_mut() {
let before = arr.len();
arr.retain(|e| e.get("name").and_then(|v| v.as_str()) != Some("bctx"));
if arr.len() < before {
std::fs::write(&path, serde_json::to_string_pretty(&cfg)?)?;
println!(" ✓ ~/.continue/config.json — bctx entry removed");
}
}
println!(" → Reload Continue extension to deactivate.");
Ok(())
}
fn uninstall_vscode_extension(label: &str) -> Result<()> {
let home = super::home_dir();
#[cfg(target_os = "macos")]
remove_vscode_mcp_entry(&home, "Code/User")?;
#[cfg(not(target_os = "macos"))]
{
let path = PathBuf::from(&home).join(".config/Code/User/mcp.json");
remove_servers_key(&path)?;
}
println!(" ({label}) → Reload VSCode window to deactivate.");
Ok(())
}
fn uninstall_antigravity() -> Result<()> {
let home = super::home_dir();
let path = PathBuf::from(&home).join(".gemini/antigravity/mcp_config.json");
remove_mcp_key(&path, "mcpServers")?;
#[cfg(target_os = "macos")]
remove_vscode_mcp_entry(&home, "Antigravity/User")?;
println!(" → Restart Antigravity to deactivate.");
Ok(())
}
fn uninstall_amazonq() -> Result<()> {
let home = super::home_dir();
let path = PathBuf::from(&home).join(".aws/amazonq/mcp.json");
remove_mcp_key(&path, "mcpServers")?;
println!(" → Restart Amazon Q to deactivate.");
Ok(())
}
fn uninstall_kiro() -> Result<()> {
let home = super::home_dir();
let path = PathBuf::from(&home).join(".kiro/settings/mcp.json");
remove_mcp_key(&path, "mcpServers")?;
println!(" → Restart Kiro to deactivate.");
Ok(())
}
fn uninstall_codex() -> Result<()> {
let home = super::home_dir();
let path = PathBuf::from(&home).join(".codex/mcp.json");
remove_mcp_key(&path, "mcpServers")?;
println!(" → Restart Codex CLI to deactivate.");
Ok(())
}
fn uninstall_trae() -> Result<()> {
#[cfg(target_os = "macos")]
{
let home = super::home_dir();
remove_vscode_mcp_entry(&home, "Trae/User")?;
}
println!(" → Restart Trae to deactivate.");
Ok(())
}
fn uninstall_shell(shell: &str) -> Result<()> {
let home = super::home_dir();
let candidates: &[&str] = match shell {
"zsh" => &[".zshrc"],
"bash" => &[".bashrc", ".bash_profile"],
_ => unreachable!(),
};
for rel in candidates {
let path = PathBuf::from(&home).join(rel);
remove_shell_hook(&path)?;
}
Ok(())
}
fn remove_shell_hook(path: &Path) -> Result<()> {
if !path.exists() {
return Ok(());
}
let content = std::fs::read_to_string(path)?;
if !content.contains("# bctx —") {
return Ok(());
}
let mut output = String::with_capacity(content.len());
let mut skip = false;
for line in content.lines() {
if line.contains("# bctx —") {
skip = true;
if output.ends_with('\n') {
output.truncate(output.trim_end_matches('\n').len());
if !output.is_empty() {
output.push('\n');
}
}
continue;
}
if skip {
if line.starts_with("unset _bctx_cmd") || line.starts_with("# bctx end") {
skip = false;
}
continue;
}
output.push_str(line);
output.push('\n');
}
std::fs::write(path, &output)?;
println!(" ✓ {} — bctx hook removed", path.display());
println!(" → Run: source {}", path.display());
Ok(())
}
fn remove_mcp_key(path: &Path, key: &str) -> Result<()> {
if !path.exists() {
return Ok(());
}
let raw = std::fs::read_to_string(path)?;
let mut cfg: serde_json::Value = serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
if cfg[key].get("bctx").is_some() {
cfg[key]
.as_object_mut()
.expect("mcp key is object")
.remove("bctx");
std::fs::write(path, serde_json::to_string_pretty(&cfg)?)?;
println!(" ✓ {} — bctx removed", path.display());
}
Ok(())
}
fn remove_servers_key(path: &Path) -> Result<()> {
remove_mcp_key(path, "servers")
}
#[cfg(target_os = "macos")]
fn remove_vscode_mcp_entry(home: &str, app_subdir: &str) -> Result<()> {
let path = PathBuf::from(home)
.join("Library/Application Support")
.join(app_subdir)
.join("mcp.json");
remove_servers_key(&path)
}