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 == "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!(
88        "{}",
89        "to catch build/test failures early.".dimmed()
90    );
91    println!();
92
93    // Get auto-detected commands
94    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            // Auto-detect - nothing to save, that's the default
119            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            // Custom configuration
130            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            // Skip
136            println!(
137                "{}",
138                "  Skipped - configure later with: scud config backpressure".dimmed()
139            );
140        }
141        _ => {}
142    }
143
144    Ok(())
145}
146
147/// Configure custom backpressure commands interactively
148fn configure_backpressure_commands(auto_detected: &[String]) -> Result<Vec<String>> {
149    println!();
150    println!("{}", "Common validation commands:".blue());
151
152    // Build a list of common commands based on what might be useful
153    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    // Mark auto-detected as selected by default
170    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    // Allow adding custom commands
191    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
227/// Save backpressure configuration to config file
228fn save_backpressure_config(storage: &Storage, commands: &[String]) -> Result<()> {
229    let config_path = storage.config_file();
230
231    // Load existing config
232    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    // Ensure swarm section exists
239    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    // Create backpressure section
253    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    // Save
265    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        // Non-interactive mode with command-line argument - use defaults for all tiers
288        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        // Use defaults for smart/fast from Config (respects env vars)
300        let defaults = Config::default();
301        (
302            provider,
303            model,
304            defaults.llm.smart_provider,
305            defaults.llm.smart_model,
306            defaults.llm.fast_provider,
307            defaults.llm.fast_model,
308        )
309    } else if is_interactive() {
310        println!(
311            "{}",
312            "SCUD supports separate models for different types of tasks:".blue()
313        );
314        println!("  • Fast models: Quick coding, generation tasks");
315        println!("  • Smart models: Complex reasoning, analysis, validation");
316        println!();
317
318        // Configure FAST model/provider
319        println!("{}", "=== FAST MODEL CONFIGURATION ===".yellow().bold());
320        let (fast_provider, fast_model) = configure_provider_and_model("fast")?;
321
322        // Configure SMART model/provider
323        println!();
324        println!("{}", "=== SMART MODEL CONFIGURATION ===".yellow().bold());
325        let (smart_provider, smart_model) = configure_provider_and_model("smart")?;
326
327        // Use fast provider/model as defaults for backward compatibility
328        let provider = fast_provider.clone();
329        let model = fast_model.clone();
330
331        (
332            provider,
333            model,
334            smart_provider,
335            smart_model,
336            fast_provider,
337            fast_model,
338        )
339    } else {
340        // Non-interactive without provider arg: use defaults from Config (respects env vars)
341        let defaults = Config::default();
342        (
343            defaults.llm.provider,
344            defaults.llm.model,
345            defaults.llm.smart_provider,
346            defaults.llm.smart_model,
347            defaults.llm.fast_provider,
348            defaults.llm.fast_model,
349        )
350    };
351
352    let config = Config {
353        llm: LLMConfig {
354            provider,
355            model,
356            smart_provider,
357            smart_model,
358            fast_provider,
359            fast_model,
360            max_tokens: 16000,
361        },
362    };
363
364    storage.initialize_with_config(&config)?;
365
366    // Interactive backpressure configuration
367    if is_interactive() {
368        configure_backpressure_interactive(&storage)?;
369    }
370
371    println!("\n{}", "SCUD initialized successfully!".green().bold());
372
373    // Auto-install all agents and commands
374    println!("\n{}", "Installing SCUD agents and commands...".blue());
375    if let Err(e) = config_cmd::agents_add(Some(storage.project_root().to_path_buf()), None, true) {
376        println!("{}", format!("  Could not install agents: {}", e).yellow());
377        println!("  You can install them later with: scud config agents add --all");
378    }
379
380    // Update CLAUDE.md with SCUD instructions
381    if let Err(e) = update_claude_md(&storage) {
382        println!(
383            "{}",
384            format!("  Could not update CLAUDE.md: {}", e).yellow()
385        );
386    }
387
388    println!("\n{}", "Configuration:".blue());
389    println!(
390        "  Default Provider: {} ({})",
391        config.llm.provider.yellow(),
392        config.llm.model.yellow()
393    );
394    println!(
395        "  Fast Provider: {} ({})",
396        config.llm.fast_provider.yellow(),
397        config.llm.fast_model.yellow()
398    );
399    println!(
400        "  Smart Provider: {} ({})",
401        config.llm.smart_provider.yellow(),
402        config.llm.smart_model.yellow()
403    );
404    if config.requires_api_key() {
405        println!("\n{}", "Environment variables required:".blue());
406        let mut env_vars = std::collections::HashSet::new();
407        env_vars.insert(config.api_key_env_var());
408        if config.llm.fast_provider != config.llm.provider {
409            env_vars.insert(Config::api_key_env_var_for_provider(
410                &config.llm.fast_provider,
411            ));
412        }
413        if config.llm.smart_provider != config.llm.provider
414            && config.llm.smart_provider != config.llm.fast_provider
415        {
416            env_vars.insert(Config::api_key_env_var_for_provider(
417                &config.llm.smart_provider,
418            ));
419        }
420        for env_var in env_vars {
421            if env_var != "NONE" {
422                println!("  export {}=your-api-key", env_var.yellow());
423            }
424        }
425    } else {
426        println!("\n{}", "No API keys required (using CLI tools)".green());
427    }
428    println!("\n{}", "Next steps:".blue());
429    println!("  1. Set your API key environment variable");
430    println!("  2. Run: scud tags");
431    println!("  3. Create or import tasks, then use: /scud:next\n");
432
433    Ok(())
434}
435
436/// Update CLAUDE.md with SCUD instructions
437fn update_claude_md(storage: &Storage) -> Result<()> {
438    let claude_md_path = storage.project_root().join("CLAUDE.md");
439
440    let scud_section = r#"
441## SCUD Task Management
442
443This project uses SCUD for AI-driven task management.
444
445### Quick Start
446- `scud tags` - List available phases
447- `scud next` - Find next available task
448- `scud set-status <id> in-progress` - Claim a task
449- `scud view` - Open interactive task viewer
450
451### Slash Commands
452Use `/scud:` commands in Claude Code for task operations.
453"#;
454
455    let marker = "## SCUD Task Management";
456
457    if claude_md_path.exists() {
458        let content = fs::read_to_string(&claude_md_path)?;
459        if content.contains(marker) {
460            return Ok(()); // Already has SCUD section
461        }
462        // Append to existing file
463        let new_content = format!("{}\n{}", content.trim_end(), scud_section);
464        fs::write(&claude_md_path, new_content)?;
465    } else {
466        // Create new file
467        fs::write(&claude_md_path, scud_section.trim_start())?;
468    }
469
470    println!("  {} Updated CLAUDE.md with SCUD instructions", "✓".green());
471    Ok(())
472}