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 DoctorOutput {
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
33fn workspace_checks() -> Vec<CheckItem> {
38 vec![
39 path_check(
40 "ggen.toml",
41 "ggen.toml",
42 "Found ggen.toml in current directory",
43 "ggen.toml not found in current directory",
44 ),
45 path_check(
46 "Cargo.toml",
47 "Cargo.toml",
48 "Found Cargo.toml — likely in a Rust workspace",
49 "Cargo.toml not found — not in a Rust workspace root",
50 ),
51 path_check(
52 ".specify directory",
53 ".specify",
54 "Found .specify directory — RDF specs present",
55 ".specify directory not found — no RDF specs",
56 ),
57 ]
58}
59
60fn path_check(name: &str, path: &str, found_msg: &str, missing_msg: &str) -> CheckItem {
61 let found = Path::new(path).exists();
62 CheckItem {
63 name: name.to_string(),
64 passed: found,
65 detail: if found {
66 found_msg.to_string()
67 } else {
68 missing_msg.to_string()
69 },
70 recovery: if found {
71 None
72 } else {
73 match name {
74 "ggen.toml" => Some("Run 'ggen init' to create a manifest".to_string()),
75 "Cargo.toml" => Some("Check if you are in the correct project root".to_string()),
76 ".specify directory" => {
77 Some("Ensure your RDF ontologies are in .specify/".to_string())
78 }
79 _ => None,
80 }
81 },
82 }
83}
84
85fn toolchain_checks(all: bool, check: Option<String>) -> Result<Vec<CheckItem>> {
86 let domain_result = crate::runtime::block_on(execute_doctor(DoctorInput {
87 verbose: false,
88 check,
89 env: false,
90 all,
91 }))
92 .map_err(|e| NounVerbError::execution_error(format!("tokio runtime error: {}", e)))?
93 .map_err(|e| NounVerbError::execution_error(format!("doctor domain error: {}", e)))?;
94
95 Ok(domain_result
96 .checks
97 .into_iter()
98 .map(|c| CheckItem {
99 passed: matches!(c.status, CheckStatus::Ok),
100 detail: c.message,
101 name: c.name,
102 recovery: c.recovery,
103 })
104 .collect())
105}
106
107fn is_healthy(checks: &[CheckItem]) -> bool {
108 let tool_ok = |name: &str| {
109 checks
110 .iter()
111 .find(|c| c.name == name)
112 .map(|c| c.passed)
113 .unwrap_or(false)
114 };
115 tool_ok("ggen.toml") && tool_ok("Rust") && tool_ok("Cargo")
116}
117
118#[verb("doctor", "root")]
128pub fn doctor(all: bool, check: Option<String>) -> Result<DoctorOutput> {
129 let cwd = std::env::current_dir()
130 .map(|p| p.display().to_string())
131 .unwrap_or_else(|_| "<unknown>".to_string());
132 let ggen_toml_found = Path::new("ggen.toml").exists();
133 let binary_version = env!("CARGO_PKG_VERSION").to_string();
134
135 let mut checks = workspace_checks();
136 checks.extend(toolchain_checks(all, check)?);
137
138 let healthy = is_healthy(&checks);
139 let message = if healthy {
140 "All critical health checks passed".to_string()
141 } else {
142 "One or more health checks failed — see checks for details".to_string()
143 };
144
145 Ok(DoctorOutput {
146 healthy,
147 binary_version,
148 ggen_toml_found,
149 workspace_root: cwd,
150 checks,
151 message,
152 })
153}