Skip to main content

context7_cli/
health.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Health check subcommand implementation.
3//!
4//! Validates config, API keys and API reachability.
5//!
6//! Exit codes follow BSD conventions:
7//! - 0  success (all checks passed)
8//! - 66 no API keys configured (EX_NOINPUT)
9//! - 69 API service unreachable (EX_UNAVAILABLE)
10//! - 74 configuration corrupt or unreadable (EX_IOERR)
11use std::sync::Arc;
12
13use anyhow::Result;
14use colored::Colorize;
15use serde::Serialize;
16use tokio::time::Duration;
17use tracing::info;
18
19use crate::api::{search_library, create_http_client, run_with_retry};
20use crate::i18n::{t, Message};
21use crate::output::{emit_ndjson, print_health_line, health_symbol};
22use crate::storage::load_api_keys;
23
24// ─── TYPES ───────────────────────────────────────────────────────────────────
25
26/// Consolidated result of the health checks.
27#[derive(Debug, Serialize)]
28pub struct HealthReport {
29    /// Whether the local config and HTTP client initialised successfully.
30    pub config_ok: bool,
31    /// Number of API keys currently configured.
32    pub keys_count: usize,
33    /// Whether the Context7 API responded during the probe.
34    pub api_reachable: bool,
35    /// Optional diagnostic detail (e.g. error message, timeout reason).
36    pub api_details: Option<String>,
37}
38
39// ─── ENTRY POINT ─────────────────────────────────────────────────────────────
40
41/// Runs the health checks and returns the appropriate BSD exit code.
42///
43/// Sequence: config → keys → API probe (timeout 10s).
44#[must_use]
45pub async fn run_health(json: bool) -> Result<i32> {
46    info!("Running health check");
47
48    if !json {
49        print_health_line(&t(Message::HealthRunning).cyan().to_string());
50    }
51
52    // ── Check 1: config / HTTP client ──────────────────────────────────────
53    let client = match create_http_client() {
54        Ok(c) => {
55            if !json {
56                print_health_line(&format!(
57                    "{} {}",
58                    health_symbol(true),
59                    t(Message::HealthConfigOk).green()
60                ));
61            }
62            c
63        }
64        Err(err) => {
65            let detail = err.to_string();
66            if json {
67                let report = HealthReport {
68                    config_ok: false,
69                    keys_count: 0,
70                    api_reachable: false,
71                    api_details: Some(detail.clone()),
72                };
73                emit_ndjson("health", &report);
74            } else {
75                print_health_line(&format!(
76                    "{} {} — {}",
77                    health_symbol(false),
78                    t(Message::HealthConfigFailed).red(),
79                    detail
80                ));
81            }
82            return Ok(74);
83        }
84    };
85
86    // ── Check 2: API keys ───────────────────────────────────────────────
87    let keys = match load_api_keys() {
88        Ok(c) if !c.is_empty() => {
89            if !json {
90                print_health_line(&format!(
91                    "{} {} {}",
92                    health_symbol(true),
93                    t(Message::HealthKeysOk).green(),
94                    c.len().to_string().bold()
95                ));
96            }
97            c
98        }
99        _ => {
100            if json {
101                let report = HealthReport {
102                    config_ok: true,
103                    keys_count: 0,
104                    api_reachable: false,
105                    api_details: None,
106                };
107                emit_ndjson("health", &report);
108            } else {
109                print_health_line(&format!(
110                    "{} {}",
111                    health_symbol(false),
112                    t(Message::HealthKeysMissing).yellow()
113                ));
114            }
115            return Ok(66);
116        }
117    };
118
119    // ── Check 3: API probe with 10s timeout ──────────────────────────────────
120    let client_arc = Arc::new(client);
121    let probe_result = tokio::time::timeout(
122        Duration::from_secs(10),
123        run_with_retry(&keys, move |key| {
124            let c = Arc::clone(&client_arc);
125            async move { search_library(&c, &key, "react", "health probe").await }
126        }),
127    )
128    .await;
129
130    let api_reachable = matches!(probe_result, Ok(Ok(_)));
131    let api_details = match &probe_result {
132        Err(_) => Some("timeout after 10s".to_string()),
133        Ok(Err(e)) => Some(e.to_string()),
134        Ok(Ok(_)) => None,
135    };
136
137    if json {
138        let report = HealthReport {
139            config_ok: true,
140            keys_count: keys.len(),
141            api_reachable,
142            api_details: api_details.clone(),
143        };
144        emit_ndjson("health", &report);
145    } else if api_reachable {
146        print_health_line(&format!(
147            "{} {}",
148            health_symbol(true),
149            t(Message::HealthApiOk).green()
150        ));
151    } else {
152        let detail = api_details.as_deref().unwrap_or("");
153        print_health_line(&format!(
154            "{} {} — {}",
155            health_symbol(false),
156            t(Message::HealthApiOffline).red(),
157            detail
158        ));
159    }
160
161    if api_reachable {
162        Ok(0)
163    } else {
164        Ok(69)
165    }
166}