forge-guard 0.1.3

Pre-deployment smart contract auditing framework for Foundry
Documentation
//! `forge-guard plugins` — manage audit plugins.

use crate::core::ProjectConfig;
use crate::plugins::{register_default_plugins, PluginRegistry};

use super::{PluginAction, PluginArgs};
use anyhow::{Context, Result};
use colored::*;
use std::path::PathBuf;

/// Manage audit plugins.
pub fn run(args: &PluginArgs) -> Result<()> {
    let config = ProjectConfig::from_default_location();
    let mut registry =
        PluginRegistry::new(&config).context("Failed to initialize plugin registry")?;
    // Register built-in plugins
    register_default_plugins(&mut registry);

    match &args.action {
        Some(action) => match action {
            PluginAction::List => list_plugins(&registry),
            PluginAction::Install { name, source } => install_plugin(name, source.as_deref()),
            PluginAction::Remove { name } => remove_plugin(name),
            PluginAction::Enable { name } => enable_plugin(&mut registry, name),
            PluginAction::Disable { name } => disable_plugin(&mut registry, name),
            PluginAction::New { name } => create_plugin_scaffold(name),
        },
        None => list_plugins(&registry),
    }
}

fn list_plugins(registry: &PluginRegistry) -> Result<()> {
    let plugins = registry.list_plugins();

    println!("{}", "🔌 Installed Plugins".bold());
    println!("{}", "─────────────────────".dimmed());

    if plugins.is_empty() {
        println!("  No plugins installed.");
        println!("  Use `forge-guard plugins install <name>` to add one.");
        return Ok(());
    }

    println!(
        "  {} total ({} built-in, {} external)",
        plugins.len().to_string().bold(),
        plugins
            .iter()
            .filter(|p| matches!(p.plugin_type, crate::plugins::PluginType::Builtin))
            .count(),
        plugins
            .iter()
            .filter(|p| matches!(p.plugin_type, crate::plugins::PluginType::External))
            .count(),
    );
    println!();

    for plugin in &plugins {
        let status = if plugin.enabled {
            "✅ enabled".green()
        } else {
            "⛔ disabled".dimmed()
        };

        let kind = match plugin.plugin_type {
            crate::plugins::PluginType::Builtin => "[built-in]".dimmed(),
            crate::plugins::PluginType::External => "[external]".cyan(),
        };

        let path_display = plugin
            .path
            .as_ref()
            .map(|p| format!(" at {}", p.display()))
            .unwrap_or_default();

        println!(
            "  {} {:30} v{:<8} {} {}",
            status,
            plugin.name.bold(),
            plugin.version,
            kind,
            plugin.description,
        );
        if !path_display.is_empty() {
            println!("  {:>6}{}", "", path_display.dimmed());
        }
    }

    Ok(())
}

fn install_plugin(name: &str, source: Option<&str>) -> Result<()> {
    eprintln!("📦 Installing plugin: {}...", name.bold());

    let plugin_dir = PathBuf::from(".forge-guard").join("plugins").join(name);

    if plugin_dir.exists() {
        anyhow::bail!(
            "Plugin '{}' is already installed at {}",
            name,
            plugin_dir.display()
        );
    }

    std::fs::create_dir_all(&plugin_dir).with_context(|| {
        format!(
            "Failed to create plugin directory: {}",
            plugin_dir.display()
        )
    })?;

    if let Some(src) = source {
        eprintln!("   From: {}", src);
        // Try to clone from git source
        if src.ends_with(".git") || src.starts_with("https://") || src.starts_with("git@") {
            let status = std::process::Command::new("git")
                .args(["clone", src, &plugin_dir.to_string_lossy()])
                .stdout(std::process::Stdio::null())
                .stderr(std::process::Stdio::null())
                .status()
                .context("Failed to run git clone — is git installed?")?;

            if !status.success() {
                anyhow::bail!("Failed to clone plugin from '{}'", src);
            }
            eprintln!("   ✅ Cloned from {}", src);
        } else {
            // Try to download from a URL
            let status = std::process::Command::new("curl")
                .args([
                    "-sSL",
                    "-o",
                    &plugin_dir.join("plugin.tar.gz").to_string_lossy(),
                    src,
                ])
                .stdout(std::process::Stdio::null())
                .stderr(std::process::Stdio::null())
                .status();

            match status {
                Ok(s) if s.success() => {
                    eprintln!("   ✅ Downloaded from {}", src);
                }
                _ => {
                    anyhow::bail!(
                        "Could not install from '{}'. Provide a git URL or use `forge-guard plugins new {}` to create a scaffold.",
                        src, name
                    );
                }
            }
        }
    } else {
        // No source provided — create a basic plugin.toml
        let plugin_toml = format!(
            "[package]\nname = \"{}\"\nversion = \"0.1.0\"\ndescription = \"A forge-guard plugin\"\n\n# Binary to execute (relative to this directory)\nbinary = \"target/release/{}\"\n",
            name, name
        );
        std::fs::write(plugin_dir.join("plugin.toml"), &plugin_toml)
            .context("Failed to write plugin.toml")?;
        eprintln!("   ✅ Created plugin.toml scaffold");
    }

    eprintln!(
        "{} Plugin '{}' installed at {}.",
        "".green(),
        name,
        plugin_dir.display()
    );
    if source.is_none() {
        eprintln!(
            "   {}",
            "Edit plugin.toml and implement your plugin logic.".dimmed()
        );
        eprintln!(
            "   {}",
            "Plugins communicate via JSON IPC on stdin/stdout.".dimmed()
        );
        eprintln!(
            "   {}",
            "See forge-guard docs for the protocol specification.".dimmed()
        );
    }
    Ok(())
}

fn remove_plugin(name: &str) -> Result<()> {
    eprintln!("🗑️  Removing plugin: {}...", name.bold());

    let plugin_dir = PathBuf::from(".forge-guard").join("plugins").join(name);

    if !plugin_dir.exists() {
        anyhow::bail!(
            "Plugin '{}' is not installed at {}",
            name,
            plugin_dir.display()
        );
    }

    // Check if it's a built-in (cannot remove)
    if name.starts_with("forge-guard-") && name != "forge-guard-example" {
        anyhow::bail!(
            "Plugin '{}' is a built-in plugin and cannot be removed.",
            name
        );
    }

    std::fs::remove_dir_all(&plugin_dir).with_context(|| {
        format!(
            "Failed to remove plugin directory: {}",
            plugin_dir.display()
        )
    })?;

    eprintln!("{} Plugin '{}' removed.", "".green(), name);
    Ok(())
}

fn enable_plugin(registry: &mut PluginRegistry, name: &str) -> Result<()> {
    if registry.enable_plugin(name) {
        eprintln!("{} Plugin '{}' enabled.", "".green(), name);
    } else {
        anyhow::bail!(
            "Plugin '{}' not found. Use `forge-guard plugins list` to see all plugins.",
            name
        );
    }
    Ok(())
}

fn disable_plugin(registry: &mut PluginRegistry, name: &str) -> Result<()> {
    if registry.disable_plugin(name) {
        eprintln!("{} Plugin '{}' disabled.", "".yellow(), name);
    } else {
        anyhow::bail!(
            "Plugin '{}' not found. Use `forge-guard plugins list` to see all plugins.",
            name
        );
    }
    Ok(())
}

fn create_plugin_scaffold(name: &str) -> Result<()> {
    let dir = PathBuf::from(".forge-guard").join("plugins").join(name);

    if dir.exists() {
        anyhow::bail!("Plugin directory already exists: {}", dir.display());
    }

    std::fs::create_dir_all(&dir)?;

    // Create plugin.toml metadata
    let plugin_toml = format!(
        "[package]\nname = \"{}\"\nversion = \"0.1.0\"\ndescription = \"A custom forge-guard plugin\"\n\n# Binary to execute (relative to this directory)\nbinary = \"target/release/{}\"\n\n# Protocol version\nprotocol-version = \"1.0\"\n",
        name, name
    );
    std::fs::write(dir.join("plugin.toml"), &plugin_toml)?;

    // Create a Rust plugin scaffold
    let _name_camel: String = name
        .split('-')
        .map(|s| {
            let mut c = s.chars();
            match c.next() {
                Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
                None => String::new(),
            }
        })
        .collect();

    let plugin_rs = format!(
        r#"//! {} — Forge Guard plugin
//!
//! Generated by `forge-guard plugins new {}`
//!
//! This plugin communicates with forge-guard via JSON IPC:
//! - Reads PluginIpcInput from stdin
//! - Writes PluginIpcOutput to stdout
//!
//! Run `cargo build --release` to compile, then the plugin
//! will be automatically discovered by forge-guard.

use serde::{{Deserialize, Serialize}};
use std::io::{{self, Read, Write}};

/// Input received from forge-guard.
#[derive(Debug, Deserialize)]
struct PluginInput {{
    protocol_version: String,
    plugin_name: String,
    context: serde_json::Value,
}}

/// Output sent back to forge-guard.
#[derive(Debug, Serialize)]
struct PluginOutput {{
    success: bool,
    #[serde(default)]
    findings: Vec<PluginFinding>,
    #[serde(default)]
    error: Option<String>,
    #[serde(default)]
    stats: PluginStats,
}}

#[derive(Debug, Serialize)]
struct PluginFinding {{
    title: String,
    description: String,
    severity: String,
    #[serde(default)]
    file: Option<String>,
    #[serde(default)]
    line: Option<usize>,
    #[serde(default)]
    column: Option<usize>,
    #[serde(default)]
    code_snippet: Option<String>,
    #[serde(default)]
    recommendation: Option<String>,
    #[serde(default)]
    category: Option<String>,
    #[serde(default)]
    blocks_deployment: bool,
    #[serde(default)]
    references: Vec<String>,
}}

#[derive(Debug, Serialize, Default)]
struct PluginStats {{
    files_analyzed: u32,
    duration_ms: u64,
}}

fn main() {{
    // Read the JSON input from stdin
    let mut input = String::new();
    io::stdin().read_to_string(&mut input).expect("Failed to read stdin");

    let _input: PluginInput = match serde_json::from_str(&input) {{
        Ok(i) => i,
        Err(e) => {{
            let output = PluginOutput {{
                success: false,
                findings: Vec::new(),
                error: Some(format!("Failed to parse input: {{}}", e)),
                stats: PluginStats::default(),
            }};
            let json = serde_json::to_string(&output).unwrap();
            println!("{{}}", json);
            return;
        }}
    }};

    // TODO: Implement your analysis logic here
    // The `input.context` field contains project configuration and file paths

    let output = PluginOutput {{
        success: true,
        findings: Vec::new(),
        error: None,
        stats: PluginStats {{
            files_analyzed: 0,
            duration_ms: 0,
        }},
    }};

    let json = serde_json::to_string(&output).unwrap();
    println!("{{}}", json);
}}
"#,
        name, name
    );
    std::fs::write(dir.join("main.rs"), &plugin_rs)?;

    // Create Cargo.toml
    let cargo_toml = format!(
        "[package]\nname = \"forge-guard-plugin-{}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[[bin]]\nname = \"{}\"\npath = \"main.rs\"\n\n[dependencies]\nserde = {{ version = \"1\", features = [\"derive\"] }}\nserde_json = \"1\"\n",
        name, name
    );
    std::fs::write(dir.join("Cargo.toml"), &cargo_toml)?;

    eprintln!(
        "{} Plugin scaffold created at {}",
        "".green(),
        dir.display()
    );
    eprintln!("   📄 plugin.toml  — Plugin metadata");
    eprintln!("   📄 Cargo.toml   — Rust project configuration");
    eprintln!("   📄 main.rs      — Plugin implementation");
    eprintln!();
    eprintln!("   Next steps:");
    eprintln!("     cd {}", dir.display());
    eprintln!("     cargo build --release");
    eprintln!("     forge-guard plugins list  (should show your plugin)");
    Ok(())
}