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 if config.verbose {
182 let team_display = config.team.as_deref().unwrap_or("(none)");
183 println!(
184 " [secrets] scope: env={env}, tenant={}, team={team_display}, provider={provider_id}",
185 config.tenant
186 );
187 let example_uri = crate::canonical_secret_uri(
188 &env,
189 &config.tenant,
190 config.team.as_deref(),
191 provider_id,
192 "_example_key",
193 );
194 println!(" [secrets] URI pattern: {example_uri}");
195 if let Some(config_map) = answers.as_object() {
196 let keys: Vec<&String> = config_map.keys().collect();
197 println!(" [secrets] answer keys: {keys:?}");
198 }
199 }
200 let rt = tokio::runtime::Runtime::new()
201 .context("failed to create tokio runtime for secrets persistence")?;
202 let persisted = rt.block_on(crate::qa::persist::persist_all_config_as_secrets(
203 bundle_path,
204 &env,
205 &config.tenant,
206 config.team.as_deref(),
207 provider_id,
208 answers,
209 pack_path,
210 ))?;
211 if config.verbose {
212 if persisted.is_empty() {
213 println!(
214 " [secrets] WARNING: 0 key(s) persisted for {provider_id} (all values empty?)"
215 );
216 } else {
217 println!(
218 " [secrets] persisted {} key(s) for {provider_id}: {:?}",
219 persisted.len(),
220 persisted
221 );
222 }
223 }
224
225 match crate::tenant_config::sync_oauth_to_tenant_config(
227 bundle_path,
228 &config.tenant,
229 provider_id,
230 answers,
231 ) {
232 Ok(true) => {
233 if config.verbose {
234 println!(" [oauth] updated tenant config for {provider_id}");
235 }
236 }
237 Ok(false) => {}
238 Err(e) => {
239 println!(" [oauth] WARNING: failed to update tenant config: {e}");
240 }
241 }
242
243 if let Some(result) = crate::webhook::register_webhook(
245 provider_id,
246 answers,
247 &config.tenant,
248 config.team.as_deref(),
249 ) {
250 let ok = result.get("ok").and_then(Value::as_bool).unwrap_or(false);
251 if ok {
252 println!(" [webhook] registered for {provider_id}");
253 } else {
254 let err = result
255 .get("error")
256 .and_then(Value::as_str)
257 .unwrap_or("unknown");
258 println!(" [webhook] WARNING: registration failed for {provider_id}: {err}");
259 }
260 }
261
262 count += 1;
263 }
264
265 crate::platform_setup::persist_static_routes_artifact(bundle_path, &metadata.static_routes)?;
266 let _ = crate::deployment_targets::persist_explicit_deployment_targets(
267 bundle_path,
268 &metadata.deployment_targets,
269 );
270
271 let provider_configs: Vec<(String, Value)> = metadata
273 .setup_answers
274 .iter()
275 .map(|(id, val)| (id.clone(), val.clone()))
276 .collect();
277 let team = config.team.as_deref().unwrap_or("default");
278 crate::webhook::print_post_setup_instructions(&provider_configs, &config.tenant, team);
279
280 Ok(count)
281}
282
283pub fn execute_remove_provider_artifacts(
285 bundle_path: &Path,
286 providers_remove: &[String],
287) -> anyhow::Result<usize> {
288 let mut removed = 0usize;
289 let discovered = discovery::discover(bundle_path).ok();
290 for provider_id in providers_remove {
291 if let Some(discovered) = discovered.as_ref()
292 && let Some(provider) = discovered
293 .providers
294 .iter()
295 .find(|provider| provider.provider_id == *provider_id)
296 {
297 if provider.pack_path.exists() {
298 std::fs::remove_file(&provider.pack_path).with_context(|| {
299 format!(
300 "failed to remove provider pack {}",
301 provider.pack_path.display()
302 )
303 })?;
304 }
305 removed += 1;
306 } else {
307 let target_dir = get_pack_target_dir(bundle_path, provider_id);
308 let target_path = target_dir.join(format!("{provider_id}.gtpack"));
309 if target_path.exists() {
310 std::fs::remove_file(&target_path).with_context(|| {
311 format!("failed to remove provider pack {}", target_path.display())
312 })?;
313 removed += 1;
314 }
315 }
316
317 let config_dir = bundle_path.join("state").join("config").join(provider_id);
318 if config_dir.exists() {
319 std::fs::remove_dir_all(&config_dir).with_context(|| {
320 format!(
321 "failed to remove provider config dir {}",
322 config_dir.display()
323 )
324 })?;
325 }
326 }
327 Ok(removed)
328}
329
330pub fn auto_install_provider_packs(bundle_path: &Path, metadata: &SetupPlanMetadata) {
333 let bundle_abs =
334 std::fs::canonicalize(bundle_path).unwrap_or_else(|_| bundle_path.to_path_buf());
335
336 for provider_id in metadata.setup_answers.keys() {
337 let target_dir = get_pack_target_dir(bundle_path, provider_id);
338 let target_path = target_dir.join(format!("{provider_id}.gtpack"));
339 if target_path.exists() {
340 continue;
341 }
342
343 let domain = domain_from_provider_id(provider_id);
345
346 if let Some(source) = find_provider_pack_source(provider_id, domain, &bundle_abs) {
348 if let Err(err) = std::fs::create_dir_all(&target_dir) {
349 eprintln!(
350 " [provider] WARNING: failed to create {}: {err}",
351 target_dir.display()
352 );
353 continue;
354 }
355 match std::fs::copy(&source, &target_path) {
356 Ok(_) => println!(
357 " [provider] installed {provider_id}.gtpack from {}",
358 source.display()
359 ),
360 Err(err) => eprintln!(
361 " [provider] WARNING: failed to copy {}: {err}",
362 source.display()
363 ),
364 }
365 } else {
366 eprintln!(" [provider] WARNING: {provider_id}.gtpack not found in sibling bundles");
367 }
368 }
369}
370
371pub fn domain_from_provider_id(provider_id: &str) -> &str {
373 const DOMAIN_PREFIXES: &[&str] = &[
374 "messaging-",
375 "events-",
376 "oauth-",
377 "secrets-",
378 "mcp-",
379 "state-",
380 "telemetry-",
381 ];
382 for prefix in DOMAIN_PREFIXES {
383 if provider_id.starts_with(prefix) {
384 return prefix.trim_end_matches('-');
385 }
386 }
387 "messaging" }
389
390pub fn find_provider_pack_source(
396 provider_id: &str,
397 domain: &str,
398 bundle_abs: &Path,
399) -> Option<PathBuf> {
400 let parent = bundle_abs.parent()?;
401 let filename = format!("{provider_id}.gtpack");
402
403 if let Ok(entries) = std::fs::read_dir(parent) {
405 for entry in entries.flatten() {
406 let sibling = entry.path();
407 if sibling == *bundle_abs || !sibling.is_dir() {
408 continue;
409 }
410 let candidate = sibling.join("providers").join(domain).join(&filename);
411 if candidate.is_file() {
412 return Some(candidate);
413 }
414 }
415 }
416
417 for ancestor in parent.ancestors().take(4) {
419 let candidate = ancestor
420 .join("greentic-messaging-providers")
421 .join("target")
422 .join("packs")
423 .join(&filename);
424 if candidate.is_file() {
425 return Some(candidate);
426 }
427 }
428
429 None
430}
431
432pub fn execute_write_gmap_rules(
434 bundle_path: &Path,
435 metadata: &SetupPlanMetadata,
436) -> anyhow::Result<()> {
437 for tenant_sel in &metadata.tenants {
438 let gmap_path =
439 bundle::gmap_path(bundle_path, &tenant_sel.tenant, tenant_sel.team.as_deref());
440
441 if let Some(parent) = gmap_path.parent() {
442 std::fs::create_dir_all(parent)?;
443 }
444
445 let mut content = String::new();
447 if tenant_sel.allow_paths.is_empty() {
448 content.push_str("_ = forbidden\n");
449 } else {
450 for path in &tenant_sel.allow_paths {
451 content.push_str(&format!("{} = allowed\n", path));
452 }
453 content.push_str("_ = forbidden\n");
454 }
455
456 std::fs::write(&gmap_path, content)
457 .with_context(|| format!("failed to write gmap: {}", gmap_path.display()))?;
458 }
459 Ok(())
460}
461
462pub fn execute_copy_resolved_manifests(
464 bundle_path: &Path,
465 metadata: &SetupPlanMetadata,
466) -> anyhow::Result<Vec<PathBuf>> {
467 let mut manifests = Vec::new();
468 let resolved_dir = bundle_path.join("resolved");
469 std::fs::create_dir_all(&resolved_dir)?;
470
471 for tenant_sel in &metadata.tenants {
472 let filename =
473 bundle::resolved_manifest_filename(&tenant_sel.tenant, tenant_sel.team.as_deref());
474 let manifest_path = resolved_dir.join(&filename);
475
476 if !manifest_path.exists() {
478 std::fs::write(&manifest_path, "# Resolved manifest placeholder\n")?;
479 }
480 manifests.push(manifest_path);
481 }
482
483 Ok(manifests)
484}
485
486pub fn execute_validate_bundle(bundle_path: &Path) -> anyhow::Result<()> {
488 bundle::validate_bundle_exists(bundle_path)
489}
490
491pub fn execute_build_flow_index(_bundle_path: &Path, _config: &SetupConfig) -> anyhow::Result<()> {
501 tracing::debug!("fast2flow indexing skipped (fast2flow-bundle not available)");
502 Ok(())
503}