Skip to main content

ferro_cli/commands/
doctor.rs

1//! `ferro doctor` — runs all 11 health checks (D-01..D-09, D-22).
2//!
3//! Complementary to `ferro:info` MCP tool — does not replace it.
4
5use crate::doctor::check::{CheckCategory, CheckStatus, Report};
6use crate::doctor::registry::default_checks;
7use crate::project::find_project_root;
8use console::style;
9
10pub fn run(json: bool, deploy_only: bool) {
11    let root = match find_project_root(None) {
12        Ok(r) => r,
13        Err(_) => {
14            eprintln!(
15                "{} Cargo.toml not found (run `ferro doctor` inside a Ferro project)",
16                style("Error:").red().bold()
17            );
18            std::process::exit(1);
19        }
20    };
21
22    let all_checks = default_checks();
23    let checks: Vec<_> = if deploy_only {
24        all_checks
25            .into_iter()
26            .filter(|c| c.category() == CheckCategory::Deploy)
27            .collect()
28    } else {
29        all_checks
30    };
31    let results: Vec<_> = checks.iter().map(|c| c.run(&root)).collect();
32    let report = Report::build(results);
33
34    if json {
35        match serde_json::to_string_pretty(&report) {
36            Ok(s) => println!("{s}"),
37            Err(e) => {
38                eprintln!("{} JSON serialize error: {e}", style("Error:").red().bold());
39                std::process::exit(1);
40            }
41        }
42    } else {
43        print_human(&report);
44    }
45
46    std::process::exit(report.exit_code());
47}
48
49fn print_human(report: &Report) {
50    for c in &report.checks {
51        let icon = match c.status {
52            CheckStatus::Ok => style("✓").green().to_string(),
53            CheckStatus::Warn => style("⚠").yellow().to_string(),
54            CheckStatus::Error => style("✗").red().to_string(),
55        };
56        println!("{icon} {} — {}", style(c.name).bold(), c.message);
57        if let Some(d) = &c.details {
58            println!("    {}", style(d).dim());
59        }
60    }
61    println!();
62    let s = &report.summary;
63    let summary_line = format!("Summary: {} ok, {} warn, {} error", s.ok, s.warn, s.error);
64    let styled = match s.overall {
65        CheckStatus::Ok => style(summary_line).green().bold(),
66        CheckStatus::Warn => style(summary_line).yellow().bold(),
67        CheckStatus::Error => style(summary_line).red().bold(),
68    };
69    println!("{styled}");
70}