The engine was there and nothing pointed at it, which is the same failure as a comment outliving its code: an agent would go on asking the owner to open DevTools and read numbers back, because that is the only loop it knew. bin/lighthouse.ts is the callable surface — the panel's engine with a table instead of JSON, exiting non-zero when anything is short of 100 so it can gate something without being parsed. affordances/ carries the short always-loaded note: you can measure this yourself, here is the line, do not ask a person. skills/ carries what the findings mean, why the built site and the dev server are different questions, and the three defects this has already caught — each one a rename or a setting that left something behind and produced no error. Both are read in place from the plugin directory by capabilitySources, so they arrive with the plugin and need no publish step. The invocation globs ~/officer*/platform because the checkout is officer-dev on one machine and officerdev on the other, and a doc that is right on one box and wrong on the other is worse than no doc. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
79 lines
3.0 KiB
TypeScript
79 lines
3.0 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. */
|
|
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);
|