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 Health,
109}
110
111#[derive(Debug, Subcommand)]
113pub enum OperacaoKeys {
114 Add {
116 key: String,
118 },
119 List,
121 Remove {
123 index: usize,
125 },
126 Clear {
128 #[arg(long)]
130 yes: bool,
131 },
132 Path,
134 Import {
136 file: std::path::PathBuf,
138 },
139 Export,
141}
142
143fn verificar_e_exibir_dica_nao_encontrada<T>(resultado: &anyhow::Result<T>) {
147 if let Err(ref e) = resultado {
148 if let Some(ErroContext7::BibliotecaNaoEncontrada { .. }) = e.downcast_ref::<ErroContext7>()
149 {
150 exibir_dica_biblioteca_nao_encontrada();
151 }
152 }
153}
154
155pub fn executar_keys(operacao: OperacaoKeys, json: bool) -> Result<()> {
159 match operacao {
160 OperacaoKeys::Add { key } => cmd_keys_add(&key),
161 OperacaoKeys::List => cmd_keys_list(json),
162 OperacaoKeys::Remove { index } => cmd_keys_remove(index),
163 OperacaoKeys::Clear { yes } => cmd_keys_clear(yes),
164 OperacaoKeys::Path => cmd_keys_path(),
165 OperacaoKeys::Import { file } => cmd_keys_import(&file),
166 OperacaoKeys::Export => cmd_keys_export(),
167 }
168}
169
170pub async fn executar_library(name: String, query: Option<String>, json: bool) -> Result<()> {
172 info!("Buscando biblioteca: {}", name);
173
174 let chaves = carregar_chaves_api()?;
175 let cliente = criar_cliente_http()?;
176
177 info!(
178 "Iniciando context7 com {} chaves de API disponíveis",
179 chaves.len()
180 );
181
182 let query_contexto = query.as_deref().unwrap_or(&name).to_string();
184
185 let cliente_arc = std::sync::Arc::new(cliente);
186 let name_clone = name.clone();
189 let query_clone = query_contexto.clone();
190 let resultado = executar_com_retry(&chaves, move |chave| {
191 let c = std::sync::Arc::clone(&cliente_arc);
192 let n = name_clone.clone();
193 let q = query_clone.clone();
194 async move { buscar_biblioteca(&c, &chave, &n, &q).await }
195 })
196 .await;
197
198 verificar_e_exibir_dica_nao_encontrada(&resultado);
200
201 let resultado =
202 resultado.with_context(|| format!("{} '{}'", t(Mensagem::FalhaBuscarBiblioteca), name))?;
203
204 if json {
205 exibir_json_resultados(
206 &serde_json::to_string_pretty(&resultado.results)
207 .with_context(|| t(Mensagem::FalhaSerializarJson))?,
208 );
209 } else {
210 exibir_bibliotecas_formatado(&resultado.results);
211 }
212 Ok(())
213}
214
215pub async fn executar_docs(
217 library_id: String,
218 query: Option<String>,
219 text: bool,
220 json: bool,
221) -> Result<()> {
222 info!("Buscando documentação para: {}", library_id);
223
224 let chaves = carregar_chaves_api()?;
225 let cliente = criar_cliente_http()?;
226
227 info!(
228 "Iniciando context7 com {} chaves de API disponíveis",
229 chaves.len()
230 );
231
232 let cliente_arc = std::sync::Arc::new(cliente);
233 let id_clone = library_id.clone();
236 let query_clone = query.clone();
237
238 if text {
239 let texto = executar_com_retry(&chaves, move |chave| {
241 let c = std::sync::Arc::clone(&cliente_arc);
242 let id = id_clone.clone();
243 let q = query_clone.clone();
244 async move { buscar_documentacao_texto(&c, &chave, &id, q.as_deref()).await }
245 })
246 .await;
247
248 verificar_e_exibir_dica_nao_encontrada(&texto);
250
251 let texto = texto.with_context(|| {
252 format!("{} '{}'", t(Mensagem::FalhaBuscarDocumentacao), library_id)
253 })?;
254
255 exibir_texto_plano(&texto);
256 return Ok(());
257 }
258
259 let resultado = executar_com_retry(&chaves, move |chave| {
261 let c = std::sync::Arc::clone(&cliente_arc);
262 let id = id_clone.clone();
263 let q = query_clone.clone();
264 async move { buscar_documentacao(&c, &chave, &id, q.as_deref()).await }
265 })
266 .await;
267
268 verificar_e_exibir_dica_nao_encontrada(&resultado);
270
271 let resultado = resultado
272 .with_context(|| format!("{} '{}'", t(Mensagem::FalhaBuscarDocumentacao), library_id))?;
273
274 if json {
275 exibir_json_resultados(
276 &serde_json::to_string_pretty(&resultado)
277 .with_context(|| t(Mensagem::FalhaSerializarDocs))?,
278 );
279 } else {
280 exibir_documentacao_formatada(&resultado);
281 }
282 Ok(())
283}