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_auth_method, select_provider_model,
};
use crate::garden;
use crate::providers::ProviderRegistry;
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 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 = ui::retry_prompt(|| {
MultiSelect::new(" Select providers to add:", provider_options.clone()).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
})
.ok_or_else(|| {
anyhow::anyhow!("Provider '{}' not found in registry", provider_label)
})?;
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 = ui::retry_prompt(|| {
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 = ui::retry_prompt(|| {
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 {}` 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!("Providers ยท {}", name));
if providers.is_empty() {
println!();
ui::warn("No providers configured yet.");
println!();
ui::hint(&format!(
"Add one with: garden provider add {}",
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_edit(garden_name: Option<&str>) -> Result<()> {
let name = garden::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
));
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 = ui::retry_prompt(|| {
Select::new(" Select a provider to edit:", provider_labels.clone()).prompt()
})?;
let idx = current_providers
.iter()
.position(|entry| {
format!(
"{} {} โ {}",
entry.provider.icon, entry.provider.label, entry.model
) == selection
})
.ok_or_else(|| anyhow::anyhow!("Provider '{}' not found", selection))?;
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 = ui::retry_prompt(|| {
Select::new(" What would you like to change?", edit_choices.clone()).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 = ui::retry_prompt(|| {
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 = ui::retry_prompt(|| {
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 =
ui::retry_prompt(|| 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 {}` to apply changes.",
name
));
Ok(())
}
pub fn cmd_remove(garden_name: Option<&str>) -> Result<()> {
let name = garden::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 = ui::retry_prompt(|| {
MultiSelect::new(" Select providers to remove:", provider_labels.clone()).prompt()
})?;
if to_remove.is_empty() {
ui::warn("Nothing selected.");
return Ok(());
}
let confirm = ui::retry_prompt(|| {
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 {}` to apply changes.",
name
));
Ok(())
}
pub fn cmd_refresh(provider_id: Option<&str>) -> Result<()> {
use crate::providers::{refresh_models_sync, ProviderRegistry};
println!();
ui::section_header_no_step("๐", "Refresh Provider Models");
if let Some(id) = provider_id {
let manifest = ProviderRegistry::find(id)
.ok_or_else(|| anyhow::anyhow!("Provider '{}' not found", id))?;
if manifest.catalog_url.is_none() {
println!();
ui::warn(&format!(
" {} has no catalog_url โ no refresh needed",
manifest.label
));
return Ok(());
}
println!();
ui::spinner(&format!("Refreshing {} models...", manifest.label), 500);
match refresh_models_sync(&manifest) {
Ok(models) => {
println!();
ui::success(&format!(
" {} refreshed: {} models cached",
manifest.label,
models.len()
));
for m in models.iter().take(5) {
ui::hint(&format!(" - {}", m));
}
if models.len() > 5 {
ui::hint(&format!(" ... and {} more", models.len() - 5));
}
}
Err(e) => {
anyhow::bail!("Failed to refresh {}: {}", manifest.label, e);
}
}
} else {
let providers = ProviderRegistry::providers();
let catalog_providers: Vec<_> = providers
.iter()
.filter(|p| p.catalog_url.is_some())
.collect();
if catalog_providers.is_empty() {
println!();
ui::warn(" No providers with model catalogs found.");
return Ok(());
}
println!();
println!(" Refreshing {} provider(s)...", catalog_providers.len());
let mut any_err = false;
for manifest in &catalog_providers {
match refresh_models_sync(manifest) {
Ok(models) => {
println!();
ui::success(&format!(" {}: {} models", manifest.label, models.len()));
}
Err(e) => {
any_err = true;
ui::error(&format!(" {}: failed ({})", manifest.label, e));
}
}
}
if any_err {
anyhow::bail!("Some providers failed to refresh");
}
}
println!();
ui::success("Cache refreshed.");
Ok(())
}