scud/commands/
init.rs

1use anyhow::Result;
2use colored::Colorize;
3use dialoguer::{Input, Select};
4use std::fs;
5use std::path::PathBuf;
6
7use crate::commands::config as config_cmd;
8use crate::commands::helpers::is_interactive;
9use crate::config::{Config, LLMConfig};
10use crate::storage::Storage;
11
12/// Helper function to configure provider and model for a specific tier
13fn configure_provider_and_model(tier: &str) -> Result<(String, String)> {
14    let providers = vec![
15        "Claude Code (recommended - no API key needed)",
16        "OpenAI Codex CLI (no API key needed)",
17        "xAI (Grok)",
18        "Anthropic (Claude API)",
19        "OpenAI (GPT API)",
20        "OpenRouter",
21    ];
22    let provider_selection = Select::new()
23        .with_prompt(&format!("Select {} LLM provider", tier))
24        .items(&providers)
25        .default(if tier == "fast" { 2 } else { 0 }) // Default xAI for fast, Claude for smart
26        .interact()?;
27
28    let provider = match provider_selection {
29        0 => "claude-cli",
30        1 => "codex",
31        2 => "xai",
32        3 => "anthropic",
33        4 => "openai",
34        5 => "openrouter",
35        _ => "claude-cli",
36    };
37
38    // Build model options: suggested models + "Custom" option
39    let suggested = Config::suggested_models_for_provider(provider);
40    let mut model_options: Vec<String> = suggested.iter().map(|s| s.to_string()).collect();
41    model_options.push("Custom (enter model name)".to_string());
42
43    let default_model_index = if tier == "fast" && provider == "xai" {
44        suggested
45            .iter()
46            .position(|m| *m == "grok-code-fast-1")
47            .unwrap_or(0)
48    } else if tier == "smart" && provider == "claude-cli" {
49        suggested.iter().position(|m| *m == "opus").unwrap_or(0)
50    } else {
51        0
52    };
53
54    let model_selection = Select::new()
55        .with_prompt(&format!(
56            "Select {} model (or choose Custom to enter any model)",
57            tier
58        ))
59        .items(&model_options)
60        .default(default_model_index)
61        .interact()?;
62
63    let model = if model_selection == model_options.len() - 1 {
64        // User selected "Custom"
65        Input::<String>::new()
66            .with_prompt("Enter model name")
67            .interact_text()?
68    } else {
69        suggested[model_selection].to_string()
70    };
71
72    Ok((provider.to_string(), model))
73}
74
75pub fn run(project_root: Option<PathBuf>, provider_arg: Option<String>) -> Result<()> {
76    let storage = Storage::new(project_root);
77
78    if storage.is_initialized() {
79        println!("{}", "✓ SCUD is already initialized".green());
80        return Ok(());
81    }
82
83    println!("{}", "Initializing SCUD...".blue());
84    println!();
85
86    let (provider, model, smart_provider, smart_model, fast_provider, fast_model) = if let Some(
87        provider_name,
88    ) =
89        provider_arg
90    {
91        // Non-interactive mode with command-line argument - use defaults for all tiers
92        let provider = provider_name.to_lowercase();
93        if !matches!(
94            provider.as_str(),
95            "xai" | "anthropic" | "openai" | "openrouter" | "claude-cli" | "codex"
96        ) {
97            anyhow::bail!(
98                "Invalid provider: {}. Valid options: claude-cli, codex, xai, anthropic, openai, openrouter",
99                provider
100            );
101        }
102        let model = Config::default_model_for_provider(&provider).to_string();
103        // Use defaults for smart/fast (could be customized later)
104        let smart_provider = "claude-cli".to_string();
105        let smart_model = "opus".to_string();
106        let fast_provider = "xai".to_string();
107        let fast_model = "grok-code-fast-1".to_string();
108        (
109            provider,
110            model,
111            smart_provider,
112            smart_model,
113            fast_provider,
114            fast_model,
115        )
116    } else if is_interactive() {
117        println!(
118            "{}",
119            "SCUD supports separate models for different types of tasks:".blue()
120        );
121        println!("  • Fast models: Quick coding, generation tasks");
122        println!("  • Smart models: Complex reasoning, analysis, validation");
123        println!();
124
125        // Configure FAST model/provider
126        println!("{}", "=== FAST MODEL CONFIGURATION ===".yellow().bold());
127        let (fast_provider, fast_model) = configure_provider_and_model("fast")?;
128
129        // Configure SMART model/provider
130        println!();
131        println!("{}", "=== SMART MODEL CONFIGURATION ===".yellow().bold());
132        let (smart_provider, smart_model) = configure_provider_and_model("smart")?;
133
134        // Use fast provider/model as defaults for backward compatibility
135        let provider = fast_provider.clone();
136        let model = fast_model.clone();
137
138        (
139            provider,
140            model,
141            smart_provider,
142            smart_model,
143            fast_provider,
144            fast_model,
145        )
146    } else {
147        // Non-interactive without provider arg: use default (claude-cli)
148        let provider = "claude-cli";
149        let model = Config::default_model_for_provider(provider);
150        // Use defaults for smart/fast
151        let smart_provider = "claude-cli".to_string();
152        let smart_model = "opus".to_string();
153        let fast_provider = "xai".to_string();
154        let fast_model = "grok-code-fast-1".to_string();
155        (
156            provider.to_string(),
157            model.to_string(),
158            smart_provider,
159            smart_model,
160            fast_provider,
161            fast_model,
162        )
163    };
164
165    let config = Config {
166        llm: LLMConfig {
167            provider,
168            model,
169            smart_provider,
170            smart_model,
171            fast_provider,
172            fast_model,
173            max_tokens: 16000,
174        },
175    };
176
177    storage.initialize_with_config(&config)?;
178
179    println!("\n{}", "SCUD initialized successfully!".green().bold());
180
181    // Auto-install all agents and commands
182    println!("\n{}", "Installing SCUD agents and commands...".blue());
183    if let Err(e) = config_cmd::agents_add(Some(storage.project_root().to_path_buf()), None, true) {
184        println!("{}", format!("  Could not install agents: {}", e).yellow());
185        println!("  You can install them later with: scud config agents add --all");
186    }
187
188    // Update CLAUDE.md with SCUD instructions
189    if let Err(e) = update_claude_md(&storage) {
190        println!(
191            "{}",
192            format!("  Could not update CLAUDE.md: {}", e).yellow()
193        );
194    }
195
196    println!("\n{}", "Configuration:".blue());
197    println!(
198        "  Default Provider: {} ({})",
199        config.llm.provider.yellow(),
200        config.llm.model.yellow()
201    );
202    println!(
203        "  Fast Provider: {} ({})",
204        config.llm.fast_provider.yellow(),
205        config.llm.fast_model.yellow()
206    );
207    println!(
208        "  Smart Provider: {} ({})",
209        config.llm.smart_provider.yellow(),
210        config.llm.smart_model.yellow()
211    );
212    if config.requires_api_key() {
213        println!("\n{}", "Environment variables required:".blue());
214        let mut env_vars = std::collections::HashSet::new();
215        env_vars.insert(config.api_key_env_var());
216        if config.llm.fast_provider != config.llm.provider {
217            env_vars.insert(Config::api_key_env_var_for_provider(
218                &config.llm.fast_provider,
219            ));
220        }
221        if config.llm.smart_provider != config.llm.provider
222            && config.llm.smart_provider != config.llm.fast_provider
223        {
224            env_vars.insert(Config::api_key_env_var_for_provider(
225                &config.llm.smart_provider,
226            ));
227        }
228        for env_var in env_vars {
229            if env_var != "NONE" {
230                println!("  export {}=your-api-key", env_var.yellow());
231            }
232        }
233    } else {
234        println!("\n{}", "No API keys required (using CLI tools)".green());
235    }
236    println!("\n{}", "Next steps:".blue());
237    println!("  1. Set your API key environment variable");
238    println!("  2. Run: scud tags");
239    println!("  3. Create or import tasks, then use: /scud:next\n");
240
241    Ok(())
242}
243
244/// Update CLAUDE.md with SCUD instructions
245fn update_claude_md(storage: &Storage) -> Result<()> {
246    let claude_md_path = storage.project_root().join("CLAUDE.md");
247
248    let scud_section = r#"
249## SCUD Task Management
250
251This project uses SCUD for AI-driven task management.
252
253### Quick Start
254- `scud tags` - List available phases
255- `scud next` - Find next available task
256- `scud set-status <id> in-progress` - Claim a task
257- `scud view` - Open interactive task viewer
258
259### Slash Commands
260Use `/scud:` commands in Claude Code for task operations.
261"#;
262
263    let marker = "## SCUD Task Management";
264
265    if claude_md_path.exists() {
266        let content = fs::read_to_string(&claude_md_path)?;
267        if content.contains(marker) {
268            return Ok(()); // Already has SCUD section
269        }
270        // Append to existing file
271        let new_content = format!("{}\n{}", content.trim_end(), scud_section);
272        fs::write(&claude_md_path, new_content)?;
273    } else {
274        // Create new file
275        fs::write(&claude_md_path, scud_section.trim_start())?;
276    }
277
278    println!("  {} Updated CLAUDE.md with SCUD instructions", "✓".green());
279    Ok(())
280}