feat: reorganize codebase with single source of truth + merge prompts into styles (#116)
BREAKING CHANGES: - Moved canonical data/scripts to src/ui-ux-pro-max/ - Removed duplicate folders (.codex/, .gemini/, .trae/, .codebuddy/, .continue/, skills/, .qoder/) - CLI now uses template system instead of copying pre-built folders New features: - Merged prompts.csv into styles.csv with 4 new columns: - AI Prompt Keywords - CSS/Technical Keywords - Implementation Checklist - Design System Variables - All 67 styles now have complete prompt data - Added Astro stack (53 guidelines) - Added 10 new 2025 UI trend styles CLI changes: - New template rendering system (cli/src/utils/template.ts) - Reduced cli/assets from ~34MB to ~564KB - Assets now contain only: data/, scripts/, templates/ File structure: - src/ui-ux-pro-max/ - Single source of truth - .claude/skills/ - Symlinks to src/ for development - .shared/ - Symlink to src/ui-ux-pro-max/ Bumped CLI version: 2.1.3 → 2.2.0 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,9 @@ export function detectAIType(cwd: string = process.cwd()): DetectionResult {
|
||||
if (existsSync(join(cwd, '.continue'))) {
|
||||
detected.push('continue');
|
||||
}
|
||||
if (existsSync(join(cwd, '.codebuddy'))) {
|
||||
detected.push('codebuddy');
|
||||
}
|
||||
|
||||
// Suggest based on what's detected
|
||||
let suggested: AIType | null = null;
|
||||
@@ -70,7 +73,7 @@ export function getAITypeDescription(aiType: AIType): string {
|
||||
case 'windsurf':
|
||||
return 'Windsurf (.windsurf/workflows/ + .shared/)';
|
||||
case 'antigravity':
|
||||
return 'Antigravity (.agent/workflows/ + .shared/)';
|
||||
return 'Antigravity (.agent/skills/)';
|
||||
case 'copilot':
|
||||
return 'GitHub Copilot (.github/prompts/ + .shared/)';
|
||||
case 'kiro':
|
||||
@@ -84,11 +87,13 @@ export function getAITypeDescription(aiType: AIType): string {
|
||||
case 'gemini':
|
||||
return 'Gemini CLI (.gemini/skills/ + .shared/)';
|
||||
case 'trae':
|
||||
return 'Trae (.trae/skills/ + .shared/)';
|
||||
return 'Trae (.trae/skills/)';
|
||||
case 'opencode':
|
||||
return 'OpenCode (.opencode/skills/ + .shared/)';
|
||||
return 'OpenCode (.opencode/skills/)';
|
||||
case 'continue':
|
||||
return 'Continue (.continue/skills/)';
|
||||
case 'codebuddy':
|
||||
return 'CodeBuddy (.codebuddy/skills/)';
|
||||
case 'all':
|
||||
return 'All AI assistants';
|
||||
}
|
||||
|
||||
247
cli/src/utils/template.ts
Normal file
247
cli/src/utils/template.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { readFile, mkdir, writeFile, cp, access, readdir } from 'node:fs/promises';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
// After bun build: dist/index.js -> ../assets = cli/assets ✓
|
||||
const ASSETS_DIR = join(__dirname, '..', 'assets');
|
||||
|
||||
export interface PlatformConfig {
|
||||
platform: string;
|
||||
displayName: string;
|
||||
installType: 'full' | 'reference';
|
||||
folderStructure: {
|
||||
root: string;
|
||||
skillPath: string;
|
||||
filename: string;
|
||||
};
|
||||
scriptPath: string;
|
||||
frontmatter: Record<string, string> | null;
|
||||
sections: {
|
||||
quickReference: boolean;
|
||||
};
|
||||
title: string;
|
||||
description: string;
|
||||
skillOrWorkflow: string;
|
||||
}
|
||||
|
||||
// Map AIType to platform config file name
|
||||
const AI_TO_PLATFORM: Record<string, string> = {
|
||||
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',
|
||||
};
|
||||
|
||||
async function exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load platform configuration from JSON file
|
||||
*/
|
||||
export async function loadPlatformConfig(aiType: string): Promise<PlatformConfig> {
|
||||
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<Map<string, PlatformConfig>> {
|
||||
const configs = new Map<string, PlatformConfig>();
|
||||
|
||||
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<string> {
|
||||
const templatePath = join(ASSETS_DIR, 'templates', templateName);
|
||||
return readFile(templatePath, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render frontmatter section
|
||||
*/
|
||||
function renderFrontmatter(frontmatter: Record<string, string> | 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
|
||||
*/
|
||||
export async function renderSkillFile(config: PlatformConfig): Promise<string> {
|
||||
// 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);
|
||||
|
||||
return frontmatter + content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy data and scripts to target directory
|
||||
*/
|
||||
async function copyDataAndScripts(targetSkillDir: string): Promise<void> {
|
||||
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 mkdir(dataTarget, { recursive: true });
|
||||
await cp(dataSource, dataTarget, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy scripts
|
||||
if (await exists(scriptsSource)) {
|
||||
await mkdir(scriptsTarget, { recursive: true });
|
||||
await cp(scriptsSource, scriptsTarget, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure .shared folder exists with data and scripts
|
||||
*/
|
||||
async function ensureSharedExists(targetDir: string): Promise<boolean> {
|
||||
const sharedDir = join(targetDir, '.shared', 'ui-ux-pro-max');
|
||||
|
||||
// Check if already exists
|
||||
if (await exists(sharedDir)) {
|
||||
return false; // Already exists, didn't create
|
||||
}
|
||||
|
||||
await mkdir(sharedDir, { recursive: true });
|
||||
await copyDataAndScripts(sharedDir);
|
||||
return true; // Created new
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate platform files for a specific AI type
|
||||
*/
|
||||
export async function generatePlatformFiles(
|
||||
targetDir: string,
|
||||
aiType: string
|
||||
): Promise<string[]> {
|
||||
const config = await loadPlatformConfig(aiType);
|
||||
const createdFolders: string[] = [];
|
||||
|
||||
// Determine full skill directory path
|
||||
const skillDir = join(
|
||||
targetDir,
|
||||
config.folderStructure.root,
|
||||
config.folderStructure.skillPath
|
||||
);
|
||||
|
||||
// Create directory structure
|
||||
await mkdir(skillDir, { recursive: true });
|
||||
|
||||
// Render and write skill file
|
||||
const skillContent = await renderSkillFile(config);
|
||||
const skillFilePath = join(skillDir, config.folderStructure.filename);
|
||||
await writeFile(skillFilePath, skillContent, 'utf-8');
|
||||
createdFolders.push(config.folderStructure.root);
|
||||
|
||||
// Handle data/scripts based on install type
|
||||
if (config.installType === 'full') {
|
||||
// Full install: copy data and scripts into the skill directory
|
||||
await copyDataAndScripts(skillDir);
|
||||
} else {
|
||||
// Reference install: ensure .shared exists
|
||||
const createdShared = await ensureSharedExists(targetDir);
|
||||
if (createdShared) {
|
||||
createdFolders.push('.shared');
|
||||
}
|
||||
}
|
||||
|
||||
return createdFolders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate files for all AI types
|
||||
*/
|
||||
export async function generateAllPlatformFiles(targetDir: string): Promise<string[]> {
|
||||
const allFolders = new Set<string>();
|
||||
|
||||
for (const aiType of Object.keys(AI_TO_PLATFORM)) {
|
||||
try {
|
||||
const folders = await generatePlatformFiles(targetDir, aiType);
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user