UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

305 lines (293 loc) 10.2 kB
/** * SkillSmith Skill Generator * * Generates skills with platform-aware deployment. * * @module smiths/skillsmith/generator */ import { promises as fs } from 'fs'; import * as path from 'path'; import { PlatformSkillResolver } from './platform-resolver.js'; /** * Generate a skill from options */ export async function generateSkill(options) { // Validate skill name const nameValidation = PlatformSkillResolver.validateSkillName(options.name); if (!nameValidation.valid) { throw new Error(`Invalid skill name: ${nameValidation.error}`); } // Description is REQUIRED. Codex rejects SKILL.md files without a non-empty // description field; Claude Code uses it for natural-language invocation. // Never allow generation of a SKILL.md with a missing or blank description. if (typeof options.description !== 'string' || options.description.trim() === '') { throw new Error(`Skill description is required and must be non-empty. ` + `Codex (and other platforms) reject SKILL.md files without a description. ` + `Skill: '${options.name}'`); } // Check if platform supports skills natively const supportsSkills = PlatformSkillResolver.supportsSkills(options.platform); if (!supportsSkills) { const alternative = PlatformSkillResolver.getAlternativeStrategy(options.platform); console.warn(`Platform '${options.platform}' does not natively support skills.`); if (alternative) { console.warn(`Consider deploying as ${alternative} instead.`); } } // Generate skill content const content = generateSkillContent(options); // Generate references if requested const references = options.createReferences ? generateDefaultReferences(options) : undefined; // Resolve deployment path const deployPath = PlatformSkillResolver.getSkillPath(options.platform, options.projectPath, options.name); return { name: options.name, path: deployPath, content, platform: options.platform, references, version: options.version || '1.0.0', }; } /** * Deploy a generated skill to the target platform */ export async function deploySkill(skill, projectPath, dryRun = false) { const filesCreated = []; try { if (dryRun) { console.log(`[DRY RUN] Would deploy skill to: ${skill.path}`); return { skill, success: true, deployPath: skill.path, filesCreated: [], }; } // Create skill directory await fs.mkdir(skill.path, { recursive: true }); // Write SKILL.md const skillFilePath = PlatformSkillResolver.getSkillFilePath(skill.platform, projectPath, skill.name); await fs.writeFile(skillFilePath, skill.content, 'utf-8'); filesCreated.push(skillFilePath); // Create references directory and files if (skill.references && skill.references.length > 0) { const referencesPath = PlatformSkillResolver.getReferencesPath(skill.platform, projectPath, skill.name); await fs.mkdir(referencesPath, { recursive: true }); for (const ref of skill.references) { const refPath = path.join(referencesPath, ref.filename); await fs.writeFile(refPath, ref.content, 'utf-8'); filesCreated.push(refPath); } } return { skill, success: true, deployPath: skill.path, filesCreated, }; } catch (error) { return { skill, success: false, deployPath: skill.path, error: error instanceof Error ? error.message : String(error), filesCreated, }; } } /** * Generate SKILL.md content */ function generateSkillContent(options) { const frontmatter = generateFrontmatter(options); const body = generateSkillBody(options); return `${frontmatter}\n${body}`; } /** * Generate frontmatter YAML * * `description` is REQUIRED — Codex rejects SKILL.md files without a non-empty * description, and Claude Code relies on it for natural-language invocation. * This function throws rather than emitting a frontmatter block with a missing * or blank description (which would otherwise produce `description: ""` — * a silent failure that breaks deployments). */ function generateFrontmatter(options) { if (typeof options.description !== 'string' || options.description.trim() === '') { throw new Error(`generateFrontmatter: 'description' is required and must be non-empty ` + `(skill: '${options.name ?? '<unnamed>'}'). Codex and other platforms ` + `reject SKILL.md files without a description.`); } const fm = { name: options.name, description: options.description.trim(), version: options.version || '1.0.0', }; if (options.tools && options.tools.length > 0) { fm.tools = options.tools.join(', '); } const lines = ['---']; lines.push(`name: ${fm.name}`); lines.push(`namespace: aiwg`); lines.push(`description: ${fm.description}`); lines.push(`version: ${fm.version}`); if (fm.tools) { lines.push(`tools: ${fm.tools}`); } lines.push('---'); return lines.join('\n'); } /** * Generate skill body content */ function generateSkillBody(options) { const sections = []; // Title sections.push(`# ${toTitleCase(options.name)} Skill`); sections.push(''); // Purpose sections.push('## Purpose'); sections.push(''); sections.push(options.description); sections.push(''); // Trigger Phrases sections.push('## Trigger Phrases'); sections.push(''); sections.push('Activate this skill when the user says:'); if (options.triggerPhrases && options.triggerPhrases.length > 0) { options.triggerPhrases.forEach((phrase) => { sections.push(`- "${phrase}"`); }); } else { sections.push(`- "${options.name}"`); sections.push('- "use ' + options.name + '"'); } sections.push(''); // Input sections.push('## Input'); sections.push(''); sections.push('This skill expects:'); sections.push('- User request or content to process'); if (options.guidance) { sections.push(`- ${options.guidance}`); } sections.push(''); // Execution Process sections.push('## Execution Process'); sections.push(''); sections.push('1. Validate input requirements'); sections.push('2. Process the request'); sections.push('3. Generate output'); sections.push('4. Return results to user'); sections.push(''); // Output sections.push('## Output'); sections.push(''); sections.push('This skill produces:'); sections.push('- Processed result based on input'); sections.push('- Status information'); sections.push(''); // Examples sections.push('## Examples'); sections.push(''); sections.push('### Example 1: Basic Usage'); sections.push(`**User**: "${options.triggerPhrases?.[0] || options.name}"`); sections.push(`**Result**: Skill executes and returns result`); sections.push(''); // Tools (if specified) if (options.tools && options.tools.length > 0) { sections.push('## Tools Used'); sections.push(''); options.tools.forEach((tool) => { sections.push(`- ${tool}`); }); sections.push(''); } // References (if creating reference directory) if (options.createReferences) { sections.push('## References'); sections.push(''); sections.push('See `references/` directory for:'); sections.push('- Usage examples'); sections.push('- Configuration options'); sections.push('- Troubleshooting guide'); sections.push(''); } return sections.join('\n'); } /** * Generate default reference files */ function generateDefaultReferences(options) { const references = []; // Usage examples references.push({ filename: 'usage-examples.md', description: 'Usage examples and patterns', content: generateUsageExamplesContent(options), }); // Configuration references.push({ filename: 'configuration.md', description: 'Configuration options', content: generateConfigurationContent(options), }); return references; } /** * Generate usage examples content */ function generateUsageExamplesContent(options) { const triggers = options.triggerPhrases || [options.name]; return `# Usage Examples: ${toTitleCase(options.name)} ## Basic Usage \`\`\` User: "${triggers[0]}" \`\`\` ## Alternative Triggers ${triggers.slice(1).map(t => `- "${t}"`).join('\n') || '_No alternative triggers defined_'} ## Common Patterns This skill can be combined with other workflows for enhanced results. See the main README for integration examples. `; } /** * Generate configuration content */ function generateConfigurationContent(options) { return `# Configuration: ${toTitleCase(options.name)} ## Platform Support **${options.platform}**: ${PlatformSkillResolver.supportsSkills(options.platform) ? 'Native skill support' : 'Skills mapped to commands'} ## Options This skill uses default configuration. Custom options can be added to the skill manifest. ## Environment Variables No environment variables required for basic operation. `; } /** * Convert kebab-case to Title Case */ function toTitleCase(kebab) { return kebab .split('-') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } /** * Interactive skill design workflow */ export async function interactiveSkillDesign() { // This would be implemented with user prompts in a real CLI // For now, return a stub throw new Error('Interactive mode not yet implemented. Use --guidance to provide design input.'); } //# sourceMappingURL=generator.js.map