Skip to main content

ralph/cli/
doctor.rs

1//! `ralph doctor` command: handler.
2
3use anyhow::Result;
4use clap::{Args, ValueEnum};
5
6use crate::sanity::{self, SanityOptions};
7use crate::{commands::doctor, config};
8
9/// Output format for doctor command.
10#[derive(Debug, Clone, Copy, Default, ValueEnum)]
11pub enum DoctorFormat {
12    /// Human-readable text output (default).
13    #[default]
14    Text,
15    /// Machine-readable JSON output for scripting/CI.
16    Json,
17}
18
19/// Arguments for the `ralph doctor` command.
20#[derive(Args)]
21pub struct DoctorArgs {
22    /// Automatically fix all issues without prompting.
23    #[arg(long, conflicts_with = "no_sanity_checks")]
24    pub auto_fix: bool,
25
26    /// Skip sanity checks and only run doctor diagnostics.
27    #[arg(long, conflicts_with = "auto_fix")]
28    pub no_sanity_checks: bool,
29
30    /// Output format (text or json).
31    #[arg(long, value_enum, default_value = "text")]
32    pub format: DoctorFormat,
33}
34
35pub fn handle_doctor(args: DoctorArgs) -> Result<()> {
36    // Use resolve_from_cwd_for_doctor to skip instruction_files validation,
37    // allowing doctor to diagnose and warn about missing files without failing early.
38    let resolved = config::resolve_from_cwd_for_doctor()?;
39
40    // Run sanity checks first (unless skipped)
41    if !args.no_sanity_checks {
42        let options = SanityOptions {
43            auto_fix: args.auto_fix,
44            skip: false,
45            non_interactive: false, // doctor is always interactive by default
46        };
47        let sanity_result = sanity::run_sanity_checks(&resolved, &options)?;
48
49        // Report sanity check results
50        if !sanity_result.auto_fixes.is_empty() {
51            log::info!(
52                "Sanity checks applied {} fix(es):",
53                sanity_result.auto_fixes.len()
54            );
55            for fix in &sanity_result.auto_fixes {
56                log::info!("  - {}", fix);
57            }
58        }
59
60        if !sanity_result.needs_attention.is_empty() {
61            log::warn!(
62                "Sanity checks found {} issue(s) needing attention:",
63                sanity_result.needs_attention.len()
64            );
65            for issue in &sanity_result.needs_attention {
66                match issue.severity {
67                    sanity::IssueSeverity::Warning => log::warn!("  - {}", issue.message),
68                    sanity::IssueSeverity::Error => log::error!("  - {}", issue.message),
69                }
70            }
71        }
72    }
73
74    // Run doctor checks with auto_fix flag and format
75    let report = doctor::run_doctor(&resolved, args.auto_fix)?;
76
77    // Output based on format
78    match args.format {
79        DoctorFormat::Text => {
80            doctor::print_doctor_report_text(&report);
81        }
82        DoctorFormat::Json => {
83            println!("{}", serde_json::to_string_pretty(&report)?);
84        }
85    }
86
87    // Return appropriate exit code based on report success
88    if report.success {
89        Ok(())
90    } else {
91        anyhow::bail!("Doctor check failed: one or more critical issues found")
92    }
93}