1use crate::contracts::{Runner, TaskPriority};
17use anyhow::{Context, Result};
18use dialoguer::{Confirm, Input, Select};
19use std::path::Path;
20
21#[derive(Debug, Clone)]
23pub struct WizardAnswers {
24 pub runner: Runner,
26 pub model: String,
28 pub phases: u8,
30 pub create_first_task: bool,
32 pub first_task_title: Option<String>,
34 pub first_task_description: Option<String>,
36 pub first_task_priority: TaskPriority,
38}
39
40impl Default for WizardAnswers {
41 fn default() -> Self {
42 Self {
43 runner: Runner::Claude,
44 model: "sonnet".to_string(),
45 phases: 3,
46 create_first_task: false,
47 first_task_title: None,
48 first_task_description: None,
49 first_task_priority: TaskPriority::Medium,
50 }
51 }
52}
53
54pub fn run_wizard() -> Result<WizardAnswers> {
56 print_welcome();
58
59 let runners = [
61 (
62 "Claude",
63 "Anthropic's Claude Code CLI - Best for complex reasoning",
64 ),
65 ("Codex", "OpenAI's Codex CLI - Great for code generation"),
66 ("OpenCode", "OpenCode agent - Open source alternative"),
67 (
68 "Gemini",
69 "Google's Gemini CLI - Good for large context windows",
70 ),
71 ("Cursor", "Cursor's agent mode - IDE-integrated workflow"),
72 ("Kimi", "Moonshot AI Kimi - Strong coding capabilities"),
73 ("Pi", "Inflection Pi - Conversational AI assistant"),
74 ];
75
76 let runner_idx = Select::new()
77 .with_prompt("Select your AI runner")
78 .items(
79 runners
80 .iter()
81 .map(|(name, desc)| format!("{} - {}", name, desc))
82 .collect::<Vec<_>>(),
83 )
84 .default(0)
85 .interact()
86 .context("failed to get runner selection")?;
87
88 let runner = match runner_idx {
89 0 => Runner::Claude,
90 1 => Runner::Codex,
91 2 => Runner::Opencode,
92 3 => Runner::Gemini,
93 4 => Runner::Cursor,
94 5 => Runner::Kimi,
95 6 => Runner::Pi,
96 _ => Runner::Claude, };
98
99 let model = select_model(&runner)?;
101
102 let phases = select_phases()?;
104
105 let create_first_task = Confirm::new()
107 .with_prompt("Would you like to create your first task now?")
108 .default(true)
109 .interact()
110 .context("failed to get first task confirmation")?;
111
112 let (first_task_title, first_task_description, first_task_priority) = if create_first_task {
113 let title: String = Input::new()
114 .with_prompt("Task title")
115 .allow_empty(false)
116 .interact_text()
117 .context("failed to get task title")?;
118
119 let description: String = Input::new()
120 .with_prompt("Task description (what should be done)")
121 .allow_empty(true)
122 .interact_text()
123 .context("failed to get task description")?;
124
125 let priorities = vec!["Low", "Medium", "High", "Critical"];
126 let priority_idx = Select::new()
127 .with_prompt("Task priority")
128 .items(&priorities)
129 .default(1)
130 .interact()
131 .context("failed to get priority selection")?;
132
133 let priority = match priority_idx {
134 0 => TaskPriority::Low,
135 1 => TaskPriority::Medium,
136 2 => TaskPriority::High,
137 3 => TaskPriority::Critical,
138 _ => TaskPriority::Medium,
139 };
140
141 (Some(title), Some(description), priority)
142 } else {
143 (None, None, TaskPriority::Medium)
144 };
145
146 let answers = WizardAnswers {
148 runner,
149 model,
150 phases,
151 create_first_task,
152 first_task_title,
153 first_task_description,
154 first_task_priority,
155 };
156
157 print_summary(&answers);
158
159 let proceed = Confirm::new()
160 .with_prompt("Proceed with setup?")
161 .default(true)
162 .interact()
163 .context("failed to get confirmation")?;
164
165 if !proceed {
166 anyhow::bail!("Setup cancelled by user");
167 }
168
169 Ok(answers)
170}
171
172fn print_welcome() {
174 println!();
175 println!(
176 "{}",
177 colored::Colorize::bright_cyan(r" ____ __ __")
178 );
179 println!(
180 "{}",
181 colored::Colorize::bright_cyan(r" / __ \___ / /_____ / /_____ ___")
182 );
183 println!(
184 "{}",
185 colored::Colorize::bright_cyan(r" / /_/ / _ \/ __/ __ \/ __/ __ `__ \ ")
186 );
187 println!(
188 "{}",
189 colored::Colorize::bright_cyan(r" / _, _/ __/ /_/ /_/ / /_/ / / / / /")
190 );
191 println!(
192 "{}",
193 colored::Colorize::bright_cyan(r"/_/ |_|\___/\__/ .___/\__/_/ /_/ /_/")
194 );
195 println!("{}", colored::Colorize::bright_cyan(r" /_/"));
196 println!();
197 println!("{}", colored::Colorize::bold("Welcome to Ralph!"));
198 println!();
199 println!("Ralph is an AI task queue for structured agent workflows.");
200 println!("This wizard will help you set up your project and create your first task.");
201 println!();
202}
203
204fn select_model(runner: &Runner) -> Result<String> {
206 let models: Vec<(&str, &str)> = match runner {
207 Runner::Claude => vec![
208 ("sonnet", "Balanced speed and intelligence (recommended)"),
209 ("opus", "Most powerful, best for complex tasks"),
210 ("haiku", "Fastest, good for simple tasks"),
211 ("custom", "Other model (specify)"),
212 ],
213 Runner::Codex => vec![
214 (
215 "gpt-5.4",
216 "Latest general GPT-5 model for Codex (recommended)",
217 ),
218 ("gpt-5.3-codex", "Codex optimized for coding"),
219 ("gpt-5.3-codex-spark", "Codex Spark variant for coding"),
220 ("gpt-5.3", "General GPT-5.3"),
221 ("gpt-5.2-codex", "Codex optimized for coding (legacy)"),
222 ("gpt-5.2", "General GPT-5.2 (legacy)"),
223 ("custom", "Other model (specify)"),
224 ],
225 Runner::Gemini => vec![
226 (
227 "zai-coding-plan/glm-4.7",
228 "Default Gemini model (recommended)",
229 ),
230 ("custom", "Other model (specify)"),
231 ],
232 Runner::Opencode => vec![
233 ("zai-coding-plan/glm-4.7", "GLM-4.7 model (recommended)"),
234 ("custom", "Other model (specify)"),
235 ],
236 Runner::Kimi => vec![
237 ("kimi-for-coding", "Kimi coding model (recommended)"),
238 ("custom", "Other model (specify)"),
239 ],
240 Runner::Pi => vec![
241 ("gpt-5.3", "GPT-5.3 model (recommended)"),
242 ("custom", "Other model (specify)"),
243 ],
244 Runner::Cursor => vec![
245 ("auto", "Let Cursor choose automatically (recommended)"),
246 ("custom", "Other model (specify)"),
247 ],
248 Runner::Plugin(_) => vec![
249 ("default", "Use runner default"),
250 ("custom", "Specify custom model"),
251 ],
252 };
253
254 let items: Vec<String> = models
255 .iter()
256 .map(|(name, desc)| format!("{} - {}", name, desc))
257 .collect();
258
259 let idx = Select::new()
260 .with_prompt("Select model")
261 .items(&items)
262 .default(0)
263 .interact()
264 .context("failed to get model selection")?;
265
266 let selected = models[idx].0;
267
268 if selected == "custom" {
269 let custom: String = Input::new()
270 .with_prompt("Enter model name")
271 .allow_empty(false)
272 .interact_text()
273 .context("failed to get custom model")?;
274 Ok(custom)
275 } else {
276 Ok(selected.to_string())
277 }
278}
279
280fn select_phases() -> Result<u8> {
282 let phase_options = [
283 (
284 "3-phase (Full)",
285 "Plan → Implement + CI → Review + Complete [Recommended]",
286 ),
287 (
288 "2-phase (Standard)",
289 "Plan → Implement (faster, less review)",
290 ),
291 (
292 "1-phase (Quick)",
293 "Single-pass execution (simple fixes only)",
294 ),
295 ];
296
297 let items: Vec<String> = phase_options
298 .iter()
299 .map(|(name, desc)| format!("{} - {}", name, desc))
300 .collect();
301
302 let idx = Select::new()
303 .with_prompt("Select workflow mode")
304 .items(&items)
305 .default(0)
306 .interact()
307 .context("failed to get phase selection")?;
308
309 Ok(match idx {
310 0 => 3,
311 1 => 2,
312 2 => 1,
313 _ => 3,
314 })
315}
316
317fn print_summary(answers: &WizardAnswers) {
319 println!();
320 println!("{}", colored::Colorize::bold("Setup Summary:"));
321 println!("{}", colored::Colorize::bright_black("──────────────"));
322 println!(
323 "Runner: {} ({})",
324 colored::Colorize::bright_green(format!("{:?}", answers.runner).as_str()),
325 answers.model
326 );
327 println!(
328 "Workflow: {}-phase",
329 colored::Colorize::bright_green(format!("{}", answers.phases).as_str())
330 );
331
332 if answers.create_first_task {
333 if let Some(ref title) = answers.first_task_title {
334 println!(
335 "First Task: {}",
336 colored::Colorize::bright_green(title.as_str())
337 );
338 }
339 } else {
340 println!("First Task: {}", colored::Colorize::bright_black("(none)"));
341 }
342
343 println!();
344 println!("Files to create:");
345 println!(" - .ralph/config.jsonc");
346 println!(" - .ralph/queue.jsonc");
347 println!(" - .ralph/done.jsonc");
348 println!();
349}
350
351pub fn print_completion_message(answers: Option<&WizardAnswers>, _queue_path: &Path) {
353 println!();
354 println!(
355 "{}",
356 colored::Colorize::bright_green("✓ Ralph initialized successfully!")
357 );
358 println!();
359 println!("{}", colored::Colorize::bold("Next steps:"));
360 println!(" 1. Run 'ralph app open' to open the macOS app (optional)");
361 println!(" 2. Run 'ralph run one' to execute your first task");
362 println!(" 3. Edit .ralph/config.jsonc to customize settings");
363
364 if let Some(answers) = answers
365 && answers.create_first_task
366 {
367 println!();
368 println!("Your first task is ready to go!");
369 }
370
371 println!();
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn wizard_answers_default() {
380 let answers = WizardAnswers::default();
381 assert_eq!(answers.runner, Runner::Claude);
382 assert_eq!(answers.model, "sonnet");
383 assert_eq!(answers.phases, 3);
384 assert!(!answers.create_first_task);
385 assert!(answers.first_task_title.is_none());
386 assert!(answers.first_task_description.is_none());
387 assert_eq!(answers.first_task_priority, TaskPriority::Medium);
388 }
389}