Skip to main content

scud/commands/
init.rs

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
13/// Helper function to configure provider and model for a specific tier
14fn 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 }) // Default xAI for fast, Claude for smart
27        .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    // Build model options: suggested models + "Custom" option
40    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        // User selected "Custom"
66        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
76/// Interactive backpressure configuration during init
77fn 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    // Get auto-detected commands
91    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            // Auto-detect - nothing to save, that's the default
116            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            // Custom configuration
127            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            // Skip
133            println!(
134                "{}",
135                "  Skipped - configure later with: scud config backpressure".dimmed()
136            );
137        }
138        _ => {}
139    }
140
141    Ok(())
142}
143
144/// Configure custom backpressure commands interactively
145fn configure_backpressure_commands(auto_detected: &[String]) -> Result<Vec<String>> {
146    println!();
147    println!("{}", "Common validation commands:".blue());
148
149    // Build a list of common commands based on what might be useful
150    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    // Mark auto-detected as selected by default
167    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    // Allow adding custom commands
185    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
219/// Save backpressure configuration to config file
220fn save_backpressure_config(storage: &Storage, commands: &[String]) -> Result<()> {
221    let config_path = storage.config_file();
222
223    // Load existing config
224    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    // Ensure swarm section exists
231    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    // Create backpressure section
241    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    // Save
253    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        // Non-interactive mode with command-line argument - use defaults for all tiers
276        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        // Use defaults for smart/fast from Config (respects env vars)
288        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        // Configure FAST model/provider
307        println!("{}", "=== FAST MODEL CONFIGURATION ===".yellow().bold());
308        let (fast_provider, fast_model) = configure_provider_and_model("fast")?;
309
310        // Configure SMART model/provider
311        println!();
312        println!("{}", "=== SMART MODEL CONFIGURATION ===".yellow().bold());
313        let (smart_provider, smart_model) = configure_provider_and_model("smart")?;
314
315        // Use fast provider/model as defaults for backward compatibility
316        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        // Non-interactive without provider arg: use defaults from Config (respects env vars)
329        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        swarm: crate::config::SwarmConfig::default(),
351    };
352
353    storage.initialize_with_config(&config)?;
354
355    // Interactive backpressure configuration
356    if is_interactive() {
357        configure_backpressure_interactive(&storage)?;
358    }
359
360    println!("\n{}", "SCUD initialized successfully!".green().bold());
361
362    // Auto-install all agents and commands
363    println!("\n{}", "Installing SCUD agents and commands...".blue());
364    if let Err(e) = config_cmd::agents_add(Some(storage.project_root().to_path_buf()), None, true) {
365        println!("{}", format!("  Could not install agents: {}", e).yellow());
366        println!("  You can install them later with: scud config agents add --all");
367    }
368
369    // Install spawn agents (harness/model routing definitions)
370    println!("\n{}", "Installing spawn agent definitions...".blue());
371    if let Err(e) = config_cmd::spawn_agents_add(
372        Some(storage.project_root().to_path_buf()),
373        None,
374        true,
375        false,
376    ) {
377        println!(
378            "{}",
379            format!("  Could not install spawn agents: {}", e).yellow()
380        );
381        println!("  You can install them later with: scud config spawn-agents add --all");
382    }
383
384    // Update CLAUDE.md with SCUD instructions
385    if let Err(e) = update_claude_md(&storage) {
386        println!(
387            "{}",
388            format!("  Could not update CLAUDE.md: {}", e).yellow()
389        );
390    }
391
392    println!("\n{}", "Configuration:".blue());
393    println!(
394        "  Default Provider: {} ({})",
395        config.llm.provider.yellow(),
396        config.llm.model.yellow()
397    );
398    println!(
399        "  Fast Provider: {} ({})",
400        config.llm.fast_provider.yellow(),
401        config.llm.fast_model.yellow()
402    );
403    println!(
404        "  Smart Provider: {} ({})",
405        config.llm.smart_provider.yellow(),
406        config.llm.smart_model.yellow()
407    );
408    if config.requires_api_key() {
409        println!("\n{}", "Environment variables required:".blue());
410        let mut env_vars = std::collections::HashSet::new();
411        env_vars.insert(config.api_key_env_var());
412        if config.llm.fast_provider != config.llm.provider {
413            env_vars.insert(Config::api_key_env_var_for_provider(
414                &config.llm.fast_provider,
415            ));
416        }
417        if config.llm.smart_provider != config.llm.provider
418            && config.llm.smart_provider != config.llm.fast_provider
419        {
420            env_vars.insert(Config::api_key_env_var_for_provider(
421                &config.llm.smart_provider,
422            ));
423        }
424        for env_var in env_vars {
425            if env_var != "NONE" {
426                println!("  export {}=your-api-key", env_var.yellow());
427            }
428        }
429    } else {
430        println!("\n{}", "No API keys required (using CLI tools)".green());
431    }
432    println!("\n{}", "Next steps:".blue());
433    println!("  1. Set your API key environment variable");
434    println!("  2. Run: scud tags");
435    println!("  3. Create or import tasks, then use: /scud:next\n");
436
437    Ok(())
438}
439
440/// Update CLAUDE.md with SCUD instructions
441fn update_claude_md(storage: &Storage) -> Result<()> {
442    let claude_md_path = storage.project_root().join("CLAUDE.md");
443
444    let scud_section = r#"
445## SCUD Task Management
446
447This project uses SCUD for AI-driven task management.
448
449### Quick Start
450- `scud tags` - List available phases
451- `scud next` - Find next available task
452- `scud set-status <id> in-progress` - Claim a task
453- `scud view` - Open interactive task viewer
454
455### Slash Commands
456Use `/scud:` commands in Claude Code for task operations.
457"#;
458
459    let marker = "## SCUD Task Management";
460
461    if claude_md_path.exists() {
462        let content = fs::read_to_string(&claude_md_path)?;
463        if content.contains(marker) {
464            return Ok(()); // Already has SCUD section
465        }
466        // Append to existing file
467        let new_content = format!("{}\n{}", content.trim_end(), scud_section);
468        fs::write(&claude_md_path, new_content)?;
469    } else {
470        // Create new file
471        fs::write(&claude_md_path, scud_section.trim_start())?;
472    }
473
474    println!("  {} Updated CLAUDE.md with SCUD instructions", "✓".green());
475    Ok(())
476}