packc/
new.rs

1#![forbid(unsafe_code)]
2
3use anyhow::{Context, Result};
4use clap::Parser;
5use greentic_distributor_client::{DistClient, DistOptions};
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use crate::runtime::{NetworkPolicy, RuntimeContext};
10
11#[derive(Debug, Parser)]
12pub struct NewArgs {
13    /// Directory to create the pack in
14    #[arg(long = "dir", value_name = "DIR")]
15    pub dir: PathBuf,
16    /// Pack id to use
17    #[arg(value_name = "PACK_ID")]
18    pub pack_id: String,
19}
20
21const TEMPLATE_REF: &str = "oci://ghcr.io/greentic-ai/components/templates:latest";
22const PLACEHOLDER_DIGEST: &str =
23    "sha256:0000000000000000000000000000000000000000000000000000000000000000";
24
25pub async fn handle(args: NewArgs, json: bool, runtime: &RuntimeContext) -> Result<()> {
26    let root = args.dir.canonicalize().unwrap_or_else(|_| args.dir.clone());
27    fs::create_dir_all(&root)?;
28
29    write_pack_yaml(&root, &args.pack_id)?;
30    write_flow(&root)?;
31    let resolved_digest = resolve_template_digest(runtime).await;
32    write_flow_sidecar(&root, &resolved_digest)?;
33    create_components_dir(&root)?;
34
35    if json {
36        println!(
37            "{}",
38            serde_json::to_string_pretty(&serde_json::json!({
39                "status": "ok",
40                "pack_dir": root,
41            }))?
42        );
43    } else {
44        println!("created pack at {}", root.display());
45    }
46
47    Ok(())
48}
49
50fn write_pack_yaml(root: &Path, pack_id: &str) -> Result<()> {
51    let pack_yaml = format!(
52        r#"pack_id: {pack_id}
53version: 0.1.0
54kind: application
55publisher: Greentic
56
57components: []
58
59flows:
60  - id: main
61    file: flows/main.ygtc
62    tags: [default]
63    entrypoints: [default]
64
65dependencies: []
66
67assets: []
68
69extensions:
70  greentic.components:
71    kind: greentic.components
72    version: v1
73    inline:
74      refs:
75        - "oci://ghcr.io/greentic-ai/components/templates:latest"
76      mode: eager
77      allow_tags: true
78"#
79    );
80    let path = root.join("pack.yaml");
81    fs::write(&path, pack_yaml).with_context(|| format!("failed to write {}", path.display()))
82}
83
84fn write_flow(root: &Path) -> Result<()> {
85    let flows_dir = root.join("flows");
86    fs::create_dir_all(&flows_dir)?;
87    let flow_path = flows_dir.join("main.ygtc");
88    const FLOW: &str = r#"id: main
89title: Welcome
90description: Minimal starter flow
91type: messaging
92start: start
93
94nodes:
95  start:
96    templating.handlebars:
97      text: "Hello from greentic-pack starter!"
98    routing: out
99"#;
100    fs::write(&flow_path, FLOW).with_context(|| format!("failed to write {}", flow_path.display()))
101}
102
103fn write_flow_sidecar(root: &Path, digest: &str) -> Result<()> {
104    let sidecar_path = root.join("flows/main.ygtc.resolve.json");
105    let sidecar = serde_json::json!({
106        "schema_version": 1,
107        "flow": "flows/main.ygtc",
108        "nodes": {
109            "start": {
110                "source": {
111                    "kind": "oci",
112                    "ref": TEMPLATE_REF,
113                    "digest": digest,
114                }
115            }
116        }
117    });
118    let rendered = serde_json::to_string_pretty(&sidecar)?;
119    fs::write(&sidecar_path, rendered)
120        .with_context(|| format!("failed to write {}", sidecar_path.display()))
121}
122
123fn create_components_dir(root: &Path) -> Result<()> {
124    let components_dir = root.join("components");
125    fs::create_dir_all(&components_dir)
126        .with_context(|| format!("failed to create {}", components_dir.display()))
127}
128
129async fn resolve_template_digest(runtime: &RuntimeContext) -> String {
130    if runtime.network_policy() == NetworkPolicy::Offline {
131        eprintln!(
132            "warning: offline mode prevents resolving {}; using placeholder digest (run `greentic-pack resolve` when online)",
133            TEMPLATE_REF
134        );
135        return PLACEHOLDER_DIGEST.to_string();
136    }
137    let opts = DistOptions {
138        cache_dir: runtime.cache_dir(),
139        allow_tags: true,
140        offline: runtime.network_policy() == NetworkPolicy::Offline,
141    };
142    let client = DistClient::new(opts);
143    match client.resolve_ref(TEMPLATE_REF).await {
144        Ok(resolved) => resolved.digest,
145        Err(err) => {
146            eprintln!(
147                "warning: failed to resolve {}: {}; using placeholder digest",
148                TEMPLATE_REF, err
149            );
150            PLACEHOLDER_DIGEST.to_string()
151        }
152    }
153}