1use anyhow::Result;
2use colored::Colorize;
3use dialoguer::{Confirm, Input, MultiSelect, Select};
4use std::fs;
5use std::path::PathBuf;
6
7use crate::backpressure::BackpressureConfig;
8use crate::commands::config as config_cmd;
9use crate::commands::helpers::is_interactive;
10use crate::config::{Config, LLMConfig};
11use crate::storage::Storage;
12
13fn configure_provider_and_model(tier: &str) -> Result<(String, String)> {
15 let providers = vec![
16 "Claude Code (recommended - no API key needed)",
17 "OpenAI Codex CLI (no API key needed)",
18 "xAI (Grok)",
19 "Anthropic (Claude API)",
20 "OpenAI (GPT API)",
21 "OpenRouter",
22 ];
23 let provider_selection = Select::new()
24 .with_prompt(format!("Select {} LLM provider", tier))
25 .items(&providers)
26 .default(if tier == "fast" { 2 } else { 0 }) .interact()?;
28
29 let provider = match provider_selection {
30 0 => "claude-cli",
31 1 => "codex",
32 2 => "xai",
33 3 => "anthropic",
34 4 => "openai",
35 5 => "openrouter",
36 _ => "claude-cli",
37 };
38
39 let suggested = Config::suggested_models_for_provider(provider);
41 let mut model_options: Vec<String> = suggested.iter().map(|s| s.to_string()).collect();
42 model_options.push("Custom (enter model name)".to_string());
43
44 let default_model_index = if tier == "fast" && provider == "xai" {
45 suggested
46 .iter()
47 .position(|m| *m == "grok-code-fast-1")
48 .unwrap_or(0)
49 } else if tier == "smart" && provider == "claude-cli" {
50 suggested.iter().position(|m| *m == "opus").unwrap_or(0)
51 } else {
52 0
53 };
54
55 let model_selection = Select::new()
56 .with_prompt(format!(
57 "Select {} model (or choose Custom to enter any model)",
58 tier
59 ))
60 .items(&model_options)
61 .default(default_model_index)
62 .interact()?;
63
64 let model = if model_selection == model_options.len() - 1 {
65 Input::<String>::new()
67 .with_prompt("Enter model name")
68 .interact_text()?
69 } else {
70 suggested[model_selection].to_string()
71 };
72
73 Ok((provider.to_string(), model))
74}
75
76fn configure_backpressure_interactive(storage: &Storage) -> Result<()> {
78 println!();
79 println!(
80 "{}",
81 "=== VALIDATION COMMANDS (BACKPRESSURE) ===".yellow().bold()
82 );
83 println!(
84 "{}",
85 "Backpressure runs validation commands between task waves".dimmed()
86 );
87 println!(
88 "{}",
89 "to catch build/test failures early.".dimmed()
90 );
91 println!();
92
93 let auto_config = BackpressureConfig::load(Some(&storage.project_root().to_path_buf()))?;
95
96 if !auto_config.commands.is_empty() {
97 println!("{}", "Auto-detected commands:".blue());
98 for cmd in &auto_config.commands {
99 println!(" {} {}", "·".green(), cmd);
100 }
101 println!();
102 }
103
104 let options = vec![
105 "Use auto-detect (recommended)",
106 "Configure custom commands",
107 "Skip (configure later with: scud config backpressure)",
108 ];
109
110 let selection = Select::new()
111 .with_prompt("How would you like to configure validation?")
112 .items(&options)
113 .default(0)
114 .interact()?;
115
116 match selection {
117 0 => {
118 if auto_config.commands.is_empty() {
120 println!(
121 "{}",
122 " ⚠ No project type detected - add commands later with: scud config backpressure".yellow()
123 );
124 } else {
125 println!("{}", " ✓ Using auto-detected commands".green());
126 }
127 }
128 1 => {
129 let commands = configure_backpressure_commands(&auto_config.commands)?;
131 save_backpressure_config(storage, &commands)?;
132 println!("{}", " ✓ Custom backpressure commands saved".green());
133 }
134 2 => {
135 println!(
137 "{}",
138 " Skipped - configure later with: scud config backpressure".dimmed()
139 );
140 }
141 _ => {}
142 }
143
144 Ok(())
145}
146
147fn configure_backpressure_commands(auto_detected: &[String]) -> Result<Vec<String>> {
149 println!();
150 println!("{}", "Common validation commands:".blue());
151
152 let mut suggestions: Vec<(&str, bool)> = vec![
154 ("cargo build", false),
155 ("cargo build --release", false),
156 ("cargo test", false),
157 ("cargo clippy -- -D warnings", false),
158 ("cargo fmt --check", false),
159 ("npm run build", false),
160 ("npm test", false),
161 ("npm run lint", false),
162 ("npm run typecheck", false),
163 ("go build ./...", false),
164 ("go test ./...", false),
165 ("pytest", false),
166 ("python -m mypy .", false),
167 ];
168
169 for (cmd, selected) in &mut suggestions {
171 if auto_detected.contains(&cmd.to_string()) {
172 *selected = true;
173 }
174 }
175
176 let items: Vec<&str> = suggestions.iter().map(|(cmd, _)| *cmd).collect();
177 let defaults: Vec<bool> = suggestions.iter().map(|(_, selected)| *selected).collect();
178
179 let selections = MultiSelect::new()
180 .with_prompt("Select commands to run (space to toggle, enter to confirm)")
181 .items(&items)
182 .defaults(&defaults)
183 .interact()?;
184
185 let mut commands: Vec<String> = selections
186 .iter()
187 .map(|&i| items[i].to_string())
188 .collect();
189
190 loop {
192 let add_custom = Confirm::new()
193 .with_prompt("Add a custom command?")
194 .default(false)
195 .interact()?;
196
197 if !add_custom {
198 break;
199 }
200
201 let custom: String = Input::new()
202 .with_prompt("Enter command")
203 .interact_text()?;
204
205 if !custom.trim().is_empty() {
206 commands.push(custom.trim().to_string());
207 println!(" {} Added: {}", "✓".green(), custom.trim());
208 }
209 }
210
211 if commands.is_empty() {
212 println!(
213 "{}",
214 " No commands selected - backpressure will be skipped".yellow()
215 );
216 } else {
217 println!();
218 println!("{}", "Selected commands:".blue());
219 for (i, cmd) in commands.iter().enumerate() {
220 println!(" {}. {}", i + 1, cmd.green());
221 }
222 }
223
224 Ok(commands)
225}
226
227fn save_backpressure_config(storage: &Storage, commands: &[String]) -> Result<()> {
229 let config_path = storage.config_file();
230
231 let content = fs::read_to_string(&config_path).unwrap_or_default();
233 let mut config: toml::Value =
234 toml::from_str(&content).unwrap_or(toml::Value::Table(toml::map::Map::new()));
235
236 let table = config.as_table_mut().expect("Config must be a table");
237
238 if !table.contains_key("swarm") {
240 table.insert(
241 "swarm".to_string(),
242 toml::Value::Table(toml::map::Map::new()),
243 );
244 }
245
246 let swarm = table
247 .get_mut("swarm")
248 .unwrap()
249 .as_table_mut()
250 .unwrap();
251
252 let mut bp = toml::map::Map::new();
254 let cmd_array: Vec<toml::Value> = commands
255 .iter()
256 .map(|s| toml::Value::String(s.clone()))
257 .collect();
258 bp.insert("commands".to_string(), toml::Value::Array(cmd_array));
259 bp.insert("stop_on_failure".to_string(), toml::Value::Boolean(true));
260 bp.insert("timeout_secs".to_string(), toml::Value::Integer(300));
261
262 swarm.insert("backpressure".to_string(), toml::Value::Table(bp));
263
264 let output = toml::to_string_pretty(&config)?;
266 fs::write(&config_path, output)?;
267
268 Ok(())
269}
270
271pub fn run(project_root: Option<PathBuf>, provider_arg: Option<String>) -> Result<()> {
272 let storage = Storage::new(project_root);
273
274 if storage.is_initialized() {
275 println!("{}", "✓ SCUD is already initialized".green());
276 return Ok(());
277 }
278
279 println!("{}", "Initializing SCUD...".blue());
280 println!();
281
282 let (provider, model, smart_provider, smart_model, fast_provider, fast_model) = if let Some(
283 provider_name,
284 ) =
285 provider_arg
286 {
287 let provider = provider_name.to_lowercase();
289 if !matches!(
290 provider.as_str(),
291 "xai" | "anthropic" | "openai" | "openrouter" | "claude-cli" | "codex"
292 ) {
293 anyhow::bail!(
294 "Invalid provider: {}. Valid options: claude-cli, codex, xai, anthropic, openai, openrouter",
295 provider
296 );
297 }
298 let model = Config::default_model_for_provider(&provider).to_string();
299 let smart_provider = "claude-cli".to_string();
301 let smart_model = "opus".to_string();
302 let fast_provider = "xai".to_string();
303 let fast_model = "grok-code-fast-1".to_string();
304 (
305 provider,
306 model,
307 smart_provider,
308 smart_model,
309 fast_provider,
310 fast_model,
311 )
312 } else if is_interactive() {
313 println!(
314 "{}",
315 "SCUD supports separate models for different types of tasks:".blue()
316 );
317 println!(" • Fast models: Quick coding, generation tasks");
318 println!(" • Smart models: Complex reasoning, analysis, validation");
319 println!();
320
321 println!("{}", "=== FAST MODEL CONFIGURATION ===".yellow().bold());
323 let (fast_provider, fast_model) = configure_provider_and_model("fast")?;
324
325 println!();
327 println!("{}", "=== SMART MODEL CONFIGURATION ===".yellow().bold());
328 let (smart_provider, smart_model) = configure_provider_and_model("smart")?;
329
330 let provider = fast_provider.clone();
332 let model = fast_model.clone();
333
334 (
335 provider,
336 model,
337 smart_provider,
338 smart_model,
339 fast_provider,
340 fast_model,
341 )
342 } else {
343 let provider = "claude-cli";
345 let model = Config::default_model_for_provider(provider);
346 let smart_provider = "claude-cli".to_string();
348 let smart_model = "opus".to_string();
349 let fast_provider = "xai".to_string();
350 let fast_model = "grok-code-fast-1".to_string();
351 (
352 provider.to_string(),
353 model.to_string(),
354 smart_provider,
355 smart_model,
356 fast_provider,
357 fast_model,
358 )
359 };
360
361 let config = Config {
362 llm: LLMConfig {
363 provider,
364 model,
365 smart_provider,
366 smart_model,
367 fast_provider,
368 fast_model,
369 max_tokens: 16000,
370 },
371 };
372
373 storage.initialize_with_config(&config)?;
374
375 if is_interactive() {
377 configure_backpressure_interactive(&storage)?;
378 }
379
380 println!("\n{}", "SCUD initialized successfully!".green().bold());
381
382 println!("\n{}", "Installing SCUD agents and commands...".blue());
384 if let Err(e) = config_cmd::agents_add(Some(storage.project_root().to_path_buf()), None, true) {
385 println!("{}", format!(" Could not install agents: {}", e).yellow());
386 println!(" You can install them later with: scud config agents add --all");
387 }
388
389 if let Err(e) = update_claude_md(&storage) {
391 println!(
392 "{}",
393 format!(" Could not update CLAUDE.md: {}", e).yellow()
394 );
395 }
396
397 println!("\n{}", "Configuration:".blue());
398 println!(
399 " Default Provider: {} ({})",
400 config.llm.provider.yellow(),
401 config.llm.model.yellow()
402 );
403 println!(
404 " Fast Provider: {} ({})",
405 config.llm.fast_provider.yellow(),
406 config.llm.fast_model.yellow()
407 );
408 println!(
409 " Smart Provider: {} ({})",
410 config.llm.smart_provider.yellow(),
411 config.llm.smart_model.yellow()
412 );
413 if config.requires_api_key() {
414 println!("\n{}", "Environment variables required:".blue());
415 let mut env_vars = std::collections::HashSet::new();
416 env_vars.insert(config.api_key_env_var());
417 if config.llm.fast_provider != config.llm.provider {
418 env_vars.insert(Config::api_key_env_var_for_provider(
419 &config.llm.fast_provider,
420 ));
421 }
422 if config.llm.smart_provider != config.llm.provider
423 && config.llm.smart_provider != config.llm.fast_provider
424 {
425 env_vars.insert(Config::api_key_env_var_for_provider(
426 &config.llm.smart_provider,
427 ));
428 }
429 for env_var in env_vars {
430 if env_var != "NONE" {
431 println!(" export {}=your-api-key", env_var.yellow());
432 }
433 }
434 } else {
435 println!("\n{}", "No API keys required (using CLI tools)".green());
436 }
437 println!("\n{}", "Next steps:".blue());
438 println!(" 1. Set your API key environment variable");
439 println!(" 2. Run: scud tags");
440 println!(" 3. Create or import tasks, then use: /scud:next\n");
441
442 Ok(())
443}
444
445fn update_claude_md(storage: &Storage) -> Result<()> {
447 let claude_md_path = storage.project_root().join("CLAUDE.md");
448
449 let scud_section = r#"
450## SCUD Task Management
451
452This project uses SCUD for AI-driven task management.
453
454### Quick Start
455- `scud tags` - List available phases
456- `scud next` - Find next available task
457- `scud set-status <id> in-progress` - Claim a task
458- `scud view` - Open interactive task viewer
459
460### Slash Commands
461Use `/scud:` commands in Claude Code for task operations.
462"#;
463
464 let marker = "## SCUD Task Management";
465
466 if claude_md_path.exists() {
467 let content = fs::read_to_string(&claude_md_path)?;
468 if content.contains(marker) {
469 return Ok(()); }
471 let new_content = format!("{}\n{}", content.trim_end(), scud_section);
473 fs::write(&claude_md_path, new_content)?;
474 } else {
475 fs::write(&claude_md_path, scud_section.trim_start())?;
477 }
478
479 println!(" {} Updated CLAUDE.md with SCUD instructions", "✓".green());
480 Ok(())
481}