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:
@@ -6,6 +6,7 @@ import prompts from 'prompts';
|
||||
import type { AIType } from '../types/index.js';
|
||||
import { AI_TYPES } from '../types/index.js';
|
||||
import { copyFolders, installFromZip, createTempDir, cleanup } from '../utils/extract.js';
|
||||
import { generatePlatformFiles, generateAllPlatformFiles } from '../utils/template.js';
|
||||
import { detectAIType, getAITypeDescription } from '../utils/detect.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import {
|
||||
@@ -24,10 +25,11 @@ interface InitOptions {
|
||||
ai?: AIType;
|
||||
force?: boolean;
|
||||
offline?: boolean;
|
||||
legacy?: boolean; // Use old ZIP-based install
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to install from GitHub release
|
||||
* Try to install from GitHub release (legacy method)
|
||||
* Returns the copied folders if successful, null if failed
|
||||
*/
|
||||
async function tryGitHubInstall(
|
||||
@@ -70,27 +72,44 @@ async function tryGitHubInstall(
|
||||
}
|
||||
|
||||
if (error instanceof GitHubRateLimitError) {
|
||||
spinner.warn('GitHub rate limit reached, using bundled assets...');
|
||||
spinner.warn('GitHub rate limit reached, using template generation...');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error instanceof GitHubDownloadError) {
|
||||
spinner.warn('GitHub download failed, using bundled assets...');
|
||||
spinner.warn('GitHub download failed, using template generation...');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Network errors or other fetch failures
|
||||
if (error instanceof TypeError && error.message.includes('fetch')) {
|
||||
spinner.warn('Network error, using bundled assets...');
|
||||
spinner.warn('Network error, using template generation...');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Unknown errors - still fall back to bundled assets
|
||||
spinner.warn('Download failed, using bundled assets...');
|
||||
// Unknown errors - still fall back
|
||||
spinner.warn('Download failed, using template generation...');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install using template generation (new method)
|
||||
*/
|
||||
async function templateInstall(
|
||||
targetDir: string,
|
||||
aiType: AIType,
|
||||
spinner: ReturnType<typeof ora>
|
||||
): Promise<string[]> {
|
||||
spinner.text = 'Generating skill files from templates...';
|
||||
|
||||
if (aiType === 'all') {
|
||||
return generateAllPlatformFiles(targetDir);
|
||||
}
|
||||
|
||||
return generatePlatformFiles(targetDir, aiType);
|
||||
}
|
||||
|
||||
export async function initCommand(options: InitOptions): Promise<void> {
|
||||
logger.title('UI/UX Pro Max Installer');
|
||||
|
||||
@@ -128,25 +147,39 @@ export async function initCommand(options: InitOptions): Promise<void> {
|
||||
const spinner = ora('Installing files...').start();
|
||||
const cwd = process.cwd();
|
||||
let copiedFolders: string[] = [];
|
||||
let usedGitHub = false;
|
||||
let installMethod = 'template';
|
||||
|
||||
try {
|
||||
// Try GitHub download first (unless offline mode)
|
||||
if (!options.offline) {
|
||||
const githubResult = await tryGitHubInstall(cwd, aiType, spinner);
|
||||
if (githubResult) {
|
||||
copiedFolders = githubResult;
|
||||
usedGitHub = true;
|
||||
// Use legacy ZIP-based install if --legacy flag is set
|
||||
if (options.legacy) {
|
||||
// Try GitHub download first (unless offline mode)
|
||||
if (!options.offline) {
|
||||
const githubResult = await tryGitHubInstall(cwd, aiType, spinner);
|
||||
if (githubResult) {
|
||||
copiedFolders = githubResult;
|
||||
installMethod = 'github';
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to bundled assets if GitHub failed or offline mode
|
||||
if (installMethod !== 'github') {
|
||||
spinner.text = 'Installing from bundled assets...';
|
||||
copiedFolders = await copyFolders(ASSETS_DIR, cwd, aiType);
|
||||
installMethod = 'bundled';
|
||||
}
|
||||
} else {
|
||||
// Use new template-based generation (default)
|
||||
copiedFolders = await templateInstall(cwd, aiType, spinner);
|
||||
installMethod = 'template';
|
||||
}
|
||||
|
||||
// Fall back to bundled assets if GitHub failed or offline mode
|
||||
if (!usedGitHub) {
|
||||
spinner.text = 'Installing from bundled assets...';
|
||||
copiedFolders = await copyFolders(ASSETS_DIR, cwd, aiType);
|
||||
}
|
||||
const methodMessage = {
|
||||
github: 'Installed from GitHub release!',
|
||||
bundled: 'Installed from bundled assets!',
|
||||
template: 'Generated from templates!',
|
||||
}[installMethod];
|
||||
|
||||
spinner.succeed(usedGitHub ? 'Installed from GitHub release!' : 'Installed from bundled assets!');
|
||||
spinner.succeed(methodMessage);
|
||||
|
||||
// Summary
|
||||
console.log();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export type AIType = 'claude' | 'cursor' | 'windsurf' | 'antigravity' | 'copilot' | 'kiro' | 'roocode' | 'codex' | 'qoder' | 'gemini' | 'trae' | 'opencode' | 'continue' | 'all';
|
||||
export type AIType = 'claude' | 'cursor' | 'windsurf' | 'antigravity' | 'copilot' | 'kiro' | 'roocode' | 'codex' | 'qoder' | 'gemini' | 'trae' | 'opencode' | 'continue' | 'codebuddy' | 'all';
|
||||
|
||||
export type InstallType = 'full' | 'reference';
|
||||
|
||||
export interface Release {
|
||||
tag_name: string;
|
||||
@@ -20,8 +22,28 @@ export interface InstallConfig {
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export const AI_TYPES: AIType[] = ['claude', 'cursor', 'windsurf', 'antigravity', 'copilot', 'roocode', 'kiro', 'codex', 'qoder', 'gemini', 'trae', 'opencode', 'continue', 'all'];
|
||||
export interface PlatformConfig {
|
||||
platform: string;
|
||||
displayName: string;
|
||||
installType: InstallType;
|
||||
folderStructure: {
|
||||
root: string;
|
||||
skillPath: string;
|
||||
filename: string;
|
||||
};
|
||||
scriptPath: string;
|
||||
frontmatter: Record<string, string> | null;
|
||||
sections: {
|
||||
quickReference: boolean;
|
||||
};
|
||||
title: string;
|
||||
description: string;
|
||||
skillOrWorkflow: string;
|
||||
}
|
||||
|
||||
export const AI_TYPES: AIType[] = ['claude', 'cursor', 'windsurf', 'antigravity', 'copilot', 'roocode', 'kiro', 'codex', 'qoder', 'gemini', 'trae', 'opencode', 'continue', 'codebuddy', 'all'];
|
||||
|
||||
// Legacy folder mapping for backward compatibility with ZIP-based installs
|
||||
export const AI_FOLDERS: Record<Exclude<AIType, 'all'>, string[]> = {
|
||||
claude: ['.claude'],
|
||||
cursor: ['.cursor', '.shared'],
|
||||
@@ -36,4 +58,5 @@ export const AI_FOLDERS: Record<Exclude<AIType, 'all'>, string[]> = {
|
||||
trae: ['.trae', '.shared'],
|
||||
opencode: ['.opencode', '.shared'],
|
||||
continue: ['.continue'],
|
||||
codebuddy: ['.codebuddy'],
|
||||
};
|
||||
|
||||
@@ -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