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 #[arg(long = "dir", value_name = "DIR")]
15 pub dir: PathBuf,
16 #[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}