@reddoorla/maintenance 0.55.0 → 0.56.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.
Files changed (148) hide show
  1. package/README.md +3 -1
  2. package/dist/airtable-2BJX4ATJ.js +9 -0
  3. package/dist/airtable-2BJX4ATJ.js.map +1 -0
  4. package/dist/announce-JEVCDDKL.js +171 -0
  5. package/dist/announce-JEVCDDKL.js.map +1 -0
  6. package/dist/bump-deps-O3SOLQYY.js +57 -0
  7. package/dist/bump-deps-O3SOLQYY.js.map +1 -0
  8. package/dist/chunk-2X322ZES.js +99 -0
  9. package/dist/chunk-2X322ZES.js.map +1 -0
  10. package/dist/chunk-4NAOR44V.js +153 -0
  11. package/dist/chunk-4NAOR44V.js.map +1 -0
  12. package/dist/chunk-4PY4I5OR.js +1167 -0
  13. package/dist/chunk-4PY4I5OR.js.map +1 -0
  14. package/dist/chunk-5WL3ET24.js +203 -0
  15. package/dist/chunk-5WL3ET24.js.map +1 -0
  16. package/dist/chunk-6LIWDIXU.js +38 -0
  17. package/dist/chunk-6LIWDIXU.js.map +1 -0
  18. package/dist/chunk-6VAL7XJT.js +52 -0
  19. package/dist/chunk-6VAL7XJT.js.map +1 -0
  20. package/dist/chunk-BWLUNY7L.js +585 -0
  21. package/dist/chunk-BWLUNY7L.js.map +1 -0
  22. package/dist/chunk-CASCWQQX.js +161 -0
  23. package/dist/chunk-CASCWQQX.js.map +1 -0
  24. package/dist/chunk-CLVTVDOU.js +274 -0
  25. package/dist/chunk-CLVTVDOU.js.map +1 -0
  26. package/dist/chunk-D3DQFC32.js +79 -0
  27. package/dist/chunk-D3DQFC32.js.map +1 -0
  28. package/dist/chunk-DIVB6ZF7.js +217 -0
  29. package/dist/chunk-DIVB6ZF7.js.map +1 -0
  30. package/dist/chunk-EYODPUHV.js +37 -0
  31. package/dist/chunk-EYODPUHV.js.map +1 -0
  32. package/dist/chunk-FDIFTIXC.js +70 -0
  33. package/dist/chunk-FDIFTIXC.js.map +1 -0
  34. package/dist/chunk-FUD4I2ZW.js +35 -0
  35. package/dist/chunk-FUD4I2ZW.js.map +1 -0
  36. package/dist/chunk-HG5DTFNF.js +62 -0
  37. package/dist/chunk-HG5DTFNF.js.map +1 -0
  38. package/dist/chunk-HN6I2XWI.js +65 -0
  39. package/dist/chunk-HN6I2XWI.js.map +1 -0
  40. package/dist/chunk-HVOOCK6L.js +84 -0
  41. package/dist/chunk-HVOOCK6L.js.map +1 -0
  42. package/dist/chunk-IMIR7SC2.js +51 -0
  43. package/dist/chunk-IMIR7SC2.js.map +1 -0
  44. package/dist/chunk-J3OPWFZE.js +64 -0
  45. package/dist/chunk-J3OPWFZE.js.map +1 -0
  46. package/dist/chunk-JNXCDUYB.js +211 -0
  47. package/dist/chunk-JNXCDUYB.js.map +1 -0
  48. package/dist/chunk-JQG222CI.js +49 -0
  49. package/dist/chunk-JQG222CI.js.map +1 -0
  50. package/dist/chunk-LKGVSM2O.js +43 -0
  51. package/dist/chunk-LKGVSM2O.js.map +1 -0
  52. package/dist/chunk-LYCKN7ZR.js +41 -0
  53. package/dist/chunk-LYCKN7ZR.js.map +1 -0
  54. package/dist/chunk-O65I7G7G.js +246 -0
  55. package/dist/chunk-O65I7G7G.js.map +1 -0
  56. package/dist/chunk-OCS7SZUZ.js +243 -0
  57. package/dist/chunk-OCS7SZUZ.js.map +1 -0
  58. package/dist/chunk-OGGRFBL4.js +14 -0
  59. package/dist/chunk-OGGRFBL4.js.map +1 -0
  60. package/dist/chunk-OQPMZ6BM.js +368 -0
  61. package/dist/chunk-OQPMZ6BM.js.map +1 -0
  62. package/dist/chunk-OTGB6YQO.js +52 -0
  63. package/dist/chunk-OTGB6YQO.js.map +1 -0
  64. package/dist/chunk-PF6BNYGE.js +162 -0
  65. package/dist/chunk-PF6BNYGE.js.map +1 -0
  66. package/dist/chunk-QTHKOIFJ.js +13 -0
  67. package/dist/chunk-QTHKOIFJ.js.map +1 -0
  68. package/dist/chunk-U5SZ4FOP.js +26 -0
  69. package/dist/chunk-U5SZ4FOP.js.map +1 -0
  70. package/dist/chunk-VXSX2FHO.js +269 -0
  71. package/dist/chunk-VXSX2FHO.js.map +1 -0
  72. package/dist/chunk-W3G7XFBZ.js +444 -0
  73. package/dist/chunk-W3G7XFBZ.js.map +1 -0
  74. package/dist/chunk-WEKL2HYJ.js +31 -0
  75. package/dist/chunk-WEKL2HYJ.js.map +1 -0
  76. package/dist/chunk-WVRPOXKH.js +74 -0
  77. package/dist/chunk-WVRPOXKH.js.map +1 -0
  78. package/dist/chunk-XKOZUVK2.js +121 -0
  79. package/dist/chunk-XKOZUVK2.js.map +1 -0
  80. package/dist/chunk-XO2RUYVM.js +138 -0
  81. package/dist/chunk-XO2RUYVM.js.map +1 -0
  82. package/dist/chunk-XTK5VIQB.js +97 -0
  83. package/dist/chunk-XTK5VIQB.js.map +1 -0
  84. package/dist/chunk-XXTZBPUY.js +9 -0
  85. package/dist/chunk-XXTZBPUY.js.map +1 -0
  86. package/dist/chunk-ZYBOT5PM.js +153 -0
  87. package/dist/chunk-ZYBOT5PM.js.map +1 -0
  88. package/dist/cli/bin.js +75 -7785
  89. package/dist/cli/bin.js.map +1 -1
  90. package/dist/cli/commands/audit.js +37 -2336
  91. package/dist/cli/commands/audit.js.map +1 -1
  92. package/dist/client-EPVO3FEH.js +10 -0
  93. package/dist/client-EPVO3FEH.js.map +1 -0
  94. package/dist/client-JA6QLEDW.js +48 -0
  95. package/dist/client-JA6QLEDW.js.map +1 -0
  96. package/dist/configs/lighthouse.js +4 -31
  97. package/dist/configs/lighthouse.js.map +1 -1
  98. package/dist/configs/playwright-a11y.js +6 -33
  99. package/dist/configs/playwright-a11y.js.map +1 -1
  100. package/dist/convert-to-pnpm-UR64X3OX.js +51 -0
  101. package/dist/convert-to-pnpm-UR64X3OX.js.map +1 -0
  102. package/dist/db-AXKMRTVA.js +23 -0
  103. package/dist/db-AXKMRTVA.js.map +1 -0
  104. package/dist/digest-YTO73BEO.js +408 -0
  105. package/dist/digest-YTO73BEO.js.map +1 -0
  106. package/dist/forms/index.js +3 -8
  107. package/dist/forms/index.js.map +1 -1
  108. package/dist/github-signals-YD64R2AF.js +122 -0
  109. package/dist/github-signals-YD64R2AF.js.map +1 -0
  110. package/dist/index.js +102 -4757
  111. package/dist/index.js.map +1 -1
  112. package/dist/init-JITQXFVL.js +85 -0
  113. package/dist/init-JITQXFVL.js.map +1 -0
  114. package/dist/launch-HXHEL6ID.js +207 -0
  115. package/dist/launch-HXHEL6ID.js.map +1 -0
  116. package/dist/lighthouse-airtable-4FKYKFH6.js +12 -0
  117. package/dist/lighthouse-airtable-4FKYKFH6.js.map +1 -0
  118. package/dist/migrate-P2LS4WXI.js +7 -0
  119. package/dist/migrate-P2LS4WXI.js.map +1 -0
  120. package/dist/onboard-TCQOP2W4.js +69 -0
  121. package/dist/onboard-TCQOP2W4.js.map +1 -0
  122. package/dist/orchestrate-RMSBHLQK.js +19 -0
  123. package/dist/orchestrate-RMSBHLQK.js.map +1 -0
  124. package/dist/recipes/sync-configs.js +9 -505
  125. package/dist/recipes/sync-configs.js.map +1 -1
  126. package/dist/renovate-dispatch-ZGLXNPEK.js +97 -0
  127. package/dist/renovate-dispatch-ZGLXNPEK.js.map +1 -0
  128. package/dist/report-SD3CNBOQ.js +240 -0
  129. package/dist/report-SD3CNBOQ.js.map +1 -0
  130. package/dist/resend-G2BJMAQV.js +7 -0
  131. package/dist/resend-G2BJMAQV.js.map +1 -0
  132. package/dist/self-updating-CIUMOMYI.js +61 -0
  133. package/dist/self-updating-CIUMOMYI.js.map +1 -0
  134. package/dist/svelte-codemods-BYRJAU7I.js +51 -0
  135. package/dist/svelte-codemods-BYRJAU7I.js.map +1 -0
  136. package/dist/sync-configs-JPRLAW5J.js +109 -0
  137. package/dist/sync-configs-JPRLAW5J.js.map +1 -0
  138. package/dist/upgrade-ERA45EVR.js +64 -0
  139. package/dist/upgrade-ERA45EVR.js.map +1 -0
  140. package/dist/util/git.js +19 -76
  141. package/dist/util/git.js.map +1 -1
  142. package/dist/util/pkg.js +5 -45
  143. package/dist/util/pkg.js.map +1 -1
  144. package/dist/websites-X5DLC2PC.js +43 -0
  145. package/dist/websites-X5DLC2PC.js.map +1 -0
  146. package/dist/write-audits-to-airtable-CE4POBAF.js +13 -0
  147. package/dist/write-audits-to-airtable-CE4POBAF.js.map +1 -0
  148. package/package.json +13 -12
@@ -1,2328 +1,29 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __esm = (fn, res) => function __init() {
4
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
- };
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
-
11
- // src/util/url.ts
12
- function isHttpUrl(s) {
13
- let parsed;
14
- try {
15
- parsed = new URL(s);
16
- } catch {
17
- return false;
18
- }
19
- return parsed.protocol === "http:" || parsed.protocol === "https:";
20
- }
21
- var init_url = __esm({
22
- "src/util/url.ts"() {
23
- "use strict";
24
- }
25
- });
26
-
27
- // src/util/credentials.ts
28
- import { readFileSync } from "fs";
29
- import { homedir } from "os";
30
- import { join as join7 } from "path";
31
- function defaultCredentialsPath() {
32
- const base = process.env.XDG_CONFIG_HOME ?? join7(homedir(), ".config");
33
- return join7(base, "reddoor-maint", "credentials.env");
34
- }
35
- var init_credentials = __esm({
36
- "src/util/credentials.ts"() {
37
- "use strict";
38
- }
39
- });
40
-
41
- // src/reports/airtable/throttle.ts
42
- function createMinIntervalThrottle(opts) {
43
- const { minIntervalMs, now, delay } = opts;
44
- return function wrap(fn) {
45
- let chain = Promise.resolve();
46
- let last = Number.NEGATIVE_INFINITY;
47
- return (...args) => {
48
- chain = chain.then(async () => {
49
- const wait = minIntervalMs - (now() - last);
50
- if (wait > 0) await delay(wait);
51
- last = now();
52
- fn(...args);
53
- }).catch(() => {
54
- });
55
- };
56
- };
57
- }
58
- function applyThrottle(base, opts) {
59
- const real = base._base?.runAction;
60
- if (typeof real !== "function") return base;
61
- const wrap = createMinIntervalThrottle(opts);
62
- const throttled = wrap(real.bind(base._base));
63
- base._base.runAction = throttled;
64
- base.runAction = throttled;
65
- return base;
66
- }
67
- var init_throttle = __esm({
68
- "src/reports/airtable/throttle.ts"() {
69
- "use strict";
70
- }
71
- });
72
-
73
- // src/reports/airtable/client.ts
74
- var client_exports = {};
75
- __export(client_exports, {
76
- openBase: () => openBase,
77
- readAirtableConfig: () => readAirtableConfig
78
- });
79
- import Airtable from "airtable";
80
- function missing(name) {
81
- return Object.assign(
82
- new Error(
83
- `${name} not set. Export it in your shell or put it in ${defaultCredentialsPath()} as ${name}=...`
84
- ),
85
- { exitCode: 2 }
86
- );
87
- }
88
- function readAirtableConfig() {
89
- const apiKey = process.env.AIRTABLE_PAT;
90
- const baseId = process.env.AIRTABLE_BASE_ID;
91
- if (!apiKey) throw missing("AIRTABLE_PAT");
92
- if (!baseId) throw missing("AIRTABLE_BASE_ID");
93
- return { apiKey, baseId };
94
- }
95
- function openBase(cfg) {
96
- const base = new Airtable({ apiKey: cfg.apiKey }).base(cfg.baseId);
97
- return applyThrottle(base, {
98
- minIntervalMs: MIN_REQUEST_INTERVAL_MS,
99
- now: () => Date.now(),
100
- delay: (ms) => new Promise((resolve3) => setTimeout(resolve3, ms))
101
- });
102
- }
103
- var MIN_REQUEST_INTERVAL_MS;
104
- var init_client = __esm({
105
- "src/reports/airtable/client.ts"() {
106
- "use strict";
107
- init_credentials();
108
- init_throttle();
109
- MIN_REQUEST_INTERVAL_MS = 220;
110
- }
111
- });
112
-
113
- // src/reports/airtable/websites.ts
114
- var websites_exports = {};
115
- __export(websites_exports, {
116
- ACTIVE_STATUSES: () => ACTIVE_STATUSES,
117
- SEVERITY_RANK: () => SEVERITY_RANK,
118
- WEBSITES_TABLE: () => WEBSITES_TABLE,
119
- getWebsiteBySlug: () => getWebsiteBySlug,
120
- isDashboardVisible: () => isDashboardVisible,
121
- listWebsites: () => listWebsites,
122
- mapRow: () => mapRow,
123
- normalizeSecurityAdvisory: () => normalizeSecurityAdvisory,
124
- parseNotifyRouting: () => parseNotifyRouting,
125
- parseSecurityAdvisories: () => parseSecurityAdvisories,
126
- siteSlug: () => siteSlug,
127
- updateA11yCounts: () => updateA11yCounts,
128
- updateAnalyticsHealth: () => updateAnalyticsHealth,
129
- updateAuditFields: () => updateAuditFields,
130
- updateDepsCounts: () => updateDepsCounts,
131
- updateGitHubSignals: () => updateGitHubSignals,
132
- updateLaunched: () => updateLaunched,
133
- updateScores: () => updateScores,
134
- updateSecurityCounts: () => updateSecurityCounts
135
- });
136
- function siteSlug(name) {
137
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
138
- }
139
- function trimToNull(raw) {
140
- if (typeof raw !== "string") return null;
141
- const trimmed = raw.trim();
142
- return trimmed.length > 0 ? trimmed : null;
143
- }
144
- function parseNotifyRouting(raw) {
145
- if (typeof raw !== "string") return null;
146
- const trimmed = raw.trim();
147
- if (!trimmed) return null;
148
- let parsed;
149
- try {
150
- parsed = JSON.parse(trimmed);
151
- } catch {
152
- return null;
153
- }
154
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
155
- const o = parsed;
156
- if (typeof o.field !== "string" || !o.field.trim()) return null;
157
- if (!o.routes || typeof o.routes !== "object" || Array.isArray(o.routes)) return null;
158
- const routing = {
159
- field: o.field,
160
- routes: o.routes
161
- };
162
- if (o.default !== void 0) routing.default = o.default;
163
- if (Array.isArray(o.cc)) routing.cc = o.cc.filter((x) => typeof x === "string");
164
- return routing;
165
- }
166
- function isDashboardVisible(site) {
167
- return site.status !== null && ACTIVE_STATUSES.has(site.status);
168
- }
169
- function toFrequency(raw) {
170
- return typeof raw === "string" && FREQUENCIES.includes(raw) ? raw : "None";
171
- }
172
- function mapRow(rec) {
173
- const f = rec.fields;
174
- const attachments = f["Header image"] ?? [];
175
- const header = attachments[0] ?? null;
176
- return {
177
- id: rec.id,
178
- name: String(f["Name"] ?? ""),
179
- url: String(f["url"] ?? ""),
180
- status: f["Status"] ?? null,
181
- pointOfContact: f["point of contact"] ?? null,
182
- maintenanceFreq: toFrequency(f["maintenence freq"]),
183
- testingFreq: toFrequency(f["testing freq"]),
184
- maintenanceDay: f["maintenance day"] ?? null,
185
- testingDay: f["testing day"] ?? null,
186
- ga4PropertyId: f["GA4 property ID"] ?? null,
187
- searchQuery: f["Search query"] ?? null,
188
- searchConsoleProperty: f["Search Console property"] ?? null,
189
- analyticsSoftFailAt: f["Analytics soft-fail at"] ?? null,
190
- gitRepo: f["Git repo"] ?? null,
191
- reportRecipientsTo: f["Report recipients (To)"] ?? null,
192
- reportRecipientsCc: f["Report recipients (CC)"] ?? null,
193
- headerImage: header,
194
- pScore: f["pScore"] ?? null,
195
- rScore: f["rScore"] ?? null,
196
- bpScore: f["bpScore"] ?? null,
197
- seoScore: f["seoScore"] ?? null,
198
- lastLighthouseAuditAt: f["Last lighthouse audit at"] ?? null,
199
- a11yViolations: f["A11y Violations"] ?? null,
200
- depsDrifted: f["Deps Drifted"] ?? null,
201
- depsMajorBehind: f["Deps Major Behind"] ?? null,
202
- depsOutdated: f["Deps Outdated"] ?? null,
203
- securityVulnsCritical: f["Security Vulns Critical"] ?? null,
204
- securityVulnsHigh: f["Security Vulns High"] ?? null,
205
- securityVulnsModerate: f["Security Vulns Moderate"] ?? null,
206
- securityVulnsLow: f["Security Vulns Low"] ?? null,
207
- lastSecurityAuditAt: f["Last security audit at"] ?? null,
208
- securityAdvisories: parseSecurityAdvisories(f["Security advisories"]),
209
- certDaysRemaining: f["Cert days remaining"] ?? null,
210
- domainCheckedAt: f["Domain checked at"] ?? null,
211
- crossbrowserOk: typeof f["Crossbrowser OK"] === "boolean" ? f["Crossbrowser OK"] : null,
212
- mobileOk: typeof f["Mobile OK"] === "boolean" ? f["Mobile OK"] : null,
213
- linksOk: typeof f["Links OK"] === "boolean" ? f["Links OK"] : null,
214
- brokenLinks: typeof f["Broken links"] === "number" ? f["Broken links"] : null,
215
- browserCheckedAt: f["Browser checked at"] ?? null,
216
- copyIntro: trimToNull(f["Copy \u2014 Intro"]),
217
- copyContact: trimToNull(f["Copy \u2014 Contact"]),
218
- copyFooter: trimToNull(f["Copy \u2014 Footer"]),
219
- launchedAt: f["Launched at"] ?? null,
220
- newsletterWebhook: trimToNull(f["Newsletter Webhook"]),
221
- notifyRouting: parseNotifyRouting(f["Notify Routing"]),
222
- mailchimpApiKey: trimToNull(f["Mailchimp API Key"]),
223
- mailchimpAudienceId: trimToNull(f["Mailchimp Audience ID"]),
224
- renovateFailingCis: f["Renovate Failing CIs"] ?? null,
225
- defaultBranchCi: f["Default Branch CI"] ?? null,
226
- lastCommitAt: f["Last Commit At"] ?? null,
227
- githubSignalsAt: f["GitHub Signals At"] ?? null
228
- };
229
- }
230
- async function listWebsites(base) {
231
- const out = [];
232
- await base(WEBSITES_TABLE).select({ pageSize: 100 }).eachPage((records, fetchNextPage) => {
233
- for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));
234
- fetchNextPage();
235
- });
236
- return out;
237
- }
238
- async function getWebsiteBySlug(base, slug) {
239
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) return null;
240
- const formula = `REGEX_REPLACE(REGEX_REPLACE(LOWER({Name}),"[^a-z0-9]+","-"),"^-|-$","")=${JSON.stringify(
241
- slug
242
- )}`;
243
- const rows = [];
244
- await base(WEBSITES_TABLE).select({ filterByFormula: formula, maxRecords: 1 }).eachPage((records, fetchNextPage) => {
245
- for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));
246
- fetchNextPage();
247
- });
248
- return rows.find((w) => siteSlug(w.name) === slug) ?? null;
249
- }
250
- function scoreFields(scores) {
251
- return {
252
- pScore: scores.performance,
253
- rScore: scores.accessibility,
254
- bpScore: scores.bestPractices,
255
- seoScore: scores.seo,
256
- "Last lighthouse audit at": (/* @__PURE__ */ new Date()).toISOString()
257
- };
258
- }
259
- function a11yFields(counts) {
260
- return { "A11y Violations": counts.violations };
261
- }
262
- function depsFields(counts) {
263
- const fields = {
264
- "Deps Drifted": counts.drifted,
265
- "Deps Major Behind": counts.majorBehind
266
- };
267
- if (counts.outdated !== null) {
268
- fields["Deps Outdated"] = counts.outdated;
269
- }
270
- return fields;
271
- }
272
- function normalizeSecurityAdvisory(raw) {
273
- if (!raw || typeof raw !== "object") return null;
274
- const e = raw;
275
- const module = typeof e["module"] === "string" ? e["module"] : null;
276
- const severity = e["severity"];
277
- if (module === null) return null;
278
- if (severity !== "low" && severity !== "moderate" && severity !== "high" && severity !== "critical")
279
- return null;
280
- const cves = Array.isArray(e["cves"]) ? e["cves"].filter((c) => typeof c === "string") : [];
281
- return {
282
- module,
283
- severity,
284
- title: typeof e["title"] === "string" ? e["title"] : "",
285
- cves,
286
- url: typeof e["url"] === "string" ? e["url"] : null
287
- };
288
- }
289
- function parseSecurityAdvisories(raw) {
290
- if (typeof raw !== "string" || raw.trim() === "") return null;
291
- let parsed;
292
- try {
293
- parsed = JSON.parse(raw);
294
- } catch {
295
- return null;
296
- }
297
- if (!Array.isArray(parsed)) return null;
298
- return parsed.map(normalizeSecurityAdvisory).filter((a) => a !== null);
299
- }
300
- function securityAdvisoryFields(advisories) {
301
- const capped = [...advisories].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]).slice(0, MAX_PERSISTED_ADVISORIES);
302
- return { "Security advisories": JSON.stringify(capped) };
303
- }
304
- function securityFields(counts) {
305
- return {
306
- "Security Vulns Critical": counts.critical,
307
- "Security Vulns High": counts.high,
308
- "Security Vulns Moderate": counts.moderate,
309
- "Security Vulns Low": counts.low,
310
- // Stamp freshness alongside the counts so the Security Updates auto-tick can require a recent
311
- // audit (a clean count from months ago must not silently keep ticking the box).
312
- "Last security audit at": (/* @__PURE__ */ new Date()).toISOString()
313
- };
314
- }
315
- function domainFields(result) {
316
- const fields = { "Domain checked at": result.checkedAt };
317
- fields["Cert days remaining"] = result.certDaysRemaining;
318
- return fields;
319
- }
320
- function browserFields(r) {
321
- return {
322
- "Crossbrowser OK": r.desktopOk,
323
- "Mobile OK": r.mobileOk,
324
- "Links OK": r.linksOk,
325
- "Broken links": r.brokenLinks,
326
- "Browser checked at": r.checkedAt
327
- };
328
- }
329
- async function updateScores(base, recordId, scores) {
330
- await base(WEBSITES_TABLE).update([{ id: recordId, fields: scoreFields(scores) }]);
331
- }
332
- async function updateAnalyticsHealth(base, recordId, at) {
333
- const fields = { "Analytics soft-fail at": at };
334
- await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
335
- }
336
- async function updateA11yCounts(base, recordId, counts) {
337
- await base(WEBSITES_TABLE).update([{ id: recordId, fields: a11yFields(counts) }]);
338
- }
339
- async function updateDepsCounts(base, recordId, counts) {
340
- await base(WEBSITES_TABLE).update([{ id: recordId, fields: depsFields(counts) }]);
341
- }
342
- async function updateSecurityCounts(base, recordId, counts) {
343
- await base(WEBSITES_TABLE).update([{ id: recordId, fields: securityFields(counts) }]);
344
- }
345
- async function updateAuditFields(base, recordId, audits) {
346
- const fields = {};
347
- if (audits.scores) Object.assign(fields, scoreFields(audits.scores));
348
- if (audits.a11y) Object.assign(fields, a11yFields(audits.a11y));
349
- if (audits.deps) Object.assign(fields, depsFields(audits.deps));
350
- if (audits.security) Object.assign(fields, securityFields(audits.security));
351
- if (audits.securityAdvisories)
352
- Object.assign(fields, securityAdvisoryFields(audits.securityAdvisories));
353
- if (audits.domain) Object.assign(fields, domainFields(audits.domain));
354
- if (audits.browser) Object.assign(fields, browserFields(audits.browser));
355
- await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
356
- return fields;
357
- }
358
- async function updateGitHubSignals(base, recordId, signals) {
359
- const fields = {
360
- "Renovate Failing CIs": signals.renovateFailingCis,
361
- "Default Branch CI": signals.ciState,
362
- "GitHub Signals At": signals.sweptAt
363
- };
364
- if (signals.lastCommitAt !== null) {
365
- fields["Last Commit At"] = signals.lastCommitAt;
366
- }
367
- await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
368
- }
369
- async function updateLaunched(base, recordId, at) {
370
- const fields = { Status: "maintenance", "Launched at": at };
371
- await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
372
- }
373
- var WEBSITES_TABLE, ACTIVE_STATUSES, FREQUENCIES, SEVERITY_RANK, MAX_PERSISTED_ADVISORIES;
374
- var init_websites = __esm({
375
- "src/reports/airtable/websites.ts"() {
376
- "use strict";
377
- WEBSITES_TABLE = "Websites";
378
- ACTIVE_STATUSES = /* @__PURE__ */ new Set([
379
- "maintenance",
380
- "launch period"
381
- ]);
382
- FREQUENCIES = ["None", "Monthly", "Quarterly", "Yearly"];
383
- SEVERITY_RANK = {
384
- critical: 0,
385
- high: 1,
386
- moderate: 2,
387
- low: 3
388
- };
389
- MAX_PERSISTED_ADVISORIES = 25;
390
- }
391
- });
392
-
393
- // src/inventory/airtable.ts
394
- var airtable_exports = {};
395
- __export(airtable_exports, {
396
- fromAirtableBase: () => fromAirtableBase
397
- });
398
- function fromAirtableBase(base, opts = {}) {
399
- return async () => {
400
- const workdir = opts.workdir ?? process.env.REDDOOR_FLEET_WORKDIR;
401
- if (!workdir) {
402
- throw new Error(
403
- "fromAirtableBase requires `workdir` option or REDDOOR_FLEET_WORKDIR env (sites need a local path)"
404
- );
405
- }
406
- const websites = await listWebsites(base);
407
- return websites.filter((w) => w.status !== null && ACTIVE_STATUSES.has(w.status) && w.url.length > 0).flatMap((w) => {
408
- const slug = siteSlug(w.name);
409
- if (slug.length === 0) {
410
- console.warn(
411
- `[inventory] skipping "${w.name}" (row ${w.id}): Name has no slug-able characters (empty slug)`
412
- );
413
- return [];
414
- }
415
- const site = {
416
- path: `${workdir}/${slug}`,
417
- name: slug,
418
- meta: { airtableRowId: w.id, displayName: w.name }
419
- };
420
- if (isHttpUrl(w.url)) {
421
- site.deployedUrl = w.url;
422
- } else {
423
- console.warn(
424
- `[inventory] skipping deployed audit for "${w.name}": url is not http(s): ${JSON.stringify(w.url)}`
425
- );
426
- }
427
- if (w.gitRepo) site.gitRepo = w.gitRepo;
428
- return [site];
429
- });
430
- };
431
- }
432
- var init_airtable = __esm({
433
- "src/inventory/airtable.ts"() {
434
- "use strict";
435
- init_websites();
436
- init_url();
437
- }
438
- });
439
-
440
- // src/audits/lighthouse-airtable.ts
441
- var lighthouse_airtable_exports = {};
442
- __export(lighthouse_airtable_exports, {
443
- hasRealScores: () => hasRealScores,
444
- lighthouseScoresFromResult: () => lighthouseScoresFromResult,
445
- resolveSlugFromCwd: () => resolveSlugFromCwd
446
- });
447
- import { readFile as readFile7 } from "fs/promises";
448
- import { join as join10 } from "path";
449
- function hasRealScores(result) {
450
- if (result.audit !== "lighthouse") return false;
451
- const details = result.details ?? {};
452
- const summary = details.summary ?? {};
453
- return LIGHTHOUSE_CATEGORIES.some(
454
- (k) => typeof summary[k] === "number" && !Number.isNaN(summary[k])
455
- );
456
- }
457
- function lighthouseScoresFromResult(result) {
458
- if (result.audit !== "lighthouse") {
459
- throw new Error(`Expected a 'lighthouse' AuditResult, got '${result.audit}'`);
460
- }
461
- const details = result.details ?? {};
462
- const summary = details.summary ?? {};
463
- const toPct = (n) => typeof n === "number" && !Number.isNaN(n) ? Math.round(n * 100) : 0;
464
- return {
465
- performance: toPct(summary["performance"]),
466
- accessibility: toPct(summary["accessibility"]),
467
- bestPractices: toPct(summary["best-practices"]),
468
- seo: toPct(summary["seo"])
469
- };
470
- }
471
- async function resolveSlugFromCwd(cwd) {
472
- try {
473
- const pkgPath = join10(cwd, "package.json");
474
- const raw = await readFile7(pkgPath, "utf-8");
475
- const pkg = JSON.parse(raw);
476
- if (!pkg.name) throw new Error("package.json has no 'name' field");
477
- return siteSlug(pkg.name);
478
- } catch (e) {
479
- throw new Error(
480
- `Could not derive site slug from ${cwd}/package.json: ${e.message}. Pass --write-airtable=<slug> explicitly.`,
481
- { cause: e }
482
- );
483
- }
484
- }
485
- var LIGHTHOUSE_CATEGORIES;
486
- var init_lighthouse_airtable = __esm({
487
- "src/audits/lighthouse-airtable.ts"() {
488
- "use strict";
489
- init_websites();
490
- LIGHTHOUSE_CATEGORIES = ["performance", "accessibility", "best-practices", "seo"];
491
- }
492
- });
493
-
494
- // src/audits/a11y-airtable.ts
495
- function hasA11yCounts(result) {
496
- if (result.audit !== "a11y") return false;
497
- const details = result.details;
498
- return typeof details?.totalViolations === "number";
499
- }
500
- function a11yCountsFromResult(result) {
501
- if (result.audit !== "a11y") {
502
- throw new Error(`Expected an 'a11y' AuditResult, got '${result.audit}'`);
503
- }
504
- const details = result.details;
505
- return { violations: details?.totalViolations ?? 0 };
506
- }
507
- var init_a11y_airtable = __esm({
508
- "src/audits/a11y-airtable.ts"() {
509
- "use strict";
510
- }
511
- });
512
-
513
- // src/audits/deps-airtable.ts
514
- function hasDepsCounts(result) {
515
- if (result.audit !== "deps") return false;
516
- const d = result.details;
517
- return Array.isArray(d?.entries);
518
- }
519
- function depsCountsFromResult(result) {
520
- if (result.audit !== "deps") {
521
- throw new Error(`Expected a 'deps' AuditResult, got '${result.audit}'`);
522
- }
523
- const details = result.details ?? {};
524
- const entries = details.entries ?? [];
525
- const drifted = entries.filter((e) => e.drift !== "same").length;
526
- const majorBehind = entries.filter((e) => e.drift === "major").length;
527
- const outdated = details.outdated?.outdated ?? null;
528
- return { drifted, majorBehind, outdated };
529
- }
530
- var init_deps_airtable = __esm({
531
- "src/audits/deps-airtable.ts"() {
532
- "use strict";
533
- }
534
- });
535
-
536
- // src/audits/security-airtable.ts
537
- function hasSecurityCounts(result) {
538
- if (result.audit !== "security") return false;
539
- const details = result.details;
540
- return !!details && typeof details.counts === "object";
541
- }
542
- function securityCountsFromResult(result) {
543
- if (result.audit !== "security") {
544
- throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);
545
- }
546
- const details = result.details;
547
- const c = details?.counts ?? { low: 0, moderate: 0, high: 0, critical: 0 };
548
- return { critical: c.critical, high: c.high, moderate: c.moderate, low: c.low };
549
- }
550
- function advisoriesFromResult(result) {
551
- if (result.audit !== "security") {
552
- throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);
553
- }
554
- const details = result.details;
555
- const raw = details?.advisories;
556
- if (!Array.isArray(raw)) return [];
557
- return raw.map(normalizeSecurityAdvisory).filter((a) => a !== null);
558
- }
559
- var init_security_airtable = __esm({
560
- "src/audits/security-airtable.ts"() {
561
- "use strict";
562
- init_websites();
563
- }
564
- });
565
-
566
- // src/audits/domain-airtable.ts
567
- function hasDomainResult(result) {
568
- if (result.audit !== "domain") return false;
569
- const d = result.details;
570
- return !!d && typeof d.checkedAt === "string";
571
- }
572
- function domainResultFromAudit(result) {
573
- if (result.audit !== "domain") {
574
- throw new Error(`Expected a 'domain' AuditResult, got '${result.audit}'`);
575
- }
576
- const d = result.details;
577
- return {
578
- certDaysRemaining: typeof d?.certDaysRemaining === "number" ? d.certDaysRemaining : null,
579
- checkedAt: typeof d?.checkedAt === "string" ? d.checkedAt : (/* @__PURE__ */ new Date()).toISOString()
580
- };
581
- }
582
- var init_domain_airtable = __esm({
583
- "src/audits/domain-airtable.ts"() {
584
- "use strict";
585
- }
586
- });
587
-
588
- // src/audits/browser-airtable.ts
589
- function hasBrowserResult(result) {
590
- if (result.audit !== "browser") return false;
591
- const d = result.details;
592
- return !!d && typeof d.checkedAt === "string";
593
- }
594
- function browserFieldsFromAudit(result) {
595
- if (result.audit !== "browser") {
596
- throw new Error(`Expected a 'browser' AuditResult, got '${result.audit}'`);
597
- }
598
- const d = result.details;
599
- return {
600
- desktopOk: d?.desktopOk === true,
601
- mobileOk: d?.mobileOk === true,
602
- linksOk: d?.linksOk === true,
603
- brokenLinks: typeof d?.brokenLinks === "number" ? d.brokenLinks : 0,
604
- checkedAt: typeof d?.checkedAt === "string" ? d.checkedAt : (/* @__PURE__ */ new Date()).toISOString()
605
- };
606
- }
607
- var init_browser_airtable = __esm({
608
- "src/audits/browser-airtable.ts"() {
609
- "use strict";
610
- }
611
- });
612
-
613
- // src/audits/write-audits-to-airtable.ts
614
- var write_audits_to_airtable_exports = {};
615
- __export(write_audits_to_airtable_exports, {
616
- formatFleetWriteSummary: () => formatFleetWriteSummary,
617
- writeAuditsToAirtable: () => writeAuditsToAirtable,
618
- writeFleetAuditsToAirtable: () => writeFleetAuditsToAirtable
619
- });
620
- async function writeAuditsToAirtable(args) {
621
- const { base, websites, slug, results } = args;
622
- const lhResult = results.find((r) => r.audit === "lighthouse");
623
- const target = websites.find((w) => siteSlug(w.name) === slug);
624
- if (!target) {
625
- throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
626
- }
627
- const writes = [];
628
- const audits = {};
629
- const lhHasScores = lhResult ? hasRealScores(lhResult) : false;
630
- if (lhResult && lhHasScores) {
631
- const scores = lighthouseScoresFromResult(lhResult);
632
- audits.scores = scores;
633
- writes.push({ audit: "lighthouse", counts: scores });
634
- }
635
- const a11y = results.find((r) => r.audit === "a11y");
636
- if (a11y && hasA11yCounts(a11y)) {
637
- const counts = a11yCountsFromResult(a11y);
638
- audits.a11y = counts;
639
- writes.push({ audit: "a11y", counts });
640
- }
641
- const deps = results.find((r) => r.audit === "deps");
642
- if (deps && hasDepsCounts(deps)) {
643
- const counts = depsCountsFromResult(deps);
644
- audits.deps = counts;
645
- writes.push({ audit: "deps", counts });
646
- }
647
- const sec = results.find((r) => r.audit === "security");
648
- if (sec && hasSecurityCounts(sec)) {
649
- const counts = securityCountsFromResult(sec);
650
- audits.security = counts;
651
- audits.securityAdvisories = advisoriesFromResult(sec);
652
- writes.push({ audit: "security", counts });
653
- }
654
- const dom = results.find((r) => r.audit === "domain");
655
- if (dom && hasDomainResult(dom)) {
656
- const result = domainResultFromAudit(dom);
657
- audits.domain = result;
658
- writes.push({ audit: "domain", counts: result });
659
- }
660
- const browser = results.find((r) => r.audit === "browser");
661
- if (browser && hasBrowserResult(browser)) {
662
- const fields = browserFieldsFromAudit(browser);
663
- audits.browser = fields;
664
- writes.push({ audit: "browser", counts: fields });
665
- }
666
- if (Object.keys(audits).length > 0) {
667
- await updateAuditFields(base, target.id, audits);
668
- }
669
- if (lhResult && !lhHasScores) {
670
- const persisted = writes.map((w) => w.audit);
671
- throw Object.assign(
672
- new Error(
673
- `Lighthouse audit produced no scores; ${persisted.length ? `wrote ${persisted.join("/")} but refused Lighthouse` : "wrote nothing"}. Summary: ${lhResult.summary}`
674
- ),
675
- { exitCode: 1 }
676
- );
677
- }
678
- return { siteName: target.name, writes };
679
- }
680
- function formatFleetWriteSummary(result) {
681
- const wrote = result.written.length;
682
- const failed = result.failed.length;
683
- const total = wrote + failed;
684
- let out = `\u2192 wrote ${wrote} site(s) to Airtable`;
685
- if (failed > 0) {
686
- out += `
687
- \u26A0 ${failed} site(s) not written: ${result.failed.map((f) => `${f.slug} (${f.error})`).join("; ")}`;
688
- }
689
- out += `
690
- FLEET_WRITE_SUMMARY wrote=${wrote} failed=${failed} total=${total}`;
691
- return out;
692
- }
693
- async function writeFleetAuditsToAirtable(args) {
694
- const { base, websites, results } = args;
695
- const bySlug = /* @__PURE__ */ new Map();
696
- for (const r of results) {
697
- const arr = bySlug.get(r.site) ?? [];
698
- arr.push(r);
699
- bySlug.set(r.site, arr);
700
- }
701
- const written = [];
702
- const failed = [];
703
- for (const [slug, siteResults] of bySlug) {
704
- try {
705
- written.push(await writeAuditsToAirtable({ base, websites, slug, results: siteResults }));
706
- } catch (e) {
707
- failed.push({ slug, error: e.message });
708
- }
709
- }
710
- return { written, failed };
711
- }
712
- var init_write_audits_to_airtable = __esm({
713
- "src/audits/write-audits-to-airtable.ts"() {
714
- "use strict";
715
- init_websites();
716
- init_lighthouse_airtable();
717
- init_a11y_airtable();
718
- init_deps_airtable();
719
- init_security_airtable();
720
- init_domain_airtable();
721
- init_browser_airtable();
722
- }
723
- });
1
+ import {
2
+ fleetWorkdir,
3
+ formatSkippedNotice,
4
+ prepareFleetSites
5
+ } from "../../chunk-4NAOR44V.js";
6
+ import {
7
+ resolveSites
8
+ } from "../../chunk-J3OPWFZE.js";
9
+ import {
10
+ ALL_AUDIT_NAMES,
11
+ runOneAudit
12
+ } from "../../chunk-4PY4I5OR.js";
13
+ import "../../chunk-LKGVSM2O.js";
14
+ import "../../chunk-FDIFTIXC.js";
15
+ import "../../chunk-HVOOCK6L.js";
16
+ import "../../chunk-XXTZBPUY.js";
17
+ import "../../chunk-XTK5VIQB.js";
18
+ import {
19
+ isHttpUrl
20
+ } from "../../chunk-U5SZ4FOP.js";
21
+ import "../../chunk-EYODPUHV.js";
22
+ import "../../chunk-LYCKN7ZR.js";
724
23
 
725
24
  // src/cli/commands/audit.ts
726
- import { resolve as resolve2 } from "path";
25
+ import { resolve } from "path";
727
26
  import { Listr } from "listr2";
728
-
729
- // src/audits/util/spawn.ts
730
- import { spawn } from "child_process";
731
- import { StringDecoder } from "string_decoder";
732
- var TRUNCATION_MARKER = "\n\u2026[output truncated]";
733
- function makeSpawn(internals = {}) {
734
- const spawnImpl = internals.spawnImpl ?? spawn;
735
- const killImpl = internals.killImpl ?? ((pid, sig) => process.kill(pid, sig));
736
- const killGraceMs = internals.killGraceMs ?? 5e3;
737
- const maxOutputBytes = internals.maxOutputBytes ?? 10 * 1024 * 1024;
738
- return (cmd, args, opts = {}) => new Promise((resolve3, reject) => {
739
- const streaming = opts.streaming === true;
740
- const child = spawnImpl(cmd, [...args], {
741
- cwd: opts.cwd,
742
- env: opts.env ?? process.env,
743
- stdio: streaming ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"],
744
- // Detach ONLY when a timeout can fire: the child then leads its own
745
- // process group, so the timeout can kill the WHOLE tree (vite, and
746
- // Chromium under lhci/playwright) via process.kill(-pid), not just the
747
- // npx/pnpm wrapper. Without it, killing the wrapper orphaned the
748
- // grandchildren — a zombie vite squatting its port, Chrome left running.
749
- // We do NOT detach timeout-less streaming calls (pnpm install/up):
750
- // detaching gains nothing there (no timeout → no group-kill) and would
751
- // break terminal Ctrl-C, which only reaches the foreground group — i.e.
752
- // it would re-orphan the very children this guards. We never unref() the
753
- // child since we still await it.
754
- detached: opts.timeoutMs !== void 0
755
- });
756
- const cap = (acc, chunk) => {
757
- if (acc.length >= maxOutputBytes) return acc;
758
- const next = acc + chunk;
759
- return next.length > maxOutputBytes ? next.slice(0, maxOutputBytes) + TRUNCATION_MARKER : next;
760
- };
761
- let stdout = "";
762
- let stderr = "";
763
- const outDecoder = new StringDecoder("utf-8");
764
- const errDecoder = new StringDecoder("utf-8");
765
- if (!streaming) {
766
- child.stdout?.on(
767
- "data",
768
- (chunk) => stdout = cap(stdout, outDecoder.write(chunk))
769
- );
770
- child.stderr?.on(
771
- "data",
772
- (chunk) => stderr = cap(stderr, errDecoder.write(chunk))
773
- );
774
- }
775
- const killGroup = (sig) => {
776
- if (child.pid === void 0) return;
777
- try {
778
- killImpl(-child.pid, sig);
779
- } catch {
780
- }
781
- };
782
- let killTimer;
783
- const timer = opts.timeoutMs ? setTimeout(() => {
784
- killGroup("SIGTERM");
785
- killTimer = setTimeout(() => killGroup("SIGKILL"), killGraceMs);
786
- killTimer.unref();
787
- reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));
788
- }, opts.timeoutMs) : void 0;
789
- const clearTimers = () => {
790
- if (timer) clearTimeout(timer);
791
- if (killTimer) clearTimeout(killTimer);
792
- };
793
- child.on("error", (err) => {
794
- clearTimers();
795
- reject(err);
796
- });
797
- child.on("close", (code) => {
798
- clearTimers();
799
- if (!streaming) {
800
- stdout = cap(stdout, outDecoder.end());
801
- stderr = cap(stderr, errDecoder.end());
802
- }
803
- resolve3({ code: code ?? -1, stdout, stderr });
804
- });
805
- });
806
- }
807
- var defaultSpawn = makeSpawn();
808
-
809
- // src/audits/deps.ts
810
- import { readFile } from "fs/promises";
811
- import { join as join2 } from "path";
812
-
813
- // src/util/site.ts
814
- function siteLabel(site) {
815
- return site.name || site.path;
816
- }
817
-
818
- // src/configs/baseline-versions.ts
819
- var baselineVersions = {
820
- // SvelteKit core
821
- svelte: "^5.55.10",
822
- "@sveltejs/kit": "^2.61.1",
823
- "@sveltejs/adapter-netlify": "^6.0.4",
824
- "@sveltejs/adapter-auto": "^7.0.1",
825
- "@sveltejs/vite-plugin-svelte": "^7.1.2",
826
- "svelte-check": "^4.4.8",
827
- // Build tooling
828
- vite: "^8.0.14",
829
- vitest: "^4.1.7",
830
- typescript: "^6.0.3",
831
- // Tailwind 4
832
- tailwindcss: "^4.3.0",
833
- "@tailwindcss/vite": "^4.3.0",
834
- // Prismic
835
- "@prismicio/client": "^7.21.8",
836
- "@prismicio/svelte": "^2.2.1",
837
- "@slicemachine/adapter-sveltekit": "^0.3.96",
838
- "slice-machine-ui": "^2.21.3",
839
- // Test tooling
840
- "@playwright/test": "^1.60.0",
841
- "@axe-core/playwright": "^4.11.3",
842
- "@lhci/cli": "^0.15.1",
843
- // Lint
844
- eslint: "^10.4.0",
845
- "eslint-plugin-svelte": "^3.18.0",
846
- "eslint-config-prettier": "^10.1.8",
847
- prettier: "^3.8.3",
848
- "prettier-plugin-svelte": "^4.0.1",
849
- "typescript-eslint": "^8.60.0",
850
- "@eslint/js": "^10.0.1",
851
- globals: "^17.6.0",
852
- // Misc
853
- "@lucide/svelte": "^1.17.0",
854
- "@zerodevx/svelte-img": "^2.1.2"
855
- };
856
-
857
- // src/audits/deps-outdated.ts
858
- import { stat } from "fs/promises";
859
- import { join } from "path";
860
- async function exists(path) {
861
- try {
862
- await stat(path);
863
- return true;
864
- } catch {
865
- return false;
866
- }
867
- }
868
- function majorOf(version) {
869
- const head = version.replace(/^[\^~]/, "").split(".")[0] ?? "0";
870
- const n = Number.parseInt(head, 10);
871
- return Number.isNaN(n) ? 0 : n;
872
- }
873
- async function scanOutdated(sitePath, spawn2) {
874
- if (!await exists(join(sitePath, "pnpm-lock.yaml"))) return null;
875
- try {
876
- if (!await exists(join(sitePath, "node_modules"))) {
877
- const install = await spawn2("pnpm", ["install", "--frozen-lockfile"], {
878
- cwd: sitePath,
879
- timeoutMs: 18e4
880
- });
881
- if (install.code !== 0) return null;
882
- }
883
- const res = await spawn2("pnpm", ["outdated", "--json"], {
884
- cwd: sitePath,
885
- timeoutMs: 6e4
886
- });
887
- const parsed = JSON.parse(res.stdout || "{}");
888
- const entries = Object.values(parsed);
889
- return {
890
- outdated: entries.length,
891
- major: entries.filter((e) => e.current && e.latest && majorOf(e.latest) > majorOf(e.current)).length
892
- };
893
- } catch {
894
- return null;
895
- }
896
- }
897
-
898
- // src/audits/deps.ts
899
- function stripCaret(range) {
900
- return range.replace(/^[\^~]/, "");
901
- }
902
- function isComparableRange(spec) {
903
- return /^[\^~]?\d/.test(spec.trim());
904
- }
905
- function parseSemver(v) {
906
- const cleaned = stripCaret(v).split("-")[0] ?? "0.0.0";
907
- const parts = cleaned.split(".").map((n) => Number.parseInt(n, 10));
908
- return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
909
- }
910
- function compareSemver(actual, baseline) {
911
- const [aMajor, aMinor, aPatch] = parseSemver(actual);
912
- const [bMajor, bMinor, bPatch] = parseSemver(baseline);
913
- if (aMajor > bMajor) return "newer";
914
- if (aMajor < bMajor) return "major";
915
- if (aMinor > bMinor) return "newer";
916
- if (aMinor < bMinor) return "minor";
917
- if (aPatch > bPatch) return "newer";
918
- if (aPatch < bPatch) return "patch";
919
- return "same";
920
- }
921
- async function depsAudit(ctx) {
922
- const pkgPath = join2(ctx.site.path, "package.json");
923
- let pkgRaw;
924
- try {
925
- pkgRaw = await readFile(pkgPath, "utf-8");
926
- } catch (err) {
927
- return {
928
- audit: "deps",
929
- site: siteLabel(ctx.site),
930
- status: "skip",
931
- summary: `no package.json at ${pkgPath}`,
932
- details: { error: String(err) }
933
- };
934
- }
935
- let pkg;
936
- try {
937
- pkg = JSON.parse(pkgRaw);
938
- } catch (err) {
939
- return {
940
- audit: "deps",
941
- site: siteLabel(ctx.site),
942
- status: "fail",
943
- summary: `package.json is not valid JSON: ${err.message}`,
944
- details: { error: String(err) }
945
- };
946
- }
947
- const installed = {
948
- ...pkg.dependencies ?? {},
949
- ...pkg.devDependencies ?? {}
950
- };
951
- const entries = [];
952
- for (const [name, baseline] of Object.entries(baselineVersions)) {
953
- const actual = installed[name];
954
- if (!actual) continue;
955
- if (!isComparableRange(actual)) continue;
956
- entries.push({
957
- pkg: name,
958
- baseline,
959
- actual,
960
- drift: compareSemver(actual, baseline)
961
- });
962
- }
963
- const anyMajor = entries.some((d) => d.drift === "major");
964
- const anyMinor = entries.some((d) => d.drift === "minor");
965
- const anyNewer = entries.some((d) => d.drift === "newer");
966
- const status = anyMajor ? "fail" : anyMinor || anyNewer ? "warn" : "pass";
967
- const driftSummary = status === "pass" ? `all ${entries.length} tracked deps in line with baseline` : status === "warn" ? `${entries.filter((d) => d.drift !== "same").length} of ${entries.length} tracked deps drifted` : `${entries.filter((d) => d.drift === "major").length} deps lagging by a major version`;
968
- const outdated = await scanOutdated(ctx.site.path, ctx.spawn ?? defaultSpawn);
969
- const summary = outdated ? `${driftSummary}; ${outdated.outdated} outdated install(s) (${outdated.major} major)` : driftSummary;
970
- return {
971
- audit: "deps",
972
- site: siteLabel(ctx.site),
973
- status,
974
- summary,
975
- details: { entries, outdated }
976
- };
977
- }
978
-
979
- // src/audits/lint.ts
980
- import { existsSync } from "fs";
981
- import { readFile as readFile2 } from "fs/promises";
982
- import { join as join3 } from "path";
983
- import { ESLint } from "eslint";
984
- import { check as prettierCheck, resolveConfig as prettierResolveConfig } from "prettier";
985
- import { glob } from "tinyglobby";
986
- var TARGET_GLOBS = ["**/*.{ts,js,svelte}"];
987
- var IGNORE = ["node_modules/**", "dist/**", ".svelte-kit/**", "build/**", ".netlify/**"];
988
- async function listFiles(cwd) {
989
- return glob(TARGET_GLOBS, { cwd, ignore: IGNORE, absolute: false });
990
- }
991
- async function lintAudit(ctx) {
992
- const { site } = ctx;
993
- const configPath = join3(site.path, "eslint.config.js");
994
- if (!existsSync(configPath)) {
995
- return {
996
- audit: "lint",
997
- site: siteLabel(site),
998
- status: "skip",
999
- summary: "no eslint config at site root"
1000
- };
1001
- }
1002
- const eslint = new ESLint({
1003
- cwd: site.path,
1004
- overrideConfigFile: configPath,
1005
- errorOnUnmatchedPattern: false
1006
- });
1007
- const relFiles = await listFiles(site.path);
1008
- const eslintResults = await eslint.lintFiles(relFiles);
1009
- const eslintErrors = eslintResults.reduce((n, r) => n + r.errorCount, 0);
1010
- const eslintWarnings = eslintResults.reduce((n, r) => n + r.warningCount, 0);
1011
- const prettierUnformatted = [];
1012
- for (const rel of relFiles) {
1013
- const absForResolve = join3(site.path, rel);
1014
- const source = await readFile2(absForResolve, "utf-8");
1015
- const options = await prettierResolveConfig(absForResolve) ?? {};
1016
- const ok = await prettierCheck(source, { ...options, filepath: absForResolve });
1017
- if (!ok) prettierUnformatted.push(rel);
1018
- }
1019
- const status = eslintErrors > 0 || prettierUnformatted.length > 0 ? "fail" : eslintWarnings > 0 ? "warn" : "pass";
1020
- const summary = status === "pass" ? `lint clean across ${relFiles.length} files` : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;
1021
- return {
1022
- audit: "lint",
1023
- site: siteLabel(site),
1024
- status,
1025
- summary,
1026
- details: {
1027
- eslintErrors,
1028
- eslintWarnings,
1029
- prettierUnformatted,
1030
- files: relFiles.length
1031
- }
1032
- };
1033
- }
1034
-
1035
- // src/audits/security.ts
1036
- function classify(v) {
1037
- if (v.critical > 0 || v.high > 0) return "fail";
1038
- if (v.moderate > 0 || v.low > 0) return "warn";
1039
- return "pass";
1040
- }
1041
- function normalizeSeverity(s) {
1042
- if (s === "low" || s === "moderate" || s === "high" || s === "critical") return s;
1043
- return "low";
1044
- }
1045
- function extractAdvisoriesFromPnpm(parsed) {
1046
- const out = [];
1047
- for (const a of Object.values(parsed.advisories ?? {})) {
1048
- if (!a) continue;
1049
- out.push({
1050
- module: a.module_name ?? "unknown",
1051
- severity: normalizeSeverity(a.severity),
1052
- title: a.title ?? "(no title)",
1053
- ...a.cves ? { cves: a.cves } : {},
1054
- ...a.url ? { url: a.url } : {}
1055
- });
1056
- }
1057
- return out;
1058
- }
1059
- function resolveNpmAdvisoryRoot(startName, vulnerabilities) {
1060
- const seen = /* @__PURE__ */ new Set();
1061
- let current = startName;
1062
- while (!seen.has(current)) {
1063
- seen.add(current);
1064
- const entry = vulnerabilities[current];
1065
- if (!entry || !Array.isArray(entry.via)) return { rootName: current };
1066
- const detailed = entry.via.find(
1067
- (e) => typeof e === "object" && e !== null
1068
- );
1069
- if (detailed) return { rootName: current, detail: detailed };
1070
- const next = entry.via.find((e) => typeof e === "string");
1071
- if (!next || next === current) return { rootName: current };
1072
- current = next;
1073
- }
1074
- return { rootName: current };
1075
- }
1076
- function extractAdvisoriesFromNpm(parsed) {
1077
- const vulnerabilities = parsed.vulnerabilities ?? {};
1078
- const roots = /* @__PURE__ */ new Map();
1079
- for (const [name, v] of Object.entries(vulnerabilities)) {
1080
- if (!v) continue;
1081
- const { rootName, detail } = resolveNpmAdvisoryRoot(name, vulnerabilities);
1082
- if (roots.has(rootName)) continue;
1083
- const rootEntry = vulnerabilities[rootName];
1084
- const severity = normalizeSeverity(rootEntry?.severity ?? v.severity);
1085
- const title = detail?.title ?? rootName;
1086
- const url = detail?.url;
1087
- roots.set(rootName, {
1088
- module: rootEntry?.name ?? rootName,
1089
- severity,
1090
- title,
1091
- ...url ? { url } : {}
1092
- });
1093
- }
1094
- return [...roots.values()];
1095
- }
1096
- async function runAuditTool(spawn2, cmd, args, cwd) {
1097
- let raw;
1098
- try {
1099
- raw = await spawn2(cmd, args, { cwd });
1100
- } catch (err) {
1101
- const e = err;
1102
- if (e.code === "ENOENT" || /ENOENT/.test(String(err))) return { kind: "missing" };
1103
- return { kind: "error", reason: `spawn failed: ${String(err).slice(0, 200)}` };
1104
- }
1105
- if (raw.code !== 0 && raw.code !== 1) {
1106
- return {
1107
- kind: "error",
1108
- reason: `exit ${raw.code}${raw.stderr ? `: ${raw.stderr.slice(0, 150)}` : ""}`
1109
- };
1110
- }
1111
- let parsed;
1112
- try {
1113
- parsed = JSON.parse(raw.stdout || "{}");
1114
- } catch (err) {
1115
- return { kind: "error", reason: `unparseable JSON: ${String(err).slice(0, 100)}` };
1116
- }
1117
- const errEnvelope = parsed.error;
1118
- if (errEnvelope && typeof errEnvelope === "object") {
1119
- return { kind: "error", reason: errEnvelope.code ?? "error envelope returned" };
1120
- }
1121
- const vulnsMeta = parsed.metadata?.vulnerabilities;
1122
- if (!vulnsMeta || Object.keys(vulnsMeta).length === 0) {
1123
- return { kind: "error", reason: "no metadata.vulnerabilities in output" };
1124
- }
1125
- return { kind: "ok", parsed };
1126
- }
1127
- async function securityAudit(ctx) {
1128
- const spawn2 = ctx.spawn ?? defaultSpawn;
1129
- const site = ctx.site;
1130
- const label = siteLabel(site);
1131
- let used = "pnpm audit";
1132
- let result = await runAuditTool(spawn2, "pnpm", ["audit", "--json", "--prod"], site.path);
1133
- if (result.kind !== "ok") {
1134
- const pnpmReason = result.kind === "missing" ? "not installed" : result.reason;
1135
- const npmResult = await runAuditTool(
1136
- spawn2,
1137
- "npm",
1138
- ["audit", "--json", "--omit=dev"],
1139
- site.path
1140
- );
1141
- if (npmResult.kind === "ok") {
1142
- result = npmResult;
1143
- used = "npm audit";
1144
- } else {
1145
- const npmReason = npmResult.kind === "missing" ? "not installed" : npmResult.reason;
1146
- return {
1147
- audit: "security",
1148
- site: label,
1149
- status: "skip",
1150
- summary: `cannot run audit \u2014 pnpm: ${pnpmReason}; npm: ${npmReason}`
1151
- };
1152
- }
1153
- }
1154
- const parsed = result.parsed;
1155
- const counts = {
1156
- low: parsed.metadata?.vulnerabilities?.low ?? 0,
1157
- moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,
1158
- high: parsed.metadata?.vulnerabilities?.high ?? 0,
1159
- critical: parsed.metadata?.vulnerabilities?.critical ?? 0
1160
- };
1161
- const advisories = used === "pnpm audit" ? extractAdvisoriesFromPnpm(parsed) : extractAdvisoriesFromNpm(parsed);
1162
- const status = classify(counts);
1163
- const total = counts.low + counts.moderate + counts.high + counts.critical;
1164
- const summary = status === "pass" ? `${used}: 0 vulnerabilities` : `${used}: ${total} vulnerabilities (${counts.critical}C/${counts.high}H/${counts.moderate}M/${counts.low}L)`;
1165
- return {
1166
- audit: "security",
1167
- site: label,
1168
- status,
1169
- summary,
1170
- details: { counts, advisories }
1171
- };
1172
- }
1173
-
1174
- // src/audits/lighthouse.ts
1175
- import { readFile as readFile4, writeFile, mkdtemp, rm, readdir } from "fs/promises";
1176
- import { tmpdir } from "os";
1177
- import { join as join5 } from "path";
1178
-
1179
- // src/configs/lighthouse.ts
1180
- var lighthouseConfig = {
1181
- ci: {
1182
- collect: {
1183
- url: ["http://localhost:5173/dev/a11y-fixtures"],
1184
- // `npm run vite:dev` works on both pnpm and npm sites — pnpm respects
1185
- // the `run` form too. Keeps this config portable across the fleet
1186
- // while sites transition to pnpm.
1187
- startServerCommand: "npm run vite:dev",
1188
- startServerReadyPattern: "ready in",
1189
- startServerReadyTimeout: 12e4,
1190
- numberOfRuns: 1,
1191
- settings: {
1192
- preset: "desktop",
1193
- skipAudits: ["uses-http2"]
1194
- }
1195
- },
1196
- assert: {
1197
- assertions: {
1198
- "categories:accessibility": ["error", { minScore: 0.95 }],
1199
- "categories:best-practices": ["error", { minScore: 0.9 }],
1200
- "categories:seo": ["error", { minScore: 0.9 }],
1201
- "categories:performance": ["warn", { minScore: 0.7 }]
1202
- }
1203
- },
1204
- upload: {
1205
- target: "temporary-public-storage"
1206
- }
1207
- }
1208
- };
1209
-
1210
- // src/audits/util/site-config.ts
1211
- import { readFile as readFile3 } from "fs/promises";
1212
- import { join as join4 } from "path";
1213
- async function readSiteConfig(sitePath) {
1214
- let raw;
1215
- try {
1216
- raw = await readFile3(join4(sitePath, "package.json"), "utf-8");
1217
- } catch {
1218
- return {};
1219
- }
1220
- let pkg;
1221
- try {
1222
- pkg = JSON.parse(raw);
1223
- } catch {
1224
- return {};
1225
- }
1226
- if (!pkg || typeof pkg !== "object") return {};
1227
- const cfg = pkg.reddoor;
1228
- if (!cfg || typeof cfg !== "object") return {};
1229
- const out = {};
1230
- const url = cfg.lighthouseUrl;
1231
- if (typeof url === "string" && url.length > 0) {
1232
- out.lighthouseUrl = url;
1233
- }
1234
- return out;
1235
- }
1236
-
1237
- // src/util/free-port.ts
1238
- import { createServer } from "net";
1239
- async function findFreePort() {
1240
- return new Promise((resolve3, reject) => {
1241
- const server = createServer();
1242
- server.unref();
1243
- server.on("error", reject);
1244
- server.listen(0, "127.0.0.1", () => {
1245
- const addr = server.address();
1246
- if (typeof addr === "object" && addr) {
1247
- const port = addr.port;
1248
- server.close(() => resolve3(port));
1249
- } else {
1250
- server.close();
1251
- reject(new Error("findFreePort: could not determine assigned port from socket"));
1252
- }
1253
- });
1254
- });
1255
- }
1256
- function withFreePort(url, port) {
1257
- const u = new URL(url);
1258
- u.hostname = "localhost";
1259
- u.port = String(port);
1260
- return u.toString();
1261
- }
1262
-
1263
- // src/audits/lighthouse.ts
1264
- async function readJsonMaybe(path) {
1265
- try {
1266
- const raw = await readFile4(path, "utf-8");
1267
- return JSON.parse(raw);
1268
- } catch {
1269
- return null;
1270
- }
1271
- }
1272
- async function readLhrEntries(resultsDir) {
1273
- const files = await readdir(resultsDir).catch(() => []);
1274
- const entries = [];
1275
- for (const f of files) {
1276
- if (!f.startsWith("lhr-") || !f.endsWith(".json")) continue;
1277
- const lhr = await readJsonMaybe(join5(resultsDir, f));
1278
- if (!lhr || !lhr.categories) continue;
1279
- const summary = {};
1280
- for (const [k, v] of Object.entries(lhr.categories)) {
1281
- if (typeof v?.score === "number") summary[k] = v.score;
1282
- }
1283
- entries.push({ url: lhr.requestedUrl, summary });
1284
- }
1285
- return entries;
1286
- }
1287
- function averageSummaries(entries) {
1288
- if (entries.length === 0) return {};
1289
- const sums = {};
1290
- const counts = {};
1291
- for (const e of entries) {
1292
- for (const [k, v] of Object.entries(e.summary ?? {})) {
1293
- if (typeof v !== "number") continue;
1294
- sums[k] = (sums[k] ?? 0) + v;
1295
- counts[k] = (counts[k] ?? 0) + 1;
1296
- }
1297
- }
1298
- const out = {};
1299
- for (const k of Object.keys(sums)) {
1300
- const total = sums[k] ?? 0;
1301
- const count = counts[k] ?? 1;
1302
- out[k] = total / count;
1303
- }
1304
- return out;
1305
- }
1306
- function categoryFromAssertion(a) {
1307
- const colonIdx = a.name.indexOf(":");
1308
- return colonIdx >= 0 ? a.name.slice(colonIdx + 1) : a.name;
1309
- }
1310
- function messageForAssertion(a) {
1311
- const actual = typeof a.actual === "number" ? a.actual.toFixed(2) : "n/a";
1312
- return `${a.name} ${a.operator} ${a.expected} (actual: ${actual})`;
1313
- }
1314
- async function parseLhciResults(resultsDir, label, raw) {
1315
- const manifest = await readLhrEntries(resultsDir);
1316
- if (manifest.length === 0) {
1317
- return {
1318
- audit: "lighthouse",
1319
- site: label,
1320
- status: "fail",
1321
- summary: `lighthouse: no lhr-*.json written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
1322
- };
1323
- }
1324
- const assertionResults = await readJsonMaybe(join5(resultsDir, "assertion-results.json")) ?? [];
1325
- const failed = assertionResults.filter((a) => !a.passed);
1326
- const assertions = failed.map((a) => ({
1327
- category: categoryFromAssertion(a),
1328
- level: a.level,
1329
- message: messageForAssertion(a)
1330
- }));
1331
- const anyError = assertions.some((a) => a.level === "error");
1332
- const anyWarn = assertions.some((a) => a.level === "warn");
1333
- const status = anyError ? "fail" : anyWarn ? "warn" : "pass";
1334
- const normalized = {
1335
- summary: averageSummaries(manifest),
1336
- assertionsFailed: failed.length,
1337
- assertions
1338
- };
1339
- const summary = status === "pass" ? "lighthouse: all categories passing" : `lighthouse: ${failed.length} assertion(s) failed`;
1340
- return { audit: "lighthouse", site: label, status, summary, details: normalized };
1341
- }
1342
- async function checkoutLighthouse(spawn2, site, label) {
1343
- const siteCfg = await readSiteConfig(site.path);
1344
- const port = await findFreePort();
1345
- const baseUrl = siteCfg.lighthouseUrl ?? lighthouseConfig.ci.collect.url[0];
1346
- const resolvedConfig = {
1347
- ...lighthouseConfig,
1348
- ci: {
1349
- ...lighthouseConfig.ci,
1350
- collect: {
1351
- ...lighthouseConfig.ci.collect,
1352
- url: [withFreePort(baseUrl, port)],
1353
- startServerCommand: `npm run vite:dev -- --port ${port} --strictPort`
1354
- }
1355
- }
1356
- };
1357
- const configDir = await mkdtemp(join5(tmpdir(), "reddoor-lhci-"));
1358
- const configPath = join5(configDir, "lighthouserc.json");
1359
- await writeFile(configPath, JSON.stringify(resolvedConfig), "utf-8");
1360
- const resultsDir = join5(site.path, ".lighthouseci");
1361
- await rm(resultsDir, { recursive: true, force: true });
1362
- let raw;
1363
- try {
1364
- raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
1365
- cwd: site.path,
1366
- timeoutMs: 5 * 6e4
1367
- });
1368
- } catch (err) {
1369
- await rm(configDir, { recursive: true, force: true });
1370
- const e = err;
1371
- if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
1372
- return {
1373
- audit: "lighthouse",
1374
- site: label,
1375
- status: "skip",
1376
- summary: "npx/@lhci/cli not available"
1377
- };
1378
- }
1379
- throw err;
1380
- }
1381
- await rm(configDir, { recursive: true, force: true });
1382
- return parseLhciResults(resultsDir, label, raw);
1383
- }
1384
- async function deployedLighthouse(spawn2, deployedUrl, label) {
1385
- const workDir = await mkdtemp(join5(tmpdir(), "reddoor-lh-deployed-"));
1386
- const resolvedConfig = {
1387
- ci: {
1388
- // Deliberately NOT spread from lighthouseConfig.ci.collect: deployed mode
1389
- // must omit startServerCommand and the dev-server settings entirely.
1390
- collect: {
1391
- url: [deployedUrl],
1392
- // 3 runs to damp Lighthouse's run-to-run variance; parseLhciResults
1393
- // averages the lhr files. (Median is a tracked future refinement.)
1394
- numberOfRuns: 3,
1395
- settings: { preset: "desktop", skipAudits: ["uses-http2"] }
1396
- },
1397
- assert: lighthouseConfig.ci.assert,
1398
- upload: { target: "filesystem", outputDir: join5(workDir, "lhci-report") }
1399
- }
1400
- };
1401
- const configPath = join5(workDir, "lighthouserc.json");
1402
- await writeFile(configPath, JSON.stringify(resolvedConfig), "utf-8");
1403
- const resultsDir = join5(workDir, ".lighthouseci");
1404
- let raw;
1405
- try {
1406
- raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
1407
- cwd: workDir,
1408
- // 3 serial cold runs of a slow deployed site (lhci's own maxWaitForLoad
1409
- // ~45-60s each) + first-use Chrome download can plausibly exceed 3 min →
1410
- // SIGTERM → no lhr-*.json → spurious "no scores". Match the 5-min budget
1411
- // the checkout path already gives (erp-industrials nightly flake,
1412
- // morning-brief 2026-06-10 MEDIUM-F).
1413
- timeoutMs: 5 * 6e4
1414
- });
1415
- } catch (err) {
1416
- await rm(workDir, { recursive: true, force: true });
1417
- const e = err;
1418
- if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
1419
- return {
1420
- audit: "lighthouse",
1421
- site: label,
1422
- status: "skip",
1423
- summary: "npx/@lhci/cli not available"
1424
- };
1425
- }
1426
- throw err;
1427
- }
1428
- try {
1429
- return await parseLhciResults(resultsDir, label, raw);
1430
- } finally {
1431
- await rm(workDir, { recursive: true, force: true });
1432
- }
1433
- }
1434
- async function lighthouseAudit(ctx) {
1435
- const spawn2 = ctx.spawn ?? defaultSpawn;
1436
- const site = ctx.site;
1437
- const label = siteLabel(site);
1438
- return site.deployedUrl ? deployedLighthouse(spawn2, site.deployedUrl, label) : checkoutLighthouse(spawn2, site, label);
1439
- }
1440
-
1441
- // src/audits/a11y.ts
1442
- import { readFile as readFile5, writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
1443
- import { join as join6 } from "path";
1444
-
1445
- // src/configs/playwright-a11y.ts
1446
- import { defineConfig, devices } from "@playwright/test";
1447
- var a11yRoutes = [
1448
- { path: "/dev/a11y-fixtures", name: "a11y fixtures" },
1449
- { path: "/dev/animate-in", name: "animate-in demo" }
1450
- ];
1451
- var smokeRoutes = [{ path: "/", name: "home" }];
1452
- var playwrightA11yConfig = defineConfig({
1453
- testDir: "tests",
1454
- testMatch: /.*\.spec\.ts$/,
1455
- fullyParallel: true,
1456
- forbidOnly: !!process.env.CI,
1457
- retries: process.env.CI ? 2 : 0,
1458
- reporter: process.env.CI ? "github" : "list",
1459
- use: {
1460
- baseURL: "http://localhost:5173",
1461
- trace: "on-first-retry"
1462
- },
1463
- projects: [
1464
- {
1465
- name: "chromium",
1466
- use: { ...devices["Desktop Chrome"] }
1467
- }
1468
- ],
1469
- webServer: {
1470
- // Portable across pnpm and npm sites — pnpm respects `npm run` too.
1471
- command: "npm run vite:dev",
1472
- url: "http://localhost:5173/dev/a11y-fixtures",
1473
- reuseExistingServer: !process.env.CI,
1474
- timeout: 12e4
1475
- }
1476
- });
1477
-
1478
- // src/audits/a11y.ts
1479
- var RESULTS_REL = ".reddoor-a11y/results.json";
1480
- async function readJsonMaybe2(path) {
1481
- try {
1482
- const raw = await readFile5(path, "utf-8");
1483
- return JSON.parse(raw);
1484
- } catch {
1485
- return null;
1486
- }
1487
- }
1488
- function buildPlaywrightConfig(port, sitePath) {
1489
- return `import { defineConfig } from "@playwright/test";
1490
-
1491
- export default defineConfig({
1492
- testDir: ".",
1493
- testMatch: /.*\\.spec\\.ts$/,
1494
- fullyParallel: true,
1495
- forbidOnly: !!process.env.CI,
1496
- retries: process.env.CI ? 2 : 0,
1497
- reporter: process.env.CI ? "github" : "list",
1498
- use: {
1499
- baseURL: "http://localhost:${port}",
1500
- trace: "on-first-retry",
1501
- },
1502
- webServer: {
1503
- // --strictPort: refuse to bump to a different port if ours is taken,
1504
- // so the audit fails loudly instead of probing a zombie.
1505
- // reuseExistingServer:false: never reuse \u2014 we control the lifecycle.
1506
- // cwd: playwright's default webServer.cwd is the config file's
1507
- // directory. Our config lives in /tmp so without this override,
1508
- // "npm run vite:dev" tries to read /tmp/.../package.json and
1509
- // ENOENTs before vite ever starts. Caltex 2026-05-28 (0.10.5).
1510
- command: "npm run vite:dev -- --port ${port} --strictPort",
1511
- url: "http://localhost:${port}/dev/a11y-fixtures",
1512
- cwd: ${JSON.stringify(sitePath)},
1513
- reuseExistingServer: false,
1514
- timeout: 120_000,
1515
- },
1516
- });
1517
- `;
1518
- }
1519
- function buildSpec() {
1520
- return `import { test, expect } from "@playwright/test";
1521
- import AxeBuilder from "@axe-core/playwright";
1522
- import { mkdir, writeFile } from "node:fs/promises";
1523
- import { dirname } from "node:path";
1524
-
1525
- const pages = ${JSON.stringify(a11yRoutes)};
1526
- const smokePages = ${JSON.stringify(smokeRoutes)};
1527
- const OUTPUT = process.env.REDDOOR_A11Y_OUTPUT;
1528
-
1529
- // Playwright's default per-test timeout is 30s. We loop through every
1530
- // configured route in a single test, so the budget needs to scale.
1531
- test.setTimeout(5 * 60_000);
1532
-
1533
- test("a11y + hydration across configured routes", async ({ page }) => {
1534
- const violations = [];
1535
-
1536
- // Capture uncaught client-side exceptions across every route we visit. A page
1537
- // that builds + SSRs cleanly can still throw on hydrate and blank itself
1538
- // (data-dynamiq: a Svelte 4->5 run() referenced a $state declared after it) --
1539
- // axe never sees that, so we listen for it directly and tag the route in scope.
1540
- let currentRoute = "";
1541
- page.on("pageerror", (err) => {
1542
- violations.push({
1543
- id: "client-error",
1544
- impact: "critical",
1545
- route: currentRoute,
1546
- help: String(err && err.message ? err.message : err),
1547
- });
1548
- });
1549
-
1550
- for (const { path, name } of pages) {
1551
- currentRoute = name;
1552
- await page.goto(path);
1553
- // Snap CSS transitions/animations to their resting state before axe runs.
1554
- // AnimateIn-style fixtures transition opacity 0->1; sampling mid-transition
1555
- // makes axe compute color-contrast against semi-transparent text, yielding a
1556
- // flaky "serious" color-contrast violation (~1/3 of runs on /dev/animate-in).
1557
- // Disabling transitions/animations forces the final, rendered state
1558
- // deterministically -- which is also what users (and prefers-reduced-motion
1559
- // users) actually see, so it's the correct thing to assert.
1560
- await page.addStyleTag({
1561
- content: "*,*::before,*::after{transition:none!important;animation:none!important;}",
1562
- });
1563
- const results = await new AxeBuilder({ page })
1564
- .withTags(["wcag2a","wcag2aa","wcag21a","wcag21aa","wcag22aa"])
1565
- .analyze();
1566
- for (const v of results.violations) {
1567
- violations.push({
1568
- id: v.id,
1569
- impact: v.impact ?? "moderate",
1570
- route: name,
1571
- help: v.help,
1572
- helpUrl: v.helpUrl,
1573
- nodes: v.nodes.map((n) => ({ html: n.html, target: n.target })),
1574
- });
1575
- }
1576
- }
1577
-
1578
- // Hydration smoke check: load real routes (the homepage) and fail on any
1579
- // uncaught client-side error. No axe here -- real routes carry pre-existing
1580
- // a11y debt we don't gate on; we only assert they don't crash on hydrate.
1581
- // HTTP/SSR errors don't fire 'pageerror', so a data-less CI homepage that
1582
- // renders empty-but-valid won't false-fail -- only a real client crash does.
1583
- for (const { path, name } of smokePages) {
1584
- currentRoute = name;
1585
- await page.goto(path);
1586
- // Let hydration + first effects run so a TDZ/ReferenceError surfaces.
1587
- await page.waitForTimeout(2000);
1588
- }
1589
-
1590
- const byImpact = {};
1591
- for (const v of violations) {
1592
- byImpact[v.impact] = (byImpact[v.impact] ?? 0) + 1;
1593
- }
1594
- if (OUTPUT) {
1595
- await mkdir(dirname(OUTPUT), { recursive: true });
1596
- await writeFile(
1597
- OUTPUT,
1598
- JSON.stringify({ totalViolations: violations.length, byImpact, violations }, null, 2),
1599
- );
1600
- }
1601
- expect(violations).toEqual([]);
1602
- });
1603
- `;
1604
- }
1605
- async function a11yAudit(ctx) {
1606
- const spawn2 = ctx.spawn ?? defaultSpawn;
1607
- const site = ctx.site;
1608
- const label = siteLabel(site);
1609
- const specDir = await mkdtemp2(join6(site.path, ".reddoor-a11y-spec-"));
1610
- try {
1611
- const specPath = join6(specDir, "a11y.spec.ts");
1612
- await writeFile2(specPath, buildSpec(), "utf-8");
1613
- const port = await findFreePort();
1614
- const configPath = join6(specDir, "playwright.config.ts");
1615
- await writeFile2(configPath, buildPlaywrightConfig(port, site.path), "utf-8");
1616
- const resultsPath = join6(site.path, RESULTS_REL);
1617
- await rm2(join6(site.path, ".reddoor-a11y"), { recursive: true, force: true });
1618
- let raw;
1619
- try {
1620
- raw = await spawn2(
1621
- "npx",
1622
- ["--yes", "playwright", "test", `--config=${configPath}`, "--reporter=line", specPath],
1623
- {
1624
- cwd: site.path,
1625
- env: { ...process.env, REDDOOR_A11Y_OUTPUT: resultsPath },
1626
- // playwright on a cold tree downloads Chrome, boots the site's dev
1627
- // server, and runs axe over every configured route. The shared 30 s
1628
- // default in runAudits is fine for deps/lint/security but starves
1629
- // playwright (mirrors the lighthouse fix shipped earlier).
1630
- timeoutMs: 5 * 6e4
1631
- }
1632
- );
1633
- } catch (err) {
1634
- const e = err;
1635
- if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
1636
- return {
1637
- audit: "a11y",
1638
- site: label,
1639
- status: "skip",
1640
- summary: "npx/playwright not available"
1641
- };
1642
- }
1643
- throw err;
1644
- }
1645
- const artifact = await readJsonMaybe2(resultsPath);
1646
- if (!artifact) {
1647
- return {
1648
- audit: "a11y",
1649
- site: label,
1650
- status: "fail",
1651
- summary: `a11y: no results written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
1652
- };
1653
- }
1654
- const hasSerious = (artifact.byImpact.serious ?? 0) > 0 || (artifact.byImpact.critical ?? 0) > 0;
1655
- const hasAny = artifact.totalViolations > 0;
1656
- const status = hasSerious ? "fail" : hasAny ? "warn" : "pass";
1657
- const summary = status === "pass" ? `a11y: 0 violations across ${a11yRoutes.length} routes (+${smokeRoutes.length} hydration smoke)` : `a11y: ${artifact.totalViolations} violations`;
1658
- return {
1659
- audit: "a11y",
1660
- site: label,
1661
- status,
1662
- summary,
1663
- details: artifact
1664
- };
1665
- } finally {
1666
- await rm2(specDir, { recursive: true, force: true });
1667
- }
1668
- }
1669
-
1670
- // src/audits/domain.ts
1671
- import { promises as dnsPromises } from "dns";
1672
- import tls from "tls";
1673
- var MS_PER_DAY = 24 * 60 * 60 * 1e3;
1674
- async function checkDomain(url, deps) {
1675
- let host;
1676
- try {
1677
- host = new URL(url).hostname;
1678
- } catch {
1679
- return { resolved: false, certDaysRemaining: null };
1680
- }
1681
- try {
1682
- await deps.lookup(host);
1683
- } catch {
1684
- return { resolved: false, certDaysRemaining: null };
1685
- }
1686
- let validTo;
1687
- try {
1688
- validTo = await deps.certValidTo(host);
1689
- } catch {
1690
- validTo = null;
1691
- }
1692
- if (!validTo || Number.isNaN(validTo.getTime()))
1693
- return { resolved: true, certDaysRemaining: null };
1694
- return {
1695
- resolved: true,
1696
- certDaysRemaining: Math.floor((validTo.getTime() - deps.now.getTime()) / MS_PER_DAY)
1697
- };
1698
- }
1699
- function defaultDomainDeps(now) {
1700
- return {
1701
- lookup: async (host) => {
1702
- await dnsPromises.lookup(host);
1703
- },
1704
- certValidTo: (host) => new Promise((resolvePromise) => {
1705
- const socket = tls.connect(
1706
- { host, port: 443, servername: host, timeout: 1e4, rejectUnauthorized: true },
1707
- () => {
1708
- const cert = socket.authorized ? socket.getPeerCertificate() : null;
1709
- socket.end();
1710
- const validTo = cert && cert.valid_to ? new Date(cert.valid_to) : null;
1711
- resolvePromise(validTo);
1712
- }
1713
- );
1714
- socket.on("error", () => resolvePromise(null));
1715
- socket.on("timeout", () => {
1716
- socket.destroy();
1717
- resolvePromise(null);
1718
- });
1719
- }),
1720
- now
1721
- };
1722
- }
1723
- async function domainAudit(ctx) {
1724
- const { site } = ctx;
1725
- const label = siteLabel(site);
1726
- if (!site.deployedUrl) {
1727
- return { audit: "domain", site: label, status: "skip", summary: "no deployed URL" };
1728
- }
1729
- const now = ctx.now ?? /* @__PURE__ */ new Date();
1730
- const deps = ctx.domainDeps ?? defaultDomainDeps(now);
1731
- const check = await checkDomain(site.deployedUrl, deps);
1732
- const checkedAt = now.toISOString();
1733
- const status = check.resolved && check.certDaysRemaining !== null && check.certDaysRemaining > 14 ? "pass" : "warn";
1734
- const summary = !check.resolved ? "did not resolve" : check.certDaysRemaining === null ? "resolved, no usable TLS cert" : `resolved, cert ${check.certDaysRemaining}d remaining`;
1735
- return {
1736
- audit: "domain",
1737
- site: label,
1738
- status,
1739
- summary,
1740
- details: { resolved: check.resolved, certDaysRemaining: check.certDaysRemaining, checkedAt }
1741
- };
1742
- }
1743
-
1744
- // src/audits/route-discovery.ts
1745
- var DEFAULT_CAP = 15;
1746
- function parseSitemapUrls(xml) {
1747
- const out = [];
1748
- const re = /<loc>\s*([^<\s]+)\s*<\/loc>/gi;
1749
- let m;
1750
- while ((m = re.exec(xml)) !== null) {
1751
- const url = m[1];
1752
- if (url) out.push(url.trim());
1753
- }
1754
- return out;
1755
- }
1756
- function parseHtmlLinks(html, baseUrl) {
1757
- const out = /* @__PURE__ */ new Set();
1758
- const re = /<a\b[^>]*\bhref\s*=\s*["']([^"']+)["']/gi;
1759
- let m;
1760
- while ((m = re.exec(html)) !== null) {
1761
- const href = m[1];
1762
- if (!href || href.startsWith("#") || /^(mailto:|tel:|javascript:)/i.test(href)) continue;
1763
- try {
1764
- const u = new URL(href, baseUrl);
1765
- if (u.origin !== new URL(baseUrl).origin) continue;
1766
- out.add(u.pathname);
1767
- } catch {
1768
- }
1769
- }
1770
- return [...out];
1771
- }
1772
- function family(pathname) {
1773
- return pathname.split("/").filter(Boolean)[0] ?? "";
1774
- }
1775
- function sampleRoutePaths(urlsOrPaths, cap = DEFAULT_CAP) {
1776
- const seen = /* @__PURE__ */ new Set(["/"]);
1777
- const buckets = /* @__PURE__ */ new Map();
1778
- for (const raw of urlsOrPaths) {
1779
- let pathname;
1780
- try {
1781
- pathname = raw.startsWith("/") ? new URL(raw, "https://x.invalid").pathname : new URL(raw).pathname;
1782
- } catch {
1783
- continue;
1784
- }
1785
- if (pathname === "/") continue;
1786
- if (seen.has(pathname)) continue;
1787
- seen.add(pathname);
1788
- const fam = family(pathname);
1789
- const arr = buckets.get(fam) ?? [];
1790
- arr.push(pathname);
1791
- buckets.set(fam, arr);
1792
- }
1793
- const result = ["/"];
1794
- const families = [...buckets.values()];
1795
- let guard = 0;
1796
- while (result.length < cap && families.some((f) => f.length > 0) && guard++ < 1e4) {
1797
- for (const fam of families) {
1798
- if (result.length >= cap) break;
1799
- const next = fam.shift();
1800
- if (next) result.push(next);
1801
- }
1802
- }
1803
- return result;
1804
- }
1805
- function familyCountsOf(paths) {
1806
- const counts = {};
1807
- for (const p of paths) {
1808
- const key = p === "/" ? "/" : `/${family(p)}`;
1809
- counts[key] = (counts[key] ?? 0) + 1;
1810
- }
1811
- return counts;
1812
- }
1813
- async function discoverRoutes(deployedUrl, deps, cap = DEFAULT_CAP) {
1814
- const origin = new URL(deployedUrl).origin;
1815
- const abs = (paths) => paths.map((p) => new URL(p, origin).href);
1816
- const sitemapXml = await deps.fetchText(new URL("/sitemap.xml", origin).href);
1817
- if (sitemapXml) {
1818
- const urls = parseSitemapUrls(sitemapXml);
1819
- if (urls.length > 0) {
1820
- const paths = sampleRoutePaths(urls, cap);
1821
- return { routes: abs(paths), source: "sitemap", familyCounts: familyCountsOf(paths) };
1822
- }
1823
- }
1824
- const homeHtml = await deps.fetchText(origin);
1825
- if (homeHtml) {
1826
- const links = parseHtmlLinks(homeHtml, origin);
1827
- if (links.length > 0) {
1828
- const paths = sampleRoutePaths(links, cap);
1829
- return { routes: abs(paths), source: "homepage-links", familyCounts: familyCountsOf(paths) };
1830
- }
1831
- }
1832
- return { routes: [new URL("/", origin).href], source: "root-only", familyCounts: { "/": 1 } };
1833
- }
1834
-
1835
- // src/audits/browser.ts
1836
- function isBroken(status) {
1837
- return status === null || status >= 400;
1838
- }
1839
- function summarizeBrowser(routes, links, familyCounts) {
1840
- const desktopChecks = routes.flatMap((r) => r.desktop);
1841
- const mobileChecks = routes.flatMap((r) => r.mobile);
1842
- const desktopOk = routes.length > 0 && routes.every((r) => r.desktop.length > 0 && r.desktop.every((d) => d.ok));
1843
- const mobileOk = routes.length > 0 && routes.every((r) => r.mobile.length > 0 && r.mobile.every((m) => m.ok));
1844
- const brokenLinks = links.filter((l) => isBroken(l.status)).length;
1845
- const linksOk = links.length > 0 && brokenLinks === 0;
1846
- const engines = [...new Set(desktopChecks.map((d) => d.engine))];
1847
- const devices2 = [...new Set(mobileChecks.map((m) => m.device))];
1848
- const families = Object.entries(familyCounts).map(([f, n]) => f === "/" ? "/" : `${f} \xD7${n}`).join(", ");
1849
- const note = `${routes.length} routes (${families}); desktop ${engines.join("/") || "\u2014"}; mobile ${devices2.join("/") || "\u2014"}; ${links.length} links, ${brokenLinks} broken`;
1850
- return { desktopOk, mobileOk, linksOk, brokenLinks, routesChecked: routes.length, note };
1851
- }
1852
- async function browserAudit(ctx) {
1853
- const { site } = ctx;
1854
- const label = siteLabel(site);
1855
- if (!site.deployedUrl) {
1856
- return { audit: "browser", site: label, status: "skip", summary: "no deployed URL" };
1857
- }
1858
- const now = ctx.now ?? /* @__PURE__ */ new Date();
1859
- const discoverDeps = ctx.discoverDeps ?? defaultDiscoverDeps();
1860
- const runner = ctx.browserRunner ?? await defaultBrowserRunner();
1861
- try {
1862
- const discovered = await discoverRoutes(site.deployedUrl, discoverDeps);
1863
- const routeResults = await runner.probe(discovered.routes);
1864
- const internalLinks = [...new Set(routeResults.flatMap((r) => r.links))];
1865
- const linkResults = await runner.checkLinks(internalLinks);
1866
- const summary = summarizeBrowser(
1867
- routeResults,
1868
- linkResults,
1869
- discovered.familyCounts ?? familyCountsOf(discovered.routes)
1870
- );
1871
- const status = summary.desktopOk && summary.mobileOk && summary.linksOk ? "pass" : "warn";
1872
- return {
1873
- audit: "browser",
1874
- site: label,
1875
- status,
1876
- summary: summary.note,
1877
- details: { ...summary, checkedAt: now.toISOString() }
1878
- };
1879
- } finally {
1880
- await runner.close?.();
1881
- }
1882
- }
1883
- var FETCH_TIMEOUT_MS = 1e4;
1884
- function defaultDiscoverDeps() {
1885
- return {
1886
- fetchText: async (url) => {
1887
- try {
1888
- const res = await fetch(url, {
1889
- redirect: "follow",
1890
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
1891
- });
1892
- if (!res.ok) return null;
1893
- return await res.text();
1894
- } catch {
1895
- return null;
1896
- }
1897
- }
1898
- };
1899
- }
1900
- var DESKTOP_VIEWPORT = { width: 1366, height: 900 };
1901
- var PAGE_TIMEOUT_MS = 3e4;
1902
- async function defaultBrowserRunner() {
1903
- const { chromium, firefox, webkit, devices: devices2 } = await import("@playwright/test");
1904
- const desktopEngines = [
1905
- { engine: "chromium", type: chromium },
1906
- { engine: "firefox", type: firefox },
1907
- { engine: "webkit", type: webkit }
1908
- ];
1909
- const mobileTargets = [
1910
- { device: "Pixel 7", descriptor: devices2["Pixel 7"] },
1911
- { device: "iPhone 14", descriptor: devices2["iPhone 14"] }
1912
- ];
1913
- return {
1914
- async probe(urls) {
1915
- const results = [];
1916
- const browsers = await Promise.all(desktopEngines.map((e) => e.type.launch()));
1917
- const mobileBrowsers = await Promise.all(mobileTargets.map(() => chromium.launch()));
1918
- try {
1919
- for (const url of urls) {
1920
- const desktop = [];
1921
- const linkSet = /* @__PURE__ */ new Set();
1922
- for (let i = 0; i < desktopEngines.length; i++) {
1923
- const engine = desktopEngines[i].engine;
1924
- const browser = browsers[i];
1925
- const ctx = await browser.newContext({ viewport: DESKTOP_VIEWPORT });
1926
- const page = await ctx.newPage();
1927
- const errors = [];
1928
- page.on("pageerror", (e) => errors.push(String(e)));
1929
- let ok = false;
1930
- try {
1931
- const resp = await page.goto(url, {
1932
- waitUntil: "domcontentloaded",
1933
- timeout: PAGE_TIMEOUT_MS
1934
- });
1935
- const hasMain = await page.locator("main, [role=main]").first().isVisible().catch(() => false);
1936
- ok = !!resp && resp.ok() && errors.length === 0 && hasMain;
1937
- if (engine === "chromium") {
1938
- const hrefs = await page.evaluate("Array.from(document.querySelectorAll('a[href]')).map((a) => a.href)").catch(() => []);
1939
- const origin = new URL(url).origin;
1940
- for (const h of hrefs) {
1941
- try {
1942
- if (new URL(h).origin === origin) linkSet.add(new URL(h).href);
1943
- } catch {
1944
- }
1945
- }
1946
- }
1947
- } catch {
1948
- ok = false;
1949
- } finally {
1950
- await ctx.close().catch(() => {
1951
- });
1952
- }
1953
- desktop.push({ engine, ok });
1954
- }
1955
- const mobile = [];
1956
- for (let i = 0; i < mobileTargets.length; i++) {
1957
- const { device, descriptor } = mobileTargets[i];
1958
- const browser = mobileBrowsers[i];
1959
- const ctx = await browser.newContext({ ...descriptor });
1960
- const page = await ctx.newPage();
1961
- const errors = [];
1962
- page.on("pageerror", (e) => errors.push(String(e)));
1963
- let ok = false;
1964
- try {
1965
- const resp = await page.goto(url, {
1966
- waitUntil: "domcontentloaded",
1967
- timeout: PAGE_TIMEOUT_MS
1968
- });
1969
- const overflow = await page.evaluate("document.documentElement.scrollWidth > window.innerWidth + 2").catch(() => true);
1970
- ok = !!resp && resp.ok() && errors.length === 0 && !overflow;
1971
- } catch {
1972
- ok = false;
1973
- } finally {
1974
- await ctx.close().catch(() => {
1975
- });
1976
- }
1977
- mobile.push({ device, ok });
1978
- }
1979
- results.push({ url, desktop, mobile, links: [...linkSet] });
1980
- }
1981
- } finally {
1982
- await Promise.all([...browsers, ...mobileBrowsers].map((b) => b.close().catch(() => {
1983
- })));
1984
- }
1985
- return results;
1986
- },
1987
- async checkLinks(urls) {
1988
- const out = [];
1989
- for (const url of urls) {
1990
- let status;
1991
- try {
1992
- let res = await fetch(url, {
1993
- method: "HEAD",
1994
- redirect: "follow",
1995
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
1996
- });
1997
- if (res.status === 405 || res.status === 501) {
1998
- res = await fetch(url, {
1999
- method: "GET",
2000
- redirect: "follow",
2001
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2002
- });
2003
- }
2004
- status = res.status;
2005
- } catch {
2006
- status = null;
2007
- }
2008
- out.push({ url, status });
2009
- }
2010
- return out;
2011
- }
2012
- };
2013
- }
2014
-
2015
- // src/audits/index.ts
2016
- var REGISTRY = {
2017
- deps: depsAudit,
2018
- lint: lintAudit,
2019
- security: securityAudit,
2020
- lighthouse: lighthouseAudit,
2021
- a11y: a11yAudit,
2022
- domain: domainAudit,
2023
- browser: browserAudit
2024
- };
2025
- var ALL_AUDIT_NAMES = Object.keys(REGISTRY);
2026
- var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
2027
- function timedSpawn(timeoutMs) {
2028
- return (cmd, args, opts = {}) => defaultSpawn(cmd, args, { ...opts, timeoutMs: opts.timeoutMs ?? timeoutMs });
2029
- }
2030
- async function runOneAudit(site, name) {
2031
- if (!(name in REGISTRY)) throw new Error(`unknown audit: ${name}`);
2032
- const spawn2 = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);
2033
- const label = site.name || site.path;
2034
- try {
2035
- return await REGISTRY[name]({ site, spawn: spawn2 });
2036
- } catch (err) {
2037
- return {
2038
- audit: name,
2039
- site: label,
2040
- status: "fail",
2041
- summary: `${name}: unexpected error \u2014 ${String(err)}`
2042
- };
2043
- }
2044
- }
2045
-
2046
- // src/cli/fleet/resolve-sites.ts
2047
- import { pathToFileURL } from "url";
2048
- import { resolve, extname } from "path";
2049
-
2050
- // src/inventory/local.ts
2051
- import { basename } from "path";
2052
- function localPath(path, opts = {}) {
2053
- const site = { path, name: opts.name || basename(path) };
2054
- return async () => [site];
2055
- }
2056
-
2057
- // src/inventory/json.ts
2058
- init_url();
2059
- import { readFile as readFile6 } from "fs/promises";
2060
- import { isAbsolute } from "path";
2061
- function validate(raw) {
2062
- if (!Array.isArray(raw)) {
2063
- throw new Error("inventory JSON must be an array of sites");
2064
- }
2065
- return raw.map((entry, i) => {
2066
- if (!entry || typeof entry !== "object") {
2067
- throw new Error(`inventory entry ${i} is not an object`);
2068
- }
2069
- const e = entry;
2070
- if (typeof e.path !== "string" || e.path.length === 0) {
2071
- throw new Error(`inventory entry ${i} is missing required field: path`);
2072
- }
2073
- if (!isAbsolute(e.path)) {
2074
- throw new Error(
2075
- `inventory entry ${i}: path must be absolute (got "${e.path}"). Relative paths are rejected so cwd at invocation can't change which site is targeted.`
2076
- );
2077
- }
2078
- const site = { path: e.path };
2079
- if (typeof e.name === "string") site.name = e.name;
2080
- if (typeof e.repoUrl === "string") site.repoUrl = e.repoUrl;
2081
- if (typeof e.gitRepo === "string") site.gitRepo = e.gitRepo;
2082
- if (typeof e.deployedUrl === "string") {
2083
- if (isHttpUrl(e.deployedUrl)) {
2084
- site.deployedUrl = e.deployedUrl;
2085
- } else {
2086
- console.warn(
2087
- `[inventory] entry ${i}: ignoring deployedUrl that is not http(s): ${JSON.stringify(e.deployedUrl)}`
2088
- );
2089
- }
2090
- }
2091
- if (typeof e.meta === "object" && e.meta !== null) {
2092
- site.meta = e.meta;
2093
- }
2094
- return site;
2095
- });
2096
- }
2097
- function fromJsonFile(path) {
2098
- return async () => {
2099
- const text = await readFile6(path, "utf-8");
2100
- let raw;
2101
- try {
2102
- raw = JSON.parse(text);
2103
- } catch (e) {
2104
- throw new Error(`could not parse inventory file ${path}: ${e.message}`, {
2105
- cause: e
2106
- });
2107
- }
2108
- return validate(raw);
2109
- };
2110
- }
2111
-
2112
- // src/cli/fleet/resolve-sites.ts
2113
- init_url();
2114
- function sanitizeDynamicSites(sites) {
2115
- return sites.map((s) => {
2116
- if (s.deployedUrl !== void 0 && !isHttpUrl(s.deployedUrl)) {
2117
- console.warn(
2118
- `[inventory] dynamic inventory: ignoring deployedUrl that is not http(s) for ${s.name ?? s.path}: ${JSON.stringify(s.deployedUrl)}`
2119
- );
2120
- const copy = { ...s };
2121
- delete copy.deployedUrl;
2122
- return copy;
2123
- }
2124
- return s;
2125
- });
2126
- }
2127
- async function resolveSites(input) {
2128
- if (input.site && input.fleet) {
2129
- throw Object.assign(new Error("cannot combine a positional [site] with --fleet"), {
2130
- exitCode: 2
2131
- });
2132
- }
2133
- if (input.fleet === "airtable") {
2134
- const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
2135
- const { fromAirtableBase: fromAirtableBase2 } = await Promise.resolve().then(() => (init_airtable(), airtable_exports));
2136
- const base = openBase2(readAirtableConfig2());
2137
- const provider = fromAirtableBase2(base, input.workdir ? { workdir: input.workdir } : {});
2138
- return provider();
2139
- }
2140
- if (input.fleet) {
2141
- const fleetPath = resolve(input.cwd, input.fleet);
2142
- const ext = extname(fleetPath).toLowerCase();
2143
- if (ext === ".json") {
2144
- return fromJsonFile(fleetPath)();
2145
- }
2146
- if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
2147
- const mod = await import(pathToFileURL(fleetPath).href);
2148
- if (!mod.default || typeof mod.default !== "function") {
2149
- throw Object.assign(new Error(`--fleet ${input.fleet}: default export is not a function`), {
2150
- exitCode: 2
2151
- });
2152
- }
2153
- return sanitizeDynamicSites(await mod.default());
2154
- }
2155
- throw Object.assign(
2156
- new Error(`--fleet ${input.fleet}: unsupported extension ${ext || "(none)"}`),
2157
- { exitCode: 2 }
2158
- );
2159
- }
2160
- return localPath(resolve(input.cwd, input.site ?? input.cwd))();
2161
- }
2162
-
2163
- // src/cli/fleet/clone-if-needed.ts
2164
- import { stat as stat2, readdir as readdir2, mkdir } from "fs/promises";
2165
- import { isAbsolute as isAbsolute2, join as join8 } from "path";
2166
-
2167
- // src/util/git.ts
2168
- import { execFile } from "child_process";
2169
- import { promisify } from "util";
2170
- var exec = promisify(execFile);
2171
- var OWNER_REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
2172
- function isOwnerRepo(repo) {
2173
- if (repo.includes("..")) return false;
2174
- return OWNER_REPO_RE.test(repo);
2175
- }
2176
- function sameOwnerRepo(a, b) {
2177
- const na = isOwnerRepo(a.trim()) ? a.trim() : parseOwnerRepo(a);
2178
- const nb = isOwnerRepo(b.trim()) ? b.trim() : parseOwnerRepo(b);
2179
- if (na === null || nb === null) return false;
2180
- return na.toLowerCase() === nb.toLowerCase();
2181
- }
2182
- function parseOwnerRepo(remoteUrl) {
2183
- const trimmed = remoteUrl.trim().replace(/\.git$/, "").replace(/\/$/, "");
2184
- const scp = trimmed.match(/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:(.+)$/);
2185
- const path = scp ? scp[1] : trimmed.replace(/^https?:\/\/[^/]+\//, "");
2186
- const segments = path.split("/").filter(Boolean);
2187
- if (segments.length < 2) return null;
2188
- return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`;
2189
- }
2190
-
2191
- // src/cli/fleet/clone-if-needed.ts
2192
- function deriveNameFromRepoUrl(repoUrl) {
2193
- const slash = repoUrl.split("/").pop() ?? repoUrl;
2194
- return slash.replace(/\.git$/, "");
2195
- }
2196
- function assertSafeName(name) {
2197
- if (name.length === 0) {
2198
- throw new Error("unsafe site name (empty)");
2199
- }
2200
- if (isAbsolute2(name)) {
2201
- throw new Error(`unsafe site name (absolute path not allowed): ${name}`);
2202
- }
2203
- if (name.includes("/") || name.includes("\\")) {
2204
- throw new Error(`unsafe site name (path separator not allowed): ${name}`);
2205
- }
2206
- if (name.split(/[\\/]/).some((seg) => seg === "..")) {
2207
- throw new Error(`unsafe site name (traversal segment not allowed): ${name}`);
2208
- }
2209
- }
2210
- function assertSafeRepoUrl(repoUrl) {
2211
- if (!/^(https?|ssh|git|file):\/\//.test(repoUrl) && !/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:/.test(repoUrl)) {
2212
- throw new Error(
2213
- `unsafe repoUrl: must start with a scheme (https://, ssh://, git://, file://) or use scp-style "user@host:path" (got: ${JSON.stringify(repoUrl)})`
2214
- );
2215
- }
2216
- }
2217
- async function isNonEmptyDir(path) {
2218
- try {
2219
- const s = await stat2(path);
2220
- if (!s.isDirectory()) return false;
2221
- const entries = await readdir2(path);
2222
- return entries.length > 0;
2223
- } catch {
2224
- return false;
2225
- }
2226
- }
2227
- function expectedRepoRef(site) {
2228
- return site.gitRepo ?? site.repoUrl ?? void 0;
2229
- }
2230
- async function assertCheckoutMatches(site, path, spawn2) {
2231
- const expected = expectedRepoRef(site);
2232
- if (!expected) return;
2233
- const r = await spawn2("git", ["-C", path, "remote", "get-url", "origin"], {
2234
- timeoutMs: 3e4
2235
- });
2236
- if (r.code !== 0) return;
2237
- const origin = r.stdout.trim();
2238
- if (origin.length === 0) return;
2239
- if (!sameOwnerRepo(origin, expected)) {
2240
- throw new Error(
2241
- `checkout at ${path} is the wrong repo: origin is ${JSON.stringify(origin)} but site expects ${JSON.stringify(expected)} (slug collision?) \u2014 refusing to reuse it`
2242
- );
2243
- }
2244
- }
2245
- var GIT_REPO_RE = /^[\w.-]+\/[\w.-]+$/;
2246
- function resolveCloneUrl(site) {
2247
- if (site.repoUrl) return site.repoUrl;
2248
- if (!site.gitRepo) return void 0;
2249
- if (!GIT_REPO_RE.test(site.gitRepo)) {
2250
- throw new Error(`unsafe gitRepo: expected "owner/repo" (got: ${JSON.stringify(site.gitRepo)})`);
2251
- }
2252
- return `https://github.com/${site.gitRepo}.git`;
2253
- }
2254
- async function cloneIfNeeded(site, opts) {
2255
- const spawn2 = opts.spawn ?? defaultSpawn;
2256
- if (await isNonEmptyDir(site.path)) {
2257
- await assertCheckoutMatches(site, site.path, spawn2);
2258
- return site;
2259
- }
2260
- const repoUrl = resolveCloneUrl(site);
2261
- if (!repoUrl) {
2262
- throw new Error(
2263
- `site path does not exist (${site.path}) and no repoUrl or gitRepo is set \u2014 cannot clone`
2264
- );
2265
- }
2266
- const name = site.name || deriveNameFromRepoUrl(repoUrl);
2267
- assertSafeName(name);
2268
- assertSafeRepoUrl(repoUrl);
2269
- const target = join8(opts.workdir, name);
2270
- await mkdir(opts.workdir, { recursive: true });
2271
- if (await isNonEmptyDir(target)) {
2272
- await assertCheckoutMatches(site, target, spawn2);
2273
- return { ...site, name, path: target };
2274
- }
2275
- const result = await spawn2("git", ["clone", "--", repoUrl, target], {
2276
- cwd: opts.workdir,
2277
- timeoutMs: 5 * 6e4
2278
- });
2279
- if (result.code !== 0) {
2280
- throw new Error(`git clone failed (code ${result.code}): ${result.stderr}`);
2281
- }
2282
- return { ...site, name, path: target };
2283
- }
2284
-
2285
- // src/cli/fleet/prepare-sites.ts
2286
- async function prepareFleetSites(sites, opts) {
2287
- const clone = opts.clone ?? cloneIfNeeded;
2288
- const needsCheckout = opts.needsCheckout ?? (() => true);
2289
- const settled = await Promise.all(
2290
- sites.map(async (site) => {
2291
- if (!needsCheckout(site)) return { ok: true, site };
2292
- try {
2293
- return { ok: true, site: await clone(site, { workdir: opts.workdir }) };
2294
- } catch (e) {
2295
- return { ok: false, site: site.name || site.path, reason: e.message };
2296
- }
2297
- })
2298
- );
2299
- const prepared = [];
2300
- const skipped = [];
2301
- for (const r of settled) {
2302
- if (r.ok) prepared.push(r.site);
2303
- else skipped.push({ site: r.site, reason: r.reason });
2304
- }
2305
- return { prepared, skipped };
2306
- }
2307
- function formatSkippedNotice(skipped) {
2308
- if (skipped.length === 0) return null;
2309
- const detail = skipped.map((s) => `${s.site} (${s.reason})`).join("; ");
2310
- return `\u26A0 ${skipped.length} site(s) skipped (could not prepare): ${detail}`;
2311
- }
2312
-
2313
- // src/cli/commands/audit.ts
2314
- init_url();
2315
-
2316
- // src/util/fleet-workdir.ts
2317
- import { tmpdir as tmpdir2 } from "os";
2318
- import { join as join9 } from "path";
2319
- function fleetWorkdir() {
2320
- const home = process.env.HOME?.trim();
2321
- const base = home ? home : tmpdir2();
2322
- return join9(base, ".reddoor-maint", "sites");
2323
- }
2324
-
2325
- // src/cli/commands/audit.ts
2326
27
  function parseOnly(value) {
2327
28
  if (!value) return void 0;
2328
29
  const names = value.split(",").map((s) => s.trim());
@@ -2469,7 +170,7 @@ function applyDeployedUrl(sites, url) {
2469
170
  }
2470
171
  async function runAuditCommand(site, opts) {
2471
172
  const which = parseOnly(opts.only) ?? ALL_AUDIT_NAMES;
2472
- const cwd = opts.cwd ? resolve2(opts.cwd) : process.cwd();
173
+ const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
2473
174
  if (typeof opts.writeAirtable === "string" && opts.fleet !== void 0) {
2474
175
  throw Object.assign(
2475
176
  new Error(
@@ -2513,32 +214,32 @@ async function runAuditCommand(site, opts) {
2513
214
  ${skipNotice}`;
2514
215
  let writeBackFailed = false;
2515
216
  if (opts.writeAirtable !== void 0) {
2516
- const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
2517
- const { listWebsites: listWebsites2 } = await Promise.resolve().then(() => (init_websites(), websites_exports));
217
+ const { openBase, readAirtableConfig } = await import("../../client-EPVO3FEH.js");
218
+ const { listWebsites } = await import("../../websites-X5DLC2PC.js");
2518
219
  if (opts.fleet !== void 0) {
2519
- const { writeFleetAuditsToAirtable: writeFleetAuditsToAirtable2, formatFleetWriteSummary: formatFleetWriteSummary2 } = await Promise.resolve().then(() => (init_write_audits_to_airtable(), write_audits_to_airtable_exports));
2520
- const base = openBase2(readAirtableConfig2());
2521
- const websites = await listWebsites2(base);
2522
- const fleetWrite = await writeFleetAuditsToAirtable2({ base, websites, results });
220
+ const { writeFleetAuditsToAirtable, formatFleetWriteSummary } = await import("../../write-audits-to-airtable-CE4POBAF.js");
221
+ const base = openBase(readAirtableConfig());
222
+ const websites = await listWebsites(base);
223
+ const fleetWrite = await writeFleetAuditsToAirtable({ base, websites, results });
2523
224
  if (fleetWrite.failed.length > 0) writeBackFailed = true;
2524
225
  if (!opts.json) output += `
2525
226
 
2526
- ${formatFleetWriteSummary2(fleetWrite)}`;
227
+ ${formatFleetWriteSummary(fleetWrite)}`;
2527
228
  } else {
2528
- const { resolveSlugFromCwd: resolveSlugFromCwd2 } = await Promise.resolve().then(() => (init_lighthouse_airtable(), lighthouse_airtable_exports));
2529
- const { writeAuditsToAirtable: writeAuditsToAirtable2 } = await Promise.resolve().then(() => (init_write_audits_to_airtable(), write_audits_to_airtable_exports));
2530
- const slug = typeof opts.writeAirtable === "string" && opts.writeAirtable.length > 0 ? opts.writeAirtable : await resolveSlugFromCwd2(cwd);
229
+ const { resolveSlugFromCwd } = await import("../../lighthouse-airtable-4FKYKFH6.js");
230
+ const { writeAuditsToAirtable } = await import("../../write-audits-to-airtable-CE4POBAF.js");
231
+ const slug = typeof opts.writeAirtable === "string" && opts.writeAirtable.length > 0 ? opts.writeAirtable : await resolveSlugFromCwd(cwd);
2531
232
  let writeSummary = null;
2532
233
  await new Listr(
2533
234
  [
2534
235
  {
2535
236
  title: `Write to Airtable[${slug}]`,
2536
237
  task: async (_ctx, task) => {
2537
- const base = openBase2(readAirtableConfig2());
238
+ const base = openBase(readAirtableConfig());
2538
239
  task.output = "loading Websites\u2026";
2539
- const websites = await listWebsites2(base);
240
+ const websites = await listWebsites(base);
2540
241
  task.output = "writing scores\u2026";
2541
- writeSummary = await writeAuditsToAirtable2({ base, websites, slug, results });
242
+ writeSummary = await writeAuditsToAirtable({ base, websites, slug, results });
2542
243
  task.title = `Wrote to Websites[${writeSummary.siteName}] (${writeSummary.writes.length} audit type${writeSummary.writes.length === 1 ? "" : "s"})`;
2543
244
  }
2544
245
  }