use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use tracing::info;
use crate::api::{
search_library, fetch_documentation, fetch_documentation_text, create_http_client,
run_with_retry,
};
use crate::errors::Context7Error;
use crate::i18n::{t, Message};
use crate::output::{
print_libraries_formatted, print_library_not_found_hint,
print_documentation_formatted, print_json_results, print_plain_text,
};
use crate::storage::{
load_api_keys, cmd_keys_add, cmd_keys_clear, cmd_keys_export, cmd_keys_import,
cmd_keys_list, cmd_keys_path, cmd_keys_remove,
};
#[derive(Debug, Parser)]
#[command(
name = "context7",
version,
about = "CLI client for the Context7 API (bilingual EN/PT)",
long_about = None,
)]
pub struct Cli {
#[arg(long, global = true, env = "CONTEXT7_LANG")]
pub lang: Option<String>,
#[arg(long, global = true)]
pub json: bool,
#[arg(long, global = true)]
pub no_color: bool,
#[arg(long, global = true, conflicts_with = "json")]
pub plain: bool,
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(long, global = true)]
pub quiet: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
#[command(alias = "lib", alias = "search")]
Library {
name: String,
query: Option<String>,
},
#[command(alias = "doc", alias = "context")]
Docs {
library_id: String,
#[arg(short = 'q', long)]
query: Option<String>,
#[arg(long, conflicts_with = "json")]
text: bool,
},
#[command(alias = "key")]
Keys {
#[command(subcommand)]
operation: KeysOperation,
},
#[command(alias = "completion")]
Completions {
shell: clap_complete::Shell,
},
Health,
}
#[derive(Debug, Subcommand)]
pub enum KeysOperation {
Add {
key: String,
},
List,
Remove {
index: usize,
},
Clear {
#[arg(long)]
yes: bool,
},
Path,
Import {
file: std::path::PathBuf,
},
Export,
}
fn check_and_show_not_found_hint<T>(result: &anyhow::Result<T>) {
if let Err(ref e) = result {
if let Some(Context7Error::LibraryNotFound { .. }) = e.downcast_ref::<Context7Error>()
{
print_library_not_found_hint();
}
}
}
#[must_use]
pub fn run_keys(operation: KeysOperation, json: bool) -> Result<()> {
match operation {
KeysOperation::Add { key } => cmd_keys_add(&key),
KeysOperation::List => cmd_keys_list(json),
KeysOperation::Remove { index } => cmd_keys_remove(index),
KeysOperation::Clear { yes } => cmd_keys_clear(yes),
KeysOperation::Path => cmd_keys_path(),
KeysOperation::Import { file } => cmd_keys_import(&file),
KeysOperation::Export => cmd_keys_export(),
}
}
#[must_use]
pub async fn run_library(name: String, query: Option<String>, json: bool) -> Result<()> {
info!("Searching library: {}", name);
let keys = load_api_keys()?;
let client = create_http_client()?;
info!(
"Starting context7 with {} API keys available",
keys.len()
);
let context_query = query.as_deref().unwrap_or(&name).to_string();
let client_arc = std::sync::Arc::new(client);
let name_clone = name.clone();
let query_clone = context_query.clone();
let result = run_with_retry(&keys, move |key| {
let c = std::sync::Arc::clone(&client_arc);
let n = name_clone.clone();
let q = query_clone.clone();
async move { search_library(&c, &key, &n, &q).await }
})
.await;
check_and_show_not_found_hint(&result);
let result =
result.with_context(|| format!("{} '{}'", t(Message::LibrarySearchFailure), name))?;
if json {
print_json_results(
&serde_json::to_string_pretty(&result.results)
.with_context(|| t(Message::JsonSerialiseFailure))?,
);
} else {
print_libraries_formatted(&result.results);
}
Ok(())
}
#[must_use]
pub async fn run_docs(
library_id: String,
query: Option<String>,
text: bool,
json: bool,
) -> Result<()> {
info!("Fetching documentation for: {}", library_id);
let keys = load_api_keys()?;
let client = create_http_client()?;
info!(
"Starting context7 with {} API keys available",
keys.len()
);
let client_arc = std::sync::Arc::new(client);
let id_clone = library_id.clone();
let query_clone = query.clone();
if text {
let text_result = run_with_retry(&keys, move |key| {
let c = std::sync::Arc::clone(&client_arc);
let id = id_clone.clone();
let q = query_clone.clone();
async move { fetch_documentation_text(&c, &key, &id, q.as_deref()).await }
})
.await;
check_and_show_not_found_hint(&text_result);
let text_result = text_result.with_context(|| {
format!("{} '{}'", t(Message::DocsFetchFailure), library_id)
})?;
print_plain_text(&text_result);
return Ok(());
}
let result = run_with_retry(&keys, move |key| {
let c = std::sync::Arc::clone(&client_arc);
let id = id_clone.clone();
let q = query_clone.clone();
async move { fetch_documentation(&c, &key, &id, q.as_deref()).await }
})
.await;
check_and_show_not_found_hint(&result);
let result = result
.with_context(|| format!("{} '{}'", t(Message::DocsFetchFailure), library_id))?;
if json {
print_json_results(
&serde_json::to_string_pretty(&result)
.with_context(|| t(Message::DocsSerialiseFailure))?,
);
} else {
print_documentation_formatted(&result);
}
Ok(())
}