syncable_cli/wizard/
config_form.rs

1//! Deployment configuration form for the wizard
2
3use 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/// Result of config form step
10#[derive(Debug, Clone)]
11pub enum ConfigFormResult {
12    /// User completed the form
13    Completed(WizardDeploymentConfig),
14    /// User wants to go back
15    Back,
16    /// User cancelled the wizard
17    Cancelled,
18}
19
20/// Collect deployment configuration details from user
21///
22/// Region, machine type, Dockerfile path, and build context are already selected
23/// in previous steps. This form collects service name, port, branch, public access,
24/// health check, and auto-deploy settings.
25#[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    // Show previously selected options
46    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    // Pre-populate from discovery
65    let default_name = discovered_dockerfile.suggested_service_name.clone();
66    let default_port = discovered_dockerfile.suggested_port.unwrap_or(8080);
67
68    // Get current git branch for default
69    let default_branch = get_current_branch().unwrap_or_else(|| "main".to_string());
70
71    // Service name
72    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    // Port
85    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    // Branch
99    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    // Public access toggle (for Cloud Runner)
112    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 // Default to public for K8s
131    };
132
133    // Health check (optional)
134    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    // Auto-deploy disabled by default (CI/CD not ready yet)
165    let auto_deploy = false;
166
167    // Build the config
168    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
191/// Get current git branch name
192fn 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
208/// Sanitize service name for K8s compatibility
209fn 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}