greentic_setup/cli_helpers/
mod.rs1mod bundle;
4mod env_vars;
5mod prompts;
6
7use std::path::Path;
8
9use anyhow::Result;
10
11use crate::discovery;
12use crate::engine::LoadedAnswers;
13use crate::platform_setup::{
14 PlatformSetupAnswers, StaticRoutesPolicy, load_effective_static_routes_defaults,
15 prompt_static_routes_policy, prompt_static_routes_policy_with_answers,
16};
17use crate::qa::wizard;
18use crate::setup_to_formspec;
19
20pub use bundle::{
22 SetupOutputTarget, copy_dir_recursive, detect_domain_from_filename, resolve_bundle_dir,
23 resolve_bundle_source, resolve_pack_source, setup_output_target,
24};
25pub use env_vars::{
26 EnvVarPlaceholder, apply_resolved_env_vars, collect_env_var_placeholders,
27 confirm_env_var_placeholders,
28};
29pub use prompts::{SetupParams, prompt_setup_params};
30
31pub fn resolve_setup_scope(
36 tenant: String,
37 team: Option<String>,
38 env: String,
39 loaded: &LoadedAnswers,
40) -> (String, Option<String>, String) {
41 let tenant = if tenant == "demo" {
42 loaded.tenant.clone().unwrap_or(tenant)
43 } else {
44 tenant
45 };
46 let team = if team.is_none() {
47 loaded.team.clone()
48 } else {
49 team
50 };
51 let env = if env == "dev" {
52 loaded.env.clone().unwrap_or(env)
53 } else {
54 env
55 };
56 (tenant, team, env)
57}
58
59pub fn run_interactive_wizard(
61 bundle_path: &Path,
62 tenant: &str,
63 team: Option<&str>,
64 env: &str,
65 advanced: bool,
66) -> Result<LoadedAnswers> {
67 use serde_json::Value;
68
69 let mut all_answers = serde_json::Map::new();
70 let existing_static_routes = load_effective_static_routes_defaults(bundle_path, tenant, team)?;
71 let static_routes = prompt_static_routes_policy(env, existing_static_routes.as_ref())?;
72 let deployment_targets = crate::deployment_targets::prompt_deployment_targets(
73 &crate::deployment_targets::discover_deployer_pack_candidates(bundle_path)?,
74 )?;
75
76 let discovered = discovery::discover(bundle_path)?;
77
78 if discovered.providers.is_empty() {
79 println!("No providers found in bundle. Nothing to configure.");
80 return Ok(LoadedAnswers {
81 tenant: None,
82 team: None,
83 env: None,
84 platform_setup: PlatformSetupAnswers {
85 static_routes: Some(static_routes.to_answers()),
86 deployment_targets,
87 },
88 setup_answers: all_answers,
89 });
90 }
91
92 println!(
93 "Found {} provider(s) to configure:",
94 discovered.providers.len()
95 );
96 for provider in &discovered.providers {
97 println!(" - {} ({})", provider.provider_id, provider.domain);
98 }
99 println!();
100
101 let provider_form_specs: Vec<wizard::ProviderFormSpec> = discovered
104 .providers
105 .iter()
106 .filter_map(|provider| {
107 setup_to_formspec::pack_to_form_spec(&provider.pack_path, &provider.provider_id).map(
108 |form_spec| wizard::ProviderFormSpec {
109 provider_id: provider.provider_id.clone(),
110 form_spec,
111 },
112 )
113 })
114 .collect();
115
116 let shared_answers = if provider_form_specs.len() > 1 {
119 let shared_result = wizard::collect_shared_questions(&provider_form_specs);
120 if !shared_result.shared_questions.is_empty() {
121 let empty = Value::Object(serde_json::Map::new());
122 wizard::prompt_shared_questions(&shared_result, advanced, &empty)?
123 } else {
124 Value::Object(serde_json::Map::new())
125 }
126 } else {
127 Value::Object(serde_json::Map::new())
128 };
129
130 for provider in &discovered.providers {
132 let provider_id = &provider.provider_id;
133 let form_spec = setup_to_formspec::pack_to_form_spec(&provider.pack_path, provider_id);
134
135 if let Some(spec) = form_spec {
136 if spec.questions.is_empty() {
137 println!("Provider {}: No configuration required.", provider_id);
138 all_answers.insert(provider_id.clone(), Value::Object(serde_json::Map::new()));
139 continue;
140 }
141
142 let answers = wizard::prompt_form_spec_answers_with_existing(
144 &spec,
145 provider_id,
146 advanced,
147 &shared_answers,
148 )?;
149 all_answers.insert(provider_id.clone(), answers);
150 } else {
151 println!(
152 "Provider {}: No setup questions found (may use flow-based setup).",
153 provider_id
154 );
155 all_answers.insert(provider_id.clone(), Value::Object(serde_json::Map::new()));
156 }
157
158 println!();
159 }
160
161 Ok(LoadedAnswers {
162 tenant: None,
163 team: None,
164 env: None,
165 platform_setup: PlatformSetupAnswers {
166 static_routes: Some(static_routes.to_answers()),
167 deployment_targets,
168 },
169 setup_answers: all_answers,
170 })
171}
172
173pub fn complete_loaded_answers_with_prompts(
175 bundle_path: &Path,
176 tenant: &str,
177 team: Option<&str>,
178 env: &str,
179 advanced: bool,
180 mut loaded: LoadedAnswers,
181) -> Result<LoadedAnswers> {
182 let existing_static_routes = load_effective_static_routes_defaults(bundle_path, tenant, team)?;
183 let static_routes_need_prompt = match loaded.platform_setup.static_routes.as_ref() {
184 None => true,
185 Some(answers) => StaticRoutesPolicy::normalize(Some(answers), env).is_err(),
186 };
187 if static_routes_need_prompt {
188 let static_routes =
189 if let Some(current_answers) = loaded.platform_setup.static_routes.as_ref() {
190 prompt_static_routes_policy_with_answers(
191 env,
192 Some(current_answers),
193 existing_static_routes.as_ref(),
194 )?
195 } else {
196 prompt_static_routes_policy(env, existing_static_routes.as_ref())?
197 };
198 loaded.platform_setup.static_routes = Some(static_routes.to_answers());
199 }
200 if loaded.platform_setup.deployment_targets.is_empty() {
201 loaded.platform_setup.deployment_targets =
202 crate::deployment_targets::prompt_deployment_targets(
203 &crate::deployment_targets::discover_deployer_pack_candidates(bundle_path)?,
204 )?;
205 }
206
207 let env_placeholders = collect_env_var_placeholders(&loaded);
209 if !env_placeholders.is_empty() {
210 let resolved_env_vars = confirm_env_var_placeholders(&env_placeholders)?;
211
212 if !resolved_env_vars.is_empty() {
214 apply_resolved_env_vars(&mut loaded, &resolved_env_vars);
215 }
216 }
217
218 let discovered = discovery::discover(bundle_path)?;
219
220 let all_provider_form_specs: Vec<wizard::ProviderFormSpec> = discovered
223 .providers
224 .iter()
225 .filter_map(|provider| {
226 setup_to_formspec::pack_to_form_spec(&provider.pack_path, &provider.provider_id).map(
227 |form_spec| wizard::ProviderFormSpec {
228 provider_id: provider.provider_id.clone(),
229 form_spec,
230 },
231 )
232 })
233 .collect();
234
235 let mut existing_shared_values = serde_json::Map::new();
238 let shared_result = if all_provider_form_specs.len() > 1 {
239 let result = wizard::collect_shared_questions(&all_provider_form_specs);
240 for question in &result.shared_questions {
242 for (_provider_id, provider_answers) in &loaded.setup_answers {
243 if let Some(value) = provider_answers.get(&question.id) {
244 if !(value.is_null() || value.is_string() && value.as_str() == Some("")) {
246 existing_shared_values.insert(question.id.clone(), value.clone());
247 break;
248 }
249 }
250 }
251 }
252 Some(result)
253 } else {
254 None
255 };
256
257 let shared_answers = if let Some(ref result) = shared_result {
260 if !result.shared_questions.is_empty() {
261 let existing = serde_json::Value::Object(existing_shared_values);
262 wizard::prompt_shared_questions(result, advanced, &existing)?
263 } else {
264 serde_json::Value::Object(serde_json::Map::new())
265 }
266 } else {
267 serde_json::Value::Object(serde_json::Map::new())
268 };
269
270 for provider in &discovered.providers {
272 let provider_id = &provider.provider_id;
273 let existing = loaded
274 .setup_answers
275 .get(provider_id)
276 .cloned()
277 .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
278
279 let mut merged = existing.as_object().cloned().unwrap_or_default();
282 if let Some(shared_obj) = shared_answers.as_object() {
283 for (key, value) in shared_obj {
284 let is_non_empty =
286 !(value.is_null() || value.is_string() && value.as_str() == Some(""));
287 if is_non_empty {
288 merged.insert(key.clone(), value.clone());
289 }
290 }
291 }
292 let merged_value = serde_json::Value::Object(merged);
293
294 let form_spec = setup_to_formspec::pack_to_form_spec(&provider.pack_path, provider_id);
295 let completed = if let Some(spec) = form_spec {
296 if spec.questions.is_empty() {
297 existing
298 } else {
299 wizard::prompt_form_spec_answers_with_existing(
300 &spec,
301 provider_id,
302 advanced,
303 &merged_value,
304 )?
305 }
306 } else {
307 existing
308 };
309 loaded.setup_answers.insert(provider_id.clone(), completed);
310 }
311
312 Ok(loaded)
313}
314
315pub fn ensure_deployment_targets_present(bundle_path: &Path, loaded: &LoadedAnswers) -> Result<()> {
317 if !loaded.platform_setup.deployment_targets.is_empty() {
318 return Ok(());
319 }
320 let candidates = crate::deployment_targets::discover_deployer_pack_candidates(bundle_path)?;
321 if candidates.is_empty() {
322 return Ok(());
323 }
324 anyhow::bail!(
325 "bundle contains deployer packs ({}) but answers did not define platform_setup.deployment_targets",
326 candidates
327 .iter()
328 .map(|value| value.display().to_string())
329 .collect::<Vec<_>>()
330 .join(", ")
331 )
332}
333
334#[cfg(test)]
335mod tests {
336 use super::resolve_setup_scope;
337 use crate::engine::LoadedAnswers;
338
339 #[test]
340 fn resolve_setup_scope_prefers_answers_when_cli_is_default() {
341 let loaded = LoadedAnswers {
342 tenant: Some("acme".to_string()),
343 team: Some("core".to_string()),
344 env: Some("prod".to_string()),
345 ..Default::default()
346 };
347 let resolved = resolve_setup_scope("demo".to_string(), None, "dev".to_string(), &loaded);
348 assert_eq!(resolved.0, "acme");
349 assert_eq!(resolved.1.as_deref(), Some("core"));
350 assert_eq!(resolved.2, "prod");
351 }
352
353 #[test]
354 fn resolve_setup_scope_keeps_explicit_cli_values() {
355 let loaded = LoadedAnswers {
356 tenant: Some("acme".to_string()),
357 team: Some("core".to_string()),
358 env: Some("prod".to_string()),
359 ..Default::default()
360 };
361 let resolved = resolve_setup_scope(
362 "sandbox".to_string(),
363 Some("ops".to_string()),
364 "staging".to_string(),
365 &loaded,
366 );
367 assert_eq!(resolved.0, "sandbox");
368 assert_eq!(resolved.1.as_deref(), Some("ops"));
369 assert_eq!(resolved.2, "staging");
370 }
371}