@reddoorla/maintenance 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/bin.js +277 -50
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +273 -53
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.js +140 -63
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
|
@@ -39,7 +39,10 @@ __export(websites_exports, {
|
|
|
39
39
|
listWebsites: () => listWebsites,
|
|
40
40
|
mapRow: () => mapRow,
|
|
41
41
|
siteSlug: () => siteSlug,
|
|
42
|
-
|
|
42
|
+
updateA11yCounts: () => updateA11yCounts,
|
|
43
|
+
updateDepsCounts: () => updateDepsCounts,
|
|
44
|
+
updateScores: () => updateScores,
|
|
45
|
+
updateSecurityCounts: () => updateSecurityCounts
|
|
43
46
|
});
|
|
44
47
|
function siteSlug(name) {
|
|
45
48
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -67,6 +70,13 @@ function mapRow(rec) {
|
|
|
67
70
|
bpScore: f["bpScore"] ?? null,
|
|
68
71
|
seoScore: f["seoScore"] ?? null,
|
|
69
72
|
lastLighthouseAuditAt: f["Last lighthouse audit at"] ?? null,
|
|
73
|
+
a11yViolations: f["A11y Violations"] ?? null,
|
|
74
|
+
depsDrifted: f["Deps Drifted"] ?? null,
|
|
75
|
+
depsMajorBehind: f["Deps Major Behind"] ?? null,
|
|
76
|
+
securityVulnsCritical: f["Security Vulns Critical"] ?? null,
|
|
77
|
+
securityVulnsHigh: f["Security Vulns High"] ?? null,
|
|
78
|
+
securityVulnsModerate: f["Security Vulns Moderate"] ?? null,
|
|
79
|
+
securityVulnsLow: f["Security Vulns Low"] ?? null,
|
|
70
80
|
dashboardToken: (() => {
|
|
71
81
|
const raw = f["Dashboard Token"];
|
|
72
82
|
if (typeof raw !== "string") return null;
|
|
@@ -97,6 +107,28 @@ async function updateScores(base, recordId, scores) {
|
|
|
97
107
|
};
|
|
98
108
|
await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
|
|
99
109
|
}
|
|
110
|
+
async function updateA11yCounts(base, recordId, counts) {
|
|
111
|
+
const fields = {
|
|
112
|
+
"A11y Violations": counts.violations
|
|
113
|
+
};
|
|
114
|
+
await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
|
|
115
|
+
}
|
|
116
|
+
async function updateDepsCounts(base, recordId, counts) {
|
|
117
|
+
const fields = {
|
|
118
|
+
"Deps Drifted": counts.drifted,
|
|
119
|
+
"Deps Major Behind": counts.majorBehind
|
|
120
|
+
};
|
|
121
|
+
await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
|
|
122
|
+
}
|
|
123
|
+
async function updateSecurityCounts(base, recordId, counts) {
|
|
124
|
+
const fields = {
|
|
125
|
+
"Security Vulns Critical": counts.critical,
|
|
126
|
+
"Security Vulns High": counts.high,
|
|
127
|
+
"Security Vulns Moderate": counts.moderate,
|
|
128
|
+
"Security Vulns Low": counts.low
|
|
129
|
+
};
|
|
130
|
+
await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
|
|
131
|
+
}
|
|
100
132
|
var WEBSITES_TABLE;
|
|
101
133
|
var init_websites = __esm({
|
|
102
134
|
"src/reports/airtable/websites.ts"() {
|
|
@@ -192,8 +224,131 @@ var init_lighthouse_airtable = __esm({
|
|
|
192
224
|
}
|
|
193
225
|
});
|
|
194
226
|
|
|
227
|
+
// src/audits/a11y-airtable.ts
|
|
228
|
+
function hasA11yCounts(result) {
|
|
229
|
+
if (result.audit !== "a11y") return false;
|
|
230
|
+
const details = result.details;
|
|
231
|
+
return typeof details?.totalViolations === "number";
|
|
232
|
+
}
|
|
233
|
+
function a11yCountsFromResult(result) {
|
|
234
|
+
if (result.audit !== "a11y") {
|
|
235
|
+
throw new Error(`Expected an 'a11y' AuditResult, got '${result.audit}'`);
|
|
236
|
+
}
|
|
237
|
+
const details = result.details;
|
|
238
|
+
return { violations: details?.totalViolations ?? 0 };
|
|
239
|
+
}
|
|
240
|
+
var init_a11y_airtable = __esm({
|
|
241
|
+
"src/audits/a11y-airtable.ts"() {
|
|
242
|
+
"use strict";
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// src/audits/deps-airtable.ts
|
|
247
|
+
function hasDepsCounts(result) {
|
|
248
|
+
if (result.audit !== "deps") return false;
|
|
249
|
+
return Array.isArray(result.details);
|
|
250
|
+
}
|
|
251
|
+
function depsCountsFromResult(result) {
|
|
252
|
+
if (result.audit !== "deps") {
|
|
253
|
+
throw new Error(`Expected a 'deps' AuditResult, got '${result.audit}'`);
|
|
254
|
+
}
|
|
255
|
+
const entries = result.details ?? [];
|
|
256
|
+
const drifted = entries.filter((e) => e.drift !== "same").length;
|
|
257
|
+
const majorBehind = entries.filter((e) => e.drift === "major").length;
|
|
258
|
+
return { drifted, majorBehind };
|
|
259
|
+
}
|
|
260
|
+
var init_deps_airtable = __esm({
|
|
261
|
+
"src/audits/deps-airtable.ts"() {
|
|
262
|
+
"use strict";
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// src/audits/security-airtable.ts
|
|
267
|
+
function hasSecurityCounts(result) {
|
|
268
|
+
if (result.audit !== "security") return false;
|
|
269
|
+
const details = result.details;
|
|
270
|
+
return !!details && typeof details.counts === "object";
|
|
271
|
+
}
|
|
272
|
+
function securityCountsFromResult(result) {
|
|
273
|
+
if (result.audit !== "security") {
|
|
274
|
+
throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);
|
|
275
|
+
}
|
|
276
|
+
const details = result.details;
|
|
277
|
+
const c = details?.counts ?? { low: 0, moderate: 0, high: 0, critical: 0 };
|
|
278
|
+
return { critical: c.critical, high: c.high, moderate: c.moderate, low: c.low };
|
|
279
|
+
}
|
|
280
|
+
var init_security_airtable = __esm({
|
|
281
|
+
"src/audits/security-airtable.ts"() {
|
|
282
|
+
"use strict";
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// src/audits/write-audits-to-airtable.ts
|
|
287
|
+
var write_audits_to_airtable_exports = {};
|
|
288
|
+
__export(write_audits_to_airtable_exports, {
|
|
289
|
+
writeAuditsToAirtable: () => writeAuditsToAirtable
|
|
290
|
+
});
|
|
291
|
+
async function writeAuditsToAirtable(args) {
|
|
292
|
+
const { base, websites, slug, results } = args;
|
|
293
|
+
const lhResult = results.find((r) => r.audit === "lighthouse");
|
|
294
|
+
if (!lhResult) {
|
|
295
|
+
throw Object.assign(
|
|
296
|
+
new Error(
|
|
297
|
+
"--write-airtable requires a lighthouse result; did you pass --only without lighthouse?"
|
|
298
|
+
),
|
|
299
|
+
{ exitCode: 2 }
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (!hasRealScores(lhResult)) {
|
|
303
|
+
throw Object.assign(
|
|
304
|
+
new Error(
|
|
305
|
+
`Lighthouse audit produced no scores; refusing to write to Airtable. Summary: ${lhResult.summary}`
|
|
306
|
+
),
|
|
307
|
+
{ exitCode: 1 }
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
const target = websites.find((w) => siteSlug(w.name) === slug);
|
|
311
|
+
if (!target) {
|
|
312
|
+
throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
|
|
313
|
+
}
|
|
314
|
+
const writes = [];
|
|
315
|
+
const scores = lighthouseScoresFromResult(lhResult);
|
|
316
|
+
await updateScores(base, target.id, scores);
|
|
317
|
+
writes.push({ audit: "lighthouse", counts: scores });
|
|
318
|
+
const a11y = results.find((r) => r.audit === "a11y");
|
|
319
|
+
if (a11y && hasA11yCounts(a11y)) {
|
|
320
|
+
const counts = a11yCountsFromResult(a11y);
|
|
321
|
+
await updateA11yCounts(base, target.id, counts);
|
|
322
|
+
writes.push({ audit: "a11y", counts });
|
|
323
|
+
}
|
|
324
|
+
const deps = results.find((r) => r.audit === "deps");
|
|
325
|
+
if (deps && hasDepsCounts(deps)) {
|
|
326
|
+
const counts = depsCountsFromResult(deps);
|
|
327
|
+
await updateDepsCounts(base, target.id, counts);
|
|
328
|
+
writes.push({ audit: "deps", counts });
|
|
329
|
+
}
|
|
330
|
+
const sec = results.find((r) => r.audit === "security");
|
|
331
|
+
if (sec && hasSecurityCounts(sec)) {
|
|
332
|
+
const counts = securityCountsFromResult(sec);
|
|
333
|
+
await updateSecurityCounts(base, target.id, counts);
|
|
334
|
+
writes.push({ audit: "security", counts });
|
|
335
|
+
}
|
|
336
|
+
return { siteName: target.name, writes };
|
|
337
|
+
}
|
|
338
|
+
var init_write_audits_to_airtable = __esm({
|
|
339
|
+
"src/audits/write-audits-to-airtable.ts"() {
|
|
340
|
+
"use strict";
|
|
341
|
+
init_websites();
|
|
342
|
+
init_lighthouse_airtable();
|
|
343
|
+
init_a11y_airtable();
|
|
344
|
+
init_deps_airtable();
|
|
345
|
+
init_security_airtable();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
195
349
|
// src/cli/commands/audit.ts
|
|
196
350
|
import { resolve as resolve2 } from "path";
|
|
351
|
+
import { Listr } from "listr2";
|
|
197
352
|
|
|
198
353
|
// src/audits/util/spawn.ts
|
|
199
354
|
import { spawn } from "child_process";
|
|
@@ -950,29 +1105,20 @@ var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
|
|
|
950
1105
|
function timedSpawn(timeoutMs) {
|
|
951
1106
|
return (cmd, args, opts = {}) => defaultSpawn(cmd, args, { ...opts, timeoutMs: opts.timeoutMs ?? timeoutMs });
|
|
952
1107
|
}
|
|
953
|
-
async function
|
|
954
|
-
|
|
955
|
-
for (const n of names) {
|
|
956
|
-
if (!(n in REGISTRY)) throw new Error(`unknown audit: ${n}`);
|
|
957
|
-
}
|
|
1108
|
+
async function runOneAudit(site, name) {
|
|
1109
|
+
if (!(name in REGISTRY)) throw new Error(`unknown audit: ${name}`);
|
|
958
1110
|
const spawn2 = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);
|
|
959
1111
|
const label = site.name ?? site.path;
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
)
|
|
971
|
-
);
|
|
972
|
-
}
|
|
973
|
-
async function runAuditsAcross(sites, which) {
|
|
974
|
-
const all = await Promise.all(sites.map((s) => runAudits(s, which)));
|
|
975
|
-
return all.flat();
|
|
1112
|
+
try {
|
|
1113
|
+
return await REGISTRY[name]({ site, spawn: spawn2 });
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
return {
|
|
1116
|
+
audit: name,
|
|
1117
|
+
site: label,
|
|
1118
|
+
status: "fail",
|
|
1119
|
+
summary: `${name}: unexpected error \u2014 ${String(err)}`
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
976
1122
|
}
|
|
977
1123
|
|
|
978
1124
|
// src/cli/fleet/resolve-sites.ts
|
|
@@ -1138,8 +1284,87 @@ function formatTable(results) {
|
|
|
1138
1284
|
function exitCode(results) {
|
|
1139
1285
|
return results.some((r) => r.status === "fail") ? 1 : 0;
|
|
1140
1286
|
}
|
|
1287
|
+
function formatDuration(ms) {
|
|
1288
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
1289
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
1290
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
1291
|
+
const m = Math.floor(totalSeconds / 60);
|
|
1292
|
+
const s = totalSeconds % 60;
|
|
1293
|
+
return `${m}m${s.toString().padStart(2, "0")}s`;
|
|
1294
|
+
}
|
|
1295
|
+
function buildAuditTasks(sites, which, results, renderer) {
|
|
1296
|
+
const singleSite = sites.length === 1;
|
|
1297
|
+
if (singleSite) {
|
|
1298
|
+
const site = sites[0];
|
|
1299
|
+
return new Listr(
|
|
1300
|
+
which.map((name) => ({
|
|
1301
|
+
title: name,
|
|
1302
|
+
task: async (_ctx, task) => {
|
|
1303
|
+
const start = Date.now();
|
|
1304
|
+
const result = await runOneAudit(site, name);
|
|
1305
|
+
results.push(result);
|
|
1306
|
+
const elapsed = formatDuration(Date.now() - start);
|
|
1307
|
+
task.title = `${name}: ${result.summary} (${elapsed})`;
|
|
1308
|
+
if (result.status === "fail") throw new Error(result.summary);
|
|
1309
|
+
}
|
|
1310
|
+
})),
|
|
1311
|
+
{ concurrent: true, exitOnError: false, renderer }
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
return new Listr(
|
|
1315
|
+
sites.map((site) => {
|
|
1316
|
+
const label = site.name ?? site.path;
|
|
1317
|
+
return {
|
|
1318
|
+
title: label,
|
|
1319
|
+
task: async (_ctx, task) => {
|
|
1320
|
+
const start = Date.now();
|
|
1321
|
+
let done = 0;
|
|
1322
|
+
task.output = `0/${which.length} audits`;
|
|
1323
|
+
const settled = await Promise.all(
|
|
1324
|
+
which.map(async (name) => {
|
|
1325
|
+
const r = await runOneAudit(site, name);
|
|
1326
|
+
results.push(r);
|
|
1327
|
+
done += 1;
|
|
1328
|
+
task.output = `${done}/${which.length} audits`;
|
|
1329
|
+
return r;
|
|
1330
|
+
})
|
|
1331
|
+
);
|
|
1332
|
+
const elapsed = formatDuration(Date.now() - start);
|
|
1333
|
+
const failed = settled.filter((r) => r.status === "fail").length;
|
|
1334
|
+
const warned = settled.filter((r) => r.status === "warn").length;
|
|
1335
|
+
const note = failed > 0 ? `${failed} failed` : warned > 0 ? `${warned} warning${warned === 1 ? "" : "s"}` : "all green";
|
|
1336
|
+
task.title = `${label}: ${note} (${elapsed})`;
|
|
1337
|
+
if (failed > 0) throw new Error(`${label}: ${failed} audit(s) failed`);
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
}),
|
|
1341
|
+
{ concurrent: true, exitOnError: false, renderer }
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
function formatWriteSummary(summary) {
|
|
1345
|
+
const lines = summary.writes.map((w) => {
|
|
1346
|
+
if (w.audit === "lighthouse") {
|
|
1347
|
+
const s = w.counts;
|
|
1348
|
+
return ` lighthouse: P=${s.performance} A=${s.accessibility} BP=${s.bestPractices} SEO=${s.seo}`;
|
|
1349
|
+
}
|
|
1350
|
+
if (w.audit === "a11y") {
|
|
1351
|
+
return ` a11y: ${w.counts.violations} violations`;
|
|
1352
|
+
}
|
|
1353
|
+
if (w.audit === "deps") {
|
|
1354
|
+
const c2 = w.counts;
|
|
1355
|
+
return ` deps: ${c2.drifted} drifted (${c2.majorBehind} major)`;
|
|
1356
|
+
}
|
|
1357
|
+
const c = w.counts;
|
|
1358
|
+
return ` security: ${c.critical}C/${c.high}H/${c.moderate}M/${c.low}L`;
|
|
1359
|
+
});
|
|
1360
|
+
return `\u2192 wrote to Websites[${summary.siteName}]:
|
|
1361
|
+
${lines.join("\n")}`;
|
|
1362
|
+
}
|
|
1363
|
+
function rendererFor(json) {
|
|
1364
|
+
return json ? "silent" : "default";
|
|
1365
|
+
}
|
|
1141
1366
|
async function runAuditCommand(site, opts) {
|
|
1142
|
-
const which = parseOnly(opts.only);
|
|
1367
|
+
const which = parseOnly(opts.only) ?? ALL_AUDIT_NAMES;
|
|
1143
1368
|
const cwd = opts.cwd ? resolve2(opts.cwd) : process.cwd();
|
|
1144
1369
|
let sites = await resolveSites({
|
|
1145
1370
|
...site !== void 0 ? { site } : {},
|
|
@@ -1151,41 +1376,36 @@ async function runAuditCommand(site, opts) {
|
|
|
1151
1376
|
const workdir = opts.workdir ?? `${process.env.HOME ?? ""}/.reddoor-maint/sites`;
|
|
1152
1377
|
sites = await Promise.all(sites.map((s) => cloneIfNeeded(s, { workdir })));
|
|
1153
1378
|
}
|
|
1154
|
-
const results =
|
|
1379
|
+
const results = [];
|
|
1380
|
+
const renderer = rendererFor(opts.json);
|
|
1381
|
+
await buildAuditTasks(sites, which, results, renderer).run();
|
|
1155
1382
|
let output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);
|
|
1156
1383
|
if (opts.writeAirtable !== void 0) {
|
|
1157
1384
|
const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1158
|
-
const { listWebsites: listWebsites2
|
|
1159
|
-
const {
|
|
1385
|
+
const { listWebsites: listWebsites2 } = await Promise.resolve().then(() => (init_websites(), websites_exports));
|
|
1386
|
+
const { resolveSlugFromCwd: resolveSlugFromCwd2 } = await Promise.resolve().then(() => (init_lighthouse_airtable(), lighthouse_airtable_exports));
|
|
1387
|
+
const { writeAuditsToAirtable: writeAuditsToAirtable2 } = await Promise.resolve().then(() => (init_write_audits_to_airtable(), write_audits_to_airtable_exports));
|
|
1160
1388
|
const slug = typeof opts.writeAirtable === "string" && opts.writeAirtable.length > 0 ? opts.writeAirtable : await resolveSlugFromCwd2(cwd);
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
const websites = await listWebsites2(base);
|
|
1180
|
-
const target = websites.find((w) => siteSlug2(w.name) === slug);
|
|
1181
|
-
if (!target) {
|
|
1182
|
-
throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
|
|
1183
|
-
}
|
|
1184
|
-
const scores = lighthouseScoresFromResult2(lhResult);
|
|
1185
|
-
await updateScores2(base, target.id, scores);
|
|
1186
|
-
output += `
|
|
1389
|
+
let writeSummary = null;
|
|
1390
|
+
await new Listr(
|
|
1391
|
+
[
|
|
1392
|
+
{
|
|
1393
|
+
title: `Write to Airtable[${slug}]`,
|
|
1394
|
+
task: async (_ctx, task) => {
|
|
1395
|
+
const base = openBase2(readAirtableConfig2());
|
|
1396
|
+
task.output = "loading Websites\u2026";
|
|
1397
|
+
const websites = await listWebsites2(base);
|
|
1398
|
+
task.output = "writing scores\u2026";
|
|
1399
|
+
writeSummary = await writeAuditsToAirtable2({ base, websites, slug, results });
|
|
1400
|
+
task.title = `Wrote to Websites[${writeSummary.siteName}] (${writeSummary.writes.length} audit type${writeSummary.writes.length === 1 ? "" : "s"})`;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
],
|
|
1404
|
+
{ renderer }
|
|
1405
|
+
).run();
|
|
1406
|
+
if (writeSummary) output += `
|
|
1187
1407
|
|
|
1188
|
-
|
|
1408
|
+
${formatWriteSummary(writeSummary)}`;
|
|
1189
1409
|
}
|
|
1190
1410
|
return { output, code: exitCode(results) };
|
|
1191
1411
|
}
|