1#![forbid(unsafe_code)]
2
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result, anyhow};
7use clap::Args;
8use greentic_pack::pack_lock::{LockedComponent, PackLockV1, write_pack_lock};
9use greentic_types::ComponentId;
10use greentic_types::flow_resolve_summary::{FlowResolveSummarySourceRefV1, FlowResolveSummaryV1};
11
12use crate::config::load_pack_config;
13use crate::flow_resolve::{read_flow_resolve_summary_for_flow, strip_file_uri_prefix};
14use crate::runtime::RuntimeContext;
15
16#[derive(Debug, Args)]
17pub struct ResolveArgs {
18 #[arg(long = "in", value_name = "DIR", default_value = ".")]
20 pub input: PathBuf,
21
22 #[arg(long = "lock", value_name = "FILE")]
24 pub lock: Option<PathBuf>,
25}
26
27pub async fn handle(args: ResolveArgs, runtime: &RuntimeContext, emit_path: bool) -> Result<()> {
28 let pack_dir = args
29 .input
30 .canonicalize()
31 .with_context(|| format!("failed to resolve pack dir {}", args.input.display()))?;
32 let lock_path = resolve_lock_path(&pack_dir, args.lock.as_deref());
33
34 let config = load_pack_config(&pack_dir)?;
35 let mut entries: Vec<LockedComponent> = Vec::new();
36 let _ = runtime;
37 for flow in &config.flows {
38 let summary = read_flow_resolve_summary_for_flow(&pack_dir, flow)?;
39 collect_from_summary(&pack_dir, flow, &summary, &mut entries)?;
40 }
41
42 let lock = PackLockV1::new(entries);
43 write_pack_lock(&lock_path, &lock)?;
44 if emit_path {
45 eprintln!("wrote {}", lock_path.display());
46 }
47
48 Ok(())
49}
50
51fn resolve_lock_path(pack_dir: &Path, override_path: Option<&Path>) -> PathBuf {
52 match override_path {
53 Some(path) if path.is_absolute() => path.to_path_buf(),
54 Some(path) => pack_dir.join(path),
55 None => pack_dir.join("pack.lock.json"),
56 }
57}
58
59fn collect_from_summary(
60 pack_dir: &Path,
61 flow: &crate::config::FlowConfig,
62 doc: &FlowResolveSummaryV1,
63 out: &mut Vec<LockedComponent>,
64) -> Result<()> {
65 let mut seen: BTreeMap<String, (String, String, ComponentId)> = BTreeMap::new();
66
67 for (node, resolve) in &doc.nodes {
68 let name = format!("{}___{node}", flow.id);
69 let source_ref = &resolve.source;
70 let (reference, digest) = match source_ref {
71 FlowResolveSummarySourceRefV1::Local { path } => {
72 let abs = normalize_local(pack_dir, flow, path)?;
73 (
74 format!("file://{}", abs.to_string_lossy()),
75 resolve.digest.clone(),
76 )
77 }
78 FlowResolveSummarySourceRefV1::Oci { .. }
79 | FlowResolveSummarySourceRefV1::Repo { .. }
80 | FlowResolveSummarySourceRefV1::Store { .. } => {
81 (format_reference(source_ref), resolve.digest.clone())
82 }
83 };
84 seen.insert(name, (reference, digest, resolve.component_id.clone()));
85 }
86
87 for (name, (reference, digest, component_id)) in seen {
88 out.push(LockedComponent {
89 name,
90 r#ref: reference,
91 digest,
92 component_id: Some(component_id),
93 bundled: false,
94 bundled_path: None,
95 wasm_sha256: None,
96 resolved_digest: None,
97 });
98 }
99
100 Ok(())
101}
102
103fn normalize_local(
104 pack_dir: &Path,
105 flow: &crate::config::FlowConfig,
106 rel: &str,
107) -> Result<PathBuf> {
108 let flow_path = if flow.file.is_absolute() {
109 flow.file.clone()
110 } else {
111 pack_dir.join(&flow.file)
112 };
113 let parent = flow_path
114 .parent()
115 .ok_or_else(|| anyhow!("flow path {} has no parent", flow_path.display()))?;
116 let rel = strip_file_uri_prefix(rel);
117 Ok(parent.join(rel))
118}
119
120fn format_reference(source: &FlowResolveSummarySourceRefV1) -> String {
121 match source {
122 FlowResolveSummarySourceRefV1::Local { path } => path.clone(),
123 FlowResolveSummarySourceRefV1::Oci { r#ref } => {
124 if r#ref.contains("://") {
125 r#ref.clone()
126 } else {
127 format!("oci://{}", r#ref)
128 }
129 }
130 FlowResolveSummarySourceRefV1::Repo { r#ref } => {
131 if r#ref.contains("://") {
132 r#ref.clone()
133 } else {
134 format!("repo://{}", r#ref)
135 }
136 }
137 FlowResolveSummarySourceRefV1::Store { r#ref } => {
138 if r#ref.contains("://") {
139 r#ref.clone()
140 } else {
141 format!("store://{}", r#ref)
142 }
143 }
144 }
145}