@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 CHANGED
@@ -9,6 +9,54 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/util/credentials.ts
13
+ import { readFileSync } from "fs";
14
+ import { homedir } from "os";
15
+ import { join } from "path";
16
+ function defaultCredentialsPath() {
17
+ const base = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
18
+ return join(base, "reddoor-maint", "credentials.env");
19
+ }
20
+ function parseEnvFile(contents) {
21
+ const out = {};
22
+ for (const rawLine of contents.split(/\r?\n/)) {
23
+ const line = rawLine.trim();
24
+ if (!line || line.startsWith("#")) continue;
25
+ const eq = line.indexOf("=");
26
+ if (eq <= 0) continue;
27
+ const key = line.slice(0, eq).trim();
28
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
29
+ let value = line.slice(eq + 1).trim();
30
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
31
+ value = value.slice(1, -1);
32
+ }
33
+ out[key] = value;
34
+ }
35
+ return out;
36
+ }
37
+ function loadCredentialsIntoEnv(path = defaultCredentialsPath()) {
38
+ let contents;
39
+ try {
40
+ contents = readFileSync(path, "utf-8");
41
+ } catch {
42
+ return [];
43
+ }
44
+ const parsed = parseEnvFile(contents);
45
+ const applied = [];
46
+ for (const [k, v] of Object.entries(parsed)) {
47
+ if (process.env[k] === void 0) {
48
+ process.env[k] = v;
49
+ applied.push(k);
50
+ }
51
+ }
52
+ return applied;
53
+ }
54
+ var init_credentials = __esm({
55
+ "src/util/credentials.ts"() {
56
+ "use strict";
57
+ }
58
+ });
59
+
12
60
  // src/reports/airtable/client.ts
13
61
  var client_exports = {};
14
62
  __export(client_exports, {
@@ -16,11 +64,19 @@ __export(client_exports, {
16
64
  readAirtableConfig: () => readAirtableConfig
17
65
  });
18
66
  import Airtable from "airtable";
67
+ function missing(name) {
68
+ return Object.assign(
69
+ new Error(
70
+ `${name} not set. Export it in your shell or put it in ${defaultCredentialsPath()} as ${name}=...`
71
+ ),
72
+ { exitCode: 2 }
73
+ );
74
+ }
19
75
  function readAirtableConfig() {
20
76
  const apiKey = process.env.AIRTABLE_PAT;
21
77
  const baseId = process.env.AIRTABLE_BASE_ID;
22
- if (!apiKey) throw Object.assign(new Error("AIRTABLE_PAT not set"), { exitCode: 2 });
23
- if (!baseId) throw Object.assign(new Error("AIRTABLE_BASE_ID not set"), { exitCode: 2 });
78
+ if (!apiKey) throw missing("AIRTABLE_PAT");
79
+ if (!baseId) throw missing("AIRTABLE_BASE_ID");
24
80
  return { apiKey, baseId };
25
81
  }
26
82
  function openBase(cfg) {
@@ -29,6 +85,7 @@ function openBase(cfg) {
29
85
  var init_client = __esm({
30
86
  "src/reports/airtable/client.ts"() {
31
87
  "use strict";
88
+ init_credentials();
32
89
  }
33
90
  });
34
91
 
@@ -179,7 +236,7 @@ __export(lighthouse_airtable_exports, {
179
236
  resolveSlugFromCwd: () => resolveSlugFromCwd
180
237
  });
181
238
  import { readFile as readFile7 } from "fs/promises";
182
- import { join as join7 } from "path";
239
+ import { join as join8 } from "path";
183
240
  function hasRealScores(result) {
184
241
  if (result.audit !== "lighthouse") return false;
185
242
  const details = result.details ?? {};
@@ -204,7 +261,7 @@ function lighthouseScoresFromResult(result) {
204
261
  }
205
262
  async function resolveSlugFromCwd(cwd) {
206
263
  try {
207
- const pkgPath = join7(cwd, "package.json");
264
+ const pkgPath = join8(cwd, "package.json");
208
265
  const raw = await readFile7(pkgPath, "utf-8");
209
266
  const pkg = JSON.parse(raw);
210
267
  if (!pkg.name) throw new Error("package.json has no 'name' field");
@@ -457,18 +514,18 @@ var init_reports = __esm({
457
514
  // src/reports/maintenance-email/assets/index.ts
458
515
  import { readFile as readFile13 } from "fs/promises";
459
516
  import { existsSync as existsSync3 } from "fs";
460
- import { dirname as dirname2, join as join21 } from "path";
517
+ import { dirname as dirname2, join as join22 } from "path";
461
518
  import { fileURLToPath as fileURLToPath2 } from "url";
462
519
  function resolveAssetsDir() {
463
520
  if (cachedAssetsDir) return cachedAssetsDir;
464
521
  let dir = dirname2(fileURLToPath2(import.meta.url));
465
522
  while (true) {
466
- const srcCandidate = join21(dir, "src", "reports", "maintenance-email", "assets", "check.png");
523
+ const srcCandidate = join22(dir, "src", "reports", "maintenance-email", "assets", "check.png");
467
524
  if (existsSync3(srcCandidate)) {
468
525
  cachedAssetsDir = dirname2(srcCandidate);
469
526
  return cachedAssetsDir;
470
527
  }
471
- const distCandidate = join21(dir, "dist", "reports", "maintenance-email", "assets", "check.png");
528
+ const distCandidate = join22(dir, "dist", "reports", "maintenance-email", "assets", "check.png");
472
529
  if (existsSync3(distCandidate)) {
473
530
  cachedAssetsDir = dirname2(distCandidate);
474
531
  return cachedAssetsDir;
@@ -485,8 +542,8 @@ function resolveAssetsDir() {
485
542
  async function loadBundledImages() {
486
543
  const assetsDir = resolveAssetsDir();
487
544
  const [check, blurred] = await Promise.all([
488
- readFile13(join21(assetsDir, "check.png")),
489
- readFile13(join21(assetsDir, "blurredTests.jpg"))
545
+ readFile13(join22(assetsDir, "check.png")),
546
+ readFile13(join22(assetsDir, "blurredTests.jpg"))
490
547
  ]);
491
548
  return {
492
549
  check: {
@@ -932,12 +989,14 @@ var init_orchestrate = __esm({
932
989
  });
933
990
 
934
991
  // src/cli/bin.ts
992
+ init_credentials();
935
993
  import { dirname as dirname6 } from "path";
936
994
  import { fileURLToPath as fileURLToPath3 } from "url";
937
995
  import { cac } from "cac";
938
996
 
939
997
  // src/cli/commands/audit.ts
940
998
  import { resolve as resolve2 } from "path";
999
+ import { Listr } from "listr2";
941
1000
 
942
1001
  // src/audits/util/spawn.ts
943
1002
  import { spawn } from "child_process";
@@ -970,7 +1029,7 @@ var defaultSpawn = (cmd, args, opts = {}) => new Promise((resolve10, reject) =>
970
1029
 
971
1030
  // src/audits/deps.ts
972
1031
  import { readFile } from "fs/promises";
973
- import { join } from "path";
1032
+ import { join as join2 } from "path";
974
1033
 
975
1034
  // src/util/site.ts
976
1035
  function siteLabel(site) {
@@ -1037,7 +1096,7 @@ function compareSemver(actual, baseline) {
1037
1096
  return "same";
1038
1097
  }
1039
1098
  async function depsAudit(ctx) {
1040
- const pkgPath = join(ctx.site.path, "package.json");
1099
+ const pkgPath = join2(ctx.site.path, "package.json");
1041
1100
  let pkgRaw;
1042
1101
  try {
1043
1102
  pkgRaw = await readFile(pkgPath, "utf-8");
@@ -1083,7 +1142,7 @@ async function depsAudit(ctx) {
1083
1142
  // src/audits/lint.ts
1084
1143
  import { existsSync } from "fs";
1085
1144
  import { readFile as readFile2 } from "fs/promises";
1086
- import { join as join2 } from "path";
1145
+ import { join as join3 } from "path";
1087
1146
  import { ESLint } from "eslint";
1088
1147
  import { check as prettierCheck, resolveConfig as prettierResolveConfig } from "prettier";
1089
1148
  import { glob } from "tinyglobby";
@@ -1094,7 +1153,7 @@ async function listFiles(cwd) {
1094
1153
  }
1095
1154
  async function lintAudit(ctx) {
1096
1155
  const { site } = ctx;
1097
- const configPath = join2(site.path, "eslint.config.js");
1156
+ const configPath = join3(site.path, "eslint.config.js");
1098
1157
  if (!existsSync(configPath)) {
1099
1158
  return {
1100
1159
  audit: "lint",
@@ -1114,7 +1173,7 @@ async function lintAudit(ctx) {
1114
1173
  const eslintWarnings = eslintResults.reduce((n, r) => n + r.warningCount, 0);
1115
1174
  const prettierUnformatted = [];
1116
1175
  for (const rel of relFiles) {
1117
- const absForResolve = join2(site.path, rel);
1176
+ const absForResolve = join3(site.path, rel);
1118
1177
  const source = await readFile2(absForResolve, "utf-8");
1119
1178
  const options = await prettierResolveConfig(absForResolve) ?? {};
1120
1179
  const ok = await prettierCheck(source, { ...options, filepath: absForResolve });
@@ -1278,7 +1337,7 @@ async function securityAudit(ctx) {
1278
1337
  // src/audits/lighthouse.ts
1279
1338
  import { readFile as readFile4, writeFile, mkdtemp, rm, readdir } from "fs/promises";
1280
1339
  import { tmpdir } from "os";
1281
- import { join as join4 } from "path";
1340
+ import { join as join5 } from "path";
1282
1341
 
1283
1342
  // src/configs/lighthouse.ts
1284
1343
  var lighthouseConfig = {
@@ -1313,11 +1372,11 @@ var lighthouseConfig = {
1313
1372
 
1314
1373
  // src/audits/util/site-config.ts
1315
1374
  import { readFile as readFile3 } from "fs/promises";
1316
- import { join as join3 } from "path";
1375
+ import { join as join4 } from "path";
1317
1376
  async function readSiteConfig(sitePath) {
1318
1377
  let raw;
1319
1378
  try {
1320
- raw = await readFile3(join3(sitePath, "package.json"), "utf-8");
1379
+ raw = await readFile3(join4(sitePath, "package.json"), "utf-8");
1321
1380
  } catch {
1322
1381
  return {};
1323
1382
  }
@@ -1378,7 +1437,7 @@ async function readLhrEntries(resultsDir) {
1378
1437
  const entries = [];
1379
1438
  for (const f of files) {
1380
1439
  if (!f.startsWith("lhr-") || !f.endsWith(".json")) continue;
1381
- const lhr = await readJsonMaybe(join4(resultsDir, f));
1440
+ const lhr = await readJsonMaybe(join5(resultsDir, f));
1382
1441
  if (!lhr || !lhr.categories) continue;
1383
1442
  const summary = {};
1384
1443
  for (const [k, v] of Object.entries(lhr.categories)) {
@@ -1432,10 +1491,10 @@ async function lighthouseAudit(ctx) {
1432
1491
  }
1433
1492
  }
1434
1493
  };
1435
- const configDir = await mkdtemp(join4(tmpdir(), "reddoor-lhci-"));
1436
- const configPath = join4(configDir, "lighthouserc.json");
1494
+ const configDir = await mkdtemp(join5(tmpdir(), "reddoor-lhci-"));
1495
+ const configPath = join5(configDir, "lighthouserc.json");
1437
1496
  await writeFile(configPath, JSON.stringify(resolvedConfig), "utf-8");
1438
- const resultsDir = join4(site.path, ".lighthouseci");
1497
+ const resultsDir = join5(site.path, ".lighthouseci");
1439
1498
  await rm(resultsDir, { recursive: true, force: true });
1440
1499
  let raw;
1441
1500
  try {
@@ -1470,7 +1529,7 @@ async function lighthouseAudit(ctx) {
1470
1529
  summary: `lighthouse: no lhr-*.json written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
1471
1530
  };
1472
1531
  }
1473
- const assertionResults = await readJsonMaybe(join4(resultsDir, "assertion-results.json")) ?? [];
1532
+ const assertionResults = await readJsonMaybe(join5(resultsDir, "assertion-results.json")) ?? [];
1474
1533
  const failed = assertionResults.filter((a) => !a.passed);
1475
1534
  const assertions = failed.map((a) => ({
1476
1535
  category: categoryFromAssertion(a),
@@ -1497,7 +1556,7 @@ async function lighthouseAudit(ctx) {
1497
1556
 
1498
1557
  // src/audits/a11y.ts
1499
1558
  import { readFile as readFile5, writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
1500
- import { join as join5 } from "path";
1559
+ import { join as join6 } from "path";
1501
1560
 
1502
1561
  // src/configs/playwright-a11y.ts
1503
1562
  import { defineConfig, devices } from "@playwright/test";
@@ -1622,14 +1681,14 @@ async function a11yAudit(ctx) {
1622
1681
  const spawn2 = ctx.spawn ?? defaultSpawn;
1623
1682
  const site = ctx.site;
1624
1683
  const label = siteLabel(site);
1625
- const specDir = await mkdtemp2(join5(site.path, ".reddoor-a11y-spec-"));
1626
- const specPath = join5(specDir, "a11y.spec.ts");
1684
+ const specDir = await mkdtemp2(join6(site.path, ".reddoor-a11y-spec-"));
1685
+ const specPath = join6(specDir, "a11y.spec.ts");
1627
1686
  await writeFile2(specPath, buildSpec(), "utf-8");
1628
1687
  const port = await findFreePort();
1629
- const configPath = join5(specDir, "playwright.config.ts");
1688
+ const configPath = join6(specDir, "playwright.config.ts");
1630
1689
  await writeFile2(configPath, buildPlaywrightConfig(port, site.path), "utf-8");
1631
- const resultsPath = join5(site.path, RESULTS_REL);
1632
- await rm2(join5(site.path, ".reddoor-a11y"), { recursive: true, force: true });
1690
+ const resultsPath = join6(site.path, RESULTS_REL);
1691
+ await rm2(join6(site.path, ".reddoor-a11y"), { recursive: true, force: true });
1633
1692
  let raw;
1634
1693
  try {
1635
1694
  raw = await spawn2(
@@ -1694,29 +1753,27 @@ var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
1694
1753
  function timedSpawn(timeoutMs) {
1695
1754
  return (cmd, args, opts = {}) => defaultSpawn(cmd, args, { ...opts, timeoutMs: opts.timeoutMs ?? timeoutMs });
1696
1755
  }
1756
+ async function runOneAudit(site, name) {
1757
+ if (!(name in REGISTRY)) throw new Error(`unknown audit: ${name}`);
1758
+ const spawn2 = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);
1759
+ const label = site.name ?? site.path;
1760
+ try {
1761
+ return await REGISTRY[name]({ site, spawn: spawn2 });
1762
+ } catch (err) {
1763
+ return {
1764
+ audit: name,
1765
+ site: label,
1766
+ status: "fail",
1767
+ summary: `${name}: unexpected error \u2014 ${String(err)}`
1768
+ };
1769
+ }
1770
+ }
1697
1771
  async function runAudits(site, which) {
1698
1772
  const names = which ?? ALL_AUDIT_NAMES;
1699
1773
  for (const n of names) {
1700
1774
  if (!(n in REGISTRY)) throw new Error(`unknown audit: ${n}`);
1701
1775
  }
1702
- const spawn2 = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);
1703
- const label = site.name ?? site.path;
1704
- return Promise.all(
1705
- names.map(
1706
- (n) => REGISTRY[n]({ site, spawn: spawn2 }).catch(
1707
- (err) => ({
1708
- audit: n,
1709
- site: label,
1710
- status: "fail",
1711
- summary: `${n}: unexpected error \u2014 ${String(err)}`
1712
- })
1713
- )
1714
- )
1715
- );
1716
- }
1717
- async function runAuditsAcross(sites, which) {
1718
- const all = await Promise.all(sites.map((s) => runAudits(s, which)));
1719
- return all.flat();
1776
+ return Promise.all(names.map((n) => runOneAudit(site, n)));
1720
1777
  }
1721
1778
 
1722
1779
  // src/cli/fleet/resolve-sites.ts
@@ -1807,7 +1864,7 @@ async function resolveSites(input) {
1807
1864
 
1808
1865
  // src/cli/fleet/clone-if-needed.ts
1809
1866
  import { stat, readdir as readdir2, mkdir } from "fs/promises";
1810
- import { isAbsolute as isAbsolute2, join as join6 } from "path";
1867
+ import { isAbsolute as isAbsolute2, join as join7 } from "path";
1811
1868
  function deriveNameFromRepoUrl(repoUrl) {
1812
1869
  const slash = repoUrl.split("/").pop() ?? repoUrl;
1813
1870
  return slash.replace(/\.git$/, "");
@@ -1848,7 +1905,7 @@ async function cloneIfNeeded(site, opts) {
1848
1905
  const name = site.name ?? deriveNameFromRepoUrl(site.repoUrl);
1849
1906
  assertSafeName(name);
1850
1907
  assertSafeRepoUrl(site.repoUrl);
1851
- const target = join6(opts.workdir, name);
1908
+ const target = join7(opts.workdir, name);
1852
1909
  await mkdir(opts.workdir, { recursive: true });
1853
1910
  if (await isNonEmptyDir(target)) {
1854
1911
  return { ...site, name, path: target };
@@ -1882,8 +1939,87 @@ function formatTable(results) {
1882
1939
  function exitCode(results) {
1883
1940
  return results.some((r) => r.status === "fail") ? 1 : 0;
1884
1941
  }
1942
+ function formatDuration(ms) {
1943
+ if (ms < 1e3) return `${ms}ms`;
1944
+ const totalSeconds = Math.round(ms / 1e3);
1945
+ if (totalSeconds < 60) return `${totalSeconds}s`;
1946
+ const m = Math.floor(totalSeconds / 60);
1947
+ const s = totalSeconds % 60;
1948
+ return `${m}m${s.toString().padStart(2, "0")}s`;
1949
+ }
1950
+ function buildAuditTasks(sites, which, results, renderer) {
1951
+ const singleSite = sites.length === 1;
1952
+ if (singleSite) {
1953
+ const site = sites[0];
1954
+ return new Listr(
1955
+ which.map((name) => ({
1956
+ title: name,
1957
+ task: async (_ctx, task) => {
1958
+ const start = Date.now();
1959
+ const result = await runOneAudit(site, name);
1960
+ results.push(result);
1961
+ const elapsed = formatDuration(Date.now() - start);
1962
+ task.title = `${name}: ${result.summary} (${elapsed})`;
1963
+ if (result.status === "fail") throw new Error(result.summary);
1964
+ }
1965
+ })),
1966
+ { concurrent: true, exitOnError: false, renderer }
1967
+ );
1968
+ }
1969
+ return new Listr(
1970
+ sites.map((site) => {
1971
+ const label = site.name ?? site.path;
1972
+ return {
1973
+ title: label,
1974
+ task: async (_ctx, task) => {
1975
+ const start = Date.now();
1976
+ let done = 0;
1977
+ task.output = `0/${which.length} audits`;
1978
+ const settled = await Promise.all(
1979
+ which.map(async (name) => {
1980
+ const r = await runOneAudit(site, name);
1981
+ results.push(r);
1982
+ done += 1;
1983
+ task.output = `${done}/${which.length} audits`;
1984
+ return r;
1985
+ })
1986
+ );
1987
+ const elapsed = formatDuration(Date.now() - start);
1988
+ const failed = settled.filter((r) => r.status === "fail").length;
1989
+ const warned = settled.filter((r) => r.status === "warn").length;
1990
+ const note = failed > 0 ? `${failed} failed` : warned > 0 ? `${warned} warning${warned === 1 ? "" : "s"}` : "all green";
1991
+ task.title = `${label}: ${note} (${elapsed})`;
1992
+ if (failed > 0) throw new Error(`${label}: ${failed} audit(s) failed`);
1993
+ }
1994
+ };
1995
+ }),
1996
+ { concurrent: true, exitOnError: false, renderer }
1997
+ );
1998
+ }
1999
+ function formatWriteSummary(summary) {
2000
+ const lines = summary.writes.map((w) => {
2001
+ if (w.audit === "lighthouse") {
2002
+ const s = w.counts;
2003
+ return ` lighthouse: P=${s.performance} A=${s.accessibility} BP=${s.bestPractices} SEO=${s.seo}`;
2004
+ }
2005
+ if (w.audit === "a11y") {
2006
+ return ` a11y: ${w.counts.violations} violations`;
2007
+ }
2008
+ if (w.audit === "deps") {
2009
+ const c2 = w.counts;
2010
+ return ` deps: ${c2.drifted} drifted (${c2.majorBehind} major)`;
2011
+ }
2012
+ const c = w.counts;
2013
+ return ` security: ${c.critical}C/${c.high}H/${c.moderate}M/${c.low}L`;
2014
+ });
2015
+ return `\u2192 wrote to Websites[${summary.siteName}]:
2016
+ ${lines.join("\n")}`;
2017
+ }
2018
+ function rendererFor(json) {
2019
+ return json ? "silent" : "default";
2020
+ }
1885
2021
  async function runAuditCommand(site, opts) {
1886
- const which = parseOnly(opts.only);
2022
+ const which = parseOnly(opts.only) ?? ALL_AUDIT_NAMES;
1887
2023
  const cwd = opts.cwd ? resolve2(opts.cwd) : process.cwd();
1888
2024
  let sites = await resolveSites({
1889
2025
  ...site !== void 0 ? { site } : {},
@@ -1895,7 +2031,9 @@ async function runAuditCommand(site, opts) {
1895
2031
  const workdir = opts.workdir ?? `${process.env.HOME ?? ""}/.reddoor-maint/sites`;
1896
2032
  sites = await Promise.all(sites.map((s) => cloneIfNeeded(s, { workdir })));
1897
2033
  }
1898
- const results = await runAuditsAcross(sites, which);
2034
+ const results = [];
2035
+ const renderer = rendererFor(opts.json);
2036
+ await buildAuditTasks(sites, which, results, renderer).run();
1899
2037
  let output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);
1900
2038
  if (opts.writeAirtable !== void 0) {
1901
2039
  const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
@@ -1903,39 +2041,37 @@ async function runAuditCommand(site, opts) {
1903
2041
  const { resolveSlugFromCwd: resolveSlugFromCwd2 } = await Promise.resolve().then(() => (init_lighthouse_airtable(), lighthouse_airtable_exports));
1904
2042
  const { writeAuditsToAirtable: writeAuditsToAirtable2 } = await Promise.resolve().then(() => (init_write_audits_to_airtable(), write_audits_to_airtable_exports));
1905
2043
  const slug = typeof opts.writeAirtable === "string" && opts.writeAirtable.length > 0 ? opts.writeAirtable : await resolveSlugFromCwd2(cwd);
1906
- const base = openBase2(readAirtableConfig2());
1907
- const websites = await listWebsites2(base);
1908
- const summary = await writeAuditsToAirtable2({ base, websites, slug, results });
1909
- const lines = summary.writes.map((w) => {
1910
- if (w.audit === "lighthouse") {
1911
- const s = w.counts;
1912
- return ` lighthouse: P=${s.performance} A=${s.accessibility} BP=${s.bestPractices} SEO=${s.seo}`;
1913
- }
1914
- if (w.audit === "a11y") {
1915
- return ` a11y: ${w.counts.violations} violations`;
1916
- }
1917
- if (w.audit === "deps") {
1918
- const c2 = w.counts;
1919
- return ` deps: ${c2.drifted} drifted (${c2.majorBehind} major)`;
1920
- }
1921
- const c = w.counts;
1922
- return ` security: ${c.critical}C/${c.high}H/${c.moderate}M/${c.low}L`;
1923
- });
1924
- output += `
2044
+ let writeSummary = null;
2045
+ await new Listr(
2046
+ [
2047
+ {
2048
+ title: `Write to Airtable[${slug}]`,
2049
+ task: async (_ctx, task) => {
2050
+ const base = openBase2(readAirtableConfig2());
2051
+ task.output = "loading Websites\u2026";
2052
+ const websites = await listWebsites2(base);
2053
+ task.output = "writing scores\u2026";
2054
+ writeSummary = await writeAuditsToAirtable2({ base, websites, slug, results });
2055
+ task.title = `Wrote to Websites[${writeSummary.siteName}] (${writeSummary.writes.length} audit type${writeSummary.writes.length === 1 ? "" : "s"})`;
2056
+ }
2057
+ }
2058
+ ],
2059
+ { renderer }
2060
+ ).run();
2061
+ if (writeSummary) output += `
1925
2062
 
1926
- \u2192 wrote to Websites[${summary.siteName}]:
1927
- ${lines.join("\n")}`;
2063
+ ${formatWriteSummary(writeSummary)}`;
1928
2064
  }
1929
2065
  return { output, code: exitCode(results) };
1930
2066
  }
1931
2067
 
1932
2068
  // src/cli/commands/sync-configs.ts
1933
2069
  import { readFile as readFile9 } from "fs/promises";
1934
- import { join as join9, resolve as resolve3 } from "path";
2070
+ import { join as join10, resolve as resolve3 } from "path";
1935
2071
 
1936
2072
  // src/recipes/sync-configs.ts
1937
2073
  import { readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
1938
- import { join as join8 } from "path";
2074
+ import { join as join9 } from "path";
1939
2075
 
1940
2076
  // src/recipes/sync-configs/templates.ts
1941
2077
  var eslint = {
@@ -2208,13 +2344,13 @@ async function readMaybe(path) {
2208
2344
  async function planTemplateDiffs(cwd, templates) {
2209
2345
  const diffs = [];
2210
2346
  for (const t of templates) {
2211
- const existing = await readMaybe(join8(cwd, t.path));
2347
+ const existing = await readMaybe(join9(cwd, t.path));
2212
2348
  if (existing !== t.contents) diffs.push(t);
2213
2349
  }
2214
2350
  return diffs;
2215
2351
  }
2216
2352
  async function planGitignore(cwd) {
2217
- const existing = await readMaybe(join8(cwd, ".gitignore"));
2353
+ const existing = await readMaybe(join9(cwd, ".gitignore"));
2218
2354
  const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);
2219
2355
  const tracked = await listTrackedFiles(cwd);
2220
2356
  const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);
@@ -2222,7 +2358,7 @@ async function planGitignore(cwd) {
2222
2358
  return { kind: "apply", content: merge.content, toUntrack, added: merge.added };
2223
2359
  }
2224
2360
  async function applyGitignore(cwd, plan) {
2225
- await writeFile3(join8(cwd, ".gitignore"), plan.content, "utf-8");
2361
+ await writeFile3(join9(cwd, ".gitignore"), plan.content, "utf-8");
2226
2362
  if (plan.toUntrack.length > 0) {
2227
2363
  await removeFromIndex(cwd, plan.toUntrack);
2228
2364
  }
@@ -2245,7 +2381,7 @@ async function syncConfigs(site, opts = {}) {
2245
2381
  },
2246
2382
  apply: async ({ templateDiffs, gitignorePlan }, { commit: commit2 }) => {
2247
2383
  for (const t of templateDiffs) {
2248
- await writeFile3(join8(site.path, t.path), t.contents, "utf-8");
2384
+ await writeFile3(join9(site.path, t.path), t.contents, "utf-8");
2249
2385
  await commit2(`chore: sync ${t.config} config from @reddoorla/maintenance`);
2250
2386
  }
2251
2387
  if (gitignorePlan.kind === "apply") {
@@ -2274,7 +2410,7 @@ function parseOnly2(value) {
2274
2410
  async function dryPlanGitignore(cwd) {
2275
2411
  let existing;
2276
2412
  try {
2277
- existing = await readFile9(join9(cwd, ".gitignore"), "utf-8");
2413
+ existing = await readFile9(join10(cwd, ".gitignore"), "utf-8");
2278
2414
  } catch {
2279
2415
  return "would create .gitignore";
2280
2416
  }
@@ -2289,7 +2425,7 @@ async function dryPlan(cwd, which) {
2289
2425
  for (const t of templateTargets) {
2290
2426
  let existing = "";
2291
2427
  try {
2292
- existing = await readFile9(join9(cwd, t.path), "utf-8");
2428
+ existing = await readFile9(join10(cwd, t.path), "utf-8");
2293
2429
  } catch {
2294
2430
  }
2295
2431
  if (existing !== t.contents) lines.push(`would update ${t.path} (config: ${t.config})`);
@@ -2337,7 +2473,7 @@ import { resolve as resolve4 } from "path";
2337
2473
 
2338
2474
  // src/recipes/bump-deps.ts
2339
2475
  import { stat as stat2 } from "fs/promises";
2340
- import { join as join10 } from "path";
2476
+ import { join as join11 } from "path";
2341
2477
  async function exists(path) {
2342
2478
  try {
2343
2479
  await stat2(path);
@@ -2366,10 +2502,10 @@ async function bumpDeps(site, opts = {}) {
2366
2502
  // land on top of whatever else was in the tree.
2367
2503
  checkTreeFirst: true,
2368
2504
  plan: async () => {
2369
- const hasPnpmLock = await exists(join10(site.path, "pnpm-lock.yaml"));
2505
+ const hasPnpmLock = await exists(join11(site.path, "pnpm-lock.yaml"));
2370
2506
  if (!hasPnpmLock) {
2371
- const hasNpmLock = await exists(join10(site.path, "package-lock.json"));
2372
- const hasYarnLock = await exists(join10(site.path, "yarn.lock"));
2507
+ const hasNpmLock = await exists(join11(site.path, "package-lock.json"));
2508
+ const hasYarnLock = await exists(join11(site.path, "yarn.lock"));
2373
2509
  if (hasNpmLock || hasYarnLock) {
2374
2510
  const competing = hasNpmLock ? "package-lock.json" : "yarn.lock";
2375
2511
  return {
@@ -2439,7 +2575,7 @@ async function runBumpDepsCommand(site, opts) {
2439
2575
  import { resolve as resolve5 } from "path";
2440
2576
 
2441
2577
  // src/recipes/svelte-5/index.ts
2442
- import { join as join16 } from "path";
2578
+ import { join as join17 } from "path";
2443
2579
 
2444
2580
  // src/util/pkg.ts
2445
2581
  import { readFile as readFile10, writeFile as writeFile4 } from "fs/promises";
@@ -2488,7 +2624,7 @@ function bumpDep(pkg, name, version2, opts = {}) {
2488
2624
  }
2489
2625
 
2490
2626
  // src/recipes/svelte-5/step-bump-versions.ts
2491
- import { join as join11 } from "path";
2627
+ import { join as join12 } from "path";
2492
2628
  var SVELTE_5_VERSIONS = {
2493
2629
  svelte: "^5.55.5",
2494
2630
  "@sveltejs/kit": "^2.59.0",
@@ -2501,7 +2637,7 @@ var SVELTE_5_VERSIONS = {
2501
2637
  "typescript-svelte-plugin": "^0.3.52"
2502
2638
  };
2503
2639
  async function bumpToSvelte5Versions(cwd) {
2504
- const pkgPath = join11(cwd, "package.json");
2640
+ const pkgPath = join12(cwd, "package.json");
2505
2641
  const pkg = await readPackageJson(pkgPath);
2506
2642
  let next = pkg;
2507
2643
  for (const [name, version2] of Object.entries(SVELTE_5_VERSIONS)) {
@@ -2514,7 +2650,7 @@ async function bumpToSvelte5Versions(cwd) {
2514
2650
 
2515
2651
  // src/recipes/svelte-5/step-svelte-config.ts
2516
2652
  import { readFile as readFile11, writeFile as writeFile5 } from "fs/promises";
2517
- import { join as join12 } from "path";
2653
+ import { join as join13 } from "path";
2518
2654
  var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
2519
2655
  var IMPORT_FROM_VITE_PLUGIN = new RegExp(
2520
2656
  String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
@@ -2555,7 +2691,7 @@ function dropPreprocessKey(source) {
2555
2691
  return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
2556
2692
  }
2557
2693
  async function migrateSvelteConfig(cwd) {
2558
- const path = join12(cwd, "svelte.config.js");
2694
+ const path = join13(cwd, "svelte.config.js");
2559
2695
  let src;
2560
2696
  try {
2561
2697
  src = await readFile11(path, "utf-8");
@@ -2592,9 +2728,9 @@ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
2592
2728
  }
2593
2729
 
2594
2730
  // src/recipes/svelte-5/step-tailwind-upgrade.ts
2595
- import { join as join13 } from "path";
2731
+ import { join as join14 } from "path";
2596
2732
  async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
2597
- const pkg = await readPackageJson(join13(cwd, "package.json"));
2733
+ const pkg = await readPackageJson(join14(cwd, "package.json"));
2598
2734
  const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
2599
2735
  if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
2600
2736
  if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
@@ -2616,7 +2752,7 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
2616
2752
 
2617
2753
  // src/recipes/svelte-5/step-gotchas.ts
2618
2754
  import { readFile as readFile12, writeFile as writeFile6 } from "fs/promises";
2619
- import { join as join14 } from "path";
2755
+ import { join as join15 } from "path";
2620
2756
  import { glob as glob2 } from "tinyglobby";
2621
2757
 
2622
2758
  // src/recipes/svelte-5/codemods/on-event-to-handler.ts
@@ -2962,7 +3098,7 @@ async function planGotchaCodemods(cwd) {
2962
3098
  const changes = [];
2963
3099
  const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
2964
3100
  for (const rel of relPaths) {
2965
- const path = join14(cwd, rel);
3101
+ const path = join15(cwd, rel);
2966
3102
  const before = await readFile12(path, "utf-8");
2967
3103
  const after = CODEMODS.reduce((s, fn) => fn(s), before);
2968
3104
  if (after !== before) changes.push({ rel, after });
@@ -2972,7 +3108,7 @@ async function planGotchaCodemods(cwd) {
2972
3108
  async function applyGotchaCodemods(cwd) {
2973
3109
  const changes = await planGotchaCodemods(cwd);
2974
3110
  for (const c of changes) {
2975
- await writeFile6(join14(cwd, c.rel), c.after, "utf-8");
3111
+ await writeFile6(join15(cwd, c.rel), c.after, "utf-8");
2976
3112
  }
2977
3113
  return { filesChanged: changes.length };
2978
3114
  }
@@ -2996,7 +3132,7 @@ async function verifyMigration(cwd, spawn2 = defaultSpawn) {
2996
3132
 
2997
3133
  // src/recipes/svelte-5/step-summary.ts
2998
3134
  import { writeFile as writeFile7 } from "fs/promises";
2999
- import { join as join15 } from "path";
3135
+ import { join as join16 } from "path";
3000
3136
  async function writeMigrationSummary(input) {
3001
3137
  const lines = [
3002
3138
  `# Svelte 4 \u2192 5 migration summary`,
@@ -3013,7 +3149,7 @@ async function writeMigrationSummary(input) {
3013
3149
  `- Verify Playwright a11y tests still pass.`
3014
3150
  ];
3015
3151
  const content = lines.join("\n") + "\n";
3016
- const path = join15(input.cwd, "MIGRATION_SVELTE_5.md");
3152
+ const path = join16(input.cwd, "MIGRATION_SVELTE_5.md");
3017
3153
  await writeFile7(path, content, "utf-8");
3018
3154
  return path;
3019
3155
  }
@@ -3021,7 +3157,7 @@ async function writeMigrationSummary(input) {
3021
3157
  // src/recipes/svelte-5/index.ts
3022
3158
  async function alreadyOnSvelte5(cwd) {
3023
3159
  try {
3024
- const pkg = await readPackageJson(join16(cwd, "package.json"));
3160
+ const pkg = await readPackageJson(join17(cwd, "package.json"));
3025
3161
  const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
3026
3162
  return !!v && /^\^?5\./.test(v);
3027
3163
  } catch {
@@ -3116,7 +3252,7 @@ import { resolve as resolve6 } from "path";
3116
3252
 
3117
3253
  // src/recipes/convert-to-pnpm.ts
3118
3254
  import { rm as rm3, stat as stat3 } from "fs/promises";
3119
- import { join as join17 } from "path";
3255
+ import { join as join18 } from "path";
3120
3256
 
3121
3257
  // src/recipes/convert-to-pnpm/script-rewrites.ts
3122
3258
  function rewriteScriptForPnpm(script) {
@@ -3149,9 +3285,9 @@ async function exists2(path) {
3149
3285
  async function convertToPnpm(site, opts = {}) {
3150
3286
  const spawn2 = opts.spawn ?? defaultSpawn;
3151
3287
  const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;
3152
- const pnpmLockPath = join17(site.path, "pnpm-lock.yaml");
3153
- const npmLockPath = join17(site.path, "package-lock.json");
3154
- const yarnLockPath = join17(site.path, "yarn.lock");
3288
+ const pnpmLockPath = join18(site.path, "pnpm-lock.yaml");
3289
+ const npmLockPath = join18(site.path, "package-lock.json");
3290
+ const yarnLockPath = join18(site.path, "yarn.lock");
3155
3291
  return withRecipe({
3156
3292
  name: "convert-to-pnpm",
3157
3293
  site,
@@ -3174,7 +3310,7 @@ async function convertToPnpm(site, opts = {}) {
3174
3310
  if (hasYarnLock) await rm3(yarnLockPath, { force: true });
3175
3311
  const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
3176
3312
  await commit2(`chore(pnpm): remove ${sourceLock}`);
3177
- const pkgPath = join17(cwd, "package.json");
3313
+ const pkgPath = join18(cwd, "package.json");
3178
3314
  const pkg = await readPackageJson(pkgPath);
3179
3315
  const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
3180
3316
  if (pkg.scripts && typeof pkg.scripts === "object") {
@@ -3187,7 +3323,7 @@ async function convertToPnpm(site, opts = {}) {
3187
3323
  }
3188
3324
  await writePackageJson(pkgPath, next);
3189
3325
  await commit2("chore(pnpm): pin packageManager + rewrite npm scripts");
3190
- await rm3(join17(cwd, "node_modules"), { recursive: true, force: true });
3326
+ await rm3(join18(cwd, "node_modules"), { recursive: true, force: true });
3191
3327
  const installResult = await spawn2("pnpm", ["install"], { cwd, streaming: true });
3192
3328
  if (installResult.code !== 0) {
3193
3329
  return { kind: "failed", notes: `pnpm install failed (exit ${installResult.code})` };
@@ -3228,19 +3364,19 @@ import { resolve as resolve7 } from "path";
3228
3364
 
3229
3365
  // src/recipes/onboard.ts
3230
3366
  import { stat as stat4 } from "fs/promises";
3231
- import { join as join19 } from "path";
3367
+ import { join as join20 } from "path";
3232
3368
 
3233
3369
  // src/util/self-version.ts
3234
- import { readFileSync, existsSync as existsSync2 } from "fs";
3370
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
3235
3371
  import { fileURLToPath } from "url";
3236
- import { dirname, join as join18 } from "path";
3372
+ import { dirname, join as join19 } from "path";
3237
3373
  function selfPackageVersion(callerImportMetaUrl) {
3238
3374
  try {
3239
3375
  let dir = dirname(fileURLToPath(callerImportMetaUrl));
3240
3376
  while (true) {
3241
- const candidate = join18(dir, "package.json");
3377
+ const candidate = join19(dir, "package.json");
3242
3378
  if (existsSync2(candidate)) {
3243
- const raw = readFileSync(candidate, "utf-8");
3379
+ const raw = readFileSync2(candidate, "utf-8");
3244
3380
  const pkg = JSON.parse(raw);
3245
3381
  if (pkg.name === "@reddoorla/maintenance") {
3246
3382
  return pkg.version ?? "0.0.0";
@@ -3297,13 +3433,13 @@ async function onboard(site, opts = {}) {
3297
3433
  name: "onboard",
3298
3434
  site,
3299
3435
  plan: async () => {
3300
- if (!await exists3(join19(site.path, "pnpm-lock.yaml"))) {
3436
+ if (!await exists3(join20(site.path, "pnpm-lock.yaml"))) {
3301
3437
  return {
3302
3438
  kind: "failed",
3303
3439
  notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
3304
3440
  };
3305
3441
  }
3306
- const pkgPath = join19(site.path, "package.json");
3442
+ const pkgPath = join20(site.path, "package.json");
3307
3443
  const pkg = await readPackageJson(pkgPath);
3308
3444
  const toAdd = [];
3309
3445
  if (!isDeclared(pkg, PACKAGE_NAME)) {
@@ -3323,7 +3459,7 @@ async function onboard(site, opts = {}) {
3323
3459
  return { kind: "apply", plan: { pkg, toAdd } };
3324
3460
  },
3325
3461
  apply: async ({ pkg, toAdd }, { commit: commit2, cwd }) => {
3326
- const pkgPath = join19(cwd, "package.json");
3462
+ const pkgPath = join20(cwd, "package.json");
3327
3463
  let next = pkg;
3328
3464
  for (const dep of toAdd) {
3329
3465
  next = bumpDep(next, dep.name, dep.version);
@@ -3392,7 +3528,7 @@ import { resolve as resolve8 } from "path";
3392
3528
 
3393
3529
  // src/recipes/svelte-codemods.ts
3394
3530
  import { writeFile as writeFile8 } from "fs/promises";
3395
- import { join as join20 } from "path";
3531
+ import { join as join21 } from "path";
3396
3532
  async function svelteCodemods(site) {
3397
3533
  return withRecipe({
3398
3534
  name: "svelte-codemods",
@@ -3406,7 +3542,7 @@ async function svelteCodemods(site) {
3406
3542
  },
3407
3543
  apply: async (changes, { commit: commit2, cwd }) => {
3408
3544
  for (const c of changes) {
3409
- await writeFile8(join20(cwd, c.rel), c.after, "utf-8");
3545
+ await writeFile8(join21(cwd, c.rel), c.after, "utf-8");
3410
3546
  }
3411
3547
  await commit2(`refactor(svelte5): apply codemods (${changes.length} files)`);
3412
3548
  return { kind: "ok" };
@@ -3633,7 +3769,7 @@ import { resolve as resolve9 } from "path";
3633
3769
 
3634
3770
  // src/recipes/a11y-fixtures-page/index.ts
3635
3771
  import { access, mkdir as mkdir3, writeFile as writeFile10 } from "fs/promises";
3636
- import { dirname as dirname4, join as join22 } from "path";
3772
+ import { dirname as dirname4, join as join23 } from "path";
3637
3773
 
3638
3774
  // src/recipes/a11y-fixtures-page/template.ts
3639
3775
  var A11Y_FIXTURES_PAGE_RELATIVE = "src/routes/dev/a11y-fixtures/+page.svelte";
@@ -3684,7 +3820,7 @@ async function fileExists(path) {
3684
3820
  }
3685
3821
  }
3686
3822
  async function a11yFixturesPage(site) {
3687
- const target = join22(site.path, A11Y_FIXTURES_PAGE_RELATIVE);
3823
+ const target = join23(site.path, A11Y_FIXTURES_PAGE_RELATIVE);
3688
3824
  return withRecipe({
3689
3825
  name: "a11y-fixtures-page",
3690
3826
  site,
@@ -3791,15 +3927,15 @@ async function runInitCommand(site, opts) {
3791
3927
  }
3792
3928
 
3793
3929
  // src/cli/version.ts
3794
- import { readFileSync as readFileSync2, existsSync as existsSync4 } from "fs";
3795
- import { dirname as dirname5, join as join23 } from "path";
3930
+ import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
3931
+ import { dirname as dirname5, join as join24 } from "path";
3796
3932
  function resolvePackageVersion(fromDir) {
3797
3933
  try {
3798
3934
  let dir = fromDir;
3799
3935
  while (true) {
3800
- const candidate = join23(dir, "package.json");
3936
+ const candidate = join24(dir, "package.json");
3801
3937
  if (existsSync4(candidate)) {
3802
- const raw = readFileSync2(candidate, "utf-8");
3938
+ const raw = readFileSync3(candidate, "utf-8");
3803
3939
  const pkg = JSON.parse(raw);
3804
3940
  if (pkg.name === "@reddoorla/maintenance") {
3805
3941
  return pkg.version ?? "unknown";
@@ -3815,6 +3951,7 @@ function resolvePackageVersion(fromDir) {
3815
3951
  }
3816
3952
 
3817
3953
  // src/cli/bin.ts
3954
+ loadCredentialsIntoEnv();
3818
3955
  var here = dirname6(fileURLToPath3(import.meta.url));
3819
3956
  var version = resolvePackageVersion(here);
3820
3957
  var AUDIT_DESCRIPTIONS = {