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
108/// Operations available under the `keys` subcommand.
109#[derive(Debug, Subcommand)]
110pub enum OperacaoKeys {
111    /// Add an API key to XDG storage.
112    Add {
113        /// API key to add (e.g. `ctx7sk-abc123…`).
114        key: String,
115    },
116    /// List all stored keys (masked).
117    List,
118    /// Remove a key by 1-based index (use `keys list` to see indices).
119    Remove {
120        /// Index of the key to remove (starting at 1).
121        index: usize,
122    },
123    /// Remove all stored keys.
124    Clear {
125        /// Confirm removal without an interactive prompt.
126        #[arg(long)]
127        yes: bool,
128    },
129    /// Print the XDG config file path.
130    Path,
131    /// Import keys from a `.env` file (reads `CONTEXT7_API=` entries).
132    Import {
133        /// Path to the `.env` file to import.
134        file: std::path::PathBuf,
135    },
136    /// Export all keys to stdout (one per line, unmasked).
137    Export,
138}
139
140// ─── HELPERS INTERNOS ────────────────────────────────────────────────────────
141
142/// Exibe dica se o erro for `BibliotecaNaoEncontrada`.
143fn verificar_e_exibir_dica_nao_encontrada<T>(resultado: &anyhow::Result<T>) {
144    if let Err(ref e) = resultado {
145        if let Some(ErroContext7::BibliotecaNaoEncontrada { .. }) = e.downcast_ref::<ErroContext7>()
146        {
147            exibir_dica_biblioteca_nao_encontrada();
148        }
149    }
150}
151
152// ─── DISPATCHERS ─────────────────────────────────────────────────────────────
153
154/// Dispatches `keys` subcommand operations — no HTTP client or API keys needed.
155pub fn executar_keys(operacao: OperacaoKeys, json: bool) -> Result<()> {
156    match operacao {
157        OperacaoKeys::Add { key } => cmd_keys_add(&key),
158        OperacaoKeys::List => cmd_keys_list(json),
159        OperacaoKeys::Remove { index } => cmd_keys_remove(index),
160        OperacaoKeys::Clear { yes } => cmd_keys_clear(yes),
161        OperacaoKeys::Path => cmd_keys_path(),
162        OperacaoKeys::Import { file } => cmd_keys_import(&file),
163        OperacaoKeys::Export => cmd_keys_export(),
164    }
165}
166
167/// Dispatches the `library` subcommand — searches libraries via the API.
168pub async fn executar_library(name: String, query: Option<String>, json: bool) -> Result<()> {
169    info!("Buscando biblioteca: {}", name);
170
171    let chaves = carregar_chaves_api()?;
172    let cliente = criar_cliente_http()?;
173
174    info!(
175        "Iniciando context7 com {} chaves de API disponíveis",
176        chaves.len()
177    );
178
179    // API requires the query parameter; fall back to the library name itself
180    let query_contexto = query.as_deref().unwrap_or(&name).to_string();
181
182    let cliente_arc = std::sync::Arc::new(cliente);
183    let name_clone = name.clone();
184    let query_clone = query_contexto.clone();
185    let resultado = executar_com_retry(&chaves, move |chave| {
186        let c = std::sync::Arc::clone(&cliente_arc);
187        let n = name_clone.clone();
188        let q = query_clone.clone();
189        async move { buscar_biblioteca(&c, &chave, &n, &q).await }
190    })
191    .await;
192
193    // Show hint before propagating BibliotecaNaoEncontrada
194    verificar_e_exibir_dica_nao_encontrada(&resultado);
195
196    let resultado =
197        resultado.with_context(|| format!("{} '{}'", t(Mensagem::FalhaBuscarBiblioteca), name))?;
198
199    if json {
200        exibir_json_resultados(
201            &serde_json::to_string_pretty(&resultado.results)
202                .with_context(|| t(Mensagem::FalhaSerializarJson))?,
203        );
204    } else {
205        exibir_bibliotecas_formatado(&resultado.results);
206    }
207    Ok(())
208}
209
210/// Dispatches the `docs` subcommand — fetches library documentation via the API.
211pub async fn executar_docs(
212    library_id: String,
213    query: Option<String>,
214    text: bool,
215    json: bool,
216) -> Result<()> {
217    info!("Buscando documentação para: {}", library_id);
218
219    let chaves = carregar_chaves_api()?;
220    let cliente = criar_cliente_http()?;
221
222    info!(
223        "Iniciando context7 com {} chaves de API disponíveis",
224        chaves.len()
225    );
226
227    let cliente_arc = std::sync::Arc::new(cliente);
228    let id_clone = library_id.clone();
229    let query_clone = query.clone();
230
231    if text {
232        // Plain-text mode: use txt endpoint, print raw markdown
233        let texto = executar_com_retry(&chaves, move |chave| {
234            let c = std::sync::Arc::clone(&cliente_arc);
235            let id = id_clone.clone();
236            let q = query_clone.clone();
237            async move { buscar_documentacao_texto(&c, &chave, &id, q.as_deref()).await }
238        })
239        .await;
240
241        // Show hint before propagating BibliotecaNaoEncontrada
242        verificar_e_exibir_dica_nao_encontrada(&texto);
243
244        let texto = texto.with_context(|| {
245            format!("{} '{}'", t(Mensagem::FalhaBuscarDocumentacao), library_id)
246        })?;
247
248        exibir_texto_plano(&texto);
249        return Ok(());
250    }
251
252    // JSON or formatted mode: use json endpoint
253    let resultado = executar_com_retry(&chaves, move |chave| {
254        let c = std::sync::Arc::clone(&cliente_arc);
255        let id = id_clone.clone();
256        let q = query_clone.clone();
257        async move { buscar_documentacao(&c, &chave, &id, q.as_deref()).await }
258    })
259    .await;
260
261    // Show hint before propagating BibliotecaNaoEncontrada
262    verificar_e_exibir_dica_nao_encontrada(&resultado);
263
264    let resultado = resultado
265        .with_context(|| format!("{} '{}'", t(Mensagem::FalhaBuscarDocumentacao), library_id))?;
266
267    if json {
268        exibir_json_resultados(
269            &serde_json::to_string_pretty(&resultado)
270                .with_context(|| t(Mensagem::FalhaSerializarDocs))?,
271        );
272    } else {
273        exibir_documentacao_formatada(&resultado);
274    }
275    Ok(())
276}