Skip to main content

context7_cli/
health.rs

1/// Health check subcommand — validates config, API keys and API reachability.
2///
3/// Exit codes follow BSD conventions:
4/// - 0  success (all checks passed)
5/// - 66 no API keys configured (EX_NOINPUT)
6/// - 69 API service unreachable (EX_UNAVAILABLE)
7/// - 74 configuration corrupt or unreadable (EX_IOERR)
8use std::sync::Arc;
9
10use anyhow::Result;
11use colored::Colorize;
12use serde::Serialize;
13use tokio::time::Duration;
14use tracing::info;
15
16use crate::api::{buscar_biblioteca, criar_cliente_http, executar_com_retry};
17use crate::i18n::{t, Mensagem};
18use crate::output::{emitir_ndjson, imprimir_linha_health, simbolo_health};
19use crate::storage::carregar_chaves_api;
20
21// ─── TIPOS ───────────────────────────────────────────────────────────────────
22
23/// Resultado consolidado dos checks de saúde.
24#[derive(Debug, Serialize)]
25pub struct RelatorioHealth {
26    pub config_ok: bool,
27    pub keys_count: usize,
28    pub api_alcancavel: bool,
29    pub detalhes_api: Option<String>,
30}
31
32// ─── ENTRY POINT ─────────────────────────────────────────────────────────────
33
34/// Executa os checks de saúde e retorna o exit code BSD adequado.
35///
36/// Sequência: config → keys → API probe (timeout 10s).
37pub async fn executar_health(json: bool) -> Result<i32> {
38    info!("Iniciando health check");
39
40    if !json {
41        imprimir_linha_health(&t(Mensagem::HealthExecutando).cyan().to_string());
42    }
43
44    // ── Check 1: config / cliente HTTP ──────────────────────────────────────
45    let cliente = match criar_cliente_http() {
46        Ok(c) => {
47            if !json {
48                imprimir_linha_health(&format!(
49                    "{} {}",
50                    simbolo_health(true),
51                    t(Mensagem::HealthConfigOk).green()
52                ));
53            }
54            c
55        }
56        Err(err) => {
57            let detalhe = err.to_string();
58            if json {
59                let relatorio = RelatorioHealth {
60                    config_ok: false,
61                    keys_count: 0,
62                    api_alcancavel: false,
63                    detalhes_api: Some(detalhe.clone()),
64                };
65                emitir_ndjson("health", &relatorio);
66            } else {
67                imprimir_linha_health(&format!(
68                    "{} {} — {}",
69                    simbolo_health(false),
70                    t(Mensagem::HealthConfigFalhou).red(),
71                    detalhe
72                ));
73            }
74            return Ok(74);
75        }
76    };
77
78    // ── Check 2: chaves de API ───────────────────────────────────────────────
79    let chaves = match carregar_chaves_api() {
80        Ok(c) if !c.is_empty() => {
81            if !json {
82                imprimir_linha_health(&format!(
83                    "{} {} {}",
84                    simbolo_health(true),
85                    t(Mensagem::HealthKeysOk).green(),
86                    c.len().to_string().bold()
87                ));
88            }
89            c
90        }
91        _ => {
92            if json {
93                let relatorio = RelatorioHealth {
94                    config_ok: true,
95                    keys_count: 0,
96                    api_alcancavel: false,
97                    detalhes_api: None,
98                };
99                emitir_ndjson("health", &relatorio);
100            } else {
101                imprimir_linha_health(&format!(
102                    "{} {}",
103                    simbolo_health(false),
104                    t(Mensagem::HealthKeysFaltando).yellow()
105                ));
106            }
107            return Ok(66);
108        }
109    };
110
111    // ── Check 3: API probe com timeout 10s ──────────────────────────────────
112    let cliente_arc = Arc::new(cliente);
113    let probe_result = tokio::time::timeout(
114        Duration::from_secs(10),
115        executar_com_retry(&chaves, move |chave| {
116            let c = Arc::clone(&cliente_arc);
117            async move { buscar_biblioteca(&c, &chave, "react", "health probe").await }
118        }),
119    )
120    .await;
121
122    let api_alcancavel = matches!(probe_result, Ok(Ok(_)));
123    let detalhes_api = match &probe_result {
124        Err(_) => Some("timeout after 10s".to_string()),
125        Ok(Err(e)) => Some(e.to_string()),
126        Ok(Ok(_)) => None,
127    };
128
129    if json {
130        let relatorio = RelatorioHealth {
131            config_ok: true,
132            keys_count: chaves.len(),
133            api_alcancavel,
134            detalhes_api: detalhes_api.clone(),
135        };
136        emitir_ndjson("health", &relatorio);
137    } else if api_alcancavel {
138        imprimir_linha_health(&format!(
139            "{} {}",
140            simbolo_health(true),
141            t(Mensagem::HealthApiOk).green()
142        ));
143    } else {
144        let detalhe = detalhes_api.as_deref().unwrap_or("");
145        imprimir_linha_health(&format!(
146            "{} {} — {}",
147            simbolo_health(false),
148            t(Mensagem::HealthApiOffline).red(),
149            detalhe
150        ));
151    }
152
153    if api_alcancavel {
154        Ok(0)
155    } else {
156        Ok(69)
157    }
158}