greentic_component/cmd/
doctor.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use clap::{Args, Parser};
5
6use crate::{ComponentError, manifest::validate_manifest, prepare_component};
7
8#[derive(Args, Debug, Clone)]
9#[command(about = "Run health checks against a Greentic component artifact")]
10pub struct DoctorArgs {
11    /// Path or identifier resolvable by the loader
12    pub target: String,
13}
14
15#[derive(Parser, Debug)]
16struct DoctorCli {
17    #[command(flatten)]
18    args: DoctorArgs,
19}
20
21pub fn parse_from_cli() -> DoctorArgs {
22    DoctorCli::parse().args
23}
24
25pub fn run(args: DoctorArgs) -> Result<(), ComponentError> {
26    if let Some(report) = detect_scaffold(&args.target) {
27        report.print();
28        return Ok(());
29    }
30    let prepared = prepare_component(&args.target)?;
31
32    let manifest_json = fs::read_to_string(&prepared.manifest_path)?;
33    validate_manifest(&manifest_json)?;
34    println!("manifest schema: ok");
35
36    println!("hash verification: ok ({})", prepared.wasm_hash);
37    println!("world check: ok ({})", prepared.manifest.world.as_str());
38    println!(
39        "lifecycle exports: init={} health={} shutdown={}",
40        prepared.lifecycle.init, prepared.lifecycle.health, prepared.lifecycle.shutdown
41    );
42    println!(
43        "describe payload versions: {}",
44        prepared.describe.versions.len()
45    );
46    if prepared.redaction_paths().is_empty() {
47        println!("redaction hints: none (ensure secrets use x-redact)");
48    } else {
49        println!("redaction hints: {}", prepared.redaction_paths().len());
50        for path in prepared.redaction_paths() {
51            println!("  - {}", path.as_str());
52        }
53    }
54    if prepared.defaults_applied().is_empty() {
55        println!("defaults applied: none");
56    } else {
57        println!("defaults applied:");
58        for entry in prepared.defaults_applied() {
59            println!("  - {entry}");
60        }
61    }
62    let caps = &prepared.manifest.capabilities;
63    println!("supports: {:?}", prepared.manifest.supports);
64    println!(
65        "capabilities declared: wasi(fs={}, env={}, random={}, clocks={}) host(secrets={}, state={}, messaging={}, events={}, http={}, telemetry={}, iac={})",
66        caps.wasi.filesystem.is_some(),
67        caps.wasi.env.is_some(),
68        caps.wasi.random,
69        caps.wasi.clocks,
70        caps.host.secrets.is_some(),
71        caps.host.state.is_some(),
72        caps.host.messaging.is_some(),
73        caps.host.events.is_some(),
74        caps.host.http.is_some(),
75        caps.host.telemetry.is_some(),
76        caps.host.iac.is_some()
77    );
78    println!("limits configured: {}", prepared.manifest.limits.is_some());
79    Ok(())
80}
81
82fn detect_scaffold(target: &str) -> Option<ScaffoldReport> {
83    let path = PathBuf::from(target);
84    let metadata = fs::metadata(&path).ok()?;
85    if !metadata.is_dir() {
86        return None;
87    }
88    ScaffoldReport::from_dir(&path)
89}
90
91struct ScaffoldReport {
92    root: PathBuf,
93    manifest: bool,
94    cargo: bool,
95    schemas: bool,
96    src: bool,
97}
98
99impl ScaffoldReport {
100    fn from_dir(root: &Path) -> Option<Self> {
101        let manifest = root.join("component.manifest.json");
102        if !manifest.exists() {
103            return None;
104        }
105        Some(Self {
106            root: root.to_path_buf(),
107            manifest: manifest.is_file(),
108            cargo: root.join("Cargo.toml").is_file(),
109            schemas: root.join("schemas").is_dir(),
110            src: root.join("src").is_dir(),
111        })
112    }
113
114    fn print(&self) {
115        println!("Detected Greentic scaffold at {}", self.root.display());
116        self.print_line("component.manifest.json", self.manifest);
117        self.print_line("Cargo.toml", self.cargo);
118        self.print_line("src/", self.src);
119        self.print_line("schemas/", self.schemas);
120        if self.is_complete() {
121            println!(
122                "Next steps: run `cargo check --target wasm32-wasip2` and `greentic-component doctor` once the wasm is built."
123            );
124        } else {
125            println!(
126                "Some scaffold pieces are missing. Re-run `greentic-component new` or restore the template files."
127            );
128        }
129    }
130
131    fn print_line(&self, label: &str, ok: bool) {
132        if ok {
133            println!("  [ok] {label}");
134        } else {
135            println!("  [missing] {label}");
136        }
137    }
138
139    fn is_complete(&self) -> bool {
140        self.manifest && self.schemas
141    }
142}