@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.
@@ -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 Object.assign(new Error("AIRTABLE_PAT not set"), { exitCode: 2 });
22
- if (!baseId) throw Object.assign(new Error("AIRTABLE_BASE_ID not set"), { exitCode: 2 });
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 join7 } from "path";
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 = join7(cwd, "package.json");
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 runAudits(site, which) {
1108
- const names = which ?? ALL_AUDIT_NAMES;
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
- return Promise.all(
1115
- names.map(
1116
- (n) => REGISTRY[n]({ site, spawn: spawn2 }).catch(
1117
- (err) => ({
1118
- audit: n,
1119
- site: label,
1120
- status: "fail",
1121
- summary: `${n}: unexpected error \u2014 ${String(err)}`
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 join6 } from "path";
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 = join6(opts.workdir, name);
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 = await runAuditsAcross(sites, which);
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
- const base = openBase2(readAirtableConfig2());
1317
- const websites = await listWebsites2(base);
1318
- const summary = await writeAuditsToAirtable2({ base, websites, slug, results });
1319
- const lines = summary.writes.map((w) => {
1320
- if (w.audit === "lighthouse") {
1321
- const s = w.counts;
1322
- return ` lighthouse: P=${s.performance} A=${s.accessibility} BP=${s.bestPractices} SEO=${s.seo}`;
1323
- }
1324
- if (w.audit === "a11y") {
1325
- return ` a11y: ${w.counts.violations} violations`;
1326
- }
1327
- if (w.audit === "deps") {
1328
- const c2 = w.counts;
1329
- return ` deps: ${c2.drifted} drifted (${c2.majorBehind} major)`;
1330
- }
1331
- const c = w.counts;
1332
- return ` security: ${c.critical}C/${c.high}H/${c.moderate}M/${c.low}L`;
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
- \u2192 wrote to Websites[${summary.siteName}]:
1337
- ${lines.join("\n")}`;
1431
+ ${formatWriteSummary(writeSummary)}`;
1338
1432
  }
1339
1433
  return { output, code: exitCode(results) };
1340
1434
  }