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 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
33// ============================================================================
34// Domain Integration Helpers
35// ============================================================================
36
37fn 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// ============================================================================
119// Verb Functions
120// ============================================================================
121
122/// Health check: ggen.toml presence, binary version, workspace state, and
123/// toolchain (rust/cargo/git/marketplace/cache). Fast, local-only by
124/// default — pass `all` to also run SLO microbenchmarks and probe the
125/// observability stack (Tempo/OTel/Jaeger), or `check` to run a single
126/// named check (e.g. "rust", "slo", "observability").
127#[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}