use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest {
pub plugin: PluginInfo,
#[serde(default)]
pub commands: Vec<ManifestCommand>,
#[serde(default)]
pub config: Option<PluginConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginInfo {
pub name: String,
pub version: String,
pub description: String,
#[serde(default)]
pub author: String,
#[serde(default)]
pub license: String,
#[serde(default)]
pub homepage: String,
#[serde(default)]
pub repository: String,
#[serde(default)]
pub experimental: bool,
#[serde(default)]
pub min_meta_version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestCommand {
pub name: String,
pub description: String,
#[serde(default)]
pub long_description: Option<String>,
#[serde(default)]
pub aliases: Vec<String>,
#[serde(default)]
pub args: Vec<ManifestArg>,
#[serde(default)]
pub subcommands: Vec<ManifestCommand>,
#[serde(default)]
pub examples: Vec<Example>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestArg {
pub name: String,
#[serde(default)]
pub short: Option<char>,
#[serde(default)]
pub long: Option<String>,
pub help: String,
#[serde(default)]
pub required: bool,
#[serde(default)]
pub takes_value: bool,
#[serde(default)]
pub default_value: Option<String>,
#[serde(default)]
pub possible_values: Vec<String>,
#[serde(default)]
pub value_type: ArgValueType,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ArgValueType {
#[default]
String,
Number,
Bool,
Path,
Url,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Example {
pub command: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginConfig {
#[serde(default)]
pub execution: ExecutionConfig,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default)]
pub required_env: Vec<String>,
#[serde(default)]
pub dependencies: Vec<Dependency>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ExecutionConfig {
#[serde(default = "default_exec_mode")]
pub mode: String,
pub binary: Option<String>,
pub docker_image: Option<String>,
pub wasm_module: Option<String>,
#[serde(default = "default_protocol")]
pub protocol: String,
}
fn default_exec_mode() -> String {
"process".to_string()
}
fn default_protocol() -> String {
"cli".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
pub name: String,
pub version: String,
#[serde(default)]
pub optional: bool,
}
impl PluginManifest {
pub fn from_file(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
Self::from_toml_str(&content)
}
pub fn from_toml_str(content: &str) -> Result<Self> {
let manifest: PluginManifest = toml::from_str(content)?;
manifest.validate()?;
Ok(manifest)
}
pub fn validate(&self) -> Result<()> {
if self.plugin.name.is_empty() {
return Err(anyhow::anyhow!("Plugin name cannot be empty"));
}
if self.plugin.version.is_empty() {
return Err(anyhow::anyhow!("Plugin version cannot be empty"));
}
for cmd in &self.commands {
Self::validate_command(cmd)?;
}
if let Some(ref config) = self.config {
let exec = &config.execution;
match exec.mode.as_str() {
"process" => {
if exec.binary.is_none() {
return Err(anyhow::anyhow!("Binary path required for process mode"));
}
}
"docker" => {
if exec.docker_image.is_none() {
return Err(anyhow::anyhow!("Docker image required for docker mode"));
}
}
"wasm" => {
if exec.wasm_module.is_none() {
return Err(anyhow::anyhow!("WASM module required for wasm mode"));
}
}
mode => {
return Err(anyhow::anyhow!("Unknown execution mode: {}", mode));
}
}
}
Ok(())
}
fn validate_command(cmd: &ManifestCommand) -> Result<()> {
if cmd.name.is_empty() {
return Err(anyhow::anyhow!("Command name cannot be empty"));
}
for arg in &cmd.args {
if arg.name.is_empty() {
return Err(anyhow::anyhow!("Argument name cannot be empty"));
}
if !arg.required && arg.short.is_none() && arg.long.is_none() {
return Err(anyhow::anyhow!(
"Argument '{}' must have either short or long flag",
arg.name
));
}
}
for subcmd in &cmd.subcommands {
Self::validate_command(subcmd)?;
}
Ok(())
}
pub fn example() -> Self {
PluginManifest {
plugin: PluginInfo {
name: "example-plugin".to_string(),
version: "0.1.0".to_string(),
description: "An example metarepo plugin".to_string(),
author: "Your Name".to_string(),
license: "MIT".to_string(),
homepage: "https://github.com/yourusername/example-plugin".to_string(),
repository: "https://github.com/yourusername/example-plugin".to_string(),
experimental: false,
min_meta_version: Some("0.4.0".to_string()),
},
commands: vec![ManifestCommand {
name: "example".to_string(),
description: "Example command".to_string(),
long_description: Some(
"This is a longer description of the example command.".to_string(),
),
aliases: vec!["ex".to_string()],
args: vec![
ManifestArg {
name: "verbose".to_string(),
short: Some('v'),
long: Some("verbose".to_string()),
help: "Enable verbose output".to_string(),
required: false,
takes_value: false,
default_value: None,
possible_values: vec![],
value_type: ArgValueType::Bool,
},
ManifestArg {
name: "input".to_string(),
short: Some('i'),
long: Some("input".to_string()),
help: "Input file path".to_string(),
required: true,
takes_value: true,
default_value: None,
possible_values: vec![],
value_type: ArgValueType::Path,
},
],
subcommands: vec![ManifestCommand {
name: "run".to_string(),
description: "Run the example".to_string(),
long_description: None,
aliases: vec![],
args: vec![],
subcommands: vec![],
examples: vec![],
}],
examples: vec![Example {
command: "meta example -v --input file.txt run".to_string(),
description: "Run the example with verbose output".to_string(),
}],
}],
config: Some(PluginConfig {
execution: ExecutionConfig {
mode: "process".to_string(),
binary: Some("./bin/example-plugin".to_string()),
docker_image: None,
wasm_module: None,
protocol: "cli".to_string(),
},
capabilities: vec!["filesystem".to_string(), "network".to_string()],
required_env: vec![],
dependencies: vec![],
}),
}
}
pub fn write_example(path: &Path) -> Result<()> {
let manifest = Self::example();
let content = toml::to_string_pretty(&manifest)?;
std::fs::write(path, content)?;
Ok(())
}
}