greentic_component/cmd/
store.rs

1use std::fs;
2use std::path::PathBuf;
3
4use anyhow::{Context, Result, anyhow};
5use clap::{Args, Subcommand};
6use serde_json::Value;
7
8use crate::path_safety::normalize_under_root;
9use greentic_distributor_client::{DistClient, DistOptions};
10
11#[derive(Subcommand, Debug, Clone)]
12pub enum StoreCommand {
13    /// Fetch a component from a source and write the wasm bytes to disk
14    Fetch(StoreFetchArgs),
15}
16
17#[derive(Args, Debug, Clone)]
18pub struct StoreFetchArgs {
19    /// Destination directory for the fetched component bytes
20    #[arg(long, value_name = "DIR")]
21    pub out: PathBuf,
22    /// Optional cache directory for fetched components
23    #[arg(long, value_name = "DIR")]
24    pub cache_dir: Option<PathBuf>,
25    /// Source reference to resolve (file://, oci://, repo://, store://, etc.)
26    #[arg(value_name = "SOURCE")]
27    pub source: String,
28}
29
30pub fn run(command: StoreCommand) -> Result<()> {
31    match command {
32        StoreCommand::Fetch(args) => fetch(args),
33    }
34}
35
36fn fetch(args: StoreFetchArgs) -> Result<()> {
37    let mut opts = DistOptions::default();
38    if let Some(cache_dir) = &args.cache_dir {
39        opts.cache_dir = cache_dir.clone();
40    }
41    let client = DistClient::new(opts);
42    let rt = tokio::runtime::Runtime::new().context("failed to create async runtime")?;
43    let resolved = rt
44        .block_on(async { client.ensure_cached(&args.source).await })
45        .context("store fetch failed")?;
46    let cache_path = resolved
47        .cache_path
48        .ok_or_else(|| anyhow!("resolved source has no cached component path"))?;
49    fs::create_dir_all(&args.out)
50        .with_context(|| format!("failed to create output dir {}", args.out.display()))?;
51    let manifest_cache_path = cache_path
52        .parent()
53        .map(|dir| dir.join("component.manifest.json"));
54    let manifest_out_path = args.out.join("component.manifest.json");
55    let mut wasm_out_path = args.out.join("component.wasm");
56    if let Some(manifest_cache_path) = manifest_cache_path
57        && manifest_cache_path.exists()
58    {
59        let manifest_bytes = fs::read(&manifest_cache_path).with_context(|| {
60            format!(
61                "failed to read cached manifest {}",
62                manifest_cache_path.display()
63            )
64        })?;
65        fs::write(&manifest_out_path, &manifest_bytes)
66            .with_context(|| format!("failed to write manifest {}", manifest_out_path.display()))?;
67        let manifest: Value = serde_json::from_slice(&manifest_bytes).with_context(|| {
68            format!(
69                "failed to parse component.manifest.json from {}",
70                manifest_cache_path.display()
71            )
72        })?;
73        if let Some(component_wasm) = manifest
74            .get("artifacts")
75            .and_then(|artifacts| artifacts.get("component_wasm"))
76            .and_then(|value| value.as_str())
77        {
78            let candidate = PathBuf::from(component_wasm);
79            wasm_out_path = normalize_under_root(&args.out, &candidate).with_context(|| {
80                format!("invalid artifacts.component_wasm path `{}`", component_wasm)
81            })?;
82            if let Some(parent) = wasm_out_path.parent() {
83                fs::create_dir_all(parent)
84                    .with_context(|| format!("failed to create output dir {}", parent.display()))?;
85            }
86        }
87    }
88    fs::copy(&cache_path, &wasm_out_path).with_context(|| {
89        format!(
90            "failed to copy cached component {} to {}",
91            cache_path.display(),
92            wasm_out_path.display()
93        )
94    })?;
95    println!(
96        "Wrote {} (digest {}) for source {}",
97        wasm_out_path.display(),
98        resolved.digest,
99        args.source,
100    );
101    if manifest_out_path.exists() {
102        println!("Wrote {}", manifest_out_path.display());
103    }
104    Ok(())
105}