fundaia 0.53.0

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
use anyhow::Result;
use owo_colors::OwoColorize;

use crate::api::client::ApiClient;
use crate::api::models::DeployHook;
use crate::output::Format;
use crate::render::sanitise;
use crate::render::table::{Cell, Table};
use crate::render::theme;
use crate::render::time;

pub async fn list(
    client: &ApiClient,
    project_reference: &str,
    service_reference: &str,
    format: Format,
) -> Result<()> {
    let (project, service) = client.locate(project_reference, service_reference).await?;
    let answer = client.deploy_hooks(&service.id).await?;

    if format.answered_in_json(&answer.raw) {
        return Ok(());
    }

    let mut table = Table::new(&["hook", "token", "última llamada"]);
    for hook in &answer.body.hooks {
        table.push(vec![
            Cell::styled(sanitise::plain(&hook.name), theme::strong()),
            Cell::styled(sanitise::plain(&hook.token_hint), theme::muted()),
            Cell::styled(last_call(hook), theme::muted()),
        ]);
    }

    table.print_or(format_args!(
        "`{}` no tiene deploy hooks. fundaia hooks add {} {} <nombre>",
        service.name, project.slug, service.slug
    ));
    Ok(())
}

pub async fn add(
    client: &ApiClient,
    project_reference: &str,
    service_reference: &str,
    name: &str,
    format: Format,
) -> Result<()> {
    let (_, service) = client.locate(project_reference, service_reference).await?;
    let answer = client.create_deploy_hook(&service.id, name).await?;

    if format.answered_in_json(&answer.raw) {
        return Ok(());
    }

    let created = &answer.body;
    println!();
    println!(
        "  {} {} despliega {} desde fuera",
        theme::CHECK.style(theme::success()),
        sanitise::plain(&created.hook.name).style(theme::strong()),
        service.name.style(theme::strong()),
    );
    println!(
        "  {}",
        "La URL es el secreto y no se volverá a mostrar: guárdala donde guardes tus secretos de CI."
            .style(theme::muted()),
    );
    println!();
    println!("  {}", created.url.style(theme::accent()));
    println!();
    println!(
        "  {} {}",
        "Pruébala con:".style(theme::muted()),
        curl(&created.url)
    );
    println!();
    Ok(())
}

pub async fn remove(
    client: &ApiClient,
    project_reference: &str,
    service_reference: &str,
    hook: &str,
    format: Format,
) -> Result<()> {
    let (_, service) = client.locate(project_reference, service_reference).await?;
    let answer = client.revoke_deploy_hook(&service.id, hook).await?;

    if format.answered_in_json(&answer) {
        return Ok(());
    }

    println!(
        "  {} {} revocado: su URL ya no despliega {}",
        theme::CHECK.style(theme::success()),
        sanitise::plain(hook).style(theme::strong()),
        service.name.style(theme::strong()),
    );
    Ok(())
}

fn last_call(hook: &DeployHook) -> String {
    match &hook.last_used_at {
        None => "nunca".to_owned(),
        Some(at) => time::relative_time(at).unwrap_or_else(|| at.clone()),
    }
}

fn curl(url: &str) -> String {
    format!("curl -X POST {url}")
}

#[cfg(test)]
mod tests {
    use super::*;

    fn hook(last_used_at: Option<&str>) -> DeployHook {
        DeployHook {
            id: "dhk_1".to_owned(),
            name: "CI".to_owned(),
            token_hint: "fdh_…abcd".to_owned(),
            created_at: "2026-10-16T10:00:00.000Z".to_owned(),
            last_used_at: last_used_at.map(str::to_owned),
        }
    }

    #[test]
    fn it_should_say_a_hook_was_never_called() {
        assert_eq!(last_call(&hook(None)), "nunca");
    }

    #[test]
    fn it_should_show_how_to_call_a_hook() {
        assert_eq!(
            curl("https://infrastructure.fundaia.com/api/infra/hooks/fdh_x"),
            "curl -X POST https://infrastructure.fundaia.com/api/infra/hooks/fdh_x"
        );
    }
}