1#![forbid(unsafe_code)]
2
3use std::{
4 fs,
5 path::{Path, PathBuf},
6};
7
8use anyhow::{Context, Result, anyhow, bail};
9use clap::Parser;
10use greentic_pack::{PackLoad, SigningPolicy, open_pack};
11use greentic_types::component_source::ComponentSourceRef;
12use greentic_types::pack::extensions::component_sources::{
13 ArtifactLocationV1, ComponentSourcesV1, EXT_COMPONENT_SOURCES_V1,
14};
15use greentic_types::pack_manifest::PackManifest;
16use greentic_types::provider::ProviderDecl;
17use tempfile::TempDir;
18
19use crate::build;
20use crate::runtime::RuntimeContext;
21
22#[derive(Debug, Parser)]
23pub struct InspectArgs {
24 #[arg(value_name = "PATH")]
26 pub path: Option<PathBuf>,
27
28 #[arg(long, value_name = "FILE", conflicts_with = "input")]
30 pub pack: Option<PathBuf>,
31
32 #[arg(long = "in", value_name = "DIR", conflicts_with = "pack")]
34 pub input: Option<PathBuf>,
35
36 #[arg(long)]
38 pub archive: bool,
39
40 #[arg(long)]
42 pub source: bool,
43
44 #[arg(long = "allow-oci-tags", default_value_t = false)]
46 pub allow_oci_tags: bool,
47}
48
49pub async fn handle(args: InspectArgs, json: bool, runtime: &RuntimeContext) -> Result<()> {
50 let mode = resolve_mode(&args)?;
51
52 let load = match mode {
53 InspectMode::Archive(path) => inspect_pack_file(&path)?,
54 InspectMode::Source(path) => {
55 inspect_source_dir(&path, runtime, args.allow_oci_tags).await?
56 }
57 };
58
59 if json {
60 let payload = serde_json::json!({
61 "manifest": load.manifest,
62 "report": {
63 "signature_ok": load.report.signature_ok,
64 "sbom_ok": load.report.sbom_ok,
65 "warnings": load.report.warnings,
66 },
67 "sbom": load.sbom,
68 });
69 println!("{}", serde_json::to_string_pretty(&payload)?);
70 return Ok(());
71 }
72
73 print_human(&load);
74 Ok(())
75}
76
77fn inspect_pack_file(path: &Path) -> Result<PackLoad> {
78 let load = open_pack(path, SigningPolicy::DevOk)
79 .map_err(|err| anyhow!(err.message))
80 .with_context(|| format!("failed to open pack {}", path.display()))?;
81 Ok(load)
82}
83
84enum InspectMode {
85 Archive(PathBuf),
86 Source(PathBuf),
87}
88
89fn resolve_mode(args: &InspectArgs) -> Result<InspectMode> {
90 if args.archive && args.source {
91 bail!("--archive and --source are mutually exclusive");
92 }
93 if args.pack.is_some() && args.input.is_some() {
94 bail!("exactly one of --pack or --in may be supplied");
95 }
96
97 if let Some(path) = &args.pack {
98 return Ok(InspectMode::Archive(path.clone()));
99 }
100 if let Some(path) = &args.input {
101 return Ok(InspectMode::Source(path.clone()));
102 }
103 if let Some(path) = &args.path {
104 let meta =
105 fs::metadata(path).with_context(|| format!("failed to stat {}", path.display()))?;
106 if args.archive || (path.extension() == Some(std::ffi::OsStr::new("gtpack"))) {
107 return Ok(InspectMode::Archive(path.clone()));
108 }
109 if args.source || meta.is_dir() {
110 return Ok(InspectMode::Source(path.clone()));
111 }
112 if meta.is_file() {
113 return Ok(InspectMode::Archive(path.clone()));
114 }
115 }
116 Ok(InspectMode::Source(
117 std::env::current_dir().context("determine current directory")?,
118 ))
119}
120
121async fn inspect_source_dir(
122 dir: &Path,
123 runtime: &RuntimeContext,
124 allow_oci_tags: bool,
125) -> Result<PackLoad> {
126 let pack_dir = dir
127 .canonicalize()
128 .with_context(|| format!("failed to resolve pack dir {}", dir.display()))?;
129
130 let temp = TempDir::new().context("failed to allocate temp dir for inspect")?;
131 let manifest_out = temp.path().join("manifest.cbor");
132 let gtpack_out = temp.path().join("pack.gtpack");
133
134 let opts = build::BuildOptions {
135 pack_dir,
136 component_out: None,
137 manifest_out,
138 sbom_out: None,
139 gtpack_out: Some(gtpack_out.clone()),
140 lock_path: gtpack_out.with_extension("lock.json"), bundle: build::BundleMode::Cache,
142 dry_run: false,
143 secrets_req: None,
144 default_secret_scope: None,
145 allow_oci_tags,
146 runtime: runtime.clone(),
147 skip_update: false,
148 };
149
150 build::run(&opts).await?;
151
152 inspect_pack_file(>pack_out)
153}
154
155fn print_human(load: &PackLoad) {
156 let manifest = &load.manifest;
157 let report = &load.report;
158 println!(
159 "Pack: {} ({})",
160 manifest.meta.pack_id, manifest.meta.version
161 );
162 println!("Name: {}", manifest.meta.name);
163 println!("Flows: {}", manifest.flows.len());
164 if manifest.flows.is_empty() {
165 println!("Flows list: none");
166 } else {
167 println!("Flows list:");
168 for flow in &manifest.flows {
169 println!(
170 " - {} (entry: {}, kind: {})",
171 flow.id, flow.entry, flow.kind
172 );
173 }
174 }
175 println!("Components: {}", manifest.components.len());
176 if manifest.components.is_empty() {
177 println!("Components list: none");
178 } else {
179 println!("Components list:");
180 for component in &manifest.components {
181 println!(" - {} ({})", component.name, component.version);
182 }
183 }
184 if let Some(gmanifest) = load.gpack_manifest.as_ref()
185 && let Some(value) = gmanifest
186 .extensions
187 .as_ref()
188 .and_then(|m| m.get(EXT_COMPONENT_SOURCES_V1))
189 .and_then(|ext| ext.inline.as_ref())
190 .and_then(|inline| match inline {
191 greentic_types::ExtensionInline::Other(v) => Some(v),
192 _ => None,
193 })
194 && let Ok(cs) = ComponentSourcesV1::from_extension_value(value)
195 {
196 let mut inline = 0usize;
197 let mut remote = 0usize;
198 let mut oci = 0usize;
199 let mut repo = 0usize;
200 let mut store = 0usize;
201 for entry in &cs.components {
202 match entry.artifact {
203 ArtifactLocationV1::Inline { .. } => inline += 1,
204 ArtifactLocationV1::Remote => remote += 1,
205 }
206 match entry.source {
207 ComponentSourceRef::Oci(_) => oci += 1,
208 ComponentSourceRef::Repo(_) => repo += 1,
209 ComponentSourceRef::Store(_) => store += 1,
210 }
211 }
212 println!(
213 "Component sources: {} total (origins: oci {}, repo {}, store {}; artifacts: inline {}, remote {})",
214 cs.components.len(),
215 oci,
216 repo,
217 store,
218 inline,
219 remote
220 );
221 if cs.components.is_empty() {
222 println!("Component source entries: none");
223 } else {
224 println!("Component source entries:");
225 for entry in &cs.components {
226 println!(
227 " - {} source={} artifact={}",
228 entry.name,
229 format_component_source(&entry.source),
230 format_component_artifact(&entry.artifact)
231 );
232 }
233 }
234 } else {
235 println!("Component sources: none");
236 }
237
238 if let Some(gmanifest) = load.gpack_manifest.as_ref() {
239 let providers = providers_from_manifest(gmanifest);
240 if providers.is_empty() {
241 println!("Providers: none");
242 } else {
243 println!("Providers:");
244 for provider in providers {
245 println!(
246 " - {} ({}) {}",
247 provider.provider_type,
248 provider_kind(&provider),
249 summarize_provider(&provider)
250 );
251 }
252 }
253 } else {
254 println!("Providers: none");
255 }
256
257 if !report.warnings.is_empty() {
258 println!("Warnings:");
259 for warning in &report.warnings {
260 println!(" - {}", warning);
261 }
262 }
263}
264
265fn providers_from_manifest(manifest: &PackManifest) -> Vec<ProviderDecl> {
266 let mut providers = manifest
267 .provider_extension_inline()
268 .map(|inline| inline.providers.clone())
269 .unwrap_or_default();
270 providers.sort_by(|a, b| a.provider_type.cmp(&b.provider_type));
271 providers
272}
273
274fn provider_kind(provider: &ProviderDecl) -> String {
275 provider
276 .runtime
277 .world
278 .split('@')
279 .next()
280 .unwrap_or_default()
281 .to_string()
282}
283
284fn summarize_provider(provider: &ProviderDecl) -> String {
285 let caps = provider.capabilities.len();
286 let ops = provider.ops.len();
287 let mut parts = vec![format!("caps:{caps}"), format!("ops:{ops}")];
288 parts.push(format!("config:{}", provider.config_schema_ref));
289 if let Some(docs) = provider.docs_ref.as_deref() {
290 parts.push(format!("docs:{docs}"));
291 }
292 parts.join(" ")
293}
294
295fn format_component_source(source: &ComponentSourceRef) -> String {
296 match source {
297 ComponentSourceRef::Oci(value) => format_source_ref("oci", value),
298 ComponentSourceRef::Repo(value) => format_source_ref("repo", value),
299 ComponentSourceRef::Store(value) => format_source_ref("store", value),
300 }
301}
302
303fn format_source_ref(scheme: &str, value: &str) -> String {
304 if value.contains("://") {
305 value.to_string()
306 } else {
307 format!("{scheme}://{value}")
308 }
309}
310
311fn format_component_artifact(artifact: &ArtifactLocationV1) -> String {
312 match artifact {
313 ArtifactLocationV1::Inline { wasm_path, .. } => format!("inline ({})", wasm_path),
314 ArtifactLocationV1::Remote => "remote".to_string(),
315 }
316}