@reddoorla/maintenance 0.7.0 → 0.8.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/README.md +7 -1
- package/dist/cli/bin.js +327 -96
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +6 -0
- package/dist/cli/commands/audit.js +223 -5
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.d.ts +35 -9
- package/dist/index.js +156 -79
- package/dist/index.js.map +1 -1
- package/dist/reports/maintenance-email/assets/blurredTests.jpg +0 -0
- package/dist/reports/maintenance-email/assets/check.png +0 -0
- package/package.json +1 -1
package/dist/cli/bin.js
CHANGED
|
@@ -10,6 +10,11 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/reports/airtable/client.ts
|
|
13
|
+
var client_exports = {};
|
|
14
|
+
__export(client_exports, {
|
|
15
|
+
openBase: () => openBase,
|
|
16
|
+
readAirtableConfig: () => readAirtableConfig
|
|
17
|
+
});
|
|
13
18
|
import Airtable from "airtable";
|
|
14
19
|
function readAirtableConfig() {
|
|
15
20
|
const apiKey = process.env.AIRTABLE_PAT;
|
|
@@ -28,6 +33,14 @@ var init_client = __esm({
|
|
|
28
33
|
});
|
|
29
34
|
|
|
30
35
|
// src/reports/airtable/websites.ts
|
|
36
|
+
var websites_exports = {};
|
|
37
|
+
__export(websites_exports, {
|
|
38
|
+
WEBSITES_TABLE: () => WEBSITES_TABLE,
|
|
39
|
+
getWebsiteBySlug: () => getWebsiteBySlug,
|
|
40
|
+
listWebsites: () => listWebsites,
|
|
41
|
+
siteSlug: () => siteSlug,
|
|
42
|
+
updateScores: () => updateScores
|
|
43
|
+
});
|
|
31
44
|
function siteSlug(name) {
|
|
32
45
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
33
46
|
}
|
|
@@ -39,6 +52,7 @@ function mapRow(rec) {
|
|
|
39
52
|
id: rec.id,
|
|
40
53
|
name: String(f["Name"] ?? ""),
|
|
41
54
|
url: String(f["url"] ?? ""),
|
|
55
|
+
status: f["Status"] ?? null,
|
|
42
56
|
pointOfContact: f["point of contact"] ?? null,
|
|
43
57
|
maintenanceFreq: f["maintenence freq"] ?? "None",
|
|
44
58
|
testingFreq: f["testing freq"] ?? "None",
|
|
@@ -51,7 +65,8 @@ function mapRow(rec) {
|
|
|
51
65
|
pScore: f["pScore"] ?? null,
|
|
52
66
|
rScore: f["rScore"] ?? null,
|
|
53
67
|
bpScore: f["bpScore"] ?? null,
|
|
54
|
-
seoScore: f["seoScore"] ?? null
|
|
68
|
+
seoScore: f["seoScore"] ?? null,
|
|
69
|
+
lastLighthouseAuditAt: f["Last lighthouse audit at"] ?? null
|
|
55
70
|
};
|
|
56
71
|
}
|
|
57
72
|
async function listWebsites(base) {
|
|
@@ -62,6 +77,20 @@ async function listWebsites(base) {
|
|
|
62
77
|
});
|
|
63
78
|
return out;
|
|
64
79
|
}
|
|
80
|
+
async function getWebsiteBySlug(base, slug) {
|
|
81
|
+
const all = await listWebsites(base);
|
|
82
|
+
return all.find((w) => siteSlug(w.name) === slug) ?? null;
|
|
83
|
+
}
|
|
84
|
+
async function updateScores(base, recordId, scores) {
|
|
85
|
+
const fields = {
|
|
86
|
+
pScore: scores.performance,
|
|
87
|
+
rScore: scores.accessibility,
|
|
88
|
+
bpScore: scores.bestPractices,
|
|
89
|
+
seoScore: scores.seo,
|
|
90
|
+
"Last lighthouse audit at": (/* @__PURE__ */ new Date()).toISOString()
|
|
91
|
+
};
|
|
92
|
+
await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
|
|
93
|
+
}
|
|
65
94
|
var WEBSITES_TABLE;
|
|
66
95
|
var init_websites = __esm({
|
|
67
96
|
"src/reports/airtable/websites.ts"() {
|
|
@@ -70,6 +99,82 @@ var init_websites = __esm({
|
|
|
70
99
|
}
|
|
71
100
|
});
|
|
72
101
|
|
|
102
|
+
// src/inventory/airtable.ts
|
|
103
|
+
var airtable_exports = {};
|
|
104
|
+
__export(airtable_exports, {
|
|
105
|
+
fromAirtableBase: () => fromAirtableBase
|
|
106
|
+
});
|
|
107
|
+
function fromAirtableBase(base, opts = {}) {
|
|
108
|
+
return async () => {
|
|
109
|
+
const workdir = opts.workdir ?? process.env.REDDOOR_FLEET_WORKDIR;
|
|
110
|
+
if (!workdir) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
"fromAirtableBase requires `workdir` option or REDDOOR_FLEET_WORKDIR env (sites need a local path)"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const websites = await listWebsites(base);
|
|
116
|
+
return websites.filter((w) => w.maintenanceFreq !== "None" || w.testingFreq !== "None").map((w) => {
|
|
117
|
+
const slug = siteSlug(w.name);
|
|
118
|
+
const site = {
|
|
119
|
+
path: `${workdir}/${slug}`,
|
|
120
|
+
name: slug,
|
|
121
|
+
meta: { airtableRowId: w.id, displayName: w.name }
|
|
122
|
+
};
|
|
123
|
+
if (w.url) site.repoUrl = w.url;
|
|
124
|
+
return site;
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
var init_airtable = __esm({
|
|
129
|
+
"src/inventory/airtable.ts"() {
|
|
130
|
+
"use strict";
|
|
131
|
+
init_websites();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// src/audits/lighthouse-airtable.ts
|
|
136
|
+
var lighthouse_airtable_exports = {};
|
|
137
|
+
__export(lighthouse_airtable_exports, {
|
|
138
|
+
lighthouseScoresFromResult: () => lighthouseScoresFromResult,
|
|
139
|
+
resolveSlugFromCwd: () => resolveSlugFromCwd
|
|
140
|
+
});
|
|
141
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
142
|
+
import { join as join6 } from "path";
|
|
143
|
+
function lighthouseScoresFromResult(result) {
|
|
144
|
+
if (result.audit !== "lighthouse") {
|
|
145
|
+
throw new Error(`Expected a 'lighthouse' AuditResult, got '${result.audit}'`);
|
|
146
|
+
}
|
|
147
|
+
const details = result.details ?? {};
|
|
148
|
+
const summary = details.summary ?? {};
|
|
149
|
+
const toPct = (n) => typeof n === "number" && !Number.isNaN(n) ? Math.round(n * 100) : 0;
|
|
150
|
+
return {
|
|
151
|
+
performance: toPct(summary["performance"]),
|
|
152
|
+
accessibility: toPct(summary["accessibility"]),
|
|
153
|
+
bestPractices: toPct(summary["best-practices"]),
|
|
154
|
+
seo: toPct(summary["seo"])
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function resolveSlugFromCwd(cwd) {
|
|
158
|
+
try {
|
|
159
|
+
const pkgPath = join6(cwd, "package.json");
|
|
160
|
+
const raw = await readFile6(pkgPath, "utf-8");
|
|
161
|
+
const pkg = JSON.parse(raw);
|
|
162
|
+
if (!pkg.name) throw new Error("package.json has no 'name' field");
|
|
163
|
+
return siteSlug(pkg.name);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Could not derive site slug from ${cwd}/package.json: ${e.message}. Pass --write-airtable=<slug> explicitly.`,
|
|
167
|
+
{ cause: e }
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
var init_lighthouse_airtable = __esm({
|
|
172
|
+
"src/audits/lighthouse-airtable.ts"() {
|
|
173
|
+
"use strict";
|
|
174
|
+
init_websites();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
73
178
|
// src/reports/airtable/reports.ts
|
|
74
179
|
function mapRow2(rec) {
|
|
75
180
|
const f = rec.fields;
|
|
@@ -177,6 +282,40 @@ var init_reports = __esm({
|
|
|
177
282
|
}
|
|
178
283
|
});
|
|
179
284
|
|
|
285
|
+
// src/reports/maintenance-email/assets/index.ts
|
|
286
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
287
|
+
import { dirname as dirname2, join as join20 } from "path";
|
|
288
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
289
|
+
async function loadBundledImages() {
|
|
290
|
+
const [check, blurred] = await Promise.all([
|
|
291
|
+
readFile12(join20(here, "check.png")),
|
|
292
|
+
readFile12(join20(here, "blurredTests.jpg"))
|
|
293
|
+
]);
|
|
294
|
+
return {
|
|
295
|
+
check: {
|
|
296
|
+
bytes: new Uint8Array(check),
|
|
297
|
+
contentType: "image/png",
|
|
298
|
+
cid: CHECK_CID,
|
|
299
|
+
filename: "check.png"
|
|
300
|
+
},
|
|
301
|
+
blurred: {
|
|
302
|
+
bytes: new Uint8Array(blurred),
|
|
303
|
+
contentType: "image/jpeg",
|
|
304
|
+
cid: BLURRED_CID,
|
|
305
|
+
filename: "blurredTests.jpg"
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
var here, CHECK_CID, BLURRED_CID;
|
|
310
|
+
var init_assets = __esm({
|
|
311
|
+
"src/reports/maintenance-email/assets/index.ts"() {
|
|
312
|
+
"use strict";
|
|
313
|
+
here = dirname2(fileURLToPath2(import.meta.url));
|
|
314
|
+
CHECK_CID = "rd-check-png";
|
|
315
|
+
BLURRED_CID = "rd-blurred-tests-jpg";
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
180
319
|
// src/reports/maintenance-email/template.ts
|
|
181
320
|
function fmtDate(d) {
|
|
182
321
|
if (!d) return "";
|
|
@@ -344,8 +483,9 @@ var CHECK_PNG, BLURRED_TESTS;
|
|
|
344
483
|
var init_template = __esm({
|
|
345
484
|
"src/reports/maintenance-email/template.ts"() {
|
|
346
485
|
"use strict";
|
|
347
|
-
|
|
348
|
-
|
|
486
|
+
init_assets();
|
|
487
|
+
CHECK_PNG = `cid:${CHECK_CID}`;
|
|
488
|
+
BLURRED_TESTS = `cid:${BLURRED_CID}`;
|
|
349
489
|
}
|
|
350
490
|
});
|
|
351
491
|
|
|
@@ -375,6 +515,27 @@ async function fetchAttachmentBytes(url) {
|
|
|
375
515
|
const ab = await res.arrayBuffer();
|
|
376
516
|
return { bytes: new Uint8Array(ab), contentType };
|
|
377
517
|
}
|
|
518
|
+
async function uploadAttachment(recordId, fieldName, body, filename, contentType) {
|
|
519
|
+
const apiKey = process.env.AIRTABLE_PAT;
|
|
520
|
+
const baseId = process.env.AIRTABLE_BASE_ID;
|
|
521
|
+
if (!apiKey || !baseId) {
|
|
522
|
+
throw new Error("AIRTABLE_PAT and AIRTABLE_BASE_ID must be set");
|
|
523
|
+
}
|
|
524
|
+
const base64 = typeof body === "string" ? Buffer.from(body, "utf-8").toString("base64") : Buffer.from(body).toString("base64");
|
|
525
|
+
const payload = { contentType, file: base64, filename };
|
|
526
|
+
const url = `https://content.airtable.com/v0/${baseId}/${recordId}/${encodeURIComponent(fieldName)}/uploadAttachment`;
|
|
527
|
+
const res = await fetch(url, {
|
|
528
|
+
method: "POST",
|
|
529
|
+
headers: {
|
|
530
|
+
Authorization: `Bearer ${apiKey}`,
|
|
531
|
+
"Content-Type": "application/json"
|
|
532
|
+
},
|
|
533
|
+
body: JSON.stringify(payload)
|
|
534
|
+
});
|
|
535
|
+
if (!res.ok) {
|
|
536
|
+
throw new Error(`Airtable upload failed: ${res.status} ${res.statusText} ${await res.text()}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
378
539
|
var init_attachments = __esm({
|
|
379
540
|
"src/reports/airtable/attachments.ts"() {
|
|
380
541
|
"use strict";
|
|
@@ -454,6 +615,7 @@ async function sendOne(client, base, site, report) {
|
|
|
454
615
|
throw new Error(`Report ${report.reportId} has no Lighthouse scores`);
|
|
455
616
|
}
|
|
456
617
|
const { bytes, contentType } = await fetchAttachmentBytes(site.headerImage.url);
|
|
618
|
+
const bundled = await loadBundledImages();
|
|
457
619
|
const slug = siteSlug(site.name);
|
|
458
620
|
const cidName = `${slug}-header`;
|
|
459
621
|
const { html } = await renderReportHtml({
|
|
@@ -506,6 +668,21 @@ async function sendOne(client, base, site, report) {
|
|
|
506
668
|
content: Buffer.from(bytes).toString("base64"),
|
|
507
669
|
contentType,
|
|
508
670
|
inlineContentId: cidName
|
|
671
|
+
},
|
|
672
|
+
// Bundled images referenced via cid:rd-check-png / cid:rd-blurred-tests-jpg
|
|
673
|
+
// in the template. Attached inline so the email is self-contained — no
|
|
674
|
+
// external CDN dependency, no image-blocked broken icons in webmail.
|
|
675
|
+
{
|
|
676
|
+
filename: bundled.check.filename,
|
|
677
|
+
content: Buffer.from(bundled.check.bytes).toString("base64"),
|
|
678
|
+
contentType: bundled.check.contentType,
|
|
679
|
+
inlineContentId: bundled.check.cid
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
filename: bundled.blurred.filename,
|
|
683
|
+
content: Buffer.from(bundled.blurred.bytes).toString("base64"),
|
|
684
|
+
contentType: bundled.blurred.contentType,
|
|
685
|
+
inlineContentId: bundled.blurred.cid
|
|
509
686
|
}
|
|
510
687
|
],
|
|
511
688
|
// Stable across retries of the same row — if Airtable stamping fails after a
|
|
@@ -550,6 +727,7 @@ var init_orchestrate = __esm({
|
|
|
550
727
|
init_websites();
|
|
551
728
|
init_attachments();
|
|
552
729
|
init_render();
|
|
730
|
+
init_assets();
|
|
553
731
|
init_resend();
|
|
554
732
|
FROM_ADDRESS = "Reddoor Reports <reports@reddoorla.com>";
|
|
555
733
|
REPLY_TO = "info@reddoorla.com";
|
|
@@ -557,8 +735,8 @@ var init_orchestrate = __esm({
|
|
|
557
735
|
});
|
|
558
736
|
|
|
559
737
|
// src/cli/bin.ts
|
|
560
|
-
import { dirname as
|
|
561
|
-
import { fileURLToPath as
|
|
738
|
+
import { dirname as dirname4 } from "path";
|
|
739
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
562
740
|
import { cac } from "cac";
|
|
563
741
|
|
|
564
742
|
// src/cli/commands/audit.ts
|
|
@@ -1210,6 +1388,10 @@ async function runAudits(site, which) {
|
|
|
1210
1388
|
)
|
|
1211
1389
|
);
|
|
1212
1390
|
}
|
|
1391
|
+
async function runAuditsAcross(sites, which) {
|
|
1392
|
+
const all = await Promise.all(sites.map((s) => runAudits(s, which)));
|
|
1393
|
+
return all.flat();
|
|
1394
|
+
}
|
|
1213
1395
|
|
|
1214
1396
|
// src/cli/fleet/resolve-sites.ts
|
|
1215
1397
|
import { pathToFileURL } from "url";
|
|
@@ -1265,6 +1447,13 @@ async function resolveSites(input) {
|
|
|
1265
1447
|
exitCode: 2
|
|
1266
1448
|
});
|
|
1267
1449
|
}
|
|
1450
|
+
if (input.fleet === "airtable") {
|
|
1451
|
+
const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1452
|
+
const { fromAirtableBase: fromAirtableBase2 } = await Promise.resolve().then(() => (init_airtable(), airtable_exports));
|
|
1453
|
+
const base = openBase2(readAirtableConfig2());
|
|
1454
|
+
const provider = fromAirtableBase2(base, input.workdir ? { workdir: input.workdir } : {});
|
|
1455
|
+
return provider();
|
|
1456
|
+
}
|
|
1268
1457
|
if (input.fleet) {
|
|
1269
1458
|
const fleetPath = resolve(input.cwd, input.fleet);
|
|
1270
1459
|
const ext = extname(fleetPath).toLowerCase();
|
|
@@ -1373,28 +1562,59 @@ async function runAuditCommand(site, opts) {
|
|
|
1373
1562
|
let sites = await resolveSites({
|
|
1374
1563
|
...site !== void 0 ? { site } : {},
|
|
1375
1564
|
...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
|
|
1565
|
+
...opts.workdir !== void 0 ? { workdir: opts.workdir } : {},
|
|
1376
1566
|
cwd
|
|
1377
1567
|
});
|
|
1378
1568
|
if (opts.fleet) {
|
|
1379
1569
|
const workdir = opts.workdir ?? `${process.env.HOME ?? ""}/.reddoor-maint/sites`;
|
|
1380
1570
|
sites = await Promise.all(sites.map((s) => cloneIfNeeded(s, { workdir })));
|
|
1381
1571
|
}
|
|
1382
|
-
const results =
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1572
|
+
const results = await runAuditsAcross(sites, which);
|
|
1573
|
+
let output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);
|
|
1574
|
+
if (opts.writeAirtable !== void 0) {
|
|
1575
|
+
const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1576
|
+
const { listWebsites: listWebsites2, updateScores: updateScores2, siteSlug: siteSlug2 } = await Promise.resolve().then(() => (init_websites(), websites_exports));
|
|
1577
|
+
const { lighthouseScoresFromResult: lighthouseScoresFromResult2, resolveSlugFromCwd: resolveSlugFromCwd2 } = await Promise.resolve().then(() => (init_lighthouse_airtable(), lighthouse_airtable_exports));
|
|
1578
|
+
const slug = typeof opts.writeAirtable === "string" && opts.writeAirtable.length > 0 ? opts.writeAirtable : await resolveSlugFromCwd2(cwd);
|
|
1579
|
+
const lhResult = results.find((r) => r.audit === "lighthouse");
|
|
1580
|
+
if (!lhResult) {
|
|
1581
|
+
throw Object.assign(
|
|
1582
|
+
new Error(
|
|
1583
|
+
"--write-airtable requires a lighthouse result; did you pass --only without lighthouse?"
|
|
1584
|
+
),
|
|
1585
|
+
{ exitCode: 2 }
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
if (lhResult.status === "fail") {
|
|
1589
|
+
throw Object.assign(
|
|
1590
|
+
new Error(
|
|
1591
|
+
`Lighthouse audit failed; refusing to write scores to Airtable. Summary: ${lhResult.summary}`
|
|
1592
|
+
),
|
|
1593
|
+
{ exitCode: 1 }
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
const base = openBase2(readAirtableConfig2());
|
|
1597
|
+
const websites = await listWebsites2(base);
|
|
1598
|
+
const target = websites.find((w) => siteSlug2(w.name) === slug);
|
|
1599
|
+
if (!target) {
|
|
1600
|
+
throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
|
|
1601
|
+
}
|
|
1602
|
+
const scores = lighthouseScoresFromResult2(lhResult);
|
|
1603
|
+
await updateScores2(base, target.id, scores);
|
|
1604
|
+
output += `
|
|
1605
|
+
|
|
1606
|
+
\u2192 wrote scores to Websites[${target.name}]: P=${scores.performance} A=${scores.accessibility} BP=${scores.bestPractices} SEO=${scores.seo}`;
|
|
1386
1607
|
}
|
|
1387
|
-
const output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);
|
|
1388
1608
|
return { output, code: exitCode(results) };
|
|
1389
1609
|
}
|
|
1390
1610
|
|
|
1391
1611
|
// src/cli/commands/sync-configs.ts
|
|
1392
|
-
import { readFile as
|
|
1393
|
-
import { join as
|
|
1612
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1613
|
+
import { join as join8, resolve as resolve3 } from "path";
|
|
1394
1614
|
|
|
1395
1615
|
// src/recipes/sync-configs.ts
|
|
1396
|
-
import { readFile as
|
|
1397
|
-
import { join as
|
|
1616
|
+
import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
1617
|
+
import { join as join7 } from "path";
|
|
1398
1618
|
|
|
1399
1619
|
// src/recipes/sync-configs/templates.ts
|
|
1400
1620
|
var eslint = {
|
|
@@ -1658,7 +1878,7 @@ function isConfigName(value) {
|
|
|
1658
1878
|
}
|
|
1659
1879
|
async function readMaybe(path) {
|
|
1660
1880
|
try {
|
|
1661
|
-
return await
|
|
1881
|
+
return await readFile7(path, "utf-8");
|
|
1662
1882
|
} catch {
|
|
1663
1883
|
return null;
|
|
1664
1884
|
}
|
|
@@ -1666,13 +1886,13 @@ async function readMaybe(path) {
|
|
|
1666
1886
|
async function planTemplateDiffs(cwd, templates) {
|
|
1667
1887
|
const diffs = [];
|
|
1668
1888
|
for (const t of templates) {
|
|
1669
|
-
const existing = await readMaybe(
|
|
1889
|
+
const existing = await readMaybe(join7(cwd, t.path));
|
|
1670
1890
|
if (existing !== t.contents) diffs.push(t);
|
|
1671
1891
|
}
|
|
1672
1892
|
return diffs;
|
|
1673
1893
|
}
|
|
1674
1894
|
async function planGitignore(cwd) {
|
|
1675
|
-
const existing = await readMaybe(
|
|
1895
|
+
const existing = await readMaybe(join7(cwd, ".gitignore"));
|
|
1676
1896
|
const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);
|
|
1677
1897
|
const tracked = await listTrackedFiles(cwd);
|
|
1678
1898
|
const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);
|
|
@@ -1680,7 +1900,7 @@ async function planGitignore(cwd) {
|
|
|
1680
1900
|
return { kind: "apply", content: merge.content, toUntrack, added: merge.added };
|
|
1681
1901
|
}
|
|
1682
1902
|
async function applyGitignore(cwd, plan) {
|
|
1683
|
-
await writeFile3(
|
|
1903
|
+
await writeFile3(join7(cwd, ".gitignore"), plan.content, "utf-8");
|
|
1684
1904
|
if (plan.toUntrack.length > 0) {
|
|
1685
1905
|
await removeFromIndex(cwd, plan.toUntrack);
|
|
1686
1906
|
}
|
|
@@ -1703,7 +1923,7 @@ async function syncConfigs(site, opts = {}) {
|
|
|
1703
1923
|
},
|
|
1704
1924
|
apply: async ({ templateDiffs, gitignorePlan }, { commit: commit2 }) => {
|
|
1705
1925
|
for (const t of templateDiffs) {
|
|
1706
|
-
await writeFile3(
|
|
1926
|
+
await writeFile3(join7(site.path, t.path), t.contents, "utf-8");
|
|
1707
1927
|
await commit2(`chore: sync ${t.config} config from @reddoorla/maintenance`);
|
|
1708
1928
|
}
|
|
1709
1929
|
if (gitignorePlan.kind === "apply") {
|
|
@@ -1732,7 +1952,7 @@ function parseOnly2(value) {
|
|
|
1732
1952
|
async function dryPlanGitignore(cwd) {
|
|
1733
1953
|
let existing;
|
|
1734
1954
|
try {
|
|
1735
|
-
existing = await
|
|
1955
|
+
existing = await readFile8(join8(cwd, ".gitignore"), "utf-8");
|
|
1736
1956
|
} catch {
|
|
1737
1957
|
return "would create .gitignore";
|
|
1738
1958
|
}
|
|
@@ -1747,7 +1967,7 @@ async function dryPlan(cwd, which) {
|
|
|
1747
1967
|
for (const t of templateTargets) {
|
|
1748
1968
|
let existing = "";
|
|
1749
1969
|
try {
|
|
1750
|
-
existing = await
|
|
1970
|
+
existing = await readFile8(join8(cwd, t.path), "utf-8");
|
|
1751
1971
|
} catch {
|
|
1752
1972
|
}
|
|
1753
1973
|
if (existing !== t.contents) lines.push(`would update ${t.path} (config: ${t.config})`);
|
|
@@ -1795,7 +2015,7 @@ import { resolve as resolve4 } from "path";
|
|
|
1795
2015
|
|
|
1796
2016
|
// src/recipes/bump-deps.ts
|
|
1797
2017
|
import { stat as stat2 } from "fs/promises";
|
|
1798
|
-
import { join as
|
|
2018
|
+
import { join as join9 } from "path";
|
|
1799
2019
|
async function exists(path) {
|
|
1800
2020
|
try {
|
|
1801
2021
|
await stat2(path);
|
|
@@ -1824,10 +2044,10 @@ async function bumpDeps(site, opts = {}) {
|
|
|
1824
2044
|
// land on top of whatever else was in the tree.
|
|
1825
2045
|
checkTreeFirst: true,
|
|
1826
2046
|
plan: async () => {
|
|
1827
|
-
const hasPnpmLock = await exists(
|
|
2047
|
+
const hasPnpmLock = await exists(join9(site.path, "pnpm-lock.yaml"));
|
|
1828
2048
|
if (!hasPnpmLock) {
|
|
1829
|
-
const hasNpmLock = await exists(
|
|
1830
|
-
const hasYarnLock = await exists(
|
|
2049
|
+
const hasNpmLock = await exists(join9(site.path, "package-lock.json"));
|
|
2050
|
+
const hasYarnLock = await exists(join9(site.path, "yarn.lock"));
|
|
1831
2051
|
if (hasNpmLock || hasYarnLock) {
|
|
1832
2052
|
const competing = hasNpmLock ? "package-lock.json" : "yarn.lock";
|
|
1833
2053
|
return {
|
|
@@ -1897,12 +2117,12 @@ async function runBumpDepsCommand(site, opts) {
|
|
|
1897
2117
|
import { resolve as resolve5 } from "path";
|
|
1898
2118
|
|
|
1899
2119
|
// src/recipes/svelte-5/index.ts
|
|
1900
|
-
import { join as
|
|
2120
|
+
import { join as join15 } from "path";
|
|
1901
2121
|
|
|
1902
2122
|
// src/util/pkg.ts
|
|
1903
|
-
import { readFile as
|
|
2123
|
+
import { readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
|
|
1904
2124
|
async function readPackageJson(path) {
|
|
1905
|
-
const raw = await
|
|
2125
|
+
const raw = await readFile9(path, "utf-8");
|
|
1906
2126
|
return JSON.parse(raw);
|
|
1907
2127
|
}
|
|
1908
2128
|
function detectIndentFromContent(raw) {
|
|
@@ -1912,7 +2132,7 @@ function detectIndentFromContent(raw) {
|
|
|
1912
2132
|
async function writePackageJson(path, pkg) {
|
|
1913
2133
|
let indent = " ";
|
|
1914
2134
|
try {
|
|
1915
|
-
const existing = await
|
|
2135
|
+
const existing = await readFile9(path, "utf-8");
|
|
1916
2136
|
indent = detectIndentFromContent(existing);
|
|
1917
2137
|
} catch {
|
|
1918
2138
|
}
|
|
@@ -1946,7 +2166,7 @@ function bumpDep(pkg, name, version2, opts = {}) {
|
|
|
1946
2166
|
}
|
|
1947
2167
|
|
|
1948
2168
|
// src/recipes/svelte-5/step-bump-versions.ts
|
|
1949
|
-
import { join as
|
|
2169
|
+
import { join as join10 } from "path";
|
|
1950
2170
|
var SVELTE_5_VERSIONS = {
|
|
1951
2171
|
svelte: "^5.55.5",
|
|
1952
2172
|
"@sveltejs/kit": "^2.59.0",
|
|
@@ -1959,7 +2179,7 @@ var SVELTE_5_VERSIONS = {
|
|
|
1959
2179
|
"typescript-svelte-plugin": "^0.3.52"
|
|
1960
2180
|
};
|
|
1961
2181
|
async function bumpToSvelte5Versions(cwd) {
|
|
1962
|
-
const pkgPath =
|
|
2182
|
+
const pkgPath = join10(cwd, "package.json");
|
|
1963
2183
|
const pkg = await readPackageJson(pkgPath);
|
|
1964
2184
|
let next = pkg;
|
|
1965
2185
|
for (const [name, version2] of Object.entries(SVELTE_5_VERSIONS)) {
|
|
@@ -1971,8 +2191,8 @@ async function bumpToSvelte5Versions(cwd) {
|
|
|
1971
2191
|
}
|
|
1972
2192
|
|
|
1973
2193
|
// src/recipes/svelte-5/step-svelte-config.ts
|
|
1974
|
-
import { readFile as
|
|
1975
|
-
import { join as
|
|
2194
|
+
import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
|
|
2195
|
+
import { join as join11 } from "path";
|
|
1976
2196
|
var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
|
|
1977
2197
|
var IMPORT_FROM_VITE_PLUGIN = new RegExp(
|
|
1978
2198
|
String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
|
|
@@ -2013,10 +2233,10 @@ function dropPreprocessKey(source) {
|
|
|
2013
2233
|
return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
|
|
2014
2234
|
}
|
|
2015
2235
|
async function migrateSvelteConfig(cwd) {
|
|
2016
|
-
const path =
|
|
2236
|
+
const path = join11(cwd, "svelte.config.js");
|
|
2017
2237
|
let src;
|
|
2018
2238
|
try {
|
|
2019
|
-
src = await
|
|
2239
|
+
src = await readFile10(path, "utf-8");
|
|
2020
2240
|
} catch {
|
|
2021
2241
|
return false;
|
|
2022
2242
|
}
|
|
@@ -2050,9 +2270,9 @@ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
|
|
|
2050
2270
|
}
|
|
2051
2271
|
|
|
2052
2272
|
// src/recipes/svelte-5/step-tailwind-upgrade.ts
|
|
2053
|
-
import { join as
|
|
2273
|
+
import { join as join12 } from "path";
|
|
2054
2274
|
async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
2055
|
-
const pkg = await readPackageJson(
|
|
2275
|
+
const pkg = await readPackageJson(join12(cwd, "package.json"));
|
|
2056
2276
|
const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
|
|
2057
2277
|
if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
|
|
2058
2278
|
if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
|
|
@@ -2073,8 +2293,8 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
|
2073
2293
|
}
|
|
2074
2294
|
|
|
2075
2295
|
// src/recipes/svelte-5/step-gotchas.ts
|
|
2076
|
-
import { readFile as
|
|
2077
|
-
import { join as
|
|
2296
|
+
import { readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
2297
|
+
import { join as join13 } from "path";
|
|
2078
2298
|
import { glob as glob2 } from "tinyglobby";
|
|
2079
2299
|
|
|
2080
2300
|
// src/recipes/svelte-5/codemods/on-event-to-handler.ts
|
|
@@ -2406,8 +2626,8 @@ async function planGotchaCodemods(cwd) {
|
|
|
2406
2626
|
const changes = [];
|
|
2407
2627
|
const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
|
|
2408
2628
|
for (const rel of relPaths) {
|
|
2409
|
-
const path =
|
|
2410
|
-
const before = await
|
|
2629
|
+
const path = join13(cwd, rel);
|
|
2630
|
+
const before = await readFile11(path, "utf-8");
|
|
2411
2631
|
const after = CODEMODS.reduce((s, fn) => fn(s), before);
|
|
2412
2632
|
if (after !== before) changes.push({ rel, after });
|
|
2413
2633
|
}
|
|
@@ -2416,7 +2636,7 @@ async function planGotchaCodemods(cwd) {
|
|
|
2416
2636
|
async function applyGotchaCodemods(cwd) {
|
|
2417
2637
|
const changes = await planGotchaCodemods(cwd);
|
|
2418
2638
|
for (const c of changes) {
|
|
2419
|
-
await writeFile6(
|
|
2639
|
+
await writeFile6(join13(cwd, c.rel), c.after, "utf-8");
|
|
2420
2640
|
}
|
|
2421
2641
|
return { filesChanged: changes.length };
|
|
2422
2642
|
}
|
|
@@ -2440,7 +2660,7 @@ async function verifyMigration(cwd, spawn2 = defaultSpawn) {
|
|
|
2440
2660
|
|
|
2441
2661
|
// src/recipes/svelte-5/step-summary.ts
|
|
2442
2662
|
import { writeFile as writeFile7 } from "fs/promises";
|
|
2443
|
-
import { join as
|
|
2663
|
+
import { join as join14 } from "path";
|
|
2444
2664
|
async function writeMigrationSummary(input) {
|
|
2445
2665
|
const lines = [
|
|
2446
2666
|
`# Svelte 4 \u2192 5 migration summary`,
|
|
@@ -2457,7 +2677,7 @@ async function writeMigrationSummary(input) {
|
|
|
2457
2677
|
`- Verify Playwright a11y tests still pass.`
|
|
2458
2678
|
];
|
|
2459
2679
|
const content = lines.join("\n") + "\n";
|
|
2460
|
-
const path =
|
|
2680
|
+
const path = join14(input.cwd, "MIGRATION_SVELTE_5.md");
|
|
2461
2681
|
await writeFile7(path, content, "utf-8");
|
|
2462
2682
|
return path;
|
|
2463
2683
|
}
|
|
@@ -2465,7 +2685,7 @@ async function writeMigrationSummary(input) {
|
|
|
2465
2685
|
// src/recipes/svelte-5/index.ts
|
|
2466
2686
|
async function alreadyOnSvelte5(cwd) {
|
|
2467
2687
|
try {
|
|
2468
|
-
const pkg = await readPackageJson(
|
|
2688
|
+
const pkg = await readPackageJson(join15(cwd, "package.json"));
|
|
2469
2689
|
const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
|
|
2470
2690
|
return !!v && /^\^?5\./.test(v);
|
|
2471
2691
|
} catch {
|
|
@@ -2560,7 +2780,7 @@ import { resolve as resolve6 } from "path";
|
|
|
2560
2780
|
|
|
2561
2781
|
// src/recipes/convert-to-pnpm.ts
|
|
2562
2782
|
import { rm as rm3, stat as stat3 } from "fs/promises";
|
|
2563
|
-
import { join as
|
|
2783
|
+
import { join as join16 } from "path";
|
|
2564
2784
|
|
|
2565
2785
|
// src/recipes/convert-to-pnpm/script-rewrites.ts
|
|
2566
2786
|
function rewriteScriptForPnpm(script) {
|
|
@@ -2593,9 +2813,9 @@ async function exists2(path) {
|
|
|
2593
2813
|
async function convertToPnpm(site, opts = {}) {
|
|
2594
2814
|
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
2595
2815
|
const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;
|
|
2596
|
-
const pnpmLockPath =
|
|
2597
|
-
const npmLockPath =
|
|
2598
|
-
const yarnLockPath =
|
|
2816
|
+
const pnpmLockPath = join16(site.path, "pnpm-lock.yaml");
|
|
2817
|
+
const npmLockPath = join16(site.path, "package-lock.json");
|
|
2818
|
+
const yarnLockPath = join16(site.path, "yarn.lock");
|
|
2599
2819
|
return withRecipe({
|
|
2600
2820
|
name: "convert-to-pnpm",
|
|
2601
2821
|
site,
|
|
@@ -2618,7 +2838,7 @@ async function convertToPnpm(site, opts = {}) {
|
|
|
2618
2838
|
if (hasYarnLock) await rm3(yarnLockPath, { force: true });
|
|
2619
2839
|
const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
|
|
2620
2840
|
await commit2(`chore(pnpm): remove ${sourceLock}`);
|
|
2621
|
-
const pkgPath =
|
|
2841
|
+
const pkgPath = join16(cwd, "package.json");
|
|
2622
2842
|
const pkg = await readPackageJson(pkgPath);
|
|
2623
2843
|
const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
|
|
2624
2844
|
if (pkg.scripts && typeof pkg.scripts === "object") {
|
|
@@ -2631,7 +2851,7 @@ async function convertToPnpm(site, opts = {}) {
|
|
|
2631
2851
|
}
|
|
2632
2852
|
await writePackageJson(pkgPath, next);
|
|
2633
2853
|
await commit2("chore(pnpm): pin packageManager + rewrite npm scripts");
|
|
2634
|
-
await rm3(
|
|
2854
|
+
await rm3(join16(cwd, "node_modules"), { recursive: true, force: true });
|
|
2635
2855
|
const installResult = await spawn2("pnpm", ["install"], { cwd, streaming: true });
|
|
2636
2856
|
if (installResult.code !== 0) {
|
|
2637
2857
|
return { kind: "failed", notes: `pnpm install failed (exit ${installResult.code})` };
|
|
@@ -2672,16 +2892,16 @@ import { resolve as resolve7 } from "path";
|
|
|
2672
2892
|
|
|
2673
2893
|
// src/recipes/onboard.ts
|
|
2674
2894
|
import { stat as stat4 } from "fs/promises";
|
|
2675
|
-
import { join as
|
|
2895
|
+
import { join as join18 } from "path";
|
|
2676
2896
|
|
|
2677
2897
|
// src/util/self-version.ts
|
|
2678
2898
|
import { readFileSync } from "fs";
|
|
2679
2899
|
import { fileURLToPath } from "url";
|
|
2680
|
-
import { dirname, join as
|
|
2900
|
+
import { dirname, join as join17 } from "path";
|
|
2681
2901
|
function selfPackageVersion(callerImportMetaUrl) {
|
|
2682
2902
|
try {
|
|
2683
|
-
const
|
|
2684
|
-
const raw = readFileSync(
|
|
2903
|
+
const here3 = dirname(fileURLToPath(callerImportMetaUrl));
|
|
2904
|
+
const raw = readFileSync(join17(here3, "..", "..", "package.json"), "utf-8");
|
|
2685
2905
|
const pkg = JSON.parse(raw);
|
|
2686
2906
|
return pkg.version ?? "0.0.0";
|
|
2687
2907
|
} catch {
|
|
@@ -2731,13 +2951,13 @@ async function onboard(site, opts = {}) {
|
|
|
2731
2951
|
name: "onboard",
|
|
2732
2952
|
site,
|
|
2733
2953
|
plan: async () => {
|
|
2734
|
-
if (!await exists3(
|
|
2954
|
+
if (!await exists3(join18(site.path, "pnpm-lock.yaml"))) {
|
|
2735
2955
|
return {
|
|
2736
2956
|
kind: "failed",
|
|
2737
2957
|
notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
|
|
2738
2958
|
};
|
|
2739
2959
|
}
|
|
2740
|
-
const pkgPath =
|
|
2960
|
+
const pkgPath = join18(site.path, "package.json");
|
|
2741
2961
|
const pkg = await readPackageJson(pkgPath);
|
|
2742
2962
|
const toAdd = [];
|
|
2743
2963
|
if (!isDeclared(pkg, PACKAGE_NAME)) {
|
|
@@ -2757,7 +2977,7 @@ async function onboard(site, opts = {}) {
|
|
|
2757
2977
|
return { kind: "apply", plan: { pkg, toAdd } };
|
|
2758
2978
|
},
|
|
2759
2979
|
apply: async ({ pkg, toAdd }, { commit: commit2, cwd }) => {
|
|
2760
|
-
const pkgPath =
|
|
2980
|
+
const pkgPath = join18(cwd, "package.json");
|
|
2761
2981
|
let next = pkg;
|
|
2762
2982
|
for (const dep of toAdd) {
|
|
2763
2983
|
next = bumpDep(next, dep.name, dep.version);
|
|
@@ -2826,7 +3046,7 @@ import { resolve as resolve8 } from "path";
|
|
|
2826
3046
|
|
|
2827
3047
|
// src/recipes/svelte-codemods.ts
|
|
2828
3048
|
import { writeFile as writeFile8 } from "fs/promises";
|
|
2829
|
-
import { join as
|
|
3049
|
+
import { join as join19 } from "path";
|
|
2830
3050
|
async function svelteCodemods(site) {
|
|
2831
3051
|
return withRecipe({
|
|
2832
3052
|
name: "svelte-codemods",
|
|
@@ -2840,7 +3060,7 @@ async function svelteCodemods(site) {
|
|
|
2840
3060
|
},
|
|
2841
3061
|
apply: async (changes, { commit: commit2, cwd }) => {
|
|
2842
3062
|
for (const c of changes) {
|
|
2843
|
-
await writeFile8(
|
|
3063
|
+
await writeFile8(join19(cwd, c.rel), c.after, "utf-8");
|
|
2844
3064
|
}
|
|
2845
3065
|
await commit2(`refactor(svelte5): apply codemods (${changes.length} files)`);
|
|
2846
3066
|
return { kind: "ok" };
|
|
@@ -2879,6 +3099,12 @@ init_websites();
|
|
|
2879
3099
|
init_reports();
|
|
2880
3100
|
|
|
2881
3101
|
// src/reports/due.ts
|
|
3102
|
+
var ELIGIBLE_STATUSES = /* @__PURE__ */ new Set([
|
|
3103
|
+
"in development",
|
|
3104
|
+
"launch period",
|
|
3105
|
+
"maintenance",
|
|
3106
|
+
"hosting"
|
|
3107
|
+
]);
|
|
2882
3108
|
var MONTHS = {
|
|
2883
3109
|
Monthly: 1,
|
|
2884
3110
|
Quarterly: 3,
|
|
@@ -2908,6 +3134,7 @@ function findDueReports(websites, reports, today) {
|
|
|
2908
3134
|
const out = [];
|
|
2909
3135
|
const todayStart = startOfDay(today);
|
|
2910
3136
|
for (const site of websites) {
|
|
3137
|
+
if (site.status !== null && !ELIGIBLE_STATUSES.has(site.status)) continue;
|
|
2911
3138
|
for (const type of ["Maintenance", "Testing"]) {
|
|
2912
3139
|
const freq = type === "Maintenance" ? site.maintenanceFreq : site.testingFreq;
|
|
2913
3140
|
if (freq === "None") continue;
|
|
@@ -2931,8 +3158,9 @@ function findDueReports(websites, reports, today) {
|
|
|
2931
3158
|
init_render();
|
|
2932
3159
|
init_websites();
|
|
2933
3160
|
init_reports();
|
|
3161
|
+
init_attachments();
|
|
2934
3162
|
import { mkdir as mkdir2, writeFile as writeFile9 } from "fs/promises";
|
|
2935
|
-
import { dirname as
|
|
3163
|
+
import { dirname as dirname3 } from "path";
|
|
2936
3164
|
function scoresFromWebsite(siteRow) {
|
|
2937
3165
|
const { pScore, rScore, bpScore, seoScore } = siteRow;
|
|
2938
3166
|
if (pScore === null || rScore === null || bpScore === null || seoScore === null) {
|
|
@@ -2970,7 +3198,7 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
2970
3198
|
});
|
|
2971
3199
|
if (options.previewOnly) {
|
|
2972
3200
|
const path = options.previewPath ?? `reports/${slug}/draft.html`;
|
|
2973
|
-
await mkdir2(
|
|
3201
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
2974
3202
|
await writeFile9(path, html, "utf-8");
|
|
2975
3203
|
return { reportRow: null, htmlPath: path, html };
|
|
2976
3204
|
}
|
|
@@ -2986,7 +3214,8 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
2986
3214
|
lighthouse: scores,
|
|
2987
3215
|
lastTestedDate
|
|
2988
3216
|
});
|
|
2989
|
-
|
|
3217
|
+
const htmlFilename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;
|
|
3218
|
+
await uploadAttachment(created.id, "Rendered HTML", html, htmlFilename, "text/html");
|
|
2990
3219
|
await setDraftReady(base, created.id, true);
|
|
2991
3220
|
return { reportRow: created, htmlPath: null, html };
|
|
2992
3221
|
}
|
|
@@ -2996,28 +3225,6 @@ async function derivePeriodStart(base, siteRow, reportType, today) {
|
|
|
2996
3225
|
const latest = sameType[sameType.length - 1];
|
|
2997
3226
|
return latest ? new Date(latest) : daysAgo(today, 30);
|
|
2998
3227
|
}
|
|
2999
|
-
async function uploadHtmlAttachment(recordId, html, slug, periodEnd) {
|
|
3000
|
-
const apiKey = process.env.AIRTABLE_PAT;
|
|
3001
|
-
const baseId = process.env.AIRTABLE_BASE_ID;
|
|
3002
|
-
const filename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;
|
|
3003
|
-
const body = {
|
|
3004
|
-
contentType: "text/html",
|
|
3005
|
-
file: Buffer.from(html, "utf-8").toString("base64"),
|
|
3006
|
-
filename
|
|
3007
|
-
};
|
|
3008
|
-
const url = `https://content.airtable.com/v0/${baseId}/${recordId}/Rendered%20HTML/uploadAttachment`;
|
|
3009
|
-
const res = await fetch(url, {
|
|
3010
|
-
method: "POST",
|
|
3011
|
-
headers: {
|
|
3012
|
-
Authorization: `Bearer ${apiKey}`,
|
|
3013
|
-
"Content-Type": "application/json"
|
|
3014
|
-
},
|
|
3015
|
-
body: JSON.stringify(body)
|
|
3016
|
-
});
|
|
3017
|
-
if (!res.ok) {
|
|
3018
|
-
throw new Error(`Airtable upload failed: ${res.status} ${res.statusText} ${await res.text()}`);
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
3228
|
|
|
3022
3229
|
// src/cli/commands/report.ts
|
|
3023
3230
|
async function runReportCommand(slug, opts) {
|
|
@@ -3077,10 +3284,10 @@ async function runSingleSiteDraft(slug, opts) {
|
|
|
3077
3284
|
|
|
3078
3285
|
// src/cli/version.ts
|
|
3079
3286
|
import { readFileSync as readFileSync2 } from "fs";
|
|
3080
|
-
import { join as
|
|
3287
|
+
import { join as join21 } from "path";
|
|
3081
3288
|
function resolvePackageVersion(fromDir) {
|
|
3082
3289
|
try {
|
|
3083
|
-
const raw = readFileSync2(
|
|
3290
|
+
const raw = readFileSync2(join21(fromDir, "..", "..", "package.json"), "utf-8");
|
|
3084
3291
|
const pkg = JSON.parse(raw);
|
|
3085
3292
|
return pkg.version ?? "unknown";
|
|
3086
3293
|
} catch {
|
|
@@ -3089,8 +3296,8 @@ function resolvePackageVersion(fromDir) {
|
|
|
3089
3296
|
}
|
|
3090
3297
|
|
|
3091
3298
|
// src/cli/bin.ts
|
|
3092
|
-
var
|
|
3093
|
-
var version = resolvePackageVersion(
|
|
3299
|
+
var here2 = dirname4(fileURLToPath3(import.meta.url));
|
|
3300
|
+
var version = resolvePackageVersion(here2);
|
|
3094
3301
|
var AUDIT_DESCRIPTIONS = {
|
|
3095
3302
|
deps: "Diff site package.json against the bundled baseline version map.",
|
|
3096
3303
|
lighthouse: "Run @lhci/cli autorun using the canonical lighthouserc.",
|
|
@@ -3130,31 +3337,55 @@ cli.command("list-recipes", "Print the available recipes.").action(() => {
|
|
|
3130
3337
|
console.log(`${name.padEnd(16)} ${desc}`);
|
|
3131
3338
|
}
|
|
3132
3339
|
});
|
|
3133
|
-
cli.command("audit [site]", "Run audits against a site (default: cwd).").option("--only <names>", "Comma-separated audit names (e.g. deps,lighthouse)").option("--json", "Machine-readable JSON output").option(
|
|
3340
|
+
cli.command("audit [site]", "Run audits against a site (default: cwd).").option("--only <names>", "Comma-separated audit names (e.g. deps,lighthouse)").option("--json", "Machine-readable JSON output").option(
|
|
3341
|
+
"--fleet <inventory>",
|
|
3342
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3343
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").option(
|
|
3344
|
+
"--write-airtable [slug]",
|
|
3345
|
+
"After lighthouse runs, write pScore/rScore/bpScore/seoScore + timestamp to the matching Websites row. Slug defaults to cwd's package.json#name."
|
|
3346
|
+
).action(
|
|
3134
3347
|
async (site, opts) => runOrExit(() => runAuditCommand(site, opts), opts)
|
|
3135
3348
|
);
|
|
3136
|
-
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(
|
|
3349
|
+
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(
|
|
3350
|
+
"--fleet <inventory>",
|
|
3351
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3352
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
3137
3353
|
async (site, opts) => runOrExit(() => runSyncConfigsCommand(site, opts), opts)
|
|
3138
3354
|
);
|
|
3139
|
-
cli.command("bump-deps [site]", "Bump dependencies.").option("--group <group>", "patch | minor | major", { default: "minor" }).option(
|
|
3355
|
+
cli.command("bump-deps [site]", "Bump dependencies.").option("--group <group>", "patch | minor | major", { default: "minor" }).option(
|
|
3356
|
+
"--fleet <inventory>",
|
|
3357
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3358
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
3140
3359
|
async (site, opts) => runOrExit(() => runBumpDepsCommand(site, opts), opts)
|
|
3141
3360
|
);
|
|
3142
|
-
cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to-5).").example("reddoor-maint upgrade svelte-4-to-5 ./my-site").option(
|
|
3361
|
+
cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to-5).").example("reddoor-maint upgrade svelte-4-to-5 ./my-site").option(
|
|
3362
|
+
"--fleet <inventory>",
|
|
3363
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3364
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
3143
3365
|
async (upgrade, site, opts) => runOrExit(() => runUpgradeCommand(upgrade, site, opts), opts)
|
|
3144
3366
|
);
|
|
3145
3367
|
cli.command(
|
|
3146
3368
|
"convert-to-pnpm [site]",
|
|
3147
3369
|
"Convert an npm/yarn site to pnpm (lockfile, packageManager, scripts)."
|
|
3148
|
-
).option(
|
|
3370
|
+
).option(
|
|
3371
|
+
"--fleet <inventory>",
|
|
3372
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3373
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
3149
3374
|
async (site, opts) => runOrExit(() => runConvertToPnpmCommand(site, opts), opts)
|
|
3150
3375
|
);
|
|
3151
|
-
cli.command("svelte-codemods [site]", "Apply Svelte 5 gotcha codemods to an already-migrated site.").option(
|
|
3376
|
+
cli.command("svelte-codemods [site]", "Apply Svelte 5 gotcha codemods to an already-migrated site.").option(
|
|
3377
|
+
"--fleet <inventory>",
|
|
3378
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3379
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
3152
3380
|
async (site, opts) => runOrExit(() => runSvelteCodemodsCommand(site, opts), opts)
|
|
3153
3381
|
);
|
|
3154
3382
|
cli.command(
|
|
3155
3383
|
"onboard [site]",
|
|
3156
3384
|
"Install @reddoorla/maintenance + audit deps on a site (run after convert-to-pnpm)."
|
|
3157
|
-
).option("--audits <names>", "Comma-separated audit subset: lighthouse,a11y (default: both)").option(
|
|
3385
|
+
).option("--audits <names>", "Comma-separated audit subset: lighthouse,a11y (default: both)").option(
|
|
3386
|
+
"--fleet <inventory>",
|
|
3387
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3388
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
3158
3389
|
async (site, opts) => runOrExit(() => runOnboardCommand(site, opts), opts)
|
|
3159
3390
|
);
|
|
3160
3391
|
cli.command("report [site]", "Draft or send maintenance/testing reports.").option("--due", "Scan all Websites and draft overdue reports.").option(
|