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;
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"),
];
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()?;
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);
}
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!();
let token = Password::new(" Telegram bot token:")
.without_confirmation()
.with_help_message("Get this from @BotFather on Telegram")
.prompt()?;
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(());
}
let mut bots = current_bots;
bots.push(BotConfig {
name: bot_name.clone(),
role: role.to_string(),
token,
});
save_updated_config(&name, &bots, ¤t_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(())
}
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(())
}
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(());
}
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 {
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()
};
let confirm = Confirm::new(&format!(" Remove agent '{}'?", to_remove))
.with_default(false)
.prompt()?;
if !confirm {
ui::warn("Cancelled.");
return Ok(());
}
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, ¤t_providers)?;
println!();
ui::success(&format!(
"Agent '{}' removed from garden '{}'.",
to_remove, name
));
ui::hint(&format!(
"Run `garden up --name {}` to apply changes.",
name
));
Ok(())
}
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());
}
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())
}