1mod daemon;
7mod memory;
8mod platform;
9mod sandbox;
10
11use owo_colors::OwoColorize;
12
13#[derive(Debug, Clone)]
15pub struct Check {
16 pub id: String,
18 pub category: &'static str,
20 pub status: Status,
22 pub value: String,
24 pub description: String,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum Status {
31 Pass,
32 Warn,
33 Fail,
34}
35
36impl std::fmt::Display for Status {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 Self::Pass => write!(f, "PASS"),
40 Self::Warn => write!(f, "WARN"),
41 Self::Fail => write!(f, "FAIL"),
42 }
43 }
44}
45
46const ALL_CATEGORIES: &[&str] = &["daemon", "memory", "sandbox", "platform"];
48
49pub fn cmd_doctor(
51 categories: &str,
52 output_format: &str,
53 exit_code: bool,
54 quiet: bool,
55) -> anyhow::Result<()> {
56 let selected: Vec<&str> = if categories == "all" {
57 ALL_CATEGORIES.to_vec()
58 } else {
59 categories.split(',').map(str::trim).collect()
60 };
61
62 let mut checks = Vec::new();
63
64 for cat in &selected {
65 match *cat {
66 "daemon" => checks.extend(daemon::checks()),
67 "memory" => checks.extend(memory::checks()),
68 "sandbox" => checks.extend(sandbox::checks()),
69 "platform" => checks.extend(platform::checks()),
70 other => {
71 eprintln!("Unknown doctor category: {other}");
72 eprintln!("Available: {}", ALL_CATEGORIES.join(", "));
73 }
74 }
75 }
76
77 let pass_count = checks.iter().filter(|c| c.status == Status::Pass).count();
78 let warn_count = checks.iter().filter(|c| c.status == Status::Warn).count();
79 let fail_count = checks.iter().filter(|c| c.status == Status::Fail).count();
80
81 match output_format {
82 "json" => print_json(&checks, pass_count, warn_count, fail_count, quiet),
83 _ => print_text(&checks, pass_count, warn_count, fail_count, quiet),
84 }
85
86 if exit_code {
87 if fail_count > 0 {
88 std::process::exit(1);
89 } else if warn_count > 0 {
90 std::process::exit(2);
91 }
92 }
93
94 Ok(())
95}
96
97fn print_text(checks: &[Check], pass: usize, warn: usize, fail: usize, quiet: bool) {
98 if quiet {
99 return;
100 }
101
102 let mut current_category = "";
103 for check in checks {
104 if check.category != current_category {
105 if !current_category.is_empty() {
106 println!();
107 }
108 println!("{}", check.category.to_uppercase().bold());
109 current_category = check.category;
110 }
111
112 let icon = match check.status {
113 Status::Pass => "✓".green().to_string(),
114 Status::Warn => "⚠".yellow().to_string(),
115 Status::Fail => "✗".red().to_string(),
116 };
117
118 let value_display = match check.status {
119 Status::Pass => check.value.green().to_string(),
120 Status::Warn => check.value.yellow().to_string(),
121 Status::Fail => check.value.red().to_string(),
122 };
123
124 println!(" {icon} {:<32} {value_display}", check.id,);
125 if check.status != Status::Pass && !check.description.is_empty() {
126 println!(" {}", check.description.dimmed());
127 }
128 }
129
130 println!();
131 let summary = format!("{pass} passed, {warn} warnings, {fail} failures");
132 if fail > 0 {
133 println!("{}", summary.red());
134 } else if warn > 0 {
135 println!("{}", summary.yellow());
136 } else {
137 println!("{}", summary.green());
138 }
139}
140
141fn print_json(checks: &[Check], pass: usize, warn: usize, fail: usize, quiet: bool) {
142 if quiet {
143 return;
144 }
145
146 let checks_json: Vec<serde_json::Value> = checks
147 .iter()
148 .map(|c| {
149 serde_json::json!({
150 "id": c.id,
151 "category": c.category,
152 "status": c.status.to_string().to_lowercase(),
153 "value": c.value,
154 "description": c.description,
155 })
156 })
157 .collect();
158
159 let output = serde_json::json!({
160 "timestamp": chrono_now(),
161 "version": env!("CARGO_PKG_VERSION"),
162 "summary": { "pass": pass, "warn": warn, "fail": fail },
163 "checks": checks_json,
164 });
165
166 println!(
167 "{}",
168 serde_json::to_string_pretty(&output).unwrap_or_default()
169 );
170}
171
172fn chrono_now() -> String {
173 let output = std::process::Command::new("date")
175 .arg("-Iseconds")
176 .output()
177 .ok()
178 .and_then(|o| String::from_utf8(o.stdout).ok())
179 .unwrap_or_default();
180 output.trim().to_string()
181}