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;
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_default_plugins(&mut registry);
match &args.action {
Some(action) => match action {
PluginAction::List => list_plugins(®istry),
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(®istry),
}
}
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 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);
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 {
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 plugins new {}` to create a scaffold.",
src, name
);
}
}
}
} else {
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()
);
}
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 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 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)?;
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)?;
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 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)?;
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 plugins list (should show your plugin)");
Ok(())
}