Skip to main content

ggen_cli_lib/cmds/
doctor.rs

1//! Doctor Commands
2//!
3//! This module provides health-check and diagnostic commands for the ggen workspace.
4
5use 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// ============================================================================
12// Output Types
13// ============================================================================
14
15#[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
42// ============================================================================
43// Domain Integration Helpers
44// ============================================================================
45
46fn 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// ============================================================================
127// Verb Functions
128// ============================================================================
129
130/// Run a full health check: ggen.toml presence, binary version, workspace state, and toolchain
131#[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/// Quick validation: verifies the workspace can be found and ggen.toml is present
160#[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}