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;

struct Recommendation {
    repo: &'static str,
    reason: &'static str,
}

fn recommendations_for_language(lang: &str) -> Vec<Recommendation> {
    let mut recs = vec![
        Recommendation {
            repo: "CorvidLabs/fledge-plugin-github",
            reason: "PR and issue workflows",
        },
        Recommendation {
            repo: "CorvidLabs/fledge-plugin-deps",
            reason: "dependency checking",
        },
        Recommendation {
            repo: "CorvidLabs/fledge-plugin-metrics",
            reason: "project code metrics",
        },
        Recommendation {
            repo: "CorvidLabs/fledge-plugin-todo",
            reason: "TODO/FIXME tracking",
        },
        Recommendation {
            repo: "CorvidLabs/fledge-plugin-secrets",
            reason: "secret leak detection",
        },
        Recommendation {
            repo: "CorvidLabs/fledge-plugin-gitleaks",
            reason: "git history secret scanning",
        },
    ];

    match lang {
        "rust" => {
            recs.push(Recommendation {
                repo: "CorvidLabs/fledge-plugin-bench",
                reason: "Rust benchmarking",
            });
            recs.push(Recommendation {
                repo: "CorvidLabs/fledge-plugin-coverage",
                reason: "code coverage reporting",
            });
        }
        "node" => {
            recs.push(Recommendation {
                repo: "CorvidLabs/fledge-plugin-coverage",
                reason: "code coverage reporting",
            });
        }
        "python" => {
            recs.push(Recommendation {
                repo: "CorvidLabs/fledge-plugin-coverage",
                reason: "code coverage reporting",
            });
        }
        "go" => {
            recs.push(Recommendation {
                repo: "CorvidLabs/fledge-plugin-bench",
                reason: "benchmarking",
            });
            recs.push(Recommendation {
                repo: "CorvidLabs/fledge-plugin-coverage",
                reason: "code coverage reporting",
            });
        }
        "swift" => {
            recs.push(Recommendation {
                repo: "CorvidLabs/fledge-plugin-coverage",
                reason: "code coverage reporting",
            });
        }
        _ => {}
    }

    if std::path::Path::new("Dockerfile").exists()
        || std::path::Path::new("docker-compose.yml").exists()
    {
        recs.push(Recommendation {
            repo: "CorvidLabs/fledge-plugin-docker",
            reason: "Docker image management",
        });
    }

    if std::path::Path::new(".github").exists() {
        recs.push(Recommendation {
            repo: "CorvidLabs/fledge-plugin-github",
            reason: "GitHub Actions integration",
        });
    }

    recs
}

pub(crate) fn recommend_plugins(json: bool) -> Result<()> {
    let cwd = std::env::current_dir().context("getting current directory")?;
    let lang = crate::run::detect_project_type(&cwd);

    let registry =
        super::load_registry().unwrap_or_else(|_| super::PluginsRegistry { plugins: vec![] });
    let installed: Vec<&str> = registry.plugins.iter().map(|p| p.name.as_str()).collect();

    let recs = recommendations_for_language(lang);
    let new_recs: Vec<&Recommendation> = recs
        .iter()
        .filter(|r| {
            let name = r
                .repo
                .rsplit('/')
                .next()
                .unwrap_or("")
                .replace("fledge-plugin-", "");
            !installed.iter().any(|i| *i == name)
        })
        .collect();

    if json {
        let entries: Vec<serde_json::Value> = new_recs
            .iter()
            .map(|r| {
                serde_json::json!({
                    "repo": r.repo,
                    "reason": r.reason,
                })
            })
            .collect();
        let result = serde_json::json!({
            "schema_version": PLUGINS_SEARCH_SCHEMA,
            "language": lang,
            "installed_count": installed.len(),
            "recommendations": entries,
        });
        println!("{}", serde_json::to_string_pretty(&result)?);
        return Ok(());
    }

    println!(
        "{} Detected project type: {}\n",
        style("*").cyan().bold(),
        style(lang).green().bold()
    );

    if new_recs.is_empty() {
        println!(
            "  {} All recommended plugins are already installed!",
            style("").green()
        );
        return Ok(());
    }

    println!("{}", style("Recommended plugins:").bold());
    let max_name = new_recs.iter().map(|r| r.repo.len()).max().unwrap_or(0);
    for r in &new_recs {
        println!(
            "  {:<width$}  {}",
            style(r.repo).green(),
            style(r.reason).dim(),
            width = max_name,
        );
    }

    if !crate::utils::is_interactive() {
        println!(
            "\n  Install with: {}",
            style("fledge plugins install <owner/repo>").cyan()
        );
        return Ok(());
    }

    println!();
    let install_all = dialoguer::Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
        .with_prompt(format!(
            "Install all {} recommended plugins?",
            new_recs.len()
        ))
        .default(true)
        .interact()?;

    if !install_all {
        println!(
            "\n  Install individually: {}",
            style("fledge plugins install <owner/repo>").cyan()
        );
        return Ok(());
    }

    println!();
    for r in &new_recs {
        if let Err(e) = super::install::install_action(Some(r.repo), false, false, false) {
            eprintln!("  {} Failed to install {}: {}", style("").red(), r.repo, e);
        }
    }

    Ok(())
}