@reddoorla/maintenance 0.14.0 → 0.16.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 +255 -118
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +144 -50
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.js +37 -16
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
|
@@ -8,6 +8,20 @@ var __export = (target, all) => {
|
|
|
8
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// src/util/credentials.ts
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { join as join6 } from "path";
|
|
15
|
+
function defaultCredentialsPath() {
|
|
16
|
+
const base = process.env.XDG_CONFIG_HOME ?? join6(homedir(), ".config");
|
|
17
|
+
return join6(base, "reddoor-maint", "credentials.env");
|
|
18
|
+
}
|
|
19
|
+
var init_credentials = __esm({
|
|
20
|
+
"src/util/credentials.ts"() {
|
|
21
|
+
"use strict";
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
11
25
|
// src/reports/airtable/client.ts
|
|
12
26
|
var client_exports = {};
|
|
13
27
|
__export(client_exports, {
|
|
@@ -15,11 +29,19 @@ __export(client_exports, {
|
|
|
15
29
|
readAirtableConfig: () => readAirtableConfig
|
|
16
30
|
});
|
|
17
31
|
import Airtable from "airtable";
|
|
32
|
+
function missing(name) {
|
|
33
|
+
return Object.assign(
|
|
34
|
+
new Error(
|
|
35
|
+
`${name} not set. Export it in your shell or put it in ${defaultCredentialsPath()} as ${name}=...`
|
|
36
|
+
),
|
|
37
|
+
{ exitCode: 2 }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
18
40
|
function readAirtableConfig() {
|
|
19
41
|
const apiKey = process.env.AIRTABLE_PAT;
|
|
20
42
|
const baseId = process.env.AIRTABLE_BASE_ID;
|
|
21
|
-
if (!apiKey) throw
|
|
22
|
-
if (!baseId) throw
|
|
43
|
+
if (!apiKey) throw missing("AIRTABLE_PAT");
|
|
44
|
+
if (!baseId) throw missing("AIRTABLE_BASE_ID");
|
|
23
45
|
return { apiKey, baseId };
|
|
24
46
|
}
|
|
25
47
|
function openBase(cfg) {
|
|
@@ -28,6 +50,7 @@ function openBase(cfg) {
|
|
|
28
50
|
var init_client = __esm({
|
|
29
51
|
"src/reports/airtable/client.ts"() {
|
|
30
52
|
"use strict";
|
|
53
|
+
init_credentials();
|
|
31
54
|
}
|
|
32
55
|
});
|
|
33
56
|
|
|
@@ -178,7 +201,7 @@ __export(lighthouse_airtable_exports, {
|
|
|
178
201
|
resolveSlugFromCwd: () => resolveSlugFromCwd
|
|
179
202
|
});
|
|
180
203
|
import { readFile as readFile7 } from "fs/promises";
|
|
181
|
-
import { join as
|
|
204
|
+
import { join as join8 } from "path";
|
|
182
205
|
function hasRealScores(result) {
|
|
183
206
|
if (result.audit !== "lighthouse") return false;
|
|
184
207
|
const details = result.details ?? {};
|
|
@@ -203,7 +226,7 @@ function lighthouseScoresFromResult(result) {
|
|
|
203
226
|
}
|
|
204
227
|
async function resolveSlugFromCwd(cwd) {
|
|
205
228
|
try {
|
|
206
|
-
const pkgPath =
|
|
229
|
+
const pkgPath = join8(cwd, "package.json");
|
|
207
230
|
const raw = await readFile7(pkgPath, "utf-8");
|
|
208
231
|
const pkg = JSON.parse(raw);
|
|
209
232
|
if (!pkg.name) throw new Error("package.json has no 'name' field");
|
|
@@ -348,6 +371,7 @@ var init_write_audits_to_airtable = __esm({
|
|
|
348
371
|
|
|
349
372
|
// src/cli/commands/audit.ts
|
|
350
373
|
import { resolve as resolve2 } from "path";
|
|
374
|
+
import { Listr } from "listr2";
|
|
351
375
|
|
|
352
376
|
// src/audits/util/spawn.ts
|
|
353
377
|
import { spawn } from "child_process";
|
|
@@ -1104,29 +1128,20 @@ var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
|
|
|
1104
1128
|
function timedSpawn(timeoutMs) {
|
|
1105
1129
|
return (cmd, args, opts = {}) => defaultSpawn(cmd, args, { ...opts, timeoutMs: opts.timeoutMs ?? timeoutMs });
|
|
1106
1130
|
}
|
|
1107
|
-
async function
|
|
1108
|
-
|
|
1109
|
-
for (const n of names) {
|
|
1110
|
-
if (!(n in REGISTRY)) throw new Error(`unknown audit: ${n}`);
|
|
1111
|
-
}
|
|
1131
|
+
async function runOneAudit(site, name) {
|
|
1132
|
+
if (!(name in REGISTRY)) throw new Error(`unknown audit: ${name}`);
|
|
1112
1133
|
const spawn2 = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);
|
|
1113
1134
|
const label = site.name ?? site.path;
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
)
|
|
1125
|
-
);
|
|
1126
|
-
}
|
|
1127
|
-
async function runAuditsAcross(sites, which) {
|
|
1128
|
-
const all = await Promise.all(sites.map((s) => runAudits(s, which)));
|
|
1129
|
-
return all.flat();
|
|
1135
|
+
try {
|
|
1136
|
+
return await REGISTRY[name]({ site, spawn: spawn2 });
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
return {
|
|
1139
|
+
audit: name,
|
|
1140
|
+
site: label,
|
|
1141
|
+
status: "fail",
|
|
1142
|
+
summary: `${name}: unexpected error \u2014 ${String(err)}`
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1130
1145
|
}
|
|
1131
1146
|
|
|
1132
1147
|
// src/cli/fleet/resolve-sites.ts
|
|
@@ -1217,7 +1232,7 @@ async function resolveSites(input) {
|
|
|
1217
1232
|
|
|
1218
1233
|
// src/cli/fleet/clone-if-needed.ts
|
|
1219
1234
|
import { stat, readdir as readdir2, mkdir } from "fs/promises";
|
|
1220
|
-
import { isAbsolute as isAbsolute2, join as
|
|
1235
|
+
import { isAbsolute as isAbsolute2, join as join7 } from "path";
|
|
1221
1236
|
function deriveNameFromRepoUrl(repoUrl) {
|
|
1222
1237
|
const slash = repoUrl.split("/").pop() ?? repoUrl;
|
|
1223
1238
|
return slash.replace(/\.git$/, "");
|
|
@@ -1258,7 +1273,7 @@ async function cloneIfNeeded(site, opts) {
|
|
|
1258
1273
|
const name = site.name ?? deriveNameFromRepoUrl(site.repoUrl);
|
|
1259
1274
|
assertSafeName(name);
|
|
1260
1275
|
assertSafeRepoUrl(site.repoUrl);
|
|
1261
|
-
const target =
|
|
1276
|
+
const target = join7(opts.workdir, name);
|
|
1262
1277
|
await mkdir(opts.workdir, { recursive: true });
|
|
1263
1278
|
if (await isNonEmptyDir(target)) {
|
|
1264
1279
|
return { ...site, name, path: target };
|
|
@@ -1292,8 +1307,87 @@ function formatTable(results) {
|
|
|
1292
1307
|
function exitCode(results) {
|
|
1293
1308
|
return results.some((r) => r.status === "fail") ? 1 : 0;
|
|
1294
1309
|
}
|
|
1310
|
+
function formatDuration(ms) {
|
|
1311
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
1312
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
1313
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
1314
|
+
const m = Math.floor(totalSeconds / 60);
|
|
1315
|
+
const s = totalSeconds % 60;
|
|
1316
|
+
return `${m}m${s.toString().padStart(2, "0")}s`;
|
|
1317
|
+
}
|
|
1318
|
+
function buildAuditTasks(sites, which, results, renderer) {
|
|
1319
|
+
const singleSite = sites.length === 1;
|
|
1320
|
+
if (singleSite) {
|
|
1321
|
+
const site = sites[0];
|
|
1322
|
+
return new Listr(
|
|
1323
|
+
which.map((name) => ({
|
|
1324
|
+
title: name,
|
|
1325
|
+
task: async (_ctx, task) => {
|
|
1326
|
+
const start = Date.now();
|
|
1327
|
+
const result = await runOneAudit(site, name);
|
|
1328
|
+
results.push(result);
|
|
1329
|
+
const elapsed = formatDuration(Date.now() - start);
|
|
1330
|
+
task.title = `${name}: ${result.summary} (${elapsed})`;
|
|
1331
|
+
if (result.status === "fail") throw new Error(result.summary);
|
|
1332
|
+
}
|
|
1333
|
+
})),
|
|
1334
|
+
{ concurrent: true, exitOnError: false, renderer }
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
return new Listr(
|
|
1338
|
+
sites.map((site) => {
|
|
1339
|
+
const label = site.name ?? site.path;
|
|
1340
|
+
return {
|
|
1341
|
+
title: label,
|
|
1342
|
+
task: async (_ctx, task) => {
|
|
1343
|
+
const start = Date.now();
|
|
1344
|
+
let done = 0;
|
|
1345
|
+
task.output = `0/${which.length} audits`;
|
|
1346
|
+
const settled = await Promise.all(
|
|
1347
|
+
which.map(async (name) => {
|
|
1348
|
+
const r = await runOneAudit(site, name);
|
|
1349
|
+
results.push(r);
|
|
1350
|
+
done += 1;
|
|
1351
|
+
task.output = `${done}/${which.length} audits`;
|
|
1352
|
+
return r;
|
|
1353
|
+
})
|
|
1354
|
+
);
|
|
1355
|
+
const elapsed = formatDuration(Date.now() - start);
|
|
1356
|
+
const failed = settled.filter((r) => r.status === "fail").length;
|
|
1357
|
+
const warned = settled.filter((r) => r.status === "warn").length;
|
|
1358
|
+
const note = failed > 0 ? `${failed} failed` : warned > 0 ? `${warned} warning${warned === 1 ? "" : "s"}` : "all green";
|
|
1359
|
+
task.title = `${label}: ${note} (${elapsed})`;
|
|
1360
|
+
if (failed > 0) throw new Error(`${label}: ${failed} audit(s) failed`);
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
}),
|
|
1364
|
+
{ concurrent: true, exitOnError: false, renderer }
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
function formatWriteSummary(summary) {
|
|
1368
|
+
const lines = summary.writes.map((w) => {
|
|
1369
|
+
if (w.audit === "lighthouse") {
|
|
1370
|
+
const s = w.counts;
|
|
1371
|
+
return ` lighthouse: P=${s.performance} A=${s.accessibility} BP=${s.bestPractices} SEO=${s.seo}`;
|
|
1372
|
+
}
|
|
1373
|
+
if (w.audit === "a11y") {
|
|
1374
|
+
return ` a11y: ${w.counts.violations} violations`;
|
|
1375
|
+
}
|
|
1376
|
+
if (w.audit === "deps") {
|
|
1377
|
+
const c2 = w.counts;
|
|
1378
|
+
return ` deps: ${c2.drifted} drifted (${c2.majorBehind} major)`;
|
|
1379
|
+
}
|
|
1380
|
+
const c = w.counts;
|
|
1381
|
+
return ` security: ${c.critical}C/${c.high}H/${c.moderate}M/${c.low}L`;
|
|
1382
|
+
});
|
|
1383
|
+
return `\u2192 wrote to Websites[${summary.siteName}]:
|
|
1384
|
+
${lines.join("\n")}`;
|
|
1385
|
+
}
|
|
1386
|
+
function rendererFor(json) {
|
|
1387
|
+
return json ? "silent" : "default";
|
|
1388
|
+
}
|
|
1295
1389
|
async function runAuditCommand(site, opts) {
|
|
1296
|
-
const which = parseOnly(opts.only);
|
|
1390
|
+
const which = parseOnly(opts.only) ?? ALL_AUDIT_NAMES;
|
|
1297
1391
|
const cwd = opts.cwd ? resolve2(opts.cwd) : process.cwd();
|
|
1298
1392
|
let sites = await resolveSites({
|
|
1299
1393
|
...site !== void 0 ? { site } : {},
|
|
@@ -1305,7 +1399,9 @@ async function runAuditCommand(site, opts) {
|
|
|
1305
1399
|
const workdir = opts.workdir ?? `${process.env.HOME ?? ""}/.reddoor-maint/sites`;
|
|
1306
1400
|
sites = await Promise.all(sites.map((s) => cloneIfNeeded(s, { workdir })));
|
|
1307
1401
|
}
|
|
1308
|
-
const results =
|
|
1402
|
+
const results = [];
|
|
1403
|
+
const renderer = rendererFor(opts.json);
|
|
1404
|
+
await buildAuditTasks(sites, which, results, renderer).run();
|
|
1309
1405
|
let output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);
|
|
1310
1406
|
if (opts.writeAirtable !== void 0) {
|
|
1311
1407
|
const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
@@ -1313,28 +1409,26 @@ async function runAuditCommand(site, opts) {
|
|
|
1313
1409
|
const { resolveSlugFromCwd: resolveSlugFromCwd2 } = await Promise.resolve().then(() => (init_lighthouse_airtable(), lighthouse_airtable_exports));
|
|
1314
1410
|
const { writeAuditsToAirtable: writeAuditsToAirtable2 } = await Promise.resolve().then(() => (init_write_audits_to_airtable(), write_audits_to_airtable_exports));
|
|
1315
1411
|
const slug = typeof opts.writeAirtable === "string" && opts.writeAirtable.length > 0 ? opts.writeAirtable : await resolveSlugFromCwd2(cwd);
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
output += `
|
|
1412
|
+
let writeSummary = null;
|
|
1413
|
+
await new Listr(
|
|
1414
|
+
[
|
|
1415
|
+
{
|
|
1416
|
+
title: `Write to Airtable[${slug}]`,
|
|
1417
|
+
task: async (_ctx, task) => {
|
|
1418
|
+
const base = openBase2(readAirtableConfig2());
|
|
1419
|
+
task.output = "loading Websites\u2026";
|
|
1420
|
+
const websites = await listWebsites2(base);
|
|
1421
|
+
task.output = "writing scores\u2026";
|
|
1422
|
+
writeSummary = await writeAuditsToAirtable2({ base, websites, slug, results });
|
|
1423
|
+
task.title = `Wrote to Websites[${writeSummary.siteName}] (${writeSummary.writes.length} audit type${writeSummary.writes.length === 1 ? "" : "s"})`;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
],
|
|
1427
|
+
{ renderer }
|
|
1428
|
+
).run();
|
|
1429
|
+
if (writeSummary) output += `
|
|
1335
1430
|
|
|
1336
|
-
|
|
1337
|
-
${lines.join("\n")}`;
|
|
1431
|
+
${formatWriteSummary(writeSummary)}`;
|
|
1338
1432
|
}
|
|
1339
1433
|
return { output, code: exitCode(results) };
|
|
1340
1434
|
}
|