mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 00:26:41 +03:00
initial commit and build out
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Build System for Cross-Provider Design Skills & Commands
|
||||
*
|
||||
* 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)
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readSourceFiles } from './lib/utils.js';
|
||||
import {
|
||||
transformCursor,
|
||||
transformClaudeCode,
|
||||
transformGemini,
|
||||
transformCodex
|
||||
} from './lib/transformers/index.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
|
||||
/**
|
||||
* Main build process
|
||||
*/
|
||||
function build() {
|
||||
console.log('🔨 Building cross-provider design plugins...\n');
|
||||
|
||||
// Read source files
|
||||
const { commands, skills } = readSourceFiles(ROOT_DIR);
|
||||
console.log(`📖 Read ${commands.length} commands and ${skills.length} skills\n`);
|
||||
|
||||
// Transform for each provider
|
||||
transformCursor(commands, skills, DIST_DIR);
|
||||
transformClaudeCode(commands, skills, DIST_DIR);
|
||||
transformGemini(commands, skills, DIST_DIR);
|
||||
transformCodex(commands, skills, DIST_DIR);
|
||||
|
||||
console.log('\n✨ Build complete!');
|
||||
}
|
||||
|
||||
// Run the build
|
||||
build();
|
||||
@@ -0,0 +1,48 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Claude Code Transformer (Full Featured)
|
||||
*
|
||||
* Keeps full YAML frontmatter with args support.
|
||||
* Skills stored in subdirectories with SKILL.md filename.
|
||||
*/
|
||||
export function transformClaudeCode(commands, skills, distDir) {
|
||||
const commandsDir = path.join(distDir, 'claude-code/commands');
|
||||
const skillsDir = path.join(distDir, 'claude-code/skills');
|
||||
|
||||
cleanDir(path.join(distDir, 'claude-code'));
|
||||
ensureDir(commandsDir);
|
||||
ensureDir(skillsDir);
|
||||
|
||||
// Commands: Keep frontmatter + body
|
||||
for (const command of commands) {
|
||||
const frontmatter = generateYamlFrontmatter({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
...(command.args.length > 0 && { args: command.args })
|
||||
});
|
||||
|
||||
const content = `${frontmatter}\n\n${command.body}`;
|
||||
const outputPath = path.join(commandsDir, `${command.name}.md`);
|
||||
writeFile(outputPath, content);
|
||||
}
|
||||
|
||||
// Skills: Keep frontmatter + body in subdirectories
|
||||
for (const skill of skills) {
|
||||
const skillDir = path.join(skillsDir, skill.name);
|
||||
|
||||
const frontmatter = generateYamlFrontmatter({
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
...(skill.license && { license: skill.license })
|
||||
});
|
||||
|
||||
const content = `${frontmatter}\n\n${skill.body}`;
|
||||
const outputPath = path.join(skillDir, 'SKILL.md');
|
||||
writeFile(outputPath, content);
|
||||
}
|
||||
|
||||
console.log(`✓ Claude Code: ${commands.length} commands, ${skills.length} skills`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Codex Transformer (Full Featured - Custom Prompts + Modular Skills)
|
||||
*
|
||||
* Commands: Uses argument-hint format with $VARIABLE placeholders
|
||||
* Skills: Creates modular files with guiding AGENTS.md
|
||||
*/
|
||||
export function transformCodex(commands, skills, distDir) {
|
||||
const codexDir = path.join(distDir, 'codex');
|
||||
const promptsDir = path.join(codexDir, 'prompts');
|
||||
|
||||
cleanDir(codexDir);
|
||||
ensureDir(promptsDir);
|
||||
|
||||
// Commands: Transform to Codex prompt format
|
||||
for (const command of commands) {
|
||||
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('---');
|
||||
|
||||
// Transform {{argname}} to $ARGNAME for Codex
|
||||
let body = command.body;
|
||||
body = body.replace(/\{\{([^}]+)\}\}/g, (match, argName) => {
|
||||
return `$${argName.toUpperCase()}`;
|
||||
});
|
||||
|
||||
const content = `${yamlLines.join('\n')}\n\n${body}`;
|
||||
const outputPath = path.join(promptsDir, `${command.name}.md`);
|
||||
writeFile(outputPath, content);
|
||||
}
|
||||
|
||||
// Skills: Create modular files (body only)
|
||||
const skillEntries = [];
|
||||
for (const skill of skills) {
|
||||
const outputPath = path.join(codexDir, `AGENTS.${skill.name}.md`);
|
||||
writeFile(outputPath, skill.body);
|
||||
skillEntries.push({
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
file: `AGENTS.${skill.name}.md`
|
||||
});
|
||||
}
|
||||
|
||||
// Create main AGENTS.md that guides Codex to the right skill files
|
||||
const agentsMd = [
|
||||
'# Codex Agent Instructions',
|
||||
'',
|
||||
'This repository contains specialized skills for different tasks. When the user requests work in a particular domain, read the corresponding skill file for detailed guidance.',
|
||||
'',
|
||||
'## Available Skills',
|
||||
'',
|
||||
'Each skill provides deep expertise in its domain. Use the descriptions below to decide which skill file to read:',
|
||||
'',
|
||||
...skillEntries.map(skill =>
|
||||
`### ${skill.name}\n\n**When to use**: ${skill.description}\n\n**Read**: \`${skill.file}\` for complete instructions.\n`
|
||||
),
|
||||
'',
|
||||
'## How to Use Skills',
|
||||
'',
|
||||
'1. Identify the user\'s request domain',
|
||||
'2. Match it to a skill description above',
|
||||
'3. Read the corresponding skill file',
|
||||
'4. Follow the guidance in that file',
|
||||
'',
|
||||
'Multiple skills can be combined when the task requires expertise from different domains.'
|
||||
].join('\n');
|
||||
|
||||
writeFile(path.join(codexDir, 'AGENTS.md'), agentsMd);
|
||||
|
||||
console.log(`✓ Codex: ${commands.length} prompts, ${skills.length} skills (modular)`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Cursor Transformer (Downgraded - No Frontmatter/Args)
|
||||
*
|
||||
* Strips all frontmatter and metadata, outputs body only.
|
||||
* Cursor doesn't support arguments or frontmatter.
|
||||
*/
|
||||
export function transformCursor(commands, skills, distDir) {
|
||||
const commandsDir = path.join(distDir, 'cursor/commands');
|
||||
const rulesDir = path.join(distDir, 'cursor/rules');
|
||||
|
||||
cleanDir(path.join(distDir, 'cursor'));
|
||||
ensureDir(commandsDir);
|
||||
ensureDir(rulesDir);
|
||||
|
||||
// Commands: Body only (no frontmatter)
|
||||
for (const command of commands) {
|
||||
const outputPath = path.join(commandsDir, `${command.name}.md`);
|
||||
writeFile(outputPath, command.body);
|
||||
}
|
||||
|
||||
// Skills: Body only (no frontmatter)
|
||||
for (const skill of skills) {
|
||||
const outputPath = path.join(rulesDir, `${skill.name}.md`);
|
||||
writeFile(outputPath, skill.body);
|
||||
}
|
||||
|
||||
console.log(`✓ Cursor: ${commands.length} commands, ${skills.length} skills (downgraded)`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Gemini Transformer (Full Featured - TOML + Modular Skills)
|
||||
*
|
||||
* Commands: Converts to TOML format with {{args}} placeholders
|
||||
* Skills: Creates modular files imported via @./GEMINI.{name}.md syntax
|
||||
*/
|
||||
export function transformGemini(commands, skills, distDir) {
|
||||
const geminiDir = path.join(distDir, 'gemini');
|
||||
const commandsDir = path.join(geminiDir, 'commands');
|
||||
|
||||
cleanDir(geminiDir);
|
||||
ensureDir(commandsDir);
|
||||
|
||||
// Commands: Transform to TOML
|
||||
for (const command of commands) {
|
||||
// Replace named placeholders with {{args}}
|
||||
let prompt = command.body.replace(/\{\{[^}]+\}\}/g, '{{args}}');
|
||||
|
||||
const toml = [
|
||||
`description = "${command.description.replace(/"/g, '\\"')}"`,
|
||||
`prompt = """`,
|
||||
prompt,
|
||||
`"""`
|
||||
].join('\n');
|
||||
|
||||
const outputPath = path.join(commandsDir, `${command.name}.toml`);
|
||||
writeFile(outputPath, toml);
|
||||
}
|
||||
|
||||
// Skills: Create modular files
|
||||
for (const skill of skills) {
|
||||
const outputPath = path.join(geminiDir, `GEMINI.${skill.name}.md`);
|
||||
writeFile(outputPath, skill.body);
|
||||
}
|
||||
|
||||
// Create main GEMINI.md that imports skill files
|
||||
const geminiMd = [
|
||||
'# Gemini Context',
|
||||
'',
|
||||
'This repository contains specialized skills for different tasks. When you detect a user request in a particular domain, the corresponding skill file will be automatically loaded to provide detailed guidance.',
|
||||
'',
|
||||
'## Available Skills',
|
||||
'',
|
||||
'Each skill provides deep expertise in its domain. The skills below are automatically imported and will guide your responses:',
|
||||
'',
|
||||
...skills.map(skill =>
|
||||
`### ${skill.name}\n\n**When to use**: ${skill.description}\n\n@./GEMINI.${skill.name}.md\n`
|
||||
),
|
||||
'',
|
||||
'## How Skills Work',
|
||||
'',
|
||||
'1. Skills are automatically loaded via the import statements above',
|
||||
'2. When a user request matches a skill domain, apply that skill\'s guidance',
|
||||
'3. Multiple skills can be combined when the task requires expertise from different domains',
|
||||
'4. Follow the detailed instructions provided in each imported skill file'
|
||||
].join('\n');
|
||||
|
||||
writeFile(path.join(geminiDir, 'GEMINI.md'), geminiMd);
|
||||
|
||||
console.log(`✓ Gemini: ${commands.length} commands (TOML), ${skills.length} skills (modular)`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export { transformCursor } from './cursor.js';
|
||||
export { transformClaudeCode } from './claude-code.js';
|
||||
export { transformGemini } from './gemini.js';
|
||||
export { transformCodex } from './codex.js';
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Parse frontmatter from markdown content
|
||||
* Returns { frontmatter: object, body: string }
|
||||
*/
|
||||
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) {
|
||||
if (trimmed.startsWith('- name:')) {
|
||||
// New object in array
|
||||
const obj = {};
|
||||
obj.name = trimmed.slice(7).trim();
|
||||
currentArray.push(obj);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Property of array object (indented further)
|
||||
if (leadingSpaces >= 4 && currentArray && currentArray.length > 0) {
|
||||
const colonIndex = trimmed.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = trimmed.slice(0, colonIndex).trim();
|
||||
const value = trimmed.slice(colonIndex + 1).trim();
|
||||
const lastObj = currentArray[currentArray.length - 1];
|
||||
lastObj[key] = value === 'true' ? true : value === 'false' ? false : value;
|
||||
}
|
||||
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;
|
||||
currentKey = key;
|
||||
currentArray = null;
|
||||
} else {
|
||||
// Start of array
|
||||
currentKey = key;
|
||||
currentArray = [];
|
||||
frontmatter[key] = currentArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { frontmatter, body: body.trim() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively read all .md files from a directory
|
||||
*/
|
||||
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
|
||||
*/
|
||||
export function readSourceFiles(rootDir) {
|
||||
const commandsDir = path.join(rootDir, 'source/commands');
|
||||
const skillsDir = path.join(rootDir, 'source/skills');
|
||||
|
||||
const commandFiles = readFilesRecursive(commandsDir);
|
||||
const skillFiles = readFilesRecursive(skillsDir);
|
||||
|
||||
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 || [],
|
||||
body,
|
||||
filePath
|
||||
};
|
||||
});
|
||||
|
||||
const skills = skillFiles.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 || '',
|
||||
license: frontmatter.license || '',
|
||||
body,
|
||||
filePath
|
||||
};
|
||||
});
|
||||
|
||||
return { commands, skills };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure directory exists, create if needed
|
||||
*/
|
||||
export function ensureDir(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean directory (remove all contents)
|
||||
*/
|
||||
export function cleanDir(dirPath) {
|
||||
if (fs.existsSync(dirPath)) {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write file with automatic directory creation
|
||||
*/
|
||||
export function writeFile(filePath, content) {
|
||||
const dir = path.dirname(filePath);
|
||||
ensureDir(dir);
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}:`);
|
||||
for (const item of value) {
|
||||
if (typeof item === 'object') {
|
||||
lines.push(` - name: ${item.name}`);
|
||||
if (item.description) lines.push(` description: ${item.description}`);
|
||||
if (item.required !== undefined) lines.push(` required: ${item.required}`);
|
||||
} else {
|
||||
lines.push(` - ${item}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lines.push(`${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user