Skip to main content

context7_cli/
cli.rs

1/// CLI argument definitions and command dispatchers.
2///
3/// Defines [`Cli`], [`Comando`], and [`OperacaoKeys`] via `clap` derives,
4/// plus the async dispatcher functions that call into [`crate::api`],
5/// [`crate::storage`], and [`crate::output`].
6use anyhow::{Context, Result};
7use clap::{Parser, Subcommand};
8use tracing::info;
9
10use crate::api::{
11    buscar_biblioteca, buscar_documentacao, buscar_documentacao_texto, criar_cliente_http,
12    executar_com_retry,
13};
14use crate::errors::ErroContext7;
15use crate::i18n::{t, Mensagem};
16use crate::output::{
17    exibir_bibliotecas_formatado, exibir_dica_biblioteca_nao_encontrada,
18    exibir_documentacao_formatada, exibir_json_resultados, exibir_texto_plano,
19};
20use crate::storage::{
21    carregar_chaves_api, cmd_keys_add, cmd_keys_clear, cmd_keys_export, cmd_keys_import,
22    cmd_keys_list, cmd_keys_path, cmd_keys_remove,
23};
24
25// ─── STRUCTS CLI ─────────────────────────────────────────────────────────────
26
27/// Top-level CLI entry point parsed by `clap`.
28#[derive(Debug, Parser)]
29#[command(
30    name = "context7",
31    version,
32    about = "CLI client for the Context7 API (bilingual EN/PT)",
33    long_about = None,
34)]
35pub struct Cli {
36    /// UI language: `en` or `pt`. Default: auto-detect from system locale.
37    #[arg(long, global = true, env = "CONTEXT7_LANG")]
38    pub lang: Option<String>,
39
40    /// Output raw JSON instead of formatted text.
41    #[arg(long, global = true)]
42    pub json: bool,
43
44    /// Disable colored output (also respected via NO_COLOR env var).
45    #[arg(long, global = true)]
46    pub no_color: bool,
47
48    /// Plain text output without ANSI formatting (incompatible with --json).
49    #[arg(long, global = true, conflicts_with = "json")]
50    pub plain: bool,
51
52    /// Increase verbosity (-v info, -vv debug, -vvv trace).
53    #[arg(short, long, global = true, action = clap::ArgAction::Count)]
54    pub verbose: u8,
55
56    /// Suppress all output except errors.
57    #[arg(long, global = true)]
58    pub quiet: bool,
59
60    /// Subcommand to execute.
61    #[command(subcommand)]
62    pub comando: Comando,
63}
64
65/// Top-level subcommands.
66#[derive(Debug, Subcommand)]
67pub enum Comando {
68    /// Search libraries by name.
69    #[command(alias = "lib", alias = "search")]
70    Library {
71        /// Library name to search for.
72        name: String,
73        /// Optional context for relevance ranking (e.g. "effect hooks").
74        query: Option<String>,
75    },
76
77    /// Fetch documentation for a library.
78    #[command(alias = "doc", alias = "context")]
79    Docs {
80        /// Library identifier (e.g. `/rust-lang/rust`).
81        library_id: String,
82
83        /// Topic or search query.
84        #[arg(short = 'q', long)]
85        query: Option<String>,
86
87        /// Output plain text instead of formatted output (incompatible with `--json`).
88        #[arg(long, conflicts_with = "json")]
89        text: bool,
90    },
91
92    /// Manage locally stored API keys.
93    #[command(alias = "key")]
94    Keys {
95        /// Key management operation.
96        #[command(subcommand)]
97        operacao: OperacaoKeys,
98    },
99
100    /// Generate shell completions for bash, zsh, fish, or PowerShell.
101    #[command(alias = "completion")]
102    Completions {
103        /// Shell to generate completions for.
104        shell: clap_complete::Shell,
105    },
106
107    /// Validate config, keys, and API reachability.
108    Health,
109}
110
111/// Operations available under the `keys` subcommand.
112#[derive(Debug, Subcommand)]
113pub enum OperacaoKeys {
114    /// Add an API key to XDG storage.
115    Add {
116        /// API key to add (e.g. `ctx7sk-abc123…`).
117        key: String,
118    },
119    /// List all stored keys (masked).
120    List,
121    /// Remove a key by 1-based index (use `keys list` to see indices).
122    Remove {
123        /// Index of the key to remove (starting at 1).
124        index: usize,
125    },
126    /// Remove all stored keys.
127    Clear {
128        /// Confirm removal without an interactive prompt.
129        #[arg(long)]
130        yes: bool,
131    },
132    /// Print the XDG config file path.
133    Path,
134    /// Import keys from a `.env` file (reads `CONTEXT7_API=` entries).
135    Import {
136        /// Path to the `.env` file to import.
137        file: std::path::PathBuf,
138    },
139    /// Export all keys to stdout (one per line, unmasked).
140    Export,
141}
142
143// ─── HELPERS INTERNOS ────────────────────────────────────────────────────────
144
145/// Exibe dica se o erro for `BibliotecaNaoEncontrada`.
146fn verificar_e_exibir_dica_nao_encontrada<T>(resultado: &anyhow::Result<T>) {
147    if let Err(ref e) = resultado {
148        if let Some(ErroContext7::BibliotecaNaoEncontrada { .. }) = e.downcast_ref::<ErroContext7>()
149        {
150            exibir_dica_biblioteca_nao_encontrada();
151        }
152    }
153}
154
155// ─── DISPATCHERS ─────────────────────────────────────────────────────────────
156
157/// Dispatches `keys` subcommand operations — no HTTP client or API keys needed.
158pub fn executar_keys(operacao: OperacaoKeys, json: bool) -> Result<()> {
159    match operacao {
160        OperacaoKeys::Add { key } => cmd_keys_add(&key),
161        OperacaoKeys::List => cmd_keys_list(json),
162        OperacaoKeys::Remove { index } => cmd_keys_remove(index),
163        OperacaoKeys::Clear { yes } => cmd_keys_clear(yes),
164        OperacaoKeys::Path => cmd_keys_path(),
165        OperacaoKeys::Import { file } => cmd_keys_import(&file),
166        OperacaoKeys::Export => cmd_keys_export(),
167    }
168}
169
170/// Dispatches the `library` subcommand — searches libraries via the API.
171pub async fn executar_library(name: String, query: Option<String>, json: bool) -> Result<()> {
172    info!("Buscando biblioteca: {}", name);
173
174    let chaves = carregar_chaves_api()?;
175    let cliente = criar_cliente_http()?;
176
177    info!(
178        "Iniciando context7 com {} chaves de API disponíveis",
179        chaves.len()
180    );
181
182    // API requires the query parameter; fall back to the library name itself
183    let query_contexto = query.as_deref().unwrap_or(&name).to_string();
184
185    let cliente_arc = std::sync::Arc::new(cliente);
186    // Double-clone necessário: outer clone move ownership para closure Fn,
187    // inner clone cria cópia para cada iteração do retry (closure chamada N vezes).
188    let name_clone = name.clone();
189    let query_clone = query_contexto.clone();
190    let resultado = executar_com_retry(&chaves, move |chave| {
191        let c = std::sync::Arc::clone(&cliente_arc);
192        let n = name_clone.clone();
193        let q = query_clone.clone();
194        async move { buscar_biblioteca(&c, &chave, &n, &q).await }
195    })
196    .await;
197
198    // Show hint before propagating BibliotecaNaoEncontrada
199    verificar_e_exibir_dica_nao_encontrada(&resultado);
200
201    let resultado =
202        resultado.with_context(|| format!("{} '{}'", t(Mensagem::FalhaBuscarBiblioteca), name))?;
203
204    if json {
205        exibir_json_resultados(
206            &serde_json::to_string_pretty(&resultado.results)
207                .with_context(|| t(Mensagem::FalhaSerializarJson))?,
208        );
209    } else {
210        exibir_bibliotecas_formatado(&resultado.results);
211    }
212    Ok(())
213}
214
215/// Dispatches the `docs` subcommand — fetches library documentation via the API.
216pub async fn executar_docs(
217    library_id: String,
218    query: Option<String>,
219    text: bool,
220    json: bool,
221) -> Result<()> {
222    info!("Buscando documentação para: {}", library_id);
223
224    let chaves = carregar_chaves_api()?;
225    let cliente = criar_cliente_http()?;
226
227    info!(
228        "Iniciando context7 com {} chaves de API disponíveis",
229        chaves.len()
230    );
231
232    let cliente_arc = std::sync::Arc::new(cliente);
233    // Double-clone necessário: outer clone move ownership para closure Fn,
234    // inner clone cria cópia para cada iteração do retry (closure chamada N vezes).
235    let id_clone = library_id.clone();
236    let query_clone = query.clone();
237
238    if text {
239        // Plain-text mode: use txt endpoint, print raw markdown
240        let texto = executar_com_retry(&chaves, move |chave| {
241            let c = std::sync::Arc::clone(&cliente_arc);
242            let id = id_clone.clone();
243            let q = query_clone.clone();
244            async move { buscar_documentacao_texto(&c, &chave, &id, q.as_deref()).await }
245        })
246        .await;
247
248        // Show hint before propagating BibliotecaNaoEncontrada
249        verificar_e_exibir_dica_nao_encontrada(&texto);
250
251        let texto = texto.with_context(|| {
252            format!("{} '{}'", t(Mensagem::FalhaBuscarDocumentacao), library_id)
253        })?;
254
255        exibir_texto_plano(&texto);
256        return Ok(());
257    }
258
259    // JSON or formatted mode: use json endpoint
260    let resultado = executar_com_retry(&chaves, move |chave| {
261        let c = std::sync::Arc::clone(&cliente_arc);
262        let id = id_clone.clone();
263        let q = query_clone.clone();
264        async move { buscar_documentacao(&c, &chave, &id, q.as_deref()).await }
265    })
266    .await;
267
268    // Show hint before propagating BibliotecaNaoEncontrada
269    verificar_e_exibir_dica_nao_encontrada(&resultado);
270
271    let resultado = resultado
272        .with_context(|| format!("{} '{}'", t(Mensagem::FalhaBuscarDocumentacao), library_id))?;
273
274    if json {
275        exibir_json_resultados(
276            &serde_json::to_string_pretty(&resultado)
277                .with_context(|| t(Mensagem::FalhaSerializarDocs))?,
278        );
279    } else {
280        exibir_documentacao_formatada(&resultado);
281    }
282    Ok(())
283}