dkp 0.4.1

dkp — Domain Knowledge Pack management CLI
Documentation
use anyhow::Result;
use clap::Args;
use serde::Serialize;
use std::path::PathBuf;

use dkp_core::{Pack, registry::types::SearchResult as RegistrySearchResult, search::SearchIndex};

use comfy_table::{Table, presets::UTF8_FULL};

use crate::{
    cli::CmdCtx,
    cmd::registry::account::{load_credentials_from_ctx, resolve_registry_url},
    output::Render,
};

#[derive(Args, Debug)]
pub struct SearchArgs {
    /// Pack path (local mode) or search query terms (--registry mode)
    #[arg(trailing_var_arg = true, allow_hyphen_values = false)]
    pub args: Vec<String>,

    /// Search the registry index instead of a local pack
    #[arg(long)]
    pub registry: bool,

    /// Narrow results to a specific asset type
    #[arg(long, value_name = "TYPE")]
    pub r#type: Option<String>,

    /// Maximum number of results
    #[arg(long, default_value = "10")]
    pub limit: usize,

    // Registry-only flags
    #[arg(long, value_name = "NAME", requires = "registry")]
    pub domain: Option<String>,

    #[arg(long, value_name = "LEVEL", requires = "registry")]
    pub conformance: Option<String>,
}

#[derive(Debug, Serialize)]
struct SearchResults {
    query: String,
    results: Vec<dkp_core::search::SearchResult>,
}

impl Render for SearchResults {
    fn render_plain(&self) -> String {
        if self.results.is_empty() {
            return format!("No results for \"{}\"\n", self.query);
        }
        let mut out = String::new();
        for (i, r) in self.results.iter().enumerate() {
            out.push_str(&format!(
                "[{}] {} (score: {:.2})\n{}\n  Source: {}\n\n",
                r.id, r.title, r.score, r.excerpt, r.source_file
            ));
            let _ = i;
        }
        out
    }

    fn render_table(&self) -> String {
        if self.results.is_empty() {
            return format!("No results for \"{}\"\n", self.query);
        }
        let mut table = Table::new();
        table.load_preset(UTF8_FULL);
        table.set_header(["ID", "Type", "Title", "Score", "Excerpt"]);
        for r in &self.results {
            table.add_row([
                &r.id,
                &r.asset_type,
                &r.title,
                &format!("{:.2}", r.score),
                &r.excerpt,
            ]);
        }
        table.to_string()
    }
}

#[derive(Debug, Serialize)]
struct RegistrySearchResults {
    query: String,
    total: u64,
    results: Vec<RegistrySearchResult>,
}

impl Render for RegistrySearchResults {
    fn render_plain(&self) -> String {
        if self.results.is_empty() {
            return format!("No registry results for \"{}\"\n", self.query);
        }
        let mut out = format!(
            "{} result(s) for \"{}\" (showing {}):\n\n",
            self.total,
            self.query,
            self.results.len()
        );
        for r in &self.results {
            out.push_str(&format!(
                "{}@{} [{}]\n  {}\n  Domain: {}  Tags: {}\n\n",
                r.name,
                r.version,
                r.conformance,
                r.description.as_deref().unwrap_or(""),
                r.domain,
                r.tags.join(", "),
            ));
        }
        out
    }

    fn render_table(&self) -> String {
        if self.results.is_empty() {
            return format!("No registry results for \"{}\"\n", self.query);
        }
        let mut table = Table::new();
        table.load_preset(UTF8_FULL);
        table.set_header(["Name", "Version", "Domain", "Conformance", "Description"]);
        for r in &self.results {
            table.add_row([
                &r.name,
                &r.version,
                &r.domain,
                &r.conformance,
                r.description.as_deref().unwrap_or(""),
            ]);
        }
        table.to_string()
    }
}

pub async fn run(args: SearchArgs, cli: &CmdCtx) -> Result<()> {
    if args.registry {
        if args.args.is_empty() {
            anyhow::bail!("query required (usage: dkp search --registry <query>)");
        }
        let query = args.args.join(" ");
        let base = resolve_registry_url(&cli.config.registry.url);
        let token = load_credentials_from_ctx(&cli.config.registry.url)
            .ok()
            .flatten()
            .map(|(_, t)| t);
        let client = dkp_core::registry::RegistryClient::new(base, token);
        let resp = client
            .search(
                &query,
                args.domain.as_deref(),
                args.conformance.as_deref(),
                args.limit as u32,
                0,
            )
            .await?;
        RegistrySearchResults {
            query,
            total: resp.total,
            results: resp.results,
        }
        .print(cli.output);
        return Ok(());
    }

    let mut positional = args.args.into_iter();
    let pack_path: PathBuf = positional.next().map(PathBuf::from).ok_or_else(|| {
        anyhow::anyhow!("pack path required for local search (usage: dkp search <pack> <query>)")
    })?;
    let query = positional
        .next()
        .ok_or_else(|| anyhow::anyhow!("query required (usage: dkp search <pack> <query>)"))?;
    let pack = Pack::open(&pack_path)?;
    let index = SearchIndex::build(&pack)?;

    let fetch_limit = if args.r#type.is_some() {
        args.limit * 10
    } else {
        args.limit
    };
    let mut results = index.search(&query, fetch_limit)?;
    if let Some(ref type_filter) = args.r#type {
        results.retain(|r| r.asset_type == type_filter.as_str());
        results.truncate(args.limit);
    }

    SearchResults { query, results }.print(cli.output);
    Ok(())
}