use std::path::PathBuf;
use crate::agents::{self, DoctorCounters, HealthcheckContext};
use crate::display::format_token_count;
use crate::tokensave::TokenSave;
pub fn run_doctor(agent_filter: Option<&str>) {
debug_assert!(!env!("CARGO_PKG_VERSION").is_empty(), "CARGO_PKG_VERSION must not be empty");
let mut dc = DoctorCounters::new();
eprintln!("\n\x1b[1mtokensave doctor v{}\x1b[0m\n", env!("CARGO_PKG_VERSION"));
check_binary(&mut dc);
eprintln!("\n\x1b[1mCurrent project\x1b[0m");
let project_path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
if TokenSave::is_initialized(&project_path) {
dc.pass(&format!("Index found: {}/.tokensave/", project_path.display()));
} else {
dc.warn(&format!("No index at {}/.tokensave/ — run `tokensave sync`", project_path.display()));
}
check_global_db(&mut dc);
check_user_config(&mut dc);
if let Some(ref home) = agents::home_dir() {
let hctx = HealthcheckContext {
home: home.clone(),
project_path: project_path.clone(),
};
let agents_to_check: Vec<Box<dyn agents::AgentIntegration>> = match agent_filter {
Some(id) => match agents::get_integration(id) {
Ok(ag) => vec![ag],
Err(e) => {
dc.fail(&format!("{e}"));
vec![]
}
},
None => agents::all_integrations(),
};
for ag in &agents_to_check {
ag.healthcheck(&mut dc, &hctx);
}
} else {
dc.fail("Could not determine home directory");
}
check_daemon(&mut dc);
check_network(&mut dc);
print_summary(&dc);
}
fn check_binary(dc: &mut DoctorCounters) {
eprintln!("\x1b[1mBinary\x1b[0m");
if let Ok(exe) = std::env::current_exe() {
dc.pass(&format!("Binary: {}", exe.display()));
} else {
dc.fail("Could not determine binary path");
}
dc.pass(&format!("Version: {}", env!("CARGO_PKG_VERSION")));
}
fn check_global_db(dc: &mut DoctorCounters) {
eprintln!("\n\x1b[1mGlobal database\x1b[0m");
if let Some(db_path) = crate::global_db::global_db_path() {
if db_path.exists() {
dc.pass(&format!("Global DB: {}", db_path.display()));
} else {
dc.warn("Global DB not yet created (created on first sync)");
}
} else {
dc.fail("Could not determine home directory for global DB");
}
}
fn check_user_config(dc: &mut DoctorCounters) {
eprintln!("\n\x1b[1mUser config\x1b[0m");
if let Some(config_path) = crate::user_config::config_path() {
if config_path.exists() {
let config = crate::user_config::UserConfig::load();
dc.pass(&format!("Config: {}", config_path.display()));
if config.upload_enabled {
dc.pass("Upload enabled");
} else {
dc.info("Upload disabled (opt-out)");
}
if config.pending_upload > 0 {
dc.info(&format!("Pending upload: {} tokens", config.pending_upload));
}
} else {
dc.warn("Config not yet created (created on first sync)");
}
} else {
dc.fail("Could not determine home directory for config");
}
}
fn check_daemon(dc: &mut DoctorCounters) {
eprintln!("\n\x1b[1mDaemon\x1b[0m");
match crate::daemon::running_daemon_pid() {
Some(pid) => dc.pass(&format!("Daemon is running (PID: {pid})")),
None => dc.warn("Daemon is not running — run `tokensave daemon` to start"),
}
if crate::daemon::is_autostart_enabled() {
dc.pass("Autostart enabled");
} else {
dc.warn("Autostart not configured — run `tokensave daemon --enable-autostart`");
}
}
fn check_network(dc: &mut DoctorCounters) {
eprintln!("\n\x1b[1mNetwork\x1b[0m");
if let Some(total) = crate::cloud::fetch_worldwide_total() {
dc.pass(&format!("Worldwide counter reachable (total: {})", format_token_count(total)));
} else {
dc.warn("Worldwide counter unreachable (offline or timeout)");
}
if crate::cloud::fetch_latest_version().is_some() {
dc.pass("GitHub releases API reachable");
} else {
dc.warn("GitHub releases API unreachable (offline or timeout)");
}
}
fn print_summary(dc: &DoctorCounters) {
eprintln!();
if dc.issues == 0 && dc.warnings == 0 {
eprintln!("\x1b[32mAll checks passed.\x1b[0m");
} else if dc.issues == 0 {
eprintln!("\x1b[33m{} warning(s), no issues.\x1b[0m", dc.warnings);
} else {
eprintln!("\x1b[31m{} issue(s), {} warning(s).\x1b[0m", dc.issues, dc.warnings);
eprintln!("Run \x1b[1mtokensave install\x1b[0m to fix most issues.");
}
eprintln!();
}