1use anyhow::{Context, Result};
8use clap::{Parser, Subcommand};
9use tracing::info;
10
11use crate::api::{
12 search_library, fetch_documentation, fetch_documentation_text, create_http_client,
13 run_with_retry,
14};
15use crate::errors::Context7Error;
16use crate::i18n::{t, Message};
17use crate::output::{
18 print_libraries_formatted, print_library_not_found_hint,
19 print_documentation_formatted, print_json_results, print_plain_text,
20};
21use crate::storage::{
22 load_api_keys, cmd_keys_add, cmd_keys_clear, cmd_keys_export, cmd_keys_import,
23 cmd_keys_list, cmd_keys_path, cmd_keys_remove,
24};
25
26#[derive(Debug, Parser)]
30#[command(
31 name = "context7",
32 version,
33 about = "CLI client for the Context7 API (bilingual EN/PT)",
34 long_about = None,
35)]
36pub struct Cli {
37 #[arg(long, global = true, env = "CONTEXT7_LANG")]
39 pub lang: Option<String>,
40
41 #[arg(long, global = true)]
43 pub json: bool,
44
45 #[arg(long, global = true)]
47 pub no_color: bool,
48
49 #[arg(long, global = true, conflicts_with = "json")]
51 pub plain: bool,
52
53 #[arg(short, long, global = true, action = clap::ArgAction::Count)]
55 pub verbose: u8,
56
57 #[arg(long, global = true)]
59 pub quiet: bool,
60
61 #[command(subcommand)]
63 pub command: Command,
64}
65
66#[derive(Debug, Subcommand)]
68pub enum Command {
69 #[command(alias = "lib", alias = "search")]
71 Library {
72 name: String,
74 query: Option<String>,
76 },
77
78 #[command(alias = "doc", alias = "context")]
80 Docs {
81 library_id: String,
83
84 #[arg(short = 'q', long)]
86 query: Option<String>,
87
88 #[arg(long, conflicts_with = "json")]
90 text: bool,
91 },
92
93 #[command(alias = "key")]
95 Keys {
96 #[command(subcommand)]
98 operation: KeysOperation,
99 },
100
101 #[command(alias = "completion")]
103 Completions {
104 shell: clap_complete::Shell,
106 },
107
108 Health,
110}
111
112#[derive(Debug, Subcommand)]
114pub enum KeysOperation {
115 Add {
117 key: String,
119 },
120 List,
122 Remove {
124 index: usize,
126 },
127 Clear {
129 #[arg(long)]
131 yes: bool,
132 },
133 Path,
135 Import {
137 file: std::path::PathBuf,
139 },
140 Export,
142}
143
144fn check_and_show_not_found_hint<T>(result: &anyhow::Result<T>) {
148 if let Err(ref e) = result {
149 if let Some(Context7Error::LibraryNotFound { .. }) = e.downcast_ref::<Context7Error>()
150 {
151 print_library_not_found_hint();
152 }
153 }
154}
155
156#[must_use]
160pub fn run_keys(operation: KeysOperation, json: bool) -> Result<()> {
161 match operation {
162 KeysOperation::Add { key } => cmd_keys_add(&key),
163 KeysOperation::List => cmd_keys_list(json),
164 KeysOperation::Remove { index } => cmd_keys_remove(index),
165 KeysOperation::Clear { yes } => cmd_keys_clear(yes),
166 KeysOperation::Path => cmd_keys_path(),
167 KeysOperation::Import { file } => cmd_keys_import(&file),
168 KeysOperation::Export => cmd_keys_export(),
169 }
170}
171
172#[must_use]
174pub async fn run_library(name: String, query: Option<String>, json: bool) -> Result<()> {
175 info!("Searching library: {}", name);
176
177 let keys = load_api_keys()?;
178 let client = create_http_client()?;
179
180 info!(
181 "Starting context7 with {} API keys available",
182 keys.len()
183 );
184
185 let context_query = query.as_deref().unwrap_or(&name).to_string();
187
188 let client_arc = std::sync::Arc::new(client);
189 let name_clone = name.clone();
192 let query_clone = context_query.clone();
193 let result = run_with_retry(&keys, move |key| {
194 let c = std::sync::Arc::clone(&client_arc);
195 let n = name_clone.clone();
196 let q = query_clone.clone();
197 async move { search_library(&c, &key, &n, &q).await }
198 })
199 .await;
200
201 check_and_show_not_found_hint(&result);
203
204 let result =
205 result.with_context(|| format!("{} '{}'", t(Message::LibrarySearchFailure), name))?;
206
207 if json {
208 print_json_results(
209 &serde_json::to_string_pretty(&result.results)
210 .with_context(|| t(Message::JsonSerialiseFailure))?,
211 );
212 } else {
213 print_libraries_formatted(&result.results);
214 }
215 Ok(())
216}
217
218#[must_use]
220pub async fn run_docs(
221 library_id: String,
222 query: Option<String>,
223 text: bool,
224 json: bool,
225) -> Result<()> {
226 info!("Fetching documentation for: {}", library_id);
227
228 let keys = load_api_keys()?;
229 let client = create_http_client()?;
230
231 info!(
232 "Starting context7 with {} API keys available",
233 keys.len()
234 );
235
236 let client_arc = std::sync::Arc::new(client);
237 let id_clone = library_id.clone();
240 let query_clone = query.clone();
241
242 if text {
243 let text_result = run_with_retry(&keys, move |key| {
245 let c = std::sync::Arc::clone(&client_arc);
246 let id = id_clone.clone();
247 let q = query_clone.clone();
248 async move { fetch_documentation_text(&c, &key, &id, q.as_deref()).await }
249 })
250 .await;
251
252 check_and_show_not_found_hint(&text_result);
254
255 let text_result = text_result.with_context(|| {
256 format!("{} '{}'", t(Message::DocsFetchFailure), library_id)
257 })?;
258
259 print_plain_text(&text_result);
260 return Ok(());
261 }
262
263 let result = run_with_retry(&keys, move |key| {
265 let c = std::sync::Arc::clone(&client_arc);
266 let id = id_clone.clone();
267 let q = query_clone.clone();
268 async move { fetch_documentation(&c, &key, &id, q.as_deref()).await }
269 })
270 .await;
271
272 check_and_show_not_found_hint(&result);
274
275 let result = result
276 .with_context(|| format!("{} '{}'", t(Message::DocsFetchFailure), library_id))?;
277
278 if json {
279 print_json_results(
280 &serde_json::to_string_pretty(&result)
281 .with_context(|| t(Message::DocsSerialiseFailure))?,
282 );
283 } else {
284 print_documentation_formatted(&result);
285 }
286 Ok(())
287}