1import path from 'path';
2import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders, prefixSkillReferences, PROVIDER_PLACEHOLDERS } from '../utils.js';
3
4/**
5 * Map from frontmatter field name to extraction spec.
6 *
7 * - sourceKey: property name on the skill object
8 * - yamlKey: key name in YAML frontmatter
9 * - condition: if provided, field is only emitted when this returns true
10 * - value: if provided, use this instead of skill[sourceKey]
11 */
12const FIELD_SPECS = {
13 'user-invocable': {
14 sourceKey: 'userInvocable',
15 yamlKey: 'user-invocable',
16 condition: (skill) => skill.userInvocable,
17 value: () => true,
18 },
19 'argument-hint': {
20 sourceKey: 'argumentHint',
21 yamlKey: 'argument-hint',
22 condition: (skill) => skill.userInvocable && skill.argumentHint,
23 },
24 license: {
25 sourceKey: 'license',
26 yamlKey: 'license',
27 },
28 compatibility: {
29 sourceKey: 'compatibility',
30 yamlKey: 'compatibility',
31 },
32 metadata: {
33 sourceKey: 'metadata',
34 yamlKey: 'metadata',
35 },
36 'allowed-tools': {
37 sourceKey: 'allowedTools',
38 yamlKey: 'allowed-tools',
39 },
40};
41
42/**
43 * Create a transformer function for a given provider config.
44 *
45 * @param {Object} config - Provider configuration from providers.js
46 * @returns {Function} transform(skills, distDir, options?)
47 */
48export function createTransformer(config) {
49 const { provider, configDir, displayName, frontmatterFields = [], bodyTransform, placeholderProvider } = config;
50 const placeholderKey = placeholderProvider || provider;
51
52 const activeFields = frontmatterFields
53 .map((name) => FIELD_SPECS[name])
54 .filter(Boolean);
55
56 return function transform(skills, distDir, options = {}) {
57 const { prefix = '', outputSuffix = '', skillsVersion = '' } = options;
58 const providerDir = path.join(distDir, `${provider}${outputSuffix}`);
59 const skillsDir = path.join(providerDir, `${configDir}/skills`);
60
61 cleanDir(providerDir);
62 ensureDir(skillsDir);
63
64 const allSkillNames = skills.map((s) => s.name);
65 const commandNames = skills
66 .filter((s) => s.userInvocable)
67 .map((s) => `${prefix}${s.name}`);
68
69 let refCount = 0;
70 let scriptCount = 0;
71
72 for (const skill of skills) {
73 const skillName = `${prefix}${skill.name}`;
74 const skillDir = path.join(skillsDir, skillName);
75
76 // Build frontmatter
77 const frontmatterObj = {
78 name: skillName,
79 description: skill.description,
80 };
81 if (skillsVersion) frontmatterObj.version = skillsVersion;
82
83 for (const spec of activeFields) {
84 if (spec.condition && !spec.condition(skill)) continue;
85 const val = spec.value ? spec.value(skill) : skill[spec.sourceKey];
86 if (val) frontmatterObj[spec.yamlKey] = val;
87 }
88
89 const frontmatter = generateYamlFrontmatter(frontmatterObj);
90
91 // Build body
92 const cmdPrefix = (PROVIDER_PLACEHOLDERS[placeholderKey] || {}).command_prefix || '/';
93 let skillBody = replacePlaceholders(skill.body, placeholderKey, commandNames, allSkillNames);
94
95 // Replace {{scripts_path}} with provider-aware path to skill's scripts directory
96 const scriptsPath = `${configDir}/skills/${skillName}/scripts`;
97 skillBody = skillBody.replace(/\{\{scripts_path\}\}/g, scriptsPath);
98 if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames, cmdPrefix);
99 if (bodyTransform) skillBody = bodyTransform(skillBody, skill);
100
101 const content = `${frontmatter}\n\n${skillBody}`;
102 writeFile(path.join(skillDir, 'SKILL.md'), content);
103
104 // Copy reference files
105 if (skill.references && skill.references.length > 0) {
106 const refDir = path.join(skillDir, 'reference');
107 ensureDir(refDir);
108 for (const ref of skill.references) {
109 const refContent = replacePlaceholders(ref.content, placeholderKey, [], allSkillNames);
110 writeFile(path.join(refDir, `${ref.name}.md`), refContent);
111 refCount++;
112 }
113 }
114
115 // Copy script files
116 if (skill.scripts && skill.scripts.length > 0) {
117 const scriptsOutDir = path.join(skillDir, 'scripts');
118 ensureDir(scriptsOutDir);
119 for (const script of skill.scripts) {
120 writeFile(path.join(scriptsOutDir, script.name), script.content);
121 scriptCount++;
122 }
123 }
124 }
125
126 const userInvocableCount = skills.filter((s) => s.userInvocable).length;
127 const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
128 const scriptInfo = scriptCount > 0 ? ` (${scriptCount} script files)` : '';
129 const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
130 console.log(`✓ ${displayName}${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}${scriptInfo}`);
131 };
132}