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 match crate::tenant_config::sync_oauth_to_tenant_config(
201 bundle_path,
202 &config.tenant,
203 provider_id,
204 answers,
205 ) {
206 Ok(true) => {
207 if config.verbose {
208 println!(" [oauth] updated tenant config for {provider_id}");
209 }
210 }
211 Ok(false) => {}
212 Err(e) => {
213 println!(" [oauth] WARNING: failed to update tenant config: {e}");
214 }
215 }
216
217 if let Some(result) = crate::webhook::register_webhook(
219 provider_id,
220 answers,
221 &config.tenant,
222 config.team.as_deref(),
223 ) {
224 let ok = result.get("ok").and_then(Value::as_bool).unwrap_or(false);
225 if ok {
226 println!(" [webhook] registered for {provider_id}");
227 } else {
228 let err = result
229 .get("error")
230 .and_then(Value::as_str)
231 .unwrap_or("unknown");
232 println!(" [webhook] WARNING: registration failed for {provider_id}: {err}");
233 }
234 }
235
236 count += 1;
237 }
238
239 crate::platform_setup::persist_static_routes_artifact(bundle_path, &metadata.static_routes)?;
240 let _ = crate::deployment_targets::persist_explicit_deployment_targets(
241 bundle_path,
242 &metadata.deployment_targets,
243 );
244
245 let provider_configs: Vec<(String, Value)> = metadata
247 .setup_answers
248 .iter()
249 .map(|(id, val)| (id.clone(), val.clone()))
250 .collect();
251 let team = config.team.as_deref().unwrap_or("default");
252 crate::webhook::print_post_setup_instructions(&provider_configs, &config.tenant, team);
253
254 Ok(count)
255}
256
257pub fn execute_remove_provider_artifacts(
259 bundle_path: &Path,
260 providers_remove: &[String],
261) -> anyhow::Result<usize> {
262 let mut removed = 0usize;
263 let discovered = discovery::discover(bundle_path).ok();
264 for provider_id in providers_remove {
265 if let Some(discovered) = discovered.as_ref()
266 && let Some(provider) = discovered
267 .providers
268 .iter()
269 .find(|provider| provider.provider_id == *provider_id)
270 {
271 if provider.pack_path.exists() {
272 std::fs::remove_file(&provider.pack_path).with_context(|| {
273 format!(
274 "failed to remove provider pack {}",
275 provider.pack_path.display()
276 )
277 })?;
278 }
279 removed += 1;
280 } else {
281 let target_dir = get_pack_target_dir(bundle_path, provider_id);
282 let target_path = target_dir.join(format!("{provider_id}.gtpack"));
283 if target_path.exists() {
284 std::fs::remove_file(&target_path).with_context(|| {
285 format!("failed to remove provider pack {}", target_path.display())
286 })?;
287 removed += 1;
288 }
289 }
290
291 let config_dir = bundle_path.join("state").join("config").join(provider_id);
292 if config_dir.exists() {
293 std::fs::remove_dir_all(&config_dir).with_context(|| {
294 format!(
295 "failed to remove provider config dir {}",
296 config_dir.display()
297 )
298 })?;
299 }
300 }
301 Ok(removed)
302}
303
304pub fn auto_install_provider_packs(bundle_path: &Path, metadata: &SetupPlanMetadata) {
307 let bundle_abs =
308 std::fs::canonicalize(bundle_path).unwrap_or_else(|_| bundle_path.to_path_buf());
309
310 for provider_id in metadata.setup_answers.keys() {
311 let target_dir = get_pack_target_dir(bundle_path, provider_id);
312 let target_path = target_dir.join(format!("{provider_id}.gtpack"));
313 if target_path.exists() {
314 continue;
315 }
316
317 let domain = domain_from_provider_id(provider_id);
319
320 if let Some(source) = find_provider_pack_source(provider_id, domain, &bundle_abs) {
322 if let Err(err) = std::fs::create_dir_all(&target_dir) {
323 eprintln!(
324 " [provider] WARNING: failed to create {}: {err}",
325 target_dir.display()
326 );
327 continue;
328 }
329 match std::fs::copy(&source, &target_path) {
330 Ok(_) => println!(
331 " [provider] installed {provider_id}.gtpack from {}",
332 source.display()
333 ),
334 Err(err) => eprintln!(
335 " [provider] WARNING: failed to copy {}: {err}",
336 source.display()
337 ),
338 }
339 } else {
340 eprintln!(" [provider] WARNING: {provider_id}.gtpack not found in sibling bundles");
341 }
342 }
343}
344
345pub fn domain_from_provider_id(provider_id: &str) -> &str {
347 const DOMAIN_PREFIXES: &[&str] = &[
348 "messaging-",
349 "events-",
350 "oauth-",
351 "secrets-",
352 "mcp-",
353 "state-",
354 "telemetry-",
355 ];
356 for prefix in DOMAIN_PREFIXES {
357 if provider_id.starts_with(prefix) {
358 return prefix.trim_end_matches('-');
359 }
360 }
361 "messaging" }
363
364pub fn find_provider_pack_source(
370 provider_id: &str,
371 domain: &str,
372 bundle_abs: &Path,
373) -> Option<PathBuf> {
374 let parent = bundle_abs.parent()?;
375 let filename = format!("{provider_id}.gtpack");
376
377 if let Ok(entries) = std::fs::read_dir(parent) {
379 for entry in entries.flatten() {
380 let sibling = entry.path();
381 if sibling == *bundle_abs || !sibling.is_dir() {
382 continue;
383 }
384 let candidate = sibling.join("providers").join(domain).join(&filename);
385 if candidate.is_file() {
386 return Some(candidate);
387 }
388 }
389 }
390
391 for ancestor in parent.ancestors().take(4) {
393 let candidate = ancestor
394 .join("greentic-messaging-providers")
395 .join("target")
396 .join("packs")
397 .join(&filename);
398 if candidate.is_file() {
399 return Some(candidate);
400 }
401 }
402
403 None
404}
405
406pub fn execute_write_gmap_rules(
408 bundle_path: &Path,
409 metadata: &SetupPlanMetadata,
410) -> anyhow::Result<()> {
411 for tenant_sel in &metadata.tenants {
412 let gmap_path =
413 bundle::gmap_path(bundle_path, &tenant_sel.tenant, tenant_sel.team.as_deref());
414
415 if let Some(parent) = gmap_path.parent() {
416 std::fs::create_dir_all(parent)?;
417 }
418
419 let mut content = String::new();
421 if tenant_sel.allow_paths.is_empty() {
422 content.push_str("_ = forbidden\n");
423 } else {
424 for path in &tenant_sel.allow_paths {
425 content.push_str(&format!("{} = allowed\n", path));
426 }
427 content.push_str("_ = forbidden\n");
428 }
429
430 std::fs::write(&gmap_path, content)
431 .with_context(|| format!("failed to write gmap: {}", gmap_path.display()))?;
432 }
433 Ok(())
434}
435
436pub fn execute_copy_resolved_manifests(
438 bundle_path: &Path,
439 metadata: &SetupPlanMetadata,
440) -> anyhow::Result<Vec<PathBuf>> {
441 let mut manifests = Vec::new();
442 let resolved_dir = bundle_path.join("resolved");
443 std::fs::create_dir_all(&resolved_dir)?;
444
445 for tenant_sel in &metadata.tenants {
446 let filename =
447 bundle::resolved_manifest_filename(&tenant_sel.tenant, tenant_sel.team.as_deref());
448 let manifest_path = resolved_dir.join(&filename);
449
450 if !manifest_path.exists() {
452 std::fs::write(&manifest_path, "# Resolved manifest placeholder\n")?;
453 }
454 manifests.push(manifest_path);
455 }
456
457 Ok(manifests)
458}
459
460pub fn execute_validate_bundle(bundle_path: &Path) -> anyhow::Result<()> {
462 bundle::validate_bundle_exists(bundle_path)
463}
464
465pub fn execute_build_flow_index(_bundle_path: &Path, _config: &SetupConfig) -> anyhow::Result<()> {
475 tracing::debug!("fast2flow indexing skipped (fast2flow-bundle not available)");
476 Ok(())
477}