1use 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#[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 #[arg(long, global = true, env = "CONTEXT7_LANG")]
38 pub lang: Option<String>,
39
40 #[arg(long, global = true)]
42 pub json: bool,
43
44 #[arg(long, global = true)]
46 pub no_color: bool,
47
48 #[arg(long, global = true, conflicts_with = "json")]
50 pub plain: bool,
51
52 #[arg(short, long, global = true, action = clap::ArgAction::Count)]
54 pub verbose: u8,
55
56 #[arg(long, global = true)]
58 pub quiet: bool,
59
60 #[command(subcommand)]
62 pub comando: Comando,
63}
64
65#[derive(Debug, Subcommand)]
67pub enum Comando {
68 #[command(alias = "lib", alias = "search")]
70 Library {
71 name: String,
73 query: Option<String>,
75 },
76
77 #[command(alias = "doc", alias = "context")]
79 Docs {
80 library_id: String,
82
83 #[arg(short = 'q', long)]
85 query: Option<String>,
86
87 #[arg(long, conflicts_with = "json")]
89 text: bool,
90 },
91
92 #[command(alias = "key")]
94 Keys {
95 #[command(subcommand)]
97 operacao: OperacaoKeys,
98 },
99
100 #[command(alias = "completion")]
102 Completions {
103 shell: clap_complete::Shell,
105 },
106}
107
108#[derive(Debug, Subcommand)]
110pub enum OperacaoKeys {
111 Add {
113 key: String,
115 },
116 List,
118 Remove {
120 index: usize,
122 },
123 Clear {
125 #[arg(long)]
127 yes: bool,
128 },
129 Path,
131 Import {
133 file: std::path::PathBuf,
135 },
136 Export,
138}
139
140fn 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
152pub 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
167pub 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 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 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
210pub 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 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 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 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 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}