context7-cli 0.2.5

Search library documentation from your terminal — zero runtime, bilingual (EN/PT), multi-key rotation
Documentation
/// CLI argument definitions and command dispatchers.
///
/// Defines [`Cli`], [`Comando`], and [`OperacaoKeys`] via `clap` derives,
/// plus the async dispatcher functions that call into [`crate::api`],
/// [`crate::storage`], and [`crate::output`].
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use tracing::info;

use crate::api::{
    buscar_biblioteca, buscar_documentacao, buscar_documentacao_texto, criar_cliente_http,
    executar_com_retry,
};
use crate::errors::ErroContext7;
use crate::i18n::{t, Mensagem};
use crate::output::{
    exibir_bibliotecas_formatado, exibir_dica_biblioteca_nao_encontrada,
    exibir_documentacao_formatada,
};
use crate::storage::{
    carregar_chaves_api, cmd_keys_add, cmd_keys_clear, cmd_keys_export, cmd_keys_import,
    cmd_keys_list, cmd_keys_path, cmd_keys_remove,
};

// ─── STRUCTS CLI ─────────────────────────────────────────────────────────────

/// Top-level CLI entry point parsed by `clap`.
#[derive(Debug, Parser)]
#[command(
    name = "context7",
    version,
    about = "CLI client for the Context7 API (bilingual EN/PT)",
    long_about = None,
)]
pub struct Cli {
    /// UI language: `en` or `pt`. Default: auto-detect from system locale.
    #[arg(long, global = true, env = "CONTEXT7_LANG")]
    pub lang: Option<String>,

    /// Output raw JSON instead of formatted text.
    #[arg(long, global = true)]
    pub json: bool,

    /// Subcommand to execute.
    #[command(subcommand)]
    pub comando: Comando,
}

/// Top-level subcommands.
#[derive(Debug, Subcommand)]
pub enum Comando {
    /// Search libraries by name.
    #[command(alias = "lib", alias = "search")]
    Library {
        /// Library name to search for.
        name: String,
        /// Optional context for relevance ranking (e.g. "effect hooks").
        query: Option<String>,
    },

    /// Fetch documentation for a library.
    #[command(alias = "doc", alias = "context")]
    Docs {
        /// Library identifier (e.g. `/rust-lang/rust`).
        library_id: String,

        /// Topic or search query.
        #[arg(long)]
        query: Option<String>,

        /// Output plain text instead of formatted output (incompatible with `--json`).
        #[arg(long, conflicts_with = "json")]
        text: bool,
    },

    /// Manage locally stored API keys.
    #[command(alias = "key")]
    Keys {
        /// Key management operation.
        #[command(subcommand)]
        operacao: OperacaoKeys,
    },
}

/// Operations available under the `keys` subcommand.
#[derive(Debug, Subcommand)]
pub enum OperacaoKeys {
    /// Add an API key to XDG storage.
    Add {
        /// API key to add (e.g. `ctx7sk-abc123…`).
        key: String,
    },
    /// List all stored keys (masked).
    List,
    /// Remove a key by 1-based index (use `keys list` to see indices).
    Remove {
        /// Index of the key to remove (starting at 1).
        index: usize,
    },
    /// Remove all stored keys.
    Clear {
        /// Confirm removal without an interactive prompt.
        #[arg(long)]
        yes: bool,
    },
    /// Print the XDG config file path.
    Path,
    /// Import keys from a `.env` file (reads `CONTEXT7_API=` entries).
    Import {
        /// Path to the `.env` file to import.
        file: std::path::PathBuf,
    },
    /// Export all keys to stdout (one per line, unmasked).
    Export,
}

// ─── DISPATCHERS ─────────────────────────────────────────────────────────────

/// Dispatches `keys` subcommand operations — no HTTP client or API keys needed.
pub fn executar_keys(operacao: OperacaoKeys) -> Result<()> {
    match operacao {
        OperacaoKeys::Add { key } => cmd_keys_add(&key),
        OperacaoKeys::List => cmd_keys_list(),
        OperacaoKeys::Remove { index } => cmd_keys_remove(index),
        OperacaoKeys::Clear { yes } => cmd_keys_clear(yes),
        OperacaoKeys::Path => cmd_keys_path(),
        OperacaoKeys::Import { file } => cmd_keys_import(&file),
        OperacaoKeys::Export => cmd_keys_export(),
    }
}

/// Dispatches the `library` subcommand — searches libraries via the API.
pub async fn executar_library(name: String, query: Option<String>, json: bool) -> Result<()> {
    info!("Buscando biblioteca: {}", name);

    let chaves = carregar_chaves_api()?;
    let cliente = criar_cliente_http()?;

    info!(
        "Iniciando context7 com {} chaves de API disponíveis",
        chaves.len()
    );

    // API requires the query parameter; fall back to the library name itself
    let query_contexto = query.as_deref().unwrap_or(&name).to_string();

    let cliente_arc = std::sync::Arc::new(cliente);
    let name_clone = name.clone();
    let query_clone = query_contexto.clone();
    let resultado = executar_com_retry(&chaves, move |chave| {
        let c = std::sync::Arc::clone(&cliente_arc);
        let n = name_clone.clone();
        let q = query_clone.clone();
        async move { buscar_biblioteca(&c, &chave, &n, &q).await }
    })
    .await;

    // Show hint before propagating BibliotecaNaoEncontrada
    if let Err(ref e) = resultado {
        if let Some(ErroContext7::BibliotecaNaoEncontrada { .. }) = e.downcast_ref::<ErroContext7>()
        {
            exibir_dica_biblioteca_nao_encontrada();
        }
    }

    let resultado =
        resultado.with_context(|| format!("{} '{}'", t(Mensagem::FalhaBuscarBiblioteca), name))?;

    if json {
        println!(
            "{}",
            serde_json::to_string_pretty(&resultado.results)
                .with_context(|| t(Mensagem::FalhaSerializarJson))?
        );
    } else {
        exibir_bibliotecas_formatado(&resultado.results);
    }
    Ok(())
}

/// Dispatches the `docs` subcommand — fetches library documentation via the API.
pub async fn executar_docs(
    library_id: String,
    query: Option<String>,
    text: bool,
    json: bool,
) -> Result<()> {
    info!("Buscando documentação para: {}", library_id);

    let chaves = carregar_chaves_api()?;
    let cliente = criar_cliente_http()?;

    info!(
        "Iniciando context7 com {} chaves de API disponíveis",
        chaves.len()
    );

    let cliente_arc = std::sync::Arc::new(cliente);
    let id_clone = library_id.clone();
    let query_clone = query.clone();

    if text {
        // Plain-text mode: use txt endpoint, print raw markdown
        let texto = executar_com_retry(&chaves, move |chave| {
            let c = std::sync::Arc::clone(&cliente_arc);
            let id = id_clone.clone();
            let q = query_clone.clone();
            async move { buscar_documentacao_texto(&c, &chave, &id, q.as_deref()).await }
        })
        .await;

        // Show hint before propagating BibliotecaNaoEncontrada
        if let Err(ref e) = texto {
            if let Some(ErroContext7::BibliotecaNaoEncontrada { .. }) =
                e.downcast_ref::<ErroContext7>()
            {
                exibir_dica_biblioteca_nao_encontrada();
            }
        }

        let texto = texto.with_context(|| {
            format!("{} '{}'", t(Mensagem::FalhaBuscarDocumentacao), library_id)
        })?;

        println!("{}", texto);
        return Ok(());
    }

    // JSON or formatted mode: use json endpoint
    let resultado = executar_com_retry(&chaves, move |chave| {
        let c = std::sync::Arc::clone(&cliente_arc);
        let id = id_clone.clone();
        let q = query_clone.clone();
        async move { buscar_documentacao(&c, &chave, &id, q.as_deref()).await }
    })
    .await;

    // Show hint before propagating BibliotecaNaoEncontrada
    if let Err(ref e) = resultado {
        if let Some(ErroContext7::BibliotecaNaoEncontrada { .. }) = e.downcast_ref::<ErroContext7>()
        {
            exibir_dica_biblioteca_nao_encontrada();
        }
    }

    let resultado = resultado
        .with_context(|| format!("{} '{}'", t(Mensagem::FalhaBuscarDocumentacao), library_id))?;

    if json {
        println!(
            "{}",
            serde_json::to_string_pretty(&resultado)
                .with_context(|| t(Mensagem::FalhaSerializarDocs))?
        );
    } else {
        exibir_documentacao_formatada(&resultado);
    }
    Ok(())
}