`formFactor: 'desktop'` changes which thresholds a metric is scored against and NOTHING else. Throttling and the user agent keep their defaults, which are mobile — so every desktop run was a desktop-sized window on a 4x-slowed CPU over slow 4G, announced as a moto-g. It read 86 where the same page reads 100. That gap was reported repeatedly as network latency to the host. It was not latency; it was this. Measured after the fix: desktop 100, LCP 0.4s against 2.2s. Nine more from the same review: runtimeError was never checked, so a page that failed to load reported "nothing failing" — the most reassuring possible output for a site that is down --json exited 0 before the gate, so the documented non-zero exit was true for a person reading the table and false for the only caller that could act on it scoreDisplayMode 'error' was filtered out while still nulling its category, so a category could read 0 with an empty findings list a missing metric serialised as 0, which a script reads as a perfect measurement `source` is a string on some audits and an object on others; String() on the object printed [object Object] the watchdog's surrender was permanent — the health branch sat below the check, so once it stopped trying it also stopped noticing, and a server fixed by hand left it asleep until the platform restarted a failed restart leaves no process at all, because startServer deletes before it starts. status null then reads as "somebody stopped it", which the watchdog leaves alone — one bad restart would have turned it off and taken the server with it, silently the skill claimed the panel uses this engine. Nothing but the CLI does and gave `bun run build:site` with no cwd, which only works from the website checkout — api/static-site.ts documents that and the skill did not Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
96 lines
3.8 KiB
TypeScript
96 lines
3.8 KiB
TypeScript
#!/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 <url> both form factors
|
|
* bun bin/lighthouse.ts <url> --mobile one of them
|
|
* bun bin/lighthouse.ts <url> --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 <url> [--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.
|
|
*
|
|
* `✓` is 90 and up, which is what Lighthouse calls green — NOT the same as the exit gate below, which
|
|
* is 100. The two disagreeing is deliberate and worth saying out loud: green is Lighthouse's word for
|
|
* good, and this tool's exit code answers a different question, which is whether there is anything
|
|
* left to do.
|
|
*/
|
|
const band = (score: number | null) => (score === null ? ' ? ' : score >= 90 ? ' ✓ ' : score >= 50 ? ' ~ ' : ' ✗ ');
|
|
|
|
/** Every category on every form factor at 100 — see the note on `band`. */
|
|
const perfect = (results: { scores: Record<string, number | null> }[]) =>
|
|
results.every((one) => Object.values(one.scores).every((score) => score === 100));
|
|
|
|
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));
|
|
/*
|
|
The same gate as below, not `exit(0)`.
|
|
|
|
`--json` is the mode a script uses, and a script is exactly the caller that reads an exit code.
|
|
Exiting 0 here meant the documented "non-zero when anything is short of 100" was true for a person
|
|
watching the table and false for the only consumer that could act on it.
|
|
*/
|
|
process.exit(perfect(results) ? 0 : 1);
|
|
}
|
|
|
|
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.
|
|
process.exit(perfect(results) ? 0 : 1);
|