use std::{sync::Arc, time::Duration};
use rho_providers::credentials::{available_auth_modes, CredentialStore};
use serde::Serialize;
use super::config_repository::ConfigRepository;
use crate::{
cli::Cli,
credential_store::AppCredentialStore,
doctor::{
build_report, plan_probes, probe_checks, run_probe, text_report, DoctorInputs,
DoctorProbeGate, DoctorProbeId, DoctorProbeOutcome, DoctorReport, DoctorSection,
DoctorSummary, HerdrProbe,
},
herdr::HerdrReporter,
plugins::{self, ProjectTrust},
tools::mcp::{McpLoadMode, McpSessionReport},
};
const PROBE_DEADLINE: Duration = Duration::from_secs(20);
pub(super) async fn run(json: bool, cli: &Cli) -> anyhow::Result<()> {
let config_repository = ConfigRepository::new(cli.config.clone());
let mut config = config_repository.load()?;
config.providers.activate()?;
let config_path = super::bootstrap::absolute_config_path(&config_repository)?;
if let Err(error) = crate::credential_store::initialize_from_config(&mut config, &config_path) {
eprintln!("warning: credential store not initialized: {error:#}");
}
let store: Arc<dyn CredentialStore> = Arc::new(AppCredentialStore);
if let Err(error) = super::cli_config::refresh_model_cache(cli, &config, store.as_ref()).await {
eprintln!("warning: could not refresh model list: {error:#}");
}
apply_doctor_overrides(&mut config, cli)?;
let cwd = std::env::current_dir()?;
let home = crate::paths::home_dir();
let rho_home = crate::paths::rho_dir()?;
let discovery = plugins::discover_with_trust(
&cwd,
home.as_deref(),
Some(&rho_home),
ProjectTrust::from_plugins_env(),
);
plugins::log(&discovery.report);
let mut mcp_config = config.mcp.clone();
mcp_config.merge(discovery.mcp);
let mcp_report = McpSessionReport::from_config_unloaded(&mcp_config, McpLoadMode::Native);
let available_auths = available_auth_modes(store.as_ref());
let clipboard = crate::clipboard::doctor_report();
let probes = plan_probes(&config, &config.provider, DoctorProbeGate::Live);
let mut report = build_report(DoctorInputs {
provider: &config.provider,
model: &config.model,
auth: &config.auth,
available_auths: &available_auths,
credential_store: store.as_ref(),
config_path: &config_path,
session_root: &rho_home.join("sessions"),
herdr: HerdrProbe::from_reporter(&HerdrReporter::from_env()),
clipboard: &clipboard,
mcp_report: &mcp_report,
plugins_report: &discovery.report,
probes: &probes,
});
let handles = probes
.into_iter()
.map(|id| (id.clone(), tokio::spawn(run_probe(id, store.clone()))))
.collect();
for outcome in collect_probes(handles, PROBE_DEADLINE).await {
report.replace_checks(probe_checks(&outcome, &config.provider));
}
if json {
println!(
"{}",
serde_json::to_string_pretty(&DoctorDocument::new(&report))?
);
} else {
print!("{}", text_report::render(&report));
}
let failing = report.summary().fail;
if failing > 0 {
anyhow::bail!(
"{failing} check{} failing",
if failing == 1 { "" } else { "s" }
);
}
Ok(())
}
fn apply_doctor_overrides(config: &mut crate::config::Config, cli: &Cli) -> anyhow::Result<()> {
super::cli_config::apply_overrides_allowing_empty_cache(config, cli)?;
super::cli_config::normalize_reasoning_for_cli(
config,
if cli.reasoning.is_some() {
rho_providers::model::ReasoningRequestSource::Explicit
} else {
rho_providers::model::ReasoningRequestSource::PersistedOrDefault
},
)?;
Ok(())
}
async fn collect_probes(
handles: Vec<(DoctorProbeId, tokio::task::JoinHandle<DoctorProbeOutcome>)>,
deadline: Duration,
) -> Vec<DoctorProbeOutcome> {
let deadline = tokio::time::Instant::now() + deadline;
let mut outcomes = Vec::with_capacity(handles.len());
for (id, mut handle) in handles {
outcomes.push(match tokio::time::timeout_at(deadline, &mut handle).await {
Ok(Ok(outcome)) => outcome,
Ok(Err(_)) => DoctorProbeOutcome::Failed(id),
Err(_) => {
handle.abort();
DoctorProbeOutcome::TimedOut(id)
}
});
}
outcomes
}
#[derive(Serialize)]
struct DoctorDocument<'a> {
summary: DoctorSummary,
sections: &'a [DoctorSection],
}
impl<'a> DoctorDocument<'a> {
fn new(report: &'a DoctorReport) -> Self {
Self {
summary: report.summary(),
sections: &report.sections,
}
}
}
#[cfg(test)]
#[path = "doctor_cli_tests.rs"]
mod tests;