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    /// Subcommand to execute.
45    #[command(subcommand)]
46    pub comando: Comando,
47}
48
49/// Top-level subcommands.
50#[derive(Debug, Subcommand)]
51pub enum Comando {
52    /// Search libraries by name.
53    #[command(alias = "lib", alias = "search")]
54    Library {
55        /// Library name to search for.
56        name: String,
57        /// Optional context for relevance ranking (e.g. "effect hooks").
58        query: Option<String>,
59    },
60
61    /// Fetch documentation for a library.
62    #[command(alias = "doc", alias = "context")]
63    Docs {
64        /// Library identifier (e.g. `/rust-lang/rust`).
65        library_id: String,
66
67        /// Topic or search query.
68        #[arg(short = 'q', long)]
69        query: Option<String>,
70
71        /// Output plain text instead of formatted output (incompatible with `--json`).
72        #[arg(long, conflicts_with = "json")]
73        text: bool,
74    },
75
76    /// Manage locally stored API keys.
77    #[command(alias = "key")]
78    Keys {
79        /// Key management operation.
80        #[command(subcommand)]
81        operacao: OperacaoKeys,
82    },
83
84    /// Generate shell completions for bash, zsh, fish, or PowerShell.
85    #[command(alias = "completion")]
86    Completions {
87        /// Shell to generate completions for.
88        shell: clap_complete::Shell,
89    },
90}
91
92/// Operations available under the `keys` subcommand.
93#[derive(Debug, Subcommand)]
94pub enum OperacaoKeys {
95    /// Add an API key to XDG storage.
96    Add {
97        /// API key to add (e.g. `ctx7sk-abc123…`).
98        key: String,
99    },
100    /// List all stored keys (masked).
101    List,
102    /// Remove a key by 1-based index (use `keys list` to see indices).
103    Remove {
104        /// Index of the key to remove (starting at 1).
105        index: usize,
106    },
107    /// Remove all stored keys.
108    Clear {
109        /// Confirm removal without an interactive prompt.
110        #[arg(long)]
111        yes: bool,
112    },
113    /// Print the XDG config file path.
114    Path,
115    /// Import keys from a `.env` file (reads `CONTEXT7_API=` entries).
116    Import {
117        /// Path to the `.env` file to import.
118        file: std::path::PathBuf,
119    },
120    /// Export all keys to stdout (one per line, unmasked).
121    Export,
122}
123
124// ─── DISPATCHERS ─────────────────────────────────────────────────────────────
125
126/// Dispatches `keys` subcommand operations — no HTTP client or API keys needed.
127pub fn executar_keys(operacao: OperacaoKeys, json: bool) -> Result<()> {
128    match operacao {
129        OperacaoKeys::Add { key } => cmd_keys_add(&key),
130        OperacaoKeys::List => cmd_keys_list(json),
131        OperacaoKeys::Remove { index } => cmd_keys_remove(index),
132        OperacaoKeys::Clear { yes } => cmd_keys_clear(yes),
133        OperacaoKeys::Path => cmd_keys_path(),
134        OperacaoKeys::Import { file } => cmd_keys_import(&file),
135        OperacaoKeys::Export => cmd_keys_export(),
136    }
137}
138
139/// Dispatches the `library` subcommand — searches libraries via the API.
140pub async fn executar_library(name: String, query: Option<String>, json: bool) -> Result<()> {
141    info!("Buscando biblioteca: {}", name);
142
143    let chaves = carregar_chaves_api()?;
144    let cliente = criar_cliente_http()?;
145
146    info!(
147        "Iniciando context7 com {} chaves de API disponíveis",
148        chaves.len()
149    );
150
151    // API requires the query parameter; fall back to the library name itself
152    let query_contexto = query.as_deref().unwrap_or(&name).to_string();
153
154    let cliente_arc = std::sync::Arc::new(cliente);
155    let name_clone = name.clone();
156    let query_clone = query_contexto.clone();
157    let resultado = executar_com_retry(&chaves, move |chave| {
158        let c = std::sync::Arc::clone(&cliente_arc);
159        let n = name_clone.clone();
160        let q = query_clone.clone();
161        async move { buscar_biblioteca(&c, &chave, &n, &q).await }
162    })
163    .await;
164
165    // Show hint before propagating BibliotecaNaoEncontrada
166    if let Err(ref e) = resultado {
167        if let Some(ErroContext7::BibliotecaNaoEncontrada { .. }) = e.downcast_ref::<ErroContext7>()
168        {
169            exibir_dica_biblioteca_nao_encontrada();
170        }
171    }
172
173    let resultado =
174        resultado.with_context(|| format!("{} '{}'", t(Mensagem::FalhaBuscarBiblioteca), name))?;
175
176    if json {
177        exibir_json_resultados(
178            &serde_json::to_string_pretty(&resultado.results)
179                .with_context(|| t(Mensagem::FalhaSerializarJson))?,
180        );
181    } else {
182        exibir_bibliotecas_formatado(&resultado.results);
183    }
184    Ok(())
185}
186
187/// Dispatches the `docs` subcommand — fetches library documentation via the API.
188pub async fn executar_docs(
189    library_id: String,
190    query: Option<String>,
191    text: bool,
192    json: bool,
193) -> Result<()> {
194    info!("Buscando documentação para: {}", library_id);
195
196    let chaves = carregar_chaves_api()?;
197    let cliente = criar_cliente_http()?;
198
199    info!(
200        "Iniciando context7 com {} chaves de API disponíveis",
201        chaves.len()
202    );
203
204    let cliente_arc = std::sync::Arc::new(cliente);
205    let id_clone = library_id.clone();
206    let query_clone = query.clone();
207
208    if text {
209        // Plain-text mode: use txt endpoint, print raw markdown
210        let texto = executar_com_retry(&chaves, move |chave| {
211            let c = std::sync::Arc::clone(&cliente_arc);
212            let id = id_clone.clone();
213            let q = query_clone.clone();
214            async move { buscar_documentacao_texto(&c, &chave, &id, q.as_deref()).await }
215        })
216        .await;
217
218        // Show hint before propagating BibliotecaNaoEncontrada
219        if let Err(ref e) = texto {
220            if let Some(ErroContext7::BibliotecaNaoEncontrada { .. }) =
221                e.downcast_ref::<ErroContext7>()
222            {
223                exibir_dica_biblioteca_nao_encontrada();
224            }
225        }
226
227        let texto = texto.with_context(|| {
228            format!("{} '{}'", t(Mensagem::FalhaBuscarDocumentacao), library_id)
229        })?;
230
231        exibir_texto_plano(&texto);
232        return Ok(());
233    }
234
235    // JSON or formatted mode: use json endpoint
236    let resultado = executar_com_retry(&chaves, move |chave| {
237        let c = std::sync::Arc::clone(&cliente_arc);
238        let id = id_clone.clone();
239        let q = query_clone.clone();
240        async move { buscar_documentacao(&c, &chave, &id, q.as_deref()).await }
241    })
242    .await;
243
244    // Show hint before propagating BibliotecaNaoEncontrada
245    if let Err(ref e) = resultado {
246        if let Some(ErroContext7::BibliotecaNaoEncontrada { .. }) = e.downcast_ref::<ErroContext7>()
247        {
248            exibir_dica_biblioteca_nao_encontrada();
249        }
250    }
251
252    let resultado = resultado
253        .with_context(|| format!("{} '{}'", t(Mensagem::FalhaBuscarDocumentacao), library_id))?;
254
255    if json {
256        exibir_json_resultados(
257            &serde_json::to_string_pretty(&resultado)
258                .with_context(|| t(Mensagem::FalhaSerializarDocs))?,
259        );
260    } else {
261        exibir_documentacao_formatada(&resultado);
262    }
263    Ok(())
264}