* Process external host output: routing, frontmatter, path rewrites, metadata. * Shared between Codex and Factory (and future external hosts).
( content: string, tmplContent: string, host: Host, skillDir: string, extractedDescription: string, ctx: TemplateContext, frontmatterName?: string, )
| 740 | * Shared between Codex and Factory (and future external hosts). |
| 741 | */ |
| 742 | function processExternalHost( |
| 743 | content: string, |
| 744 | tmplContent: string, |
| 745 | host: Host, |
| 746 | skillDir: string, |
| 747 | extractedDescription: string, |
| 748 | ctx: TemplateContext, |
| 749 | frontmatterName?: string, |
| 750 | ): { content: string; outputPath: string; outputDir: string; symlinkLoop: boolean } { |
| 751 | const hostConfig = getHostConfig(host); |
| 752 | |
| 753 | const name = externalSkillName(skillDir === '.' ? '' : skillDir, frontmatterName); |
| 754 | const outputDir = path.join(ROOT, hostConfig.hostSubdir, 'skills', name); |
| 755 | fs.mkdirSync(outputDir, { recursive: true }); |
| 756 | const outputPath = path.join(outputDir, 'SKILL.md'); |
| 757 | |
| 758 | // Guard against symlink loops |
| 759 | let symlinkLoop = false; |
| 760 | const claudePath = ctx.tmplPath.replace(/\.tmpl$/, ''); |
| 761 | try { |
| 762 | const resolvedClaude = fs.realpathSync(claudePath); |
| 763 | const resolvedExternal = fs.realpathSync(path.dirname(outputPath)) + '/' + path.basename(outputPath); |
| 764 | if (resolvedClaude === resolvedExternal) { |
| 765 | symlinkLoop = true; |
| 766 | } |
| 767 | } catch { |
| 768 | // realpathSync fails if file doesn't exist yet — no symlink loop |
| 769 | } |
| 770 | |
| 771 | // Extract hook safety prose BEFORE transforming frontmatter (which strips hooks) |
| 772 | const safetyProse = extractHookSafetyProse(tmplContent); |
| 773 | |
| 774 | // Transform frontmatter (host-aware) |
| 775 | let result = transformFrontmatter(content, host); |
| 776 | |
| 777 | // Insert safety advisory at the top of the body (after frontmatter) |
| 778 | if (safetyProse) { |
| 779 | const bodyStart = result.indexOf('\n---') + 4; |
| 780 | result = result.slice(0, bodyStart) + '\n' + safetyProse + '\n' + result.slice(bodyStart); |
| 781 | } |
| 782 | |
| 783 | // Config-driven path + tool rewrites (shared with processSectionTemplate so |
| 784 | // section cross-references get the same per-host treatment as SKILL.md). |
| 785 | result = applyHostRewrites(result, hostConfig); |
| 786 | |
| 787 | // Config-driven: generate metadata (e.g., openai.yaml for Codex) |
| 788 | if (hostConfig.generation.generateMetadata && !symlinkLoop) { |
| 789 | const agentsDir = path.join(outputDir, 'agents'); |
| 790 | fs.mkdirSync(agentsDir, { recursive: true }); |
| 791 | const shortDescription = condenseOpenAIShortDescription(extractedDescription); |
| 792 | fs.writeFileSync(path.join(agentsDir, 'openai.yaml'), generateOpenAIYaml(name, shortDescription)); |
| 793 | } |
| 794 | |
| 795 | return { content: result, outputPath, outputDir, symlinkLoop }; |
| 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'); |
no test coverage detected