ggen_cli_lib/cmds/
doctor.rs1use clap_noun_verb::{NounVerbError, Result};
6use clap_noun_verb_macros::verb;
7use ggen_core::domain::utils::{execute_doctor, CheckStatus, DoctorInput};
8use serde::Serialize;
9use std::path::Path;
10
11#[derive(Serialize)]
16pub struct RunOutput {
17 pub healthy: bool,
18 pub binary_version: String,
19 pub ggen_toml_found: bool,
20 pub workspace_root: String,
21 pub checks: Vec<CheckItem>,
22 pub message: String,
23}
24
25#[derive(Serialize)]
26pub struct CheckItem {
27 pub name: String,
28 pub passed: bool,
29 pub detail: String,
30 pub recovery: Option<String>,
31}
32
33#[derive(Serialize)]
34pub struct CheckOutput {
35 pub passed: bool,
36 pub ggen_toml_found: bool,
37 pub workspace_root: String,
38 pub message: String,
39 pub recovery: Option<String>,
40}
41
42fn workspace_checks() -> Vec<CheckItem> {
47 vec![
48 path_check(
49 "ggen.toml",
50 "ggen.toml",
51 "Found ggen.toml in current directory",
52 "ggen.toml not found in current directory",
53 ),
54 path_check(
55 "Cargo.toml",
56 "Cargo.toml",
57 "Found Cargo.toml — likely in a Rust workspace",
58 "Cargo.toml not found — not in a Rust workspace root",
59 ),
60 path_check(
61 ".specify directory",
62 ".specify",
63 "Found .specify directory — RDF specs present",
64 ".specify directory not found — no RDF specs",
65 ),
66 ]
67}
68
69fn path_check(name: &str, path: &str, found_msg: &str, missing_msg: &str) -> CheckItem {
70 let found = Path::new(path).exists();
71 CheckItem {
72 name: name.to_string(),
73 passed: found,
74 detail: if found {
75 found_msg.to_string()
76 } else {
77 missing_msg.to_string()
78 },
79 recovery: None,
80 }
81}
82
83fn toolchain_checks() -> Result<Vec<CheckItem>> {
84 let domain_result = crate::runtime::block_on(execute_doctor(DoctorInput {
85 verbose: false,
86 check: None,
87 env: false,
88 }))
89 .map_err(|e| NounVerbError::execution_error(format!("tokio runtime error: {}", e)))?
90 .map_err(|e| NounVerbError::execution_error(format!("doctor domain error: {}", e)))?;
91
92 Ok(domain_result
93 .checks
94 .into_iter()
95 .map(|c| CheckItem {
96 passed: matches!(c.status, CheckStatus::Ok),
97 detail: c.message,
98 name: c.name,
99 recovery: c.recovery,
100 })
101 .collect())
102}
103
104fn is_healthy(checks: &[CheckItem]) -> bool {
105 let tool_ok = |name: &str| {
106 checks
107 .iter()
108 .find(|c| c.name == name)
109 .map(|c| c.passed)
110 .unwrap_or(false)
111 };
112 tool_ok("ggen.toml") && tool_ok("Rust") && tool_ok("Cargo")
113}
114
115#[verb]
121pub fn run() -> Result<RunOutput> {
122 let cwd = std::env::current_dir()
123 .map(|p| p.display().to_string())
124 .unwrap_or_else(|_| "<unknown>".to_string());
125 let ggen_toml_found = Path::new("ggen.toml").exists();
126 let binary_version = env!("CARGO_PKG_VERSION").to_string();
127
128 let mut checks = workspace_checks();
129 checks.extend(toolchain_checks()?);
130
131 let healthy = is_healthy(&checks);
132 let message = if healthy {
133 "All critical health checks passed".to_string()
134 } else {
135 "One or more health checks failed — see checks for details".to_string()
136 };
137
138 Ok(RunOutput {
139 healthy,
140 binary_version,
141 ggen_toml_found,
142 workspace_root: cwd,
143 checks,
144 message,
145 })
146}
147
148#[verb]
150pub fn check() -> Result<CheckOutput> {
151 let cwd = std::env::current_dir()
152 .map(|p| p.display().to_string())
153 .unwrap_or_else(|_| "<unknown>".to_string());
154 let ggen_toml_found = Path::new("ggen.toml").exists();
155 let (passed, message) = if ggen_toml_found {
156 (true, "Quick check passed — ggen.toml found".to_string())
157 } else {
158 (
159 false,
160 "Quick check failed — ggen.toml not found in current directory".to_string(),
161 )
162 };
163 Ok(CheckOutput {
164 passed,
165 ggen_toml_found,
166 workspace_root: cwd,
167 message,
168 recovery: if ggen_toml_found {
169 None
170 } else {
171 Some("Create ggen.toml in the workspace root".to_string())
172 },
173 })
174}