use anyhow::{Context, Result};
use inquire::{Confirm, MultiSelect, Password, Select, Text};
use std::sync::Arc;
use crate::compose::{BotConfig, GardenConfig};
use crate::garden::load_gardens;
use crate::providers::{ProviderAuthMethod, ProviderPlugin, ProviderRegistry};
use crate::ui;
pub fn run_config(name: &str) -> Result<()> {
let registry = load_gardens()?;
if !registry.exists(name) {
anyhow::bail!("Garden '{}' not found. Run 'garden new' first.", name);
}
println!();
ui::section_header_no_step("โ๏ธ", &format!("Garden Config ยท {}", name));
loop {
let choice = Select::new(
" What would you like to do?",
vec![
"๐ค Add an agent",
"๐ค Remove an agent",
"๐ Add a provider",
"๐ Remove a provider",
"๐ View configuration",
"๐ Restart container",
"โ Exit",
],
)
.prompt()?;
match choice {
"๐ค Add an agent" => add_bot(name)?,
"๐ค Remove an agent" => remove_bot(name)?,
"๐ Add a provider" => add_provider(name)?,
"๐ Remove a provider" => remove_provider(name)?,
"๐ View configuration" => show_config(name)?,
"๐ Restart container" => restart_garden(name)?,
"โ Exit" => break,
_ => {}
}
}
Ok(())
}
fn add_bot(name: &str) -> Result<()> {
println!();
ui::divider();
println!(" {} Adding a new agent", "\x1b[1m\x1b[38;5;255m");
println!("{}", "\x1b[0m");
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 role_names: Vec<&str> = roles.iter().map(|r| r.0).collect();
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()?;
println!();
for (role_name, icon, desc) in &roles {
println!(" {} {} {}", icon, ui::role_badge(role_name), desc);
}
println!();
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 bot = BotConfig {
name: bot_name.clone(),
role: role.to_string(),
token,
};
println!();
let token_preview = if bot.token.len() > 8 {
&bot.token[..8]
} else {
&bot.token
};
ui::success(&format!(
"{} {} as {}...",
bot.name,
ui::role_badge(&bot.role),
token_preview,
));
let confirm = Confirm::new("\n Add this agent?")
.with_default(true)
.prompt()?;
if !confirm {
ui::warn("Cancelled.");
return Ok(());
}
let (current_bots, current_providers) = load_current_config(name)?;
let mut bots = current_bots;
bots.push(bot);
save_updated_config(name, &bots, ¤t_providers)?;
println!();
ui::success(&format!("Agent '{}' added.", bot_name));
Ok(())
}
fn remove_bot(name: &str) -> Result<()> {
let (current_bots, current_providers) = load_current_config(name)?;
if current_bots.is_empty() {
println!();
ui::warn("No agents registered.");
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 to_remove = MultiSelect::new(
" Select agents to remove:",
current_bots.iter().map(|b| b.name.clone()).collect(),
)
.prompt()?;
if to_remove.is_empty() {
ui::warn("Nothing selected.");
return Ok(());
}
let confirm = Confirm::new(&format!("\n Remove {} agent(s)?", to_remove.len()))
.with_default(true)
.prompt()?;
if !confirm {
ui::warn("Cancelled.");
return Ok(());
}
let remove_names: Vec<String> = to_remove.into_iter().collect();
let bots: Vec<BotConfig> = current_bots
.into_iter()
.filter(|b| !remove_names.contains(&b.name))
.collect();
save_updated_config(name, &bots, ¤t_providers)?;
println!();
ui::success("Selected agents removed.");
Ok(())
}
fn add_provider(name: &str) -> Result<()> {
println!();
ui::divider();
println!(" {} Adding a new provider", "\x1b[1m\x1b[38;5;255m");
println!("{}", "\x1b[0m");
let providers = ProviderRegistry::providers();
let provider_options: Vec<String> = providers
.iter()
.map(|p| format!("{} {}", p.icon, p.label))
.collect();
let selection = MultiSelect::new(" Select providers to add:", provider_options).prompt()?;
if selection.is_empty() {
ui::warn("Nothing selected.");
return Ok(());
}
let (current_bots, mut providers_data) = load_current_config(name)?;
let original_count = providers_data.len();
for provider_label in &selection {
let provider = providers
.iter()
.find(|p| format!("{} {}", p.icon, p.label) == *provider_label)
.expect("Provider not found");
println!();
println!(" {} {} setup:", provider.icon, provider.label);
let auth_method = if provider.auth.len() > 1 {
select_auth_method(provider)?
} else {
provider.auth.first().unwrap().clone()
};
let api_key = Password::new(&format!(" Enter {} API key:", auth_method.label))
.without_confirmation()
.with_help_message("This will be stored in .env and pi-auth.json")
.prompt()?;
providers_data.push((Arc::new(provider.clone()), auth_method.id.clone(), api_key));
println!();
ui::success(&format!(
"{} ({}) configured",
provider.label, auth_method.label
));
}
let added = providers_data.len() - original_count;
let confirm = Confirm::new(&format!("\n Add {} provider(s)?", added))
.with_default(true)
.prompt()?;
if !confirm {
ui::warn("Cancelled.");
return Ok(());
}
save_updated_config(name, ¤t_bots, &providers_data)?;
println!();
ui::success(&format!("{} provider(s) added.", added));
Ok(())
}
fn remove_provider(name: &str) -> Result<()> {
let (current_bots, current_providers) = load_current_config(name)?;
if current_providers.is_empty() {
println!();
ui::warn("No providers registered.");
return Ok(());
}
println!();
println!(" {} Registered providers:", "\x1b[2m");
for (i, (provider, method_id, _)) in current_providers.iter().enumerate() {
println!(
" {} {}. {} ({})",
"\x1b[2m",
i + 1,
provider.icon,
method_id
);
}
println!("{}", "\x1b[0m");
let provider_labels: Vec<String> = current_providers
.iter()
.map(|(p, method_id, _)| format!("{} {} ({})", p.icon, p.label, method_id))
.collect();
let to_remove = MultiSelect::new(" Select providers to remove:", provider_labels).prompt()?;
if to_remove.is_empty() {
ui::warn("Nothing selected.");
return Ok(());
}
let confirm = Confirm::new(&format!("\n Remove {} provider(s)?", to_remove.len()))
.with_default(true)
.prompt()?;
if !confirm {
ui::warn("Cancelled.");
return Ok(());
}
let remove_labels: Vec<String> = to_remove.into_iter().collect();
let providers: Vec<(Arc<ProviderPlugin>, String, String)> = current_providers
.into_iter()
.filter(|(p, method_id, _)| {
let label = format!("{} {} ({})", p.icon, p.label, method_id);
!remove_labels.contains(&label)
})
.collect();
save_updated_config(name, ¤t_bots, &providers)?;
println!();
ui::success("Selected providers removed.");
Ok(())
}
fn show_config(name: &str) -> Result<()> {
let registry = load_gardens()?;
let garden_dir = registry.garden_dir(name);
let compose_file = registry.compose_file(name);
let env_file = registry.env_file(name);
let mut rows = vec![
("๐ก".to_string(), "Garden".to_string(), name.to_string()),
(
"๐".to_string(),
"Path".to_string(),
garden_dir.display().to_string(),
),
(
"๐ณ".to_string(),
"Container".to_string(),
format!("garden-{}", name),
),
(
"๐".to_string(),
"Compose".to_string(),
if compose_file.exists() {
"โ present".to_string()
} else {
"โ missing".to_string()
},
),
(
"๐".to_string(),
".env".to_string(),
if env_file.exists() {
"โ present".to_string()
} else {
"โ missing".to_string()
},
),
];
let (bots, providers) = load_current_config(name)?;
rows.push((
"๐ค".to_string(),
"Agents".to_string(),
format!("{} total", bots.len()),
));
for bot in &bots {
rows.push((
" ".to_string(),
" ".to_string(),
format!("{} {}", bot.name, ui::role_badge(&bot.role)),
));
}
rows.push((
"๐".to_string(),
"Providers".to_string(),
format!("{} total", providers.len()),
));
for (provider, method_id, _) in &providers {
rows.push((
" ".to_string(),
" ".to_string(),
format!("{} {} ({})", provider.icon, provider.label, method_id),
));
}
ui::summary_box(&format!("โ๏ธ {} โ Configuration", name), &rows);
Ok(())
}
fn restart_garden(name: &str) -> Result<()> {
println!();
ui::spinner("Restarting container...", 500);
crate::compose::restart_garden(name)?;
println!();
ui::success("Container restarted.");
Ok(())
}
fn load_current_config(
name: &str,
) -> Result<(Vec<BotConfig>, Vec<(Arc<ProviderPlugin>, String, String)>)> {
let registry = load_gardens()?;
let workspace_dir = registry.workspace_dir(name);
let registry_file = workspace_dir.join("agents/registry.json");
let env_file = registry.env_file(name);
let mut env_map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
if env_file.exists() {
let env_content = std::fs::read_to_string(&env_file).context("Failed to read .env")?;
for line in env_content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
env_map.insert(key.trim().to_string(), value.trim().to_string());
}
}
}
let mut bots = Vec::new();
if registry_file.exists() {
let content =
std::fs::read_to_string(®istry_file).context("Failed to read registry.json")?;
let json: serde_json::Value =
serde_json::from_str(&content).context("Failed to parse registry.json")?;
if let Some(agents) = json.get("agents").and_then(|a| a.as_array()) {
for agent in agents {
if let (Some(name), Some(_bot), Some(role)) = (
agent.get("name").and_then(|v| v.as_str()),
agent.get("bot").and_then(|v| v.as_object()),
agent.get("role").and_then(|v| v.as_str()),
) {
let token_env_key = agent
.get("bot")
.and_then(|v| v.get("token_env"))
.and_then(|v| v.as_str())
.unwrap_or_default();
let token = env_map.get(token_env_key).cloned().unwrap_or_default();
bots.push(BotConfig {
name: name.to_string(),
role: role.to_string(),
token,
});
}
}
}
}
let mut providers: Vec<(Arc<ProviderPlugin>, String, String)> = Vec::new();
for known in ProviderRegistry::providers() {
let env_key = format!("{}_API_KEY", known.id.to_uppercase());
if let Some(value) = env_map.get(&env_key) {
if !value.is_empty() {
let auth = known
.auth
.first()
.map(|a| a.id.clone())
.unwrap_or_else(|| "api-key".to_string());
providers.push((Arc::new(known.clone()), auth, value.clone()));
}
}
}
Ok((bots, providers))
}
fn save_updated_config(
name: &str,
bots: &[BotConfig],
providers: &[(Arc<ProviderPlugin>, String, String)],
) -> Result<()> {
let registry = load_gardens()?;
let garden_dir = registry.garden_dir(name);
let workspace_dir = registry.workspace_dir(name);
std::fs::create_dir_all(&workspace_dir.join("agents"))
.context("Failed to create agents directory")?;
std::fs::create_dir_all(&workspace_dir.join("data"))
.context("Failed to create data directory")?;
std::fs::create_dir_all(&workspace_dir.join("logs"))
.context("Failed to create logs directory")?;
let config = GardenConfig {
name: name.to_string(),
telegram_group_id: load_telegram_group_id(name)?,
bots: bots.to_vec(),
providers: providers.to_vec(),
};
let compose_path = registry.compose_file(name);
std::fs::write(&compose_path, config.generate_compose())
.context("Failed to write docker-compose.yml")?;
let env_path = registry.env_file(name);
std::fs::write(&env_path, config.generate_env()).context("Failed to write .env file")?;
let auth_json_path = garden_dir.join("pi-auth.json");
std::fs::write(&auth_json_path, config.generate_auth_json())
.context("Failed to write pi-auth.json")?;
let registry_path = workspace_dir.join("agents/registry.json");
std::fs::write(®istry_path, config.generate_registry_json())
.context("Failed to write registry.json")?;
let allowlist_path = workspace_dir.join("agents/.allowlist");
std::fs::write(&allowlist_path, "pi-coding-agent\n").context("Failed to write allowlist")?;
Ok(())
}
fn load_telegram_group_id(name: &str) -> Result<String> {
let registry = load_gardens()?;
let env_file = registry.env_file(name);
if !env_file.exists() {
return Ok(String::new());
}
let content = std::fs::read_to_string(&env_file)?;
for line in content.lines() {
if line.starts_with("TELEGRAM_GROUP_ID=") {
if let Some(value) = line.split_once('=') {
return Ok(value.1.trim().to_string());
}
}
}
Ok(String::new())
}
fn select_auth_method(provider: &ProviderPlugin) -> Result<ProviderAuthMethod> {
if provider.auth.len() == 1 {
return Ok(provider.auth.first().unwrap().clone());
}
let method_options: Vec<String> = provider
.auth
.iter()
.map(|m| match &m.hint {
Some(h) => format!("{} ({})", m.label, h),
None => m.label.clone(),
})
.collect();
let selection = Select::new(" Select authentication method:", method_options).prompt()?;
provider
.auth
.iter()
.find(|m| match &m.hint {
Some(h) => format!("{} ({})", m.label, h) == selection,
None => m.label == selection,
})
.cloned()
.ok_or_else(|| anyhow::anyhow!("Auth method not found"))
}