greentic_component/cmd/
inspect.rs

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