greentic_component/cmd/
doctor.rs1use 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 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}