factory.js

  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}