#!/usr/bin/env node import { spawn } from 'node:child_process'; import { gzipSync } from 'node:zlib'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; const rawArgs = process.argv.slice(2); const cli = parseArgs(rawArgs); const root = path.resolve(readOption('root', process.cwd())); const continueOnError = readFlag('continue-on-error'); const defaultTimeoutMs = readIntegerOption('timeout-ms', 10 * 60 * 1000); const buildTimeoutMs = readIntegerOption('build-timeout-ms', 15 * 60 * 1000); const e2eTimeoutMs = readIntegerOption('e2e-timeout-ms', 25 * 60 * 1000); const packageManager = readOption('package-manager', 'npm'); const report = { schemaVersion: 1, command: `node ${path.relative(root, process.argv[1] ?? 'scripts/validate-clean-checkout.mjs')} ${rawArgs.join(' ')}`.trim(), root, startedAt: new Date().toISOString(), finishedAt: null, node: process.version, platform: { os: os.platform(), release: os.release(), arch: os.arch(), cpus: os.cpus().length, totalMemoryBytes: os.totalmem(), }, packageManager, package: null, steps: [], distMetrics: null, summary: null, }; let stopped = false; const essentialFiles = [ 'package.json', 'index.html', 'vite.config.ts', 'vitest.config.ts', 'playwright.config.ts', 'tsconfig.json', 'src/main.tsx', 'src/App.tsx', ]; const supportedPackageManagers = new Set(['npm']); try { if (readFlag('help') || readFlag('h')) { printHelp(); process.exitCode = 0; } else { process.exitCode = await main(); } } catch (error) { const message = error instanceof Error ? error.stack ?? error.message : String(error); const step = recordSyntheticStep({ name: 'validation harness internal error', status: 'failed', details: message, }); applyStopPolicy(step); process.exitCode = 1; } finally { await finalizeReport(); } async function main() { if (!supportedPackageManagers.has(packageManager)) { const step = recordSyntheticStep({ name: 'package manager support check', status: 'failed', details: `Unsupported package manager "${packageManager}". This validation harness intentionally runs the funded npm workflow so CI evidence is reproducible.`, }); applyStopPolicy(step); return 1; } const packageJsonPath = path.join(root, 'package.json'); if (!(await pathExists(packageJsonPath))) { const step = recordSyntheticStep({ name: 'repository manifest check', status: 'failed', details: `No package.json was found at ${packageJsonPath}. Run this command from the repository root or pass --root .`, }); applyStopPolicy(step); return 1; } const pkg = await readJson(packageJsonPath); const scripts = pkg.scripts ?? {}; report.package = { name: pkg.name ?? null, version: pkg.version ?? null, private: Boolean(pkg.private), scripts: Object.keys(scripts).sort(), dependencies: Object.keys(pkg.dependencies ?? {}).sort(), devDependencies: Object.keys(pkg.devDependencies ?? {}).sort(), }; const missingEssentialFiles = []; for (const relativePath of essentialFiles) { if (!(await pathExists(path.join(root, relativePath)))) { missingEssentialFiles.push(relativePath); } } applyStopPolicy( recordSyntheticStep({ name: 'essential project files check', status: missingEssentialFiles.length === 0 ? 'passed' : 'failed', details: missingEssentialFiles.length === 0 ? `Found ${essentialFiles.length} expected project files.` : `Missing expected files: ${missingEssentialFiles.join(', ')}`, }), ); if (stopped) return exitCodeFromReport(); const lockFiles = []; for (const relativePath of ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock']) { if (await pathExists(path.join(root, relativePath))) { lockFiles.push(relativePath); } } applyStopPolicy( recordSyntheticStep({ name: 'package manager lockfile consistency check', status: lockFiles.length <= 1 ? 'passed' : 'failed', details: lockFiles.length === 0 ? 'No lockfile is present. This is acceptable for the source deliverable; npm install will generate package-lock.json during validation.' : lockFiles.length === 1 ? `Found ${lockFiles[0]}.` : `Multiple lockfiles found (${lockFiles.join(', ')}). Keep only the lockfile for the package manager used in CI.`, }), ); if (stopped) return exitCodeFromReport(); const nodeMajor = Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10); applyStopPolicy( recordSyntheticStep({ name: 'node runtime sanity check', status: nodeMajor >= 18 ? 'passed' : 'failed', details: nodeMajor >= 20 ? `Running on ${process.version}.` : nodeMajor >= 18 ? `Running on ${process.version}. Node 20 LTS or newer is recommended for current Vite/Playwright releases; Node 18 is the minimum acceptable runtime for this harness.` : `Running on ${process.version}. Use Node 20 LTS or newer before validating this Vite application.`, }), ); if (stopped) return exitCodeFromReport(); if (!readFlag('skip-install')) { if (readFlag('frozen')) { if (await pathExists(path.join(root, 'package-lock.json'))) { await executeStep({ name: 'install dependencies with npm ci', command: 'npm', args: ['ci'], timeoutMs: readIntegerOption('install-timeout-ms', 12 * 60 * 1000), }); } else { applyStopPolicy( recordSyntheticStep({ name: 'install dependencies with npm ci', status: 'failed', details: 'The --frozen flag was provided but package-lock.json is absent. Run without --frozen for the source deliverable, or generate and commit a lockfile in the consuming repository.', }), ); } } else { await executeStep({ name: 'install dependencies with npm install', command: 'npm', args: ['install', '--no-audit', '--fund=false'], timeoutMs: readIntegerOption('install-timeout-ms', 12 * 60 * 1000), }); } } else { recordSkip('install dependencies', 'Skipped because --skip-install was provided.'); } if (stopped) return exitCodeFromReport(); await runScriptCandidate({ name: 'format check', candidates: ['format:check', 'prettier:check', 'check:format'], scripts, timeoutMs: defaultTimeoutMs, required: false, }); if (stopped) return exitCodeFromReport(); await runScriptCandidate({ name: 'lint', candidates: ['lint'], scripts, timeoutMs: defaultTimeoutMs, required: true, }); if (stopped) return exitCodeFromReport(); await runTypecheck(scripts); if (stopped) return exitCodeFromReport(); await runUnitTests(scripts); if (stopped) return exitCodeFromReport(); await runScriptCandidate({ name: 'production build', candidates: ['build'], scripts, timeoutMs: buildTimeoutMs, required: true, }); if (!stopped) { await collectAndRecordDistMetrics(); } if (stopped) return exitCodeFromReport(); await runPlaywrightValidation(scripts); return exitCodeFromReport(); } async function runTypecheck(scripts) { const scriptName = findScript(scripts, ['typecheck', 'check:types', 'tsc']); if (scriptName) { await executeStep({ name: `typecheck (${scriptName})`, command: 'npm', args: ['run', scriptName], timeoutMs: defaultTimeoutMs, }); return; } if (await pathExists(path.join(root, 'tsconfig.json'))) { await executeStep({ name: 'typecheck (npx tsc)', command: 'npx', args: ['tsc', '-p', 'tsconfig.json', '--noEmit'], timeoutMs: defaultTimeoutMs, }); return; } applyStopPolicy( recordSyntheticStep({ name: 'typecheck', status: 'failed', details: 'No typecheck script or tsconfig.json was found.', }), ); } async function runUnitTests(scripts) { let scriptName = findScript(scripts, ['test:unit', 'test:vitest', 'vitest']); if (!scriptName && typeof scripts.test === 'string' && /\bvitest\b/i.test(scripts.test)) { scriptName = 'test'; } if (scriptName) { const scriptBody = String(scripts[scriptName] ?? ''); const extraArgs = /\bvitest\b/i.test(scriptBody) && !/\bvitest\s+run\b/i.test(scriptBody) && !/(^|\s)--run(\s|$)/i.test(scriptBody) ? ['--', '--run'] : []; await executeStep({ name: `unit tests (${scriptName})`, command: 'npm', args: ['run', scriptName, ...extraArgs], timeoutMs: readIntegerOption('unit-timeout-ms', 10 * 60 * 1000), }); return; } if (await pathExists(path.join(root, 'vitest.config.ts'))) { await executeStep({ name: 'unit tests (npx vitest run)', command: 'npx', args: ['vitest', 'run'], timeoutMs: readIntegerOption('unit-timeout-ms', 10 * 60 * 1000), }); return; } applyStopPolicy( recordSyntheticStep({ name: 'unit tests', status: 'failed', details: 'No Vitest script or vitest.config.ts was found.', }), ); } async function runPlaywrightValidation(scripts) { if (readFlag('skip-e2e')) { recordSkip('playwright e2e tests', 'Skipped because --skip-e2e was provided.'); return; } const hasPlaywrightConfig = (await pathExists(path.join(root, 'playwright.config.ts'))) || (await pathExists(path.join(root, 'playwright.config.js'))) || (await pathExists(path.join(root, 'playwright.config.mjs'))); if (readFlag('install-playwright')) { await executeStep({ name: 'install Playwright browsers', command: 'npx', args: ['playwright', 'install', '--with-deps'], timeoutMs: readIntegerOption('playwright-install-timeout-ms', 20 * 60 * 1000), }); if (stopped) return; } const scriptName = findScript(scripts, ['test:e2e', 'e2e', 'playwright']); if (scriptName) { await executeStep({ name: `playwright e2e tests (${scriptName})`, command: 'npm', args: ['run', scriptName], timeoutMs: e2eTimeoutMs, }); return; } if (hasPlaywrightConfig) { await executeStep({ name: 'playwright e2e tests (npx playwright test)', command: 'npx', args: ['playwright', 'test'], timeoutMs: e2eTimeoutMs, }); return; } applyStopPolicy( recordSyntheticStep({ name: 'playwright e2e tests', status: 'failed', details: 'No Playwright script or playwright.config file was found.', }), ); } async function runScriptCandidate({ name, candidates, scripts, timeoutMs, required }) { const scriptName = findScript(scripts, candidates); if (!scriptName) { if (required) { applyStopPolicy( recordSyntheticStep({ name, status: 'failed', details: `No matching npm script was found. Expected one of: ${candidates.join(', ')}.`, }), ); } else { recordSkip(name, `No optional npm script found. Checked: ${candidates.join(', ')}.`); } return; } await executeStep({ name: `${name} (${scriptName})`, command: 'npm', args: ['run', scriptName], timeoutMs, }); } function findScript(scripts, candidates) { return candidates.find((candidate) => typeof scripts[candidate] === 'string'); } async function executeStep(stepDefinition) { if (stopped) { return recordSkip(stepDefinition.name, 'Skipped because a previous required validation step failed.'); } const step = await runStep(stepDefinition); applyStopPolicy(step); return step; } function runStep({ name, command, args = [], timeoutMs = defaultTimeoutMs }) { const startedAt = new Date(); const formattedCommand = formatCommand(command, args); console.log(`\n▶ ${name}`); console.log(`$ ${formattedCommand}\n`); let stdoutTail = ''; let stderrTail = ''; let timedOut = false; let settled = false; return new Promise((resolve) => { const child = spawn(toExecutable(command), args, { cwd: root, env: { ...process.env, CI: process.env.CI ?? 'true', NODE_ENV: process.env.NODE_ENV, }, stdio: ['ignore', 'pipe', 'pipe'], shell: false, }); const timeout = setTimeout(() => { timedOut = true; stderrTail = appendTail(stderrTail, `\nCommand timed out after ${timeoutMs}ms.\n`); child.kill('SIGTERM'); const killTimer = setTimeout(() => { child.kill('SIGKILL'); }, 10_000); if (typeof killTimer.unref === 'function') killTimer.unref(); }, timeoutMs); child.stdout.on('data', (chunk) => { const text = chunk.toString(); process.stdout.write(text); stdoutTail = appendTail(stdoutTail, text); }); child.stderr.on('data', (chunk) => { const text = chunk.toString(); process.stderr.write(text); stderrTail = appendTail(stderrTail, text); }); child.on('error', (error) => { finish({ status: 'failed', exitCode: null, signal: null, error: error.message, }); }); child.on('close', (exitCode, signal) => { finish({ status: exitCode === 0 && !timedOut ? 'passed' : 'failed', exitCode, signal, error: timedOut ? `Command timed out after ${timeoutMs}ms.` : null, }); }); function finish({ status, exitCode, signal, error }) { if (settled) return; settled = true; clearTimeout(timeout); const finishedAt = new Date(); const durationMs = finishedAt.getTime() - startedAt.getTime(); const step = { name, command: formattedCommand, status, startedAt: startedAt.toISOString(), finishedAt: finishedAt.toISOString(), durationMs, exitCode, signal, timedOut, stdoutTail: stdoutTail.trimEnd(), stderrTail: stderrTail.trimEnd(), error, }; report.steps.push(step); const icon = status === 'passed' ? '✓' : '✗'; console.log(`\n${icon} ${name} ${status} in ${formatDuration(durationMs)}\n`); resolve(step); } }); } async function collectAndRecordDistMetrics() { const distDir = path.join(root, readOption('dist-dir', 'dist')); if (!(await pathExists(distDir))) { applyStopPolicy( recordSyntheticStep({ name: 'dist asset metrics', status: 'failed', details: `Production build completed but ${path.relative(root, distDir)} was not found.`, }), ); return; } const metrics = await collectDistMetrics(distDir); report.distMetrics = metrics; const enforceBudgets = readFlag('enforce-budgets'); const status = enforceBudgets && metrics.warnings.length > 0 ? 'failed' : 'passed'; const details = [ `Files: ${metrics.fileCount}`, `Total: ${humanBytes(metrics.totalBytes)} raw / ${humanBytes(metrics.totalGzipBytes)} gzip-estimated`, `JS: ${humanBytes(metrics.totalJsBytes)} raw / ${humanBytes(metrics.totalJsGzipBytes)} gzip-estimated`, `CSS: ${humanBytes(metrics.totalCssBytes)} raw / ${humanBytes(metrics.totalCssGzipBytes)} gzip-estimated`, metrics.warnings.length > 0 ? `Budget warnings:\n${metrics.warnings.map((warning) => `- ${warning}`).join('\n')}` : 'No asset budget warnings.', ].join('\n'); printDistMetrics(metrics); applyStopPolicy( recordSyntheticStep({ name: 'dist asset metrics', status, details, }), ); } async function collectDistMetrics(distDir) { const files = await walkFiles(distDir); const gzipExtensions = new Set([ '.css', '.html', '.js', '.json', '.mjs', '.svg', '.txt', '.wasm', '.webmanifest', '.xml', ]); const budget = { totalJsGzipBytes: parseByteSize(readOption('budget-total-js-gzip'), 900 * 1024), largestJsGzipBytes: parseByteSize(readOption('budget-largest-js-gzip'), 450 * 1024), totalCssGzipBytes: parseByteSize(readOption('budget-total-css-gzip'), 120 * 1024), largestAssetBytes: parseByteSize(readOption('budget-largest-asset'), 2 * 1024 * 1024), }; const entries = []; for (const absolutePath of files) { const buffer = await fs.readFile(absolutePath); const relativePath = toPosixPath(path.relative(distDir, absolutePath)); const extension = path.extname(absolutePath).toLowerCase(); const gzipBytes = gzipExtensions.has(extension) ? gzipSync(buffer, { level: 9 }).length : null; entries.push({ path: relativePath, extension, bytes: buffer.length, gzipBytes, }); } const metrics = { distDir: toPosixPath(path.relative(root, distDir)), fileCount: entries.length, totalBytes: sum(entries.map((entry) => entry.bytes)), totalGzipBytes: sum(entries.map((entry) => entry.gzipBytes ?? entry.bytes)), totalJsBytes: sum(entries.filter((entry) => isJavaScriptAsset(entry.path)).map((entry) => entry.bytes)), totalJsGzipBytes: sum( entries.filter((entry) => isJavaScriptAsset(entry.path)).map((entry) => entry.gzipBytes ?? entry.bytes), ), totalCssBytes: sum(entries.filter((entry) => entry.extension === '.css').map((entry) => entry.bytes)), totalCssGzipBytes: sum(entries.filter((entry) => entry.extension === '.css').map((entry) => entry.gzipBytes ?? entry.bytes)), largestFiles: [...entries].sort((a, b) => b.bytes - a.bytes).slice(0, 15), largestGzipFiles: [...entries] .sort((a, b) => (b.gzipBytes ?? b.bytes) - (a.gzipBytes ?? a.bytes)) .slice(0, 15), budgets: budget, warnings: [], }; if (metrics.totalJsGzipBytes > budget.totalJsGzipBytes) { metrics.warnings.push( `Total JavaScript gzip estimate is ${humanBytes(metrics.totalJsGzipBytes)}, above the ${humanBytes( budget.totalJsGzipBytes, )} review budget. Confirm viewer/catalogue route splitting and lazy machine loading.`, ); } const largestJs = entries .filter((entry) => isJavaScriptAsset(entry.path)) .sort((a, b) => (b.gzipBytes ?? b.bytes) - (a.gzipBytes ?? a.bytes))[0]; if (largestJs && (largestJs.gzipBytes ?? largestJs.bytes) > budget.largestJsGzipBytes) { metrics.warnings.push( `Largest JavaScript asset ${largestJs.path} is ${humanBytes( largestJs.gzipBytes ?? largestJs.bytes, )} gzip-estimated, above the ${humanBytes(budget.largestJsGzipBytes)} per-chunk review budget.`, ); } if (metrics.totalCssGzipBytes > budget.totalCssGzipBytes) { metrics.warnings.push( `Total CSS gzip estimate is ${humanBytes(metrics.totalCssGzipBytes)}, above the ${humanBytes( budget.totalCssGzipBytes, )} review budget.`, ); } for (const entry of entries) { if (entry.bytes > budget.largestAssetBytes && !entry.path.endsWith('.map')) { metrics.warnings.push( `Large asset ${entry.path} is ${humanBytes(entry.bytes)} raw, above the ${humanBytes( budget.largestAssetBytes, )} review budget. Confirm compression, cache headers, and lazy loading.`, ); } } return metrics; } function printDistMetrics(metrics) { console.log('\nProduction asset metrics'); console.log(` dist directory: ${metrics.distDir}`); console.log(` files: ${metrics.fileCount}`); console.log(` total raw: ${humanBytes(metrics.totalBytes)}`); console.log(` total gzip est: ${humanBytes(metrics.totalGzipBytes)}`); console.log(` JS gzip est: ${humanBytes(metrics.totalJsGzipBytes)}`); console.log(` CSS gzip est: ${humanBytes(metrics.totalCssGzipBytes)}`); console.log('\nLargest dist files'); for (const entry of metrics.largestFiles.slice(0, 8)) { const gzipLabel = entry.gzipBytes == null ? 'not gzip-estimated' : `${humanBytes(entry.gzipBytes)} gzip`; console.log(` ${entry.path.padEnd(56)} ${humanBytes(entry.bytes).padStart(10)} raw ${gzipLabel}`); } if (metrics.warnings.length > 0) { console.log('\nAsset budget review warnings'); for (const warning of metrics.warnings) { console.log(` - ${warning}`); } } console.log(''); } async function walkFiles(directory) { const entries = await fs.readdir(directory, { withFileTypes: true }); const files = []; for (const entry of entries) { const absolutePath = path.join(directory, entry.name); if (entry.isDirectory()) { files.push(...(await walkFiles(absolutePath))); } else if (entry.isFile()) { files.push(absolutePath); } } return files; } function recordSyntheticStep({ name, status, details }) { const timestamp = new Date().toISOString(); const step = { name, command: null, status, startedAt: timestamp, finishedAt: timestamp, durationMs: 0, exitCode: status === 'passed' || status === 'skipped' ? 0 : 1, signal: null, timedOut: false, stdoutTail: '', stderrTail: status === 'failed' ? details : '', error: status === 'failed' ? details : null, details, }; report.steps.push(step); const icon = status === 'passed' ? '✓' : status === 'skipped' ? '○' : '✗'; console.log(`\n${icon} ${name}`); if (details) console.log(details); return step; } function recordSkip(name, reason) { return recordSyntheticStep({ name, status: 'skipped', details: reason, }); } function applyStopPolicy(step) { if (step.status === 'failed' && !continueOnError) { stopped = true; } } async function finalizeReport() { report.finishedAt = new Date().toISOString(); report.summary = summarizeReport(); const summary = report.summary; console.log('\nValidation summary'); console.log(` passed: ${summary.passed}`); console.log(` failed: ${summary.failed}`); console.log(` skipped: ${summary.skipped}`); console.log(` elapsed: ${formatDuration(summary.durationMs)}`); if (summary.failed > 0) { console.log('\nFailed steps'); for (const step of report.steps.filter((candidate) => candidate.status === 'failed')) { console.log(` - ${step.name}${step.error ? `: ${firstLine(step.error)}` : ''}`); } } const jsonPath = readOption('json'); if (jsonPath) { const absolutePath = path.resolve(root, jsonPath); await fs.mkdir(path.dirname(absolutePath), { recursive: true }); await fs.writeFile(absolutePath, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); console.log(`\nWrote validation report to ${path.relative(root, absolutePath)}`); } } function summarizeReport() { const started = Date.parse(report.startedAt); const finished = Date.parse(report.finishedAt ?? new Date().toISOString()); return { passed: report.steps.filter((step) => step.status === 'passed').length, failed: report.steps.filter((step) => step.status === 'failed').length, skipped: report.steps.filter((step) => step.status === 'skipped').length, durationMs: Math.max(0, finished - started), }; } function exitCodeFromReport() { return report.steps.some((step) => step.status === 'failed') ? 1 : 0; } async function readJson(filePath) { return JSON.parse(await fs.readFile(filePath, 'utf8')); } async function pathExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } function parseArgs(args) { const parsed = { _: [] }; for (let index = 0; index < args.length; index += 1) { const token = args[index]; if (!token.startsWith('--')) { parsed._.push(token); continue; } const equalsIndex = token.indexOf('='); if (equalsIndex !== -1) { parsed[token.slice(2, equalsIndex)] = token.slice(equalsIndex + 1); continue; } const key = token.slice(2); const next = args[index + 1]; if (next && !next.startsWith('--')) { parsed[key] = next; index += 1; } else { parsed[key] = true; } } return parsed; } function readFlag(name) { return cli[name] === true || cli[name] === 'true' || cli[name] === '1'; } function readOption(name, fallback = undefined) { const value = cli[name]; return value == null || value === true ? fallback : String(value); } function readIntegerOption(name, fallback) { const value = readOption(name); if (value == null) return fallback; const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } function parseByteSize(value, fallback) { if (value == null || value === '') return fallback; const match = String(value) .trim() .toLowerCase() .match(/^(\d+(?:\.\d+)?)\s*(b|kb|kib|mb|mib)?$/); if (!match) return fallback; const amount = Number.parseFloat(match[1]); const unit = match[2] ?? 'b'; const multiplier = unit === 'mb' || unit === 'mib' ? 1024 * 1024 : unit === 'kb' || unit === 'kib' ? 1024 : 1; return Math.round(amount * multiplier); } function toExecutable(command) { return process.platform === 'win32' ? `${command}.cmd` : command; } function formatCommand(command, args) { return [command, ...args].map(shellQuote).join(' '); } function shellQuote(value) { const text = String(value); if (/^[a-zA-Z0-9_./:=@+-]+$/.test(text)) return text; return JSON.stringify(text); } function appendTail(current, addition, maxLength = 80_000) { const combined = current + addition; return combined.length > maxLength ? combined.slice(combined.length - maxLength) : combined; } function humanBytes(bytes) { if (!Number.isFinite(bytes)) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; let value = bytes; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex += 1; } return `${unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`; } function formatDuration(ms) { if (ms < 1000) return `${ms}ms`; const seconds = ms / 1000; if (seconds < 60) return `${seconds.toFixed(1)}s`; const minutes = Math.floor(seconds / 60); const remainder = Math.round(seconds % 60); return `${minutes}m ${remainder}s`; } function firstLine(value) { return String(value).split('\n').find(Boolean) ?? String(value); } function toPosixPath(value) { return value.split(path.sep).join('/'); } function isJavaScriptAsset(filePath) { return /\.(js|mjs)$/i.test(filePath) && !filePath.endsWith('.map'); } function sum(values) { return values.reduce((total, value) => total + value, 0); } function printHelp() { console.log(` Mechanica clean-checkout validation harness Usage: node scripts/validate-clean-checkout.mjs [options] Primary options: --skip-install Do not run npm install. --frozen Use npm ci instead of npm install; requires package-lock.json. --install-playwright Run npx playwright install --with-deps before e2e tests. --skip-e2e Skip Playwright validation. --continue-on-error Run all possible steps even after a failure. --json Write a machine-readable validation report. --root Repository root. Defaults to the current working directory. Timeout options: --timeout-ms Default command timeout. Default: 600000. --install-timeout-ms npm install timeout. Default: 720000. --unit-timeout-ms Vitest timeout. Default: 600000. --build-timeout-ms Production build timeout. Default: 900000. --e2e-timeout-ms Playwright timeout. Default: 1500000. Asset budget options: --enforce-budgets Fail if dist asset review budgets are exceeded. --budget-total-js-gzip Default: 900kb. --budget-largest-js-gzip Default: 450kb. --budget-total-css-gzip Default: 120kb. --budget-largest-asset Default: 2mb. Examples: node scripts/validate-clean-checkout.mjs --install-playwright --json artifacts/validation-report.json node scripts/validate-clean-checkout.mjs --skip-install --skip-e2e node scripts/validate-clean-checkout.mjs --continue-on-error --enforce-budgets `.trim()); }