Skip to main content

context7_cli/
lib.rs

1//! context7-cli library crate.
2//!
3//! Exposes the public module hierarchy and the top-level [`run`] entry point
4//! used by the binary crate in `src/main.rs`.
5//!
6//! # Module overview
7//!
8//! | Module | Responsibility |
9//! |---|---|
10//! | [`errors`] | Structured error types ([`errors::ErroContext7`]) |
11//! | [`i18n`] | Bilingual i18n (EN/PT) — [`i18n::Mensagem`] variants and [`i18n::t`] lookup |
12//! | [`storage`] | XDG config storage, four-layer key hierarchy, `keys` subcommand operations |
13//! | [`api`] | HTTP client, retry-with-rotation, Context7 API calls and response types |
14//! | [`output`] | All terminal output — the **only** module allowed to use `println!` |
15//! | [`cli`] | Clap structs, subcommand dispatchers |
16
17pub mod api;
18pub mod cli;
19pub mod errors;
20pub mod health;
21pub mod i18n;
22pub mod output;
23pub mod platform;
24pub mod storage;
25
26use anyhow::{Context, Result};
27use clap::{CommandFactory, Parser};
28use std::path::PathBuf;
29use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
30
31use cli::{Cli, Comando};
32
33// ─── LOGGING ─────────────────────────────────────────────────────────────────
34
35/// Wraps the `WorkerGuard` from `tracing-appender`.
36///
37/// **Must** be kept alive until the end of `main()` to guarantee that the
38/// non-blocking log writer flushes its buffer before the process exits.
39pub struct GuardaLog(#[allow(dead_code)] tracing_appender::non_blocking::WorkerGuard);
40
41/// Initialises dual logging: terminal (stderr with ANSI) and log file.
42///
43/// Deletes the previous log file before starting (rotation-by-deletion).
44/// Returns [`GuardaLog`] — the caller **must** keep it alive until exit.
45pub fn inicializar_logging() -> Result<GuardaLog> {
46    const NOME_BINARIO: &str = env!("CARGO_PKG_NAME");
47
48    // Attempt XDG state/log directory; fall back to relative `logs/`
49    let pasta_logs = storage::descobrir_caminho_logs_xdg().unwrap_or_else(|| {
50        let raiz_compile = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
51        if raiz_compile.join("Cargo.toml").exists() {
52            raiz_compile.join("logs")
53        } else {
54            PathBuf::from("logs")
55        }
56    });
57
58    let caminho_log = pasta_logs.join(format!("{}.log", NOME_BINARIO));
59
60    // Rotation by deletion: remove previous log before initialising
61    if caminho_log.exists() {
62        std::fs::remove_file(&caminho_log)
63            .with_context(|| format!("Falha ao deletar log anterior: {}", caminho_log.display()))?;
64    }
65
66    std::fs::create_dir_all(&pasta_logs)
67        .with_context(|| format!("Falha ao criar pasta de logs: {}", pasta_logs.display()))?;
68
69    let appender_arquivo =
70        tracing_appender::rolling::never(&pasta_logs, format!("{}.log", NOME_BINARIO));
71    let (escritor_nao_bloqueante, guard) = tracing_appender::non_blocking(appender_arquivo);
72
73    // Respect RUST_LOG; otherwise default to "context7=info"
74    let filtro = EnvFilter::try_from_default_env()
75        .unwrap_or_else(|_| EnvFilter::new(format!("{}=info", NOME_BINARIO)));
76
77    let camada_terminal = tracing_subscriber::fmt::layer()
78        .with_ansi(true)
79        .with_target(false)
80        .with_writer(std::io::stderr);
81
82    let camada_arquivo = tracing_subscriber::fmt::layer()
83        .with_ansi(false)
84        .with_target(true)
85        .with_writer(escritor_nao_bloqueante);
86
87    tracing_subscriber::registry()
88        .with(filtro)
89        .with(camada_terminal)
90        .with(camada_arquivo)
91        .init();
92
93    Ok(GuardaLog(guard))
94}
95
96// ─── ENTRY POINT ─────────────────────────────────────────────────────────────
97
98/// Main library entry point called from `src/main.rs`.
99///
100/// Parses CLI arguments, initialises the i18n language setting, then
101/// dispatches to the appropriate subcommand handler.
102/// Returns `Ok(())` on success or propagates any `anyhow::Error`.
103pub async fn run() -> Result<()> {
104    let args = Cli::parse();
105
106    // Resolve and lock the UI language as early as possible so every
107    // downstream call to `i18n::t()` sees a consistent language.
108    let idioma = i18n::resolver_idioma(args.lang.as_deref());
109    i18n::definir_idioma(idioma);
110
111    // Respeitar convenções de cores: NO_COLOR (qualquer valor) desabilita
112    if std::env::var("NO_COLOR").is_ok() {
113        colored::control::set_override(false);
114    }
115    // CLICOLOR_FORCE=1 força cores mesmo em pipe
116    if std::env::var("CLICOLOR_FORCE")
117        .map(|v| v == "1")
118        .unwrap_or(false)
119    {
120        colored::control::set_override(true);
121    }
122    // Flags CLI têm prioridade sobre env vars
123    if args.no_color || args.json || args.plain {
124        colored::control::set_override(false);
125    }
126    // Suprimir stdout quando --quiet for passado
127    output::definir_silencioso(args.quiet);
128
129    tokio::select! {
130        resultado = async {
131            match args.comando {
132                Comando::Keys { operacao } => cli::executar_keys(operacao, args.json),
133
134                Comando::Library { name, query } => cli::executar_library(name, query, args.json).await,
135
136                Comando::Docs {
137                    library_id,
138                    query,
139                    text,
140                } => cli::executar_docs(library_id, query, text, args.json).await,
141
142                Comando::Completions { shell } => {
143                    clap_complete::generate(
144                        shell,
145                        &mut cli::Cli::command(),
146                        "context7",
147                        &mut std::io::stdout(),
148                    );
149                    Ok(())
150                }
151
152                Comando::Health => {
153                    let code = health::executar_health(args.json).await?;
154                    if code != 0 {
155                        std::process::exit(code);
156                    }
157                    Ok(())
158                }
159            }
160        } => resultado,
161        _ = tokio::signal::ctrl_c() => {
162            tracing::warn!("Interrompido pelo usuário (Ctrl+C)");
163            std::process::exit(130)
164        }
165    }
166}
167
168// ─── TESTES ───────────────────────────────────────────────────────────────────
169
170#[cfg(test)]
171mod testes {
172    /// Smoke test: verify that the Duration import from tokio::time compiles correctly.
173    /// This guards against accidental removal of tokio::time re-exports.
174    #[test]
175    fn testa_duration_disponivel() {
176        let _ = tokio::time::Duration::from_millis(500);
177    }
178}