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: 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// ============================================================================
116// Verb Functions
117// ============================================================================
118
119/// Run a full health check: ggen.toml presence, binary version, workspace state, and toolchain
120#[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/// Quick validation: verifies the workspace can be found and ggen.toml is present
149#[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}