#!/usr/bin/env bun /** * Lighthouse against a URL, for an agent's shell. * * The same engine the panel uses — api/lighthouse.ts — with a table instead of JSON. It exists * because the consumer is usually a terminal: somebody has just built the site and wants to know * whether the thing they changed moved the number, and the honest answer takes twenty seconds rather * than a round trip through a person with DevTools open. * * bun bin/lighthouse.ts both form factors * bun bin/lighthouse.ts --mobile one of them * bun bin/lighthouse.ts --json the whole result, for a script * * Run it from anywhere: bun resolves node_modules from this file upward, which reaches * plugins/node_modules where the plugin's dependencies live. */ import { audit, type FormFactor } from '../api/lighthouse'; const args = process.argv.slice(2); const url = args.find((one) => !one.startsWith('-')); const json = args.includes('--json'); const quiet = json || args.includes('--quiet'); if (!url) { console.error('usage: bun bin/lighthouse.ts [--mobile|--desktop] [--json]'); process.exit(2); } const factors: FormFactor[] = args.includes('--mobile') ? ['mobile'] : args.includes('--desktop') ? ['desktop'] : ['mobile', 'desktop']; /** Lighthouse's own bands, so a number reads as good or not without looking them up. */ const band = (score: number | null) => (score === null ? ' ? ' : score >= 90 ? ' ✓ ' : score >= 50 ? ' ~ ' : ' ✗ '); const results = []; for (const formFactor of factors) { // To stderr, so `--json` on stdout stays a clean pipe while the wait is still narrated. results.push(await audit(url, formFactor, (line) => !quiet && console.error(` · ${line}`))); } if (json) { console.log(JSON.stringify(results, null, 2)); process.exit(0); } for (const result of results) { console.log(`\n═══ ${result.formFactor.toUpperCase()} ${result.url}`); console.log( ' ' + Object.entries(result.scores) .map(([name, score]) => `${band(score)}${name} ${score ?? '—'}`) .join(' '), ); console.log(); for (const metric of result.metrics) console.log(` ${metric.label.padEnd(26)} ${metric.display}`); if (!result.findings.length) { console.log('\n nothing failing'); continue; } console.log(); for (const finding of result.findings) { const score = finding.score === null ? ' —' : String(Math.round(finding.score * 100)).padStart(3); console.log(` [${score}] ${finding.title}${finding.savings ? ` — ${finding.savings}` : ''}`); // The resources, which is the difference between a finding and a fix. for (const item of finding.items) console.log(` ${item.url || '(no url)'} ${item.detail}`); } } // A non-zero exit when anything is short of 100, so this can gate a build without being parsed. const perfect = results.every((one) => Object.values(one.scores).every((score) => score === 100)); process.exit(perfect ? 0 : 1);