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;
use crate::ui;
pub fn cmd_add(garden_name: Option<&str>) -> Result<()> {
let name = garden::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 = ui::retry_prompt(|| {
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 = ui::retry_prompt(|| 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 = ui::retry_prompt(|| {
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 = ui::retry_prompt(|| {
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,
username: String::new(),
priority: 100,
enabled: true,
});
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 {}` to apply changes.",
name
));
Ok(())
}
pub fn cmd_list(garden_name: Option<&str>) -> Result<()> {
let name = garden::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));
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_edit(garden_name: Option<&str>) -> Result<()> {
let name = garden::resolve_garden_name(garden_name)?;
println!();
ui::section_header_no_step("โ๏ธ", &format!("Edit Agent ยท {}", name));
let (current_bots, current_providers) = load_current_config(&name)?;
if current_bots.is_empty() {
println!();
ui::warn("No agents registered.");
ui::hint(&format!("Add one with: garden agent add {}", name));
return Ok(());
}
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_labels: Vec<String> = current_bots
.iter()
.map(|b| format!("{} {}", b.name, ui::role_badge(&b.role)))
.collect();
let selection = ui::retry_prompt(|| {
Select::new(" Select an agent to edit:", bot_labels.clone()).prompt()
})?;
let idx = current_bots
.iter()
.position(|b| format!("{} {}", b.name, ui::role_badge(&b.role)) == selection)
.ok_or_else(|| anyhow::anyhow!("Agent '{}' not found", selection))?;
let entry = ¤t_bots[idx];
let bot_name = entry.name.clone();
let old_role = entry.role.clone();
let old_token = entry.token.clone();
println!();
ui::divider();
println!(" {} โ current configuration", bot_name);
println!();
let masked_token = if old_token.len() > 8 {
format!("{}...", &old_token[..8])
} else {
"****".to_string()
};
ui::hint(&format!(" Role: {}", ui::role_badge(&old_role)));
ui::hint(&format!(" Token: {}", masked_token));
println!();
let edit_choices = vec!["๐ญ Change role", "๐ Change token", "๐ญ๐ Change both"];
let action = ui::retry_prompt(|| {
Select::new(" What would you like to change?", edit_choices.clone()).prompt()
})?;
let roles = [
("PM", "๐"),
("DEV", "๐ป"),
("CRITIC", "๐"),
("DESIGNER", "๐จ"),
("RESEARCHER", "๐ฌ"),
("TESTER", "๐งช"),
("OPS", "๐ง"),
("ANALYST", "๐"),
("OTHER", "โจ"),
];
let role_names: Vec<&str> = roles.iter().map(|r| r.0).collect();
let mut new_role = old_role.clone();
let mut new_token = old_token.clone();
match action {
"๐ญ Change role" => {
println!();
for (role_name, icon) in &roles {
println!(" {} {}", icon, ui::role_badge(role_name));
}
println!();
new_role = ui::retry_prompt(|| {
Select::new(" Choose a new role:", role_names.to_vec()).prompt()
})?
.to_string();
}
"๐ Change token" => {
new_token = ui::retry_prompt(|| {
Password::new(&format!(" Enter new token for {}:", bot_name))
.without_confirmation()
.with_help_message("Get this from @BotFather on Telegram")
.prompt()
})?;
}
"๐ญ๐ Change both" => {
println!();
for (role_name, icon) in &roles {
println!(" {} {}", icon, ui::role_badge(role_name));
}
println!();
new_role = ui::retry_prompt(|| {
Select::new(" Choose a new role:", role_names.to_vec()).prompt()
})?
.to_string();
println!();
new_token = ui::retry_prompt(|| {
Password::new(&format!(" Enter new token for {}:", bot_name))
.without_confirmation()
.with_help_message("Get this from @BotFather on Telegram")
.prompt()
})?;
}
_ => unreachable!(),
}
println!();
if new_role != old_role {
ui::success(&format!(
"Role: {} โ {}",
ui::role_badge(&old_role),
ui::role_badge(&new_role)
));
}
if new_token != old_token {
let new_masked = if new_token.len() > 8 {
format!("{}...", &new_token[..8])
} else {
"****".to_string()
};
ui::success(&format!("Token: updated to {}", new_masked));
}
let confirm =
ui::retry_prompt(|| Confirm::new(" Apply changes?").with_default(true).prompt())?;
if !confirm {
ui::warn("Cancelled.");
return Ok(());
}
let old_username = current_bots[idx].username.clone();
let old_priority = current_bots[idx].priority;
let old_enabled = current_bots[idx].enabled;
let mut bots = current_bots;
bots[idx] = BotConfig {
name: bot_name.clone(),
role: new_role,
token: new_token,
username: old_username,
priority: old_priority,
enabled: old_enabled,
};
save_updated_config(&name, &bots, ¤t_providers)?;
println!();
ui::success(&format!(
"Agent '{}' updated in garden '{}'.",
bot_name, name
));
ui::hint(&format!(
"Run `garden up {}` to apply changes.",
name
));
Ok(())
}
pub fn cmd_remove(garden_name: Option<&str>, agent_name: Option<&str>) -> Result<()> {
let name = garden::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 = ui::retry_prompt(|| {
Select::new(" Select agent to remove:", bot_names.to_vec()).prompt()
})?;
selection.to_string()
};
let confirm = ui::retry_prompt(|| {
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 =
ui::retry_prompt(|| 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 {}` to apply changes.",
name
));
Ok(())
}