mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-16 08:06:24 +03:00
Migrate to unified skills architecture, add Copilot & Antigravity (v2.0.0)
All commands are now skills with user-invokable: true. Source lives in
source/skills/{name}/SKILL.md. Added VS Code Copilot (.agents/skills/)
and Google Antigravity (.agent/skills/) transformers. All 6 providers
output to skills directories only — no more commands/prompts dirs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a94e237359
commit
87106b5271
+30
-25
@@ -1,13 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Build System for Cross-Provider Design Skills & Commands
|
||||
* Build System for Cross-Provider Design Skills
|
||||
*
|
||||
* Transforms feature-rich source files into provider-specific formats:
|
||||
* - Cursor: Downgraded (no frontmatter/args)
|
||||
* - Claude Code: Full featured (frontmatter + body)
|
||||
* - Gemini: Full featured (TOML + modular skills)
|
||||
* - Codex: Full featured (custom prompts + modular skills)
|
||||
* Transforms source skills into provider-specific formats:
|
||||
* - Cursor: .cursor/skills/
|
||||
* - Claude Code: .claude/skills/
|
||||
* - Gemini: .gemini/skills/
|
||||
* - Codex: .codex/skills/
|
||||
* - Copilot: .agents/skills/
|
||||
* - Antigravity: .agent/skills/
|
||||
*
|
||||
* Also builds Tailwind CSS for production deployment.
|
||||
*/
|
||||
@@ -20,7 +22,9 @@ import {
|
||||
transformCursor,
|
||||
transformClaudeCode,
|
||||
transformGemini,
|
||||
transformCodex
|
||||
transformCodex,
|
||||
transformCopilot,
|
||||
transformAntigravity
|
||||
} from './lib/transformers/index.js';
|
||||
import { createAllZips } from './lib/zip.js';
|
||||
import { execSync } from 'child_process';
|
||||
@@ -128,7 +132,7 @@ async function buildStaticSite() {
|
||||
* Main build process
|
||||
*/
|
||||
async function build() {
|
||||
console.log('🔨 Building cross-provider design plugins...\n');
|
||||
console.log('🔨 Building cross-provider design skills...\n');
|
||||
|
||||
// Build CSS with Tailwind CLI (handles @theme directive)
|
||||
buildTailwindCSS();
|
||||
@@ -146,23 +150,28 @@ async function build() {
|
||||
}
|
||||
}
|
||||
|
||||
// Read source files
|
||||
const { commands, skills } = readSourceFiles(ROOT_DIR);
|
||||
// Read source files (unified skills architecture)
|
||||
const { skills } = readSourceFiles(ROOT_DIR);
|
||||
const patterns = readPatterns(ROOT_DIR);
|
||||
console.log(`📖 Read ${commands.length} commands, ${skills.length} skills, and ${patterns.patterns.length + patterns.antipatterns.length} pattern categories\n`);
|
||||
const userInvokableCount = skills.filter(s => s.userInvokable).length;
|
||||
console.log(`📖 Read ${skills.length} skills (${userInvokableCount} user-invokable) and ${patterns.patterns.length + patterns.antipatterns.length} pattern categories\n`);
|
||||
|
||||
// Transform for each provider (unprefixed)
|
||||
transformCursor(commands, skills, DIST_DIR, patterns);
|
||||
transformClaudeCode(commands, skills, DIST_DIR, patterns);
|
||||
transformGemini(commands, skills, DIST_DIR, patterns);
|
||||
transformCodex(commands, skills, DIST_DIR, patterns);
|
||||
transformCursor(skills, DIST_DIR, patterns);
|
||||
transformClaudeCode(skills, DIST_DIR, patterns);
|
||||
transformGemini(skills, DIST_DIR, patterns);
|
||||
transformCodex(skills, DIST_DIR, patterns);
|
||||
transformCopilot(skills, DIST_DIR, patterns);
|
||||
transformAntigravity(skills, DIST_DIR, patterns);
|
||||
|
||||
// Transform for each provider (prefixed with i-)
|
||||
const prefixOptions = { prefix: 'i-', outputSuffix: '-prefixed' };
|
||||
transformCursor(commands, skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformClaudeCode(commands, skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformGemini(commands, skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformCodex(commands, skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformCursor(skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformClaudeCode(skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformGemini(skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformCodex(skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformCopilot(skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformAntigravity(skills, DIST_DIR, patterns, prefixOptions);
|
||||
|
||||
// Create ZIP bundles (both unprefixed and prefixed)
|
||||
await createAllZips(DIST_DIR);
|
||||
@@ -171,20 +180,16 @@ async function build() {
|
||||
const claudeCodeSrc = path.join(DIST_DIR, 'claude-code', '.claude');
|
||||
const claudeCodeDest = path.join(ROOT_DIR, '.claude');
|
||||
|
||||
// Copy commands and skills directories (preserves other files like settings.local.json)
|
||||
const commandsSrc = path.join(claudeCodeSrc, 'commands');
|
||||
// Copy skills directory (preserves other files like settings.local.json)
|
||||
const skillsSrc = path.join(claudeCodeSrc, 'skills');
|
||||
const commandsDest = path.join(claudeCodeDest, 'commands');
|
||||
const skillsDest = path.join(claudeCodeDest, 'skills');
|
||||
|
||||
// Remove existing and copy fresh
|
||||
if (fs.existsSync(commandsDest)) fs.rmSync(commandsDest, { recursive: true });
|
||||
if (fs.existsSync(skillsDest)) fs.rmSync(skillsDest, { recursive: true });
|
||||
|
||||
copyDirSync(commandsSrc, commandsDest);
|
||||
copyDirSync(skillsSrc, skillsDest);
|
||||
|
||||
console.log(`📋 Synced to .claude/: commands + skills`);
|
||||
console.log(`📋 Synced to .claude/: skills`);
|
||||
|
||||
console.log('\n✨ Build complete!');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Google Antigravity Transformer (Skills Only)
|
||||
*
|
||||
* All skills output to .agent/skills/{name}/SKILL.md
|
||||
* Frontmatter: name, description (truncated to 200 chars)
|
||||
*
|
||||
* @param {Array} skills - All skills (including user-invokable ones)
|
||||
* @param {string} distDir - Distribution output directory
|
||||
* @param {Object} patterns - Design patterns data (unused)
|
||||
* @param {Object} options - Optional settings
|
||||
* @param {string} options.prefix - Prefix to add to user-invokable skill names (e.g., 'i-')
|
||||
* @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed')
|
||||
*/
|
||||
export function transformAntigravity(skills, distDir, patterns = null, options = {}) {
|
||||
const { prefix = '', outputSuffix = '' } = options;
|
||||
const antigravityDir = path.join(distDir, `antigravity${outputSuffix}`);
|
||||
const skillsDir = path.join(antigravityDir, '.agent/skills');
|
||||
|
||||
cleanDir(antigravityDir);
|
||||
ensureDir(skillsDir);
|
||||
|
||||
let refCount = 0;
|
||||
for (const skill of skills) {
|
||||
const skillName = skill.userInvokable ? `${prefix}${skill.name}` : skill.name;
|
||||
const skillDir = path.join(skillsDir, skillName);
|
||||
|
||||
// Truncate description to 200 chars
|
||||
const description = skill.description.length > 200
|
||||
? skill.description.slice(0, 197) + '...'
|
||||
: skill.description;
|
||||
|
||||
const frontmatter = generateYamlFrontmatter({
|
||||
name: skillName,
|
||||
description,
|
||||
});
|
||||
|
||||
const skillBody = replacePlaceholders(skill.body, 'antigravity');
|
||||
const content = `${frontmatter}\n\n${skillBody}`;
|
||||
const outputPath = path.join(skillDir, 'SKILL.md');
|
||||
writeFile(outputPath, content);
|
||||
|
||||
// Copy reference files if they exist
|
||||
if (skill.references && skill.references.length > 0) {
|
||||
const refDir = path.join(skillDir, 'reference');
|
||||
ensureDir(refDir);
|
||||
for (const ref of skill.references) {
|
||||
const refOutputPath = path.join(refDir, `${ref.name}.md`);
|
||||
const refContent = replacePlaceholders(ref.content, 'antigravity');
|
||||
writeFile(refOutputPath, refContent);
|
||||
refCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userInvokableCount = skills.filter(s => s.userInvokable).length;
|
||||
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
|
||||
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
|
||||
console.log(`✓ Antigravity${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
|
||||
}
|
||||
@@ -2,104 +2,44 @@ import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Generate markdown from structured patterns/antipatterns data
|
||||
*/
|
||||
function generatePatternsMarkdown(patterns) {
|
||||
if (!patterns || (!patterns.patterns?.length && !patterns.antipatterns?.length)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let md = `## Design Patterns Reference
|
||||
|
||||
This reference defines what TO do and what NOT to do when creating frontend interfaces. These patterns fight against model bias—the tendency of LLMs to converge on the same predictable choices.
|
||||
|
||||
### What TO Do (Patterns)
|
||||
|
||||
Focus on intentional, distinctive design choices:
|
||||
`;
|
||||
|
||||
for (const category of patterns.patterns || []) {
|
||||
md += `\n**${category.name}**:\n`;
|
||||
for (const item of category.items || []) {
|
||||
md += `- ${item}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
md += `
|
||||
### What NOT to Do (Anti-Patterns)
|
||||
|
||||
These patterns create generic "AI slop" aesthetics:
|
||||
`;
|
||||
|
||||
for (const category of patterns.antipatterns || []) {
|
||||
md += `\n**${category.name}**:\n`;
|
||||
for (const item of category.items || []) {
|
||||
md += `- ${item}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
md += `
|
||||
These anti-patterns are baked into training data from countless generic templates. Without explicit guidance, AI reproduces them. This skill ensures your AI knows both what to do AND what to avoid.
|
||||
`;
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude Code Transformer (Full Featured)
|
||||
* Claude Code Transformer (Skills Only)
|
||||
*
|
||||
* Keeps full YAML frontmatter with args support.
|
||||
* Skills stored in subdirectories with SKILL.md filename.
|
||||
* Supports reference files in skill subdirectories.
|
||||
* All skills output to .claude/skills/{name}/SKILL.md
|
||||
* User-invokable skills get args support in frontmatter.
|
||||
*
|
||||
* @param {Array} skills - All skills (including user-invokable ones)
|
||||
* @param {string} distDir - Distribution output directory
|
||||
* @param {Object} patterns - Design patterns data (unused, kept for interface consistency)
|
||||
* @param {Object} options - Optional settings
|
||||
* @param {string} options.prefix - Prefix to add to command names (e.g., 'i-')
|
||||
* @param {string} options.prefix - Prefix to add to user-invokable skill names (e.g., 'i-')
|
||||
* @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed')
|
||||
*/
|
||||
export function transformClaudeCode(commands, skills, distDir, patterns = null, options = {}) {
|
||||
export function transformClaudeCode(skills, distDir, patterns = null, options = {}) {
|
||||
const { prefix = '', outputSuffix = '' } = options;
|
||||
const claudeDir = path.join(distDir, `claude-code${outputSuffix}`);
|
||||
const commandsDir = path.join(claudeDir, '.claude/commands');
|
||||
const skillsDir = path.join(claudeDir, '.claude/skills');
|
||||
|
||||
cleanDir(claudeDir);
|
||||
ensureDir(commandsDir);
|
||||
ensureDir(skillsDir);
|
||||
|
||||
// Commands: Keep frontmatter + body
|
||||
for (const command of commands) {
|
||||
const commandName = `${prefix}${command.name}`;
|
||||
const frontmatter = generateYamlFrontmatter({
|
||||
name: commandName,
|
||||
description: command.description,
|
||||
...(command.context && { context: command.context }),
|
||||
...(command.args.length > 0 && { args: command.args })
|
||||
});
|
||||
|
||||
const commandBody = replacePlaceholders(command.body, 'claude-code');
|
||||
const content = `${frontmatter}\n\n${commandBody}`;
|
||||
const outputPath = path.join(commandsDir, `${commandName}.md`);
|
||||
writeFile(outputPath, content);
|
||||
}
|
||||
|
||||
// Skills: Keep frontmatter + body in subdirectories
|
||||
let refCount = 0;
|
||||
for (const skill of skills) {
|
||||
const skillDir = path.join(skillsDir, skill.name);
|
||||
const skillName = skill.userInvokable ? `${prefix}${skill.name}` : skill.name;
|
||||
const skillDir = path.join(skillsDir, skillName);
|
||||
|
||||
const frontmatterObj = {
|
||||
name: skill.name,
|
||||
name: skillName,
|
||||
description: skill.description,
|
||||
};
|
||||
|
||||
// Add optional fields if present
|
||||
if (skill.userInvokable) frontmatterObj['user-invokable'] = true;
|
||||
if (skill.args && skill.args.length > 0) frontmatterObj.args = skill.args;
|
||||
if (skill.license) frontmatterObj.license = skill.license;
|
||||
if (skill.compatibility) frontmatterObj.compatibility = skill.compatibility;
|
||||
if (skill.metadata) frontmatterObj.metadata = skill.metadata;
|
||||
if (skill.allowedTools) frontmatterObj['allowed-tools'] = skill.allowedTools;
|
||||
|
||||
const frontmatter = generateYamlFrontmatter(frontmatterObj);
|
||||
|
||||
const skillBody = replacePlaceholders(skill.body, 'claude-code');
|
||||
const content = `${frontmatter}\n\n${skillBody}`;
|
||||
const outputPath = path.join(skillDir, 'SKILL.md');
|
||||
@@ -118,8 +58,8 @@ export function transformClaudeCode(commands, skills, distDir, patterns = null,
|
||||
}
|
||||
}
|
||||
|
||||
const userInvokableCount = skills.filter(s => s.userInvokable).length;
|
||||
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
|
||||
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
|
||||
console.log(`✓ Claude Code${prefixInfo}: ${commands.length} commands, ${skills.length} skills${refInfo}`);
|
||||
console.log(`✓ Claude Code${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,110 +2,56 @@ import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Generate markdown from structured patterns/antipatterns data
|
||||
*/
|
||||
function generatePatternsMarkdown(patterns) {
|
||||
if (!patterns || (!patterns.patterns?.length && !patterns.antipatterns?.length)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let md = `## Design Patterns Reference
|
||||
|
||||
This reference defines what TO do and what NOT to do when creating frontend interfaces. These patterns fight against model bias—the tendency of LLMs to converge on the same predictable choices.
|
||||
|
||||
### What TO Do (Patterns)
|
||||
|
||||
Focus on intentional, distinctive design choices:
|
||||
`;
|
||||
|
||||
for (const category of patterns.patterns || []) {
|
||||
md += `\n**${category.name}**:\n`;
|
||||
for (const item of category.items || []) {
|
||||
md += `- ${item}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
md += `
|
||||
### What NOT to Do (Anti-Patterns)
|
||||
|
||||
These patterns create generic "AI slop" aesthetics:
|
||||
`;
|
||||
|
||||
for (const category of patterns.antipatterns || []) {
|
||||
md += `\n**${category.name}**:\n`;
|
||||
for (const item of category.items || []) {
|
||||
md += `- ${item}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
md += `
|
||||
These anti-patterns are baked into training data from countless generic templates. Without explicit guidance, AI reproduces them. This skill ensures your AI knows both what to do AND what to avoid.
|
||||
`;
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
/**
|
||||
* Codex Transformer (Full Featured - Agent Skills Standard)
|
||||
* Codex Transformer (Skills Only)
|
||||
*
|
||||
* Commands: Uses argument-hint format with $VARIABLE placeholders in .codex/prompts/
|
||||
* Skills: Uses Agent Skills standard with SKILL.md in .codex/skills/{name}/
|
||||
* Reference files are copied to skill subdirectories
|
||||
* All skills output to .codex/skills/{name}/SKILL.md
|
||||
* Frontmatter: name, description, argument-hint (from args for user-invokable)
|
||||
* For user-invokable skills: {{argname}} becomes $ARGNAME in body
|
||||
*
|
||||
* @param {Array} skills - All skills (including user-invokable ones)
|
||||
* @param {string} distDir - Distribution output directory
|
||||
* @param {Object} patterns - Design patterns data (unused)
|
||||
* @param {Object} options - Optional settings
|
||||
* @param {string} options.prefix - Prefix to add to command names (e.g., 'i-')
|
||||
* @param {string} options.prefix - Prefix to add to user-invokable skill names (e.g., 'i-')
|
||||
* @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed')
|
||||
*/
|
||||
export function transformCodex(commands, skills, distDir, patterns = null, options = {}) {
|
||||
export function transformCodex(skills, distDir, patterns = null, options = {}) {
|
||||
const { prefix = '', outputSuffix = '' } = options;
|
||||
const codexDir = path.join(distDir, `codex${outputSuffix}`);
|
||||
const promptsDir = path.join(codexDir, '.codex/prompts');
|
||||
const skillsDir = path.join(codexDir, '.codex/skills');
|
||||
|
||||
cleanDir(codexDir);
|
||||
ensureDir(promptsDir);
|
||||
ensureDir(skillsDir);
|
||||
|
||||
// Commands: Transform to Codex prompt format
|
||||
for (const command of commands) {
|
||||
const commandName = `${prefix}${command.name}`;
|
||||
const yamlLines = ['---'];
|
||||
yamlLines.push(`description: ${command.description}`);
|
||||
|
||||
// Build argument-hint from args array
|
||||
if (command.args && command.args.length > 0) {
|
||||
const hints = command.args.map(arg => {
|
||||
const hint = arg.required ? `<${arg.name}>` : `[${arg.name.toUpperCase()}=<value>]`;
|
||||
return hint;
|
||||
});
|
||||
yamlLines.push(`argument-hint: ${hints.join(' ')}`);
|
||||
}
|
||||
|
||||
yamlLines.push('---');
|
||||
|
||||
// First replace our placeholders, then transform remaining {{argname}} to $ARGNAME
|
||||
let body = replacePlaceholders(command.body, 'codex');
|
||||
body = body.replace(/\{\{([^}]+)\}\}/g, (match, argName) => {
|
||||
return `$${argName.toUpperCase()}`;
|
||||
});
|
||||
|
||||
const content = `${yamlLines.join('\n')}\n\n${body}`;
|
||||
const outputPath = path.join(promptsDir, `${commandName}.md`);
|
||||
writeFile(outputPath, content);
|
||||
}
|
||||
|
||||
// Skills: Use Agent Skills standard with SKILL.md in subdirectories
|
||||
let refCount = 0;
|
||||
for (const skill of skills) {
|
||||
const skillDir = path.join(skillsDir, skill.name);
|
||||
const skillName = skill.userInvokable ? `${prefix}${skill.name}` : skill.name;
|
||||
const skillDir = path.join(skillsDir, skillName);
|
||||
|
||||
const frontmatter = generateYamlFrontmatter({
|
||||
name: skill.name,
|
||||
const frontmatterObj = {
|
||||
name: skillName,
|
||||
description: skill.description,
|
||||
...(skill.license && { license: skill.license })
|
||||
});
|
||||
};
|
||||
|
||||
// Build argument-hint from args array for user-invokable skills
|
||||
if (skill.userInvokable && skill.args && skill.args.length > 0) {
|
||||
const hints = skill.args.map(arg => {
|
||||
return arg.required ? `<${arg.name}>` : `[${arg.name.toUpperCase()}=<value>]`;
|
||||
});
|
||||
frontmatterObj['argument-hint'] = hints.join(' ');
|
||||
}
|
||||
if (skill.license) frontmatterObj.license = skill.license;
|
||||
|
||||
const frontmatter = generateYamlFrontmatter(frontmatterObj);
|
||||
|
||||
let skillBody = replacePlaceholders(skill.body, 'codex');
|
||||
// For user-invokable skills, transform remaining {{argname}} to $ARGNAME
|
||||
if (skill.userInvokable) {
|
||||
skillBody = skillBody.replace(/\{\{([^}]+)\}\}/g, (match, argName) => {
|
||||
return `$${argName.toUpperCase()}`;
|
||||
});
|
||||
}
|
||||
|
||||
const skillBody = replacePlaceholders(skill.body, 'codex');
|
||||
const content = `${frontmatter}\n\n${skillBody}`;
|
||||
const outputPath = path.join(skillDir, 'SKILL.md');
|
||||
writeFile(outputPath, content);
|
||||
@@ -123,7 +69,8 @@ export function transformCodex(commands, skills, distDir, patterns = null, optio
|
||||
}
|
||||
}
|
||||
|
||||
const userInvokableCount = skills.filter(s => s.userInvokable).length;
|
||||
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
|
||||
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
|
||||
console.log(`✓ Codex${prefixInfo}: ${commands.length} prompts, ${skills.length} skills${refInfo}`);
|
||||
console.log(`✓ Codex${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders } from '../utils.js';
|
||||
|
||||
/**
|
||||
* VS Code Copilot Transformer (Skills Only)
|
||||
*
|
||||
* All skills output to .agents/skills/{name}/SKILL.md (vendor-neutral path)
|
||||
* Frontmatter: name, description, user-invokable (if true), argument-hint (from args)
|
||||
*
|
||||
* @param {Array} skills - All skills (including user-invokable ones)
|
||||
* @param {string} distDir - Distribution output directory
|
||||
* @param {Object} patterns - Design patterns data (unused)
|
||||
* @param {Object} options - Optional settings
|
||||
* @param {string} options.prefix - Prefix to add to user-invokable skill names (e.g., 'i-')
|
||||
* @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed')
|
||||
*/
|
||||
export function transformCopilot(skills, distDir, patterns = null, options = {}) {
|
||||
const { prefix = '', outputSuffix = '' } = options;
|
||||
const copilotDir = path.join(distDir, `copilot${outputSuffix}`);
|
||||
const skillsDir = path.join(copilotDir, '.agents/skills');
|
||||
|
||||
cleanDir(copilotDir);
|
||||
ensureDir(skillsDir);
|
||||
|
||||
let refCount = 0;
|
||||
for (const skill of skills) {
|
||||
const skillName = skill.userInvokable ? `${prefix}${skill.name}` : skill.name;
|
||||
const skillDir = path.join(skillsDir, skillName);
|
||||
|
||||
const frontmatterObj = {
|
||||
name: skillName,
|
||||
description: skill.description,
|
||||
};
|
||||
|
||||
if (skill.userInvokable) frontmatterObj['user-invokable'] = true;
|
||||
|
||||
// Build argument-hint from args array for user-invokable skills
|
||||
if (skill.userInvokable && skill.args && skill.args.length > 0) {
|
||||
const hints = skill.args.map(arg => {
|
||||
return arg.required ? `<${arg.name}>` : `[${arg.name.toUpperCase()}=<value>]`;
|
||||
});
|
||||
frontmatterObj['argument-hint'] = hints.join(' ');
|
||||
}
|
||||
|
||||
const frontmatter = generateYamlFrontmatter(frontmatterObj);
|
||||
const skillBody = replacePlaceholders(skill.body, 'copilot');
|
||||
const content = `${frontmatter}\n\n${skillBody}`;
|
||||
const outputPath = path.join(skillDir, 'SKILL.md');
|
||||
writeFile(outputPath, content);
|
||||
|
||||
// Copy reference files if they exist
|
||||
if (skill.references && skill.references.length > 0) {
|
||||
const refDir = path.join(skillDir, 'reference');
|
||||
ensureDir(refDir);
|
||||
for (const ref of skill.references) {
|
||||
const refOutputPath = path.join(refDir, `${ref.name}.md`);
|
||||
const refContent = replacePlaceholders(ref.content, 'copilot');
|
||||
writeFile(refOutputPath, refContent);
|
||||
refCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userInvokableCount = skills.filter(s => s.userInvokable).length;
|
||||
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
|
||||
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
|
||||
console.log(`✓ Copilot${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
|
||||
}
|
||||
@@ -2,91 +2,38 @@ import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Generate markdown from structured patterns/antipatterns data
|
||||
*/
|
||||
function generatePatternsMarkdown(patterns) {
|
||||
if (!patterns || (!patterns.patterns?.length && !patterns.antipatterns?.length)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let md = `## Design Patterns Reference
|
||||
|
||||
This reference defines what TO do and what NOT to do when creating frontend interfaces. These patterns fight against model bias—the tendency of LLMs to converge on the same predictable choices.
|
||||
|
||||
### What TO Do (Patterns)
|
||||
|
||||
Focus on intentional, distinctive design choices:
|
||||
`;
|
||||
|
||||
for (const category of patterns.patterns || []) {
|
||||
md += `\n**${category.name}**:\n`;
|
||||
for (const item of category.items || []) {
|
||||
md += `- ${item}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
md += `
|
||||
### What NOT to Do (Anti-Patterns)
|
||||
|
||||
These patterns create generic "AI slop" aesthetics:
|
||||
`;
|
||||
|
||||
for (const category of patterns.antipatterns || []) {
|
||||
md += `\n**${category.name}**:\n`;
|
||||
for (const item of category.items || []) {
|
||||
md += `- ${item}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
md += `
|
||||
These anti-patterns are baked into training data from countless generic templates. Without explicit guidance, AI reproduces them. This skill ensures your AI knows both what to do AND what to avoid.
|
||||
`;
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cursor Transformer (Agent Skills Standard)
|
||||
* Cursor Transformer (Skills Only)
|
||||
*
|
||||
* Commands: Body only in .cursor/commands/ (Cursor doesn't support command frontmatter)
|
||||
* Skills: Agent Skills standard with SKILL.md in .cursor/skills/{name}/
|
||||
* Reference files are copied to skill subdirectories
|
||||
*
|
||||
* Note: Agent Skills in Cursor require nightly channel and are agent-decided rules.
|
||||
* All skills output to .cursor/skills/{name}/SKILL.md
|
||||
* Frontmatter: name, description, license
|
||||
*
|
||||
* @param {Array} skills - All skills (including user-invokable ones)
|
||||
* @param {string} distDir - Distribution output directory
|
||||
* @param {Object} patterns - Design patterns data (unused)
|
||||
* @param {Object} options - Optional settings
|
||||
* @param {string} options.prefix - Prefix to add to command names (e.g., 'i-')
|
||||
* @param {string} options.prefix - Prefix to add to user-invokable skill names (e.g., 'i-')
|
||||
* @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed')
|
||||
*/
|
||||
export function transformCursor(commands, skills, distDir, patterns = null, options = {}) {
|
||||
export function transformCursor(skills, distDir, patterns = null, options = {}) {
|
||||
const { prefix = '', outputSuffix = '' } = options;
|
||||
const cursorDir = path.join(distDir, `cursor${outputSuffix}`);
|
||||
const commandsDir = path.join(cursorDir, '.cursor/commands');
|
||||
const skillsDir = path.join(cursorDir, '.cursor/skills');
|
||||
|
||||
cleanDir(cursorDir);
|
||||
ensureDir(commandsDir);
|
||||
ensureDir(skillsDir);
|
||||
|
||||
// Commands: Body only (Cursor doesn't support command frontmatter/args)
|
||||
for (const command of commands) {
|
||||
const commandName = `${prefix}${command.name}`;
|
||||
const commandBody = replacePlaceholders(command.body, 'cursor');
|
||||
const outputPath = path.join(commandsDir, `${commandName}.md`);
|
||||
writeFile(outputPath, commandBody);
|
||||
}
|
||||
|
||||
// Skills: Agent Skills standard with SKILL.md in subdirectories
|
||||
let refCount = 0;
|
||||
for (const skill of skills) {
|
||||
const skillDir = path.join(skillsDir, skill.name);
|
||||
const skillName = skill.userInvokable ? `${prefix}${skill.name}` : skill.name;
|
||||
const skillDir = path.join(skillsDir, skillName);
|
||||
|
||||
const frontmatter = generateYamlFrontmatter({
|
||||
name: skill.name,
|
||||
const frontmatterObj = {
|
||||
name: skillName,
|
||||
description: skill.description,
|
||||
...(skill.license && { license: skill.license })
|
||||
});
|
||||
};
|
||||
if (skill.license) frontmatterObj.license = skill.license;
|
||||
|
||||
const frontmatter = generateYamlFrontmatter(frontmatterObj);
|
||||
const skillBody = replacePlaceholders(skill.body, 'cursor');
|
||||
const content = `${frontmatter}\n\n${skillBody}`;
|
||||
const outputPath = path.join(skillDir, 'SKILL.md');
|
||||
@@ -105,7 +52,8 @@ export function transformCursor(commands, skills, distDir, patterns = null, opti
|
||||
}
|
||||
}
|
||||
|
||||
const userInvokableCount = skills.filter(s => s.userInvokable).length;
|
||||
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
|
||||
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
|
||||
console.log(`✓ Cursor${prefixInfo}: ${commands.length} commands, ${skills.length} skills${refInfo}`);
|
||||
console.log(`✓ Cursor${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
|
||||
}
|
||||
|
||||
@@ -2,57 +2,43 @@ import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Gemini Transformer (Full Featured - TOML Commands + Agent Skills)
|
||||
* Gemini Transformer (Skills Only)
|
||||
*
|
||||
* Commands: Converts to TOML format with {{args}} placeholders in .gemini/commands/
|
||||
* Skills: Uses Agent Skills standard with SKILL.md in .gemini/skills/{name}/
|
||||
* Reference files are copied to skill subdirectories
|
||||
*
|
||||
* Note: Gemini CLI skills require gemini-cli@preview and enabling via /settings
|
||||
* All skills output to .gemini/skills/{name}/SKILL.md
|
||||
* Frontmatter: name, description
|
||||
* For user-invokable skills: {{arg}} placeholders become {{args}} in body
|
||||
*
|
||||
* @param {Array} skills - All skills (including user-invokable ones)
|
||||
* @param {string} distDir - Distribution output directory
|
||||
* @param {Object} patterns - Design patterns data (unused)
|
||||
* @param {Object} options - Optional settings
|
||||
* @param {string} options.prefix - Prefix to add to command names (e.g., 'i-')
|
||||
* @param {string} options.prefix - Prefix to add to user-invokable skill names (e.g., 'i-')
|
||||
* @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed')
|
||||
*/
|
||||
export function transformGemini(commands, skills, distDir, patterns = null, options = {}) {
|
||||
export function transformGemini(skills, distDir, patterns = null, options = {}) {
|
||||
const { prefix = '', outputSuffix = '' } = options;
|
||||
const geminiDir = path.join(distDir, `gemini${outputSuffix}`);
|
||||
const commandsDir = path.join(geminiDir, '.gemini/commands');
|
||||
const skillsDir = path.join(geminiDir, '.gemini/skills');
|
||||
|
||||
cleanDir(geminiDir);
|
||||
ensureDir(commandsDir);
|
||||
ensureDir(skillsDir);
|
||||
|
||||
// Commands: Transform to TOML
|
||||
for (const command of commands) {
|
||||
const commandName = `${prefix}${command.name}`;
|
||||
// First replace our placeholders, then replace remaining {{arg}} with {{args}}
|
||||
let prompt = replacePlaceholders(command.body, 'gemini');
|
||||
prompt = prompt.replace(/\{\{[^}]+\}\}/g, '{{args}}');
|
||||
|
||||
const toml = [
|
||||
`description = "${command.description.replace(/"/g, '\\"')}"`,
|
||||
`prompt = """`,
|
||||
prompt,
|
||||
`"""`
|
||||
].join('\n');
|
||||
|
||||
const outputPath = path.join(commandsDir, `${commandName}.toml`);
|
||||
writeFile(outputPath, toml);
|
||||
}
|
||||
|
||||
// Skills: Use Agent Skills standard with SKILL.md in subdirectories
|
||||
let refCount = 0;
|
||||
for (const skill of skills) {
|
||||
const skillDir = path.join(skillsDir, skill.name);
|
||||
const skillName = skill.userInvokable ? `${prefix}${skill.name}` : skill.name;
|
||||
const skillDir = path.join(skillsDir, skillName);
|
||||
|
||||
const frontmatter = generateYamlFrontmatter({
|
||||
name: skill.name,
|
||||
name: skillName,
|
||||
description: skill.description,
|
||||
});
|
||||
|
||||
const skillBody = replacePlaceholders(skill.body, 'gemini');
|
||||
let skillBody = replacePlaceholders(skill.body, 'gemini');
|
||||
// For user-invokable skills, replace remaining {{arg}} placeholders with {{args}}
|
||||
if (skill.userInvokable) {
|
||||
skillBody = skillBody.replace(/\{\{[^}]+\}\}/g, '{{args}}');
|
||||
}
|
||||
|
||||
const content = `${frontmatter}\n\n${skillBody}`;
|
||||
const outputPath = path.join(skillDir, 'SKILL.md');
|
||||
writeFile(outputPath, content);
|
||||
@@ -70,8 +56,8 @@ export function transformGemini(commands, skills, distDir, patterns = null, opti
|
||||
}
|
||||
}
|
||||
|
||||
const userInvokableCount = skills.filter(s => s.userInvokable).length;
|
||||
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
|
||||
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
|
||||
console.log(`✓ Gemini${prefixInfo}: ${commands.length} commands (TOML), ${skills.length} skills${refInfo}`);
|
||||
console.log(`✓ Gemini${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,5 @@ export { transformCursor } from './cursor.js';
|
||||
export { transformClaudeCode } from './claude-code.js';
|
||||
export { transformGemini } from './gemini.js';
|
||||
export { transformCodex } from './codex.js';
|
||||
|
||||
export { transformCopilot } from './copilot.js';
|
||||
export { transformAntigravity } from './antigravity.js';
|
||||
|
||||
+36
-64
@@ -8,26 +8,26 @@ import path from 'path';
|
||||
export function parseFrontmatter(content) {
|
||||
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
||||
const match = content.match(frontmatterRegex);
|
||||
|
||||
|
||||
if (!match) {
|
||||
return { frontmatter: {}, body: content };
|
||||
}
|
||||
|
||||
|
||||
const [, frontmatterText, body] = match;
|
||||
const frontmatter = {};
|
||||
|
||||
|
||||
// Simple YAML parser (handles basic key-value and arrays)
|
||||
const lines = frontmatterText.split('\n');
|
||||
let currentKey = null;
|
||||
let currentArray = null;
|
||||
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
|
||||
// Calculate indent level
|
||||
const leadingSpaces = line.length - line.trimStart().length;
|
||||
const trimmed = line.trim();
|
||||
|
||||
|
||||
// Array item at level 2 (nested under a key)
|
||||
if (trimmed.startsWith('- ') && leadingSpaces >= 2) {
|
||||
if (currentArray) {
|
||||
@@ -40,7 +40,7 @@ export function parseFrontmatter(content) {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Property of array object (indented further)
|
||||
if (leadingSpaces >= 4 && currentArray && currentArray.length > 0) {
|
||||
const colonIndex = trimmed.indexOf(':');
|
||||
@@ -52,16 +52,16 @@ export function parseFrontmatter(content) {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Top-level key-value pair
|
||||
if (leadingSpaces === 0) {
|
||||
const colonIndex = trimmed.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = trimmed.slice(0, colonIndex).trim();
|
||||
const value = trimmed.slice(colonIndex + 1).trim();
|
||||
|
||||
|
||||
if (value) {
|
||||
frontmatter[key] = value;
|
||||
frontmatter[key] = value === 'true' ? true : value === 'false' ? false : value;
|
||||
currentKey = key;
|
||||
currentArray = null;
|
||||
} else {
|
||||
@@ -73,7 +73,7 @@ export function parseFrontmatter(content) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return { frontmatter, body: body.trim() };
|
||||
}
|
||||
|
||||
@@ -84,51 +84,31 @@ export function readFilesRecursive(dir, fileList = []) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
return fileList;
|
||||
}
|
||||
|
||||
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
readFilesRecursive(filePath, fileList);
|
||||
} else if (file.endsWith('.md')) {
|
||||
fileList.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return fileList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse all source files
|
||||
* Supports both:
|
||||
* - Single file skills: source/skills/{name}.md
|
||||
* - Directory skills: source/skills/{name}/SKILL.md + reference/*.md
|
||||
* Read and parse all source files (unified skills architecture)
|
||||
* All source lives in source/skills/{name}/SKILL.md
|
||||
* Returns { skills } where each skill has userInvokable flag
|
||||
*/
|
||||
export function readSourceFiles(rootDir) {
|
||||
const commandsDir = path.join(rootDir, 'source/commands');
|
||||
const skillsDir = path.join(rootDir, 'source/skills');
|
||||
|
||||
const commandFiles = readFilesRecursive(commandsDir);
|
||||
|
||||
const commands = commandFiles.map(filePath => {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const { frontmatter, body } = parseFrontmatter(content);
|
||||
const name = path.basename(filePath, '.md');
|
||||
|
||||
return {
|
||||
name: frontmatter.name || name,
|
||||
description: frontmatter.description || '',
|
||||
args: frontmatter.args || [],
|
||||
context: frontmatter.context || null,
|
||||
body,
|
||||
filePath
|
||||
};
|
||||
});
|
||||
|
||||
// Read skills - handling both file and directory formats
|
||||
const skills = [];
|
||||
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
@@ -167,33 +147,19 @@ export function readSourceFiles(rootDir) {
|
||||
compatibility: frontmatter.compatibility || '',
|
||||
metadata: frontmatter.metadata || null,
|
||||
allowedTools: frontmatter['allowed-tools'] || '',
|
||||
userInvokable: frontmatter['user-invokable'] === true || frontmatter['user-invokable'] === 'true',
|
||||
args: frontmatter.args || [],
|
||||
context: frontmatter.context || null,
|
||||
body,
|
||||
filePath: skillMdPath,
|
||||
references
|
||||
});
|
||||
}
|
||||
} else if (entry.name.endsWith('.md')) {
|
||||
// Single file skill (legacy format)
|
||||
const content = fs.readFileSync(entryPath, 'utf-8');
|
||||
const { frontmatter, body } = parseFrontmatter(content);
|
||||
const name = path.basename(entry.name, '.md');
|
||||
|
||||
skills.push({
|
||||
name: frontmatter.name || name,
|
||||
description: frontmatter.description || '',
|
||||
license: frontmatter.license || '',
|
||||
compatibility: frontmatter.compatibility || '',
|
||||
metadata: frontmatter.metadata || null,
|
||||
allowedTools: frontmatter['allowed-tools'] || '',
|
||||
body,
|
||||
filePath: entryPath,
|
||||
references: []
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { commands, skills };
|
||||
return { skills };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -317,6 +283,16 @@ export const PROVIDER_PLACEHOLDERS = {
|
||||
model: 'GPT',
|
||||
config_file: 'AGENTS.md',
|
||||
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
|
||||
},
|
||||
'copilot': {
|
||||
model: 'the model',
|
||||
config_file: '.github/copilot-instructions.md',
|
||||
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
|
||||
},
|
||||
'antigravity': {
|
||||
model: 'Gemini',
|
||||
config_file: 'AGENT.md',
|
||||
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -332,17 +308,12 @@ export function replacePlaceholders(content, provider) {
|
||||
.replace(/\{\{ask_instruction\}\}/g, placeholders.ask_instruction);
|
||||
}
|
||||
|
||||
// Legacy alias for backward compatibility
|
||||
export function replaceModelPlaceholder(content, provider) {
|
||||
return replacePlaceholders(content, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate YAML frontmatter string
|
||||
*/
|
||||
export function generateYamlFrontmatter(data) {
|
||||
const lines = ['---'];
|
||||
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (Array.isArray(value)) {
|
||||
lines.push(`${key}:`);
|
||||
@@ -355,12 +326,13 @@ export function generateYamlFrontmatter(data) {
|
||||
lines.push(` - ${item}`);
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'boolean') {
|
||||
lines.push(`${key}: ${value}`);
|
||||
} else {
|
||||
lines.push(`${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
|
||||
+7
-8
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* ZIP Generation Utilities
|
||||
*
|
||||
*
|
||||
* Creates ZIP bundles for each provider's distribution
|
||||
*/
|
||||
|
||||
@@ -17,27 +17,27 @@ import { existsSync, readdirSync, statSync } from 'fs';
|
||||
export async function createProviderZip(providerDir, distDir, providerName) {
|
||||
const zipFileName = `${providerName}.zip`;
|
||||
const zipPath = path.join(distDir, zipFileName);
|
||||
|
||||
|
||||
// Check if provider directory exists
|
||||
if (!existsSync(providerDir)) {
|
||||
console.warn(`⚠️ Provider directory not found: ${providerDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Remove existing zip if present
|
||||
if (existsSync(zipPath)) {
|
||||
await $`rm ${zipPath}`.quiet();
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Create zip using bun's shell
|
||||
// cd into provider dir and zip all contents
|
||||
await $`cd ${providerDir} && zip -r ../${zipFileName} . -x "*.DS_Store"`.quiet();
|
||||
|
||||
|
||||
// Get file size for reporting
|
||||
const stats = statSync(zipPath);
|
||||
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
||||
|
||||
|
||||
console.log(` 📦 ${zipFileName} (${sizeMB} MB)`);
|
||||
} catch (error) {
|
||||
console.error(` ❌ Failed to create ${zipFileName}:`, error.message);
|
||||
@@ -51,7 +51,7 @@ export async function createProviderZip(providerDir, distDir, providerName) {
|
||||
export async function createAllZips(distDir) {
|
||||
console.log('\n📦 Creating ZIP bundles...');
|
||||
|
||||
const providers = ['cursor', 'claude-code', 'gemini', 'codex'];
|
||||
const providers = ['cursor', 'claude-code', 'gemini', 'codex', 'copilot', 'antigravity'];
|
||||
|
||||
// Create unprefixed ZIPs
|
||||
for (const provider of providers) {
|
||||
@@ -66,4 +66,3 @@ export async function createAllZips(distDir) {
|
||||
await createProviderZip(providerDir, distDir, `${provider}-prefixed`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user