use std::process::ExitCode;
use clap::Args;
use serde::Serialize;
use crate::error::CliError;
use crate::output::OutputFormat;
#[derive(Args, Debug)]
pub struct DoctorArgs {
#[arg(long)]
pub fix: bool,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
#[allow(dead_code)]
enum Status {
Ok,
Warn,
Fail,
}
#[derive(Debug, Serialize)]
struct CheckResult {
id: &'static str,
status: Status,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
fix_suggestion: Option<String>,
}
#[derive(Debug, Serialize)]
struct Summary {
ok: usize,
warn: usize,
fail: usize,
}
#[derive(Debug, Serialize)]
struct DoctorReport {
checks: Vec<CheckResult>,
summary: Summary,
}
pub fn run(args: &DoctorArgs, format: OutputFormat) -> ExitCode {
match run_inner(args, format) {
Ok(code) => code,
Err(e) => {
e.emit();
ExitCode::from(e.kind as u8)
}
}
}
fn run_inner(args: &DoctorArgs, format: OutputFormat) -> Result<ExitCode, CliError> {
if args.dry_run && !args.fix {
return Err(CliError::user(
"invalid_arguments",
"--dry-run only applies with --fix (doctor is read-only by default)",
));
}
let checks = run_checks();
let summary = summarize(&checks);
let any_fail = summary.fail > 0;
let report = DoctorReport { checks, summary };
match format {
OutputFormat::Json => crate::output::emit_json(&report, &[])?,
OutputFormat::Text => render_text(&report),
}
if args.fix && format == OutputFormat::Text {
let verb = if args.dry_run {
"would apply"
} else {
"applied"
};
eprintln!("doctor --fix: {verb} 0 fixes (no auto-fixable checks yet)");
}
Ok(if any_fail {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
})
}
fn run_checks() -> Vec<CheckResult> {
vec![CheckResult {
id: "binary.self",
status: Status::Ok,
message: format!("ossctl {} responding", env!("CARGO_PKG_VERSION")),
fix_suggestion: None,
}]
}
fn summarize(checks: &[CheckResult]) -> Summary {
let mut s = Summary {
ok: 0,
warn: 0,
fail: 0,
};
for c in checks {
match c.status {
Status::Ok => s.ok += 1,
Status::Warn => s.warn += 1,
Status::Fail => s.fail += 1,
}
}
s
}
fn render_text(report: &DoctorReport) {
for c in &report.checks {
let tag = match c.status {
Status::Ok => "OK",
Status::Warn => "WARN",
Status::Fail => "FAIL",
};
println!("{tag:<4} {} {}", c.id, c.message);
if let Some(fix) = &c.fix_suggestion {
println!(" fix: {fix}");
}
}
println!(
"summary: {} ok, {} warn, {} fail",
report.summary.ok, report.summary.warn, report.summary.fail
);
}