opencode_cloud/wizard/
mod.rs1mod auth;
6mod network;
7mod prechecks;
8mod summary;
9
10pub use auth::create_container_user;
11pub use prechecks::{verify_docker_available, verify_tty};
12
13use anyhow::{Result, anyhow};
14use console::{Term, style};
15use dialoguer::Confirm;
16use opencode_cloud_core::Config;
17use opencode_cloud_core::docker::{CONTAINER_NAME, DockerClient, container_is_running};
18
19use auth::prompt_auth;
20use network::{prompt_hostname, prompt_port};
21use summary::display_summary;
22
23#[derive(Debug, Clone)]
25pub struct WizardState {
26 pub auth_username: Option<String>,
28 pub auth_password: Option<String>,
30 pub port: u16,
32 pub bind: String,
34}
35
36impl WizardState {
37 pub fn apply_to_config(&self, config: &mut Config) {
39 if let Some(ref username) = self.auth_username {
40 config.auth_username = Some(username.clone());
41 }
42 if let Some(ref password) = self.auth_password {
43 config.auth_password = Some(password.clone());
44 }
45 config.opencode_web_port = self.port;
46 config.bind = self.bind.clone();
47 }
48}
49
50fn handle_interrupt() -> anyhow::Error {
52 let _ = Term::stdout().show_cursor();
54 anyhow!("Setup cancelled")
55}
56
57pub async fn run_wizard(existing_config: Option<&Config>) -> Result<Config> {
72 verify_tty()?;
74 verify_docker_available().await?;
75
76 let client = DockerClient::new()?;
78 let is_container_running = container_is_running(&client, CONTAINER_NAME)
79 .await
80 .unwrap_or(false);
81
82 println!();
83 println!("{}", style("opencode-cloud Setup Wizard").cyan().bold());
84 println!("{}", style("=".repeat(30)).dim());
85 println!();
86
87 if let Some(config) = existing_config {
89 let has_users = !config.users.is_empty();
90 let has_old_auth = config.has_required_auth();
91
92 if has_users || has_old_auth {
93 println!("{}", style("Current configuration:").bold());
94 if has_users {
95 println!(" Users: {}", config.users.join(", "));
96 } else if has_old_auth {
97 println!(
98 " Username: {} (legacy)",
99 config.auth_username.as_deref().unwrap_or("-")
100 );
101 println!(" Password: ********");
102 }
103 println!(" Port: {}", config.opencode_web_port);
104 println!(" Binding: {}", config.bind);
105 println!();
106
107 let reconfigure = Confirm::new()
108 .with_prompt("Reconfigure?")
109 .default(false)
110 .interact()
111 .map_err(|_| handle_interrupt())?;
112
113 if !reconfigure {
114 return Err(anyhow!("Setup cancelled"));
115 }
116 println!();
117 }
118 }
119
120 let quick = Confirm::new()
122 .with_prompt("Use defaults for everything except credentials?")
123 .default(false)
124 .interact()
125 .map_err(|_| handle_interrupt())?;
126
127 println!();
128
129 let total_steps = if quick { 1 } else { 3 };
131
132 let (username, password) = prompt_auth(1, total_steps)?;
133
134 let (port, bind) = if quick {
135 (3000, "localhost".to_string())
136 } else {
137 let port = prompt_port(2, total_steps, 3000)?;
138 let bind = prompt_hostname(3, total_steps, "localhost")?;
139 (port, bind)
140 };
141
142 let state = WizardState {
143 auth_username: Some(username.clone()),
144 auth_password: Some(password.clone()),
145 port,
146 bind,
147 };
148
149 println!();
151 display_summary(&state);
152 println!();
153
154 let save = Confirm::new()
156 .with_prompt("Save this configuration?")
157 .default(true)
158 .interact()
159 .map_err(|_| handle_interrupt())?;
160
161 if !save {
162 return Err(anyhow!("Setup cancelled"));
163 }
164
165 if is_container_running {
167 println!();
168 println!("{}", style("Creating user in container...").cyan());
169 auth::create_container_user(&client, &username, &password).await?;
170 } else {
171 println!();
172 println!(
173 "{}",
174 style("Note: User will be created when container starts.").dim()
175 );
176 }
177
178 let mut config = existing_config.cloned().unwrap_or_default();
180 state.apply_to_config(&mut config);
181
182 if !config.users.contains(&username) {
184 config.users.push(username);
185 }
186
187 if let Some(ref old_username) = config.auth_username {
189 if !old_username.is_empty() && !config.users.contains(old_username) {
190 println!(
191 "{}",
192 style(format!(
193 "Migrating existing user '{old_username}' to PAM-based authentication..."
194 ))
195 .dim()
196 );
197 config.users.push(old_username.clone());
198 }
199 }
200
201 config.auth_username = Some(String::new());
203 config.auth_password = Some(String::new());
204
205 Ok(config)
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_wizard_state_apply_to_config() {
214 let state = WizardState {
215 auth_username: Some("testuser".to_string()),
216 auth_password: Some("testpass".to_string()),
217 port: 8080,
218 bind: "0.0.0.0".to_string(),
219 };
220
221 let mut config = Config::default();
222 state.apply_to_config(&mut config);
223
224 assert_eq!(config.auth_username, Some("testuser".to_string()));
225 assert_eq!(config.auth_password, Some("testpass".to_string()));
226 assert_eq!(config.opencode_web_port, 8080);
227 assert_eq!(config.bind, "0.0.0.0");
228 }
229
230 #[test]
231 fn test_wizard_state_preserves_other_config_fields() {
232 let state = WizardState {
233 auth_username: Some("admin".to_string()),
234 auth_password: Some("secret".to_string()),
235 port: 3000,
236 bind: "localhost".to_string(),
237 };
238
239 let mut config = Config {
240 auto_restart: false,
241 restart_retries: 10,
242 ..Config::default()
243 };
244 state.apply_to_config(&mut config);
245
246 assert!(!config.auto_restart);
248 assert_eq!(config.restart_retries, 10);
249
250 assert_eq!(config.auth_username, Some("admin".to_string()));
252 }
253}