greentic_component/cmd/
inspect.rs

1use clap::{Args, Parser};
2use serde_json::Value;
3
4use crate::{ComponentError, PreparedComponent, prepare_component};
5
6#[derive(Args, Debug, Clone)]
7#[command(about = "Inspect a Greentic component artifact")]
8pub struct InspectArgs {
9    /// Path or identifier resolvable by the loader
10    pub target: String,
11    /// Emit structured JSON instead of human output
12    #[arg(long)]
13    pub json: bool,
14    /// Treat warnings as errors
15    #[arg(long)]
16    pub strict: bool,
17}
18
19#[derive(Parser, Debug)]
20struct InspectCli {
21    #[command(flatten)]
22    args: InspectArgs,
23}
24
25pub fn parse_from_cli() -> InspectArgs {
26    InspectCli::parse().args
27}
28
29#[derive(Default)]
30pub struct InspectResult {
31    pub warnings: Vec<String>,
32}
33
34pub fn run(args: &InspectArgs) -> Result<InspectResult, ComponentError> {
35    let prepared = prepare_component(&args.target)?;
36    if args.json {
37        let json = serde_json::to_string_pretty(&build_report(&prepared))
38            .expect("serializing inspect report");
39        println!("{json}");
40    } else {
41        println!("component: {}", prepared.manifest.id.as_str());
42        println!("  wasm: {}", prepared.wasm_path.display());
43        println!("  world ok: {}", prepared.world_ok);
44        println!("  hash: {}", prepared.wasm_hash);
45        println!("  supports: {:?}", prepared.manifest.supports);
46        println!(
47            "  profiles: default={:?} supported={:?}",
48            prepared.manifest.profiles.default, prepared.manifest.profiles.supported
49        );
50        println!(
51            "  lifecycle: init={} health={} shutdown={}",
52            prepared.lifecycle.init, prepared.lifecycle.health, prepared.lifecycle.shutdown
53        );
54        let caps = &prepared.manifest.capabilities;
55        println!(
56            "  capabilities: wasi(fs={}, env={}, random={}, clocks={}) host(secrets={}, state={}, messaging={}, events={}, http={}, telemetry={}, iac={})",
57            caps.wasi.filesystem.is_some(),
58            caps.wasi.env.is_some(),
59            caps.wasi.random,
60            caps.wasi.clocks,
61            caps.host.secrets.is_some(),
62            caps.host.state.is_some(),
63            caps.host.messaging.is_some(),
64            caps.host.events.is_some(),
65            caps.host.http.is_some(),
66            caps.host.telemetry.is_some(),
67            caps.host.iac.is_some(),
68        );
69        println!(
70            "  limits: {}",
71            prepared
72                .manifest
73                .limits
74                .as_ref()
75                .map(|l| format!("{} MB / {} ms", l.memory_mb, l.wall_time_ms))
76                .unwrap_or_else(|| "default".into())
77        );
78        println!(
79            "  telemetry prefix: {}",
80            prepared
81                .manifest
82                .telemetry
83                .as_ref()
84                .map(|t| t.span_prefix.as_str())
85                .unwrap_or("<none>")
86        );
87        println!("  describe versions: {}", prepared.describe.versions.len());
88        println!("  redaction paths: {}", prepared.redaction_paths().len());
89        println!("  defaults applied: {}", prepared.defaults_applied().len());
90    }
91    Ok(InspectResult::default())
92}
93
94pub fn emit_warnings(warnings: &[String]) {
95    for warning in warnings {
96        eprintln!("warning: {warning}");
97    }
98}
99
100pub fn build_report(prepared: &PreparedComponent) -> Value {
101    let caps = &prepared.manifest.capabilities;
102    serde_json::json!({
103        "manifest": &prepared.manifest,
104        "manifest_path": prepared.manifest_path,
105        "wasm_path": prepared.wasm_path,
106        "wasm_hash": prepared.wasm_hash,
107        "hash_verified": prepared.hash_verified,
108        "world": {
109            "expected": prepared.manifest.world.as_str(),
110            "ok": prepared.world_ok,
111        },
112        "lifecycle": {
113            "init": prepared.lifecycle.init,
114            "health": prepared.lifecycle.health,
115            "shutdown": prepared.lifecycle.shutdown,
116        },
117        "describe": prepared.describe,
118        "capabilities": prepared.manifest.capabilities,
119        "limits": prepared.manifest.limits,
120        "telemetry": prepared.manifest.telemetry,
121        "redactions": prepared
122            .redaction_paths()
123            .iter()
124            .map(|p| p.as_str().to_string())
125            .collect::<Vec<_>>(),
126        "defaults_applied": prepared.defaults_applied(),
127        "summary": {
128            "supports": prepared.manifest.supports,
129            "profiles": prepared.manifest.profiles,
130            "capabilities": {
131                "wasi": {
132                    "filesystem": caps.wasi.filesystem.is_some(),
133                    "env": caps.wasi.env.is_some(),
134                    "random": caps.wasi.random,
135                    "clocks": caps.wasi.clocks
136                },
137                "host": {
138                    "secrets": caps.host.secrets.is_some(),
139                    "state": caps.host.state.is_some(),
140                    "messaging": caps.host.messaging.is_some(),
141                    "events": caps.host.events.is_some(),
142                    "http": caps.host.http.is_some(),
143                    "telemetry": caps.host.telemetry.is_some(),
144                    "iac": caps.host.iac.is_some()
145                }
146            },
147        }
148    })
149}