chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
const localBin = (name) => join('node_modules', '.bin', isWindows ? `${name}.cmd` : name);
|
||||
|
||||
function run(command, args) {
|
||||
const result = spawnSync(command, args, { stdio: 'inherit', shell: false });
|
||||
if (result.error && result.error.code === 'ENOENT') {
|
||||
return { missing: true, status: 127 };
|
||||
}
|
||||
return { missing: false, status: result.status ?? 1 };
|
||||
}
|
||||
|
||||
const bunResult = run('bun', ['build', 'src/index.ts', '--outdir', 'dist', '--target', 'node']);
|
||||
if (!bunResult.missing) {
|
||||
process.exit(bunResult.status);
|
||||
}
|
||||
|
||||
const tsc = localBin('tsc');
|
||||
if (!existsSync(tsc)) {
|
||||
console.error('Build failed: neither bun nor local TypeScript compiler is available.');
|
||||
console.error('Run `npm ci` first, or install Bun from https://bun.sh/docs/installation.');
|
||||
process.exit(127);
|
||||
}
|
||||
|
||||
console.warn('bun not found; falling back to TypeScript compiler output.');
|
||||
const tscResult = run(tsc, []);
|
||||
process.exit(tscResult.status);
|
||||
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import {
|
||||
access,
|
||||
mkdir,
|
||||
readdir,
|
||||
readFile,
|
||||
rm,
|
||||
writeFile,
|
||||
} from 'node:fs/promises';
|
||||
import { dirname, join, relative, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(__dirname, '..', '..');
|
||||
const sourceRoot = join(repoRoot, 'src', 'ui-ux-pro-max');
|
||||
const assetRoot = join(repoRoot, 'cli', 'assets');
|
||||
const dirsToSync = ['data', 'scripts', 'templates'];
|
||||
const checkOnly = process.argv.includes('--check');
|
||||
|
||||
// The 6 sibling sub-skills are bundled (as static copies) so `uipro init`
|
||||
// installs all 7 skills, not just the template-rendered orchestrator. Source
|
||||
// of truth is .claude/skills/ (the orchestrator ui-ux-pro-max is rendered from
|
||||
// templates at install time, so it is not mirrored here).
|
||||
const skillsSourceRoot = join(repoRoot, '.claude', 'skills');
|
||||
const skillsAssetRoot = join(assetRoot, 'skills');
|
||||
const subSkills = ['banner-design', 'brand', 'design', 'design-system', 'slides', 'ui-styling'];
|
||||
|
||||
// ponytail: only text is bundled. Excludes (a) heavy binary assets — the
|
||||
// canvas fonts are ~5.8MB and a skill registers from its SKILL.md, not its
|
||||
// fonts — and (b) Python build cruft (__pycache__/*.pyc, .coverage) that would
|
||||
// otherwise be picked up from a local run.
|
||||
const isExcludedAssetFile = (rel) =>
|
||||
rel.split('/').some((seg) => seg === 'canvas-fonts' || seg === '__pycache__') ||
|
||||
/\.(ttf|otf|woff2?|png|jpe?g|gif|ico|coverage|pyc)$/i.test(rel);
|
||||
|
||||
// ponytail: all synced assets are text (csv/json/md/py); normalize CRLF->LF so
|
||||
// the byte hash and the on-disk copy don't drift with git autocrlf across platforms.
|
||||
const toLF = (text) => text.replace(/\r\n/g, '\n');
|
||||
|
||||
async function exists(path) {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function assertInsideRepo(path) {
|
||||
const resolvedPath = resolve(path);
|
||||
if (!resolvedPath.startsWith(repoRoot)) {
|
||||
throw new Error(`Refusing to modify path outside repository: ${resolvedPath}`);
|
||||
}
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
async function listFiles(root) {
|
||||
const files = [];
|
||||
|
||||
async function walk(dir) {
|
||||
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
files.push(relative(root, fullPath).replaceAll('\\', '/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(root);
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
async function fileHash(path) {
|
||||
const content = toLF(await readFile(path, 'utf8'));
|
||||
return createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
async function checkAssets() {
|
||||
const drift = [];
|
||||
|
||||
for (const dir of dirsToSync) {
|
||||
const sourceDir = join(sourceRoot, dir);
|
||||
const targetDir = join(assetRoot, dir);
|
||||
|
||||
if (!(await exists(sourceDir))) {
|
||||
drift.push(`missing source directory: ${relative(repoRoot, sourceDir)}`);
|
||||
continue;
|
||||
}
|
||||
if (!(await exists(targetDir))) {
|
||||
drift.push(`missing asset directory: ${relative(repoRoot, targetDir)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceFiles = (await listFiles(sourceDir)).filter((f) => !isExcludedAssetFile(f));
|
||||
const targetFiles = (await listFiles(targetDir)).filter((f) => !isExcludedAssetFile(f));
|
||||
const allFiles = [...new Set([...sourceFiles, ...targetFiles])].sort();
|
||||
|
||||
for (const file of allFiles) {
|
||||
const sourcePath = join(sourceDir, file);
|
||||
const targetPath = join(targetDir, file);
|
||||
|
||||
if (!sourceFiles.includes(file)) {
|
||||
drift.push(`extra asset file: ${dir}/${file}`);
|
||||
} else if (!targetFiles.includes(file)) {
|
||||
drift.push(`missing asset file: ${dir}/${file}`);
|
||||
} else if ((await fileHash(sourcePath)) !== (await fileHash(targetPath))) {
|
||||
drift.push(`stale asset file: ${dir}/${file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sub-skills (text content only; fonts/binaries intentionally excluded)
|
||||
for (const name of subSkills) {
|
||||
const sourceDir = join(skillsSourceRoot, name);
|
||||
const targetDir = join(skillsAssetRoot, name);
|
||||
|
||||
if (!(await exists(sourceDir))) {
|
||||
drift.push(`missing source sub-skill: ${relative(repoRoot, sourceDir)}`);
|
||||
continue;
|
||||
}
|
||||
if (!(await exists(targetDir))) {
|
||||
drift.push(`missing asset sub-skill: skills/${name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceFiles = (await listFiles(sourceDir)).filter((f) => !isExcludedAssetFile(f));
|
||||
const targetFiles = (await listFiles(targetDir)).filter((f) => !isExcludedAssetFile(f));
|
||||
const allFiles = [...new Set([...sourceFiles, ...targetFiles])].sort();
|
||||
|
||||
for (const file of allFiles) {
|
||||
const sourcePath = join(sourceDir, file);
|
||||
const targetPath = join(targetDir, file);
|
||||
|
||||
if (!sourceFiles.includes(file)) {
|
||||
drift.push(`extra asset file: skills/${name}/${file}`);
|
||||
} else if (!targetFiles.includes(file)) {
|
||||
drift.push(`missing asset file: skills/${name}/${file}`);
|
||||
} else if ((await fileHash(sourcePath)) !== (await fileHash(targetPath))) {
|
||||
drift.push(`stale asset file: skills/${name}/${file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (drift.length > 0) {
|
||||
console.error('CLI assets are out of sync with src/ui-ux-pro-max:');
|
||||
for (const item of drift) {
|
||||
console.error(` - ${item}`);
|
||||
}
|
||||
console.error('\nRun: npm run sync:assets');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('CLI assets are in sync.');
|
||||
}
|
||||
|
||||
async function syncAssets() {
|
||||
assertInsideRepo(assetRoot);
|
||||
await mkdir(assetRoot, { recursive: true });
|
||||
|
||||
for (const dir of dirsToSync) {
|
||||
const sourceDir = join(sourceRoot, dir);
|
||||
const targetDir = assertInsideRepo(join(assetRoot, dir));
|
||||
|
||||
if (!(await exists(sourceDir))) {
|
||||
throw new Error(`Source directory does not exist: ${sourceDir}`);
|
||||
}
|
||||
|
||||
if (await exists(targetDir)) {
|
||||
await rm(targetDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
for (const file of await listFiles(sourceDir)) {
|
||||
if (isExcludedAssetFile(file)) continue;
|
||||
const targetPath = assertInsideRepo(join(targetDir, file));
|
||||
await mkdir(dirname(targetPath), { recursive: true });
|
||||
await writeFile(targetPath, toLF(await readFile(join(sourceDir, file), 'utf8')));
|
||||
}
|
||||
}
|
||||
|
||||
// Sub-skills: copy text content only (fonts/binaries excluded) so all 7
|
||||
// skills ship in the package without bloating it with ~5.8MB of fonts.
|
||||
const skillsTarget = assertInsideRepo(skillsAssetRoot);
|
||||
if (await exists(skillsTarget)) {
|
||||
await rm(skillsTarget, { recursive: true, force: true });
|
||||
}
|
||||
for (const name of subSkills) {
|
||||
const sourceDir = join(skillsSourceRoot, name);
|
||||
if (!(await exists(sourceDir))) {
|
||||
throw new Error(`Source sub-skill does not exist: ${sourceDir}`);
|
||||
}
|
||||
for (const file of await listFiles(sourceDir)) {
|
||||
if (isExcludedAssetFile(file)) continue;
|
||||
const targetPath = assertInsideRepo(join(skillsTarget, name, file));
|
||||
await mkdir(dirname(targetPath), { recursive: true });
|
||||
await writeFile(targetPath, toLF(await readFile(join(sourceDir, file), 'utf8')));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Synced CLI assets from src/ui-ux-pro-max + 6 sub-skills (normalized to LF).');
|
||||
}
|
||||
|
||||
if (checkOnly) {
|
||||
await checkAssets();
|
||||
} else {
|
||||
await syncAssets();
|
||||
}
|
||||
Reference in New Issue
Block a user