context7-cli 0.5.1

Search library documentation from your terminal — zero runtime, bilingual (EN/PT), multi-key rotation
Documentation
/// Health check subcommand — validates config, API keys and API reachability.
///
/// Exit codes follow BSD conventions:
/// - 0  success (all checks passed)
/// - 66 no API keys configured (EX_NOINPUT)
/// - 69 API service unreachable (EX_UNAVAILABLE)
/// - 74 configuration corrupt or unreadable (EX_IOERR)
use std::sync::Arc;

use anyhow::Result;
use colored::Colorize;
use serde::Serialize;
use tokio::time::Duration;
use tracing::info;

use crate::api::{buscar_biblioteca, criar_cliente_http, executar_com_retry};
use crate::i18n::{t, Mensagem};
use crate::output::{emitir_ndjson, imprimir_linha_health, simbolo_health};
use crate::storage::carregar_chaves_api;

// ─── TIPOS ───────────────────────────────────────────────────────────────────

/// Resultado consolidado dos checks de saúde.
#[derive(Debug, Serialize)]
pub struct RelatorioHealth {
    pub config_ok: bool,
    pub keys_count: usize,
    pub api_alcancavel: bool,
    pub detalhes_api: Option<String>,
}

// ─── ENTRY POINT ─────────────────────────────────────────────────────────────

/// Executa os checks de saúde e retorna o exit code BSD adequado.
///
/// Sequência: config → keys → API probe (timeout 10s).
pub async fn executar_health(json: bool) -> Result<i32> {
    info!("Iniciando health check");

    if !json {
        imprimir_linha_health(&t(Mensagem::HealthExecutando).cyan().to_string());
    }

    // ── Check 1: config / cliente HTTP ──────────────────────────────────────
    let cliente = match criar_cliente_http() {
        Ok(c) => {
            if !json {
                imprimir_linha_health(&format!(
                    "{} {}",
                    simbolo_health(true),
                    t(Mensagem::HealthConfigOk).green()
                ));
            }
            c
        }
        Err(err) => {
            let detalhe = err.to_string();
            if json {
                let relatorio = RelatorioHealth {
                    config_ok: false,
                    keys_count: 0,
                    api_alcancavel: false,
                    detalhes_api: Some(detalhe.clone()),
                };
                emitir_ndjson("health", &relatorio);
            } else {
                imprimir_linha_health(&format!(
                    "{} {}{}",
                    simbolo_health(false),
                    t(Mensagem::HealthConfigFalhou).red(),
                    detalhe
                ));
            }
            return Ok(74);
        }
    };

    // ── Check 2: chaves de API ───────────────────────────────────────────────
    let chaves = match carregar_chaves_api() {
        Ok(c) if !c.is_empty() => {
            if !json {
                imprimir_linha_health(&format!(
                    "{} {} {}",
                    simbolo_health(true),
                    t(Mensagem::HealthKeysOk).green(),
                    c.len().to_string().bold()
                ));
            }
            c
        }
        _ => {
            if json {
                let relatorio = RelatorioHealth {
                    config_ok: true,
                    keys_count: 0,
                    api_alcancavel: false,
                    detalhes_api: None,
                };
                emitir_ndjson("health", &relatorio);
            } else {
                imprimir_linha_health(&format!(
                    "{} {}",
                    simbolo_health(false),
                    t(Mensagem::HealthKeysFaltando).yellow()
                ));
            }
            return Ok(66);
        }
    };

    // ── Check 3: API probe com timeout 10s ──────────────────────────────────
    let cliente_arc = Arc::new(cliente);
    let probe_result = tokio::time::timeout(
        Duration::from_secs(10),
        executar_com_retry(&chaves, move |chave| {
            let c = Arc::clone(&cliente_arc);
            async move { buscar_biblioteca(&c, &chave, "react", "health probe").await }
        }),
    )
    .await;

    let api_alcancavel = matches!(probe_result, Ok(Ok(_)));
    let detalhes_api = match &probe_result {
        Err(_) => Some("timeout after 10s".to_string()),
        Ok(Err(e)) => Some(e.to_string()),
        Ok(Ok(_)) => None,
    };

    if json {
        let relatorio = RelatorioHealth {
            config_ok: true,
            keys_count: chaves.len(),
            api_alcancavel,
            detalhes_api: detalhes_api.clone(),
        };
        emitir_ndjson("health", &relatorio);
    } else if api_alcancavel {
        imprimir_linha_health(&format!(
            "{} {}",
            simbolo_health(true),
            t(Mensagem::HealthApiOk).green()
        ));
    } else {
        let detalhe = detalhes_api.as_deref().unwrap_or("");
        imprimir_linha_health(&format!(
            "{} {}{}",
            simbolo_health(false),
            t(Mensagem::HealthApiOffline).red(),
            detalhe
        ));
    }

    if api_alcancavel {
        Ok(0)
    } else {
        Ok(69)
    }
}