grex_cli/cli/verbs/
doctor.rs1use crate::cli::args::{DoctorArgs, GlobalFlags};
7use anyhow::Result;
8use grex_core::doctor::{
9 run_doctor, scan_undeclared, DoctorOpts, DoctorReport, Severity, UndeclaredRepo,
10};
11use tokio_util::sync::CancellationToken;
12
13pub fn run(args: DoctorArgs, global: &GlobalFlags, _cancel: &CancellationToken) -> Result<()> {
14 let workspace = std::env::current_dir()?;
15
16 let prune_quarantine = if args.prune_quarantine {
22 Some(args.retain_days.unwrap_or(grex_core::tree::DEFAULT_RETAIN_DAYS))
23 } else {
24 None
25 };
26 let restore_quarantine = args.restore_quarantine.as_deref().map(|raw| {
31 if let Some((ts, basename)) = raw.split_once(':') {
32 (ts.to_owned(), Some(basename.to_owned()))
33 } else {
34 (raw.to_owned(), None)
35 }
36 });
37
38 let mut opts = DoctorOpts::default();
41 opts.fix = args.fix;
42 opts.lint_config = args.lint_config;
43 opts.shallow = args.shallow;
44 opts.prune_quarantine = prune_quarantine;
45 opts.restore_quarantine = restore_quarantine;
46 opts.force = args.force;
47 let report = run_doctor(&workspace, &opts)?;
48
49 if global.json {
50 println!("{}", render_json(&workspace, &report));
51 } else {
52 print_table(&report);
53 }
54
55 if args.scan_undeclared {
60 let undeclared = scan_undeclared(&workspace, args.depth)?;
61 if global.json {
62 println!("{}", render_undeclared_json(&workspace, args.depth, &undeclared));
63 } else {
64 print_undeclared(&workspace, args.depth, &undeclared);
65 }
66 }
67
68 std::process::exit(report.exit_code());
69}
70
71fn print_table(report: &DoctorReport) {
73 println!("{:<18} {:<8} DETAIL", "CHECK", "STATUS");
74 for f in &report.findings {
75 let status = match f.severity {
76 Severity::Ok => "OK",
77 Severity::Warning => "WARN",
78 Severity::Error => "ERROR",
79 };
80 let detail = if f.detail.is_empty() { "-".to_string() } else { f.detail.clone() };
81 let pack = f.pack.as_deref().unwrap_or("");
82 let label = if pack.is_empty() {
83 f.check.label().to_string()
84 } else {
85 format!("{}[{}]", f.check.label(), pack)
86 };
87 println!("{label:<18} {status:<8} {detail}");
88 }
89}
90
91fn render_json(workspace: &std::path::Path, report: &DoctorReport) -> String {
105 let findings: Vec<serde_json::Value> = report
106 .findings
107 .iter()
108 .map(|f| {
109 serde_json::json!({
110 "check": f.check.label(),
111 "severity": severity_label(f.severity),
112 "pack": f.pack,
113 "detail": f.detail,
114 "auto_fixable": f.auto_fixable,
115 "synthetic": f.synthetic,
116 })
117 })
118 .collect();
119 let workspace_str = workspace.display().to_string();
125 let doc = serde_json::json!({
126 "workspace": workspace_str,
127 "pack": workspace_str,
128 "report": {
129 "exit_code": report.exit_code(),
130 "worst_severity": severity_label(report.worst()),
131 "findings": findings,
132 },
133 });
134 serde_json::to_string(&doc).unwrap_or_else(|_| "{}".to_string())
137}
138
139fn severity_label(s: Severity) -> &'static str {
140 match s {
141 Severity::Ok => "ok",
142 Severity::Warning => "warning",
143 Severity::Error => "error",
144 }
145}
146
147fn print_undeclared(workspace: &std::path::Path, depth: Option<usize>, found: &[UndeclaredRepo]) {
151 let depth_str = depth.map_or_else(|| "unlimited".to_string(), |d| d.to_string());
152 println!();
153 println!("Scanning {} for undeclared git repos (depth: {})...", workspace.display(), depth_str,);
154 if found.is_empty() {
155 println!("No undeclared git repos found below {}.", workspace.display());
156 return;
157 }
158 println!();
159 println!(
160 "Found {} undeclared git repo{}:",
161 found.len(),
162 if found.len() == 1 { "" } else { "s" },
163 );
164 for repo in found {
165 let url = match &repo.inferred_url {
166 Some(u) => format!("<{u}>"),
167 None => "[unknown] (no remote.origin.url)".to_string(),
168 };
169 let path = repo.path.to_string_lossy().replace('\\', "/");
171 println!(" ./{path:<40} {url}");
172 }
173 println!();
174 println!("To register: grex add <url> <path>");
175}
176
177fn render_undeclared_json(
181 workspace: &std::path::Path,
182 depth: Option<usize>,
183 found: &[UndeclaredRepo],
184) -> String {
185 let entries: Vec<serde_json::Value> = found
186 .iter()
187 .map(|r| {
188 let path = r.path.to_string_lossy().replace('\\', "/");
189 serde_json::json!({
190 "path": path,
191 "inferred_url": r.inferred_url,
192 })
193 })
194 .collect();
195 let workspace_str = workspace.display().to_string();
198 let doc = serde_json::json!({
199 "scan_undeclared": {
200 "workspace": workspace_str,
201 "pack": workspace_str,
202 "depth": depth,
203 "count": found.len(),
204 "repos": entries,
205 },
206 });
207 serde_json::to_string(&doc).unwrap_or_else(|_| "{}".to_string())
208}