Files
nextlevelbuilder-ui-ux-pro-…/cli/scripts/sync-assets.mjs
Alexander b4d33beada fix CLI asset sync (#355)
* fix CLI asset sync

* fix(cli): normalize line endings in asset sync/check

check:assets hashed raw bytes, so identical CSV/JSON/py content with
CRLF vs LF (git autocrlf on checkout) was reported as stale drift,
blocking the release guard on Windows/mixed checkouts.

- fileHash now normalizes CRLF->LF before hashing, so check:assets
  compares content, not line endings.
- sync:assets writes LF-normalized copies instead of a raw byte copy,
  so re-syncing is deterministic across platforms.

All synced assets are text (csv/json/md/py); no binaries affected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci: enforce CLI asset sync on PRs

Adds a Check asset sync workflow that runs `npm run check:assets` on any
PR touching src/ui-ux-pro-max/** or cli/assets/**, so the bundled CLI
assets can't silently drift from the source of truth.

The check uses only node builtins (no install step) and normalizes
line endings before hashing, so it hard-fails on real content drift
without the CRLF/LF soft-fail hack other approaches needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 12:54:58 +07:00

143 lines
4.0 KiB
JavaScript

#!/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');
// 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);
const targetFiles = await listFiles(targetDir);
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}`);
}
}
}
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)) {
const targetPath = assertInsideRepo(join(targetDir, 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 (normalized to LF).');
}
if (checkOnly) {
await checkAssets();
} else {
await syncAssets();
}