1use std::{fs, path::Path};
2use thiserror::Error;
3
4#[derive(Error, Debug)]
6pub enum TemplateError {
7 #[error("Invalid plugin name: {0}")]
8 InvalidName(String),
9
10 #[error("I/O error: {0}")]
11 Io(#[from] std::io::Error),
12}
13
14pub type TemplateResult<T> = Result<T, TemplateError>;
16
17#[derive(Debug, Clone)]
19pub struct PluginOptions {
20 pub name: String,
22 pub description: String,
24 pub author: String,
26 pub version: String,
28 pub min_vanguard_version: Option<String>,
30 pub template: Option<String>,
32}
33
34fn to_pascal_case(s: &str) -> String {
40 let mut result = String::new();
41 let mut capitalize_next = true;
42
43 for c in s.chars() {
44 if c.is_alphanumeric() {
45 if capitalize_next {
46 result.push(c.to_ascii_uppercase());
47 capitalize_next = false;
48 } else {
49 result.push(c);
50 }
51 } else {
52 capitalize_next = true;
53 }
54 }
55
56 result
57}
58
59fn is_inside_vanguard_repo() -> bool {
61 let current_dir = std::env::current_dir().ok();
63 if let Some(dir) = current_dir {
64 let mut path = dir.clone();
66 loop {
67 if path.join("crates").join("vanguard-plugin-sdk").exists() {
69 return true;
70 }
71
72 if let Some(parent) = path.parent() {
74 path = parent.to_path_buf();
75 } else {
76 break;
77 }
78 }
79 }
80 false
81}
82
83fn generate_cargo_toml(
85 plugin_name: &str,
86 description: &str,
87 _author: &str,
88 version: &str,
89) -> String {
90 let inside_vanguard_repo = is_inside_vanguard_repo();
91
92 let sdk_dependency = if inside_vanguard_repo {
94 "vanguard-plugin-sdk = { path = \"../../crates/vanguard-plugin-sdk\" }".to_string()
96 } else {
97 "vanguard-plugin-sdk = \"0.1.4\"".to_string()
99 };
100
101 format!(
102 r#"[package]
103name = "{plugin_name}"
104version = "{version}"
105edition = "2021"
106description = "{description}"
107license = "MIT"
108
109[lib]
110crate-type = ["cdylib"]
111
112[dependencies]
113{sdk_dependency}
114async-trait = "0.1"
115serde = {{ version = "1.0", features = ["derive"] }}
116serde_json = "1.0"
117tokio = {{ version = "1.0", features = ["full"] }}
118
119[dev-dependencies]
120tokio-test = "0.4"
121
122# Make this plugin independent from the parent workspace
123[workspace]
124"#
125 )
126}
127
128fn generate_readme(plugin_name: &str, description: &str, author: &str) -> String {
130 let inside_vanguard_repo = is_inside_vanguard_repo();
131
132 let installation_note = if inside_vanguard_repo {
133 "This plugin is configured to work within the Vanguard repository. If you want to use it outside the repository, you'll need to update the dependency in Cargo.toml."
134 } else {
135 "This plugin uses the vanguard-plugin-sdk from crates.io and can be built anywhere without requiring the Vanguard source code."
136 };
137
138 format!(
139 r#"# {plugin_name}
140
141{description}
142
143## Author
144
145{author}
146
147## Installation
148
1491. Build the plugin:
150 ```bash
151 cargo build --release
152 ```
153
1542. Install the plugin in Vanguard:
155 ```bash
156 # On macOS:
157 vanguard plugin install ./target/release/lib{plugin_name}.dylib
158
159 # On Linux:
160 vanguard plugin install ./target/release/lib{plugin_name}.so
161
162 # On Windows:
163 vanguard plugin install ./target/release/{plugin_name}.dll
164 ```
165
166## Notes
167
168{installation_note}
169"#
170 )
171}
172
173fn generate_lib_rs(
175 plugin_name: &str,
176 struct_name: &str,
177 description: &str,
178 author: &str,
179 version: &str,
180 min_vanguard_version: Option<&str>,
181) -> String {
182 let min_version = min_vanguard_version.unwrap_or("0.1.0");
183
184 format!(
185 r#"use serde::{{Deserialize, Serialize}};
186use vanguard_plugin_sdk::{{metadata, plugin, plugin_config, PluginMetadata, PluginMetadataBuilder}};
187
188/// Configuration for the {plugin_name} plugin
189#[derive(Debug, Serialize, Deserialize)]
190pub struct {struct_name}Config {{
191 /// Example configuration field
192 pub value: String,
193}}
194
195plugin_config!({struct_name}Config, serde_json::json!({{
196 "type": "object",
197 "required": ["value"],
198 "properties": {{
199 "value": {{
200 "type": "string",
201 "description": "Example configuration value"
202 }}
203 }}
204}}));
205
206/// A plugin that {description}
207#[derive(Debug)]
208pub struct {struct_name}Plugin {{
209 metadata: PluginMetadata,
210 config: Option<{struct_name}Config>,
211}}
212
213impl {struct_name}Plugin {{
214 /// Create a new plugin instance
215 pub fn new() -> Self {{
216 Self {{
217 metadata: metadata()
218 .name("{plugin_name}")
219 .version("{version}")
220 .description("{description}")
221 .author("{author}")
222 .min_vanguard_version("{min_version}")
223 .build(),
224 config: None,
225 }}
226 }}
227}}
228
229plugin!({struct_name}Plugin, {struct_name}Config);
230
231#[cfg(test)]
232mod tests {{
233 use super::*;
234 use vanguard_plugin_sdk::{{ValidationResult, VanguardPlugin}};
235
236 #[tokio::test]
237 async fn test_plugin_metadata() {{
238 let plugin = {struct_name}Plugin::new();
239 assert_eq!(plugin.metadata().name, "{plugin_name}");
240 assert_eq!(plugin.metadata().version, "{version}");
241 }}
242
243 #[tokio::test]
244 async fn test_plugin_validation() {{
245 let plugin = {struct_name}Plugin::new();
246 assert!(matches!(plugin.validate().await, ValidationResult::Passed));
247 }}
248}}
249"#
250 )
251}
252
253fn generate_lib_rs_with_commands(
255 plugin_name: &str,
256 struct_name: &str,
257 description: &str,
258 author: &str,
259 version: &str,
260 min_vanguard_version: Option<&str>,
261) -> String {
262 let min_version = min_vanguard_version.unwrap_or("0.1.0");
263
264 format!(
265 r#"use serde::{{Deserialize, Serialize}};
266use vanguard_plugin_sdk::{{
267 command::{{Command, CommandContext, CommandResult, VanguardCommand}},
268 command_handler, metadata, plugin, plugin_config, PluginMetadata, PluginMetadataBuilder
269}};
270
271/// Configuration for the {plugin_name} plugin
272#[derive(Debug, Serialize, Deserialize)]
273pub struct {struct_name}Config {{
274 /// Example configuration field
275 pub value: String,
276}}
277
278plugin_config!({struct_name}Config, serde_json::json!({{
279 "type": "object",
280 "required": ["value"],
281 "properties": {{
282 "value": {{
283 "type": "string",
284 "description": "Example configuration value"
285 }}
286 }}
287}}));
288
289/// A plugin that {description}
290#[derive(Debug)]
291pub struct {struct_name}Plugin {{
292 metadata: PluginMetadata,
293 config: Option<{struct_name}Config>,
294}}
295
296impl {struct_name}Plugin {{
297 /// Create a new plugin instance
298 pub fn new() -> Self {{
299 Self {{
300 metadata: metadata()
301 .name("{plugin_name}")
302 .version("{version}")
303 .description("{description}")
304 .author("{author}")
305 .min_vanguard_version("{min_version}")
306 .build(),
307 config: None,
308 }}
309 }}
310
311 /// Example command handler function
312 fn handle_hello_command(&self, args: &[String]) -> String {{
313 let name = args.get(0).cloned().unwrap_or_else(|| "World".to_string());
314 format!("Hello, {{}}! This message is from the {plugin_name} plugin!", name)
315 }}
316}}
317
318plugin!({struct_name}Plugin, {struct_name}Config);
319
320// Implement command handling for the plugin
321command_handler!({struct_name}Plugin,
322 vec![
323 Command {{
324 name: "hello".to_string(),
325 description: "Says hello from the plugin".to_string(),
326 usage: "{plugin_name} hello [name]".to_string(),
327 aliases: vec!["hi".to_string(), "greet".to_string()],
328 }}
329 ],
330 |plugin: &{struct_name}Plugin, command: &VanguardCommand, _ctx: &CommandContext| {{
331 // Handle the command synchronously
332 let result = match command.name.as_str() {{
333 "hello" | "hi" | "greet" => {{
334 let message = plugin.handle_hello_command(&command.args);
335 println!("{{}}", message);
336 CommandResult::Success
337 }},
338 _ => CommandResult::NotHandled,
339 }};
340
341 // Return a future that resolves to the result
342 Box::pin(async move {{ result }})
343 }}
344);
345
346#[cfg(test)]
347mod tests {{
348 use super::*;
349 use vanguard_plugin_sdk::{{ValidationResult, VanguardPlugin}};
350
351 #[tokio::test]
352 async fn test_plugin_metadata() {{
353 let plugin = {struct_name}Plugin::new();
354 assert_eq!(plugin.metadata().name, "{plugin_name}");
355 assert_eq!(plugin.metadata().version, "{version}");
356 }}
357
358 #[tokio::test]
359 async fn test_plugin_validation() {{
360 let plugin = {struct_name}Plugin::new();
361 assert!(matches!(plugin.validate().await, ValidationResult::Passed));
362 }}
363
364 #[tokio::test]
365 async fn test_command_handling() {{
366 let plugin = {struct_name}Plugin::new();
367 let cmd = VanguardCommand {{
368 name: "hello".to_string(),
369 args: vec!["Tester".to_string()],
370 original: "hello Tester".to_string(),
371 }};
372 let ctx = CommandContext::default();
373 let result = plugin.handle_command(&cmd, &ctx).await;
374 assert!(matches!(result, CommandResult::Success));
375 }}
376}}
377"#
378 )
379}
380
381pub fn generate_plugin(path: impl AsRef<Path>, options: PluginOptions) -> TemplateResult<()> {
383 let path = path.as_ref();
384
385 let name = options.name.to_lowercase();
387 if name.is_empty()
388 || !name
389 .chars()
390 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
391 {
392 return Err(TemplateError::InvalidName(name));
393 }
394
395 fs::create_dir_all(path)?;
397
398 let struct_name = to_pascal_case(&options.name);
400
401 let src_dir = path.join("src");
403 fs::create_dir_all(&src_dir)?;
404
405 let readme_path = path.join("README.md");
407 let readme_content = generate_readme(&options.name, &options.description, &options.author);
408 fs::write(readme_path, readme_content)?;
409
410 let cargo_toml_path = path.join("Cargo.toml");
412 let cargo_toml_content = generate_cargo_toml(
413 &options.name,
414 &options.description,
415 &options.author,
416 &options.version,
417 );
418 fs::write(cargo_toml_path, cargo_toml_content)?;
419
420 let lib_rs_path = src_dir.join("lib.rs");
422 let lib_rs_content = match options.template.as_deref() {
423 Some("commands") => generate_lib_rs_with_commands(
424 &options.name,
425 &struct_name,
426 &options.description,
427 &options.author,
428 &options.version,
429 options.min_vanguard_version.as_deref(),
430 ),
431 _ => generate_lib_rs(
432 &options.name,
433 &struct_name,
434 &options.description,
435 &options.author,
436 &options.version,
437 options.min_vanguard_version.as_deref(),
438 ),
439 };
440 fs::write(lib_rs_path, lib_rs_content)?;
441
442 Ok(())
443}