@reddoorla/maintenance 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/bin.js +532 -224
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +29 -2
- package/dist/cli/commands/audit.js +381 -135
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.d.ts +23 -32
- package/dist/index.js +366 -195
- package/dist/index.js.map +1 -1
- package/dist/recipes/sync-configs.d.ts +1 -1
- package/dist/recipes/sync-configs.js +31 -0
- package/dist/recipes/sync-configs.js.map +1 -1
- package/dist/{types-D2TnxZOW.d.ts → types-DeKpgkG-.d.ts} +3 -0
- package/package.json +1 -1
package/dist/cli/bin.js
CHANGED
|
@@ -134,6 +134,7 @@ function mapRow(rec) {
|
|
|
134
134
|
a11yViolations: f["A11y Violations"] ?? null,
|
|
135
135
|
depsDrifted: f["Deps Drifted"] ?? null,
|
|
136
136
|
depsMajorBehind: f["Deps Major Behind"] ?? null,
|
|
137
|
+
depsOutdated: f["Deps Outdated"] ?? null,
|
|
137
138
|
securityVulnsCritical: f["Security Vulns Critical"] ?? null,
|
|
138
139
|
securityVulnsHigh: f["Security Vulns High"] ?? null,
|
|
139
140
|
securityVulnsModerate: f["Security Vulns Moderate"] ?? null,
|
|
@@ -179,6 +180,9 @@ async function updateDepsCounts(base, recordId, counts) {
|
|
|
179
180
|
"Deps Drifted": counts.drifted,
|
|
180
181
|
"Deps Major Behind": counts.majorBehind
|
|
181
182
|
};
|
|
183
|
+
if (counts.outdated !== null) {
|
|
184
|
+
fields["Deps Outdated"] = counts.outdated;
|
|
185
|
+
}
|
|
182
186
|
await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
|
|
183
187
|
}
|
|
184
188
|
async function updateSecurityCounts(base, recordId, counts) {
|
|
@@ -212,23 +216,25 @@ function fromAirtableBase(base, opts = {}) {
|
|
|
212
216
|
);
|
|
213
217
|
}
|
|
214
218
|
const websites = await listWebsites(base);
|
|
215
|
-
return websites.filter((w) => w.
|
|
219
|
+
return websites.filter((w) => AUDITABLE_STATUSES.has(w.status ?? "") && w.url.length > 0).map((w) => {
|
|
216
220
|
const slug = siteSlug(w.name);
|
|
217
221
|
const site = {
|
|
218
222
|
path: `${workdir}/${slug}`,
|
|
219
223
|
name: slug,
|
|
224
|
+
deployedUrl: w.url,
|
|
220
225
|
meta: { airtableRowId: w.id, displayName: w.name }
|
|
221
226
|
};
|
|
222
|
-
if (w.url) site.repoUrl = w.url;
|
|
223
227
|
if (w.gitRepo) site.gitRepo = w.gitRepo;
|
|
224
228
|
return site;
|
|
225
229
|
});
|
|
226
230
|
};
|
|
227
231
|
}
|
|
232
|
+
var AUDITABLE_STATUSES;
|
|
228
233
|
var init_airtable = __esm({
|
|
229
234
|
"src/inventory/airtable.ts"() {
|
|
230
235
|
"use strict";
|
|
231
236
|
init_websites();
|
|
237
|
+
AUDITABLE_STATUSES = /* @__PURE__ */ new Set(["maintenance", "launch period"]);
|
|
232
238
|
}
|
|
233
239
|
});
|
|
234
240
|
|
|
@@ -240,7 +246,7 @@ __export(lighthouse_airtable_exports, {
|
|
|
240
246
|
resolveSlugFromCwd: () => resolveSlugFromCwd
|
|
241
247
|
});
|
|
242
248
|
import { readFile as readFile7 } from "fs/promises";
|
|
243
|
-
import { join as
|
|
249
|
+
import { join as join9 } from "path";
|
|
244
250
|
function hasRealScores(result) {
|
|
245
251
|
if (result.audit !== "lighthouse") return false;
|
|
246
252
|
const details = result.details ?? {};
|
|
@@ -265,7 +271,7 @@ function lighthouseScoresFromResult(result) {
|
|
|
265
271
|
}
|
|
266
272
|
async function resolveSlugFromCwd(cwd) {
|
|
267
273
|
try {
|
|
268
|
-
const pkgPath =
|
|
274
|
+
const pkgPath = join9(cwd, "package.json");
|
|
269
275
|
const raw = await readFile7(pkgPath, "utf-8");
|
|
270
276
|
const pkg = JSON.parse(raw);
|
|
271
277
|
if (!pkg.name) throw new Error("package.json has no 'name' field");
|
|
@@ -308,16 +314,19 @@ var init_a11y_airtable = __esm({
|
|
|
308
314
|
// src/audits/deps-airtable.ts
|
|
309
315
|
function hasDepsCounts(result) {
|
|
310
316
|
if (result.audit !== "deps") return false;
|
|
311
|
-
|
|
317
|
+
const d = result.details;
|
|
318
|
+
return Array.isArray(d?.entries);
|
|
312
319
|
}
|
|
313
320
|
function depsCountsFromResult(result) {
|
|
314
321
|
if (result.audit !== "deps") {
|
|
315
322
|
throw new Error(`Expected a 'deps' AuditResult, got '${result.audit}'`);
|
|
316
323
|
}
|
|
317
|
-
const
|
|
324
|
+
const details = result.details ?? {};
|
|
325
|
+
const entries = details.entries ?? [];
|
|
318
326
|
const drifted = entries.filter((e) => e.drift !== "same").length;
|
|
319
327
|
const majorBehind = entries.filter((e) => e.drift === "major").length;
|
|
320
|
-
|
|
328
|
+
const outdated = details.outdated?.outdated ?? null;
|
|
329
|
+
return { drifted, majorBehind, outdated };
|
|
321
330
|
}
|
|
322
331
|
var init_deps_airtable = __esm({
|
|
323
332
|
"src/audits/deps-airtable.ts"() {
|
|
@@ -348,7 +357,9 @@ var init_security_airtable = __esm({
|
|
|
348
357
|
// src/audits/write-audits-to-airtable.ts
|
|
349
358
|
var write_audits_to_airtable_exports = {};
|
|
350
359
|
__export(write_audits_to_airtable_exports, {
|
|
351
|
-
|
|
360
|
+
formatFleetWriteSummary: () => formatFleetWriteSummary,
|
|
361
|
+
writeAuditsToAirtable: () => writeAuditsToAirtable,
|
|
362
|
+
writeFleetAuditsToAirtable: () => writeFleetAuditsToAirtable
|
|
352
363
|
});
|
|
353
364
|
async function writeAuditsToAirtable(args) {
|
|
354
365
|
const { base, websites, slug, results } = args;
|
|
@@ -397,6 +408,38 @@ async function writeAuditsToAirtable(args) {
|
|
|
397
408
|
}
|
|
398
409
|
return { siteName: target.name, writes };
|
|
399
410
|
}
|
|
411
|
+
function formatFleetWriteSummary(result) {
|
|
412
|
+
const wrote = result.written.length;
|
|
413
|
+
const failed = result.failed.length;
|
|
414
|
+
const total = wrote + failed;
|
|
415
|
+
let out = `\u2192 wrote ${wrote} site(s) to Airtable`;
|
|
416
|
+
if (failed > 0) {
|
|
417
|
+
out += `
|
|
418
|
+
\u26A0 ${failed} site(s) not written: ${result.failed.map((f) => `${f.slug} (${f.error})`).join("; ")}`;
|
|
419
|
+
}
|
|
420
|
+
out += `
|
|
421
|
+
FLEET_WRITE_SUMMARY wrote=${wrote} failed=${failed} total=${total}`;
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
async function writeFleetAuditsToAirtable(args) {
|
|
425
|
+
const { base, websites, results } = args;
|
|
426
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
427
|
+
for (const r of results) {
|
|
428
|
+
const arr = bySlug.get(r.site) ?? [];
|
|
429
|
+
arr.push(r);
|
|
430
|
+
bySlug.set(r.site, arr);
|
|
431
|
+
}
|
|
432
|
+
const written = [];
|
|
433
|
+
const failed = [];
|
|
434
|
+
for (const [slug, siteResults] of bySlug) {
|
|
435
|
+
try {
|
|
436
|
+
written.push(await writeAuditsToAirtable({ base, websites, slug, results: siteResults }));
|
|
437
|
+
} catch (e) {
|
|
438
|
+
failed.push({ slug, error: e.message });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return { written, failed };
|
|
442
|
+
}
|
|
400
443
|
var init_write_audits_to_airtable = __esm({
|
|
401
444
|
"src/audits/write-audits-to-airtable.ts"() {
|
|
402
445
|
"use strict";
|
|
@@ -524,18 +567,18 @@ var init_reports = __esm({
|
|
|
524
567
|
// src/reports/maintenance-email/assets/index.ts
|
|
525
568
|
import { readFile as readFile13 } from "fs/promises";
|
|
526
569
|
import { existsSync as existsSync3 } from "fs";
|
|
527
|
-
import { dirname as dirname4, join as
|
|
570
|
+
import { dirname as dirname4, join as join24 } from "path";
|
|
528
571
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
529
572
|
function resolveAssetsDir() {
|
|
530
573
|
if (cachedAssetsDir) return cachedAssetsDir;
|
|
531
574
|
let dir = dirname4(fileURLToPath2(import.meta.url));
|
|
532
575
|
while (true) {
|
|
533
|
-
const srcCandidate =
|
|
576
|
+
const srcCandidate = join24(dir, "src", "reports", "maintenance-email", "assets", "check.png");
|
|
534
577
|
if (existsSync3(srcCandidate)) {
|
|
535
578
|
cachedAssetsDir = dirname4(srcCandidate);
|
|
536
579
|
return cachedAssetsDir;
|
|
537
580
|
}
|
|
538
|
-
const distCandidate =
|
|
581
|
+
const distCandidate = join24(dir, "dist", "reports", "maintenance-email", "assets", "check.png");
|
|
539
582
|
if (existsSync3(distCandidate)) {
|
|
540
583
|
cachedAssetsDir = dirname4(distCandidate);
|
|
541
584
|
return cachedAssetsDir;
|
|
@@ -552,8 +595,8 @@ function resolveAssetsDir() {
|
|
|
552
595
|
async function loadBundledImages() {
|
|
553
596
|
const assetsDir = resolveAssetsDir();
|
|
554
597
|
const [check, blurred] = await Promise.all([
|
|
555
|
-
readFile13(
|
|
556
|
-
readFile13(
|
|
598
|
+
readFile13(join24(assetsDir, "check.png")),
|
|
599
|
+
readFile13(join24(assetsDir, "blurredTests.jpg"))
|
|
557
600
|
]);
|
|
558
601
|
return {
|
|
559
602
|
check: {
|
|
@@ -769,7 +812,7 @@ function buildMjml(data) {
|
|
|
769
812
|
<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">Just hit reply.</mj-text>
|
|
770
813
|
<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" padding-top="0px" line-height="30px" padding-bottom="36px">We're here to help in any way we can.</mj-text>
|
|
771
814
|
<mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
|
|
772
|
-
<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" line-height="20px" font-style="italic">Copyright ${(/* @__PURE__ */ new Date()).
|
|
815
|
+
<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" line-height="20px" font-style="italic">Copyright ${(/* @__PURE__ */ new Date()).getUTCFullYear()} Reddoor Creative, LLC. All rights reserved.</mj-text>
|
|
773
816
|
<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="700" line-height="16px" padding-top="0" padding-bottom="0px">Our mailing address is:</mj-text>
|
|
774
817
|
<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">Reddoor Creative, LLC</mj-text>
|
|
775
818
|
<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">29027 Dapper Dan</mj-text>
|
|
@@ -924,6 +967,14 @@ __export(orchestrate_exports, {
|
|
|
924
967
|
function monthYear(d) {
|
|
925
968
|
return `${MONTHS2[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
|
|
926
969
|
}
|
|
970
|
+
function toInlineAttachment(a) {
|
|
971
|
+
return {
|
|
972
|
+
filename: a.filename,
|
|
973
|
+
content: Buffer.from(a.bytes).toString("base64"),
|
|
974
|
+
contentType: a.contentType,
|
|
975
|
+
inlineContentId: a.cid
|
|
976
|
+
};
|
|
977
|
+
}
|
|
927
978
|
async function sendApprovedReports(options = {}) {
|
|
928
979
|
const base = openBase(readAirtableConfig());
|
|
929
980
|
const client = options.resend ?? defaultResendClient();
|
|
@@ -955,7 +1006,9 @@ async function sendOne(client, base, site, report) {
|
|
|
955
1006
|
throw new Error(`Site '${site.name}' has no Header image set on the Websites row`);
|
|
956
1007
|
}
|
|
957
1008
|
if (!report.lighthouse) {
|
|
958
|
-
throw new Error(
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
`Report ${report.reportId} has no Lighthouse scores \u2014 all four cells (Lighthouse \u2014 Performance / Accessibility / Best Practices / SEO) must be numeric on the Reports row; one non-numeric or blank cell nulls all four`
|
|
1011
|
+
);
|
|
959
1012
|
}
|
|
960
1013
|
const original = await fetchAttachmentBytes(site.headerImage.url);
|
|
961
1014
|
const header = await prepareHeaderImage(original.bytes);
|
|
@@ -991,7 +1044,7 @@ async function sendOne(client, base, site, report) {
|
|
|
991
1044
|
for (const addr of to) {
|
|
992
1045
|
if (!isProbablyEmail(addr)) {
|
|
993
1046
|
throw new Error(
|
|
994
|
-
`Site '${site.name}' recipient is malformed: ${addr} \u2014 fix Report recipients (To) or point of contact in Airtable`
|
|
1047
|
+
`Site '${site.name}' recipient is malformed: ${addr} \u2014 use a bare address only (no \`Name <addr>\` display-name syntax); fix Report recipients (To) or point of contact in Airtable`
|
|
995
1048
|
);
|
|
996
1049
|
}
|
|
997
1050
|
}
|
|
@@ -1012,27 +1065,27 @@ async function sendOne(client, base, site, report) {
|
|
|
1012
1065
|
subject,
|
|
1013
1066
|
html,
|
|
1014
1067
|
attachments: [
|
|
1015
|
-
{
|
|
1068
|
+
toInlineAttachment({
|
|
1069
|
+
bytes: header.bytes,
|
|
1016
1070
|
filename: `${cidName}.jpg`,
|
|
1017
|
-
content: Buffer.from(header.bytes).toString("base64"),
|
|
1018
1071
|
contentType: header.contentType,
|
|
1019
|
-
|
|
1020
|
-
},
|
|
1072
|
+
cid: cidName
|
|
1073
|
+
}),
|
|
1021
1074
|
// Bundled images referenced via cid:rd-check-png / cid:rd-blurred-tests-jpg
|
|
1022
1075
|
// in the template. Attached inline so the email is self-contained — no
|
|
1023
1076
|
// external CDN dependency, no image-blocked broken icons in webmail.
|
|
1024
|
-
{
|
|
1077
|
+
toInlineAttachment({
|
|
1078
|
+
bytes: bundled.check.bytes,
|
|
1025
1079
|
filename: bundled.check.filename,
|
|
1026
|
-
content: Buffer.from(bundled.check.bytes).toString("base64"),
|
|
1027
1080
|
contentType: bundled.check.contentType,
|
|
1028
|
-
|
|
1029
|
-
},
|
|
1030
|
-
{
|
|
1081
|
+
cid: bundled.check.cid
|
|
1082
|
+
}),
|
|
1083
|
+
toInlineAttachment({
|
|
1084
|
+
bytes: bundled.blurred.bytes,
|
|
1031
1085
|
filename: bundled.blurred.filename,
|
|
1032
|
-
content: Buffer.from(bundled.blurred.bytes).toString("base64"),
|
|
1033
1086
|
contentType: bundled.blurred.contentType,
|
|
1034
|
-
|
|
1035
|
-
}
|
|
1087
|
+
cid: bundled.blurred.cid
|
|
1088
|
+
})
|
|
1036
1089
|
],
|
|
1037
1090
|
// Stable across retries of the same row — if Airtable stamping fails after a
|
|
1038
1091
|
// successful Resend, the next --send-ready replays with the same key and
|
|
@@ -1110,36 +1163,74 @@ import { Listr } from "listr2";
|
|
|
1110
1163
|
|
|
1111
1164
|
// src/audits/util/spawn.ts
|
|
1112
1165
|
import { spawn } from "child_process";
|
|
1113
|
-
var
|
|
1114
|
-
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
})
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1166
|
+
var TRUNCATION_MARKER = "\n\u2026[output truncated]";
|
|
1167
|
+
function makeSpawn(internals = {}) {
|
|
1168
|
+
const spawnImpl = internals.spawnImpl ?? spawn;
|
|
1169
|
+
const killImpl = internals.killImpl ?? ((pid, sig) => process.kill(pid, sig));
|
|
1170
|
+
const killGraceMs = internals.killGraceMs ?? 5e3;
|
|
1171
|
+
const maxOutputBytes = internals.maxOutputBytes ?? 10 * 1024 * 1024;
|
|
1172
|
+
return (cmd, args, opts = {}) => new Promise((resolve11, reject) => {
|
|
1173
|
+
const streaming = opts.streaming === true;
|
|
1174
|
+
const child = spawnImpl(cmd, [...args], {
|
|
1175
|
+
cwd: opts.cwd,
|
|
1176
|
+
env: opts.env ?? process.env,
|
|
1177
|
+
stdio: streaming ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"],
|
|
1178
|
+
// Detach ONLY when a timeout can fire: the child then leads its own
|
|
1179
|
+
// process group, so the timeout can kill the WHOLE tree (vite, and
|
|
1180
|
+
// Chromium under lhci/playwright) via process.kill(-pid), not just the
|
|
1181
|
+
// npx/pnpm wrapper. Without it, killing the wrapper orphaned the
|
|
1182
|
+
// grandchildren — a zombie vite squatting its port, Chrome left running.
|
|
1183
|
+
// We do NOT detach timeout-less streaming calls (pnpm install/up):
|
|
1184
|
+
// detaching gains nothing there (no timeout → no group-kill) and would
|
|
1185
|
+
// break terminal Ctrl-C, which only reaches the foreground group — i.e.
|
|
1186
|
+
// it would re-orphan the very children this guards. We never unref() the
|
|
1187
|
+
// child since we still await it.
|
|
1188
|
+
detached: opts.timeoutMs !== void 0
|
|
1189
|
+
});
|
|
1190
|
+
const cap = (acc, chunk) => {
|
|
1191
|
+
if (acc.length >= maxOutputBytes) return acc;
|
|
1192
|
+
const next = acc + chunk;
|
|
1193
|
+
return next.length > maxOutputBytes ? next.slice(0, maxOutputBytes) + TRUNCATION_MARKER : next;
|
|
1194
|
+
};
|
|
1195
|
+
let stdout = "";
|
|
1196
|
+
let stderr = "";
|
|
1197
|
+
if (!streaming) {
|
|
1198
|
+
child.stdout?.on("data", (chunk) => stdout = cap(stdout, String(chunk)));
|
|
1199
|
+
child.stderr?.on("data", (chunk) => stderr = cap(stderr, String(chunk)));
|
|
1200
|
+
}
|
|
1201
|
+
const killGroup = (sig) => {
|
|
1202
|
+
if (child.pid === void 0) return;
|
|
1203
|
+
try {
|
|
1204
|
+
killImpl(-child.pid, sig);
|
|
1205
|
+
} catch {
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
let killTimer;
|
|
1209
|
+
const timer = opts.timeoutMs ? setTimeout(() => {
|
|
1210
|
+
killGroup("SIGTERM");
|
|
1211
|
+
killTimer = setTimeout(() => killGroup("SIGKILL"), killGraceMs);
|
|
1212
|
+
killTimer.unref();
|
|
1213
|
+
reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));
|
|
1214
|
+
}, opts.timeoutMs) : void 0;
|
|
1215
|
+
const clearTimers = () => {
|
|
1216
|
+
if (timer) clearTimeout(timer);
|
|
1217
|
+
if (killTimer) clearTimeout(killTimer);
|
|
1218
|
+
};
|
|
1219
|
+
child.on("error", (err) => {
|
|
1220
|
+
clearTimers();
|
|
1221
|
+
reject(err);
|
|
1222
|
+
});
|
|
1223
|
+
child.on("close", (code) => {
|
|
1224
|
+
clearTimers();
|
|
1225
|
+
resolve11({ code: code ?? -1, stdout, stderr });
|
|
1226
|
+
});
|
|
1137
1227
|
});
|
|
1138
|
-
}
|
|
1228
|
+
}
|
|
1229
|
+
var defaultSpawn = makeSpawn();
|
|
1139
1230
|
|
|
1140
1231
|
// src/audits/deps.ts
|
|
1141
1232
|
import { readFile } from "fs/promises";
|
|
1142
|
-
import { join as
|
|
1233
|
+
import { join as join3 } from "path";
|
|
1143
1234
|
|
|
1144
1235
|
// src/util/site.ts
|
|
1145
1236
|
function siteLabel(site) {
|
|
@@ -1185,6 +1276,47 @@ var baselineVersions = {
|
|
|
1185
1276
|
"@zerodevx/svelte-img": "^2.1.2"
|
|
1186
1277
|
};
|
|
1187
1278
|
|
|
1279
|
+
// src/audits/deps-outdated.ts
|
|
1280
|
+
import { stat } from "fs/promises";
|
|
1281
|
+
import { join as join2 } from "path";
|
|
1282
|
+
async function exists(path) {
|
|
1283
|
+
try {
|
|
1284
|
+
await stat(path);
|
|
1285
|
+
return true;
|
|
1286
|
+
} catch {
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function majorOf(version2) {
|
|
1291
|
+
const head = version2.replace(/^[\^~]/, "").split(".")[0] ?? "0";
|
|
1292
|
+
const n = Number.parseInt(head, 10);
|
|
1293
|
+
return Number.isNaN(n) ? 0 : n;
|
|
1294
|
+
}
|
|
1295
|
+
async function scanOutdated(sitePath, spawn2) {
|
|
1296
|
+
if (!await exists(join2(sitePath, "pnpm-lock.yaml"))) return null;
|
|
1297
|
+
try {
|
|
1298
|
+
if (!await exists(join2(sitePath, "node_modules"))) {
|
|
1299
|
+
const install = await spawn2("pnpm", ["install", "--frozen-lockfile"], {
|
|
1300
|
+
cwd: sitePath,
|
|
1301
|
+
timeoutMs: 18e4
|
|
1302
|
+
});
|
|
1303
|
+
if (install.code !== 0) return null;
|
|
1304
|
+
}
|
|
1305
|
+
const res = await spawn2("pnpm", ["outdated", "--json"], {
|
|
1306
|
+
cwd: sitePath,
|
|
1307
|
+
timeoutMs: 6e4
|
|
1308
|
+
});
|
|
1309
|
+
const parsed = JSON.parse(res.stdout || "{}");
|
|
1310
|
+
const entries = Object.values(parsed);
|
|
1311
|
+
return {
|
|
1312
|
+
outdated: entries.length,
|
|
1313
|
+
major: entries.filter((e) => e.current && e.latest && majorOf(e.latest) > majorOf(e.current)).length
|
|
1314
|
+
};
|
|
1315
|
+
} catch {
|
|
1316
|
+
return null;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1188
1320
|
// src/audits/deps.ts
|
|
1189
1321
|
function stripCaret(range) {
|
|
1190
1322
|
return range.replace(/^[\^~]/, "");
|
|
@@ -1206,7 +1338,7 @@ function compareSemver(actual, baseline) {
|
|
|
1206
1338
|
return "same";
|
|
1207
1339
|
}
|
|
1208
1340
|
async function depsAudit(ctx) {
|
|
1209
|
-
const pkgPath =
|
|
1341
|
+
const pkgPath = join3(ctx.site.path, "package.json");
|
|
1210
1342
|
let pkgRaw;
|
|
1211
1343
|
try {
|
|
1212
1344
|
pkgRaw = await readFile(pkgPath, "utf-8");
|
|
@@ -1224,35 +1356,37 @@ async function depsAudit(ctx) {
|
|
|
1224
1356
|
...pkg.dependencies ?? {},
|
|
1225
1357
|
...pkg.devDependencies ?? {}
|
|
1226
1358
|
};
|
|
1227
|
-
const
|
|
1359
|
+
const entries = [];
|
|
1228
1360
|
for (const [name, baseline] of Object.entries(baselineVersions)) {
|
|
1229
1361
|
const actual = installed[name];
|
|
1230
1362
|
if (!actual) continue;
|
|
1231
|
-
|
|
1363
|
+
entries.push({
|
|
1232
1364
|
pkg: name,
|
|
1233
1365
|
baseline,
|
|
1234
1366
|
actual,
|
|
1235
1367
|
drift: compareSemver(actual, baseline)
|
|
1236
1368
|
});
|
|
1237
1369
|
}
|
|
1238
|
-
const anyMajor =
|
|
1239
|
-
const anyMinor =
|
|
1240
|
-
const anyNewer =
|
|
1370
|
+
const anyMajor = entries.some((d) => d.drift === "major");
|
|
1371
|
+
const anyMinor = entries.some((d) => d.drift === "minor");
|
|
1372
|
+
const anyNewer = entries.some((d) => d.drift === "newer");
|
|
1241
1373
|
const status = anyMajor ? "fail" : anyMinor || anyNewer ? "warn" : "pass";
|
|
1242
|
-
const
|
|
1374
|
+
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`;
|
|
1375
|
+
const outdated = await scanOutdated(ctx.site.path, ctx.spawn ?? defaultSpawn);
|
|
1376
|
+
const summary = outdated ? `${driftSummary}; ${outdated.outdated} outdated install(s) (${outdated.major} major)` : driftSummary;
|
|
1243
1377
|
return {
|
|
1244
1378
|
audit: "deps",
|
|
1245
1379
|
site: siteLabel(ctx.site),
|
|
1246
1380
|
status,
|
|
1247
1381
|
summary,
|
|
1248
|
-
details
|
|
1382
|
+
details: { entries, outdated }
|
|
1249
1383
|
};
|
|
1250
1384
|
}
|
|
1251
1385
|
|
|
1252
1386
|
// src/audits/lint.ts
|
|
1253
1387
|
import { existsSync } from "fs";
|
|
1254
1388
|
import { readFile as readFile2 } from "fs/promises";
|
|
1255
|
-
import { join as
|
|
1389
|
+
import { join as join4 } from "path";
|
|
1256
1390
|
import { ESLint } from "eslint";
|
|
1257
1391
|
import { check as prettierCheck, resolveConfig as prettierResolveConfig } from "prettier";
|
|
1258
1392
|
import { glob } from "tinyglobby";
|
|
@@ -1263,7 +1397,7 @@ async function listFiles(cwd) {
|
|
|
1263
1397
|
}
|
|
1264
1398
|
async function lintAudit(ctx) {
|
|
1265
1399
|
const { site } = ctx;
|
|
1266
|
-
const configPath =
|
|
1400
|
+
const configPath = join4(site.path, "eslint.config.js");
|
|
1267
1401
|
if (!existsSync(configPath)) {
|
|
1268
1402
|
return {
|
|
1269
1403
|
audit: "lint",
|
|
@@ -1283,7 +1417,7 @@ async function lintAudit(ctx) {
|
|
|
1283
1417
|
const eslintWarnings = eslintResults.reduce((n, r) => n + r.warningCount, 0);
|
|
1284
1418
|
const prettierUnformatted = [];
|
|
1285
1419
|
for (const rel of relFiles) {
|
|
1286
|
-
const absForResolve =
|
|
1420
|
+
const absForResolve = join4(site.path, rel);
|
|
1287
1421
|
const source = await readFile2(absForResolve, "utf-8");
|
|
1288
1422
|
const options = await prettierResolveConfig(absForResolve) ?? {};
|
|
1289
1423
|
const ok = await prettierCheck(source, { ...options, filepath: absForResolve });
|
|
@@ -1447,7 +1581,7 @@ async function securityAudit(ctx) {
|
|
|
1447
1581
|
// src/audits/lighthouse.ts
|
|
1448
1582
|
import { readFile as readFile4, writeFile, mkdtemp, rm, readdir } from "fs/promises";
|
|
1449
1583
|
import { tmpdir } from "os";
|
|
1450
|
-
import { join as
|
|
1584
|
+
import { join as join6 } from "path";
|
|
1451
1585
|
|
|
1452
1586
|
// src/configs/lighthouse.ts
|
|
1453
1587
|
var lighthouseConfig = {
|
|
@@ -1482,11 +1616,11 @@ var lighthouseConfig = {
|
|
|
1482
1616
|
|
|
1483
1617
|
// src/audits/util/site-config.ts
|
|
1484
1618
|
import { readFile as readFile3 } from "fs/promises";
|
|
1485
|
-
import { join as
|
|
1619
|
+
import { join as join5 } from "path";
|
|
1486
1620
|
async function readSiteConfig(sitePath) {
|
|
1487
1621
|
let raw;
|
|
1488
1622
|
try {
|
|
1489
|
-
raw = await readFile3(
|
|
1623
|
+
raw = await readFile3(join5(sitePath, "package.json"), "utf-8");
|
|
1490
1624
|
} catch {
|
|
1491
1625
|
return {};
|
|
1492
1626
|
}
|
|
@@ -1547,7 +1681,7 @@ async function readLhrEntries(resultsDir) {
|
|
|
1547
1681
|
const entries = [];
|
|
1548
1682
|
for (const f of files) {
|
|
1549
1683
|
if (!f.startsWith("lhr-") || !f.endsWith(".json")) continue;
|
|
1550
|
-
const lhr = await readJsonMaybe(
|
|
1684
|
+
const lhr = await readJsonMaybe(join6(resultsDir, f));
|
|
1551
1685
|
if (!lhr || !lhr.categories) continue;
|
|
1552
1686
|
const summary = {};
|
|
1553
1687
|
for (const [k, v] of Object.entries(lhr.categories)) {
|
|
@@ -1583,10 +1717,35 @@ function categoryFromAssertion(a) {
|
|
|
1583
1717
|
function messageForAssertion(a) {
|
|
1584
1718
|
return `${a.name} ${a.operator} ${a.expected} (actual: ${a.actual.toFixed(2)})`;
|
|
1585
1719
|
}
|
|
1586
|
-
async function
|
|
1587
|
-
const
|
|
1588
|
-
|
|
1589
|
-
|
|
1720
|
+
async function parseLhciResults(resultsDir, label, raw) {
|
|
1721
|
+
const manifest = await readLhrEntries(resultsDir);
|
|
1722
|
+
if (manifest.length === 0) {
|
|
1723
|
+
return {
|
|
1724
|
+
audit: "lighthouse",
|
|
1725
|
+
site: label,
|
|
1726
|
+
status: "fail",
|
|
1727
|
+
summary: `lighthouse: no lhr-*.json written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
const assertionResults = await readJsonMaybe(join6(resultsDir, "assertion-results.json")) ?? [];
|
|
1731
|
+
const failed = assertionResults.filter((a) => !a.passed);
|
|
1732
|
+
const assertions = failed.map((a) => ({
|
|
1733
|
+
category: categoryFromAssertion(a),
|
|
1734
|
+
level: a.level,
|
|
1735
|
+
message: messageForAssertion(a)
|
|
1736
|
+
}));
|
|
1737
|
+
const anyError = assertions.some((a) => a.level === "error");
|
|
1738
|
+
const anyWarn = assertions.some((a) => a.level === "warn");
|
|
1739
|
+
const status = anyError ? "fail" : anyWarn ? "warn" : "pass";
|
|
1740
|
+
const normalized = {
|
|
1741
|
+
summary: averageSummaries(manifest),
|
|
1742
|
+
assertionsFailed: failed.length,
|
|
1743
|
+
assertions
|
|
1744
|
+
};
|
|
1745
|
+
const summary = status === "pass" ? "lighthouse: all categories passing" : `lighthouse: ${failed.length} assertion(s) failed`;
|
|
1746
|
+
return { audit: "lighthouse", site: label, status, summary, details: normalized };
|
|
1747
|
+
}
|
|
1748
|
+
async function checkoutLighthouse(spawn2, site, label) {
|
|
1590
1749
|
const siteCfg = await readSiteConfig(site.path);
|
|
1591
1750
|
const port = await findFreePort();
|
|
1592
1751
|
const baseUrl = siteCfg.lighthouseUrl ?? lighthouseConfig.ci.collect.url[0];
|
|
@@ -1601,19 +1760,15 @@ async function lighthouseAudit(ctx) {
|
|
|
1601
1760
|
}
|
|
1602
1761
|
}
|
|
1603
1762
|
};
|
|
1604
|
-
const configDir = await mkdtemp(
|
|
1605
|
-
const configPath =
|
|
1763
|
+
const configDir = await mkdtemp(join6(tmpdir(), "reddoor-lhci-"));
|
|
1764
|
+
const configPath = join6(configDir, "lighthouserc.json");
|
|
1606
1765
|
await writeFile(configPath, JSON.stringify(resolvedConfig), "utf-8");
|
|
1607
|
-
const resultsDir =
|
|
1766
|
+
const resultsDir = join6(site.path, ".lighthouseci");
|
|
1608
1767
|
await rm(resultsDir, { recursive: true, force: true });
|
|
1609
1768
|
let raw;
|
|
1610
1769
|
try {
|
|
1611
1770
|
raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
|
|
1612
1771
|
cwd: site.path,
|
|
1613
|
-
// lhci autorun boots the site's dev server, downloads Chrome on first
|
|
1614
|
-
// use, and runs the audit — easily 2–3 min on a cold tree. The shared
|
|
1615
|
-
// 30 s default in runAudits is fine for deps/lint/security but starves
|
|
1616
|
-
// lhci.
|
|
1617
1772
|
timeoutMs: 5 * 6e4
|
|
1618
1773
|
});
|
|
1619
1774
|
} catch (err) {
|
|
@@ -1630,43 +1785,64 @@ async function lighthouseAudit(ctx) {
|
|
|
1630
1785
|
throw err;
|
|
1631
1786
|
}
|
|
1632
1787
|
await rm(configDir, { recursive: true, force: true });
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
const status = anyError ? "fail" : anyWarn ? "warn" : "pass";
|
|
1652
|
-
const normalized = {
|
|
1653
|
-
summary: averageSummaries(manifest),
|
|
1654
|
-
assertionsFailed: failed.length,
|
|
1655
|
-
assertions
|
|
1656
|
-
};
|
|
1657
|
-
const summary = status === "pass" ? "lighthouse: all categories passing" : `lighthouse: ${failed.length} assertion(s) failed`;
|
|
1658
|
-
return {
|
|
1659
|
-
audit: "lighthouse",
|
|
1660
|
-
site: label,
|
|
1661
|
-
status,
|
|
1662
|
-
summary,
|
|
1663
|
-
details: normalized
|
|
1788
|
+
return parseLhciResults(resultsDir, label, raw);
|
|
1789
|
+
}
|
|
1790
|
+
async function deployedLighthouse(spawn2, deployedUrl, label) {
|
|
1791
|
+
const workDir = await mkdtemp(join6(tmpdir(), "reddoor-lh-deployed-"));
|
|
1792
|
+
const resolvedConfig = {
|
|
1793
|
+
ci: {
|
|
1794
|
+
// Deliberately NOT spread from lighthouseConfig.ci.collect: deployed mode
|
|
1795
|
+
// must omit startServerCommand and the dev-server settings entirely.
|
|
1796
|
+
collect: {
|
|
1797
|
+
url: [deployedUrl],
|
|
1798
|
+
// 3 runs to damp Lighthouse's run-to-run variance; parseLhciResults
|
|
1799
|
+
// averages the lhr files. (Median is a tracked future refinement.)
|
|
1800
|
+
numberOfRuns: 3,
|
|
1801
|
+
settings: { preset: "desktop", skipAudits: ["uses-http2"] }
|
|
1802
|
+
},
|
|
1803
|
+
assert: lighthouseConfig.ci.assert,
|
|
1804
|
+
upload: { target: "filesystem", outputDir: join6(workDir, "lhci-report") }
|
|
1805
|
+
}
|
|
1664
1806
|
};
|
|
1807
|
+
const configPath = join6(workDir, "lighthouserc.json");
|
|
1808
|
+
await writeFile(configPath, JSON.stringify(resolvedConfig), "utf-8");
|
|
1809
|
+
const resultsDir = join6(workDir, ".lighthouseci");
|
|
1810
|
+
let raw;
|
|
1811
|
+
try {
|
|
1812
|
+
raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
|
|
1813
|
+
cwd: workDir,
|
|
1814
|
+
// No dev-server boot: ~30s/run × 3 + first-use Chrome download headroom.
|
|
1815
|
+
timeoutMs: 3 * 6e4
|
|
1816
|
+
});
|
|
1817
|
+
} catch (err) {
|
|
1818
|
+
await rm(workDir, { recursive: true, force: true });
|
|
1819
|
+
const e = err;
|
|
1820
|
+
if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
|
|
1821
|
+
return {
|
|
1822
|
+
audit: "lighthouse",
|
|
1823
|
+
site: label,
|
|
1824
|
+
status: "skip",
|
|
1825
|
+
summary: "npx/@lhci/cli not available"
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
throw err;
|
|
1829
|
+
}
|
|
1830
|
+
try {
|
|
1831
|
+
return await parseLhciResults(resultsDir, label, raw);
|
|
1832
|
+
} finally {
|
|
1833
|
+
await rm(workDir, { recursive: true, force: true });
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
async function lighthouseAudit(ctx) {
|
|
1837
|
+
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
1838
|
+
const site = ctx.site;
|
|
1839
|
+
const label = siteLabel(site);
|
|
1840
|
+
return site.deployedUrl ? deployedLighthouse(spawn2, site.deployedUrl, label) : checkoutLighthouse(spawn2, site, label);
|
|
1665
1841
|
}
|
|
1666
1842
|
|
|
1667
1843
|
// src/audits/a11y.ts
|
|
1668
1844
|
import { readFile as readFile5, writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
1669
|
-
import { join as
|
|
1845
|
+
import { join as join7 } from "path";
|
|
1670
1846
|
|
|
1671
1847
|
// src/configs/playwright-a11y.ts
|
|
1672
1848
|
import { defineConfig, devices } from "@playwright/test";
|
|
@@ -1832,14 +2008,14 @@ async function a11yAudit(ctx) {
|
|
|
1832
2008
|
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
1833
2009
|
const site = ctx.site;
|
|
1834
2010
|
const label = siteLabel(site);
|
|
1835
|
-
const specDir = await mkdtemp2(
|
|
1836
|
-
const specPath =
|
|
2011
|
+
const specDir = await mkdtemp2(join7(site.path, ".reddoor-a11y-spec-"));
|
|
2012
|
+
const specPath = join7(specDir, "a11y.spec.ts");
|
|
1837
2013
|
await writeFile2(specPath, buildSpec(), "utf-8");
|
|
1838
2014
|
const port = await findFreePort();
|
|
1839
|
-
const configPath =
|
|
2015
|
+
const configPath = join7(specDir, "playwright.config.ts");
|
|
1840
2016
|
await writeFile2(configPath, buildPlaywrightConfig(port, site.path), "utf-8");
|
|
1841
|
-
const resultsPath =
|
|
1842
|
-
await rm2(
|
|
2017
|
+
const resultsPath = join7(site.path, RESULTS_REL);
|
|
2018
|
+
await rm2(join7(site.path, ".reddoor-a11y"), { recursive: true, force: true });
|
|
1843
2019
|
let raw;
|
|
1844
2020
|
try {
|
|
1845
2021
|
raw = await spawn2(
|
|
@@ -1961,6 +2137,8 @@ function validate(raw) {
|
|
|
1961
2137
|
const site = { path: e.path };
|
|
1962
2138
|
if (typeof e.name === "string") site.name = e.name;
|
|
1963
2139
|
if (typeof e.repoUrl === "string") site.repoUrl = e.repoUrl;
|
|
2140
|
+
if (typeof e.gitRepo === "string") site.gitRepo = e.gitRepo;
|
|
2141
|
+
if (typeof e.deployedUrl === "string") site.deployedUrl = e.deployedUrl;
|
|
1964
2142
|
if (typeof e.meta === "object" && e.meta !== null) {
|
|
1965
2143
|
site.meta = e.meta;
|
|
1966
2144
|
}
|
|
@@ -2014,8 +2192,8 @@ async function resolveSites(input) {
|
|
|
2014
2192
|
}
|
|
2015
2193
|
|
|
2016
2194
|
// src/cli/fleet/clone-if-needed.ts
|
|
2017
|
-
import { stat, readdir as readdir2, mkdir } from "fs/promises";
|
|
2018
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2195
|
+
import { stat as stat2, readdir as readdir2, mkdir } from "fs/promises";
|
|
2196
|
+
import { isAbsolute as isAbsolute2, join as join8 } from "path";
|
|
2019
2197
|
function deriveNameFromRepoUrl(repoUrl) {
|
|
2020
2198
|
const slash = repoUrl.split("/").pop() ?? repoUrl;
|
|
2021
2199
|
return slash.replace(/\.git$/, "");
|
|
@@ -2040,7 +2218,7 @@ function assertSafeRepoUrl(repoUrl) {
|
|
|
2040
2218
|
}
|
|
2041
2219
|
async function isNonEmptyDir(path) {
|
|
2042
2220
|
try {
|
|
2043
|
-
const s = await
|
|
2221
|
+
const s = await stat2(path);
|
|
2044
2222
|
if (!s.isDirectory()) return false;
|
|
2045
2223
|
const entries = await readdir2(path);
|
|
2046
2224
|
return entries.length > 0;
|
|
@@ -2048,21 +2226,33 @@ async function isNonEmptyDir(path) {
|
|
|
2048
2226
|
return false;
|
|
2049
2227
|
}
|
|
2050
2228
|
}
|
|
2229
|
+
var GIT_REPO_RE = /^[\w.-]+\/[\w.-]+$/;
|
|
2230
|
+
function resolveCloneUrl(site) {
|
|
2231
|
+
if (site.repoUrl) return site.repoUrl;
|
|
2232
|
+
if (!site.gitRepo) return void 0;
|
|
2233
|
+
if (!GIT_REPO_RE.test(site.gitRepo)) {
|
|
2234
|
+
throw new Error(`unsafe gitRepo: expected "owner/repo" (got: ${JSON.stringify(site.gitRepo)})`);
|
|
2235
|
+
}
|
|
2236
|
+
return `https://github.com/${site.gitRepo}.git`;
|
|
2237
|
+
}
|
|
2051
2238
|
async function cloneIfNeeded(site, opts) {
|
|
2052
2239
|
if (await isNonEmptyDir(site.path)) return site;
|
|
2053
|
-
|
|
2054
|
-
|
|
2240
|
+
const repoUrl = resolveCloneUrl(site);
|
|
2241
|
+
if (!repoUrl) {
|
|
2242
|
+
throw new Error(
|
|
2243
|
+
`site path does not exist (${site.path}) and no repoUrl or gitRepo is set \u2014 cannot clone`
|
|
2244
|
+
);
|
|
2055
2245
|
}
|
|
2056
|
-
const name = site.name ?? deriveNameFromRepoUrl(
|
|
2246
|
+
const name = site.name ?? deriveNameFromRepoUrl(repoUrl);
|
|
2057
2247
|
assertSafeName(name);
|
|
2058
|
-
assertSafeRepoUrl(
|
|
2059
|
-
const target =
|
|
2248
|
+
assertSafeRepoUrl(repoUrl);
|
|
2249
|
+
const target = join8(opts.workdir, name);
|
|
2060
2250
|
await mkdir(opts.workdir, { recursive: true });
|
|
2061
2251
|
if (await isNonEmptyDir(target)) {
|
|
2062
2252
|
return { ...site, name, path: target };
|
|
2063
2253
|
}
|
|
2064
2254
|
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
2065
|
-
const result = await spawn2("git", ["clone", "--",
|
|
2255
|
+
const result = await spawn2("git", ["clone", "--", repoUrl, target], {
|
|
2066
2256
|
cwd: opts.workdir,
|
|
2067
2257
|
timeoutMs: 5 * 6e4
|
|
2068
2258
|
});
|
|
@@ -2106,7 +2296,17 @@ function formatDuration(ms) {
|
|
|
2106
2296
|
const s = totalSeconds % 60;
|
|
2107
2297
|
return `${m}m${s.toString().padStart(2, "0")}s`;
|
|
2108
2298
|
}
|
|
2109
|
-
function
|
|
2299
|
+
function parseConcurrency(value) {
|
|
2300
|
+
if (value === void 0) return true;
|
|
2301
|
+
const n = Number(value);
|
|
2302
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
2303
|
+
throw Object.assign(new Error(`--concurrency must be a positive integer, got "${value}"`), {
|
|
2304
|
+
exitCode: 2
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
return n;
|
|
2308
|
+
}
|
|
2309
|
+
function buildAuditTasks(sites, which, results, renderer, concurrency) {
|
|
2110
2310
|
const singleSite = sites.length === 1;
|
|
2111
2311
|
if (singleSite) {
|
|
2112
2312
|
const site = sites[0];
|
|
@@ -2152,7 +2352,7 @@ function buildAuditTasks(sites, which, results, renderer) {
|
|
|
2152
2352
|
}
|
|
2153
2353
|
};
|
|
2154
2354
|
}),
|
|
2155
|
-
{ concurrent:
|
|
2355
|
+
{ concurrent: concurrency, exitOnError: false, renderer }
|
|
2156
2356
|
);
|
|
2157
2357
|
}
|
|
2158
2358
|
function formatWriteSummary(summary) {
|
|
@@ -2177,13 +2377,46 @@ ${lines.join("\n")}`;
|
|
|
2177
2377
|
function rendererFor(json) {
|
|
2178
2378
|
return json ? "silent" : "default";
|
|
2179
2379
|
}
|
|
2380
|
+
function deployedUrlNotice(which, url, cwd) {
|
|
2381
|
+
if (url === void 0) return null;
|
|
2382
|
+
const others = which.filter((n) => n !== "lighthouse");
|
|
2383
|
+
if (others.length === 0) return null;
|
|
2384
|
+
return `note: --url only affects lighthouse; ${others.join(", ")} ran against the local checkout at ${cwd}`;
|
|
2385
|
+
}
|
|
2386
|
+
function auditNeedsCheckout(site, which) {
|
|
2387
|
+
const deployedCapable = site.deployedUrl !== void 0 && which.every((n) => n === "lighthouse");
|
|
2388
|
+
return !deployedCapable;
|
|
2389
|
+
}
|
|
2390
|
+
function applyDeployedUrl(sites, url) {
|
|
2391
|
+
if (url === void 0) return sites;
|
|
2392
|
+
if (sites.length !== 1) {
|
|
2393
|
+
throw Object.assign(
|
|
2394
|
+
new Error(`--url expects exactly one site, but ${sites.length} resolved.`),
|
|
2395
|
+
{ exitCode: 2 }
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
try {
|
|
2399
|
+
new URL(url);
|
|
2400
|
+
} catch {
|
|
2401
|
+
throw Object.assign(new Error(`--url is not a valid URL: ${url}`), { exitCode: 2 });
|
|
2402
|
+
}
|
|
2403
|
+
return [{ ...sites[0], deployedUrl: url }];
|
|
2404
|
+
}
|
|
2180
2405
|
async function runAuditCommand(site, opts) {
|
|
2181
2406
|
const which = parseOnly(opts.only) ?? ALL_AUDIT_NAMES;
|
|
2182
2407
|
const cwd = opts.cwd ? resolve2(opts.cwd) : process.cwd();
|
|
2183
|
-
if (opts.writeAirtable
|
|
2408
|
+
if (typeof opts.writeAirtable === "string" && opts.fleet !== void 0) {
|
|
2409
|
+
throw Object.assign(
|
|
2410
|
+
new Error(
|
|
2411
|
+
"--write-airtable=<slug> is single-site; with --fleet each site's slug comes from the inventory. Use --write-airtable (no slug) + --fleet."
|
|
2412
|
+
),
|
|
2413
|
+
{ exitCode: 2 }
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
if (opts.url !== void 0 && opts.fleet !== void 0) {
|
|
2184
2417
|
throw Object.assign(
|
|
2185
2418
|
new Error(
|
|
2186
|
-
"--
|
|
2419
|
+
"--url is single-site only and cannot be combined with --fleet. Audit a single site instead."
|
|
2187
2420
|
),
|
|
2188
2421
|
{ exitCode: 2 }
|
|
2189
2422
|
);
|
|
@@ -2194,51 +2427,70 @@ async function runAuditCommand(site, opts) {
|
|
|
2194
2427
|
...opts.workdir !== void 0 ? { workdir: opts.workdir } : {},
|
|
2195
2428
|
cwd
|
|
2196
2429
|
});
|
|
2430
|
+
sites = applyDeployedUrl(sites, opts.url);
|
|
2197
2431
|
if (opts.fleet) {
|
|
2198
2432
|
const workdir = opts.workdir ?? `${process.env.HOME ?? ""}/.reddoor-maint/sites`;
|
|
2199
|
-
sites = await Promise.all(
|
|
2433
|
+
sites = await Promise.all(
|
|
2434
|
+
sites.map(
|
|
2435
|
+
(s) => auditNeedsCheckout(s, which) ? cloneIfNeeded(s, { workdir }) : Promise.resolve(s)
|
|
2436
|
+
)
|
|
2437
|
+
);
|
|
2200
2438
|
}
|
|
2201
2439
|
const results = [];
|
|
2202
2440
|
const renderer = rendererFor(opts.json);
|
|
2203
|
-
await buildAuditTasks(sites, which, results, renderer).run();
|
|
2441
|
+
await buildAuditTasks(sites, which, results, renderer, parseConcurrency(opts.concurrency)).run();
|
|
2204
2442
|
let output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);
|
|
2205
2443
|
if (opts.writeAirtable !== void 0) {
|
|
2206
2444
|
const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
2207
2445
|
const { listWebsites: listWebsites2 } = await Promise.resolve().then(() => (init_websites(), websites_exports));
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2446
|
+
if (opts.fleet !== void 0) {
|
|
2447
|
+
const { writeFleetAuditsToAirtable: writeFleetAuditsToAirtable2, formatFleetWriteSummary: formatFleetWriteSummary2 } = await Promise.resolve().then(() => (init_write_audits_to_airtable(), write_audits_to_airtable_exports));
|
|
2448
|
+
const base = openBase2(readAirtableConfig2());
|
|
2449
|
+
const websites = await listWebsites2(base);
|
|
2450
|
+
const fleetWrite = await writeFleetAuditsToAirtable2({ base, websites, results });
|
|
2451
|
+
output += `
|
|
2452
|
+
|
|
2453
|
+
${formatFleetWriteSummary2(fleetWrite)}`;
|
|
2454
|
+
} else {
|
|
2455
|
+
const { resolveSlugFromCwd: resolveSlugFromCwd2 } = await Promise.resolve().then(() => (init_lighthouse_airtable(), lighthouse_airtable_exports));
|
|
2456
|
+
const { writeAuditsToAirtable: writeAuditsToAirtable2 } = await Promise.resolve().then(() => (init_write_audits_to_airtable(), write_audits_to_airtable_exports));
|
|
2457
|
+
const slug = typeof opts.writeAirtable === "string" && opts.writeAirtable.length > 0 ? opts.writeAirtable : await resolveSlugFromCwd2(cwd);
|
|
2458
|
+
let writeSummary = null;
|
|
2459
|
+
await new Listr(
|
|
2460
|
+
[
|
|
2461
|
+
{
|
|
2462
|
+
title: `Write to Airtable[${slug}]`,
|
|
2463
|
+
task: async (_ctx, task) => {
|
|
2464
|
+
const base = openBase2(readAirtableConfig2());
|
|
2465
|
+
task.output = "loading Websites\u2026";
|
|
2466
|
+
const websites = await listWebsites2(base);
|
|
2467
|
+
task.output = "writing scores\u2026";
|
|
2468
|
+
writeSummary = await writeAuditsToAirtable2({ base, websites, slug, results });
|
|
2469
|
+
task.title = `Wrote to Websites[${writeSummary.siteName}] (${writeSummary.writes.length} audit type${writeSummary.writes.length === 1 ? "" : "s"})`;
|
|
2470
|
+
}
|
|
2223
2471
|
}
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
if (writeSummary) output += `
|
|
2472
|
+
],
|
|
2473
|
+
{ renderer }
|
|
2474
|
+
).run();
|
|
2475
|
+
if (writeSummary) output += `
|
|
2229
2476
|
|
|
2230
2477
|
${formatWriteSummary(writeSummary)}`;
|
|
2478
|
+
}
|
|
2231
2479
|
}
|
|
2480
|
+
const notice = deployedUrlNotice(which, opts.url, cwd);
|
|
2481
|
+
if (notice && !opts.json) output += `
|
|
2482
|
+
|
|
2483
|
+
${notice}`;
|
|
2232
2484
|
return { output, code: auditExitCode(results, opts.failOnViolations === true) };
|
|
2233
2485
|
}
|
|
2234
2486
|
|
|
2235
2487
|
// src/cli/commands/sync-configs.ts
|
|
2236
2488
|
import { readFile as readFile9 } from "fs/promises";
|
|
2237
|
-
import { join as
|
|
2489
|
+
import { join as join11, resolve as resolve3 } from "path";
|
|
2238
2490
|
|
|
2239
2491
|
// src/recipes/sync-configs.ts
|
|
2240
2492
|
import { readFile as readFile8, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
2241
|
-
import { join as
|
|
2493
|
+
import { join as join10, dirname } from "path";
|
|
2242
2494
|
|
|
2243
2495
|
// src/recipes/sync-configs/templates.ts
|
|
2244
2496
|
var eslint = {
|
|
@@ -2355,6 +2607,29 @@ var netlify = {
|
|
|
2355
2607
|
[build.environment]
|
|
2356
2608
|
NODE_VERSION = "22"
|
|
2357
2609
|
COREPACK_INTEGRITY_KEYS = "0"
|
|
2610
|
+
|
|
2611
|
+
# Baseline security headers for all responses. CSP is emitted per-response by
|
|
2612
|
+
# SvelteKit (see \`kit.csp\` in svelte.config.js) so it is intentionally omitted
|
|
2613
|
+
# here to avoid conflicting duplicates.
|
|
2614
|
+
[[headers]]
|
|
2615
|
+
for = "/*"
|
|
2616
|
+
[headers.values]
|
|
2617
|
+
Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
|
|
2618
|
+
X-Content-Type-Options = "nosniff"
|
|
2619
|
+
X-Frame-Options = "SAMEORIGIN"
|
|
2620
|
+
Referrer-Policy = "strict-origin-when-cross-origin"
|
|
2621
|
+
Permissions-Policy = "camera=(), microphone=(), geolocation=(), interest-cohort=()"
|
|
2622
|
+
Cross-Origin-Opener-Policy = "same-origin"
|
|
2623
|
+
|
|
2624
|
+
[[headers]]
|
|
2625
|
+
for = "/favicon.png"
|
|
2626
|
+
[headers.values]
|
|
2627
|
+
Cache-Control = "public, max-age=31536000, immutable"
|
|
2628
|
+
|
|
2629
|
+
[[headers]]
|
|
2630
|
+
for = "/_app/immutable/*"
|
|
2631
|
+
[headers.values]
|
|
2632
|
+
Cache-Control = "public, max-age=31536000, immutable"
|
|
2358
2633
|
`
|
|
2359
2634
|
};
|
|
2360
2635
|
var ALL_TEMPLATES = [
|
|
@@ -2576,9 +2851,14 @@ async function withRecipe(body) {
|
|
|
2576
2851
|
// src/recipes/sync-configs.ts
|
|
2577
2852
|
var GITIGNORE_CONFIG = "gitignore";
|
|
2578
2853
|
var SVELTE_CONFIG = "svelte";
|
|
2854
|
+
var NETLIFY_CONFIG = "netlify";
|
|
2579
2855
|
function isSvelteConfigCompliant(contents) {
|
|
2580
2856
|
return contents.includes("createSvelteConfig") && contents.includes("@sveltejs/adapter-netlify");
|
|
2581
2857
|
}
|
|
2858
|
+
var SECURITY_HEADER_RE = /Strict-Transport-Security|Content-Security-Policy|X-Frame-Options|X-Content-Type-Options|Referrer-Policy|Permissions-Policy|Cross-Origin-Opener-Policy/i;
|
|
2859
|
+
function isNetlifyConfigCompliant(contents) {
|
|
2860
|
+
return contents.includes("[[headers]]") && SECURITY_HEADER_RE.test(contents);
|
|
2861
|
+
}
|
|
2582
2862
|
var ALL_CONFIG_NAMES = [
|
|
2583
2863
|
"lighthouse",
|
|
2584
2864
|
"eslint",
|
|
@@ -2605,17 +2885,20 @@ async function readMaybe(path) {
|
|
|
2605
2885
|
async function planTemplateDiffs(cwd, templates) {
|
|
2606
2886
|
const diffs = [];
|
|
2607
2887
|
for (const t of templates) {
|
|
2608
|
-
const existing = await readMaybe(
|
|
2888
|
+
const existing = await readMaybe(join10(cwd, t.path));
|
|
2609
2889
|
if (existing === t.contents) continue;
|
|
2610
2890
|
if (t.config === SVELTE_CONFIG && existing !== null && isSvelteConfigCompliant(existing)) {
|
|
2611
2891
|
continue;
|
|
2612
2892
|
}
|
|
2893
|
+
if (t.config === NETLIFY_CONFIG && existing !== null && isNetlifyConfigCompliant(existing)) {
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2613
2896
|
diffs.push(t);
|
|
2614
2897
|
}
|
|
2615
2898
|
return diffs;
|
|
2616
2899
|
}
|
|
2617
2900
|
async function planGitignore(cwd) {
|
|
2618
|
-
const existing = await readMaybe(
|
|
2901
|
+
const existing = await readMaybe(join10(cwd, ".gitignore"));
|
|
2619
2902
|
const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);
|
|
2620
2903
|
const tracked = await listTrackedFiles(cwd);
|
|
2621
2904
|
const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);
|
|
@@ -2623,7 +2906,7 @@ async function planGitignore(cwd) {
|
|
|
2623
2906
|
return { kind: "apply", content: merge.content, toUntrack, added: merge.added };
|
|
2624
2907
|
}
|
|
2625
2908
|
async function applyGitignore(cwd, plan) {
|
|
2626
|
-
await writeFile3(
|
|
2909
|
+
await writeFile3(join10(cwd, ".gitignore"), plan.content, "utf-8");
|
|
2627
2910
|
if (plan.toUntrack.length > 0) {
|
|
2628
2911
|
await removeFromIndex(cwd, plan.toUntrack);
|
|
2629
2912
|
}
|
|
@@ -2646,7 +2929,7 @@ async function syncConfigs(site, opts = {}) {
|
|
|
2646
2929
|
},
|
|
2647
2930
|
apply: async ({ templateDiffs, gitignorePlan }, { commit: commit2 }) => {
|
|
2648
2931
|
for (const t of templateDiffs) {
|
|
2649
|
-
const dest =
|
|
2932
|
+
const dest = join10(site.path, t.path);
|
|
2650
2933
|
await mkdir2(dirname(dest), { recursive: true });
|
|
2651
2934
|
await writeFile3(dest, t.contents, "utf-8");
|
|
2652
2935
|
await commit2(`chore: sync ${t.config} config from @reddoorla/maintenance`);
|
|
@@ -2677,7 +2960,7 @@ function parseOnly2(value) {
|
|
|
2677
2960
|
async function dryPlanGitignore(cwd) {
|
|
2678
2961
|
let existing;
|
|
2679
2962
|
try {
|
|
2680
|
-
existing = await readFile9(
|
|
2963
|
+
existing = await readFile9(join11(cwd, ".gitignore"), "utf-8");
|
|
2681
2964
|
} catch {
|
|
2682
2965
|
return "would create .gitignore";
|
|
2683
2966
|
}
|
|
@@ -2692,7 +2975,7 @@ async function dryPlan(cwd, which) {
|
|
|
2692
2975
|
for (const t of templateTargets) {
|
|
2693
2976
|
let existing = "";
|
|
2694
2977
|
try {
|
|
2695
|
-
existing = await readFile9(
|
|
2978
|
+
existing = await readFile9(join11(cwd, t.path), "utf-8");
|
|
2696
2979
|
} catch {
|
|
2697
2980
|
}
|
|
2698
2981
|
if (existing !== t.contents) lines.push(`would update ${t.path} (config: ${t.config})`);
|
|
@@ -2739,11 +3022,11 @@ async function runSyncConfigsCommand(site, opts) {
|
|
|
2739
3022
|
import { resolve as resolve4 } from "path";
|
|
2740
3023
|
|
|
2741
3024
|
// src/recipes/bump-deps.ts
|
|
2742
|
-
import { stat as
|
|
2743
|
-
import { join as
|
|
2744
|
-
async function
|
|
3025
|
+
import { stat as stat3 } from "fs/promises";
|
|
3026
|
+
import { join as join12 } from "path";
|
|
3027
|
+
async function exists2(path) {
|
|
2745
3028
|
try {
|
|
2746
|
-
await
|
|
3029
|
+
await stat3(path);
|
|
2747
3030
|
return true;
|
|
2748
3031
|
} catch {
|
|
2749
3032
|
return false;
|
|
@@ -2769,10 +3052,10 @@ async function bumpDeps(site, opts = {}) {
|
|
|
2769
3052
|
// land on top of whatever else was in the tree.
|
|
2770
3053
|
checkTreeFirst: true,
|
|
2771
3054
|
plan: async () => {
|
|
2772
|
-
const hasPnpmLock = await
|
|
3055
|
+
const hasPnpmLock = await exists2(join12(site.path, "pnpm-lock.yaml"));
|
|
2773
3056
|
if (!hasPnpmLock) {
|
|
2774
|
-
const hasNpmLock = await
|
|
2775
|
-
const hasYarnLock = await
|
|
3057
|
+
const hasNpmLock = await exists2(join12(site.path, "package-lock.json"));
|
|
3058
|
+
const hasYarnLock = await exists2(join12(site.path, "yarn.lock"));
|
|
2776
3059
|
if (hasNpmLock || hasYarnLock) {
|
|
2777
3060
|
const competing = hasNpmLock ? "package-lock.json" : "yarn.lock";
|
|
2778
3061
|
return {
|
|
@@ -2843,7 +3126,7 @@ import { resolve as resolve5 } from "path";
|
|
|
2843
3126
|
|
|
2844
3127
|
// src/recipes/self-updating/index.ts
|
|
2845
3128
|
import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
|
|
2846
|
-
import { dirname as dirname2, join as
|
|
3129
|
+
import { dirname as dirname2, join as join13 } from "path";
|
|
2847
3130
|
|
|
2848
3131
|
// src/github/config.ts
|
|
2849
3132
|
function readGitHubConfig() {
|
|
@@ -3010,7 +3293,7 @@ async function selfUpdating(site, deps = {}) {
|
|
|
3010
3293
|
const branch = branchName("self-updating");
|
|
3011
3294
|
await createBranch(site.path, branch);
|
|
3012
3295
|
for (const t of templates) {
|
|
3013
|
-
const dest =
|
|
3296
|
+
const dest = join13(site.path, t.path);
|
|
3014
3297
|
await mkdir3(dirname2(dest), { recursive: true });
|
|
3015
3298
|
await writeFile4(dest, t.contents, "utf-8");
|
|
3016
3299
|
}
|
|
@@ -3085,7 +3368,7 @@ async function runSelfUpdatingCommand(site, opts) {
|
|
|
3085
3368
|
import { resolve as resolve6 } from "path";
|
|
3086
3369
|
|
|
3087
3370
|
// src/recipes/svelte-5/index.ts
|
|
3088
|
-
import { join as
|
|
3371
|
+
import { join as join19 } from "path";
|
|
3089
3372
|
|
|
3090
3373
|
// src/util/pkg.ts
|
|
3091
3374
|
import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
|
|
@@ -3134,7 +3417,7 @@ function bumpDep(pkg, name, version2, opts = {}) {
|
|
|
3134
3417
|
}
|
|
3135
3418
|
|
|
3136
3419
|
// src/recipes/svelte-5/step-bump-versions.ts
|
|
3137
|
-
import { join as
|
|
3420
|
+
import { join as join14 } from "path";
|
|
3138
3421
|
var SVELTE_5_VERSIONS = {
|
|
3139
3422
|
svelte: "^5.55.5",
|
|
3140
3423
|
"@sveltejs/kit": "^2.59.0",
|
|
@@ -3147,7 +3430,7 @@ var SVELTE_5_VERSIONS = {
|
|
|
3147
3430
|
"typescript-svelte-plugin": "^0.3.52"
|
|
3148
3431
|
};
|
|
3149
3432
|
async function bumpToSvelte5Versions(cwd) {
|
|
3150
|
-
const pkgPath =
|
|
3433
|
+
const pkgPath = join14(cwd, "package.json");
|
|
3151
3434
|
const pkg = await readPackageJson(pkgPath);
|
|
3152
3435
|
let next = pkg;
|
|
3153
3436
|
for (const [name, version2] of Object.entries(SVELTE_5_VERSIONS)) {
|
|
@@ -3160,7 +3443,7 @@ async function bumpToSvelte5Versions(cwd) {
|
|
|
3160
3443
|
|
|
3161
3444
|
// src/recipes/svelte-5/step-svelte-config.ts
|
|
3162
3445
|
import { readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
3163
|
-
import { join as
|
|
3446
|
+
import { join as join15 } from "path";
|
|
3164
3447
|
var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
|
|
3165
3448
|
var IMPORT_FROM_VITE_PLUGIN = new RegExp(
|
|
3166
3449
|
String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
|
|
@@ -3201,7 +3484,7 @@ function dropPreprocessKey(source) {
|
|
|
3201
3484
|
return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
|
|
3202
3485
|
}
|
|
3203
3486
|
async function migrateSvelteConfig(cwd) {
|
|
3204
|
-
const path =
|
|
3487
|
+
const path = join15(cwd, "svelte.config.js");
|
|
3205
3488
|
let src;
|
|
3206
3489
|
try {
|
|
3207
3490
|
src = await readFile11(path, "utf-8");
|
|
@@ -3238,9 +3521,9 @@ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
|
|
|
3238
3521
|
}
|
|
3239
3522
|
|
|
3240
3523
|
// src/recipes/svelte-5/step-tailwind-upgrade.ts
|
|
3241
|
-
import { join as
|
|
3524
|
+
import { join as join16 } from "path";
|
|
3242
3525
|
async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
3243
|
-
const pkg = await readPackageJson(
|
|
3526
|
+
const pkg = await readPackageJson(join16(cwd, "package.json"));
|
|
3244
3527
|
const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
|
|
3245
3528
|
if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
|
|
3246
3529
|
if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
|
|
@@ -3262,7 +3545,7 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
|
3262
3545
|
|
|
3263
3546
|
// src/recipes/svelte-5/step-gotchas.ts
|
|
3264
3547
|
import { readFile as readFile12, writeFile as writeFile7 } from "fs/promises";
|
|
3265
|
-
import { join as
|
|
3548
|
+
import { join as join17 } from "path";
|
|
3266
3549
|
import { glob as glob2 } from "tinyglobby";
|
|
3267
3550
|
|
|
3268
3551
|
// src/recipes/svelte-5/codemods/on-event-to-handler.ts
|
|
@@ -3608,7 +3891,7 @@ async function planGotchaCodemods(cwd) {
|
|
|
3608
3891
|
const changes = [];
|
|
3609
3892
|
const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
|
|
3610
3893
|
for (const rel of relPaths) {
|
|
3611
|
-
const path =
|
|
3894
|
+
const path = join17(cwd, rel);
|
|
3612
3895
|
const before = await readFile12(path, "utf-8");
|
|
3613
3896
|
const after = CODEMODS.reduce((s, fn) => fn(s), before);
|
|
3614
3897
|
if (after !== before) changes.push({ rel, after });
|
|
@@ -3618,7 +3901,7 @@ async function planGotchaCodemods(cwd) {
|
|
|
3618
3901
|
async function applyGotchaCodemods(cwd) {
|
|
3619
3902
|
const changes = await planGotchaCodemods(cwd);
|
|
3620
3903
|
for (const c of changes) {
|
|
3621
|
-
await writeFile7(
|
|
3904
|
+
await writeFile7(join17(cwd, c.rel), c.after, "utf-8");
|
|
3622
3905
|
}
|
|
3623
3906
|
return { filesChanged: changes.length };
|
|
3624
3907
|
}
|
|
@@ -3642,7 +3925,7 @@ async function verifyMigration(cwd, spawn2 = defaultSpawn) {
|
|
|
3642
3925
|
|
|
3643
3926
|
// src/recipes/svelte-5/step-summary.ts
|
|
3644
3927
|
import { writeFile as writeFile8 } from "fs/promises";
|
|
3645
|
-
import { join as
|
|
3928
|
+
import { join as join18 } from "path";
|
|
3646
3929
|
async function writeMigrationSummary(input) {
|
|
3647
3930
|
const lines = [
|
|
3648
3931
|
`# Svelte 4 \u2192 5 migration summary`,
|
|
@@ -3659,7 +3942,7 @@ async function writeMigrationSummary(input) {
|
|
|
3659
3942
|
`- Verify Playwright a11y tests still pass.`
|
|
3660
3943
|
];
|
|
3661
3944
|
const content = lines.join("\n") + "\n";
|
|
3662
|
-
const path =
|
|
3945
|
+
const path = join18(input.cwd, "MIGRATION_SVELTE_5.md");
|
|
3663
3946
|
await writeFile8(path, content, "utf-8");
|
|
3664
3947
|
return path;
|
|
3665
3948
|
}
|
|
@@ -3667,7 +3950,7 @@ async function writeMigrationSummary(input) {
|
|
|
3667
3950
|
// src/recipes/svelte-5/index.ts
|
|
3668
3951
|
async function alreadyOnSvelte5(cwd) {
|
|
3669
3952
|
try {
|
|
3670
|
-
const pkg = await readPackageJson(
|
|
3953
|
+
const pkg = await readPackageJson(join19(cwd, "package.json"));
|
|
3671
3954
|
const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
|
|
3672
3955
|
return !!v && /^\^?5\./.test(v);
|
|
3673
3956
|
} catch {
|
|
@@ -3761,8 +4044,8 @@ async function runUpgradeCommand(upgradeName, site, opts = {}) {
|
|
|
3761
4044
|
import { resolve as resolve7 } from "path";
|
|
3762
4045
|
|
|
3763
4046
|
// src/recipes/convert-to-pnpm.ts
|
|
3764
|
-
import { rm as rm3, stat as
|
|
3765
|
-
import { join as
|
|
4047
|
+
import { rm as rm3, stat as stat4 } from "fs/promises";
|
|
4048
|
+
import { join as join20 } from "path";
|
|
3766
4049
|
|
|
3767
4050
|
// src/recipes/convert-to-pnpm/script-rewrites.ts
|
|
3768
4051
|
function rewriteScriptForPnpm(script) {
|
|
@@ -3784,9 +4067,9 @@ function rewriteScriptsForPnpm(scripts) {
|
|
|
3784
4067
|
|
|
3785
4068
|
// src/recipes/convert-to-pnpm.ts
|
|
3786
4069
|
var DEFAULT_PNPM_VERSION = "10.33.1";
|
|
3787
|
-
async function
|
|
4070
|
+
async function exists3(path) {
|
|
3788
4071
|
try {
|
|
3789
|
-
await
|
|
4072
|
+
await stat4(path);
|
|
3790
4073
|
return true;
|
|
3791
4074
|
} catch {
|
|
3792
4075
|
return false;
|
|
@@ -3795,18 +4078,18 @@ async function exists2(path) {
|
|
|
3795
4078
|
async function convertToPnpm(site, opts = {}) {
|
|
3796
4079
|
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
3797
4080
|
const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;
|
|
3798
|
-
const pnpmLockPath =
|
|
3799
|
-
const npmLockPath =
|
|
3800
|
-
const yarnLockPath =
|
|
4081
|
+
const pnpmLockPath = join20(site.path, "pnpm-lock.yaml");
|
|
4082
|
+
const npmLockPath = join20(site.path, "package-lock.json");
|
|
4083
|
+
const yarnLockPath = join20(site.path, "yarn.lock");
|
|
3801
4084
|
return withRecipe({
|
|
3802
4085
|
name: "convert-to-pnpm",
|
|
3803
4086
|
site,
|
|
3804
4087
|
plan: async () => {
|
|
3805
|
-
if (await
|
|
4088
|
+
if (await exists3(pnpmLockPath)) {
|
|
3806
4089
|
return { kind: "noop", notes: "site already has pnpm-lock.yaml" };
|
|
3807
4090
|
}
|
|
3808
|
-
const hasNpmLock = await
|
|
3809
|
-
const hasYarnLock = await
|
|
4091
|
+
const hasNpmLock = await exists3(npmLockPath);
|
|
4092
|
+
const hasYarnLock = await exists3(yarnLockPath);
|
|
3810
4093
|
if (!hasNpmLock && !hasYarnLock) {
|
|
3811
4094
|
return {
|
|
3812
4095
|
kind: "noop",
|
|
@@ -3820,7 +4103,7 @@ async function convertToPnpm(site, opts = {}) {
|
|
|
3820
4103
|
if (hasYarnLock) await rm3(yarnLockPath, { force: true });
|
|
3821
4104
|
const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
|
|
3822
4105
|
await commit2(`chore(pnpm): remove ${sourceLock}`);
|
|
3823
|
-
const pkgPath =
|
|
4106
|
+
const pkgPath = join20(cwd, "package.json");
|
|
3824
4107
|
const pkg = await readPackageJson(pkgPath);
|
|
3825
4108
|
const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
|
|
3826
4109
|
if (pkg.scripts && typeof pkg.scripts === "object") {
|
|
@@ -3833,7 +4116,7 @@ async function convertToPnpm(site, opts = {}) {
|
|
|
3833
4116
|
}
|
|
3834
4117
|
await writePackageJson(pkgPath, next);
|
|
3835
4118
|
await commit2("chore(pnpm): pin packageManager + rewrite npm scripts");
|
|
3836
|
-
await rm3(
|
|
4119
|
+
await rm3(join20(cwd, "node_modules"), { recursive: true, force: true });
|
|
3837
4120
|
const installResult = await spawn2("pnpm", ["install"], { cwd, streaming: true });
|
|
3838
4121
|
if (installResult.code !== 0) {
|
|
3839
4122
|
return { kind: "failed", notes: `pnpm install failed (exit ${installResult.code})` };
|
|
@@ -3873,18 +4156,18 @@ async function runConvertToPnpmCommand(site, opts) {
|
|
|
3873
4156
|
import { resolve as resolve8 } from "path";
|
|
3874
4157
|
|
|
3875
4158
|
// src/recipes/onboard.ts
|
|
3876
|
-
import { stat as
|
|
3877
|
-
import { join as
|
|
4159
|
+
import { stat as stat5 } from "fs/promises";
|
|
4160
|
+
import { join as join22 } from "path";
|
|
3878
4161
|
|
|
3879
4162
|
// src/util/self-version.ts
|
|
3880
4163
|
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
3881
4164
|
import { fileURLToPath } from "url";
|
|
3882
|
-
import { dirname as dirname3, join as
|
|
4165
|
+
import { dirname as dirname3, join as join21 } from "path";
|
|
3883
4166
|
function selfPackageVersion(callerImportMetaUrl) {
|
|
3884
4167
|
try {
|
|
3885
4168
|
let dir = dirname3(fileURLToPath(callerImportMetaUrl));
|
|
3886
4169
|
while (true) {
|
|
3887
|
-
const candidate =
|
|
4170
|
+
const candidate = join21(dir, "package.json");
|
|
3888
4171
|
if (existsSync2(candidate)) {
|
|
3889
4172
|
const raw = readFileSync2(candidate, "utf-8");
|
|
3890
4173
|
const pkg = JSON.parse(raw);
|
|
@@ -3936,9 +4219,9 @@ var AUDIT_DEPS = Object.fromEntries(
|
|
|
3936
4219
|
})
|
|
3937
4220
|
])
|
|
3938
4221
|
);
|
|
3939
|
-
async function
|
|
4222
|
+
async function exists4(path) {
|
|
3940
4223
|
try {
|
|
3941
|
-
await
|
|
4224
|
+
await stat5(path);
|
|
3942
4225
|
return true;
|
|
3943
4226
|
} catch {
|
|
3944
4227
|
return false;
|
|
@@ -3955,13 +4238,13 @@ async function onboard(site, opts = {}) {
|
|
|
3955
4238
|
name: "onboard",
|
|
3956
4239
|
site,
|
|
3957
4240
|
plan: async () => {
|
|
3958
|
-
if (!await
|
|
4241
|
+
if (!await exists4(join22(site.path, "pnpm-lock.yaml"))) {
|
|
3959
4242
|
return {
|
|
3960
4243
|
kind: "failed",
|
|
3961
4244
|
notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
|
|
3962
4245
|
};
|
|
3963
4246
|
}
|
|
3964
|
-
const pkgPath =
|
|
4247
|
+
const pkgPath = join22(site.path, "package.json");
|
|
3965
4248
|
const pkg = await readPackageJson(pkgPath);
|
|
3966
4249
|
const toAdd = [];
|
|
3967
4250
|
if (!isDeclared(pkg, PACKAGE_NAME)) {
|
|
@@ -3984,7 +4267,7 @@ async function onboard(site, opts = {}) {
|
|
|
3984
4267
|
return { kind: "apply", plan: { pkg, toAdd } };
|
|
3985
4268
|
},
|
|
3986
4269
|
apply: async ({ pkg, toAdd }, { commit: commit2, cwd }) => {
|
|
3987
|
-
const pkgPath =
|
|
4270
|
+
const pkgPath = join22(cwd, "package.json");
|
|
3988
4271
|
let next = pkg;
|
|
3989
4272
|
for (const dep of toAdd) {
|
|
3990
4273
|
next = bumpDep(next, dep.name, dep.version);
|
|
@@ -4053,7 +4336,7 @@ import { resolve as resolve9 } from "path";
|
|
|
4053
4336
|
|
|
4054
4337
|
// src/recipes/svelte-codemods.ts
|
|
4055
4338
|
import { writeFile as writeFile9 } from "fs/promises";
|
|
4056
|
-
import { join as
|
|
4339
|
+
import { join as join23 } from "path";
|
|
4057
4340
|
async function svelteCodemods(site) {
|
|
4058
4341
|
return withRecipe({
|
|
4059
4342
|
name: "svelte-codemods",
|
|
@@ -4067,7 +4350,7 @@ async function svelteCodemods(site) {
|
|
|
4067
4350
|
},
|
|
4068
4351
|
apply: async (changes, { commit: commit2, cwd }) => {
|
|
4069
4352
|
for (const c of changes) {
|
|
4070
|
-
await writeFile9(
|
|
4353
|
+
await writeFile9(join23(cwd, c.rel), c.after, "utf-8");
|
|
4071
4354
|
}
|
|
4072
4355
|
await commit2(`refactor(svelte5): apply codemods (${changes.length} files)`);
|
|
4073
4356
|
return { kind: "ok" };
|
|
@@ -4171,11 +4454,11 @@ import { dirname as dirname6 } from "path";
|
|
|
4171
4454
|
|
|
4172
4455
|
// src/reports/ga/config.ts
|
|
4173
4456
|
init_credentials();
|
|
4174
|
-
import { dirname as dirname5, join as
|
|
4457
|
+
import { dirname as dirname5, join as join25 } from "path";
|
|
4175
4458
|
function readGaConfig() {
|
|
4176
4459
|
const subject = process.env.GA_SUBJECT?.trim();
|
|
4177
4460
|
if (!subject) return null;
|
|
4178
|
-
const keyPath = process.env.GA_SA_KEY_PATH?.trim() ||
|
|
4461
|
+
const keyPath = process.env.GA_SA_KEY_PATH?.trim() || join25(dirname5(defaultCredentialsPath()), "ga-service-account.json");
|
|
4179
4462
|
return { subject, keyPath };
|
|
4180
4463
|
}
|
|
4181
4464
|
|
|
@@ -4275,7 +4558,7 @@ async function fetchSearchPresence(q, periodStart, periodEnd) {
|
|
|
4275
4558
|
});
|
|
4276
4559
|
const pos = res.data.rows?.[0]?.position;
|
|
4277
4560
|
if (typeof pos === "number") {
|
|
4278
|
-
return { foundOnPage1: pos <= PAGE_1_MAX_POSITION, position: Math.round(pos) };
|
|
4561
|
+
return { foundOnPage1: pos <= PAGE_1_MAX_POSITION, position: Math.max(1, Math.round(pos)) };
|
|
4279
4562
|
}
|
|
4280
4563
|
}
|
|
4281
4564
|
return { foundOnPage1: false, position: null };
|
|
@@ -4304,8 +4587,14 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
4304
4587
|
const periodEnd = today;
|
|
4305
4588
|
const completedOn = today;
|
|
4306
4589
|
const lastTestedDate = reportType === "Maintenance" && siteRow.testingDay ? new Date(siteRow.testingDay) : null;
|
|
4307
|
-
const
|
|
4308
|
-
const
|
|
4590
|
+
const gaResult = base !== null ? await fetchGaUsers(siteRow, periodStart, periodEnd) : NO_ENRICHMENT;
|
|
4591
|
+
const searchResult = base !== null ? await fetchSearch(siteRow, periodStart, periodEnd) : NO_ENRICHMENT;
|
|
4592
|
+
const gaUsers = gaResult.value;
|
|
4593
|
+
const search = searchResult.value;
|
|
4594
|
+
const softFailures = [
|
|
4595
|
+
...gaResult.softFailed ? ["ga"] : [],
|
|
4596
|
+
...searchResult.softFailed ? ["search"] : []
|
|
4597
|
+
];
|
|
4309
4598
|
const cidName = `${slug}-header`;
|
|
4310
4599
|
const { html } = await renderReportHtml({
|
|
4311
4600
|
siteName: siteRow.name,
|
|
@@ -4324,7 +4613,7 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
4324
4613
|
const path = options.previewPath ?? `reports/${slug}/draft.html`;
|
|
4325
4614
|
await mkdir4(dirname6(path), { recursive: true });
|
|
4326
4615
|
await writeFile10(path, html, "utf-8");
|
|
4327
|
-
return { reportRow: null, htmlPath: path, html };
|
|
4616
|
+
return { reportRow: null, htmlPath: path, html, softFailures };
|
|
4328
4617
|
}
|
|
4329
4618
|
if (base === null) throw new Error("base required when previewOnly=false");
|
|
4330
4619
|
const reportId = `${siteRow.name} \u2014 ${reportType} \u2014 ${periodEnd.toISOString().slice(0, 10)}`;
|
|
@@ -4344,27 +4633,29 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
4344
4633
|
const htmlFilename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;
|
|
4345
4634
|
await uploadAttachment(created.id, "Rendered HTML", html, htmlFilename, "text/html");
|
|
4346
4635
|
await setDraftReady(base, created.id, true);
|
|
4347
|
-
return { reportRow: created, htmlPath: null, html };
|
|
4636
|
+
return { reportRow: created, htmlPath: null, html, softFailures };
|
|
4348
4637
|
}
|
|
4638
|
+
var NO_ENRICHMENT = { value: null, softFailed: false };
|
|
4349
4639
|
async function fetchGaUsers(siteRow, periodStart, periodEnd) {
|
|
4350
4640
|
const cfg = readGaConfig();
|
|
4351
|
-
if (!cfg || !siteRow.ga4PropertyId) return
|
|
4641
|
+
if (!cfg || !siteRow.ga4PropertyId) return NO_ENRICHMENT;
|
|
4352
4642
|
try {
|
|
4353
|
-
|
|
4643
|
+
const value = await fetchPeriodUsers(
|
|
4354
4644
|
{ propertyId: siteRow.ga4PropertyId, subject: cfg.subject, keyPath: cfg.keyPath },
|
|
4355
4645
|
periodStart,
|
|
4356
4646
|
periodEnd
|
|
4357
4647
|
);
|
|
4648
|
+
return { value, softFailed: false };
|
|
4358
4649
|
} catch (e) {
|
|
4359
4650
|
console.warn(`\u26A0 GA skipped for ${siteRow.name}: ${e.message}`);
|
|
4360
|
-
return null;
|
|
4651
|
+
return { value: null, softFailed: true };
|
|
4361
4652
|
}
|
|
4362
4653
|
}
|
|
4363
4654
|
async function fetchSearch(siteRow, periodStart, periodEnd) {
|
|
4364
4655
|
const cfg = readGaConfig();
|
|
4365
|
-
if (!cfg || !siteRow.searchQuery) return
|
|
4656
|
+
if (!cfg || !siteRow.searchQuery) return NO_ENRICHMENT;
|
|
4366
4657
|
try {
|
|
4367
|
-
|
|
4658
|
+
const value = await fetchSearchPresence(
|
|
4368
4659
|
{
|
|
4369
4660
|
keyPath: cfg.keyPath,
|
|
4370
4661
|
subject: cfg.subject,
|
|
@@ -4375,16 +4666,20 @@ async function fetchSearch(siteRow, periodStart, periodEnd) {
|
|
|
4375
4666
|
periodStart,
|
|
4376
4667
|
periodEnd
|
|
4377
4668
|
);
|
|
4669
|
+
return { value, softFailed: false };
|
|
4378
4670
|
} catch (e) {
|
|
4379
4671
|
console.warn(`\u26A0 Search presence skipped for ${siteRow.name}: ${e.message}`);
|
|
4380
|
-
return null;
|
|
4672
|
+
return { value: null, softFailed: true };
|
|
4381
4673
|
}
|
|
4382
4674
|
}
|
|
4383
4675
|
async function derivePeriodStart(base, siteRow, reportType, today) {
|
|
4384
4676
|
const prior = await listReportsForSite(base, siteRow.id);
|
|
4385
4677
|
const sameType = prior.filter((r) => r.reportType === reportType && r.periodEnd).map((r) => r.periodEnd).sort();
|
|
4386
4678
|
const latest = sameType[sameType.length - 1];
|
|
4387
|
-
|
|
4679
|
+
if (!latest) return daysAgo(today, 30);
|
|
4680
|
+
const start = new Date(latest);
|
|
4681
|
+
start.setUTCDate(start.getUTCDate() + 1);
|
|
4682
|
+
return start;
|
|
4388
4683
|
}
|
|
4389
4684
|
|
|
4390
4685
|
// src/cli/commands/report.ts
|
|
@@ -4417,14 +4712,21 @@ async function runDueDraft() {
|
|
|
4417
4712
|
const due = findDueReports(websites, reports, /* @__PURE__ */ new Date());
|
|
4418
4713
|
if (due.length === 0) return { output: "No reports due.", code: 0 };
|
|
4419
4714
|
const lines = [];
|
|
4715
|
+
let softFailedSites = 0;
|
|
4420
4716
|
for (const item of due) {
|
|
4421
4717
|
try {
|
|
4422
4718
|
const result = await draftReportForSite(base, item.site, item.reportType);
|
|
4423
4719
|
lines.push(`\u2713 drafted: ${result.reportRow?.reportId}`);
|
|
4720
|
+
if (result.softFailures.length > 0) softFailedSites++;
|
|
4424
4721
|
} catch (e) {
|
|
4425
4722
|
lines.push(`\u2717 failed: ${item.site.name} ${item.reportType} \u2014 ${e.message}`);
|
|
4426
4723
|
}
|
|
4427
4724
|
}
|
|
4725
|
+
if (softFailedSites > 0) {
|
|
4726
|
+
lines.push(
|
|
4727
|
+
`\u26A0 ${softFailedSites} site${softFailedSites === 1 ? "" : "s"} had GA/Search enrichment fail \u2014 drafted with blank analytics; check the logs above`
|
|
4728
|
+
);
|
|
4729
|
+
}
|
|
4428
4730
|
return { output: lines.join("\n"), code: lines.some((l) => l.startsWith("\u2717")) ? 1 : 0 };
|
|
4429
4731
|
}
|
|
4430
4732
|
async function runSingleSiteDraft(slug, opts) {
|
|
@@ -4448,7 +4750,7 @@ import { resolve as resolve10 } from "path";
|
|
|
4448
4750
|
|
|
4449
4751
|
// src/recipes/a11y-fixtures-page/index.ts
|
|
4450
4752
|
import { access, mkdir as mkdir5, writeFile as writeFile11 } from "fs/promises";
|
|
4451
|
-
import { dirname as dirname7, join as
|
|
4753
|
+
import { dirname as dirname7, join as join26 } from "path";
|
|
4452
4754
|
|
|
4453
4755
|
// src/recipes/a11y-fixtures-page/template.ts
|
|
4454
4756
|
var A11Y_FIXTURES_PAGE_RELATIVE = "src/routes/dev/a11y-fixtures/+page.svelte";
|
|
@@ -4499,7 +4801,7 @@ async function fileExists(path) {
|
|
|
4499
4801
|
}
|
|
4500
4802
|
}
|
|
4501
4803
|
async function a11yFixturesPage(site) {
|
|
4502
|
-
const target =
|
|
4804
|
+
const target = join26(site.path, A11Y_FIXTURES_PAGE_RELATIVE);
|
|
4503
4805
|
return withRecipe({
|
|
4504
4806
|
name: "a11y-fixtures-page",
|
|
4505
4807
|
site,
|
|
@@ -4607,12 +4909,12 @@ async function runInitCommand(site, opts) {
|
|
|
4607
4909
|
|
|
4608
4910
|
// src/cli/version.ts
|
|
4609
4911
|
import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
|
|
4610
|
-
import { dirname as dirname8, join as
|
|
4912
|
+
import { dirname as dirname8, join as join27 } from "path";
|
|
4611
4913
|
function resolvePackageVersion(fromDir) {
|
|
4612
4914
|
try {
|
|
4613
4915
|
let dir = fromDir;
|
|
4614
4916
|
while (true) {
|
|
4615
|
-
const candidate =
|
|
4917
|
+
const candidate = join27(dir, "package.json");
|
|
4616
4918
|
if (existsSync4(candidate)) {
|
|
4617
4919
|
const raw = readFileSync5(candidate, "utf-8");
|
|
4618
4920
|
const pkg = JSON.parse(raw);
|
|
@@ -4681,7 +4983,13 @@ cli.command("audit [site]", "Run audits against a site (default: cwd).").option(
|
|
|
4681
4983
|
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").option(
|
|
4682
4984
|
"--write-airtable [slug]",
|
|
4683
4985
|
"After lighthouse runs, write pScore/rScore/bpScore/seoScore + timestamp to the matching Websites row. Slug defaults to cwd's package.json#name."
|
|
4684
|
-
).option("--fail-on-violations", "Exit non-zero if any a11y violations are found (for CI gates)").
|
|
4986
|
+
).option("--fail-on-violations", "Exit non-zero if any a11y violations are found (for CI gates)").option(
|
|
4987
|
+
"--url <url>",
|
|
4988
|
+
"Audit this deployed URL with lighthouse (no dev server); single-site. Pair with --only lighthouse \u2014 other audits still use the local checkout."
|
|
4989
|
+
).option(
|
|
4990
|
+
"--concurrency <n>",
|
|
4991
|
+
"Max sites to audit in parallel in --fleet mode (default: all at once). Use 1 for sequential (CI)."
|
|
4992
|
+
).action(
|
|
4685
4993
|
async (site, opts) => runOrExit(() => runAuditCommand(site, opts), opts)
|
|
4686
4994
|
);
|
|
4687
4995
|
cli.command("sync-configs [site]", "Sync canonical configs into a site.").option("--only <names>", "Comma-separated config names (e.g. eslint,prettier)").option("--dry", "Print diff without writing").option(
|