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`]) and [`errors::Resultado`] alias |
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 i18n;
21pub mod output;
22pub mod storage;
23
24use anyhow::{Context, Result};
25use clap::Parser;
26use std::path::PathBuf;
27use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
28
29use cli::{Cli, Comando};
30
31// ─── LOGGING ─────────────────────────────────────────────────────────────────
32
33/// Wraps the `WorkerGuard` from `tracing-appender`.
34///
35/// **Must** be kept alive until the end of `main()` to guarantee that the
36/// non-blocking log writer flushes its buffer before the process exits.
37pub struct GuardaLog(#[allow(dead_code)] tracing_appender::non_blocking::WorkerGuard);
38
39/// Initialises dual logging: terminal (stderr with ANSI) and log file.
40///
41/// Deletes the previous log file before starting (rotation-by-deletion).
42/// Returns [`GuardaLog`] — the caller **must** keep it alive until exit.
43pub fn inicializar_logging() -> Result<GuardaLog> {
44 const NOME_BINARIO: &str = env!("CARGO_PKG_NAME");
45
46 // Attempt XDG state/log directory; fall back to relative `logs/`
47 let pasta_logs = storage::descobrir_caminho_logs_xdg().unwrap_or_else(|| {
48 let raiz_compile = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
49 if raiz_compile.join("Cargo.toml").exists() {
50 raiz_compile.join("logs")
51 } else {
52 PathBuf::from("logs")
53 }
54 });
55
56 let caminho_log = pasta_logs.join(format!("{}.log", NOME_BINARIO));
57
58 // Rotation by deletion: remove previous log before initialising
59 if caminho_log.exists() {
60 std::fs::remove_file(&caminho_log)
61 .with_context(|| format!("Falha ao deletar log anterior: {}", caminho_log.display()))?;
62 }
63
64 std::fs::create_dir_all(&pasta_logs)
65 .with_context(|| format!("Falha ao criar pasta de logs: {}", pasta_logs.display()))?;
66
67 let appender_arquivo =
68 tracing_appender::rolling::never(&pasta_logs, format!("{}.log", NOME_BINARIO));
69 let (escritor_nao_bloqueante, guard) = tracing_appender::non_blocking(appender_arquivo);
70
71 // Respect RUST_LOG; otherwise default to "context7=info"
72 let filtro = EnvFilter::try_from_default_env()
73 .unwrap_or_else(|_| EnvFilter::new(format!("{}=info", NOME_BINARIO)));
74
75 let camada_terminal = tracing_subscriber::fmt::layer()
76 .with_ansi(true)
77 .with_target(false)
78 .with_writer(std::io::stderr);
79
80 let camada_arquivo = tracing_subscriber::fmt::layer()
81 .with_ansi(false)
82 .with_target(true)
83 .with_writer(escritor_nao_bloqueante);
84
85 tracing_subscriber::registry()
86 .with(filtro)
87 .with(camada_terminal)
88 .with(camada_arquivo)
89 .init();
90
91 Ok(GuardaLog(guard))
92}
93
94// ─── ENTRY POINT ─────────────────────────────────────────────────────────────
95
96/// Main library entry point called from `src/main.rs`.
97///
98/// Parses CLI arguments, initialises the i18n language setting, then
99/// dispatches to the appropriate subcommand handler.
100/// Returns `Ok(())` on success or propagates any `anyhow::Error`.
101pub async fn run() -> Result<()> {
102 let args = Cli::parse();
103
104 // Resolve and lock the UI language as early as possible so every
105 // downstream call to `i18n::t()` sees a consistent language.
106 let idioma = i18n::resolver_idioma(args.lang.as_deref());
107 i18n::definir_idioma(idioma);
108
109 match args.comando {
110 Comando::Keys { operacao } => cli::executar_keys(operacao),
111
112 Comando::Library { nome, query } => cli::executar_library(nome, query, args.json).await,
113
114 Comando::Docs {
115 library_id,
116 query,
117 text,
118 } => cli::executar_docs(library_id, query, text, args.json).await,
119 }
120}
121
122// ─── TESTES ───────────────────────────────────────────────────────────────────
123
124#[cfg(test)]
125mod testes {
126 /// Smoke test: verify that the Duration import from tokio::time compiles correctly.
127 /// This guards against accidental removal of tokio::time re-exports.
128 #[test]
129 fn testa_duration_disponivel() {
130 let _ = tokio::time::Duration::from_millis(500);
131 }
132}