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 (could be customized later)
300        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        // Configure FAST model/provider
322        println!("{}", "=== FAST MODEL CONFIGURATION ===".yellow().bold());
323        let (fast_provider, fast_model) = configure_provider_and_model("fast")?;
324
325        // Configure SMART model/provider
326        println!();
327        println!("{}", "=== SMART MODEL CONFIGURATION ===".yellow().bold());
328        let (smart_provider, smart_model) = configure_provider_and_model("smart")?;
329
330        // Use fast provider/model as defaults for backward compatibility
331        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        // Non-interactive without provider arg: use default (claude-cli)
344        let provider = "claude-cli";
345        let model = Config::default_model_for_provider(provider);
346        // Use defaults for smart/fast
347        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    // Interactive backpressure configuration
376    if is_interactive() {
377        configure_backpressure_interactive(&storage)?;
378    }
379
380    println!("\n{}", "SCUD initialized successfully!".green().bold());
381
382    // Auto-install all agents and commands
383    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    // Update CLAUDE.md with SCUD instructions
390    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
445/// Update CLAUDE.md with SCUD instructions
446fn 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(()); // Already has SCUD section
470        }
471        // Append to existing file
472        let new_content = format!("{}\n{}", content.trim_end(), scud_section);
473        fs::write(&claude_md_path, new_content)?;
474    } else {
475        // Create new file
476        fs::write(&claude_md_path, scud_section.trim_start())?;
477    }
478
479    println!("  {} Updated CLAUDE.md with SCUD instructions", "✓".green());
480    Ok(())
481}