@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 CHANGED
@@ -40,7 +40,10 @@ __export(websites_exports, {
40
40
  listWebsites: () => listWebsites,
41
41
  mapRow: () => mapRow,
42
42
  siteSlug: () => siteSlug,
43
- updateScores: () => updateScores
43
+ updateA11yCounts: () => updateA11yCounts,
44
+ updateDepsCounts: () => updateDepsCounts,
45
+ updateScores: () => updateScores,
46
+ updateSecurityCounts: () => updateSecurityCounts
44
47
  });
45
48
  function siteSlug(name) {
46
49
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -68,6 +71,13 @@ function mapRow(rec) {
68
71
  bpScore: f["bpScore"] ?? null,
69
72
  seoScore: f["seoScore"] ?? null,
70
73
  lastLighthouseAuditAt: f["Last lighthouse audit at"] ?? null,
74
+ a11yViolations: f["A11y Violations"] ?? null,
75
+ depsDrifted: f["Deps Drifted"] ?? null,
76
+ depsMajorBehind: f["Deps Major Behind"] ?? null,
77
+ securityVulnsCritical: f["Security Vulns Critical"] ?? null,
78
+ securityVulnsHigh: f["Security Vulns High"] ?? null,
79
+ securityVulnsModerate: f["Security Vulns Moderate"] ?? null,
80
+ securityVulnsLow: f["Security Vulns Low"] ?? null,
71
81
  dashboardToken: (() => {
72
82
  const raw = f["Dashboard Token"];
73
83
  if (typeof raw !== "string") return null;
@@ -98,6 +108,28 @@ async function updateScores(base, recordId, scores) {
98
108
  };
99
109
  await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
100
110
  }
111
+ async function updateA11yCounts(base, recordId, counts) {
112
+ const fields = {
113
+ "A11y Violations": counts.violations
114
+ };
115
+ await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
116
+ }
117
+ async function updateDepsCounts(base, recordId, counts) {
118
+ const fields = {
119
+ "Deps Drifted": counts.drifted,
120
+ "Deps Major Behind": counts.majorBehind
121
+ };
122
+ await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
123
+ }
124
+ async function updateSecurityCounts(base, recordId, counts) {
125
+ const fields = {
126
+ "Security Vulns Critical": counts.critical,
127
+ "Security Vulns High": counts.high,
128
+ "Security Vulns Moderate": counts.moderate,
129
+ "Security Vulns Low": counts.low
130
+ };
131
+ await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
132
+ }
101
133
  var WEBSITES_TABLE;
102
134
  var init_websites = __esm({
103
135
  "src/reports/airtable/websites.ts"() {
@@ -193,6 +225,128 @@ var init_lighthouse_airtable = __esm({
193
225
  }
194
226
  });
195
227
 
228
+ // src/audits/a11y-airtable.ts
229
+ function hasA11yCounts(result) {
230
+ if (result.audit !== "a11y") return false;
231
+ const details = result.details;
232
+ return typeof details?.totalViolations === "number";
233
+ }
234
+ function a11yCountsFromResult(result) {
235
+ if (result.audit !== "a11y") {
236
+ throw new Error(`Expected an 'a11y' AuditResult, got '${result.audit}'`);
237
+ }
238
+ const details = result.details;
239
+ return { violations: details?.totalViolations ?? 0 };
240
+ }
241
+ var init_a11y_airtable = __esm({
242
+ "src/audits/a11y-airtable.ts"() {
243
+ "use strict";
244
+ }
245
+ });
246
+
247
+ // src/audits/deps-airtable.ts
248
+ function hasDepsCounts(result) {
249
+ if (result.audit !== "deps") return false;
250
+ return Array.isArray(result.details);
251
+ }
252
+ function depsCountsFromResult(result) {
253
+ if (result.audit !== "deps") {
254
+ throw new Error(`Expected a 'deps' AuditResult, got '${result.audit}'`);
255
+ }
256
+ const entries = result.details ?? [];
257
+ const drifted = entries.filter((e) => e.drift !== "same").length;
258
+ const majorBehind = entries.filter((e) => e.drift === "major").length;
259
+ return { drifted, majorBehind };
260
+ }
261
+ var init_deps_airtable = __esm({
262
+ "src/audits/deps-airtable.ts"() {
263
+ "use strict";
264
+ }
265
+ });
266
+
267
+ // src/audits/security-airtable.ts
268
+ function hasSecurityCounts(result) {
269
+ if (result.audit !== "security") return false;
270
+ const details = result.details;
271
+ return !!details && typeof details.counts === "object";
272
+ }
273
+ function securityCountsFromResult(result) {
274
+ if (result.audit !== "security") {
275
+ throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);
276
+ }
277
+ const details = result.details;
278
+ const c = details?.counts ?? { low: 0, moderate: 0, high: 0, critical: 0 };
279
+ return { critical: c.critical, high: c.high, moderate: c.moderate, low: c.low };
280
+ }
281
+ var init_security_airtable = __esm({
282
+ "src/audits/security-airtable.ts"() {
283
+ "use strict";
284
+ }
285
+ });
286
+
287
+ // src/audits/write-audits-to-airtable.ts
288
+ var write_audits_to_airtable_exports = {};
289
+ __export(write_audits_to_airtable_exports, {
290
+ writeAuditsToAirtable: () => writeAuditsToAirtable
291
+ });
292
+ async function writeAuditsToAirtable(args) {
293
+ const { base, websites, slug, results } = args;
294
+ const lhResult = results.find((r) => r.audit === "lighthouse");
295
+ if (!lhResult) {
296
+ throw Object.assign(
297
+ new Error(
298
+ "--write-airtable requires a lighthouse result; did you pass --only without lighthouse?"
299
+ ),
300
+ { exitCode: 2 }
301
+ );
302
+ }
303
+ if (!hasRealScores(lhResult)) {
304
+ throw Object.assign(
305
+ new Error(
306
+ `Lighthouse audit produced no scores; refusing to write to Airtable. Summary: ${lhResult.summary}`
307
+ ),
308
+ { exitCode: 1 }
309
+ );
310
+ }
311
+ const target = websites.find((w) => siteSlug(w.name) === slug);
312
+ if (!target) {
313
+ throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
314
+ }
315
+ const writes = [];
316
+ const scores = lighthouseScoresFromResult(lhResult);
317
+ await updateScores(base, target.id, scores);
318
+ writes.push({ audit: "lighthouse", counts: scores });
319
+ const a11y = results.find((r) => r.audit === "a11y");
320
+ if (a11y && hasA11yCounts(a11y)) {
321
+ const counts = a11yCountsFromResult(a11y);
322
+ await updateA11yCounts(base, target.id, counts);
323
+ writes.push({ audit: "a11y", counts });
324
+ }
325
+ const deps = results.find((r) => r.audit === "deps");
326
+ if (deps && hasDepsCounts(deps)) {
327
+ const counts = depsCountsFromResult(deps);
328
+ await updateDepsCounts(base, target.id, counts);
329
+ writes.push({ audit: "deps", counts });
330
+ }
331
+ const sec = results.find((r) => r.audit === "security");
332
+ if (sec && hasSecurityCounts(sec)) {
333
+ const counts = securityCountsFromResult(sec);
334
+ await updateSecurityCounts(base, target.id, counts);
335
+ writes.push({ audit: "security", counts });
336
+ }
337
+ return { siteName: target.name, writes };
338
+ }
339
+ var init_write_audits_to_airtable = __esm({
340
+ "src/audits/write-audits-to-airtable.ts"() {
341
+ "use strict";
342
+ init_websites();
343
+ init_lighthouse_airtable();
344
+ init_a11y_airtable();
345
+ init_deps_airtable();
346
+ init_security_airtable();
347
+ }
348
+ });
349
+
196
350
  // src/reports/airtable/reports.ts
197
351
  function mapRow2(rec) {
198
352
  const f = rec.fields;
@@ -784,6 +938,7 @@ import { cac } from "cac";
784
938
 
785
939
  // src/cli/commands/audit.ts
786
940
  import { resolve as resolve2 } from "path";
941
+ import { Listr } from "listr2";
787
942
 
788
943
  // src/audits/util/spawn.ts
789
944
  import { spawn } from "child_process";
@@ -1540,29 +1695,27 @@ var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
1540
1695
  function timedSpawn(timeoutMs) {
1541
1696
  return (cmd, args, opts = {}) => defaultSpawn(cmd, args, { ...opts, timeoutMs: opts.timeoutMs ?? timeoutMs });
1542
1697
  }
1698
+ async function runOneAudit(site, name) {
1699
+ if (!(name in REGISTRY)) throw new Error(`unknown audit: ${name}`);
1700
+ const spawn2 = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);
1701
+ const label = site.name ?? site.path;
1702
+ try {
1703
+ return await REGISTRY[name]({ site, spawn: spawn2 });
1704
+ } catch (err) {
1705
+ return {
1706
+ audit: name,
1707
+ site: label,
1708
+ status: "fail",
1709
+ summary: `${name}: unexpected error \u2014 ${String(err)}`
1710
+ };
1711
+ }
1712
+ }
1543
1713
  async function runAudits(site, which) {
1544
1714
  const names = which ?? ALL_AUDIT_NAMES;
1545
1715
  for (const n of names) {
1546
1716
  if (!(n in REGISTRY)) throw new Error(`unknown audit: ${n}`);
1547
1717
  }
1548
- const spawn2 = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);
1549
- const label = site.name ?? site.path;
1550
- return Promise.all(
1551
- names.map(
1552
- (n) => REGISTRY[n]({ site, spawn: spawn2 }).catch(
1553
- (err) => ({
1554
- audit: n,
1555
- site: label,
1556
- status: "fail",
1557
- summary: `${n}: unexpected error \u2014 ${String(err)}`
1558
- })
1559
- )
1560
- )
1561
- );
1562
- }
1563
- async function runAuditsAcross(sites, which) {
1564
- const all = await Promise.all(sites.map((s) => runAudits(s, which)));
1565
- return all.flat();
1718
+ return Promise.all(names.map((n) => runOneAudit(site, n)));
1566
1719
  }
1567
1720
 
1568
1721
  // src/cli/fleet/resolve-sites.ts
@@ -1728,8 +1881,87 @@ function formatTable(results) {
1728
1881
  function exitCode(results) {
1729
1882
  return results.some((r) => r.status === "fail") ? 1 : 0;
1730
1883
  }
1884
+ function formatDuration(ms) {
1885
+ if (ms < 1e3) return `${ms}ms`;
1886
+ const totalSeconds = Math.round(ms / 1e3);
1887
+ if (totalSeconds < 60) return `${totalSeconds}s`;
1888
+ const m = Math.floor(totalSeconds / 60);
1889
+ const s = totalSeconds % 60;
1890
+ return `${m}m${s.toString().padStart(2, "0")}s`;
1891
+ }
1892
+ function buildAuditTasks(sites, which, results, renderer) {
1893
+ const singleSite = sites.length === 1;
1894
+ if (singleSite) {
1895
+ const site = sites[0];
1896
+ return new Listr(
1897
+ which.map((name) => ({
1898
+ title: name,
1899
+ task: async (_ctx, task) => {
1900
+ const start = Date.now();
1901
+ const result = await runOneAudit(site, name);
1902
+ results.push(result);
1903
+ const elapsed = formatDuration(Date.now() - start);
1904
+ task.title = `${name}: ${result.summary} (${elapsed})`;
1905
+ if (result.status === "fail") throw new Error(result.summary);
1906
+ }
1907
+ })),
1908
+ { concurrent: true, exitOnError: false, renderer }
1909
+ );
1910
+ }
1911
+ return new Listr(
1912
+ sites.map((site) => {
1913
+ const label = site.name ?? site.path;
1914
+ return {
1915
+ title: label,
1916
+ task: async (_ctx, task) => {
1917
+ const start = Date.now();
1918
+ let done = 0;
1919
+ task.output = `0/${which.length} audits`;
1920
+ const settled = await Promise.all(
1921
+ which.map(async (name) => {
1922
+ const r = await runOneAudit(site, name);
1923
+ results.push(r);
1924
+ done += 1;
1925
+ task.output = `${done}/${which.length} audits`;
1926
+ return r;
1927
+ })
1928
+ );
1929
+ const elapsed = formatDuration(Date.now() - start);
1930
+ const failed = settled.filter((r) => r.status === "fail").length;
1931
+ const warned = settled.filter((r) => r.status === "warn").length;
1932
+ const note = failed > 0 ? `${failed} failed` : warned > 0 ? `${warned} warning${warned === 1 ? "" : "s"}` : "all green";
1933
+ task.title = `${label}: ${note} (${elapsed})`;
1934
+ if (failed > 0) throw new Error(`${label}: ${failed} audit(s) failed`);
1935
+ }
1936
+ };
1937
+ }),
1938
+ { concurrent: true, exitOnError: false, renderer }
1939
+ );
1940
+ }
1941
+ function formatWriteSummary(summary) {
1942
+ const lines = summary.writes.map((w) => {
1943
+ if (w.audit === "lighthouse") {
1944
+ const s = w.counts;
1945
+ return ` lighthouse: P=${s.performance} A=${s.accessibility} BP=${s.bestPractices} SEO=${s.seo}`;
1946
+ }
1947
+ if (w.audit === "a11y") {
1948
+ return ` a11y: ${w.counts.violations} violations`;
1949
+ }
1950
+ if (w.audit === "deps") {
1951
+ const c2 = w.counts;
1952
+ return ` deps: ${c2.drifted} drifted (${c2.majorBehind} major)`;
1953
+ }
1954
+ const c = w.counts;
1955
+ return ` security: ${c.critical}C/${c.high}H/${c.moderate}M/${c.low}L`;
1956
+ });
1957
+ return `\u2192 wrote to Websites[${summary.siteName}]:
1958
+ ${lines.join("\n")}`;
1959
+ }
1960
+ function rendererFor(json) {
1961
+ return json ? "silent" : "default";
1962
+ }
1731
1963
  async function runAuditCommand(site, opts) {
1732
- const which = parseOnly(opts.only);
1964
+ const which = parseOnly(opts.only) ?? ALL_AUDIT_NAMES;
1733
1965
  const cwd = opts.cwd ? resolve2(opts.cwd) : process.cwd();
1734
1966
  let sites = await resolveSites({
1735
1967
  ...site !== void 0 ? { site } : {},
@@ -1741,41 +1973,36 @@ async function runAuditCommand(site, opts) {
1741
1973
  const workdir = opts.workdir ?? `${process.env.HOME ?? ""}/.reddoor-maint/sites`;
1742
1974
  sites = await Promise.all(sites.map((s) => cloneIfNeeded(s, { workdir })));
1743
1975
  }
1744
- const results = await runAuditsAcross(sites, which);
1976
+ const results = [];
1977
+ const renderer = rendererFor(opts.json);
1978
+ await buildAuditTasks(sites, which, results, renderer).run();
1745
1979
  let output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);
1746
1980
  if (opts.writeAirtable !== void 0) {
1747
1981
  const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
1748
- const { listWebsites: listWebsites2, updateScores: updateScores2, siteSlug: siteSlug2 } = await Promise.resolve().then(() => (init_websites(), websites_exports));
1749
- const { lighthouseScoresFromResult: lighthouseScoresFromResult2, hasRealScores: hasRealScores2, resolveSlugFromCwd: resolveSlugFromCwd2 } = await Promise.resolve().then(() => (init_lighthouse_airtable(), lighthouse_airtable_exports));
1982
+ const { listWebsites: listWebsites2 } = await Promise.resolve().then(() => (init_websites(), websites_exports));
1983
+ const { resolveSlugFromCwd: resolveSlugFromCwd2 } = await Promise.resolve().then(() => (init_lighthouse_airtable(), lighthouse_airtable_exports));
1984
+ const { writeAuditsToAirtable: writeAuditsToAirtable2 } = await Promise.resolve().then(() => (init_write_audits_to_airtable(), write_audits_to_airtable_exports));
1750
1985
  const slug = typeof opts.writeAirtable === "string" && opts.writeAirtable.length > 0 ? opts.writeAirtable : await resolveSlugFromCwd2(cwd);
1751
- const lhResult = results.find((r) => r.audit === "lighthouse");
1752
- if (!lhResult) {
1753
- throw Object.assign(
1754
- new Error(
1755
- "--write-airtable requires a lighthouse result; did you pass --only without lighthouse?"
1756
- ),
1757
- { exitCode: 2 }
1758
- );
1759
- }
1760
- if (!hasRealScores2(lhResult)) {
1761
- throw Object.assign(
1762
- new Error(
1763
- `Lighthouse audit produced no scores; refusing to write to Airtable. Summary: ${lhResult.summary}`
1764
- ),
1765
- { exitCode: 1 }
1766
- );
1767
- }
1768
- const base = openBase2(readAirtableConfig2());
1769
- const websites = await listWebsites2(base);
1770
- const target = websites.find((w) => siteSlug2(w.name) === slug);
1771
- if (!target) {
1772
- throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
1773
- }
1774
- const scores = lighthouseScoresFromResult2(lhResult);
1775
- await updateScores2(base, target.id, scores);
1776
- output += `
1986
+ let writeSummary = null;
1987
+ await new Listr(
1988
+ [
1989
+ {
1990
+ title: `Write to Airtable[${slug}]`,
1991
+ task: async (_ctx, task) => {
1992
+ const base = openBase2(readAirtableConfig2());
1993
+ task.output = "loading Websites\u2026";
1994
+ const websites = await listWebsites2(base);
1995
+ task.output = "writing scores\u2026";
1996
+ writeSummary = await writeAuditsToAirtable2({ base, websites, slug, results });
1997
+ task.title = `Wrote to Websites[${writeSummary.siteName}] (${writeSummary.writes.length} audit type${writeSummary.writes.length === 1 ? "" : "s"})`;
1998
+ }
1999
+ }
2000
+ ],
2001
+ { renderer }
2002
+ ).run();
2003
+ if (writeSummary) output += `
1777
2004
 
1778
- \u2192 wrote scores to Websites[${target.name}]: P=${scores.performance} A=${scores.accessibility} BP=${scores.bestPractices} SEO=${scores.seo}`;
2005
+ ${formatWriteSummary(writeSummary)}`;
1779
2006
  }
1780
2007
  return { output, code: exitCode(results) };
1781
2008
  }