use anyhow::{bail, Result};
use owo_colors::OwoColorize;
use crate::api::client::{ApiClient, ApiError};
use crate::api::models::NewService;
use crate::render::theme;
pub async fn set_variable(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
key: &str,
value: &str,
secret: bool,
) -> Result<()> {
let project = client.find_project(project_reference).await?;
let service = client.find_service(&project.id, service_reference).await?;
client.set_variable(&service.id, key, value, secret).await?;
println!(
" {} {} en {}{}",
theme::CHECK.style(theme::success()),
key.style(theme::strong()),
service.name.style(theme::strong()),
if secret { " (secreto)" } else { "" },
);
println!(
" {}",
"El próximo despliegue la recogerá; el contenedor actual no la ve.".style(theme::muted()),
);
Ok(())
}
pub async fn unset_variable(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
key: &str,
) -> Result<()> {
let project = client.find_project(project_reference).await?;
let service = client.find_service(&project.id, service_reference).await?;
let variables = client.variables(&service.id).await?;
let Some(variable) = variables.into_iter().find(|variable| variable.key == key) else {
bail!(
"`{}` no define ninguna variable llamada `{key}`",
service.name
);
};
client.remove_variable(&variable.id).await?;
println!(
" {} {} eliminada",
theme::CHECK.style(theme::success()),
key.style(theme::strong())
);
Ok(())
}
pub async fn connect(
client: &ApiClient,
project_reference: &str,
consumer_reference: &str,
provider_reference: &str,
) -> Result<()> {
let project = client.find_project(project_reference).await?;
let consumer = client.find_service(&project.id, consumer_reference).await?;
let provider = client.find_service(&project.id, provider_reference).await?;
let connection = client.connect(&consumer.id, &provider.id).await?;
println!(
" {} {} {} {}",
theme::CHECK.style(theme::success()),
consumer.name.style(theme::strong()),
theme::ARROW.style(theme::muted()),
connection.provider_name.style(theme::strong()),
);
println!(" {}", connection.summary.style(theme::muted()));
for key in &connection.keys {
println!(" {} {}", "+".style(theme::success()), key);
}
Ok(())
}
pub async fn candidates(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
) -> Result<()> {
let project = client.find_project(project_reference).await?;
let service = client.find_service(&project.id, service_reference).await?;
let candidates = client.candidates(&service.id).await?;
if candidates.is_empty() {
println!("No hay nada a lo que `{}` pueda conectarse.", service.name);
return Ok(());
}
for candidate in &candidates {
println!(
" {} {}",
candidate.name.style(theme::strong()),
candidate.summary.style(theme::muted()),
);
}
Ok(())
}
pub async fn add_domain(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
host: Option<&str>,
) -> Result<()> {
let project = client.find_project(project_reference).await?;
let service = client.find_service(&project.id, service_reference).await?;
let domain = client.add_domain(&service.id, host).await?;
println!(
" {} {}",
theme::CHECK.style(theme::success()),
domain.url.style(theme::accent())
);
if let Some(instructions) = &domain.dns_instructions {
println!(" {} {}", "!".style(theme::warning()), instructions);
}
Ok(())
}
pub async fn remove_domain(client: &ApiClient, domain_id: &str) -> Result<()> {
client.remove_domain(domain_id).await?;
println!(
" {} Dominio eliminado",
theme::CHECK.style(theme::success())
);
Ok(())
}
pub async fn stop(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
) -> Result<()> {
let project = client.find_project(project_reference).await?;
let service = client.find_service(&project.id, service_reference).await?;
client.stop(&service.id).await?;
println!(
" {} {} parado — despertará con la siguiente petición",
theme::CHECK.style(theme::success()),
service.name.style(theme::strong()),
);
Ok(())
}
pub struct ServiceRequest<'a> {
pub project: &'a str,
pub name: &'a str,
pub repo: Option<&'a str>,
pub image: Option<&'a str>,
pub template: Option<&'a str>,
pub branch: &'a str,
pub connect_to: &'a [String],
}
pub async fn create_service(client: &ApiClient, request: ServiceRequest<'_>) -> Result<()> {
let project = client.find_project(request.project).await?;
let source_kind = match (request.repo, request.image, request.template) {
(Some(_), None, None) => "github",
(None, Some(_), None) => "image",
(None, None, Some(_)) => "template",
(None, None, None) => bail!("say where it comes from: --repo, --image or --template"),
_ => bail!("--repo, --image and --template are alternatives, not a set"),
};
let mut providers = Vec::new();
for reference in request.connect_to {
providers.push(client.find_service(&project.id, reference).await?.id);
}
let draft = NewService {
name: request.name.to_owned(),
source_kind: source_kind.to_owned(),
repo_full_name: request.repo.map(str::to_owned),
branch: request.repo.map(|_| request.branch.to_owned()),
image: request.image.map(str::to_owned),
template_key: request.template.map(str::to_owned),
kind: request.template.map(|_| "database".to_owned()),
connect_to: providers,
};
if let Err(error) = client.create_service(&project.id, &draft).await {
return Err(match request.template {
Some(key) => explain_template_refusal(error, key),
None => error,
});
}
println!(
" {} {} creado en {}",
theme::CHECK.style(theme::success()),
request.name.style(theme::strong()),
project.name.style(theme::strong()),
);
if !request.connect_to.is_empty() {
println!(
" {} conectado a {}",
theme::ARROW.style(theme::muted()),
request.connect_to.join(", "),
);
}
Ok(())
}
fn explain_template_refusal(error: anyhow::Error, key: &str) -> anyhow::Error {
let missing = error
.downcast_ref::<ApiError>()
.is_some_and(|api| api.code == "not_found");
if !missing {
return error;
}
error.context(format!(
"el componente `{key}` no está disponible: o no existe en esta instancia, \
o solo su dueño puede añadirlo — `fundaia templates` lista los que \
esta cuenta sí puede crear"
))
}
pub async fn templates(client: &ApiClient) -> Result<()> {
for template in client.templates().await? {
println!(
" {} {} {}",
template.key.style(theme::strong()),
template.display_name,
template.image.style(theme::muted()),
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::StatusCode;
fn refusal(code: &str) -> anyhow::Error {
ApiError {
status: StatusCode::NOT_FOUND,
code: code.to_owned(),
message: "template 'claude-code' does not exist".to_owned(),
}
.into()
}
#[test]
fn it_should_say_who_may_add_a_component_the_server_refused() {
let explained = explain_template_refusal(refusal("not_found"), "claude-code");
assert!(explained
.to_string()
.contains("solo su dueño puede añadirlo"));
}
#[test]
fn it_should_name_the_component_it_could_not_add() {
let explained = explain_template_refusal(refusal("not_found"), "claude-code");
assert!(explained.to_string().contains("claude-code"));
}
#[test]
fn it_should_keep_the_server_answer_as_the_cause() {
let explained = explain_template_refusal(refusal("not_found"), "claude-code");
assert!(explained.downcast_ref::<ApiError>().is_some());
}
#[test]
fn it_should_leave_a_failure_that_is_not_a_missing_component_alone() {
let explained = explain_template_refusal(refusal("conflict"), "claude-code");
assert!(!explained.to_string().contains("dueño"));
}
}