Skip to main content

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