use anyhow::Result;
use inquire::{Confirm, MultiSelect, Password, Select};
use std::sync::Arc;
use crate::compose::ProviderEntry;
use crate::config::{load_current_config, save_updated_config, select_provider_model};
use crate::garden::load_gardens;
use crate::providers::{ProviderAuthMethod, ProviderRegistry};
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 Provider ยท {}", name));
let providers = ProviderRegistry::providers();
let provider_options: Vec<String> = providers
.iter()
.map(|p| {
let model_hint = match &p.default_model {
Some(m) => format!(" โ {}", m),
None => String::new(),
};
format!("{} {}{}", p.icon, p.label, model_hint)
})
.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| {
let model_hint = match &p.default_model {
Some(m) => format!(" โ {}", m),
None => String::new(),
};
format!("{} {}{}", p.icon, p.label, model_hint) == *provider_label
})
.expect("Provider not found");
println!();
ui::divider();
println!(" {} {} setup:", provider.icon, provider.label);
println!();
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()?;
let model = select_provider_model(provider)?;
providers_data.push(ProviderEntry {
provider: Arc::new(provider.clone()),
auth_method_id: auth_method.id.clone(),
api_key,
model,
});
println!();
ui::success(&format!(
"{} ({}) configured โ {}",
provider.label, auth_method.label,
providers_data.last().unwrap().model
));
}
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 to garden '{}'.", added, 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!("Providers ยท {}", name));
if providers.is_empty() {
println!();
ui::warn("No providers configured yet.");
println!();
ui::hint(&format!(
"Add one with: garden provider add --name {}",
name
));
println!();
return Ok(());
}
let mut rows = vec![(
"๐".to_string(),
"Providers".to_string(),
format!("{} configured", providers.len()),
)];
for (i, entry) in providers.iter().enumerate() {
rows.push((
format!(" {}.", i + 1),
entry.provider.label.clone(),
format!("{} โ {}", entry.provider.icon, entry.model),
));
}
ui::summary_box(&format!("๐ฑ {} โ Providers", name), &rows);
Ok(())
}
pub fn cmd_remove(garden_name: Option<&str>) -> Result<()> {
let name = resolve_garden_name(garden_name)?;
println!();
ui::section_header_no_step("๐๏ธ", &format!("Remove Provider ยท {}", name));
let (current_bots, current_providers) = load_current_config(&name)?;
if current_providers.is_empty() {
println!();
ui::warn("No providers configured.");
return Ok(());
}
println!();
println!(" {} Registered providers:", "\x1b[2m");
for (i, entry) in current_providers.iter().enumerate() {
println!(
" {} {}. {} {} โ {}",
"\x1b[2m",
i + 1,
entry.provider.icon,
entry.provider.label,
entry.model,
);
}
println!("{}", "\x1b[0m");
let provider_labels: Vec<String> = current_providers
.iter()
.map(|entry| {
format!(
"{} {} ({}) โ {}",
entry.provider.icon,
entry.provider.label,
entry.auth_method_id,
entry.model
)
})
.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<ProviderEntry> = current_providers
.into_iter()
.filter(|entry| {
let label = format!(
"{} {} ({}) โ {}",
entry.provider.icon,
entry.provider.label,
entry.auth_method_id,
entry.model
);
!remove_labels.contains(&label)
})
.collect();
save_updated_config(&name, ¤t_bots, &providers)?;
println!();
ui::success(&format!(
"Selected provider(s) removed from garden '{}'.",
name
));
ui::hint(&format!(
"Run `garden up --name {}` to apply changes.",
name
));
Ok(())
}
pub fn cmd_edit(garden_name: Option<&str>) -> Result<()> {
let name = resolve_garden_name(garden_name)?;
println!();
ui::section_header_no_step("โ๏ธ", &format!("Edit Provider ยท {}", name));
let (current_bots, mut current_providers) = load_current_config(&name)?;
if current_providers.is_empty() {
println!();
ui::warn("No providers configured.");
ui::hint(&format!(
"Add one with: garden provider add --name {}",
name
));
return Ok(());
}
println!();
println!(" {} Registered providers:", "\x1b[2m");
for (i, entry) in current_providers.iter().enumerate() {
println!(
" {} {}. {} {} โ {}",
"\x1b[2m",
i + 1,
entry.provider.icon,
entry.provider.label,
entry.model,
);
}
println!("{}", "\x1b[0m");
let provider_labels: Vec<String> = current_providers
.iter()
.map(|entry| {
format!(
"{} {} โ {}",
entry.provider.icon, entry.provider.label, entry.model
)
})
.collect();
let selection = Select::new(" Select a provider to edit:", provider_labels).prompt()?;
let idx = current_providers
.iter()
.position(|entry| {
format!(
"{} {} โ {}",
entry.provider.icon, entry.provider.label, entry.model
) == selection
})
.expect("Selected provider not found");
let entry = ¤t_providers[idx];
let provider = entry.provider.clone();
let old_model = entry.model.clone();
let old_api_key = entry.api_key.clone();
let old_auth_method_id = entry.auth_method_id.clone();
println!();
ui::divider();
println!(
" {} {} โ current configuration",
provider.icon, provider.label
);
println!();
let masked_key = if old_api_key.len() > 8 {
format!("{}...", &old_api_key[..8])
} else {
"****".to_string()
};
ui::hint(&format!(" Model: {}", old_model));
ui::hint(&format!(" API key: {}", masked_key));
ui::hint(&format!(" Auth method: {}", old_auth_method_id));
println!();
let edit_choices = vec!["๐ฏ Change model", "๐ Change API key", "๐ฏ๐ Change both"];
let action = Select::new(" What would you like to change?", edit_choices).prompt()?;
let mut new_model = old_model.clone();
let mut new_api_key = old_api_key.clone();
let new_auth_method_id = old_auth_method_id.clone();
match action {
"๐ฏ Change model" => {
new_model = select_provider_model(&provider)?;
}
"๐ Change API key" => {
let api_key = Password::new(&format!(" Enter new {} API key:", provider.label))
.without_confirmation()
.with_help_message("This will be stored in .env and pi-auth.json")
.prompt()?;
new_api_key = api_key;
}
"๐ฏ๐ Change both" => {
new_model = select_provider_model(&provider)?;
println!();
let api_key = Password::new(&format!(" Enter new {} API key:", provider.label))
.without_confirmation()
.with_help_message("This will be stored in .env and pi-auth.json")
.prompt()?;
new_api_key = api_key;
}
_ => unreachable!(),
}
println!();
if new_model != old_model {
ui::success(&format!(
"Model: {} โ {}",
old_model, new_model
));
}
if new_api_key != old_api_key {
let new_masked = if new_api_key.len() > 8 {
format!("{}...", &new_api_key[..8])
} else {
"****".to_string()
};
ui::success(&format!("API key: updated to {}", new_masked));
}
let confirm = Confirm::new(" Apply changes?")
.with_default(true)
.prompt()?;
if !confirm {
ui::warn("Cancelled.");
return Ok(());
}
current_providers[idx] = ProviderEntry {
provider: provider.clone(),
auth_method_id: new_auth_method_id,
api_key: new_api_key,
model: new_model,
};
save_updated_config(&name, ¤t_bots, ¤t_providers)?;
println!();
ui::success(&format!(
"Provider '{}' updated in garden '{}'.",
provider.label, 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())
}
fn select_auth_method(provider: &crate::providers::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"))
}