import { existsSync } from 'node:fs'; import { readFile, mkdir, writeFile, cp, access, readdir, lstat, rm } from 'node:fs/promises'; import { join, dirname } from 'node:path'; import { homedir } from 'node:os'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ASSETS_CANDIDATES = [ // Bun bundle: dist/index.js join(__dirname, '..', 'assets'), // TypeScript fallback: dist/utils/template.js join(__dirname, '..', '..', 'assets'), ]; const ASSETS_DIR = ASSETS_CANDIDATES.find(path => existsSync(path)) ?? ASSETS_CANDIDATES[0]; export interface PlatformConfig { platform: string; displayName: string; installType: 'full' | 'reference'; folderStructure: { root: string; skillPath: string; filename: string; }; scriptPath: string; frontmatter: Record | null; sections: { quickReference: boolean; }; title: string; description: string; skillOrWorkflow: string; } // Map AIType to platform config file name const AI_TO_PLATFORM: Record = { claude: 'claude', cursor: 'cursor', windsurf: 'windsurf', antigravity: 'agent', copilot: 'copilot', kiro: 'kiro', opencode: 'opencode', roocode: 'roocode', codex: 'codex', qoder: 'qoder', gemini: 'gemini', trae: 'trae', continue: 'continue', codebuddy: 'codebuddy', droid: 'droid', kilocode: 'kilocode', warp: 'warp', augment: 'augment', codewhale: 'codewhale', }; async function exists(path: string): Promise { try { await access(path); return true; } catch { return false; } } /** * Load platform configuration from JSON file */ export async function loadPlatformConfig(aiType: string): Promise { const platformName = AI_TO_PLATFORM[aiType]; if (!platformName) { throw new Error(`Unknown AI type: ${aiType}`); } const configPath = join(ASSETS_DIR, 'templates', 'platforms', `${platformName}.json`); const content = await readFile(configPath, 'utf-8'); return JSON.parse(content) as PlatformConfig; } /** * Load all available platform configs */ export async function loadAllPlatformConfigs(): Promise> { const configs = new Map(); for (const [aiType, platformName] of Object.entries(AI_TO_PLATFORM)) { try { const config = await loadPlatformConfig(aiType); configs.set(aiType, config); } catch { // Skip if config doesn't exist } } return configs; } /** * Load a template file */ async function loadTemplate(templateName: string): Promise { const templatePath = join(ASSETS_DIR, 'templates', templateName); return readFile(templatePath, 'utf-8'); } /** * Render frontmatter section */ function renderFrontmatter(frontmatter: Record | null): string { if (!frontmatter) return ''; const lines = ['---']; for (const [key, value] of Object.entries(frontmatter)) { // Quote values that contain special characters if (value.includes(':') || value.includes('"') || value.includes('\n')) { lines.push(`${key}: "${value.replace(/"/g, '\\"')}"`); } else { lines.push(`${key}: ${value}`); } } lines.push('---', ''); return lines.join('\n'); } /** * Render skill file content from template * When isGlobal=true, rewrites script paths to use ~/{root}/ prefix */ export async function renderSkillFile(config: PlatformConfig, isGlobal = false): Promise { // Load base template let content = await loadTemplate('base/skill-content.md'); // Load quick reference if needed let quickReferenceContent = ''; if (config.sections.quickReference) { quickReferenceContent = await loadTemplate('base/quick-reference.md'); } // Build the final content const frontmatter = renderFrontmatter(config.frontmatter); // Replace placeholders // Add newline before quick reference content if it exists const quickRefWithNewline = quickReferenceContent ? '\n' + quickReferenceContent : ''; content = content .replace(/\{\{TITLE\}\}/g, config.title) .replace(/\{\{DESCRIPTION\}\}/g, config.description) .replace(/\{\{SCRIPT_PATH\}\}/g, config.scriptPath) .replace(/\{\{SKILL_OR_WORKFLOW\}\}/g, config.skillOrWorkflow) .replace(/\{\{QUICK_REFERENCE\}\}/g, quickRefWithNewline); // For global install, rewrite relative script paths to absolute ~/root/ paths if (isGlobal) { const globalPrefix = `~/${config.folderStructure.root}/`; content = content.replace( /python3 skills\//g, `python3 ${globalPrefix}skills/` ); } return frontmatter + content; } /** * Replace a pre-existing non-directory at `path` so a real directory can be * created there. Older CLI installs (and Windows checkouts of the repo's * symlinked data/scripts) can leave plain "pointer" files at these paths; * mkdir then throws EEXIST and the install silently leaves stale files. */ async function ensureCleanDir(path: string): Promise { try { const stat = await lstat(path); if (!stat.isDirectory()) { await rm(path, { recursive: true, force: true }); } } catch { // Nothing exists at the path yet — mkdir will create it. } } /** * Copy data and scripts to target directory */ async function copyDataAndScripts(targetSkillDir: string): Promise { const dataSource = join(ASSETS_DIR, 'data'); const scriptsSource = join(ASSETS_DIR, 'scripts'); const dataTarget = join(targetSkillDir, 'data'); const scriptsTarget = join(targetSkillDir, 'scripts'); // Copy data if (await exists(dataSource)) { await ensureCleanDir(dataTarget); await mkdir(dataTarget, { recursive: true }); await cp(dataSource, dataTarget, { recursive: true }); } // Copy scripts if (await exists(scriptsSource)) { await ensureCleanDir(scriptsTarget); await mkdir(scriptsTarget, { recursive: true }); await cp(scriptsSource, scriptsTarget, { recursive: true }); } } /** * List the static sub-skills bundled under assets/skills/ (everything except * the template-rendered orchestrator). Empty if the package predates bundling. */ export async function listBundledSubSkills(): Promise { const skillsSource = join(ASSETS_DIR, 'skills'); if (!(await exists(skillsSource))) return []; const entries = await readdir(skillsSource, { withFileTypes: true }); return entries.filter(e => e.isDirectory()).map(e => e.name).sort(); } /** * Install the bundled sub-skills as siblings of the orchestrator skill, so a * single `uipro init` delivers all 7 skills instead of only ui-ux-pro-max. */ async function copySubSkills(skillsParentDir: string, force: boolean): Promise { const skillsSource = join(ASSETS_DIR, 'skills'); if (!(await exists(skillsSource))) return; for (const name of await listBundledSubSkills()) { const target = join(skillsParentDir, name); if (await exists(target) && !force) continue; await mkdir(target, { recursive: true }); await cp(join(skillsSource, name), target, { recursive: true }); } } /** * Generate platform files for a specific AI type * All platforms use self-contained installation with data and scripts * When isGlobal=true, installs to ~/home directory with absolute script paths */ export async function generatePlatformFiles( targetDir: string, aiType: string, isGlobal = false, force = false ): Promise { const config = await loadPlatformConfig(aiType); const createdFolders: string[] = []; // For global install, target the user's home directory const effectiveDir = isGlobal ? homedir() : targetDir; // Determine full skill directory path const skillDir = join( effectiveDir, config.folderStructure.root, config.folderStructure.skillPath ); // Create directory structure await mkdir(skillDir, { recursive: true }); // Render and write skill file (pass isGlobal to adjust paths) const skillContent = await renderSkillFile(config, isGlobal); const skillFilePath = join(skillDir, config.folderStructure.filename); const fileAlreadyExists = await exists(skillFilePath); if (fileAlreadyExists && !force) { console.log(` Skipped (already exists): ${skillFilePath} — use --force to overwrite`); return []; } await writeFile(skillFilePath, skillContent, 'utf-8'); createdFolders.push(config.folderStructure.root); // Copy data and scripts into the skill directory (self-contained) await copyDataAndScripts(skillDir); // Install the sibling sub-skills (banner-design, brand, design, ...) next to // the orchestrator so all 7 skills are delivered. The skills parent is the // orchestrator's parent dir (skills/ for most platforms, prompts/ for // copilot, steering/ for kiro) — derived, not hardcoded. const skillsParentDir = join( effectiveDir, config.folderStructure.root, dirname(config.folderStructure.skillPath) ); await copySubSkills(skillsParentDir, force); return createdFolders; } /** * Generate files for all AI types */ export async function generateAllPlatformFiles(targetDir: string, isGlobal = false, force = false): Promise { const allFolders = new Set(); const generatedSkillFiles = new Set(); for (const aiType of Object.keys(AI_TO_PLATFORM)) { try { const config = await loadPlatformConfig(aiType); const skillFile = join( config.folderStructure.root, config.folderStructure.skillPath, config.folderStructure.filename ); if (generatedSkillFiles.has(skillFile)) continue; const folders = await generatePlatformFiles(targetDir, aiType, isGlobal, force); generatedSkillFiles.add(skillFile); folders.forEach(f => allFolders.add(f)); } catch { // Skip if generation fails for a platform } } return Array.from(allFolders); } /** * Get list of supported AI types */ export function getSupportedAITypes(): string[] { return Object.keys(AI_TO_PLATFORM); }