fledge 1.3.0

Dev lifecycle CLI. One tool for the dev loop, any language.
use anyhow::{Context, Result};
use console::style;

use super::PLUGINS_SEARCH_SCHEMA;

pub(crate) fn search_plugins(
    query: Option<&str>,
    author: Option<&str>,
    topic: Option<&str>,
    limit: usize,
    interactive: bool,
    json: bool,
) -> Result<()> {
    let sp = crate::spinner::Spinner::start("Searching GitHub for plugins:");

    let config = crate::config::Config::load().ok();
    let token = config.as_ref().and_then(|c| c.github_token());

    let query_str = crate::search::build_search_query(query, author, "fledge-plugin", topic);
    let limit_str = limit.to_string();
    let body = crate::github::github_api_get(
        "/search/repositories",
        token.as_deref(),
        &[
            ("q", &query_str),
            ("sort", "stars"),
            ("per_page", &limit_str),
        ],
    )
    .context("searching GitHub for plugins")?;

    sp.finish();

    let results = crate::search::parse_search_response(&body)?;

    if results.is_empty() {
        if json {
            let result = serde_json::json!({
                "schema_version": PLUGINS_SEARCH_SCHEMA,
                "results": [],
            });
            println!("{}", serde_json::to_string_pretty(&result)?);
        } else {
            println!(
                "{} No plugins found{}.",
                style("*").cyan().bold(),
                query
                    .map(|q| format!(" matching '{q}'"))
                    .unwrap_or_default()
            );
        }
        return Ok(());
    }

    if json {
        let entries: Vec<serde_json::Value> = results
            .iter()
            .map(|r| {
                let tier = crate::trust::determine_trust_tier_from_owner(&r.owner);
                serde_json::json!({
                    "name": r.name,
                    "full_name": r.full_name(),
                    "description": r.description,
                    "stars": r.stars,
                    "url": r.url,
                    "topics": r.topics,
                    "trust_tier": tier.label(),
                })
            })
            .collect();
        let result = serde_json::json!({
            "schema_version": PLUGINS_SEARCH_SCHEMA,
            "results": entries,
        });
        println!("{}", serde_json::to_string_pretty(&result)?);
        return Ok(());
    }

    if interactive {
        return interactive_search(&results);
    }

    print_results(&results);
    Ok(())
}

fn print_results(results: &[crate::search::SearchResult]) {
    println!("{}", style("Available plugins:").bold());
    let max_name = results
        .iter()
        .map(|r| r.full_name().len())
        .max()
        .unwrap_or(0);

    for r in results {
        let tier = crate::trust::determine_trust_tier_from_owner(&r.owner);
        let topics_str = if r.topics.is_empty() {
            String::new()
        } else {
            let filtered: Vec<&str> = r
                .topics
                .iter()
                .filter(|t| *t != "fledge-plugin")
                .map(|t| t.as_str())
                .collect();
            if filtered.is_empty() {
                String::new()
            } else {
                format!(" {}", style(filtered.join(", ")).cyan())
            }
        };
        println!(
            "  {:<width$}  [{}]  {}  {}{}",
            style(r.full_name()).green(),
            tier.styled_label(),
            style(&r.description).dim(),
            style(format!("{}", r.stars)).yellow(),
            topics_str,
            width = max_name,
        );
    }

    println!(
        "\n  Install with: {}",
        style("fledge plugins install <owner/repo>").cyan()
    );
    println!(
        "  Or use:       {}",
        style("fledge plugins search --interactive").cyan()
    );
}

fn interactive_search(results: &[crate::search::SearchResult]) -> Result<()> {
    crate::utils::require_interactive("--interactive")?;

    let term_width = console::Term::stdout().size().1 as usize;
    let max_item_width = term_width.saturating_sub(4);

    let items: Vec<String> = results
        .iter()
        .map(|r| {
            let tier = crate::trust::determine_trust_tier_from_owner(&r.owner);
            let line = format!(
                "{:<40} [{}]  {}",
                r.full_name(),
                tier.label(),
                r.description
            );
            if line.len() > max_item_width {
                format!("{}", &line[..max_item_width - 1])
            } else {
                line
            }
        })
        .collect();

    let selection = dialoguer::FuzzySelect::with_theme(&dialoguer::theme::ColorfulTheme::default())
        .with_prompt("Select a plugin to install")
        .items(&items)
        .default(0)
        .max_length(15)
        .highlight_matches(true)
        .interact_opt()?;

    let Some(idx) = selection else {
        println!("Cancelled.");
        return Ok(());
    };

    let chosen = &results[idx];
    println!(
        "\n  Installing {} ...\n",
        style(chosen.full_name()).green().bold()
    );

    super::install::install_action(Some(&chosen.full_name()), false, false, false)
}