📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
getLatestRelease,
|
||||
getAssetUrl,
|
||||
downloadRelease,
|
||||
getGitHubTokenGuidance,
|
||||
GitHubRateLimitError,
|
||||
GitHubDownloadError,
|
||||
} from '../utils/github.js';
|
||||
@@ -27,6 +28,7 @@ interface InitOptions {
|
||||
offline?: boolean;
|
||||
legacy?: boolean; // Use old ZIP-based install
|
||||
global?: boolean; // Install to home directory (global mode)
|
||||
token?: string; // GitHub PAT for higher API rate limits
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,13 +38,14 @@ interface InitOptions {
|
||||
async function tryGitHubInstall(
|
||||
targetDir: string,
|
||||
aiType: AIType,
|
||||
spinner: ReturnType<typeof ora>
|
||||
spinner: ReturnType<typeof ora>,
|
||||
token?: string
|
||||
): Promise<string[] | null> {
|
||||
let tempDir: string | null = null;
|
||||
|
||||
try {
|
||||
spinner.text = 'Fetching latest release from GitHub...';
|
||||
const release = await getLatestRelease();
|
||||
const release = await getLatestRelease(token);
|
||||
const assetUrl = getAssetUrl(release);
|
||||
|
||||
if (!assetUrl) {
|
||||
@@ -53,7 +56,7 @@ async function tryGitHubInstall(
|
||||
tempDir = await createTempDir();
|
||||
const zipPath = join(tempDir, 'release.zip');
|
||||
|
||||
await downloadRelease(assetUrl, zipPath);
|
||||
await downloadRelease(assetUrl, zipPath, token);
|
||||
|
||||
spinner.text = 'Extracting and installing files...';
|
||||
const { copiedFolders, tempDir: extractedTempDir } = await installFromZip(
|
||||
@@ -73,7 +76,7 @@ async function tryGitHubInstall(
|
||||
}
|
||||
|
||||
if (error instanceof GitHubRateLimitError) {
|
||||
spinner.warn('GitHub rate limit reached, using template generation...');
|
||||
spinner.warn(`GitHub rate limit reached, falling back to bundled assets.\n${getGitHubTokenGuidance()}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -164,7 +167,7 @@ export async function initCommand(options: InitOptions): Promise<void> {
|
||||
}
|
||||
// Try GitHub download first (unless offline mode)
|
||||
if (!options.offline) {
|
||||
const githubResult = await tryGitHubInstall(cwd, aiType, spinner);
|
||||
const githubResult = await tryGitHubInstall(cwd, aiType, spinner, options.token);
|
||||
if (githubResult) {
|
||||
copiedFolders = githubResult;
|
||||
installMethod = 'github';
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { rm, stat } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import prompts from 'prompts';
|
||||
import type { AIType } from '../types/index.js';
|
||||
import type { AIType, ConcreteAIType } from '../types/index.js';
|
||||
import { AI_TYPES, AI_FOLDERS } from '../types/index.js';
|
||||
import { detectAIType, getAITypeDescription } from '../utils/detect.js';
|
||||
import { listBundledSubSkills, loadPlatformConfig } from '../utils/template.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
interface UninstallOptions {
|
||||
@@ -17,19 +18,39 @@ interface UninstallOptions {
|
||||
/**
|
||||
* Remove skill directory for a given AI type
|
||||
*/
|
||||
async function removeSkillDir(baseDir: string, aiType: Exclude<AIType, 'all'>): Promise<string[]> {
|
||||
const folders = AI_FOLDERS[aiType];
|
||||
async function removeSkillDir(baseDir: string, aiType: ConcreteAIType): Promise<string[]> {
|
||||
const removed: string[] = [];
|
||||
|
||||
for (const folder of folders) {
|
||||
const skillDir = join(baseDir, folder, 'skills', 'ui-ux-pro-max');
|
||||
try {
|
||||
await stat(skillDir);
|
||||
await rm(skillDir, { recursive: true, force: true });
|
||||
removed.push(`${folder}/skills/ui-ux-pro-max`);
|
||||
} catch (err: unknown) {
|
||||
// Skip non-existent dirs; re-throw permission or other errors
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
||||
// The orchestrator plus the bundled sibling sub-skills installed by init.
|
||||
const skillNames = ['ui-ux-pro-max', ...(await listBundledSubSkills())];
|
||||
|
||||
// Parent directories to clean. Derive the real install location from the
|
||||
// platform config's skillPath (same source the installer uses), so
|
||||
// non-`skills/` platforms are handled — copilot installs under
|
||||
// `.github/prompts/`, kiro under `.kiro/steering/`. Also clean the legacy
|
||||
// `<folder>/skills/` layout (incl. `.shared/`) so older installs are removed.
|
||||
const parents = new Set<string>();
|
||||
try {
|
||||
const { folderStructure } = await loadPlatformConfig(aiType);
|
||||
parents.add(join(folderStructure.root, dirname(folderStructure.skillPath)));
|
||||
} catch {
|
||||
// No platform config — fall back to the legacy folders below.
|
||||
}
|
||||
for (const folder of AI_FOLDERS[aiType]) {
|
||||
parents.add(join(folder, 'skills'));
|
||||
}
|
||||
|
||||
for (const parent of parents) {
|
||||
for (const name of skillNames) {
|
||||
const skillDir = join(baseDir, parent, name);
|
||||
try {
|
||||
await stat(skillDir);
|
||||
await rm(skillDir, { recursive: true, force: true });
|
||||
removed.push(`${parent.replaceAll('\\', '/')}/${name}`);
|
||||
} catch (err: unknown) {
|
||||
// Skip non-existent dirs; re-throw permission or other errors
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { getLatestRelease } from '../utils/github.js';
|
||||
@@ -5,8 +9,21 @@ import { logger } from '../utils/logger.js';
|
||||
import { initCommand } from './init.js';
|
||||
import type { AIType } from '../types/index.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
interface UpdateOptions {
|
||||
ai?: AIType;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
async function getPackageVersion(): Promise<string> {
|
||||
const packagePath = join(__dirname, '..', 'package.json');
|
||||
const pkg = JSON.parse(await readFile(packagePath, 'utf-8')) as { version: string };
|
||||
return pkg.version;
|
||||
}
|
||||
|
||||
function normalizeTagVersion(tagName: string): string {
|
||||
return tagName.replace(/^v/i, '');
|
||||
}
|
||||
|
||||
export async function updateCommand(options: UpdateOptions): Promise<void> {
|
||||
@@ -15,16 +32,56 @@ export async function updateCommand(options: UpdateOptions): Promise<void> {
|
||||
const spinner = ora('Checking for updates...').start();
|
||||
|
||||
try {
|
||||
const release = await getLatestRelease();
|
||||
const release = await getLatestRelease(options.token);
|
||||
const currentVersion = await getPackageVersion();
|
||||
const latestVersion = normalizeTagVersion(release.tag_name);
|
||||
spinner.succeed(`Latest version: ${chalk.cyan(release.tag_name)}`);
|
||||
|
||||
if (currentVersion !== latestVersion) {
|
||||
console.log();
|
||||
|
||||
// Only auto-run with a well-formed semver, so nothing unexpected can
|
||||
// reach the shell that npm.cmd requires on Windows.
|
||||
if (!/^\d+\.\d+\.\d+([.-][0-9A-Za-z.-]+)?$/.test(latestVersion)) {
|
||||
logger.warn(`Installed CLI is ${chalk.cyan(currentVersion)}; latest release is ${chalk.cyan(release.tag_name)}.`);
|
||||
logger.info(`Update the CLI package: ${chalk.cyan(`npm install -g uipro-cli@${latestVersion}`)}`);
|
||||
logger.info('Then rerun: uipro init --ai <platform> --force');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Updating CLI from ${chalk.cyan(currentVersion)} to ${chalk.cyan(latestVersion)}...`);
|
||||
console.log();
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
try {
|
||||
// execFileSync with an explicit args array — no shell string to expand.
|
||||
// On Windows npm is npm.cmd, which Node only spawns via a shell.
|
||||
execFileSync(
|
||||
isWindows ? 'npm.cmd' : 'npm',
|
||||
['install', '-g', `uipro-cli@${latestVersion}`],
|
||||
{ stdio: 'inherit', shell: isWindows }
|
||||
);
|
||||
} catch {
|
||||
console.log();
|
||||
logger.error('Automatic update failed (you may need elevated/admin permissions).');
|
||||
logger.info(`Update manually: ${chalk.cyan(`npm install -g uipro-cli@${latestVersion}`)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log();
|
||||
logger.success(`Updated to ${chalk.cyan(latestVersion)}.`);
|
||||
logger.info(`Now rerun ${chalk.cyan('uipro init --ai <platform> --force')} to refresh your skill files.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log();
|
||||
logger.info('Running update (same as init with latest version)...');
|
||||
logger.info('Refreshing installed skill files from this CLI package...');
|
||||
console.log();
|
||||
|
||||
await initCommand({
|
||||
ai: options.ai,
|
||||
force: true,
|
||||
token: options.token,
|
||||
});
|
||||
} catch (error) {
|
||||
spinner.fail('Update check failed');
|
||||
|
||||
@@ -3,11 +3,15 @@ import ora from 'ora';
|
||||
import { fetchReleases } from '../utils/github.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
export async function versionsCommand(): Promise<void> {
|
||||
interface VersionsOptions {
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export async function versionsCommand(options: VersionsOptions = {}): Promise<void> {
|
||||
const spinner = ora('Fetching available versions...').start();
|
||||
|
||||
try {
|
||||
const releases = await fetchReleases();
|
||||
const releases = await fetchReleases(options.token);
|
||||
|
||||
if (releases.length === 0) {
|
||||
spinner.warn('No releases found');
|
||||
@@ -31,7 +35,7 @@ export async function versionsCommand(): Promise<void> {
|
||||
});
|
||||
|
||||
console.log();
|
||||
logger.dim('Use: uipro init --version <tag> to install a specific version');
|
||||
logger.dim('Update the CLI package first, then run: uipro init --ai <platform>');
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to fetch versions');
|
||||
if (error instanceof Error) {
|
||||
|
||||
@@ -27,8 +27,9 @@ program
|
||||
.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')
|
||||
.option('-o, --offline', 'Compatibility flag; template installs use bundled assets')
|
||||
.option('-g, --global', 'Install globally to home directory (~/) instead of current project')
|
||||
.option('-t, --token <token>', 'GitHub Personal Access Token for higher API rate limits')
|
||||
.action(async (options) => {
|
||||
if (options.ai && !AI_TYPES.includes(options.ai)) {
|
||||
console.error(`Invalid AI type: ${options.ai}`);
|
||||
@@ -40,18 +41,21 @@ program
|
||||
force: options.force,
|
||||
offline: options.offline,
|
||||
global: options.global,
|
||||
token: options.token,
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('versions')
|
||||
.description('List available versions')
|
||||
.option('-t, --token <token>', 'GitHub Personal Access Token for higher API rate limits')
|
||||
.action(versionsCommand);
|
||||
|
||||
program
|
||||
.command('update')
|
||||
.description('Update UI/UX Pro Max to latest version')
|
||||
.option('-a, --ai <type>', `AI assistant type (${AI_TYPES.join(', ')})`)
|
||||
.option('-t, --token <token>', 'GitHub Personal Access Token for higher API rate limits')
|
||||
.action(async (options) => {
|
||||
if (options.ai && !AI_TYPES.includes(options.ai)) {
|
||||
console.error(`Invalid AI type: ${options.ai}`);
|
||||
@@ -60,6 +64,7 @@ program
|
||||
}
|
||||
await updateCommand({
|
||||
ai: options.ai as AIType | undefined,
|
||||
token: options.token,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type AIType = 'claude' | 'cursor' | 'windsurf' | 'antigravity' | 'copilot' | 'kiro' | 'roocode' | 'codex' | 'qoder' | 'gemini' | 'trae' | 'opencode' | 'continue' | 'codebuddy' | 'droid' | 'kilocode' | 'warp' | 'augment' | 'all';
|
||||
export type ConcreteAIType = Exclude<AIType, 'all'>;
|
||||
|
||||
export type InstallType = 'full' | 'reference';
|
||||
|
||||
@@ -46,7 +47,7 @@ export const AI_TYPES: AIType[] = ['claude', 'cursor', 'windsurf', 'antigravity'
|
||||
// Legacy folder mapping for backward compatibility with ZIP-based installs.
|
||||
// Note: .shared is included for platforms that used ZIP installs. Post-ZIP platforms
|
||||
// (kilocode, warp, augment) include .shared as a no-op for consistent uninstall behavior.
|
||||
export const AI_FOLDERS: Record<Exclude<AIType, 'all'>, string[]> = {
|
||||
export const AI_FOLDERS: Record<ConcreteAIType, string[]> = {
|
||||
claude: ['.claude'],
|
||||
cursor: ['.cursor', '.shared'],
|
||||
windsurf: ['.windsurf', '.shared'],
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { AIType } from '../types/index.js';
|
||||
import type { AIType, ConcreteAIType } from '../types/index.js';
|
||||
|
||||
interface DetectionResult {
|
||||
detected: AIType[];
|
||||
detected: ConcreteAIType[];
|
||||
suggested: AIType | null;
|
||||
}
|
||||
|
||||
export function detectAIType(cwd: string = process.cwd()): DetectionResult {
|
||||
const detected: AIType[] = [];
|
||||
const detected: ConcreteAIType[] = [];
|
||||
|
||||
if (existsSync(join(cwd, '.claude'))) {
|
||||
detected.push('claude');
|
||||
|
||||
@@ -19,25 +19,45 @@ export class GitHubDownloadError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export function getGitHubTokenGuidance(): string {
|
||||
return (
|
||||
'To increase your GitHub API rate limit, set the UI_PRO_MAX_GITHUB_TOKEN environment variable\n' +
|
||||
'to a GitHub Personal Access Token (no scopes needed for public repos).\n' +
|
||||
'Create one at: https://github.com/settings/tokens\n' +
|
||||
'Example: UI_PRO_MAX_GITHUB_TOKEN=ghp_xxx uipro init\n' +
|
||||
'Or pass it directly: uipro init --token ghp_xxx'
|
||||
);
|
||||
}
|
||||
|
||||
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}`);
|
||||
throw new GitHubRateLimitError(
|
||||
`GitHub API rate limit exceeded. Resets at ${resetDate}.\n${getGitHubTokenGuidance()}`
|
||||
);
|
||||
}
|
||||
if (response.status === 429) {
|
||||
throw new GitHubRateLimitError('GitHub API rate limit exceeded (429 Too Many Requests)');
|
||||
throw new GitHubRateLimitError(
|
||||
`GitHub API rate limit exceeded (429 Too Many Requests).\n${getGitHubTokenGuidance()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchReleases(): Promise<Release[]> {
|
||||
function getAuthHeaders(token?: string): Record<string, string> {
|
||||
const resolved = (token || process.env['UI_PRO_MAX_GITHUB_TOKEN'] || process.env['GITHUB_TOKEN'])?.trim();
|
||||
return resolved ? { 'Authorization': `Bearer ${resolved}` } : {};
|
||||
}
|
||||
|
||||
export async function fetchReleases(token?: string): 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',
|
||||
...getAuthHeaders(token),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -50,13 +70,14 @@ export async function fetchReleases(): Promise<Release[]> {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getLatestRelease(): Promise<Release> {
|
||||
export async function getLatestRelease(token?: string): 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',
|
||||
...getAuthHeaders(token),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,11 +90,12 @@ export async function getLatestRelease(): Promise<Release> {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function downloadRelease(url: string, dest: string): Promise<void> {
|
||||
export async function downloadRelease(url: string, dest: string, token?: string): Promise<void> {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'uipro-cli',
|
||||
'Accept': 'application/octet-stream',
|
||||
...getAuthHeaders(token),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFile, mkdir, writeFile, cp, access, readdir } from 'node:fs/promises';
|
||||
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';
|
||||
@@ -156,6 +156,23 @@ export async function renderSkillFile(config: PlatformConfig, isGlobal = false):
|
||||
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<void> {
|
||||
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
|
||||
*/
|
||||
@@ -168,17 +185,46 @@ async function copyDataAndScripts(targetSkillDir: string): Promise<void> {
|
||||
|
||||
// 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<string[]> {
|
||||
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<void> {
|
||||
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
|
||||
@@ -222,6 +268,17 @@ export async function generatePlatformFiles(
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user