(tmplPath: string, host: Host = 'claude')
| 796 | } |
| 797 | |
| 798 | function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath: string; content: string; symlinkLoop?: boolean; catalogParts?: CatalogParts | null } { |
| 799 | const tmplContent = fs.readFileSync(tmplPath, 'utf-8'); |
| 800 | const relTmplPath = path.relative(ROOT, tmplPath); |
| 801 | let outputPath = tmplPath.replace(/\.tmpl$/, ''); |
| 802 | |
| 803 | // Determine skill directory relative to ROOT |
| 804 | const skillDir = path.relative(ROOT, path.dirname(tmplPath)); |
| 805 | |
| 806 | // --out-dir (Claude only): mirror the skill tree into the out-dir instead of |
| 807 | // writing in place. External hosts compute their own paths below. |
| 808 | if (OUT_DIR && host === 'claude') { |
| 809 | outputPath = path.join(OUT_DIR, skillDir, path.basename(tmplPath).replace(/\.tmpl$/, '')); |
| 810 | } |
| 811 | |
| 812 | // Extract name/description: name drives external skill naming + setup symlinks |
| 813 | // (and TemplateContext.skillName via buildContext); description feeds external |
| 814 | // host metadata. When frontmatter name: differs from directory name (e.g. |
| 815 | // run-tests/ with name: test), the frontmatter name wins. |
| 816 | const { name: extractedName, description: extractedDescription } = extractNameAndDescription(tmplContent); |
| 817 | |
| 818 | const currentHostConfig = getHostConfig(host); |
| 819 | const ctx = buildContext(tmplContent, tmplPath, host); |
| 820 | const skillName = ctx.skillName; |
| 821 | |
| 822 | // Replace placeholders + assert none remain (shared path with section generation). |
| 823 | let content = resolvePlaceholders(tmplContent, ctx, currentHostConfig, relTmplPath); |
| 824 | |
| 825 | // Preprocess voice triggers: fold into description, strip field from frontmatter. |
| 826 | // Must run BEFORE transformFrontmatter so all hosts see the updated description, |
| 827 | // and BEFORE extractedDescription is used by external host metadata. |
| 828 | content = processVoiceTriggers(content); |
| 829 | |
| 830 | // Re-extract description AFTER voice trigger preprocessing so Codex openai.yaml |
| 831 | // metadata gets the updated description with voice triggers included. |
| 832 | const postProcessDescription = extractNameAndDescription(content).description; |
| 833 | |
| 834 | // For Claude: strip sensitive: field (only Factory uses it) |
| 835 | // For external hosts: route output, transform frontmatter, rewrite paths |
| 836 | let symlinkLoop = false; |
| 837 | if (host === 'claude') { |
| 838 | content = transformFrontmatter(content, host); |
| 839 | } else { |
| 840 | const result = processExternalHost(content, tmplContent, host, skillDir, postProcessDescription, ctx, extractedName || undefined); |
| 841 | content = result.content; |
| 842 | outputPath = result.outputPath; |
| 843 | symlinkLoop = result.symlinkLoop; |
| 844 | } |
| 845 | |
| 846 | // Prepend generated header (after frontmatter) |
| 847 | const header = GENERATED_HEADER.replace('{{SOURCE}}', path.basename(tmplPath)); |
| 848 | const fmEnd = content.indexOf('---', content.indexOf('---') + 3); |
| 849 | if (fmEnd !== -1) { |
| 850 | const insertAt = content.indexOf('\n', fmEnd) + 1; |
| 851 | content = content.slice(0, insertAt) + header + content.slice(insertAt); |
| 852 | } else { |
| 853 | content = header + content; |
| 854 | } |
| 855 |
no test coverage detected