syncable_cli/wizard/
config_form.rs1use crate::analyzer::DiscoveredDockerfile;
4use crate::platform::api::types::{CloudProvider, DeploymentTarget, WizardDeploymentConfig};
5use crate::wizard::render::display_step_header;
6use colored::Colorize;
7use inquire::{Confirm, InquireError, Text};
8
9#[derive(Debug, Clone)]
11pub enum ConfigFormResult {
12 Completed(WizardDeploymentConfig),
14 Back,
16 Cancelled,
18}
19
20#[allow(clippy::too_many_arguments)]
26pub fn collect_config(
27 provider: CloudProvider,
28 target: DeploymentTarget,
29 cluster_id: Option<String>,
30 registry_id: Option<String>,
31 environment_id: &str,
32 dockerfile_path: &str,
33 build_context: &str,
34 discovered_dockerfile: &DiscoveredDockerfile,
35 region: Option<String>,
36 machine_type: Option<String>,
37 step_number: u8,
38) -> ConfigFormResult {
39 display_step_header(
40 step_number,
41 "Configure Service",
42 "Provide details for your service deployment.",
43 );
44
45 println!(
47 " {} Dockerfile: {}",
48 "│".dimmed(),
49 dockerfile_path.cyan()
50 );
51 println!(
52 " {} Build context: {}",
53 "│".dimmed(),
54 build_context.cyan()
55 );
56 if let Some(ref r) = region {
57 println!(" {} Region: {}", "│".dimmed(), r.cyan());
58 }
59 if let Some(ref m) = machine_type {
60 println!(" {} Machine: {}", "│".dimmed(), m.cyan());
61 }
62 println!();
63
64 let default_name = discovered_dockerfile.suggested_service_name.clone();
66 let default_port = discovered_dockerfile.suggested_port.unwrap_or(8080);
67
68 let default_branch = get_current_branch().unwrap_or_else(|| "main".to_string());
70
71 let service_name = match Text::new("Service name:")
73 .with_default(&default_name)
74 .with_help_message("K8s-compatible name (lowercase, hyphens)")
75 .prompt()
76 {
77 Ok(name) => sanitize_service_name(&name),
78 Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
79 return ConfigFormResult::Cancelled;
80 }
81 Err(_) => return ConfigFormResult::Cancelled,
82 };
83
84 let port_str = default_port.to_string();
86 let port = match Text::new("Service port:")
87 .with_default(&port_str)
88 .with_help_message("Port your service listens on")
89 .prompt()
90 {
91 Ok(p) => p.parse::<u16>().unwrap_or(default_port),
92 Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
93 return ConfigFormResult::Cancelled;
94 }
95 Err(_) => return ConfigFormResult::Cancelled,
96 };
97
98 let branch = match Text::new("Git branch:")
100 .with_default(&default_branch)
101 .with_help_message("Branch to deploy from")
102 .prompt()
103 {
104 Ok(b) => b,
105 Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
106 return ConfigFormResult::Cancelled;
107 }
108 Err(_) => return ConfigFormResult::Cancelled,
109 };
110
111 let is_public = if target == DeploymentTarget::CloudRunner {
113 println!();
114 println!(
115 "{}",
116 "─── Access Configuration ────────────────────".dimmed()
117 );
118 match Confirm::new("Enable public access?")
119 .with_default(true)
120 .with_help_message("Make service accessible via public IP/URL")
121 .prompt()
122 {
123 Ok(v) => v,
124 Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
125 return ConfigFormResult::Cancelled;
126 }
127 Err(_) => return ConfigFormResult::Cancelled,
128 }
129 } else {
130 true };
132
133 let health_check_path = if target == DeploymentTarget::CloudRunner {
135 match Confirm::new("Configure health check endpoint?")
136 .with_default(false)
137 .with_help_message("Optional HTTP health probe for your service")
138 .prompt()
139 {
140 Ok(true) => {
141 match Text::new("Health check path:")
142 .with_default("/health")
143 .with_help_message("e.g., /health, /healthz, /api/health")
144 .prompt()
145 {
146 Ok(path) => Some(path),
147 Err(InquireError::OperationCanceled)
148 | Err(InquireError::OperationInterrupted) => {
149 return ConfigFormResult::Cancelled;
150 }
151 Err(_) => return ConfigFormResult::Cancelled,
152 }
153 }
154 Ok(false) => None,
155 Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
156 return ConfigFormResult::Cancelled;
157 }
158 Err(_) => return ConfigFormResult::Cancelled,
159 }
160 } else {
161 None
162 };
163
164 let auto_deploy = false;
166
167 let config = WizardDeploymentConfig {
169 service_name: Some(service_name.clone()),
170 dockerfile_path: Some(dockerfile_path.to_string()),
171 build_context: Some(build_context.to_string()),
172 port: Some(port),
173 branch: Some(branch),
174 target: Some(target),
175 provider: Some(provider),
176 cluster_id,
177 registry_id,
178 environment_id: Some(environment_id.to_string()),
179 auto_deploy,
180 region,
181 machine_type,
182 is_public,
183 health_check_path,
184 };
185
186 println!("\n{} Configuration complete: {}", "✓".green(), service_name);
187
188 ConfigFormResult::Completed(config)
189}
190
191fn get_current_branch() -> Option<String> {
193 std::process::Command::new("git")
194 .args(["rev-parse", "--abbrev-ref", "HEAD"])
195 .output()
196 .ok()
197 .and_then(|output| {
198 if output.status.success() {
199 String::from_utf8(output.stdout)
200 .ok()
201 .map(|s| s.trim().to_string())
202 } else {
203 None
204 }
205 })
206}
207
208fn sanitize_service_name(name: &str) -> String {
210 name.to_lowercase()
211 .chars()
212 .map(|c| if c.is_alphanumeric() || c == '-' { c } else { '-' })
213 .collect::<String>()
214 .trim_matches('-')
215 .to_string()
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_sanitize_service_name() {
224 assert_eq!(sanitize_service_name("My Service"), "my-service");
225 assert_eq!(sanitize_service_name("foo_bar"), "foo-bar");
226 assert_eq!(sanitize_service_name("--test--"), "test");
227 assert_eq!(sanitize_service_name("API Server"), "api-server");
228 }
229
230 #[test]
231 fn test_config_form_result_variants() {
232 let config = WizardDeploymentConfig::default();
233 let _ = ConfigFormResult::Completed(config);
234 let _ = ConfigFormResult::Back;
235 let _ = ConfigFormResult::Cancelled;
236 }
237}