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"
);
}
}