1use 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18pub enum TemplateType {
19 Traditional,
21 CliTool,
23 CodeGenerator,
25 DataProcessor,
27 TestingUtility,
29 DocumentationGenerator,
31 BuildAutomation,
33 Custom(String),
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct TemplateVariable {
40 pub name: String,
42
43 pub description: String,
45
46 pub default_value: Option<String>,
48
49 pub required: bool,
51
52 pub validation_pattern: Option<String>,
54
55 pub examples: Vec<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct SkillTemplate {
62 pub name: String,
64
65 pub template_type: TemplateType,
67
68 pub description: String,
70
71 pub version: String,
73
74 pub variables: Vec<TemplateVariable>,
76
77 pub file_structure: FileStructure,
79
80 pub default_metadata: HashMap<String, String>,
82
83 pub instructions_template: String,
85
86 pub example_usage: String,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct FileStructure {
93 pub directories: Vec<String>,
95
96 pub files: HashMap<String, String>,
98
99 pub executables: HashMap<String, String>,
101}
102
103pub struct TemplateEngine {
105 templates: HashMap<String, SkillTemplate>,
106}
107
108impl TemplateEngine {
109 pub fn new() -> Self {
111 let mut engine = Self {
112 templates: HashMap::new(),
113 };
114
115 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 fn register_builtin_templates(&mut self) {
130 self.templates.insert(
132 "traditional".to_string(),
133 Self::create_traditional_template(),
134 );
135
136 self.templates
138 .insert("cli-tool".to_string(), Self::create_cli_tool_template());
139
140 self.templates.insert(
142 "code-generator".to_string(),
143 Self::create_code_generator_template(),
144 );
145
146 self.templates.insert(
148 "data-processor".to_string(),
149 Self::create_data_processor_template(),
150 );
151
152 self.templates.insert(
154 "testing-utility".to_string(),
155 Self::create_testing_utility_template(),
156 );
157 }
158
159 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 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 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 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 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 pub fn get_template_names(&self) -> Vec<String> {
448 self.templates.keys().cloned().collect()
449 }
450
451 pub fn get_template(&self, name: &str) -> Option<&SkillTemplate> {
453 self.templates.get(name)
454 }
455
456 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 self.validate_variables(template, &variables)?;
469
470 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 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 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 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 #[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 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 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 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 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 for (key, value) in variables {
606 let placeholder = format!("{{{{{}}}}}", key);
607 rendered = rendered.replace(&placeholder, value);
608 }
609
610 Ok(rendered)
614 }
615
616 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 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 content.push_str(&template.instructions_template);
648 content.push('\n');
649
650 content.push_str("\n## Example Usage\n\n");
652 content.push_str(&template.example_usage);
653
654 self.render_template(&content, variables)
656 }
657
658 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 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 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 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#[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
732pub 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 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 pub fn description(mut self, description: impl Into<String>) -> Self {
767 self.description = description.into();
768 self
769 }
770
771 pub fn version(mut self, version: impl Into<String>) -> Self {
773 self.version = version.into();
774 self
775 }
776
777 pub fn variable(mut self, variable: TemplateVariable) -> Self {
779 self.variables.push(variable);
780 self
781 }
782
783 pub fn directory(mut self, dir: impl Into<String>) -> Self {
785 self.file_structure.directories.push(dir.into());
786 self
787 }
788
789 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 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 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 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
813 self.instructions_template = instructions.into();
814 self
815 }
816
817 pub fn example_usage(mut self, example: impl Into<String>) -> Self {
819 self.example_usage = example.into();
820 self
821 }
822
823 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 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}