Files
heygen-com__hyperframes/scripts/sync-ui-primitives.ts
2026-07-09 20:20:49 -04:00

435 lines
15 KiB
TypeScript

#!/usr/bin/env tsx
import {
constants,
copyFileSync,
existsSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import {
extractCanonicalRegion,
injectOperatorBlackTokens,
normalizedSha256,
replaceCanonicalRegion,
validateDemoOnlyCss,
} from "./lib/ui-primitives/canonical.js";
import { isRecord, loadUiPrimitiveScope } from "./lib/ui-primitives/scope.js";
import {
loadOperatorBlackTokens,
renderOperatorBlackTokenBlock,
} from "./lib/ui-primitives/tokens.js";
export type SyncMode = "check" | "write";
export interface SyncPrimitiveOptions {
id: string;
canonicalPath: string;
demoPath: string;
tokenBlock: string;
mode: SyncMode;
}
export interface SyncPrimitiveResult {
id: string;
canonicalChanged: boolean;
demoChanged: boolean;
}
interface PreparedFileChange {
path: string;
content: string;
}
interface PreparedPrimitive extends SyncPrimitiveResult {
changes: PreparedFileChange[];
}
function prepareUiPrimitiveFiles(options: SyncPrimitiveOptions): PreparedPrimitive {
if (!existsSync(options.canonicalPath)) {
throw new Error(`canonical file is missing: ${options.canonicalPath}`);
}
if (!existsSync(options.demoPath)) {
throw new Error(`demo file is missing: ${options.demoPath}`);
}
const canonicalBefore = readFileSync(options.canonicalPath, "utf8");
const demoBefore = readFileSync(options.demoPath, "utf8");
const canonicalAfter = injectOperatorBlackTokens(
canonicalBefore,
options.tokenBlock,
options.canonicalPath,
);
const demoAfter = replaceCanonicalRegion(
demoBefore,
canonicalAfter,
options.id,
options.demoPath,
);
validateDemoOnlyCss(demoAfter, canonicalAfter, options.demoPath);
const region = extractCanonicalRegion(demoAfter, options.demoPath);
if (normalizedSha256(region) !== normalizedSha256(canonicalAfter)) {
throw new Error(`${options.id} canonical/demo normalized SHA-256 mismatch`);
}
const canonicalChanged = canonicalBefore !== canonicalAfter;
const demoChanged = demoBefore !== demoAfter;
const changes: PreparedFileChange[] = [];
if (canonicalChanged) changes.push({ path: options.canonicalPath, content: canonicalAfter });
if (demoChanged) changes.push({ path: options.demoPath, content: demoAfter });
return { id: options.id, canonicalChanged, demoChanged, changes };
}
interface PendingInstallation extends PreparedFileChange {
temporaryPath: string;
backupPath: string;
}
export interface AtomicCommitOptions {
beforeInstall?: (path: string, index: number) => void;
journalPath?: string;
allowedPaths?: string[];
}
type TransactionState = "preparing" | "installing" | "committed";
interface TransactionJournal {
version: 1;
state: TransactionState;
entries: Array<{ path: string }>;
}
function cleanupPath(path: string): void {
if (existsSync(path)) rmSync(path, { force: true });
}
function stagingPaths(path: string): { temporaryPath: string; backupPath: string } {
return {
temporaryPath: `${path}.hf-ui-sync.tmp`,
backupPath: `${path}.hf-ui-sync.bak`,
};
}
export function uiPrimitiveTransactionJournalPath(paths: string[]): string {
const first = paths[0];
if (!first) throw new Error("A sync transaction requires at least one file path");
return resolve(dirname(first), ".hf-ui-sync.transaction.json");
}
// fallow-ignore-next-line complexity
function parseTransactionJournal(
value: unknown,
source: string,
allowedPaths: string[],
): TransactionJournal {
if (!isRecord(value) || value.version !== 1) {
throw new Error(`${source} is not a supported UI primitive transaction journal`);
}
if (value.state !== "preparing" && value.state !== "installing" && value.state !== "committed") {
throw new Error(`${source} has an invalid transaction state`);
}
if (!Array.isArray(value.entries)) throw new Error(`${source} entries must be an array`);
const allowed = new Set(allowedPaths);
const entries: TransactionJournal["entries"] = [];
for (const entry of value.entries) {
if (!isRecord(entry)) throw new Error(`${source} contains an invalid transaction entry`);
if (JSON.stringify(Object.keys(entry).sort()) !== JSON.stringify(["path"])) {
throw new Error(`${source} transaction entries may contain only path`);
}
if (typeof entry.path !== "string" || !allowed.has(entry.path)) {
throw new Error(`${source} contains a path outside the allowed UI primitive scope`);
}
if (entries.some(({ path }) => path === entry.path)) {
throw new Error(`${source} contains duplicate transaction paths`);
}
entries.push({ path: entry.path });
}
return { version: 1, state: value.state, entries };
}
function readTransactionJournal(journalPath: string, allowedPaths: string[]): TransactionJournal {
const parsed: unknown = JSON.parse(readFileSync(journalPath, "utf8"));
return parseTransactionJournal(parsed, journalPath, allowedPaths);
}
function writeTransactionJournal(journalPath: string, journal: TransactionJournal): void {
const content = `${JSON.stringify(journal, null, 2)}\n`;
const nextPath = `${journalPath}.next`;
cleanupPath(nextPath);
writeFileSync(nextPath, content, { flag: "wx" });
renameSync(nextPath, journalPath);
}
function processIsAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return isRecord(error) && error.code === "EPERM";
}
}
function acquireTransactionLock(journalPath: string): string {
const lockPath = `${journalPath}.lock`;
try {
writeFileSync(lockPath, `${process.pid}\n`, { flag: "wx" });
return lockPath;
} catch (error) {
if (!existsSync(lockPath)) throw error;
const owner = Number.parseInt(readFileSync(lockPath, "utf8").trim(), 10);
if (Number.isFinite(owner) && owner > 0 && processIsAlive(owner)) {
throw new Error(`another UI primitive sync transaction is active (pid ${owner})`);
}
cleanupPath(lockPath);
writeFileSync(lockPath, `${process.pid}\n`, { flag: "wx" });
return lockPath;
}
}
function recoverTransaction(journalPath: string, allowedPaths: string[]): void {
const journal = readTransactionJournal(journalPath, allowedPaths);
if (journal.state !== "committed") {
for (const entry of [...journal.entries].reverse()) {
const { backupPath } = stagingPaths(entry.path);
if (existsSync(backupPath)) renameSync(backupPath, entry.path);
}
}
for (const entry of journal.entries) {
const { temporaryPath, backupPath } = stagingPaths(entry.path);
cleanupPath(temporaryPath);
cleanupPath(backupPath);
}
cleanupPath(`${journalPath}.next`);
cleanupPath(journalPath);
}
export function recoverUiPrimitiveChanges(paths: string[], journalPath?: string): void {
if (paths.length === 0) return;
const allowedPaths = [...new Set(paths)];
if (allowedPaths.length !== paths.length || allowedPaths.some((path) => resolve(path) !== path)) {
throw new Error("Allowed UI primitive transaction paths must be unique and absolute");
}
const resolvedJournalPath = journalPath ?? uiPrimitiveTransactionJournalPath(paths);
const lockPath = acquireTransactionLock(resolvedJournalPath);
try {
if (existsSync(resolvedJournalPath)) {
recoverTransaction(resolvedJournalPath, allowedPaths);
return;
}
for (const path of allowedPaths) {
const { temporaryPath, backupPath } = stagingPaths(path);
if (existsSync(backupPath)) renameSync(backupPath, path);
cleanupPath(temporaryPath);
}
cleanupPath(`${resolvedJournalPath}.next`);
} finally {
cleanupPath(lockPath);
}
}
// fallow-ignore-next-line complexity
export function commitUiPrimitiveChangesAtomically(
changes: PreparedFileChange[],
options: AtomicCommitOptions = {},
): void {
if (changes.length === 0) return;
const installations: PendingInstallation[] = changes.map((change) => ({
...change,
...stagingPaths(change.path),
}));
const journalPath =
options.journalPath ?? uiPrimitiveTransactionJournalPath(installations.map(({ path }) => path));
const entries = installations.map(({ path }) => ({ path }));
const allowedPaths = options.allowedPaths ?? installations.map(({ path }) => path);
if (
new Set(allowedPaths).size !== allowedPaths.length ||
allowedPaths.some((path) => resolve(path) !== path) ||
entries.some(({ path }) => !allowedPaths.includes(path))
) {
throw new Error("Atomic sync changes must be a subset of unique absolute allowed paths");
}
const lockPath = acquireTransactionLock(journalPath);
try {
if (existsSync(journalPath)) {
throw new Error(`unfinished sync transaction requires recovery: ${journalPath}`);
}
for (const item of installations) {
if (existsSync(item.temporaryPath) || existsSync(item.backupPath)) {
throw new Error(`refusing to overwrite stale sync staging file for ${item.path}`);
}
}
writeTransactionJournal(journalPath, { version: 1, state: "preparing", entries });
for (const item of installations)
writeFileSync(item.temporaryPath, item.content, { flag: "wx" });
for (const item of installations) {
copyFileSync(item.path, item.backupPath, constants.COPYFILE_EXCL);
}
writeTransactionJournal(journalPath, { version: 1, state: "installing", entries });
for (const [index, item] of installations.entries()) {
options.beforeInstall?.(item.path, index);
renameSync(item.temporaryPath, item.path);
}
writeTransactionJournal(journalPath, { version: 1, state: "committed", entries });
for (const item of installations) cleanupPath(item.backupPath);
cleanupPath(journalPath);
} catch (error) {
if (existsSync(journalPath)) recoverTransaction(journalPath, allowedPaths);
throw error;
} finally {
cleanupPath(`${journalPath}.next`);
cleanupPath(lockPath);
}
}
export function syncUiPrimitiveFiles(options: SyncPrimitiveOptions): SyncPrimitiveResult {
if (options.mode === "write") {
recoverUiPrimitiveChanges([options.canonicalPath, options.demoPath]);
}
const prepared = prepareUiPrimitiveFiles(options);
if (options.mode === "write") commitUiPrimitiveChangesAtomically(prepared.changes);
return {
id: prepared.id,
canonicalChanged: prepared.canonicalChanged,
demoChanged: prepared.demoChanged,
};
}
export interface SyncRunOptions {
repoRoot: string;
mode: SyncMode;
only?: string;
}
export interface SyncRunResult {
selected: number;
changed: string[];
errors: string[];
}
// fallow-ignore-next-line complexity
export function syncUiPrimitives(options: SyncRunOptions): SyncRunResult {
const scope = loadUiPrimitiveScope(
resolve(options.repoRoot, "registry/ui-primitives/operator-black.scope.json"),
);
const tokens = loadOperatorBlackTokens(
resolve(options.repoRoot, "registry/ui-primitives/operator-black.tokens.json"),
);
if (options.only && !scope.items.includes(options.only)) {
throw new Error(`Unknown Operator Black primitive: ${options.only}`);
}
const selected = options.only ? [options.only] : scope.items;
const tokenBlock = renderOperatorBlackTokenBlock(tokens);
const changed: string[] = [];
const errors: string[] = [];
const prepared: PreparedPrimitive[] = [];
const allowedScopePaths = scope.items.flatMap((id) => {
const root = resolve(options.repoRoot, "registry/components", id);
return [resolve(root, `${id}.html`), resolve(root, "demo.html")];
});
const journalPath = resolve(
options.repoRoot,
"registry/ui-primitives/.operator-black-sync.transaction.json",
);
if (options.mode === "write") {
recoverUiPrimitiveChanges(allowedScopePaths, journalPath);
}
for (const id of selected) {
const root = resolve(options.repoRoot, "registry/components", id);
const canonicalPath = resolve(root, `${id}.html`);
const demoPath = resolve(root, "demo.html");
try {
const result = prepareUiPrimitiveFiles({
id,
canonicalPath,
demoPath,
tokenBlock,
mode: "check",
});
prepared.push(result);
if (result.canonicalChanged || result.demoChanged) changed.push(id);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
errors.push(`${id}: ${message}`);
}
}
if (options.mode === "write" && errors.length === 0) {
try {
commitUiPrimitiveChangesAtomically(
prepared.flatMap((item) => item.changes),
{
journalPath,
allowedPaths: allowedScopePaths,
},
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
errors.push(`write transaction: ${message}`);
}
}
return { selected: selected.length, changed, errors };
}
interface CliOptions {
mode: SyncMode;
only?: string;
}
// fallow-ignore-next-line complexity
export function parseSyncCliOptions(args: string[]): CliOptions {
let mode: SyncMode | undefined;
let only: string | undefined;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--write" || arg === "--check") {
const nextMode: SyncMode = arg === "--write" ? "write" : "check";
if (mode) throw new Error("Choose exactly one occurrence of --write or --check");
mode = nextMode;
} else if (arg === "--only") {
if (only) throw new Error("--only may be supplied only once");
const value = args[index + 1];
if (!value || value.startsWith("--")) throw new Error("--only requires a primitive ID");
only = value;
index += 1;
} else {
throw new Error(`Unknown argument: ${arg ?? ""}`);
}
}
if (!mode) throw new Error("Choose exactly one of --write or --check");
return only ? { mode, only } : { mode };
}
function main(): void {
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, "..");
try {
const cli = parseSyncCliOptions(process.argv.slice(2));
const result = syncUiPrimitives({ repoRoot, ...cli });
for (const error of result.errors) console.error(`✗ ${error}`);
if (cli.mode === "check" && result.changed.length > 0) {
console.error(`✗ sync drift: ${result.changed.join(", ")}`);
}
const failures = result.errors.length + (cli.mode === "check" ? result.changed.length : 0);
console.log(
`Operator Black sync: ${result.selected} selected, ${result.changed.length} changed, ${result.errors.length} errors`,
);
if (failures > 0) process.exitCode = 1;
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}
const entry = process.argv[1];
if (entry && import.meta.url === pathToFileURL(entry).href) main();