clawgarden-cli 0.4.0

ClawGarden CLI - Multi-bot/multi-agent Garden management tool
//! Agent subcommands โ€” garden agent add/list/remove
//!
//! Standalone CLI commands for managing agents in a garden,
//! decoupled from the interactive `garden config` wizard.

use anyhow::Result;
use inquire::{Confirm, Password, Select, Text};

use crate::compose::BotConfig;
use crate::config::{load_current_config, save_updated_config};
use crate::garden::load_gardens;
use crate::ui;

/// Run `garden agent add`
pub fn cmd_add(garden_name: Option<&str>) -> Result<()> {
    let name = resolve_garden_name(garden_name)?;

    println!();
    ui::section_header_no_step("๐Ÿค–", &format!("Add Agent ยท {}", name));

    let roles = [
        ("PM", "๐Ÿ“‹", "Coordinates tasks & keeps the team on track"),
        ("DEV", "๐Ÿ’ป", "Writes and reviews code, implements features"),
        ("CRITIC", "๐Ÿ”", "Reviews output, catches issues & blind spots"),
        ("DESIGNER", "๐ŸŽจ", "UI/UX design, system architecture thinking"),
        ("RESEARCHER", "๐Ÿ”ฌ", "Investigates, documents, and gathers context"),
        ("TESTER", "๐Ÿงช", "Quality assurance, edge-case explorer"),
        ("OPS", "๐Ÿ”ง", "Deployment, DevOps, infrastructure management"),
        ("ANALYST", "๐Ÿ“Š", "Data analysis, metrics, insights"),
        ("OTHER", "โœจ", "Custom role โ€” define your own specialty"),
    ];

    // โ”€โ”€ Agent name โ”€โ”€
    let bot_name = Text::new("  Agent name (e.g. alex):")
        .with_validator(|input: &str| {
            if input.is_empty() {
                return Err("Please enter a name".into());
            }
            if input.contains(' ') {
                return Err("No spaces allowed".into());
            }
            Ok(inquire::validator::Validation::Valid)
        })
        .with_help_message("This will be used as the bot identifier internally")
        .prompt()?;

    // Check for duplicate
    let (current_bots, current_providers) = load_current_config(&name)?;
    if current_bots.iter().any(|b| b.name == bot_name) {
        anyhow::bail!("Agent '{}' already exists in garden '{}'", bot_name, name);
    }

    // โ”€โ”€ Role selection โ”€โ”€
    println!();
    for (role_name, icon, desc) in &roles {
        println!("    {} {} {}", icon, ui::role_badge(role_name), desc);
    }
    println!();

    let role_names: Vec<&str> = roles.iter().map(|r| r.0).collect();
    let role = Select::new("  Choose a role:", role_names.to_vec()).prompt()?;

    let role_desc = roles
        .iter()
        .find(|r| r.0 == role)
        .map(|r| r.2)
        .unwrap_or("");
    ui::hint(role_desc);
    println!();

    // โ”€โ”€ Telegram bot token โ”€โ”€
    let token = Password::new("  Telegram bot token:")
        .without_confirmation()
        .with_help_message("Get this from @BotFather on Telegram")
        .prompt()?;

    // โ”€โ”€ Confirm โ”€โ”€
    let token_preview = if token.len() > 8 {
        &token[..8]
    } else {
        &token
    };

    println!();
    ui::success(&format!(
        "{} {} as {}...",
        bot_name,
        ui::role_badge(role),
        token_preview,
    ));

    let confirm = Confirm::new("  Add this agent?")
        .with_default(true)
        .prompt()?;

    if !confirm {
        ui::warn("Cancelled.");
        return Ok(());
    }

    // โ”€โ”€ Save โ”€โ”€
    let mut bots = current_bots;
    bots.push(BotConfig {
        name: bot_name.clone(),
        role: role.to_string(),
        token,
    });

    save_updated_config(&name, &bots, &current_providers)?;

    println!();
    ui::success(&format!(
        "Agent '{}' {} added to garden '{}'.",
        bot_name,
        ui::role_badge(role),
        name
    ));
    ui::hint(&format!("Run `garden up --name {}` to apply changes.", name));

    Ok(())
}

/// Run `garden agent list`
pub fn cmd_list(garden_name: Option<&str>) -> Result<()> {
    let name = resolve_garden_name(garden_name)?;

    let (bots, providers) = load_current_config(&name)?;

    println!();
    ui::section_header_no_step("๐Ÿ“‹", &format!("Agents ยท {}", name));

    if bots.is_empty() {
        println!();
        ui::warn("No agents registered yet.");
        println!();
        ui::hint(&format!(
            "Add one with: garden agent add --name {}",
            name
        ));
        println!();
        return Ok(());
    }

    let mut rows = vec![(
        "๐Ÿค–".to_string(),
        "Agents".to_string(),
        format!("{} registered", bots.len()),
    )];

    for (i, bot) in bots.iter().enumerate() {
        let badge = ui::role_badge(&bot.role);
        rows.push((
            format!("  {}.", i + 1),
            bot.name.clone(),
            format!("{}", badge),
        ));
    }

    rows.push((
        "๐Ÿ”Œ".to_string(),
        "Providers".to_string(),
        format!("{} configured", providers.len()),
    ));

    ui::summary_box(&format!("๐ŸŒฑ {} โ€” Agents", name), &rows);

    Ok(())
}

/// Run `garden agent remove`
pub fn cmd_remove(garden_name: Option<&str>, agent_name: Option<&str>) -> Result<()> {
    let name = resolve_garden_name(garden_name)?;

    println!();
    ui::section_header_no_step("๐Ÿ—‘๏ธ", &format!("Remove Agent ยท {}", name));

    let (current_bots, current_providers) = load_current_config(&name)?;

    if current_bots.is_empty() {
        println!();
        ui::warn("No agents registered.");
        return Ok(());
    }

    // If agent name given via CLI arg, use it directly; otherwise prompt
    let to_remove = if let Some(agent) = agent_name {
        if !current_bots.iter().any(|b| b.name == agent) {
            anyhow::bail!(
                "Agent '{}' not found in garden '{}'. Registered: {}",
                agent,
                name,
                current_bots
                    .iter()
                    .map(|b| b.name.as_str())
                    .collect::<Vec<_>>()
                    .join(", ")
            );
        }
        agent.to_string()
    } else {
        // Interactive selection
        println!();
        println!("  {} Registered agents:", "\x1b[2m");
        for (i, bot) in current_bots.iter().enumerate() {
            println!(
                "    {} {}. {} {}",
                "\x1b[2m",
                i + 1,
                bot.name,
                ui::role_badge(&bot.role)
            );
        }
        println!("{}", "\x1b[0m");

        let bot_names: Vec<&str> = current_bots.iter().map(|b| b.name.as_str()).collect();
        let selection = Select::new("  Select agent to remove:", bot_names.to_vec()).prompt()?;
        selection.to_string()
    };

    // Confirm
    let confirm = Confirm::new(&format!("  Remove agent '{}'?", to_remove))
        .with_default(false)
        .prompt()?;

    if !confirm {
        ui::warn("Cancelled.");
        return Ok(());
    }

    // Remove and save
    let bots: Vec<BotConfig> = current_bots
        .into_iter()
        .filter(|b| b.name != to_remove)
        .collect();

    if bots.is_empty() {
        ui::warn("Garden will have no agents after removal.");
        let proceed = Confirm::new("  Continue?")
            .with_default(false)
            .prompt()?;
        if !proceed {
            ui::warn("Cancelled.");
            return Ok(());
        }
    }

    save_updated_config(&name, &bots, &current_providers)?;

    println!();
    ui::success(&format!(
        "Agent '{}' removed from garden '{}'.",
        to_remove, name
    ));
    ui::hint(&format!(
        "Run `garden up --name {}` to apply changes.",
        name
    ));

    Ok(())
}

// โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Resolve garden name from argument or default
fn resolve_garden_name(name: Option<&str>) -> Result<String> {
    if let Some(n) = name {
        let registry = load_gardens()?;
        if !registry.exists(n) {
            anyhow::bail!("Garden '{}' not found. Run 'garden new' first.", n);
        }
        return Ok(n.to_string());
    }

    let registry = load_gardens()?;

    if registry.gardens.is_empty() {
        anyhow::bail!("No gardens found. Run 'garden new' to create one.");
    }

    if registry.gardens.len() == 1 {
        return Ok(registry.gardens[0].name.clone());
    }

    // Multiple gardens โ€” ask user
    let names: Vec<&str> = registry.gardens.iter().map(|g| g.name.as_str()).collect();
    let selection = Select::new("  Select a garden:", names.to_vec()).prompt()?;
    Ok(selection.to_string())
}