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::i18n::{t, Mensagem};
15use crate::output::{exibir_bibliotecas_formatado, exibir_documentacao_formatada};
16use crate::storage::{
17    carregar_chaves_api, cmd_keys_add, cmd_keys_clear, cmd_keys_export, cmd_keys_import,
18    cmd_keys_list, cmd_keys_path, cmd_keys_remove,
19};
20
21// ─── STRUCTS CLI ─────────────────────────────────────────────────────────────
22
23/// Top-level CLI entry point parsed by `clap`.
24#[derive(Debug, Parser)]
25#[command(
26    name = "context7",
27    version,
28    about = "CLI client for the Context7 API (bilingual EN/PT)",
29    long_about = None,
30)]
31pub struct Cli {
32    /// UI language: `en` or `pt`. Default: auto-detect from system locale.
33    #[arg(long, global = true, env = "CONTEXT7_LANG")]
34    pub lang: Option<String>,
35
36    /// Output raw JSON instead of formatted text.
37    #[arg(long, global = true)]
38    pub json: bool,
39
40    /// Subcommand to execute.
41    #[command(subcommand)]
42    pub comando: Comando,
43}
44
45/// Top-level subcommands.
46#[derive(Debug, Subcommand)]
47pub enum Comando {
48    /// Search libraries by name.
49    #[command(alias = "lib", alias = "search")]
50    Library {
51        /// Library name to search for.
52        nome: String,
53        /// Optional context for relevance ranking (e.g. "hooks de efeito").
54        query: Option<String>,
55    },
56
57    /// Fetch documentation for a library.
58    #[command(alias = "doc", alias = "context")]
59    Docs {
60        /// Library identifier (e.g. `/rust-lang/rust`).
61        library_id: String,
62
63        /// Topic or search query.
64        #[arg(long)]
65        query: Option<String>,
66
67        /// Output plain text instead of formatted output (incompatible with `--json`).
68        #[arg(long, conflicts_with = "json")]
69        text: bool,
70    },
71
72    /// Manage locally stored API keys.
73    #[command(alias = "key")]
74    Keys {
75        /// Key management operation.
76        #[command(subcommand)]
77        operacao: OperacaoKeys,
78    },
79}
80
81/// Operations available under the `keys` subcommand.
82#[derive(Debug, Subcommand)]
83pub enum OperacaoKeys {
84    /// Add an API key to XDG storage.
85    Add {
86        /// API key to add (e.g. `ctx7sk-abc123…`).
87        chave: String,
88    },
89    /// List all stored keys (masked).
90    List,
91    /// Remove a key by 1-based index (use `keys list` to see indices).
92    Remove {
93        /// Index of the key to remove (starting at 1).
94        indice: usize,
95    },
96    /// Remove all stored keys.
97    Clear {
98        /// Confirm removal without an interactive prompt.
99        #[arg(long)]
100        yes: bool,
101    },
102    /// Print the XDG config file path.
103    Path,
104    /// Import keys from a `.env` file (reads `CONTEXT7_API=` entries).
105    Import {
106        /// Path to the `.env` file to import.
107        arquivo: std::path::PathBuf,
108    },
109    /// Export all keys to stdout (one per line, unmasked).
110    Export,
111}
112
113// ─── DISPATCHERS ─────────────────────────────────────────────────────────────
114
115/// Dispatches `keys` subcommand operations — no HTTP client or API keys needed.
116pub fn executar_keys(operacao: OperacaoKeys) -> Result<()> {
117    match operacao {
118        OperacaoKeys::Add { chave } => cmd_keys_add(&chave),
119        OperacaoKeys::List => cmd_keys_list(),
120        OperacaoKeys::Remove { indice } => cmd_keys_remove(indice),
121        OperacaoKeys::Clear { yes } => cmd_keys_clear(yes),
122        OperacaoKeys::Path => cmd_keys_path(),
123        OperacaoKeys::Import { arquivo } => cmd_keys_import(&arquivo),
124        OperacaoKeys::Export => cmd_keys_export(),
125    }
126}
127
128/// Dispatches the `library` subcommand — searches libraries via the API.
129pub async fn executar_library(nome: String, query: Option<String>, json: bool) -> Result<()> {
130    info!("Buscando biblioteca: {}", nome);
131
132    let chaves = carregar_chaves_api()?;
133    let cliente = criar_cliente_http()?;
134
135    info!(
136        "Iniciando context7 com {} chaves de API disponíveis",
137        chaves.len()
138    );
139
140    // API requires the query parameter; fall back to the library name itself
141    let query_contexto = query.as_deref().unwrap_or(&nome).to_string();
142
143    let cliente_arc = std::sync::Arc::new(cliente);
144    let nome_clone = nome.clone();
145    let query_clone = query_contexto.clone();
146    let resultado = executar_com_retry(&chaves, move |chave| {
147        let c = std::sync::Arc::clone(&cliente_arc);
148        let n = nome_clone.clone();
149        let q = query_clone.clone();
150        async move { buscar_biblioteca(&c, &chave, &n, &q).await }
151    })
152    .await
153    .with_context(|| format!("{} '{}'", t(Mensagem::FalhaBuscarBiblioteca), nome))?;
154
155    if json {
156        println!(
157            "{}",
158            serde_json::to_string_pretty(&resultado.results)
159                .with_context(|| t(Mensagem::FalhaSerializarJson))?
160        );
161    } else {
162        exibir_bibliotecas_formatado(&resultado.results);
163    }
164    Ok(())
165}
166
167/// Dispatches the `docs` subcommand — fetches library documentation via the API.
168pub async fn executar_docs(
169    library_id: String,
170    query: Option<String>,
171    text: bool,
172    json: bool,
173) -> Result<()> {
174    info!("Buscando documentação para: {}", library_id);
175
176    let chaves = carregar_chaves_api()?;
177    let cliente = criar_cliente_http()?;
178
179    info!(
180        "Iniciando context7 com {} chaves de API disponíveis",
181        chaves.len()
182    );
183
184    let cliente_arc = std::sync::Arc::new(cliente);
185    let id_clone = library_id.clone();
186    let query_clone = query.clone();
187
188    if text {
189        // Plain-text mode: use txt endpoint, print raw markdown
190        let texto = executar_com_retry(&chaves, move |chave| {
191            let c = std::sync::Arc::clone(&cliente_arc);
192            let id = id_clone.clone();
193            let q = query_clone.clone();
194            async move { buscar_documentacao_texto(&c, &chave, &id, q.as_deref()).await }
195        })
196        .await
197        .with_context(|| format!("{} '{}'", t(Mensagem::FalhaBuscarDocumentacao), library_id))?;
198
199        println!("{}", texto);
200        return Ok(());
201    }
202
203    // JSON or formatted mode: use json endpoint
204    let resultado = executar_com_retry(&chaves, move |chave| {
205        let c = std::sync::Arc::clone(&cliente_arc);
206        let id = id_clone.clone();
207        let q = query_clone.clone();
208        async move { buscar_documentacao(&c, &chave, &id, q.as_deref()).await }
209    })
210    .await
211    .with_context(|| format!("{} '{}'", t(Mensagem::FalhaBuscarDocumentacao), library_id))?;
212
213    if json {
214        println!(
215            "{}",
216            serde_json::to_string_pretty(&resultado)
217                .with_context(|| t(Mensagem::FalhaSerializarDocs))?
218        );
219    } else {
220        exibir_documentacao_formatada(&resultado);
221    }
222    Ok(())
223}