Skip to main content

scud/commands/
config.rs

1use anyhow::Result;
2use colored::Colorize;
3use std::fs;
4use std::path::PathBuf;
5
6use crate::config::Config;
7use crate::storage::Storage;
8
9/// Embedded SCUD command definitions
10/// Each command has a filename and content
11/// Commands are stored in .claude/commands/scud/<filename>.md
12const EMBEDDED_SCUD_COMMANDS: &[(&str, &str)] = &[
13    ("stats", include_str!("../../assets/commands/scud/stats.md")),
14    ("next", include_str!("../../assets/commands/scud/next.md")),
15    ("show", include_str!("../../assets/commands/scud/show.md")),
16    ("list", include_str!("../../assets/commands/scud/list.md")),
17    ("waves", include_str!("../../assets/commands/scud/waves.md")),
18    (
19        "status",
20        include_str!("../../assets/commands/scud/status.md"),
21    ),
22];
23
24/// Embedded SCUD skill definitions
25/// Skills are stored in .claude/skills/<skill-name>/SKILL.md
26const EMBEDDED_SCUD_SKILLS: &[(&str, &str)] = &[
27    (
28        "scud-tasks",
29        include_str!("../../assets/skills/scud-tasks/SKILL.md"),
30    ),
31    ("scud", include_str!("../../assets/skills/scud/SKILL.md")),
32];
33
34/// Embedded SCUD spawn agent definitions
35/// Agent definitions for model routing (stored in .scud/agents/<name>.toml)
36const EMBEDDED_SPAWN_AGENTS: &[(&str, &str)] = &[
37    (
38        "builder",
39        include_str!("../assets/spawn-agents/builder.toml"),
40    ),
41    (
42        "reviewer",
43        include_str!("../assets/spawn-agents/reviewer.toml"),
44    ),
45    (
46        "planner",
47        include_str!("../assets/spawn-agents/planner.toml"),
48    ),
49    (
50        "researcher",
51        include_str!("../assets/spawn-agents/researcher.toml"),
52    ),
53    (
54        "analyzer",
55        include_str!("../assets/spawn-agents/analyzer.toml"),
56    ),
57    (
58        "fast-builder",
59        include_str!("../assets/spawn-agents/fast-builder.toml"),
60    ),
61    (
62        "outside-generalist",
63        include_str!("../assets/spawn-agents/outside-generalist.toml"),
64    ),
65    (
66        "repairer",
67        include_str!("../assets/spawn-agents/repairer.toml"),
68    ),
69];
70
71/// SCUD agent definitions (legacy - keeping for compatibility)
72/// Each agent has a filename, aliases for CLI, and description
73/// Agents are stored in .claude/commands/scud/<filename>.md
74const SCUD_AGENTS: &[(&str, &[&str], &str)] = &[
75    (
76        "pm",
77        &["pm", "scud-pm"],
78        "Product Manager - PRD creation and requirements",
79    ),
80    (
81        "sm",
82        &["sm", "scud-sm"],
83        "Scrum Master - Task breakdown and planning",
84    ),
85    (
86        "architect",
87        &["architect", "scud-architect"],
88        "Architect - Technical design",
89    ),
90    (
91        "dev",
92        &["dev", "scud-dev"],
93        "Developer - Task implementation",
94    ),
95    (
96        "retrospective",
97        &["retrospective", "scud-retrospective"],
98        "Retrospective - Post-phase analysis",
99    ),
100    ("status", &["status"], "Status - Workflow status reporting"),
101];
102
103/// SCUD skill definitions
104/// Each skill is a directory containing SKILL.md and supporting files
105/// Skills are stored in .claude/skills/<skill-name>/
106const SCUD_SKILLS: &[(&str, &[&str], &str)] = &[
107    (
108        "scud-tasks",
109        &["scud-tasks", "tasks"],
110        "Task management - view, update, claim, and track tasks",
111    ),
112    (
113        "scud",
114        &["scud", "guide"],
115        "SCUD CLI usage guide - list, waves, tags, next, log, etc",
116    ),
117];
118
119/// OpenCode command definitions
120/// These are the same commands but for OpenCode
121/// Commands are stored in .opencode/command/
122const OPENCODE_COMMANDS: &[&str] = &[
123    "task-list",
124    "task-next",
125    "task-show",
126    "task-status",
127    "task-claim",
128    "task-release",
129    "task-waves",
130    "task-stats",
131    "task-whois",
132    "task-tags",
133    "task-doctor",
134];
135
136/// OpenCode hook definitions
137const OPENCODE_HOOKS: &[&str] = &["session-start"];
138
139/// OpenCode tool definitions
140const OPENCODE_TOOLS: &[&str] = &["find_skills", "use_skill"];
141
142pub fn show(project_root: Option<PathBuf>) -> Result<()> {
143    let storage = Storage::new(project_root);
144
145    if !storage.is_initialized() {
146        println!("{}", "✗ SCUD is not initialized".red());
147        println!("Run: scud init");
148        return Ok(());
149    }
150
151    let config = storage.load_config()?;
152
153    println!("{}", "Current Configuration:".blue().bold());
154    println!();
155    println!("  {}: {}", "Provider".yellow(), config.llm.provider);
156    println!("  {}: {}", "Model".yellow(), config.llm.model);
157    println!("  {}: {}", "Max Tokens".yellow(), config.llm.max_tokens);
158    println!();
159    println!("{}", "Environment Variable:".blue().bold());
160    println!("  {}: {}", "Required".yellow(), config.api_key_env_var());
161
162    // Check if API key is set
163    match std::env::var(config.api_key_env_var()) {
164        Ok(key) => {
165            let masked = format!(
166                "{}...{}",
167                &key[..10.min(key.len())],
168                &key[key.len().saturating_sub(4)..]
169            );
170            println!(
171                "  {}: {} {}",
172                "Status".yellow(),
173                "Set".green(),
174                masked.dimmed()
175            );
176        }
177        Err(_) => {
178            println!(
179                "  {}: {} (run: export {}=your-key)",
180                "Status".yellow(),
181                "Not Set".red(),
182                config.api_key_env_var()
183            );
184        }
185    }
186
187    println!();
188    println!("{}", "Config File:".blue().bold());
189    println!("  {}", storage.config_file().display().to_string().dimmed());
190
191    Ok(())
192}
193
194pub fn set_provider(
195    project_root: Option<PathBuf>,
196    provider: &str,
197    model: Option<String>,
198) -> Result<()> {
199    let storage = Storage::new(project_root);
200
201    if !storage.is_initialized() {
202        anyhow::bail!("SCUD is not initialized. Run: scud init");
203    }
204
205    // Validate provider
206    let provider = provider.to_lowercase();
207    if !matches!(
208        provider.as_str(),
209        "xai" | "anthropic" | "openai" | "openrouter" | "claude-cli"
210    ) {
211        anyhow::bail!(
212            "Invalid provider: {}. Valid options: xai, anthropic, openai, openrouter, claude-cli",
213            provider
214        );
215    }
216
217    let mut config = storage.load_config()?;
218    config.llm.provider = provider.clone();
219
220    // Set model - use provided or default for provider
221    config.llm.model =
222        model.unwrap_or_else(|| Config::default_model_for_provider(&provider).to_string());
223
224    // Save config
225    config.save(&storage.config_file())?;
226
227    println!("{}", "✅ Configuration updated!".green().bold());
228    println!();
229    println!("  {}: {}", "Provider".yellow(), config.llm.provider);
230    println!("  {}: {}", "Model".yellow(), config.llm.model);
231    println!();
232
233    if config.requires_api_key() {
234        println!("{}", "Remember to set your API key:".blue());
235        println!(
236            "  export {}=your-api-key",
237            config.api_key_env_var().yellow()
238        );
239    } else {
240        println!("{}", "Using Claude CLI (no API key required)".green());
241        println!(
242            "{}",
243            "Make sure 'claude' command is available in your PATH".blue()
244        );
245    }
246
247    Ok(())
248}
249
250/// Normalize agent name - accepts aliases like scud-pm, pm, architect, etc.
251fn normalize_agent_name(name: &str) -> Option<&'static str> {
252    let name_lower = name.to_lowercase();
253    for (filename, aliases, _) in SCUD_AGENTS {
254        for alias in *aliases {
255            if name_lower == *alias {
256                return Some(filename);
257            }
258        }
259    }
260    None
261}
262
263/// Normalize skill name - accepts aliases like scud-tasks, tasks, etc.
264fn normalize_skill_name(name: &str) -> Option<&'static str> {
265    let name_lower = name.to_lowercase();
266    for (dirname, aliases, _) in SCUD_SKILLS {
267        for alias in *aliases {
268            if name_lower == *alias {
269                return Some(dirname);
270            }
271        }
272    }
273    None
274}
275
276/// Get the scud commands directory path (.claude/commands/scud/)
277fn get_scud_commands_dir(project_root: Option<PathBuf>) -> PathBuf {
278    let base = project_root.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
279    base.join(".claude").join("commands").join("scud")
280}
281
282/// Get the skills directory path (.claude/skills/)
283fn get_skills_dir(project_root: Option<PathBuf>) -> PathBuf {
284    let base = project_root.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
285    base.join(".claude").join("skills")
286}
287
288/// Get the OpenCode command directory path (.opencode/command/)
289fn get_opencode_command_dir(project_root: Option<PathBuf>) -> PathBuf {
290    let base = project_root.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
291    base.join(".opencode").join("command")
292}
293
294/// Get the OpenCode hook directory path (.opencode/hook/)
295fn get_opencode_hook_dir(project_root: Option<PathBuf>) -> PathBuf {
296    let base = project_root.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
297    base.join(".opencode").join("hook")
298}
299
300/// Get the OpenCode tool directory path (.opencode/tool/)
301fn get_opencode_tool_dir(project_root: Option<PathBuf>) -> PathBuf {
302    let base = project_root.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
303    base.join(".opencode").join("tool")
304}
305
306/// Get the OpenCode skills directory path (.opencode/skills/)
307fn get_opencode_skills_dir(project_root: Option<PathBuf>) -> PathBuf {
308    let base = project_root.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
309    base.join(".opencode").join("skills")
310}
311
312// Package directory functions removed - now using embedded files
313
314/// List installed SCUD agents
315pub fn agents_list(project_root: Option<PathBuf>) -> Result<()> {
316    let scud_dir = get_scud_commands_dir(project_root.clone());
317    let skills_dir = get_skills_dir(project_root.clone());
318
319    // Agents section
320    println!("{}", "SCUD Workflow Agents".blue().bold());
321    println!("{}", "Location: .claude/commands/scud/".dimmed());
322    println!();
323
324    let mut agents_installed = 0;
325    let mut agents_not_installed = 0;
326
327    for (filename, aliases, description) in SCUD_AGENTS {
328        let agent_file = scud_dir.join(format!("{}.md", filename));
329        let installed = agent_file.exists();
330        let alias_str = aliases.join(", ");
331
332        if installed {
333            agents_installed += 1;
334            println!(
335                "  {} {} ({}) - {}",
336                "✓".green(),
337                filename.green(),
338                alias_str.dimmed(),
339                description
340            );
341        } else {
342            agents_not_installed += 1;
343            println!(
344                "  {} {} ({}) - {}",
345                "✗".red(),
346                filename.dimmed(),
347                alias_str.dimmed(),
348                description
349            );
350        }
351    }
352
353    println!();
354    println!(
355        "{} installed, {} not installed",
356        agents_installed.to_string().green(),
357        agents_not_installed.to_string().yellow()
358    );
359
360    // Skills section
361    println!();
362    println!("{}", "SCUD Skills".blue().bold());
363    println!("{}", "Location: .claude/skills/".dimmed());
364    println!();
365
366    let mut skills_installed = 0;
367    let mut skills_not_installed = 0;
368
369    for (dirname, aliases, description) in SCUD_SKILLS {
370        let skill_dir = skills_dir.join(dirname);
371        let skill_file = skill_dir.join("SKILL.md");
372        let installed = skill_file.exists();
373        let alias_str = aliases.join(", ");
374
375        if installed {
376            skills_installed += 1;
377            println!(
378                "  {} {} ({}) - {}",
379                "✓".green(),
380                dirname.green(),
381                alias_str.dimmed(),
382                description
383            );
384        } else {
385            skills_not_installed += 1;
386            println!(
387                "  {} {} ({}) - {}",
388                "✗".red(),
389                dirname.dimmed(),
390                alias_str.dimmed(),
391                description
392            );
393        }
394    }
395
396    println!();
397    println!(
398        "{} installed, {} not installed",
399        skills_installed.to_string().green(),
400        skills_not_installed.to_string().yellow()
401    );
402
403    // OpenCode section
404    println!();
405    println!("{}", "OpenCode Integration".blue().bold());
406    println!("{}", "Location: .opencode/".dimmed());
407    println!();
408
409    let opencode_cmd_dir = get_opencode_command_dir(project_root.clone());
410    let opencode_hook_dir = get_opencode_hook_dir(project_root.clone());
411    let opencode_tool_dir = get_opencode_tool_dir(project_root);
412
413    let mut opencode_installed = 0;
414
415    // Check commands
416    for cmd in OPENCODE_COMMANDS {
417        let cmd_file = opencode_cmd_dir.join(format!("{}.md", cmd));
418        if cmd_file.exists() {
419            opencode_installed += 1;
420        }
421    }
422
423    // Check hooks
424    for hook in OPENCODE_HOOKS {
425        let hook_file = opencode_hook_dir.join(format!("{}.md", hook));
426        if hook_file.exists() {
427            opencode_installed += 1;
428        }
429    }
430
431    // Check tools
432    for tool in OPENCODE_TOOLS {
433        let tool_file = opencode_tool_dir.join(format!("{}.json", tool));
434        if tool_file.exists() {
435            opencode_installed += 1;
436        }
437    }
438
439    if opencode_installed > 0 {
440        println!(
441            "  {} {} commands, {} hooks, {} tools installed",
442            "✓".green(),
443            OPENCODE_COMMANDS
444                .iter()
445                .filter(|c| opencode_cmd_dir.join(format!("{}.md", c)).exists())
446                .count(),
447            OPENCODE_HOOKS
448                .iter()
449                .filter(|h| opencode_hook_dir.join(format!("{}.md", h)).exists())
450                .count(),
451            OPENCODE_TOOLS
452                .iter()
453                .filter(|t| opencode_tool_dir.join(format!("{}.json", t)).exists())
454                .count(),
455        );
456    } else {
457        println!("  {} Not installed", "✗".red());
458    }
459
460    println!();
461    println!("{}", "Usage:".blue().bold());
462    println!("  scud config agents add <name>     Add an agent or skill");
463    println!("  scud config agents add --all      Add all agents, skills, and OpenCode support");
464    println!("  scud config agents remove <name>  Remove an agent or skill");
465    println!("  scud config agents remove --all   Remove all agents, skills, and OpenCode support");
466
467    Ok(())
468}
469
470/// Add SCUD agent(s), skill(s), and OpenCode integration
471pub fn agents_add(project_root: Option<PathBuf>, name: Option<String>, all: bool) -> Result<()> {
472    if !all && name.is_none() {
473        anyhow::bail!("Please specify an agent/skill name or use --all to add all");
474    }
475
476    // No longer need package directories - using embedded files
477    let scud_dir = get_scud_commands_dir(project_root.clone());
478    let skills_dir = get_skills_dir(project_root.clone());
479    let opencode_cmd_dir = get_opencode_command_dir(project_root.clone());
480    let opencode_hook_dir = get_opencode_hook_dir(project_root.clone());
481    let opencode_tool_dir = get_opencode_tool_dir(project_root.clone());
482    let opencode_skills_dir = get_opencode_skills_dir(project_root);
483
484    // Ensure directories exist
485    fs::create_dir_all(&scud_dir)?;
486    fs::create_dir_all(&skills_dir)?;
487
488    let mut agents_added = 0;
489    let mut agents_already_exist = 0;
490    let mut skills_added = 0;
491    let mut skills_already_exist = 0;
492    let mut opencode_added = 0;
493    let mut opencode_already_exist = 0;
494
495    // Determine what to add
496    let (agents_to_add, skills_to_add): (Vec<&str>, Vec<&str>) = if all {
497        (
498            EMBEDDED_SCUD_COMMANDS
499                .iter()
500                .map(|(name, _)| *name)
501                .collect(),
502            EMBEDDED_SCUD_SKILLS.iter().map(|(name, _)| *name).collect(),
503        )
504    } else {
505        let name_ref = name.as_ref().unwrap();
506        // Try agent first, then skill
507        if EMBEDDED_SCUD_COMMANDS.iter().any(|(n, _)| *n == name_ref) {
508            (vec![name_ref], vec![])
509        } else if EMBEDDED_SCUD_SKILLS.iter().any(|(n, _)| *n == name_ref) {
510            (vec![], vec![name_ref])
511        } else {
512            anyhow::bail!(
513                "Unknown agent/skill: '{}'. Valid agents: {}. Valid skills: {}",
514                name_ref,
515                EMBEDDED_SCUD_COMMANDS
516                    .iter()
517                    .map(|(n, _)| *n)
518                    .collect::<Vec<_>>()
519                    .join(", "),
520                EMBEDDED_SCUD_SKILLS
521                    .iter()
522                    .map(|(n, _)| *n)
523                    .collect::<Vec<_>>()
524                    .join(", ")
525            );
526        }
527    };
528
529    // Add agents (using embedded content)
530    if !agents_to_add.is_empty() {
531        println!("{}", "Agents:".blue().bold());
532        for agent_name in &agents_to_add {
533            let dest = scud_dir.join(format!("{}.md", agent_name));
534
535            if dest.exists() {
536                agents_already_exist += 1;
537                println!("  {} {} (already installed)", "·".yellow(), agent_name);
538                continue;
539            }
540
541            // Find embedded content
542            if let Some((_, content)) = EMBEDDED_SCUD_COMMANDS
543                .iter()
544                .find(|(n, _)| *n == *agent_name)
545            {
546                fs::write(&dest, content)?;
547                agents_added += 1;
548                println!("  {} {}", "✓".green(), agent_name.green());
549            } else {
550                println!(
551                    "  {} {} (embedded content not found)",
552                    "✗".red(),
553                    agent_name
554                );
555            }
556        }
557    }
558
559    // Add skills (using embedded content)
560    if !skills_to_add.is_empty() {
561        println!("{}", "Skills:".blue().bold());
562        for skill_name in &skills_to_add {
563            let dest = skills_dir.join(skill_name);
564            let skill_file = dest.join("SKILL.md");
565
566            if skill_file.exists() {
567                skills_already_exist += 1;
568                println!("  {} {} (already installed)", "·".yellow(), skill_name);
569                continue;
570            }
571
572            // Find embedded content
573            if let Some((_, content)) = EMBEDDED_SCUD_SKILLS.iter().find(|(n, _)| *n == *skill_name)
574            {
575                fs::create_dir_all(&dest)?;
576                fs::write(&skill_file, content)?;
577                skills_added += 1;
578                println!("  {} {}", "✓".green(), skill_name.green());
579
580                // Also copy skill to OpenCode skills directory
581                let opencode_dest = opencode_skills_dir.join(skill_name);
582                let opencode_skill_file = opencode_dest.join("SKILL.md");
583                if !opencode_skill_file.exists() {
584                    fs::create_dir_all(&opencode_dest)?;
585                    fs::write(&opencode_skill_file, content)?;
586                }
587            } else {
588                println!(
589                    "  {} {} (embedded content not found)",
590                    "✗".red(),
591                    skill_name
592                );
593            }
594        }
595    }
596
597    // Add OpenCode integration (only when --all) - using embedded SCUD commands
598    if all {
599        println!("{}", "OpenCode:".blue().bold());
600
601        // Ensure OpenCode directories exist
602        fs::create_dir_all(&opencode_cmd_dir)?;
603        fs::create_dir_all(&opencode_hook_dir)?;
604        fs::create_dir_all(&opencode_tool_dir)?;
605
606        // Add commands (using embedded SCUD commands)
607        for cmd in OPENCODE_COMMANDS {
608            let dest = opencode_cmd_dir.join(format!("{}.md", cmd));
609
610            if dest.exists() {
611                opencode_already_exist += 1;
612                continue;
613            }
614
615            // Map OpenCode command names to embedded SCUD commands
616            let embedded_name = match *cmd {
617                "task-list" => "list",
618                "task-next" => "next",
619                "task-show" => "show",
620                "task-status" => "status",
621                "task-claim" => "status",   // closest match
622                "task-release" => "status", // closest match
623                "task-waves" => "waves",
624                "task-stats" => "stats",
625                "task-whois" => "status",  // closest match
626                "task-tags" => "status",   // closest match
627                "task-doctor" => "status", // closest match
628                _ => continue,
629            };
630
631            if let Some((_, content)) = EMBEDDED_SCUD_COMMANDS
632                .iter()
633                .find(|(n, _)| *n == embedded_name)
634            {
635                fs::write(&dest, content)?;
636                opencode_added += 1;
637            }
638        }
639
640        // Add hooks (simplified - just create empty hook files for now)
641        for hook in OPENCODE_HOOKS {
642            let dest = opencode_hook_dir.join(format!("{}.md", hook));
643
644            if dest.exists() {
645                opencode_already_exist += 1;
646                continue;
647            }
648
649            // Create basic hook content
650            let hook_content = "# Session Start Hook\n\nThis hook runs when an OpenCode session starts.\n\n```bash\nscud warmup\n```".to_string();
651            fs::write(&dest, hook_content)?;
652            opencode_added += 1;
653        }
654
655        // Add tools (create basic tool definitions)
656        for tool in OPENCODE_TOOLS {
657            let dest = opencode_tool_dir.join(format!("{}.json", tool));
658
659            if dest.exists() {
660                opencode_already_exist += 1;
661                continue;
662            }
663
664            // Create basic tool definitions
665            let tool_content = match *tool {
666                "find_skills" => {
667                    r#"{
668  "name": "find_skills",
669  "description": "Find available skills in the codebase",
670  "inputSchema": {
671    "type": "object",
672    "properties": {
673      "query": {
674        "type": "string",
675        "description": "Search query for skills"
676      }
677    }
678  }
679}"#
680                }
681                "use_skill" => {
682                    r#"{
683  "name": "use_skill",
684  "description": "Use a specific skill",
685  "inputSchema": {
686    "type": "object",
687    "properties": {
688      "skill_name": {
689        "type": "string",
690        "description": "Name of the skill to use"
691      },
692      "parameters": {
693        "type": "object",
694        "description": "Parameters for the skill"
695      }
696    }
697  }
698}"#
699                }
700                _ => continue,
701            };
702
703            fs::write(&dest, tool_content)?;
704            opencode_added += 1;
705        }
706
707        if opencode_added > 0 {
708            println!("  {} {} files installed", "✓".green(), opencode_added);
709        }
710        if opencode_already_exist > 0 {
711            println!(
712                "  {} {} files already installed",
713                "·".yellow(),
714                opencode_already_exist
715            );
716        }
717    }
718
719    println!();
720    let total_added = agents_added + skills_added + opencode_added;
721    let total_existing = agents_already_exist + skills_already_exist + opencode_already_exist;
722
723    if total_added > 0 {
724        println!(
725            "{}",
726            format!("✅ Added {} item(s)", total_added).green().bold()
727        );
728    }
729    if total_existing > 0 {
730        println!(
731            "{}",
732            format!("{} item(s) already installed", total_existing).yellow()
733        );
734    }
735
736    Ok(())
737}
738
739/// Recursively remove a directory
740fn remove_dir_recursive(path: &PathBuf) -> Result<()> {
741    if path.exists() {
742        fs::remove_dir_all(path)?;
743    }
744    Ok(())
745}
746
747/// Remove SCUD agent(s), skill(s), and OpenCode integration
748pub fn agents_remove(project_root: Option<PathBuf>, name: Option<String>, all: bool) -> Result<()> {
749    if !all && name.is_none() {
750        anyhow::bail!("Please specify an agent/skill name or use --all to remove all");
751    }
752
753    let scud_dir = get_scud_commands_dir(project_root.clone());
754    let skills_dir = get_skills_dir(project_root.clone());
755    let opencode_cmd_dir = get_opencode_command_dir(project_root.clone());
756    let opencode_hook_dir = get_opencode_hook_dir(project_root.clone());
757    let opencode_tool_dir = get_opencode_tool_dir(project_root.clone());
758    let opencode_skills_dir = get_opencode_skills_dir(project_root);
759
760    let mut agents_removed = 0;
761    let mut agents_not_found = 0;
762    let mut skills_removed = 0;
763    let mut skills_not_found = 0;
764    let mut opencode_removed = 0;
765
766    // Determine what to remove
767    let (agents_to_remove, skills_to_remove): (Vec<&str>, Vec<&str>) = if all {
768        (
769            SCUD_AGENTS
770                .iter()
771                .map(|(filename, _, _)| *filename)
772                .collect(),
773            SCUD_SKILLS.iter().map(|(dirname, _, _)| *dirname).collect(),
774        )
775    } else {
776        let name_ref = name.as_ref().unwrap();
777        // Try agent first, then skill
778        if let Some(agent) = normalize_agent_name(name_ref) {
779            (vec![agent], vec![])
780        } else if let Some(skill) = normalize_skill_name(name_ref) {
781            (vec![], vec![skill])
782        } else {
783            anyhow::bail!(
784                "Unknown agent/skill: '{}'. Valid agents: pm, sm, architect, dev, retrospective, status. Valid skills: scud-tasks",
785                name_ref
786            );
787        }
788    };
789
790    // Remove agents
791    if !agents_to_remove.is_empty() {
792        println!("{}", "Agents:".blue().bold());
793        for agent_name in &agents_to_remove {
794            let agent_file = scud_dir.join(format!("{}.md", agent_name));
795
796            if !agent_file.exists() {
797                agents_not_found += 1;
798                println!("  {} {} (not installed)", "·".yellow(), agent_name);
799                continue;
800            }
801
802            fs::remove_file(&agent_file)?;
803            agents_removed += 1;
804            println!("  {} {}", "✓".green(), agent_name);
805        }
806    }
807
808    // Remove skills
809    if !skills_to_remove.is_empty() {
810        println!("{}", "Skills:".blue().bold());
811        for skill_name in &skills_to_remove {
812            let skill_dir = skills_dir.join(skill_name);
813
814            if !skill_dir.exists() {
815                skills_not_found += 1;
816                println!("  {} {} (not installed)", "·".yellow(), skill_name);
817                continue;
818            }
819
820            remove_dir_recursive(&skill_dir)?;
821            skills_removed += 1;
822            println!("  {} {}", "✓".green(), skill_name);
823
824            // Also remove from OpenCode skills directory
825            let opencode_skill = opencode_skills_dir.join(skill_name);
826            if opencode_skill.exists() {
827                remove_dir_recursive(&opencode_skill)?;
828            }
829        }
830    }
831
832    // Remove OpenCode integration (only when --all)
833    if all {
834        println!("{}", "OpenCode:".blue().bold());
835
836        // Remove commands
837        for cmd in OPENCODE_COMMANDS {
838            let cmd_file = opencode_cmd_dir.join(format!("{}.md", cmd));
839            if cmd_file.exists() {
840                fs::remove_file(&cmd_file)?;
841                opencode_removed += 1;
842            }
843        }
844
845        // Remove hooks
846        for hook in OPENCODE_HOOKS {
847            let hook_file = opencode_hook_dir.join(format!("{}.md", hook));
848            if hook_file.exists() {
849                fs::remove_file(&hook_file)?;
850                opencode_removed += 1;
851            }
852        }
853
854        // Remove tools
855        for tool in OPENCODE_TOOLS {
856            let tool_file = opencode_tool_dir.join(format!("{}.json", tool));
857            if tool_file.exists() {
858                fs::remove_file(&tool_file)?;
859                opencode_removed += 1;
860            }
861        }
862
863        if opencode_removed > 0 {
864            println!("  {} {} files removed", "✓".green(), opencode_removed);
865        } else {
866            println!("  {} Not installed", "·".yellow());
867        }
868    }
869
870    println!();
871    let total_removed = agents_removed + skills_removed + opencode_removed;
872    let total_not_found = agents_not_found + skills_not_found;
873
874    if total_removed > 0 {
875        println!(
876            "{}",
877            format!("✅ Removed {} item(s)", total_removed)
878                .green()
879                .bold()
880        );
881    }
882    if total_not_found > 0 {
883        println!(
884            "{}",
885            format!("{} item(s) were not installed", total_not_found).yellow()
886        );
887    }
888
889    Ok(())
890}
891
892/// Configure backpressure validation commands
893pub fn backpressure(
894    project_root: Option<PathBuf>,
895    commands: Vec<String>,
896    add: Option<String>,
897    remove: Option<String>,
898    list: bool,
899    clear: bool,
900) -> Result<()> {
901    let storage = Storage::new(project_root);
902
903    if !storage.is_initialized() {
904        anyhow::bail!("SCUD is not initialized. Run: scud init");
905    }
906
907    let config_path = storage.config_file();
908
909    // Load existing config
910    let content = fs::read_to_string(&config_path).unwrap_or_default();
911    let mut config: toml::Value =
912        toml::from_str(&content).unwrap_or(toml::Value::Table(toml::map::Map::new()));
913
914    // Get or create swarm.backpressure section
915    let bp_commands = get_backpressure_commands(&config);
916
917    if list {
918        // List current configuration
919        println!("{}", "Backpressure Configuration".blue().bold());
920        println!();
921
922        if bp_commands.is_empty() {
923            println!(
924                "  {} No commands configured (using auto-detect)",
925                "·".yellow()
926            );
927            println!();
928
929            // Show what would be auto-detected
930            let auto = crate::backpressure::BackpressureConfig::load(Some(
931                &storage.project_root().to_path_buf(),
932            ))?;
933            if !auto.commands.is_empty() {
934                println!("{}", "Auto-detected commands:".dimmed());
935                for cmd in &auto.commands {
936                    println!("  {} {}", "·".dimmed(), cmd.dimmed());
937                }
938            }
939        } else {
940            println!("{}", "Commands (in order):".blue());
941            for (i, cmd) in bp_commands.iter().enumerate() {
942                println!("  {}. {}", i + 1, cmd.green());
943            }
944        }
945
946        println!();
947        println!("{}", "Usage:".blue().bold());
948        println!("  scud config backpressure \"cmd1\" \"cmd2\"   Set commands");
949        println!("  scud config backpressure --add \"cmd\"     Add a command");
950        println!("  scud config backpressure --remove \"cmd\"  Remove a command");
951        println!("  scud config backpressure --clear         Clear (use auto-detect)");
952
953        return Ok(());
954    }
955
956    if clear {
957        // Remove backpressure section entirely
958        if let Some(swarm) = config.get_mut("swarm") {
959            if let Some(table) = swarm.as_table_mut() {
960                table.remove("backpressure");
961            }
962        }
963        save_config(&config_path, &config)?;
964        println!(
965            "{}",
966            "✓ Backpressure config cleared (will use auto-detect)".green()
967        );
968        return Ok(());
969    }
970
971    let mut new_commands = bp_commands.clone();
972
973    if let Some(cmd) = add {
974        if !new_commands.contains(&cmd) {
975            new_commands.push(cmd.clone());
976            println!("{}", format!("✓ Added: {}", cmd).green());
977        } else {
978            println!("{}", format!("· Already exists: {}", cmd).yellow());
979        }
980    } else if let Some(cmd) = remove {
981        if let Some(pos) = new_commands.iter().position(|c| c == &cmd) {
982            new_commands.remove(pos);
983            println!("{}", format!("✓ Removed: {}", cmd).green());
984        } else {
985            println!("{}", format!("· Not found: {}", cmd).yellow());
986        }
987    } else if !commands.is_empty() {
988        // Set commands directly
989        new_commands = commands;
990        println!("{}", "✓ Backpressure commands set:".green());
991        for cmd in &new_commands {
992            println!("  · {}", cmd);
993        }
994    } else {
995        // No args - show list
996        return backpressure(
997            Some(storage.project_root().to_path_buf()),
998            vec![],
999            None,
1000            None,
1001            true,
1002            false,
1003        );
1004    }
1005
1006    // Save updated config
1007    set_backpressure_commands(&mut config, &new_commands);
1008    save_config(&config_path, &config)?;
1009
1010    Ok(())
1011}
1012
1013/// Get backpressure commands from config
1014fn get_backpressure_commands(config: &toml::Value) -> Vec<String> {
1015    config
1016        .get("swarm")
1017        .and_then(|s| s.get("backpressure"))
1018        .and_then(|b| b.get("commands"))
1019        .and_then(|c| c.as_array())
1020        .map(|arr| {
1021            arr.iter()
1022                .filter_map(|v| v.as_str().map(String::from))
1023                .collect()
1024        })
1025        .unwrap_or_default()
1026}
1027
1028/// Set backpressure commands in config
1029fn set_backpressure_commands(config: &mut toml::Value, commands: &[String]) {
1030    let table = config.as_table_mut().expect("Config must be a table");
1031
1032    // Ensure swarm section exists
1033    if !table.contains_key("swarm") {
1034        table.insert(
1035            "swarm".to_string(),
1036            toml::Value::Table(toml::map::Map::new()),
1037        );
1038    }
1039
1040    let swarm = table.get_mut("swarm").unwrap().as_table_mut().unwrap();
1041
1042    // Ensure backpressure section exists
1043    if !swarm.contains_key("backpressure") {
1044        swarm.insert(
1045            "backpressure".to_string(),
1046            toml::Value::Table(toml::map::Map::new()),
1047        );
1048    }
1049
1050    let bp = swarm
1051        .get_mut("backpressure")
1052        .unwrap()
1053        .as_table_mut()
1054        .unwrap();
1055
1056    // Set commands array
1057    let cmd_array: Vec<toml::Value> = commands
1058        .iter()
1059        .map(|s| toml::Value::String(s.clone()))
1060        .collect();
1061    bp.insert("commands".to_string(), toml::Value::Array(cmd_array));
1062
1063    // Ensure defaults exist
1064    if !bp.contains_key("stop_on_failure") {
1065        bp.insert("stop_on_failure".to_string(), toml::Value::Boolean(true));
1066    }
1067    if !bp.contains_key("timeout_secs") {
1068        bp.insert("timeout_secs".to_string(), toml::Value::Integer(300));
1069    }
1070}
1071
1072/// Save config to file
1073fn save_config(path: &PathBuf, config: &toml::Value) -> Result<()> {
1074    let content = toml::to_string_pretty(config)?;
1075    fs::write(path, content)?;
1076    Ok(())
1077}
1078
1079/// Get the .scud/agents directory for spawn agent definitions
1080fn get_spawn_agents_dir(project_root: Option<PathBuf>) -> PathBuf {
1081    let base = project_root.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
1082    base.join(".scud").join("agents")
1083}
1084
1085/// Install spawn agent definitions to .scud/agents/
1086/// These define harness/model routing for different agent types
1087pub fn spawn_agents_add(
1088    project_root: Option<PathBuf>,
1089    name: Option<String>,
1090    all: bool,
1091    interactive: bool,
1092) -> Result<()> {
1093    let agents_dir = get_spawn_agents_dir(project_root);
1094    fs::create_dir_all(&agents_dir)?;
1095
1096    let agents_to_add: Vec<&str> = if all {
1097        EMBEDDED_SPAWN_AGENTS.iter().map(|(n, _)| *n).collect()
1098    } else if let Some(ref name) = name {
1099        if EMBEDDED_SPAWN_AGENTS
1100            .iter()
1101            .any(|(n, _)| *n == name.as_str())
1102        {
1103            vec![name.as_str()]
1104        } else {
1105            anyhow::bail!(
1106                "Unknown spawn agent: '{}'. Available: {}",
1107                name,
1108                EMBEDDED_SPAWN_AGENTS
1109                    .iter()
1110                    .map(|(n, _)| *n)
1111                    .collect::<Vec<_>>()
1112                    .join(", ")
1113            );
1114        }
1115    } else if interactive {
1116        // Interactive selection
1117        use dialoguer::MultiSelect;
1118
1119        let items: Vec<String> = EMBEDDED_SPAWN_AGENTS
1120            .iter()
1121            .map(|(name, content)| {
1122                // Extract description from toml
1123                let desc = content
1124                    .lines()
1125                    .find(|l| l.starts_with("description"))
1126                    .and_then(|l| l.split('=').nth(1))
1127                    .map(|s| s.trim().trim_matches('"'))
1128                    .unwrap_or("");
1129                format!("{} - {}", name, desc)
1130            })
1131            .collect();
1132
1133        let selections = MultiSelect::new()
1134            .with_prompt("Select spawn agents to install (space to toggle)")
1135            .items(&items)
1136            .defaults(&vec![true; items.len()])
1137            .interact()?;
1138
1139        selections
1140            .iter()
1141            .map(|&i| EMBEDDED_SPAWN_AGENTS[i].0)
1142            .collect()
1143    } else {
1144        anyhow::bail!("Please specify an agent name, use --all, or run interactively");
1145    };
1146
1147    if agents_to_add.is_empty() {
1148        println!("{}", "No agents selected.".yellow());
1149        return Ok(());
1150    }
1151
1152    println!("{}", "Spawn Agents:".blue().bold());
1153    let mut added = 0;
1154    let mut existing = 0;
1155
1156    for agent_name in agents_to_add {
1157        let dest = agents_dir.join(format!("{}.toml", agent_name));
1158
1159        if dest.exists() {
1160            existing += 1;
1161            println!("  {} {} (already installed)", "·".yellow(), agent_name);
1162            continue;
1163        }
1164
1165        if let Some((_, content)) = EMBEDDED_SPAWN_AGENTS.iter().find(|(n, _)| *n == agent_name) {
1166            fs::write(&dest, content)?;
1167            added += 1;
1168            println!("  {} {}", "✓".green(), agent_name.green());
1169        }
1170    }
1171
1172    println!();
1173    if added > 0 {
1174        println!(
1175            "{}",
1176            format!("✅ Installed {} spawn agent(s)", added)
1177                .green()
1178                .bold()
1179        );
1180        println!(
1181            "{}",
1182            "Agents are used via @agents section in .scg files".dimmed()
1183        );
1184    }
1185    if existing > 0 {
1186        println!(
1187            "{}",
1188            format!("{} agent(s) already installed", existing).yellow()
1189        );
1190    }
1191
1192    Ok(())
1193}
1194
1195/// List available spawn agents
1196pub fn spawn_agents_list(project_root: Option<PathBuf>) -> Result<()> {
1197    let agents_dir = get_spawn_agents_dir(project_root);
1198
1199    println!("{}", "Available Spawn Agents:".blue().bold());
1200    println!();
1201
1202    for (name, content) in EMBEDDED_SPAWN_AGENTS {
1203        let installed = agents_dir.join(format!("{}.toml", name)).exists();
1204        let status = if installed {
1205            "✓".green()
1206        } else {
1207            "·".dimmed()
1208        };
1209
1210        // Extract description and model info from toml
1211        let desc = content
1212            .lines()
1213            .find(|l| l.starts_with("description"))
1214            .and_then(|l| l.split('=').nth(1))
1215            .map(|s| s.trim().trim_matches('"'))
1216            .unwrap_or("");
1217
1218        let harness = content
1219            .lines()
1220            .find(|l| l.starts_with("harness"))
1221            .and_then(|l| l.split('=').nth(1))
1222            .map(|s| s.trim().trim_matches('"'))
1223            .unwrap_or("?");
1224
1225        let model = content
1226            .lines()
1227            .find(|l| l.trim().starts_with("model") && !l.contains('['))
1228            .and_then(|l| l.split('=').nth(1))
1229            .map(|s| s.trim().trim_matches('"'))
1230            .unwrap_or("default");
1231
1232        println!(
1233            "  {} {:<14} [{}:{}] {}",
1234            status,
1235            name.cyan(),
1236            harness,
1237            model,
1238            desc.dimmed()
1239        );
1240    }
1241
1242    println!();
1243    println!("Install: {}", "scud config spawn-agents add --all".cyan());
1244
1245    Ok(())
1246}
1247
1248/// Remove spawn agent definitions
1249pub fn spawn_agents_remove(
1250    project_root: Option<PathBuf>,
1251    name: Option<String>,
1252    all: bool,
1253) -> Result<()> {
1254    let agents_dir = get_spawn_agents_dir(project_root);
1255
1256    let agents_to_remove: Vec<&str> = if all {
1257        EMBEDDED_SPAWN_AGENTS.iter().map(|(n, _)| *n).collect()
1258    } else if let Some(ref name) = name {
1259        vec![name.as_str()]
1260    } else {
1261        anyhow::bail!("Please specify an agent name or use --all");
1262    };
1263
1264    println!("{}", "Removing Spawn Agents:".blue().bold());
1265    let mut removed = 0;
1266    let mut not_found = 0;
1267
1268    for agent_name in agents_to_remove {
1269        let path = agents_dir.join(format!("{}.toml", agent_name));
1270
1271        if !path.exists() {
1272            not_found += 1;
1273            println!("  {} {} (not installed)", "·".yellow(), agent_name);
1274            continue;
1275        }
1276
1277        fs::remove_file(&path)?;
1278        removed += 1;
1279        println!("  {} {}", "✓".green(), agent_name);
1280    }
1281
1282    println!();
1283    if removed > 0 {
1284        println!(
1285            "{}",
1286            format!("✅ Removed {} spawn agent(s)", removed)
1287                .green()
1288                .bold()
1289        );
1290    }
1291    if not_found > 0 {
1292        println!(
1293            "{}",
1294            format!("{} agent(s) were not installed", not_found).yellow()
1295        );
1296    }
1297
1298    Ok(())
1299}