1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::path::Path;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct PluginManifest {
8 pub plugin: PluginInfo,
10
11 #[serde(default)]
13 pub commands: Vec<ManifestCommand>,
14
15 #[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 #[serde(default)]
96 pub execution: ExecutionConfig,
97
98 #[serde(default)]
100 pub capabilities: Vec<String>,
101
102 #[serde(default)]
104 pub required_env: Vec<String>,
105
106 #[serde(default)]
108 pub dependencies: Vec<Dependency>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, Default)]
112pub struct ExecutionConfig {
113 #[serde(default = "default_exec_mode")]
115 pub mode: String,
116
117 pub binary: Option<String>,
119
120 pub docker_image: Option<String>,
122
123 pub wasm_module: Option<String>,
125
126 #[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 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 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 pub fn validate(&self) -> Result<()> {
163 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 for cmd in &self.commands {
174 Self::validate_command(cmd)?;
175 }
176
177 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 for arg in &cmd.args {
212 if arg.name.is_empty() {
213 return Err(anyhow::anyhow!("Argument name cannot be empty"));
214 }
215
216 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 for subcmd in &cmd.subcommands {
227 Self::validate_command(subcmd)?;
228 }
229
230 Ok(())
231 }
232
233 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 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}