Skip to main content

vtcode_skills/
templates.rs

1//! Skill Template System
2//!
3//! Provides templates for common skill patterns, enabling rapid skill development
4//! and standardization across the VT Code ecosystem.
5
6use anyhow::{Context, Result, anyhow};
7use hashbrown::HashMap;
8use serde::{Deserialize, Serialize};
9use std::path::{Path, PathBuf};
10use tracing::{debug, info};
11
12use vtcode_commons::fs::{ensure_dir_exists_sync, read_file_with_context_sync, write_file_with_context_sync};
13
14/// Skill template types
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16pub enum TemplateType {
17    /// Traditional VT Code skill with SKILL.md
18    Traditional,
19    /// CLI tool integration skill
20    CliTool,
21    /// Code generation skill
22    CodeGenerator,
23    /// Data processing skill
24    DataProcessor,
25    /// Testing utility skill
26    TestingUtility,
27    /// Documentation generator
28    DocumentationGenerator,
29    /// Build automation skill
30    BuildAutomation,
31    /// Custom template
32    Custom(String),
33}
34
35/// Template variable for customization
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct TemplateVariable {
38    /// Variable name
39    name: String,
40
41    /// Variable description
42    description: String,
43
44    /// Default value
45    default_value: Option<String>,
46
47    /// Whether variable is required
48    required: bool,
49
50    /// Validation pattern (regex)
51    validation_pattern: Option<String>,
52
53    /// Example values
54    examples: Vec<String>,
55}
56
57/// Skill template configuration
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct SkillTemplate {
60    /// Template name
61    name: String,
62
63    /// Template type
64    template_type: TemplateType,
65
66    /// Template description
67    description: String,
68
69    /// Template version
70    version: String,
71
72    /// Variables for customization
73    variables: Vec<TemplateVariable>,
74
75    /// File structure template
76    file_structure: FileStructure,
77
78    /// Default metadata
79    default_metadata: HashMap<String, String>,
80
81    /// Instructions template
82    instructions_template: String,
83
84    /// Example usage
85    example_usage: String,
86}
87
88/// File structure template
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct FileStructure {
91    /// Directories to create
92    directories: Vec<String>,
93
94    /// Files to create with templates
95    files: HashMap<String, String>,
96
97    /// Executable files (scripts, tools)
98    executables: HashMap<String, String>,
99}
100
101/// Template engine for skill generation
102pub struct TemplateEngine {
103    templates: HashMap<String, SkillTemplate>,
104}
105
106impl TemplateEngine {
107    /// Create new template engine with built-in templates
108    fn new() -> Self {
109        let mut engine = Self { templates: HashMap::new() };
110
111        // Register built-in templates
112        engine.register_builtin_templates();
113        engine
114    }
115}
116
117impl Default for TemplateEngine {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123impl TemplateEngine {
124    /// Register built-in templates
125    fn register_builtin_templates(&mut self) {
126        // Traditional skill template
127        self.templates
128            .insert("traditional".to_string(), Self::create_traditional_template());
129
130        // CLI tool template
131        self.templates.insert("cli-tool".to_string(), Self::create_cli_tool_template());
132
133        // Code generator template
134        self.templates
135            .insert("code-generator".to_string(), Self::create_code_generator_template());
136
137        // Data processor template
138        self.templates
139            .insert("data-processor".to_string(), Self::create_data_processor_template());
140
141        // Testing utility template
142        self.templates
143            .insert("testing-utility".to_string(), Self::create_testing_utility_template());
144    }
145
146    /// Create traditional skill template
147    fn create_traditional_template() -> SkillTemplate {
148        SkillTemplate {
149            name: "Traditional Skill".to_string(),
150            template_type: TemplateType::Traditional,
151            description: "Standard VT Code skill with SKILL.md format".to_string(),
152            version: "1.0.0".to_string(),
153            variables: vec![
154                TemplateVariable {
155                    name: "skill_name".to_string(),
156                    description: "Name of the skill".to_string(),
157                    default_value: None,
158                    required: true,
159                    validation_pattern: Some(r"^[a-z][a-z0-9-]*$".to_string()),
160                    examples: vec!["file-manager".to_string(), "code-analyzer".to_string()],
161                },
162                TemplateVariable {
163                    name: "description".to_string(),
164                    description: "Skill description".to_string(),
165                    default_value: None,
166                    required: true,
167                    validation_pattern: None,
168                    examples: vec!["Manages files and directories".to_string()],
169                },
170            ],
171            file_structure: FileStructure {
172                directories: vec!["scripts".to_string(), "references".to_string(), "assets".to_string()],
173                files: HashMap::from([(
174                    "SKILL.md".to_string(),
175                    include_str!("../templates/traditional/SKILL.md.template").to_string(),
176                )]),
177                executables: HashMap::from([(
178                    "scripts/helper.py".to_string(),
179                    include_str!("../templates/traditional/scripts/helper.py.template").to_string(),
180                )]),
181            },
182            default_metadata: HashMap::new(),
183            instructions_template: include_str!("../templates/traditional/instructions.md.template").to_string(),
184            example_usage: include_str!("../templates/traditional/example_usage.md.template").to_string(),
185        }
186    }
187
188    /// Create CLI tool template
189    fn create_cli_tool_template() -> SkillTemplate {
190        SkillTemplate {
191            name: "CLI Tool Integration".to_string(),
192            template_type: TemplateType::CliTool,
193            description: "Integrate external CLI tools as VT Code skills".to_string(),
194            version: "1.0.0".to_string(),
195            variables: vec![
196                TemplateVariable {
197                    name: "tool_name".to_string(),
198                    description: "Name of the CLI tool".to_string(),
199                    default_value: None,
200                    required: true,
201                    validation_pattern: Some(r"^[a-z][a-z0-9-]*$".to_string()),
202                    examples: vec!["curl-wrapper".to_string(), "git-helper".to_string()],
203                },
204                TemplateVariable {
205                    name: "tool_description".to_string(),
206                    description: "Description of what the tool does".to_string(),
207                    default_value: None,
208                    required: true,
209                    validation_pattern: None,
210                    examples: vec!["Wrapper around curl for HTTP requests".to_string()],
211                },
212                TemplateVariable {
213                    name: "tool_command".to_string(),
214                    description: "The CLI command to execute".to_string(),
215                    default_value: None,
216                    required: true,
217                    validation_pattern: None,
218                    examples: vec!["curl".to_string(), "git".to_string(), "python".to_string()],
219                },
220                TemplateVariable {
221                    name: "supports_json".to_string(),
222                    description: "Whether the tool supports JSON I/O".to_string(),
223                    default_value: Some("false".to_string()),
224                    required: false,
225                    validation_pattern: Some(r"^(true|false)$".to_string()),
226                    examples: vec![],
227                },
228            ],
229            file_structure: FileStructure {
230                directories: vec![],
231                files: HashMap::from([
232                    ("tool.json".to_string(), include_str!("../templates/cli-tool/tool.json.template").to_string()),
233                    ("README.md".to_string(), include_str!("../templates/cli-tool/README.md.template").to_string()),
234                ]),
235                executables: HashMap::from([(
236                    "tool.sh".to_string(),
237                    include_str!("../templates/cli-tool/tool.sh.template").to_string(),
238                )]),
239            },
240            default_metadata: HashMap::from([
241                ("category".to_string(), "cli-tool".to_string()),
242                ("tags".to_string(), "external,tool".to_string()),
243            ]),
244            instructions_template: include_str!("../templates/cli-tool/instructions.md.template").to_string(),
245            example_usage: include_str!("../templates/cli-tool/example_usage.md.template").to_string(),
246        }
247    }
248
249    /// Create code generator template
250    fn create_code_generator_template() -> SkillTemplate {
251        SkillTemplate {
252            name: "Code Generator".to_string(),
253            template_type: TemplateType::CodeGenerator,
254            description: "Generate code from templates and patterns".to_string(),
255            version: "1.0.0".to_string(),
256            variables: vec![
257                TemplateVariable {
258                    name: "generator_name".to_string(),
259                    description: "Name of the code generator".to_string(),
260                    default_value: None,
261                    required: true,
262                    validation_pattern: Some(r"^[a-z][a-z0-9-]*$".to_string()),
263                    examples: vec!["api-generator".to_string(), "test-generator".to_string()],
264                },
265                TemplateVariable {
266                    name: "target_language".to_string(),
267                    description: "Target programming language".to_string(),
268                    default_value: Some("rust".to_string()),
269                    required: false,
270                    validation_pattern: None,
271                    examples: vec!["rust".to_string(), "python".to_string(), "typescript".to_string()],
272                },
273                TemplateVariable {
274                    name: "template_engine".to_string(),
275                    description: "Template engine to use".to_string(),
276                    default_value: Some("handlebars".to_string()),
277                    required: false,
278                    validation_pattern: None,
279                    examples: vec!["handlebars".to_string(), "jinja2".to_string(), "mustache".to_string()],
280                },
281            ],
282            file_structure: FileStructure {
283                directories: vec![
284                    "templates".to_string(),
285                    "examples".to_string(),
286                ],
287                files: HashMap::from([
288                    ("SKILL.md".to_string(), "# {{generator_name}}\n\nGenerate {{target_language}} code from templates.".to_string()),
289                    ("generator.py".to_string(), "#!/usr/bin/env python3\n# Code generator implementation".to_string()),
290                ]),
291                executables: HashMap::from([
292                    ("generate.sh".to_string(), "#!/bin/bash\necho 'Generating code...'".to_string()),
293                ]),
294            },
295            default_metadata: HashMap::from([
296                ("category".to_string(), "generator".to_string()),
297                ("tags".to_string(), "code,generation,templates".to_string()),
298            ]),
299            instructions_template: "# {{generator_name}} Instructions\n\nGenerate {{target_language}} code from templates using {{template_engine}}.".to_string(),
300            example_usage: "## Example Usage\n\nGenerate code using the {{generator_name}} skill with {{template_engine}} templates.".to_string(),
301        }
302    }
303
304    /// Create data processor template
305    fn create_data_processor_template() -> SkillTemplate {
306        SkillTemplate {
307            name: "Data Processor".to_string(),
308            template_type: TemplateType::DataProcessor,
309            description: "Process and transform data files".to_string(),
310            version: "1.0.0".to_string(),
311            variables: vec![
312                TemplateVariable {
313                    name: "processor_name".to_string(),
314                    description: "Name of the data processor".to_string(),
315                    default_value: None,
316                    required: true,
317                    validation_pattern: Some(r"^[a-z][a-z0-9-]*$".to_string()),
318                    examples: vec!["csv-processor".to_string(), "json-transformer".to_string()],
319                },
320                TemplateVariable {
321                    name: "input_format".to_string(),
322                    description: "Input data format".to_string(),
323                    default_value: Some("json".to_string()),
324                    required: false,
325                    validation_pattern: None,
326                    examples: vec!["json".to_string(), "csv".to_string(), "xml".to_string()],
327                },
328                TemplateVariable {
329                    name: "output_format".to_string(),
330                    description: "Output data format".to_string(),
331                    default_value: Some("json".to_string()),
332                    required: false,
333                    validation_pattern: None,
334                    examples: vec!["json".to_string(), "csv".to_string(), "parquet".to_string()],
335                },
336            ],
337            file_structure: FileStructure {
338                directories: vec!["processors".to_string(), "schemas".to_string()],
339                files: HashMap::from([
340                    (
341                        "SKILL.md".to_string(),
342                        "# {{processor_name}}\n\nProcess {{input_format}} data and output {{output_format}}."
343                            .to_string(),
344                    ),
345                    ("processor.py".to_string(), "#!/usr/bin/env python3\n# Data processor implementation".to_string()),
346                ]),
347                executables: HashMap::from([(
348                    "process.sh".to_string(),
349                    "#!/bin/bash\necho 'Processing data...'".to_string(),
350                )]),
351            },
352            default_metadata: HashMap::from([
353                ("category".to_string(), "processor".to_string()),
354                ("tags".to_string(), "data,processing,transformation".to_string()),
355            ]),
356            instructions_template:
357                "# {{processor_name}} Instructions\n\nProcess data from {{input_format}} to {{output_format}} format."
358                    .to_string(),
359            example_usage: "## Example Usage\n\nProcess data using the {{processor_name}} skill.".to_string(),
360        }
361    }
362
363    /// Create testing utility template
364    fn create_testing_utility_template() -> SkillTemplate {
365        SkillTemplate {
366            name: "Testing Utility".to_string(),
367            template_type: TemplateType::TestingUtility,
368            description: "Testing and quality assurance utilities".to_string(),
369            version: "1.0.0".to_string(),
370            variables: vec![
371                TemplateVariable {
372                    name: "tester_name".to_string(),
373                    description: "Name of the testing utility".to_string(),
374                    default_value: None,
375                    required: true,
376                    validation_pattern: Some(r"^[a-z][a-z0-9-]*$".to_string()),
377                    examples: vec!["unit-tester".to_string(), "integration-runner".to_string()],
378                },
379                TemplateVariable {
380                    name: "test_framework".to_string(),
381                    description: "Testing framework to use".to_string(),
382                    default_value: Some("pytest".to_string()),
383                    required: false,
384                    validation_pattern: None,
385                    examples: vec!["pytest".to_string(), "jest".to_string(), "cargo-test".to_string()],
386                },
387                TemplateVariable {
388                    name: "coverage_tool".to_string(),
389                    description: "Code coverage tool".to_string(),
390                    default_value: Some("coverage".to_string()),
391                    required: false,
392                    validation_pattern: None,
393                    examples: vec!["coverage".to_string(), "istanbul".to_string(), "tarpaulin".to_string()],
394                },
395            ],
396            file_structure: FileStructure {
397                directories: vec!["tests".to_string(), "configs".to_string()],
398                files: HashMap::from([
399                    (
400                        "SKILL.md".to_string(),
401                        "# {{tester_name}}\n\n{{test_framework}} testing utility for quality assurance.".to_string(),
402                    ),
403                    ("test_runner.py".to_string(), "#!/usr/bin/env python3\n# Test runner implementation".to_string()),
404                ]),
405                executables: HashMap::from([(
406                    "run_tests.sh".to_string(),
407                    "#!/bin/bash\necho 'Running tests...'".to_string(),
408                )]),
409            },
410            default_metadata: HashMap::from([
411                ("category".to_string(), "testing".to_string()),
412                ("tags".to_string(), "testing,quality,assurance".to_string()),
413            ]),
414            instructions_template:
415                "# {{tester_name}} Instructions\n\nRun tests using {{test_framework}} with {{coverage_tool}} coverage."
416                    .to_string(),
417            example_usage: "## Example Usage\n\nRun tests using the {{tester_name}} skill with {{test_framework}}."
418                .to_string(),
419        }
420    }
421
422    /// Get available template names
423    pub fn get_template_names(&self) -> Vec<String> {
424        self.templates.keys().cloned().collect()
425    }
426
427    /// Get template by name
428    fn get_template(&self, name: &str) -> Option<&SkillTemplate> {
429        self.templates.get(name)
430    }
431
432    /// Generate skill from template
433    fn generate_skill(
434        &self,
435        template_name: &str,
436        variables: HashMap<String, String>,
437        output_dir: &Path,
438    ) -> Result<PathBuf> {
439        let template = self
440            .get_template(template_name)
441            .ok_or_else(|| anyhow!("Template '{template_name}' not found"))?;
442
443        // Validate required variables
444        self.validate_variables(template, &variables)?;
445
446        // Create output directory
447        let skill_name = variables
448            .get("skill_name")
449            .or_else(|| variables.get("tool_name"))
450            .or_else(|| variables.get("generator_name"))
451            .or_else(|| variables.get("processor_name"))
452            .or_else(|| variables.get("tester_name"))
453            .ok_or_else(|| anyhow!("No skill name variable found"))?;
454
455        let skill_dir = output_dir.join(skill_name);
456        ensure_dir_exists_sync(&skill_dir)?;
457
458        info!("Generating skill '{}' from template '{}' in {}", skill_name, template_name, skill_dir.display());
459
460        // Create directory structure
461        for dir in &template.file_structure.directories {
462            let dir_path = skill_dir.join(dir);
463            ensure_dir_exists_sync(&dir_path)?;
464        }
465
466        // Generate files
467        for (file_path, content_template) in &template.file_structure.files {
468            let content = self.render_template(content_template, &variables)?;
469            let full_path = skill_dir.join(file_path);
470
471            if let Some(parent) = full_path.parent() {
472                ensure_dir_exists_sync(parent)?;
473            }
474
475            write_file_with_context_sync(&full_path, &content, "template output file")?;
476            debug!("Generated file: {}", full_path.display());
477        }
478
479        // Generate executable files
480        for (file_path, content_template) in &template.file_structure.executables {
481            let content = self.render_template(content_template, &variables)?;
482            let full_path = skill_dir.join(file_path);
483
484            if let Some(parent) = full_path.parent() {
485                ensure_dir_exists_sync(parent)?;
486            }
487
488            write_file_with_context_sync(&full_path, &content, "template executable file")?;
489
490            // Make executable on Unix systems
491            #[cfg(unix)]
492            {
493                use std::os::unix::fs::PermissionsExt;
494                let mut perms = std::fs::metadata(&full_path)?.permissions();
495                perms.set_mode(0o755);
496                std::fs::set_permissions(&full_path, perms)?;
497            }
498
499            debug!("Generated executable: {}", full_path.display());
500        }
501
502        // Generate SKILL.md if not already generated
503        if !template.file_structure.files.contains_key("SKILL.md") {
504            let skill_md = self.generate_skill_md(template, &variables)?;
505            let skill_md_path = skill_dir.join("SKILL.md");
506            write_file_with_context_sync(&skill_md_path, &skill_md, "generated SKILL.md")?;
507            debug!("Generated SKILL.md: {}", skill_md_path.display());
508        }
509
510        info!("Successfully generated skill '{}' in {}", skill_name, skill_dir.display());
511        Ok(skill_dir)
512    }
513
514    /// Validate template variables
515    fn validate_variables(&self, template: &SkillTemplate, variables: &HashMap<String, String>) -> Result<()> {
516        let mut missing_required = vec![];
517        let mut invalid_values = vec![];
518
519        for variable in &template.variables {
520            if let Some(value) = variables.get(&variable.name) {
521                // Validate pattern if specified
522                if let Some(pattern) = &variable.validation_pattern {
523                    let regex = regex::Regex::new(pattern)
524                        .with_context(|| format!("Invalid validation pattern for variable '{}'", variable.name))?;
525
526                    if !regex.is_match(value) {
527                        invalid_values.push(format!(
528                            "Variable '{}' value '{}' does not match pattern '{}'",
529                            variable.name, value, pattern
530                        ));
531                    }
532                }
533            } else if variable.required && variable.default_value.is_none() {
534                missing_required.push(variable.name.clone());
535            }
536        }
537
538        if !missing_required.is_empty() {
539            return Err(anyhow!("Missing required variables: {}", missing_required.join(", ")));
540        }
541
542        if !invalid_values.is_empty() {
543            return Err(anyhow!("Invalid variable values: {}", invalid_values.join(", ")));
544        }
545
546        Ok(())
547    }
548
549    /// Render template with variables
550    fn render_template(&self, template: &str, variables: &HashMap<String, String>) -> Result<String> {
551        let mut rendered = template.to_string();
552
553        // Simple variable substitution: {{variable_name}}
554        for (key, value) in variables {
555            let placeholder = format!("{{{{{key}}}}}");
556            rendered = rendered.replace(&placeholder, value);
557        }
558
559        // Replace default values for missing variables
560        // This would need template-specific logic
561
562        Ok(rendered)
563    }
564
565    /// Generate SKILL.md content
566    fn generate_skill_md(&self, template: &SkillTemplate, variables: &HashMap<String, String>) -> Result<String> {
567        let mut content = String::new();
568
569        // YAML frontmatter
570        content.push_str("---\n");
571
572        let skill_name = variables
573            .get("skill_name")
574            .or_else(|| variables.get("tool_name"))
575            .or_else(|| variables.get("generator_name"))
576            .or_else(|| variables.get("processor_name"))
577            .or_else(|| variables.get("tester_name"))
578            .ok_or_else(|| anyhow!("No skill name found"))?;
579
580        let default_desc = "A VT Code skill".to_string();
581        let description = variables
582            .get("description")
583            .or_else(|| variables.get("tool_description"))
584            .unwrap_or(&default_desc);
585
586        content.push_str(&format!("name: {skill_name}\n"));
587        content.push_str(&format!("description: {description}\n"));
588
589        content.push_str("---\n\n");
590
591        // Add instructions
592        content.push_str(&template.instructions_template);
593        content.push('\n');
594
595        // Add example usage
596        content.push_str("\n## Example Usage\n\n");
597        content.push_str(&template.example_usage);
598
599        // Render variables in the content
600        self.render_template(&content, variables)
601    }
602
603    /// Register custom template
604    pub fn register_template(&mut self, template: SkillTemplate) -> Result<()> {
605        info!("Registering custom template: {}", template.name);
606        self.templates.insert(template.name.clone(), template);
607        Ok(())
608    }
609
610    /// Load template from file
611    pub fn load_template_from_file(&mut self, path: &Path) -> Result<()> {
612        let content = read_file_with_context_sync(path, "template file")?;
613
614        let template: SkillTemplate = serde_json::from_str(&content)
615            .with_context(|| format!("Failed to parse template from: {}", path.display()))?;
616
617        self.register_template(template)
618    }
619
620    /// Save template to file
621    pub fn save_template_to_file(&self, template_name: &str, path: &Path) -> Result<()> {
622        let template = self
623            .get_template(template_name)
624            .ok_or_else(|| anyhow!("Template '{template_name}' not found"))?;
625
626        let content = serde_json::to_string_pretty(template)
627            .with_context(|| format!("Failed to serialize template '{template_name}'"))?;
628
629        write_file_with_context_sync(path, &content, "template file")?;
630
631        info!("Saved template '{}' to {}", template_name, path.display());
632        Ok(())
633    }
634
635    /// Get template statistics
636    pub fn get_template_stats(&self) -> TemplateStats {
637        let mut stats = TemplateStats::default();
638
639        for template in self.templates.values() {
640            stats.total_templates += 1;
641
642            match &template.template_type {
643                TemplateType::Traditional => stats.traditional_templates += 1,
644                TemplateType::CliTool => stats.cli_tool_templates += 1,
645                TemplateType::CodeGenerator => stats.code_generator_templates += 1,
646                TemplateType::DataProcessor => stats.data_processor_templates += 1,
647                TemplateType::TestingUtility => stats.testing_utility_templates += 1,
648                TemplateType::DocumentationGenerator => stats.documentation_generator_templates += 1,
649                TemplateType::BuildAutomation => stats.build_automation_templates += 1,
650                TemplateType::Custom(_) => stats.custom_templates += 1,
651            }
652
653            stats.total_variables += template.variables.len() as u32;
654        }
655
656        stats
657    }
658}
659
660/// Template statistics
661#[derive(Debug, Default, Serialize, Deserialize)]
662pub struct TemplateStats {
663    total_templates: u32,
664    traditional_templates: u32,
665    cli_tool_templates: u32,
666    code_generator_templates: u32,
667    data_processor_templates: u32,
668    testing_utility_templates: u32,
669    documentation_generator_templates: u32,
670    build_automation_templates: u32,
671    custom_templates: u32,
672    total_variables: u32,
673}
674
675/// Skill template builder for programmatic template creation
676pub struct SkillTemplateBuilder {
677    name: String,
678    template_type: TemplateType,
679    description: String,
680    version: String,
681    variables: Vec<TemplateVariable>,
682    file_structure: FileStructure,
683    default_metadata: HashMap<String, String>,
684    instructions_template: String,
685    example_usage: String,
686}
687
688impl SkillTemplateBuilder {
689    /// Create new template builder
690    fn new(name: impl Into<String>, template_type: TemplateType) -> Self {
691        Self {
692            name: name.into(),
693            template_type,
694            description: String::new(),
695            version: "1.0.0".to_string(),
696            variables: vec![],
697            file_structure: FileStructure {
698                directories: vec![],
699                files: HashMap::new(),
700                executables: HashMap::new(),
701            },
702            default_metadata: HashMap::new(),
703            instructions_template: String::new(),
704            example_usage: String::new(),
705        }
706    }
707
708    /// Set description
709    fn description(mut self, description: impl Into<String>) -> Self {
710        self.description = description.into();
711        self
712    }
713
714    /// Set version
715    fn version(mut self, version: impl Into<String>) -> Self {
716        self.version = version.into();
717        self
718    }
719
720    /// Add variable
721    pub fn variable(mut self, variable: TemplateVariable) -> Self {
722        self.variables.push(variable);
723        self
724    }
725
726    /// Add directory
727    fn directory(mut self, dir: impl Into<String>) -> Self {
728        self.file_structure.directories.push(dir.into());
729        self
730    }
731
732    /// Add file
733    fn file(mut self, path: impl Into<String>, content: impl Into<String>) -> Self {
734        self.file_structure.files.insert(path.into(), content.into());
735        self
736    }
737
738    /// Add executable
739    fn executable(mut self, path: impl Into<String>, content: impl Into<String>) -> Self {
740        self.file_structure.executables.insert(path.into(), content.into());
741        self
742    }
743
744    /// Add default metadata
745    fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
746        self.default_metadata.insert(key.into(), value.into());
747        self
748    }
749
750    /// Set instructions template
751    fn instructions(mut self, instructions: impl Into<String>) -> Self {
752        self.instructions_template = instructions.into();
753        self
754    }
755
756    /// Set example usage
757    fn example_usage(mut self, example: impl Into<String>) -> Self {
758        self.example_usage = example.into();
759        self
760    }
761
762    /// Build template
763    fn build(self) -> SkillTemplate {
764        SkillTemplate {
765            name: self.name,
766            template_type: self.template_type,
767            description: self.description,
768            version: self.version,
769            variables: self.variables,
770            file_structure: self.file_structure,
771            default_metadata: self.default_metadata,
772            instructions_template: self.instructions_template,
773            example_usage: self.example_usage,
774        }
775    }
776}
777
778#[cfg(test)]
779mod tests {
780    use super::*;
781
782    #[test]
783    fn test_template_engine_creation() {
784        let engine = TemplateEngine::new();
785        assert!(!engine.templates.is_empty());
786        assert!(engine.get_template("traditional").is_some());
787        assert!(engine.get_template("cli-tool").is_some());
788    }
789
790    #[test]
791    fn test_template_builder() {
792        let template = SkillTemplateBuilder::new("test-template", TemplateType::Custom("test".to_string()))
793            .description("Test template")
794            .version("1.0.0")
795            .directory("scripts")
796            .file("README.md", "# Test README")
797            .executable("test.sh", "#!/bin/bash\necho 'test'")
798            .metadata("category", "test")
799            .instructions("Test instructions")
800            .example_usage("test example")
801            .build();
802
803        assert_eq!(template.name, "test-template");
804        assert_eq!(template.description, "Test template");
805        assert_eq!(template.file_structure.directories.len(), 1);
806        assert_eq!(template.file_structure.files.len(), 1);
807        assert_eq!(template.file_structure.executables.len(), 1);
808    }
809
810    #[test]
811    fn test_variable_validation() {
812        let engine = TemplateEngine::new();
813        let template = engine.get_template("traditional").unwrap();
814
815        let mut variables = HashMap::new();
816        variables.insert("skill_name".to_string(), "test-skill".to_string());
817        variables.insert("description".to_string(), "Test skill".to_string());
818
819        engine.validate_variables(template, &variables).unwrap();
820
821        // Test invalid skill name
822        variables.insert("skill_name".to_string(), "Invalid Skill Name!".to_string());
823        assert!(engine.validate_variables(template, &variables).is_err());
824    }
825
826    #[test]
827    fn test_traditional_template_emits_routing_scaffold() {
828        let temp_dir = tempfile::TempDir::new().unwrap();
829        let engine = TemplateEngine::new();
830        let mut variables = HashMap::new();
831        variables.insert("skill_name".to_string(), "test-skill".to_string());
832        variables.insert("description".to_string(), "Handles repeatable report generation".to_string());
833
834        let skill_dir = engine.generate_skill("traditional", variables, temp_dir.path()).unwrap();
835        let skill_md = std::fs::read_to_string(skill_dir.join("SKILL.md")).unwrap();
836
837        assert!(!skill_md.contains("when-to-use:"));
838        assert!(!skill_md.contains("when-not-to-use:"));
839        assert!(skill_md.contains("`assets/`:"));
840        assert!(skill_md.contains("Output/Artifact:"));
841    }
842}