1use std::path::{Path, PathBuf};
6
7use anyhow::Context;
8use serde_json::Value;
9
10use crate::plan::{ResolvedPackInfo, SetupPlanMetadata};
11use crate::{bundle, discovery};
12
13use super::plan_builders::compute_simple_hash;
14use super::types::SetupConfig;
15
16pub fn execute_create_bundle(
18 bundle_path: &Path,
19 metadata: &SetupPlanMetadata,
20) -> anyhow::Result<()> {
21 bundle::create_demo_bundle_structure(bundle_path, metadata.bundle_name.as_deref())
22 .context("failed to create bundle structure")
23}
24
25pub fn execute_resolve_packs(
27 _bundle_path: &Path,
28 metadata: &SetupPlanMetadata,
29) -> anyhow::Result<Vec<ResolvedPackInfo>> {
30 let mut resolved = Vec::new();
31
32 for pack_ref in &metadata.pack_refs {
33 let path = PathBuf::from(pack_ref);
36
37 let resolved_path = if path.is_absolute() {
39 path.clone()
40 } else {
41 std::env::current_dir()
42 .ok()
43 .map(|cwd| cwd.join(&path))
44 .unwrap_or_else(|| path.clone())
45 };
46
47 if resolved_path.exists() {
48 let canonical = resolved_path
49 .canonicalize()
50 .unwrap_or(resolved_path.clone());
51 resolved.push(ResolvedPackInfo {
52 source_ref: pack_ref.clone(),
53 mapped_ref: canonical.display().to_string(),
54 resolved_digest: format!("sha256:{}", compute_simple_hash(pack_ref)),
55 pack_id: canonical
56 .file_stem()
57 .and_then(|s| s.to_str())
58 .unwrap_or("unknown")
59 .to_string(),
60 entry_flows: Vec::new(),
61 cached_path: canonical.clone(),
62 output_path: canonical,
63 });
64 } else if pack_ref.starts_with("oci://")
65 || pack_ref.starts_with("repo://")
66 || pack_ref.starts_with("store://")
67 {
68 tracing::warn!("remote pack ref requires async resolution: {}", pack_ref);
71 } else {
72 tracing::warn!(
74 "pack ref not found: {} (resolved to: {})",
75 pack_ref,
76 resolved_path.display()
77 );
78 }
79 }
80
81 Ok(resolved)
82}
83
84pub fn execute_add_packs_to_bundle(
86 bundle_path: &Path,
87 resolved_packs: &[ResolvedPackInfo],
88) -> anyhow::Result<()> {
89 for pack in resolved_packs {
90 let target_dir = get_pack_target_dir(bundle_path, &pack.pack_id);
92 std::fs::create_dir_all(&target_dir)?;
93
94 let target_path = target_dir.join(format!("{}.gtpack", pack.pack_id));
95 if pack.cached_path.exists() && !target_path.exists() {
96 std::fs::copy(&pack.cached_path, &target_path).with_context(|| {
97 format!(
98 "failed to copy pack {} to {}",
99 pack.cached_path.display(),
100 target_path.display()
101 )
102 })?;
103 }
104 }
105 Ok(())
106}
107
108pub fn get_pack_target_dir(bundle_path: &Path, pack_id: &str) -> PathBuf {
113 const DOMAIN_PREFIXES: &[&str] = &[
114 "messaging-",
115 "events-",
116 "oauth-",
117 "secrets-",
118 "mcp-",
119 "state-",
120 ];
121
122 for prefix in DOMAIN_PREFIXES {
123 if pack_id.starts_with(prefix) {
124 let domain = prefix.trim_end_matches('-');
125 return bundle_path.join("providers").join(domain);
126 }
127 }
128
129 bundle_path.join("packs")
131}
132
133pub fn execute_apply_pack_setup(
135 bundle_path: &Path,
136 metadata: &SetupPlanMetadata,
137 config: &SetupConfig,
138) -> anyhow::Result<usize> {
139 let mut count = 0;
140
141 if !metadata.providers_remove.is_empty() {
142 count += execute_remove_provider_artifacts(bundle_path, &metadata.providers_remove)?;
143 }
144
145 auto_install_provider_packs(bundle_path, metadata);
148
149 let discovered = if bundle_path.exists() {
151 discovery::discover(bundle_path).ok()
152 } else {
153 None
154 };
155
156 for (provider_id, answers) in &metadata.setup_answers {
158 let config_dir = bundle_path.join("state").join("config").join(provider_id);
160 std::fs::create_dir_all(&config_dir)?;
161
162 let config_path = config_dir.join("setup-answers.json");
163 let content =
164 serde_json::to_string_pretty(answers).context("failed to serialize setup answers")?;
165 std::fs::write(&config_path, content).with_context(|| {
166 format!(
167 "failed to write setup answers to: {}",
168 config_path.display()
169 )
170 })?;
171
172 let pack_path = discovered.as_ref().and_then(|d| {
175 d.providers
176 .iter()
177 .find(|p| p.provider_id == *provider_id)
178 .map(|p| p.pack_path.as_path())
179 });
180 let env = crate::resolve_env(Some(&config.env));
181 let rt = tokio::runtime::Runtime::new()
182 .context("failed to create tokio runtime for secrets persistence")?;
183 let persisted = rt.block_on(crate::qa::persist::persist_all_config_as_secrets(
184 bundle_path,
185 &env,
186 &config.tenant,
187 config.team.as_deref(),
188 provider_id,
189 answers,
190 pack_path,
191 ))?;
192 if config.verbose && !persisted.is_empty() {
193 println!(
194 " [secrets] persisted {} key(s) for {provider_id}",
195 persisted.len()
196 );
197 }
198
199 if let Some(result) = crate::webhook::register_webhook(
201 provider_id,
202 answers,
203 &config.tenant,
204 config.team.as_deref(),
205 ) {
206 let ok = result.get("ok").and_then(Value::as_bool).unwrap_or(false);
207 if ok {
208 println!(" [webhook] registered for {provider_id}");
209 } else {
210 let err = result
211 .get("error")
212 .and_then(Value::as_str)
213 .unwrap_or("unknown");
214 println!(" [webhook] WARNING: registration failed for {provider_id}: {err}");
215 }
216 }
217
218 count += 1;
219 }
220
221 crate::platform_setup::persist_static_routes_artifact(bundle_path, &metadata.static_routes)?;
222 let _ = crate::deployment_targets::persist_explicit_deployment_targets(
223 bundle_path,
224 &metadata.deployment_targets,
225 );
226
227 let provider_configs: Vec<(String, Value)> = metadata
229 .setup_answers
230 .iter()
231 .map(|(id, val)| (id.clone(), val.clone()))
232 .collect();
233 let team = config.team.as_deref().unwrap_or("default");
234 crate::webhook::print_post_setup_instructions(&provider_configs, &config.tenant, team);
235
236 Ok(count)
237}
238
239pub fn execute_remove_provider_artifacts(
241 bundle_path: &Path,
242 providers_remove: &[String],
243) -> anyhow::Result<usize> {
244 let mut removed = 0usize;
245 let discovered = discovery::discover(bundle_path).ok();
246 for provider_id in providers_remove {
247 if let Some(discovered) = discovered.as_ref()
248 && let Some(provider) = discovered
249 .providers
250 .iter()
251 .find(|provider| provider.provider_id == *provider_id)
252 {
253 if provider.pack_path.exists() {
254 std::fs::remove_file(&provider.pack_path).with_context(|| {
255 format!(
256 "failed to remove provider pack {}",
257 provider.pack_path.display()
258 )
259 })?;
260 }
261 removed += 1;
262 } else {
263 let target_dir = get_pack_target_dir(bundle_path, provider_id);
264 let target_path = target_dir.join(format!("{provider_id}.gtpack"));
265 if target_path.exists() {
266 std::fs::remove_file(&target_path).with_context(|| {
267 format!("failed to remove provider pack {}", target_path.display())
268 })?;
269 removed += 1;
270 }
271 }
272
273 let config_dir = bundle_path.join("state").join("config").join(provider_id);
274 if config_dir.exists() {
275 std::fs::remove_dir_all(&config_dir).with_context(|| {
276 format!(
277 "failed to remove provider config dir {}",
278 config_dir.display()
279 )
280 })?;
281 }
282 }
283 Ok(removed)
284}
285
286pub fn auto_install_provider_packs(bundle_path: &Path, metadata: &SetupPlanMetadata) {
289 let bundle_abs =
290 std::fs::canonicalize(bundle_path).unwrap_or_else(|_| bundle_path.to_path_buf());
291
292 for provider_id in metadata.setup_answers.keys() {
293 let target_dir = get_pack_target_dir(bundle_path, provider_id);
294 let target_path = target_dir.join(format!("{provider_id}.gtpack"));
295 if target_path.exists() {
296 continue;
297 }
298
299 let domain = domain_from_provider_id(provider_id);
301
302 if let Some(source) = find_provider_pack_source(provider_id, domain, &bundle_abs) {
304 if let Err(err) = std::fs::create_dir_all(&target_dir) {
305 eprintln!(
306 " [provider] WARNING: failed to create {}: {err}",
307 target_dir.display()
308 );
309 continue;
310 }
311 match std::fs::copy(&source, &target_path) {
312 Ok(_) => println!(
313 " [provider] installed {provider_id}.gtpack from {}",
314 source.display()
315 ),
316 Err(err) => eprintln!(
317 " [provider] WARNING: failed to copy {}: {err}",
318 source.display()
319 ),
320 }
321 } else {
322 eprintln!(" [provider] WARNING: {provider_id}.gtpack not found in sibling bundles");
323 }
324 }
325}
326
327pub fn domain_from_provider_id(provider_id: &str) -> &str {
329 const DOMAIN_PREFIXES: &[&str] = &[
330 "messaging-",
331 "events-",
332 "oauth-",
333 "secrets-",
334 "mcp-",
335 "state-",
336 "telemetry-",
337 ];
338 for prefix in DOMAIN_PREFIXES {
339 if provider_id.starts_with(prefix) {
340 return prefix.trim_end_matches('-');
341 }
342 }
343 "messaging" }
345
346pub fn find_provider_pack_source(
352 provider_id: &str,
353 domain: &str,
354 bundle_abs: &Path,
355) -> Option<PathBuf> {
356 let parent = bundle_abs.parent()?;
357 let filename = format!("{provider_id}.gtpack");
358
359 if let Ok(entries) = std::fs::read_dir(parent) {
361 for entry in entries.flatten() {
362 let sibling = entry.path();
363 if sibling == *bundle_abs || !sibling.is_dir() {
364 continue;
365 }
366 let candidate = sibling.join("providers").join(domain).join(&filename);
367 if candidate.is_file() {
368 return Some(candidate);
369 }
370 }
371 }
372
373 for ancestor in parent.ancestors().take(4) {
375 let candidate = ancestor
376 .join("greentic-messaging-providers")
377 .join("target")
378 .join("packs")
379 .join(&filename);
380 if candidate.is_file() {
381 return Some(candidate);
382 }
383 }
384
385 None
386}
387
388pub fn execute_write_gmap_rules(
390 bundle_path: &Path,
391 metadata: &SetupPlanMetadata,
392) -> anyhow::Result<()> {
393 for tenant_sel in &metadata.tenants {
394 let gmap_path =
395 bundle::gmap_path(bundle_path, &tenant_sel.tenant, tenant_sel.team.as_deref());
396
397 if let Some(parent) = gmap_path.parent() {
398 std::fs::create_dir_all(parent)?;
399 }
400
401 let mut content = String::new();
403 if tenant_sel.allow_paths.is_empty() {
404 content.push_str("_ = forbidden\n");
405 } else {
406 for path in &tenant_sel.allow_paths {
407 content.push_str(&format!("{} = allowed\n", path));
408 }
409 content.push_str("_ = forbidden\n");
410 }
411
412 std::fs::write(&gmap_path, content)
413 .with_context(|| format!("failed to write gmap: {}", gmap_path.display()))?;
414 }
415 Ok(())
416}
417
418pub fn execute_copy_resolved_manifests(
420 bundle_path: &Path,
421 metadata: &SetupPlanMetadata,
422) -> anyhow::Result<Vec<PathBuf>> {
423 let mut manifests = Vec::new();
424 let resolved_dir = bundle_path.join("resolved");
425 std::fs::create_dir_all(&resolved_dir)?;
426
427 for tenant_sel in &metadata.tenants {
428 let filename =
429 bundle::resolved_manifest_filename(&tenant_sel.tenant, tenant_sel.team.as_deref());
430 let manifest_path = resolved_dir.join(&filename);
431
432 if !manifest_path.exists() {
434 std::fs::write(&manifest_path, "# Resolved manifest placeholder\n")?;
435 }
436 manifests.push(manifest_path);
437 }
438
439 Ok(manifests)
440}
441
442pub fn execute_validate_bundle(bundle_path: &Path) -> anyhow::Result<()> {
444 bundle::validate_bundle_exists(bundle_path)
445}