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