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: if found {
80 None
81 } else {
82 match name {
83 "ggen.toml" => Some("Run 'ggen init' to create a manifest".to_string()),
84 "Cargo.toml" => Some("Check if you are in the correct project root".to_string()),
85 ".specify directory" => {
86 Some("Ensure your RDF ontologies are in .specify/".to_string())
87 }
88 _ => None,
89 }
90 },
91 }
92}
93
94fn toolchain_checks() -> Result<Vec<CheckItem>> {
95 let domain_result = crate::runtime::block_on(execute_doctor(DoctorInput {
96 verbose: false,
97 check: None,
98 env: false,
99 }))
100 .map_err(|e| NounVerbError::execution_error(format!("tokio runtime error: {}", e)))?
101 .map_err(|e| NounVerbError::execution_error(format!("doctor domain error: {}", e)))?;
102
103 Ok(domain_result
104 .checks
105 .into_iter()
106 .map(|c| CheckItem {
107 passed: matches!(c.status, CheckStatus::Ok),
108 detail: c.message,
109 name: c.name,
110 recovery: c.recovery,
111 })
112 .collect())
113}
114
115fn is_healthy(checks: &[CheckItem]) -> bool {
116 let tool_ok = |name: &str| {
117 checks
118 .iter()
119 .find(|c| c.name == name)
120 .map(|c| c.passed)
121 .unwrap_or(false)
122 };
123 tool_ok("ggen.toml") && tool_ok("Rust") && tool_ok("Cargo")
124}
125
126#[verb]
132pub fn run() -> Result<RunOutput> {
133 let cwd = std::env::current_dir()
134 .map(|p| p.display().to_string())
135 .unwrap_or_else(|_| "<unknown>".to_string());
136 let ggen_toml_found = Path::new("ggen.toml").exists();
137 let binary_version = env!("CARGO_PKG_VERSION").to_string();
138
139 let mut checks = workspace_checks();
140 checks.extend(toolchain_checks()?);
141
142 let healthy = is_healthy(&checks);
143 let message = if healthy {
144 "All critical health checks passed".to_string()
145 } else {
146 "One or more health checks failed — see checks for details".to_string()
147 };
148
149 Ok(RunOutput {
150 healthy,
151 binary_version,
152 ggen_toml_found,
153 workspace_root: cwd,
154 checks,
155 message,
156 })
157}
158
159#[verb]
161pub fn check() -> Result<CheckOutput> {
162 let cwd = std::env::current_dir()
163 .map(|p| p.display().to_string())
164 .unwrap_or_else(|_| "<unknown>".to_string());
165 let ggen_toml_found = Path::new("ggen.toml").exists();
166 let (passed, message) = if ggen_toml_found {
167 (true, "Quick check passed — ggen.toml found".to_string())
168 } else {
169 (
170 false,
171 "Quick check failed — ggen.toml not found in current directory".to_string(),
172 )
173 };
174 Ok(CheckOutput {
175 passed,
176 ggen_toml_found,
177 workspace_root: cwd,
178 message,
179 recovery: if ggen_toml_found {
180 None
181 } else {
182 Some("Create ggen.toml in the workspace root".to_string())
183 },
184 })
185}