1use 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16pub enum TemplateType {
17 Traditional,
19 CliTool,
21 CodeGenerator,
23 DataProcessor,
25 TestingUtility,
27 DocumentationGenerator,
29 BuildAutomation,
31 Custom(String),
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct TemplateVariable {
38 name: String,
40
41 description: String,
43
44 default_value: Option<String>,
46
47 required: bool,
49
50 validation_pattern: Option<String>,
52
53 examples: Vec<String>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct SkillTemplate {
60 name: String,
62
63 template_type: TemplateType,
65
66 description: String,
68
69 version: String,
71
72 variables: Vec<TemplateVariable>,
74
75 file_structure: FileStructure,
77
78 default_metadata: HashMap<String, String>,
80
81 instructions_template: String,
83
84 example_usage: String,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct FileStructure {
91 directories: Vec<String>,
93
94 files: HashMap<String, String>,
96
97 executables: HashMap<String, String>,
99}
100
101pub struct TemplateEngine {
103 templates: HashMap<String, SkillTemplate>,
104}
105
106impl TemplateEngine {
107 fn new() -> Self {
109 let mut engine = Self { templates: HashMap::new() };
110
111 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 fn register_builtin_templates(&mut self) {
126 self.templates
128 .insert("traditional".to_string(), Self::create_traditional_template());
129
130 self.templates.insert("cli-tool".to_string(), Self::create_cli_tool_template());
132
133 self.templates
135 .insert("code-generator".to_string(), Self::create_code_generator_template());
136
137 self.templates
139 .insert("data-processor".to_string(), Self::create_data_processor_template());
140
141 self.templates
143 .insert("testing-utility".to_string(), Self::create_testing_utility_template());
144 }
145
146 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 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 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 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 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 pub fn get_template_names(&self) -> Vec<String> {
424 self.templates.keys().cloned().collect()
425 }
426
427 fn get_template(&self, name: &str) -> Option<&SkillTemplate> {
429 self.templates.get(name)
430 }
431
432 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 self.validate_variables(template, &variables)?;
445
446 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 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 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 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 #[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 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 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 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 fn render_template(&self, template: &str, variables: &HashMap<String, String>) -> Result<String> {
551 let mut rendered = template.to_string();
552
553 for (key, value) in variables {
555 let placeholder = format!("{{{{{key}}}}}");
556 rendered = rendered.replace(&placeholder, value);
557 }
558
559 Ok(rendered)
563 }
564
565 fn generate_skill_md(&self, template: &SkillTemplate, variables: &HashMap<String, String>) -> Result<String> {
567 let mut content = String::new();
568
569 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 content.push_str(&template.instructions_template);
593 content.push('\n');
594
595 content.push_str("\n## Example Usage\n\n");
597 content.push_str(&template.example_usage);
598
599 self.render_template(&content, variables)
601 }
602
603 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 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 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 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#[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
675pub 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 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 fn description(mut self, description: impl Into<String>) -> Self {
710 self.description = description.into();
711 self
712 }
713
714 fn version(mut self, version: impl Into<String>) -> Self {
716 self.version = version.into();
717 self
718 }
719
720 pub fn variable(mut self, variable: TemplateVariable) -> Self {
722 self.variables.push(variable);
723 self
724 }
725
726 fn directory(mut self, dir: impl Into<String>) -> Self {
728 self.file_structure.directories.push(dir.into());
729 self
730 }
731
732 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 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 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 fn instructions(mut self, instructions: impl Into<String>) -> Self {
752 self.instructions_template = instructions.into();
753 self
754 }
755
756 fn example_usage(mut self, example: impl Into<String>) -> Self {
758 self.example_usage = example.into();
759 self
760 }
761
762 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 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}