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 == "xai/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!("{}", "to catch build/test failures early.".dimmed());
88 println!();
89
90 let auto_config = BackpressureConfig::load(Some(&storage.project_root().to_path_buf()))?;
92
93 if !auto_config.commands.is_empty() {
94 println!("{}", "Auto-detected commands:".blue());
95 for cmd in &auto_config.commands {
96 println!(" {} {}", "·".green(), cmd);
97 }
98 println!();
99 }
100
101 let options = vec![
102 "Use auto-detect (recommended)",
103 "Configure custom commands",
104 "Skip (configure later with: scud config backpressure)",
105 ];
106
107 let selection = Select::new()
108 .with_prompt("How would you like to configure validation?")
109 .items(&options)
110 .default(0)
111 .interact()?;
112
113 match selection {
114 0 => {
115 if auto_config.commands.is_empty() {
117 println!(
118 "{}",
119 " ⚠ No project type detected - add commands later with: scud config backpressure".yellow()
120 );
121 } else {
122 println!("{}", " ✓ Using auto-detected commands".green());
123 }
124 }
125 1 => {
126 let commands = configure_backpressure_commands(&auto_config.commands)?;
128 save_backpressure_config(storage, &commands)?;
129 println!("{}", " ✓ Custom backpressure commands saved".green());
130 }
131 2 => {
132 println!(
134 "{}",
135 " Skipped - configure later with: scud config backpressure".dimmed()
136 );
137 }
138 _ => {}
139 }
140
141 Ok(())
142}
143
144fn configure_backpressure_commands(auto_detected: &[String]) -> Result<Vec<String>> {
146 println!();
147 println!("{}", "Common validation commands:".blue());
148
149 let mut suggestions: Vec<(&str, bool)> = vec![
151 ("cargo build", false),
152 ("cargo build --release", false),
153 ("cargo test", false),
154 ("cargo clippy -- -D warnings", false),
155 ("cargo fmt --check", false),
156 ("npm run build", false),
157 ("npm test", false),
158 ("npm run lint", false),
159 ("npm run typecheck", false),
160 ("go build ./...", false),
161 ("go test ./...", false),
162 ("pytest", false),
163 ("python -m mypy .", false),
164 ];
165
166 for (cmd, selected) in &mut suggestions {
168 if auto_detected.contains(&cmd.to_string()) {
169 *selected = true;
170 }
171 }
172
173 let items: Vec<&str> = suggestions.iter().map(|(cmd, _)| *cmd).collect();
174 let defaults: Vec<bool> = suggestions.iter().map(|(_, selected)| *selected).collect();
175
176 let selections = MultiSelect::new()
177 .with_prompt("Select commands to run (space to toggle, enter to confirm)")
178 .items(&items)
179 .defaults(&defaults)
180 .interact()?;
181
182 let mut commands: Vec<String> = selections.iter().map(|&i| items[i].to_string()).collect();
183
184 loop {
186 let add_custom = Confirm::new()
187 .with_prompt("Add a custom command?")
188 .default(false)
189 .interact()?;
190
191 if !add_custom {
192 break;
193 }
194
195 let custom: String = Input::new().with_prompt("Enter command").interact_text()?;
196
197 if !custom.trim().is_empty() {
198 commands.push(custom.trim().to_string());
199 println!(" {} Added: {}", "✓".green(), custom.trim());
200 }
201 }
202
203 if commands.is_empty() {
204 println!(
205 "{}",
206 " No commands selected - backpressure will be skipped".yellow()
207 );
208 } else {
209 println!();
210 println!("{}", "Selected commands:".blue());
211 for (i, cmd) in commands.iter().enumerate() {
212 println!(" {}. {}", i + 1, cmd.green());
213 }
214 }
215
216 Ok(commands)
217}
218
219fn save_backpressure_config(storage: &Storage, commands: &[String]) -> Result<()> {
221 let config_path = storage.config_file();
222
223 let content = fs::read_to_string(&config_path).unwrap_or_default();
225 let mut config: toml::Value =
226 toml::from_str(&content).unwrap_or(toml::Value::Table(toml::map::Map::new()));
227
228 let table = config.as_table_mut().expect("Config must be a table");
229
230 if !table.contains_key("swarm") {
232 table.insert(
233 "swarm".to_string(),
234 toml::Value::Table(toml::map::Map::new()),
235 );
236 }
237
238 let swarm = table.get_mut("swarm").unwrap().as_table_mut().unwrap();
239
240 let mut bp = toml::map::Map::new();
242 let cmd_array: Vec<toml::Value> = commands
243 .iter()
244 .map(|s| toml::Value::String(s.clone()))
245 .collect();
246 bp.insert("commands".to_string(), toml::Value::Array(cmd_array));
247 bp.insert("stop_on_failure".to_string(), toml::Value::Boolean(true));
248 bp.insert("timeout_secs".to_string(), toml::Value::Integer(300));
249
250 swarm.insert("backpressure".to_string(), toml::Value::Table(bp));
251
252 let output = toml::to_string_pretty(&config)?;
254 fs::write(&config_path, output)?;
255
256 Ok(())
257}
258
259pub fn run(project_root: Option<PathBuf>, provider_arg: Option<String>) -> Result<()> {
260 let storage = Storage::new(project_root);
261
262 if storage.is_initialized() {
263 println!("{}", "✓ SCUD is already initialized".green());
264 return Ok(());
265 }
266
267 println!("{}", "Initializing SCUD...".blue());
268 println!();
269
270 let (provider, model, smart_provider, smart_model, fast_provider, fast_model) = if let Some(
271 provider_name,
272 ) =
273 provider_arg
274 {
275 let provider = provider_name.to_lowercase();
277 if !matches!(
278 provider.as_str(),
279 "xai" | "anthropic" | "openai" | "openrouter" | "claude-cli" | "codex"
280 ) {
281 anyhow::bail!(
282 "Invalid provider: {}. Valid options: claude-cli, codex, xai, anthropic, openai, openrouter",
283 provider
284 );
285 }
286 let model = Config::default_model_for_provider(&provider).to_string();
287 let defaults = Config::default();
289 (
290 provider,
291 model,
292 defaults.llm.smart_provider,
293 defaults.llm.smart_model,
294 defaults.llm.fast_provider,
295 defaults.llm.fast_model,
296 )
297 } else if is_interactive() {
298 println!(
299 "{}",
300 "SCUD supports separate models for different types of tasks:".blue()
301 );
302 println!(" • Fast models: Quick coding, generation tasks");
303 println!(" • Smart models: Complex reasoning, analysis, validation");
304 println!();
305
306 println!("{}", "=== FAST MODEL CONFIGURATION ===".yellow().bold());
308 let (fast_provider, fast_model) = configure_provider_and_model("fast")?;
309
310 println!();
312 println!("{}", "=== SMART MODEL CONFIGURATION ===".yellow().bold());
313 let (smart_provider, smart_model) = configure_provider_and_model("smart")?;
314
315 let provider = fast_provider.clone();
317 let model = fast_model.clone();
318
319 (
320 provider,
321 model,
322 smart_provider,
323 smart_model,
324 fast_provider,
325 fast_model,
326 )
327 } else {
328 let defaults = Config::default();
330 (
331 defaults.llm.provider,
332 defaults.llm.model,
333 defaults.llm.smart_provider,
334 defaults.llm.smart_model,
335 defaults.llm.fast_provider,
336 defaults.llm.fast_model,
337 )
338 };
339
340 let config = Config {
341 llm: LLMConfig {
342 provider,
343 model,
344 smart_provider,
345 smart_model,
346 fast_provider,
347 fast_model,
348 max_tokens: 16000,
349 },
350 };
351
352 storage.initialize_with_config(&config)?;
353
354 if is_interactive() {
356 configure_backpressure_interactive(&storage)?;
357 }
358
359 println!("\n{}", "SCUD initialized successfully!".green().bold());
360
361 println!("\n{}", "Installing SCUD agents and commands...".blue());
363 if let Err(e) = config_cmd::agents_add(Some(storage.project_root().to_path_buf()), None, true) {
364 println!("{}", format!(" Could not install agents: {}", e).yellow());
365 println!(" You can install them later with: scud config agents add --all");
366 }
367
368 println!("\n{}", "Installing spawn agent definitions...".blue());
370 if let Err(e) = config_cmd::spawn_agents_add(
371 Some(storage.project_root().to_path_buf()),
372 None,
373 true,
374 false,
375 ) {
376 println!(
377 "{}",
378 format!(" Could not install spawn agents: {}", e).yellow()
379 );
380 println!(" You can install them later with: scud config spawn-agents add --all");
381 }
382
383 if let Err(e) = update_claude_md(&storage) {
385 println!(
386 "{}",
387 format!(" Could not update CLAUDE.md: {}", e).yellow()
388 );
389 }
390
391 println!("\n{}", "Configuration:".blue());
392 println!(
393 " Default Provider: {} ({})",
394 config.llm.provider.yellow(),
395 config.llm.model.yellow()
396 );
397 println!(
398 " Fast Provider: {} ({})",
399 config.llm.fast_provider.yellow(),
400 config.llm.fast_model.yellow()
401 );
402 println!(
403 " Smart Provider: {} ({})",
404 config.llm.smart_provider.yellow(),
405 config.llm.smart_model.yellow()
406 );
407 if config.requires_api_key() {
408 println!("\n{}", "Environment variables required:".blue());
409 let mut env_vars = std::collections::HashSet::new();
410 env_vars.insert(config.api_key_env_var());
411 if config.llm.fast_provider != config.llm.provider {
412 env_vars.insert(Config::api_key_env_var_for_provider(
413 &config.llm.fast_provider,
414 ));
415 }
416 if config.llm.smart_provider != config.llm.provider
417 && config.llm.smart_provider != config.llm.fast_provider
418 {
419 env_vars.insert(Config::api_key_env_var_for_provider(
420 &config.llm.smart_provider,
421 ));
422 }
423 for env_var in env_vars {
424 if env_var != "NONE" {
425 println!(" export {}=your-api-key", env_var.yellow());
426 }
427 }
428 } else {
429 println!("\n{}", "No API keys required (using CLI tools)".green());
430 }
431 println!("\n{}", "Next steps:".blue());
432 println!(" 1. Set your API key environment variable");
433 println!(" 2. Run: scud tags");
434 println!(" 3. Create or import tasks, then use: /scud:next\n");
435
436 Ok(())
437}
438
439fn update_claude_md(storage: &Storage) -> Result<()> {
441 let claude_md_path = storage.project_root().join("CLAUDE.md");
442
443 let scud_section = r#"
444## SCUD Task Management
445
446This project uses SCUD for AI-driven task management.
447
448### Quick Start
449- `scud tags` - List available phases
450- `scud next` - Find next available task
451- `scud set-status <id> in-progress` - Claim a task
452- `scud view` - Open interactive task viewer
453
454### Slash Commands
455Use `/scud:` commands in Claude Code for task operations.
456"#;
457
458 let marker = "## SCUD Task Management";
459
460 if claude_md_path.exists() {
461 let content = fs::read_to_string(&claude_md_path)?;
462 if content.contains(marker) {
463 return Ok(()); }
465 let new_content = format!("{}\n{}", content.trim_end(), scud_section);
467 fs::write(&claude_md_path, new_content)?;
468 } else {
469 fs::write(&claude_md_path, scud_section.trim_start())?;
471 }
472
473 println!(" {} Updated CLAUDE.md with SCUD instructions", "✓".green());
474 Ok(())
475}