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 let opts = DoctorOpts { fix: args.fix, lint_config: args.lint_config, shallow: args.shallow };
16 let report = run_doctor(&workspace, &opts)?;
17
18 if global.json {
19 println!("{}", render_json(&report));
20 } else {
21 print_table(&report);
22 }
23
24 if args.scan_undeclared {
29 let undeclared = scan_undeclared(&workspace, args.depth)?;
30 if global.json {
31 println!("{}", render_undeclared_json(&workspace, args.depth, &undeclared));
32 } else {
33 print_undeclared(&workspace, args.depth, &undeclared);
34 }
35 }
36
37 std::process::exit(report.exit_code());
38}
39
40fn print_table(report: &DoctorReport) {
42 println!("{:<18} {:<8} DETAIL", "CHECK", "STATUS");
43 for f in &report.findings {
44 let status = match f.severity {
45 Severity::Ok => "OK",
46 Severity::Warning => "WARN",
47 Severity::Error => "ERROR",
48 };
49 let detail = if f.detail.is_empty() { "-".to_string() } else { f.detail.clone() };
50 let pack = f.pack.as_deref().unwrap_or("");
51 let label = if pack.is_empty() {
52 f.check.label().to_string()
53 } else {
54 format!("{}[{}]", f.check.label(), pack)
55 };
56 println!("{label:<18} {status:<8} {detail}");
57 }
58}
59
60fn render_json(report: &DoctorReport) -> String {
65 let findings: Vec<serde_json::Value> = report
66 .findings
67 .iter()
68 .map(|f| {
69 serde_json::json!({
70 "check": f.check.label(),
71 "severity": severity_label(f.severity),
72 "pack": f.pack,
73 "detail": f.detail,
74 "auto_fixable": f.auto_fixable,
75 "synthetic": f.synthetic,
76 })
77 })
78 .collect();
79 let doc = serde_json::json!({
80 "exit_code": report.exit_code(),
81 "worst_severity": severity_label(report.worst()),
82 "findings": findings,
83 });
84 serde_json::to_string(&doc).unwrap_or_else(|_| "{}".to_string())
87}
88
89fn severity_label(s: Severity) -> &'static str {
90 match s {
91 Severity::Ok => "ok",
92 Severity::Warning => "warning",
93 Severity::Error => "error",
94 }
95}
96
97fn print_undeclared(workspace: &std::path::Path, depth: Option<usize>, found: &[UndeclaredRepo]) {
101 let depth_str = depth.map_or_else(|| "unlimited".to_string(), |d| d.to_string());
102 println!();
103 println!("Scanning {} for undeclared git repos (depth: {})...", workspace.display(), depth_str,);
104 if found.is_empty() {
105 println!("No undeclared git repos found below {}.", workspace.display());
106 return;
107 }
108 println!();
109 println!(
110 "Found {} undeclared git repo{}:",
111 found.len(),
112 if found.len() == 1 { "" } else { "s" },
113 );
114 for repo in found {
115 let url = match &repo.inferred_url {
116 Some(u) => format!("<{u}>"),
117 None => "[unknown] (no remote.origin.url)".to_string(),
118 };
119 let path = repo.path.to_string_lossy().replace('\\', "/");
121 println!(" ./{path:<40} {url}");
122 }
123 println!();
124 println!("To register: grex add <url> <path>");
125}
126
127fn render_undeclared_json(
131 workspace: &std::path::Path,
132 depth: Option<usize>,
133 found: &[UndeclaredRepo],
134) -> String {
135 let entries: Vec<serde_json::Value> = found
136 .iter()
137 .map(|r| {
138 let path = r.path.to_string_lossy().replace('\\', "/");
139 serde_json::json!({
140 "path": path,
141 "inferred_url": r.inferred_url,
142 })
143 })
144 .collect();
145 let doc = serde_json::json!({
146 "scan_undeclared": {
147 "workspace": workspace.display().to_string(),
148 "depth": depth,
149 "count": found.len(),
150 "repos": entries,
151 },
152 });
153 serde_json::to_string(&doc).unwrap_or_else(|_| "{}".to_string())
154}