use super::home_dir;
use anyhow::Result;
use std::path::Path;
pub fn handle() -> Result<()> {
println!("bctx doctor — system health check");
println!();
let home = home_dir();
let bctx_dir = format!("{home}/.bctx");
let beacons_dir = format!("{home}/.bctx/beacons");
let vault_dir = format!("{home}/.bctx/vault");
let exec_db = format!("{home}/.bctx/executions.db");
let claude_mcp = format!("{home}/.claude/mcp.json");
let cursor_mcp = format!("{home}/.cursor/mcp.json");
let cloud_token = format!("{home}/.bctx/cloud_token.json");
section("Environment");
let home_var = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
check(
&format!("{home_var} set"),
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.is_ok(),
);
check_bin("bctx in PATH", "bctx");
check_bin("git in PATH", "git");
check_bin("cargo in PATH", "cargo");
section("Local storage");
check("~/.bctx/ exists", Path::new(&bctx_dir).exists());
check("~/.bctx/executions.db exists", Path::new(&exec_db).exists());
info(
"~/.bctx/vault/",
if Path::new(&vault_dir).exists() {
"present"
} else {
"not yet created (lazy)"
},
);
info(
"~/.bctx/beacons/",
if Path::new(&beacons_dir).exists() {
"present"
} else {
"not yet created (lazy)"
},
);
section("Vault");
let crystallized = format!("{vault_dir}/crystallized.json");
let resonant = format!("{vault_dir}/resonant.json");
let crystal_facts = count_json_array(&crystallized);
let resonant_facts = count_json_array(&resonant);
check_count("crystallized facts", crystal_facts);
check_count("resonant facts", resonant_facts);
section("Beacons");
let beacon_count = count_dir_files(&beacons_dir);
check_count("beacon events recorded", beacon_count);
let hook_marker = "# bctx";
#[cfg(not(windows))]
let shell_configs: Vec<String> = vec![
format!("{home}/.zshrc"),
format!("{home}/.bashrc"),
format!("{home}/.bash_profile"),
format!("{home}/.config/fish/config.fish"),
];
#[cfg(windows)]
let shell_configs: Vec<String> = {
let docs = format!("{home}/Documents");
vec![
format!("{docs}/PowerShell/Microsoft.PowerShell_profile.ps1"),
format!("{docs}/WindowsPowerShell/Microsoft.PowerShell_profile.ps1"),
]
};
let hook_rc: Option<String> = shell_configs.iter().find_map(|rc| {
std::fs::read_to_string(rc)
.ok()
.filter(|c| c.contains(hook_marker))
.map(|_| rc.replace(&home, "~"))
});
section("Shell hook");
if let Some(ref rc_short) = hook_rc {
println!(" ✓ shell hook present ({rc_short})");
} else {
println!(" ✗ shell hook not found");
println!(" Run `bctx init` to install the shell hook.");
}
section("Agent integration");
check("Claude Code mcp.json", Path::new(&claude_mcp).exists());
check("Cursor mcp.json", Path::new(&cursor_mcp).exists());
section("Cloud");
let token_present = Path::new(&cloud_token).exists();
check("cloud_token.json present", token_present);
if token_present {
if let Some(tier) = read_cloud_tier(&cloud_token) {
println!(" tier: {tier}");
}
}
section("Recipes");
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let proj_recipe_dir = cwd.join(".bctx").join("recipes");
let user_recipe_dir = std::path::PathBuf::from(&home)
.join(".config")
.join("bctx")
.join("recipes");
let proj_count = count_dir_files(&proj_recipe_dir.to_string_lossy());
let user_count = count_dir_files(&user_recipe_dir.to_string_lossy());
info("project recipes (.bctx/recipes/)", &proj_count.to_string());
info(
"user recipes (~/.config/bctx/recipes/)",
&user_count.to_string(),
);
let recipes = weave::recipe::loader::load_from_dir(&proj_recipe_dir).unwrap_or_default();
for r in &recipes {
let errs = weave::recipe::validator::validate(r);
if errs.is_empty() {
println!(" ✓ recipe \"{}\" valid", r.name);
} else {
for e in &errs {
println!(" ✗ recipe \"{}\": {e}", r.name);
}
}
}
if proj_count + user_count == 0 {
println!(
" · no recipes loaded — create .bctx/recipes/my-tool.toml to customise compression"
);
}
section("Runtime");
let tiktoken_ok = forge::budget::estimator::TokenEstimator::count("hello world") > 0;
check("tiktoken BPE loaded", tiktoken_ok);
let vault_ok = std::panic::catch_unwind(|| {
let v = atlas::vault_store::vault().lock().unwrap();
let _ = v.query(&vault::retrieval::query::VaultQuery::new("__doctor_probe__").top_k(0));
})
.is_ok();
check("Vault query smoke test", vault_ok);
println!();
if hook_rc.is_none() {
println!(" Tip: run `bctx init` to install the shell hook and start saving tokens.");
}
if !Path::new(&claude_mcp).exists() && !Path::new(&cursor_mcp).exists() {
println!(" Tip: run `bctx init --agent claude` to set up agent hooks.");
}
if !token_present {
println!(" Tip: run `bctx login` to connect to the cloud for Vault sync.");
}
Ok(())
}
fn section(label: &str) {
println!("\n {label}");
println!(" {}", "─".repeat(label.len()));
}
fn check(label: &str, ok: bool) {
let mark = if ok { "✓" } else { "✗" };
println!(" {mark} {label}");
}
fn check_bin(label: &str, bin: &str) {
let ok = which_bin(bin);
let mark = if ok { "✓" } else { "✗" };
println!(" {mark} {label}");
}
fn check_count(label: &str, n: usize) {
println!(" ✓ {label}: {n}");
}
fn info(label: &str, detail: &str) {
println!(" · {label}: {detail}");
}
fn which_bin(bin: &str) -> bool {
let finder = if cfg!(windows) { "where" } else { "which" };
std::process::Command::new(finder)
.arg(bin)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn count_json_array(path: &str) -> usize {
let Ok(data) = std::fs::read_to_string(path) else {
return 0;
};
let Ok(arr) = serde_json::from_str::<serde_json::Value>(&data) else {
return 0;
};
arr.as_array().map(|a| a.len()).unwrap_or(0)
}
fn count_dir_files(path: &str) -> usize {
std::fs::read_dir(path)
.map(|d| d.filter_map(|e| e.ok()).count())
.unwrap_or(0)
}
fn read_cloud_tier(path: &str) -> Option<String> {
let data = std::fs::read_to_string(path).ok()?;
let v: serde_json::Value = serde_json::from_str(&data).ok()?;
v["tier"].as_str().map(String::from)
}