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
JavaScript
/**
* 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