QwenClaw v2.0 - Complete Rebuild with ALL 81+ Skills
This commit is contained in:
207
skills/ui-ux-pro-max/cli/src/commands/init.ts
Normal file
207
skills/ui-ux-pro-max/cli/src/commands/init.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
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 {
|
||||
getLatestRelease,
|
||||
getAssetUrl,
|
||||
downloadRelease,
|
||||
GitHubRateLimitError,
|
||||
GitHubDownloadError,
|
||||
} from '../utils/github.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
// From dist/index.js -> ../assets (one level up to cli/, then assets/)
|
||||
const ASSETS_DIR = join(__dirname, '..', 'assets');
|
||||
|
||||
interface InitOptions {
|
||||
ai?: AIType;
|
||||
force?: boolean;
|
||||
offline?: boolean;
|
||||
legacy?: boolean; // Use old ZIP-based install
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to install from GitHub release (legacy method)
|
||||
* Returns the copied folders if successful, null if failed
|
||||
*/
|
||||
async function tryGitHubInstall(
|
||||
targetDir: string,
|
||||
aiType: AIType,
|
||||
spinner: ReturnType<typeof ora>
|
||||
): Promise<string[] | null> {
|
||||
let tempDir: string | null = null;
|
||||
|
||||
try {
|
||||
spinner.text = 'Fetching latest release from GitHub...';
|
||||
const release = await getLatestRelease();
|
||||
const assetUrl = getAssetUrl(release);
|
||||
|
||||
if (!assetUrl) {
|
||||
throw new GitHubDownloadError('No ZIP asset found in latest release');
|
||||
}
|
||||
|
||||
spinner.text = `Downloading ${release.tag_name}...`;
|
||||
tempDir = await createTempDir();
|
||||
const zipPath = join(tempDir, 'release.zip');
|
||||
|
||||
await downloadRelease(assetUrl, zipPath);
|
||||
|
||||
spinner.text = 'Extracting and installing files...';
|
||||
const { copiedFolders, tempDir: extractedTempDir } = await installFromZip(
|
||||
zipPath,
|
||||
targetDir,
|
||||
aiType
|
||||
);
|
||||
|
||||
// Cleanup temp directory
|
||||
await cleanup(extractedTempDir);
|
||||
|
||||
return copiedFolders;
|
||||
} catch (error) {
|
||||
// Cleanup temp directory on error
|
||||
if (tempDir) {
|
||||
await cleanup(tempDir);
|
||||
}
|
||||
|
||||
if (error instanceof GitHubRateLimitError) {
|
||||
spinner.warn('GitHub rate limit reached, using template generation...');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error instanceof GitHubDownloadError) {
|
||||
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 template generation...');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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');
|
||||
|
||||
let aiType = options.ai;
|
||||
|
||||
// Auto-detect or prompt for AI type
|
||||
if (!aiType) {
|
||||
const { detected, suggested } = detectAIType();
|
||||
|
||||
if (detected.length > 0) {
|
||||
logger.info(`Detected: ${detected.map(t => chalk.cyan(t)).join(', ')}`);
|
||||
}
|
||||
|
||||
const response = await prompts({
|
||||
type: 'select',
|
||||
name: 'aiType',
|
||||
message: 'Select AI assistant to install for:',
|
||||
choices: AI_TYPES.map(type => ({
|
||||
title: getAITypeDescription(type),
|
||||
value: type,
|
||||
})),
|
||||
initial: suggested ? AI_TYPES.indexOf(suggested) : 0,
|
||||
});
|
||||
|
||||
if (!response.aiType) {
|
||||
logger.warn('Installation cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
aiType = response.aiType as AIType;
|
||||
}
|
||||
|
||||
logger.info(`Installing for: ${chalk.cyan(getAITypeDescription(aiType))}`);
|
||||
|
||||
const spinner = ora('Installing files...').start();
|
||||
const cwd = process.cwd();
|
||||
let copiedFolders: string[] = [];
|
||||
let installMethod = 'template';
|
||||
|
||||
try {
|
||||
// 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';
|
||||
}
|
||||
|
||||
const methodMessage = {
|
||||
github: 'Installed from GitHub release!',
|
||||
bundled: 'Installed from bundled assets!',
|
||||
template: 'Generated from templates!',
|
||||
}[installMethod];
|
||||
|
||||
spinner.succeed(methodMessage);
|
||||
|
||||
// Summary
|
||||
console.log();
|
||||
logger.info('Installed folders:');
|
||||
copiedFolders.forEach(folder => {
|
||||
console.log(` ${chalk.green('+')} ${folder}`);
|
||||
});
|
||||
|
||||
console.log();
|
||||
logger.success('UI/UX Pro Max installed successfully!');
|
||||
|
||||
// Next steps
|
||||
console.log();
|
||||
console.log(chalk.bold('Next steps:'));
|
||||
console.log(chalk.dim(' 1. Restart your AI coding assistant'));
|
||||
console.log(chalk.dim(' 2. Try: "Build a landing page for a SaaS product"'));
|
||||
console.log();
|
||||
} catch (error) {
|
||||
spinner.fail('Installation failed');
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
36
skills/ui-ux-pro-max/cli/src/commands/update.ts
Normal file
36
skills/ui-ux-pro-max/cli/src/commands/update.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { getLatestRelease } from '../utils/github.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { initCommand } from './init.js';
|
||||
import type { AIType } from '../types/index.js';
|
||||
|
||||
interface UpdateOptions {
|
||||
ai?: AIType;
|
||||
}
|
||||
|
||||
export async function updateCommand(options: UpdateOptions): Promise<void> {
|
||||
logger.title('UI/UX Pro Max Updater');
|
||||
|
||||
const spinner = ora('Checking for updates...').start();
|
||||
|
||||
try {
|
||||
const release = await getLatestRelease();
|
||||
spinner.succeed(`Latest version: ${chalk.cyan(release.tag_name)}`);
|
||||
|
||||
console.log();
|
||||
logger.info('Running update (same as init with latest version)...');
|
||||
console.log();
|
||||
|
||||
await initCommand({
|
||||
ai: options.ai,
|
||||
force: true,
|
||||
});
|
||||
} catch (error) {
|
||||
spinner.fail('Update check failed');
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
42
skills/ui-ux-pro-max/cli/src/commands/versions.ts
Normal file
42
skills/ui-ux-pro-max/cli/src/commands/versions.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { fetchReleases } from '../utils/github.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
export async function versionsCommand(): Promise<void> {
|
||||
const spinner = ora('Fetching available versions...').start();
|
||||
|
||||
try {
|
||||
const releases = await fetchReleases();
|
||||
|
||||
if (releases.length === 0) {
|
||||
spinner.warn('No releases found');
|
||||
return;
|
||||
}
|
||||
|
||||
spinner.succeed(`Found ${releases.length} version(s)\n`);
|
||||
|
||||
console.log(chalk.bold('Available versions:\n'));
|
||||
|
||||
releases.forEach((release, index) => {
|
||||
const isLatest = index === 0;
|
||||
const tag = release.tag_name;
|
||||
const date = new Date(release.published_at).toLocaleDateString();
|
||||
|
||||
if (isLatest) {
|
||||
console.log(` ${chalk.green('*')} ${chalk.bold(tag)} ${chalk.dim(`(${date})`)} ${chalk.green('[latest]')}`);
|
||||
} else {
|
||||
console.log(` ${tag} ${chalk.dim(`(${date})`)}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log();
|
||||
logger.dim('Use: uipro init --version <tag> to install a specific version');
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to fetch versions');
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
63
skills/ui-ux-pro-max/cli/src/index.ts
Normal file
63
skills/ui-ux-pro-max/cli/src/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { initCommand } from './commands/init.js';
|
||||
import { versionsCommand } from './commands/versions.js';
|
||||
import { updateCommand } from './commands/update.js';
|
||||
import type { AIType } from './types/index.js';
|
||||
import { AI_TYPES } from './types/index.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('uipro')
|
||||
.description('CLI to install UI/UX Pro Max skill for AI coding assistants')
|
||||
.version(pkg.version);
|
||||
|
||||
program
|
||||
.command('init')
|
||||
.description('Install UI/UX Pro Max skill to current project')
|
||||
.option('-a, --ai <type>', `AI assistant type (${AI_TYPES.join(', ')})`)
|
||||
.option('-f, --force', 'Overwrite existing files')
|
||||
.option('-o, --offline', 'Skip GitHub download, use bundled assets only')
|
||||
.action(async (options) => {
|
||||
if (options.ai && !AI_TYPES.includes(options.ai)) {
|
||||
console.error(`Invalid AI type: ${options.ai}`);
|
||||
console.error(`Valid types: ${AI_TYPES.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
await initCommand({
|
||||
ai: options.ai as AIType | undefined,
|
||||
force: options.force,
|
||||
offline: options.offline,
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('versions')
|
||||
.description('List available versions')
|
||||
.action(versionsCommand);
|
||||
|
||||
program
|
||||
.command('update')
|
||||
.description('Update UI/UX Pro Max to latest version')
|
||||
.option('-a, --ai <type>', `AI assistant type (${AI_TYPES.join(', ')})`)
|
||||
.action(async (options) => {
|
||||
if (options.ai && !AI_TYPES.includes(options.ai)) {
|
||||
console.error(`Invalid AI type: ${options.ai}`);
|
||||
console.error(`Valid types: ${AI_TYPES.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
await updateCommand({
|
||||
ai: options.ai as AIType | undefined,
|
||||
});
|
||||
});
|
||||
|
||||
program.parse();
|
||||
63
skills/ui-ux-pro-max/cli/src/types/index.ts
Normal file
63
skills/ui-ux-pro-max/cli/src/types/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
export type AIType = 'claude' | 'cursor' | 'windsurf' | 'antigravity' | 'copilot' | 'kiro' | 'roocode' | 'codex' | 'qoder' | 'gemini' | 'trae' | 'opencode' | 'continue' | 'codebuddy' | 'droid' | 'all';
|
||||
|
||||
export type InstallType = 'full' | 'reference';
|
||||
|
||||
export interface Release {
|
||||
tag_name: string;
|
||||
name: string;
|
||||
published_at: string;
|
||||
html_url: string;
|
||||
assets: Asset[];
|
||||
}
|
||||
|
||||
export interface Asset {
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface InstallConfig {
|
||||
aiType: AIType;
|
||||
version?: string;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
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', 'droid', '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'],
|
||||
windsurf: ['.windsurf', '.shared'],
|
||||
antigravity: ['.agent', '.shared'],
|
||||
copilot: ['.github', '.shared'],
|
||||
kiro: ['.kiro', '.shared'],
|
||||
codex: ['.codex'],
|
||||
roocode: ['.roo', '.shared'],
|
||||
qoder: ['.qoder', '.shared'],
|
||||
gemini: ['.gemini', '.shared'],
|
||||
trae: ['.trae', '.shared'],
|
||||
opencode: ['.opencode', '.shared'],
|
||||
continue: ['.continue'],
|
||||
codebuddy: ['.codebuddy'],
|
||||
droid: ['.factory'],
|
||||
};
|
||||
105
skills/ui-ux-pro-max/cli/src/utils/detect.ts
Normal file
105
skills/ui-ux-pro-max/cli/src/utils/detect.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { AIType } from '../types/index.js';
|
||||
|
||||
interface DetectionResult {
|
||||
detected: AIType[];
|
||||
suggested: AIType | null;
|
||||
}
|
||||
|
||||
export function detectAIType(cwd: string = process.cwd()): DetectionResult {
|
||||
const detected: AIType[] = [];
|
||||
|
||||
if (existsSync(join(cwd, '.claude'))) {
|
||||
detected.push('claude');
|
||||
}
|
||||
if (existsSync(join(cwd, '.cursor'))) {
|
||||
detected.push('cursor');
|
||||
}
|
||||
if (existsSync(join(cwd, '.windsurf'))) {
|
||||
detected.push('windsurf');
|
||||
}
|
||||
if (existsSync(join(cwd, '.agent'))) {
|
||||
detected.push('antigravity');
|
||||
}
|
||||
if (existsSync(join(cwd, '.github'))) {
|
||||
detected.push('copilot');
|
||||
}
|
||||
if (existsSync(join(cwd, '.kiro'))) {
|
||||
detected.push('kiro');
|
||||
}
|
||||
if (existsSync(join(cwd, '.codex'))) {
|
||||
detected.push('codex');
|
||||
}
|
||||
if (existsSync(join(cwd, '.roo'))) {
|
||||
detected.push('roocode');
|
||||
}
|
||||
if (existsSync(join(cwd, '.qoder'))) {
|
||||
detected.push('qoder');
|
||||
}
|
||||
if (existsSync(join(cwd, '.gemini'))) {
|
||||
detected.push('gemini');
|
||||
}
|
||||
if (existsSync(join(cwd, '.trae'))) {
|
||||
detected.push('trae');
|
||||
}
|
||||
if (existsSync(join(cwd, '.opencode'))) {
|
||||
detected.push('opencode');
|
||||
}
|
||||
if (existsSync(join(cwd, '.continue'))) {
|
||||
detected.push('continue');
|
||||
}
|
||||
if (existsSync(join(cwd, '.codebuddy'))) {
|
||||
detected.push('codebuddy');
|
||||
}
|
||||
if (existsSync(join(cwd, '.factory'))) {
|
||||
detected.push('droid');
|
||||
}
|
||||
|
||||
// Suggest based on what's detected
|
||||
let suggested: AIType | null = null;
|
||||
if (detected.length === 1) {
|
||||
suggested = detected[0];
|
||||
} else if (detected.length > 1) {
|
||||
suggested = 'all';
|
||||
}
|
||||
|
||||
return { detected, suggested };
|
||||
}
|
||||
|
||||
export function getAITypeDescription(aiType: AIType): string {
|
||||
switch (aiType) {
|
||||
case 'claude':
|
||||
return 'Claude Code (.claude/skills/)';
|
||||
case 'cursor':
|
||||
return 'Cursor (.cursor/skills/)';
|
||||
case 'windsurf':
|
||||
return 'Windsurf (.windsurf/skills/)';
|
||||
case 'antigravity':
|
||||
return 'Antigravity (.agent/skills/)';
|
||||
case 'copilot':
|
||||
return 'GitHub Copilot (.github/prompts/)';
|
||||
case 'kiro':
|
||||
return 'Kiro (.kiro/steering/)';
|
||||
case 'codex':
|
||||
return 'Codex (.codex/skills/)';
|
||||
case 'roocode':
|
||||
return 'RooCode (.roo/skills/)';
|
||||
case 'qoder':
|
||||
return 'Qoder (.qoder/skills/)';
|
||||
case 'gemini':
|
||||
return 'Gemini CLI (.gemini/skills/)';
|
||||
case 'trae':
|
||||
return 'Trae (.trae/skills/)';
|
||||
case 'opencode':
|
||||
return 'OpenCode (.opencode/skills/)';
|
||||
case 'continue':
|
||||
return 'Continue (.continue/skills/)';
|
||||
case 'codebuddy':
|
||||
return 'CodeBuddy (.codebuddy/skills/)';
|
||||
case 'droid':
|
||||
return 'Droid (Factory) (.factory/skills/)';
|
||||
case 'all':
|
||||
return 'All AI assistants';
|
||||
}
|
||||
}
|
||||
149
skills/ui-ux-pro-max/cli/src/utils/extract.ts
Normal file
149
skills/ui-ux-pro-max/cli/src/utils/extract.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { mkdir, rm, access, cp, mkdtemp, readdir } from 'node:fs/promises';
|
||||
import { join, basename } from 'node:path';
|
||||
import { exec } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { tmpdir } from 'node:os';
|
||||
import type { AIType } from '../types/index.js';
|
||||
import { AI_FOLDERS } from '../types/index.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const EXCLUDED_FILES = ['settings.local.json'];
|
||||
|
||||
export async function extractZip(zipPath: string, destDir: string): Promise<void> {
|
||||
try {
|
||||
const isWindows = process.platform === 'win32';
|
||||
if (isWindows) {
|
||||
await execAsync(`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`);
|
||||
} else {
|
||||
await execAsync(`unzip -o "${zipPath}" -d "${destDir}"`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to extract zip: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyFolders(
|
||||
sourceDir: string,
|
||||
targetDir: string,
|
||||
aiType: AIType
|
||||
): Promise<string[]> {
|
||||
const copiedFolders: string[] = [];
|
||||
|
||||
const foldersToCopy = aiType === 'all'
|
||||
? Object.values(AI_FOLDERS).flat()
|
||||
: AI_FOLDERS[aiType];
|
||||
|
||||
// Deduplicate folders (e.g., .shared might be listed multiple times)
|
||||
const uniqueFolders = [...new Set(foldersToCopy)];
|
||||
|
||||
for (const folder of uniqueFolders) {
|
||||
const sourcePath = join(sourceDir, folder);
|
||||
const targetPath = join(targetDir, folder);
|
||||
|
||||
// Check if source folder exists
|
||||
const sourceExists = await exists(sourcePath);
|
||||
if (!sourceExists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create target directory if needed
|
||||
await mkdir(targetPath, { recursive: true });
|
||||
|
||||
// Filter function to exclude certain files
|
||||
const filterFn = (src: string): boolean => {
|
||||
const fileName = basename(src);
|
||||
return !EXCLUDED_FILES.includes(fileName);
|
||||
};
|
||||
|
||||
// Copy recursively
|
||||
try {
|
||||
await cp(sourcePath, targetPath, { recursive: true, filter: filterFn });
|
||||
copiedFolders.push(folder);
|
||||
} catch {
|
||||
// Try shell fallback for older Node versions
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
await execAsync(`xcopy "${sourcePath}" "${targetPath}" /E /I /Y`);
|
||||
} else {
|
||||
await execAsync(`cp -r "${sourcePath}/." "${targetPath}"`);
|
||||
}
|
||||
copiedFolders.push(folder);
|
||||
} catch {
|
||||
// Skip if copy fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return copiedFolders;
|
||||
}
|
||||
|
||||
export async function cleanup(tempDir: string): Promise<void> {
|
||||
try {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a temporary directory for extracting ZIP files
|
||||
*/
|
||||
export async function createTempDir(): Promise<string> {
|
||||
return mkdtemp(join(tmpdir(), 'uipro-'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the extracted folder inside temp directory
|
||||
* GitHub release ZIPs often contain a single root folder
|
||||
*/
|
||||
async function findExtractedRoot(tempDir: string): Promise<string> {
|
||||
const entries = await readdir(tempDir, { withFileTypes: true });
|
||||
const dirs = entries.filter(e => e.isDirectory());
|
||||
|
||||
// If there's exactly one directory, it's likely the extracted root
|
||||
if (dirs.length === 1) {
|
||||
return join(tempDir, dirs[0].name);
|
||||
}
|
||||
|
||||
// Otherwise, assume tempDir itself is the root
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install from a downloaded and extracted ZIP file
|
||||
*/
|
||||
export async function installFromZip(
|
||||
zipPath: string,
|
||||
targetDir: string,
|
||||
aiType: AIType
|
||||
): Promise<{ copiedFolders: string[]; tempDir: string }> {
|
||||
// Create temp directory
|
||||
const tempDir = await createTempDir();
|
||||
|
||||
try {
|
||||
// Extract ZIP to temp directory
|
||||
await extractZip(zipPath, tempDir);
|
||||
|
||||
// Find the actual root of the extracted content
|
||||
const extractedRoot = await findExtractedRoot(tempDir);
|
||||
|
||||
// Copy folders from extracted content to target
|
||||
const copiedFolders = await copyFolders(extractedRoot, targetDir, aiType);
|
||||
|
||||
return { copiedFolders, tempDir };
|
||||
} catch (error) {
|
||||
// Cleanup on error
|
||||
await cleanup(tempDir);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
104
skills/ui-ux-pro-max/cli/src/utils/github.ts
Normal file
104
skills/ui-ux-pro-max/cli/src/utils/github.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import type { Release } from '../types/index.js';
|
||||
|
||||
const REPO_OWNER = 'nextlevelbuilder';
|
||||
const REPO_NAME = 'ui-ux-pro-max-skill';
|
||||
const API_BASE = 'https://api.github.com';
|
||||
|
||||
export class GitHubRateLimitError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'GitHubRateLimitError';
|
||||
}
|
||||
}
|
||||
|
||||
export class GitHubDownloadError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'GitHubDownloadError';
|
||||
}
|
||||
}
|
||||
|
||||
function checkRateLimit(response: Response): void {
|
||||
const remaining = response.headers.get('x-ratelimit-remaining');
|
||||
if (response.status === 403 && remaining === '0') {
|
||||
const resetTime = response.headers.get('x-ratelimit-reset');
|
||||
const resetDate = resetTime ? new Date(parseInt(resetTime) * 1000).toLocaleTimeString() : 'unknown';
|
||||
throw new GitHubRateLimitError(`GitHub API rate limit exceeded. Resets at ${resetDate}`);
|
||||
}
|
||||
if (response.status === 429) {
|
||||
throw new GitHubRateLimitError('GitHub API rate limit exceeded (429 Too Many Requests)');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchReleases(): Promise<Release[]> {
|
||||
const url = `${API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/releases`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'uipro-cli',
|
||||
},
|
||||
});
|
||||
|
||||
checkRateLimit(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new GitHubDownloadError(`Failed to fetch releases: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getLatestRelease(): Promise<Release> {
|
||||
const url = `${API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'uipro-cli',
|
||||
},
|
||||
});
|
||||
|
||||
checkRateLimit(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new GitHubDownloadError(`Failed to fetch latest release: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function downloadRelease(url: string, dest: string): Promise<void> {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'uipro-cli',
|
||||
'Accept': 'application/octet-stream',
|
||||
},
|
||||
});
|
||||
|
||||
checkRateLimit(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new GitHubDownloadError(`Failed to download: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
await writeFile(dest, Buffer.from(buffer));
|
||||
}
|
||||
|
||||
export function getAssetUrl(release: Release): string | null {
|
||||
// First try to find an uploaded ZIP asset
|
||||
const asset = release.assets.find(a => a.name.endsWith('.zip'));
|
||||
if (asset?.browser_download_url) {
|
||||
return asset.browser_download_url;
|
||||
}
|
||||
|
||||
// Fall back to GitHub's auto-generated archive
|
||||
// Format: https://github.com/{owner}/{repo}/archive/refs/tags/{tag}.zip
|
||||
if (release.tag_name) {
|
||||
return `https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/${release.tag_name}.zip`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
11
skills/ui-ux-pro-max/cli/src/utils/logger.ts
Normal file
11
skills/ui-ux-pro-max/cli/src/utils/logger.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
export const logger = {
|
||||
info: (msg: string) => console.log(chalk.blue('info'), msg),
|
||||
success: (msg: string) => console.log(chalk.green('success'), msg),
|
||||
warn: (msg: string) => console.log(chalk.yellow('warn'), msg),
|
||||
error: (msg: string) => console.log(chalk.red('error'), msg),
|
||||
|
||||
title: (msg: string) => console.log(chalk.bold.cyan(`\n${msg}\n`)),
|
||||
dim: (msg: string) => console.log(chalk.dim(msg)),
|
||||
};
|
||||
224
skills/ui-ux-pro-max/cli/src/utils/template.ts
Normal file
224
skills/ui-ux-pro-max/cli/src/utils/template.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
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',
|
||||
droid: 'droid',
|
||||
};
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate platform files for a specific AI type
|
||||
* All platforms use self-contained installation with data and scripts
|
||||
*/
|
||||
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);
|
||||
|
||||
// Copy data and scripts into the skill directory (self-contained)
|
||||
await copyDataAndScripts(skillDir);
|
||||
|
||||
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