Skip to main content

greentic_component/cmd/
inspect.rs

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