use anyhow::{Context, Result};
use std::path::PathBuf;
use xberg::{DoctorCheck, DoctorReport, ProbeStatus, doctor};
use super::config::load_config;
use crate::{WireFormat, style};
fn append_cache_clean_check(report: &mut DoctorReport, cleaned: Option<Option<xberg::doctor::CleanOutcome>>) {
let Some(cleaned) = cleaned else {
return;
};
report.checks.push(match cleaned {
Some(outcome) if outcome.failed == 0 => DoctorCheck::pass(
"cache.clean",
format!("removed {} stray cache file(s)", outcome.removed),
),
Some(outcome) => DoctorCheck::fail(
"cache.clean",
format!(
"removed {} stray cache file(s), failed to remove {}",
outcome.removed, outcome.failed
),
),
None => DoctorCheck::skip(
"cache.clean",
"not attempted: XBERG_CACHE_DIR override in effect and ownership cannot be verified",
),
});
}
#[expect(
clippy::print_stdout,
reason = "the doctor report is the command's stdout result output"
)]
fn print_text_report(report: &DoctorReport) {
println!("{}", style::header("Doctor"));
println!("{}", style::dim("======"));
for check in &report.checks {
let marker = match check.status {
ProbeStatus::Pass => style::success("pass"),
ProbeStatus::Warn => style::warning("warn"),
ProbeStatus::Fail => style::error("FAIL"),
ProbeStatus::Skip => style::dim("skip"),
};
println!("{marker} {} — {}", check.name, check.message);
}
let count = |status: ProbeStatus| report.checks.iter().filter(|c| c.status == status).count();
let mut parts = vec![format!("{} passed", count(ProbeStatus::Pass))];
for (status, noun) in [
(ProbeStatus::Warn, "warning(s)"),
(ProbeStatus::Skip, "skipped"),
(ProbeStatus::Fail, "failed"),
] {
let n = count(status);
if n > 0 {
parts.push(format!("{n} {noun}"));
}
}
let n = report.checks.len();
let noun = if n == 1 { "check" } else { "checks" };
println!();
println!("{n} {noun}: {}", parts.join(", "));
}
#[expect(
clippy::print_stdout,
reason = "the doctor report is the command's stdout result output"
)]
fn print_report(report: &DoctorReport, format: WireFormat) -> Result<()> {
match format {
WireFormat::Text => print_text_report(report),
WireFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(report).context("Failed to serialize doctor report to JSON")?
);
}
WireFormat::Toon => {
println!(
"{}",
serde_toon::to_string(report).context("Failed to serialize doctor report to TOON")?
);
}
}
Ok(())
}
pub fn doctor_command(
config_path: Option<PathBuf>,
no_config_discovery: bool,
format: WireFormat,
clean: bool,
) -> Result<()> {
let config = load_config(config_path, !no_config_discovery)?;
let cleaned = clean.then(xberg::doctor::clean_obsolete);
let mut report = doctor(&config);
append_cache_clean_check(&mut report, cleaned);
print_report(&report, format)?;
if !report.is_ok() {
std::process::exit(1);
}
Ok(())
}