Skip to main content

metarepo_core/
plugin_manifest.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::path::Path;
4
5/// Plugin manifest structure (plugin.toml)
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct PluginManifest {
8    /// Plugin metadata
9    pub plugin: PluginInfo,
10
11    /// Commands provided by the plugin
12    #[serde(default)]
13    pub commands: Vec<ManifestCommand>,
14
15    /// Plugin configuration options
16    #[serde(default)]
17    pub config: Option<PluginConfig>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PluginInfo {
22    pub name: String,
23    pub version: String,
24    pub description: String,
25    #[serde(default)]
26    pub author: String,
27    #[serde(default)]
28    pub license: String,
29    #[serde(default)]
30    pub homepage: String,
31    #[serde(default)]
32    pub repository: String,
33    #[serde(default)]
34    pub experimental: bool,
35    #[serde(default)]
36    pub min_meta_version: Option<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ManifestCommand {
41    pub name: String,
42    pub description: String,
43    #[serde(default)]
44    pub long_description: Option<String>,
45    #[serde(default)]
46    pub aliases: Vec<String>,
47    #[serde(default)]
48    pub args: Vec<ManifestArg>,
49    #[serde(default)]
50    pub subcommands: Vec<ManifestCommand>,
51    #[serde(default)]
52    pub examples: Vec<Example>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ManifestArg {
57    pub name: String,
58    #[serde(default)]
59    pub short: Option<char>,
60    #[serde(default)]
61    pub long: Option<String>,
62    pub help: String,
63    #[serde(default)]
64    pub required: bool,
65    #[serde(default)]
66    pub takes_value: bool,
67    #[serde(default)]
68    pub default_value: Option<String>,
69    #[serde(default)]
70    pub possible_values: Vec<String>,
71    #[serde(default)]
72    pub value_type: ArgValueType,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, Default)]
76#[serde(rename_all = "lowercase")]
77pub enum ArgValueType {
78    #[default]
79    String,
80    Number,
81    Bool,
82    Path,
83    Url,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct Example {
88    pub command: String,
89    pub description: String,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct PluginConfig {
94    /// How the plugin should be executed
95    #[serde(default)]
96    pub execution: ExecutionConfig,
97
98    /// Plugin capabilities
99    #[serde(default)]
100    pub capabilities: Vec<String>,
101
102    /// Required environment variables
103    #[serde(default)]
104    pub required_env: Vec<String>,
105
106    /// Plugin dependencies
107    #[serde(default)]
108    pub dependencies: Vec<Dependency>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, Default)]
112pub struct ExecutionConfig {
113    /// Execution mode: "process", "wasm", "docker"
114    #[serde(default = "default_exec_mode")]
115    pub mode: String,
116
117    /// Path to the executable (relative to manifest)
118    pub binary: Option<String>,
119
120    /// Docker image for docker mode
121    pub docker_image: Option<String>,
122
123    /// WASM module for wasm mode
124    pub wasm_module: Option<String>,
125
126    /// Communication protocol: "json-rpc", "cli", "grpc"
127    #[serde(default = "default_protocol")]
128    pub protocol: String,
129}
130
131fn default_exec_mode() -> String {
132    "process".to_string()
133}
134
135fn default_protocol() -> String {
136    "cli".to_string()
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct Dependency {
141    pub name: String,
142    pub version: String,
143    #[serde(default)]
144    pub optional: bool,
145}
146
147impl PluginManifest {
148    /// Load manifest from a TOML file
149    pub fn from_file(path: &Path) -> Result<Self> {
150        let content = std::fs::read_to_string(path)?;
151        Self::from_toml_str(&content)
152    }
153
154    /// Parse manifest from TOML string
155    pub fn from_toml_str(content: &str) -> Result<Self> {
156        let manifest: PluginManifest = toml::from_str(content)?;
157        manifest.validate()?;
158        Ok(manifest)
159    }
160
161    /// Validate the manifest
162    pub fn validate(&self) -> Result<()> {
163        // Validate plugin info
164        if self.plugin.name.is_empty() {
165            return Err(anyhow::anyhow!("Plugin name cannot be empty"));
166        }
167
168        if self.plugin.version.is_empty() {
169            return Err(anyhow::anyhow!("Plugin version cannot be empty"));
170        }
171
172        // Validate commands
173        for cmd in &self.commands {
174            Self::validate_command(cmd)?;
175        }
176
177        // Validate execution config if present
178        if let Some(ref config) = self.config {
179            let exec = &config.execution;
180            match exec.mode.as_str() {
181                "process" => {
182                    if exec.binary.is_none() {
183                        return Err(anyhow::anyhow!("Binary path required for process mode"));
184                    }
185                }
186                "docker" => {
187                    if exec.docker_image.is_none() {
188                        return Err(anyhow::anyhow!("Docker image required for docker mode"));
189                    }
190                }
191                "wasm" => {
192                    if exec.wasm_module.is_none() {
193                        return Err(anyhow::anyhow!("WASM module required for wasm mode"));
194                    }
195                }
196                mode => {
197                    return Err(anyhow::anyhow!("Unknown execution mode: {}", mode));
198                }
199            }
200        }
201
202        Ok(())
203    }
204
205    fn validate_command(cmd: &ManifestCommand) -> Result<()> {
206        if cmd.name.is_empty() {
207            return Err(anyhow::anyhow!("Command name cannot be empty"));
208        }
209
210        // Validate arguments
211        for arg in &cmd.args {
212            if arg.name.is_empty() {
213                return Err(anyhow::anyhow!("Argument name cannot be empty"));
214            }
215
216            // Ensure either short or long flag is provided for non-positional args
217            if !arg.required && arg.short.is_none() && arg.long.is_none() {
218                return Err(anyhow::anyhow!(
219                    "Argument '{}' must have either short or long flag",
220                    arg.name
221                ));
222            }
223        }
224
225        // Recursively validate subcommands
226        for subcmd in &cmd.subcommands {
227            Self::validate_command(subcmd)?;
228        }
229
230        Ok(())
231    }
232
233    /// Generate a sample manifest
234    pub fn example() -> Self {
235        PluginManifest {
236            plugin: PluginInfo {
237                name: "example-plugin".to_string(),
238                version: "0.1.0".to_string(),
239                description: "An example metarepo plugin".to_string(),
240                author: "Your Name".to_string(),
241                license: "MIT".to_string(),
242                homepage: "https://github.com/yourusername/example-plugin".to_string(),
243                repository: "https://github.com/yourusername/example-plugin".to_string(),
244                experimental: false,
245                min_meta_version: Some("0.4.0".to_string()),
246            },
247            commands: vec![ManifestCommand {
248                name: "example".to_string(),
249                description: "Example command".to_string(),
250                long_description: Some(
251                    "This is a longer description of the example command.".to_string(),
252                ),
253                aliases: vec!["ex".to_string()],
254                args: vec![
255                    ManifestArg {
256                        name: "verbose".to_string(),
257                        short: Some('v'),
258                        long: Some("verbose".to_string()),
259                        help: "Enable verbose output".to_string(),
260                        required: false,
261                        takes_value: false,
262                        default_value: None,
263                        possible_values: vec![],
264                        value_type: ArgValueType::Bool,
265                    },
266                    ManifestArg {
267                        name: "input".to_string(),
268                        short: Some('i'),
269                        long: Some("input".to_string()),
270                        help: "Input file path".to_string(),
271                        required: true,
272                        takes_value: true,
273                        default_value: None,
274                        possible_values: vec![],
275                        value_type: ArgValueType::Path,
276                    },
277                ],
278                subcommands: vec![ManifestCommand {
279                    name: "run".to_string(),
280                    description: "Run the example".to_string(),
281                    long_description: None,
282                    aliases: vec![],
283                    args: vec![],
284                    subcommands: vec![],
285                    examples: vec![],
286                }],
287                examples: vec![Example {
288                    command: "meta example -v --input file.txt run".to_string(),
289                    description: "Run the example with verbose output".to_string(),
290                }],
291            }],
292            config: Some(PluginConfig {
293                execution: ExecutionConfig {
294                    mode: "process".to_string(),
295                    binary: Some("./bin/example-plugin".to_string()),
296                    docker_image: None,
297                    wasm_module: None,
298                    protocol: "cli".to_string(),
299                },
300                capabilities: vec!["filesystem".to_string(), "network".to_string()],
301                required_env: vec![],
302                dependencies: vec![],
303            }),
304        }
305    }
306
307    /// Write example manifest to file
308    pub fn write_example(path: &Path) -> Result<()> {
309        let manifest = Self::example();
310        let content = toml::to_string_pretty(&manifest)?;
311        std::fs::write(path, content)?;
312        Ok(())
313    }
314}