@reddoorla/maintenance 0.10.3 → 0.10.5
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 +34 -16
- package/dist/cli/bin.js +127 -27
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +77 -12
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +111 -20
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Canonical maintenance configs, audits, and recipes for the reddoor SvelteKit + Prismic fleet.
|
|
4
4
|
|
|
5
|
-
A single CLI (`reddoor-maint`)
|
|
5
|
+
A single CLI (`reddoor-maint`) covering the full operator workflow:
|
|
6
|
+
|
|
7
|
+
- **Audits** inspect a site (lighthouse, a11y, security, lint, deps).
|
|
8
|
+
- **Recipes** mutate one (branch-isolated, idempotent, never on a dirty tree) — start with [`init`](#init) for fresh sites.
|
|
9
|
+
- **Reports** ([per-site monthly/quarterly emails](#reports-per-site-maintenancetesting-emails)) draft + send client-facing maintenance + testing summaries, backed by Airtable as the source of truth and Resend for delivery.
|
|
10
|
+
- **Canonical configs** (eslint, prettier, lighthouserc, playwright-a11y, svelte) ship as importable presets every reddoor site shares.
|
|
11
|
+
|
|
12
|
+
Designed to run against either a single local site or a fleet declared in an inventory file.
|
|
6
13
|
|
|
7
14
|
```bash
|
|
8
15
|
pnpm add -D @reddoorla/maintenance
|
|
@@ -46,6 +53,7 @@ reddoor-maint list-recipes # recipe descriptions
|
|
|
46
53
|
|
|
47
54
|
reddoor-maint init [site] # full onboarding chain (preferred entrypoint)
|
|
48
55
|
reddoor-maint audit [site] # run audits
|
|
56
|
+
reddoor-maint report [site] # draft / send per-site maintenance + testing emails (see Reports)
|
|
49
57
|
reddoor-maint sync-configs [site]
|
|
50
58
|
reddoor-maint bump-deps [site]
|
|
51
59
|
reddoor-maint convert-to-pnpm [site]
|
|
@@ -76,7 +84,7 @@ reddoor-maint upgrade svelte-4-to-5 [site]
|
|
|
76
84
|
|
|
77
85
|
## Recipes
|
|
78
86
|
|
|
79
|
-
Each recipe is `(site, opts?) => Promise<RecipeResult>` and is exported from the package entry as a library function
|
|
87
|
+
Each recipe is `(site, opts?) => Promise<RecipeResult>` and is also exported from the package entry as a library function:
|
|
80
88
|
|
|
81
89
|
```ts
|
|
82
90
|
import {
|
|
@@ -99,13 +107,11 @@ Shared contract — every recipe:
|
|
|
99
107
|
- **Is idempotent** — re-running on the already-applied state returns `{ status: "noop", commits: [] }` without creating a branch.
|
|
100
108
|
- **Returns a `RecipeResult`** — `{ recipe, site, status: "applied" | "noop" | "failed", commits: string[], notes?: string }`.
|
|
101
109
|
|
|
102
|
-
### `
|
|
103
|
-
|
|
104
|
-
Writes the canonical config templates into the site (eslint, prettier, lighthouserc, playwright config, svelte config) and merges the canonical entries into `.gitignore`. Each config is its own commit. `--dry` reports the planned diff (including gitignore drift) without writing. `--only <name>[,<name>]` restricts to a subset.
|
|
110
|
+
### `init`
|
|
105
111
|
|
|
106
|
-
|
|
112
|
+
One-shot guided onboarding — the recommended entrypoint for a fresh site. Runs `convert-to-pnpm → onboard → sync-configs → svelte-codemods → a11y-fixtures-page → audit` in sequence. Each underlying recipe still creates its own branch — `init` is a thin orchestrator, not a branch-collapser. Stops the chain on the first `failed` recipe or uncaught error; `noop` results continue the chain. Exit code is 1 if any step failed _or_ the final audit pass reports a `fail`.
|
|
107
113
|
|
|
108
|
-
|
|
114
|
+
The remaining sections describe each step `init` invokes (in chain order), plus the standalone recipes that aren't part of onboarding.
|
|
109
115
|
|
|
110
116
|
### `convert-to-pnpm`
|
|
111
117
|
|
|
@@ -115,21 +121,25 @@ Removes the npm or yarn lockfile, pins `packageManager: pnpm@X.Y.Z` in `package.
|
|
|
115
121
|
|
|
116
122
|
Adds `@reddoorla/maintenance` + the audit deps (`@lhci/cli`, `@playwright/test`, `@axe-core/playwright`) to the site's `devDependencies` if they're missing. Audit dep versions come from `src/configs/baseline-versions.ts` so they can't drift from the rest of the package. The maintenance dep is pinned to a caret range against this package's own version at runtime — no manual syncing required at each minor bump. Refuses with `{ status: "failed", notes: "run convert-to-pnpm first" }` if the site has no `pnpm-lock.yaml`.
|
|
117
123
|
|
|
118
|
-
### `
|
|
124
|
+
### `sync-configs`
|
|
119
125
|
|
|
120
|
-
|
|
126
|
+
Writes the canonical config templates into the site (eslint, prettier, lighthouserc, playwright config, svelte config) and merges the canonical entries into `.gitignore`. Each config is its own commit. `--dry` reports the planned diff (including gitignore drift) without writing. `--only <name>[,<name>]` restricts to a subset.
|
|
121
127
|
|
|
122
|
-
### `
|
|
128
|
+
### `svelte-codemods`
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
Standalone codemod pass for sites already on Svelte 5. Applies the same gotcha codemods the full `svelte-4-to-5` recipe runs (`export let` → `$props()`, `on:event` → `onevent`, `$:` → `$derived`/`$effect`, `$$props.class` rewrite, `$$restProps` → destructured `...rest`, `$state` + `$effect` → `$derived`). Useful when post-upgrade Svelte 5 surfaces new strictness warnings and the fleet needs a clean re-application.
|
|
125
131
|
|
|
126
132
|
### `a11y-fixtures-page`
|
|
127
133
|
|
|
128
134
|
Writes a starter `src/routes/dev/a11y-fixtures/+page.svelte` if the route doesn't already exist. The `lighthouse` and `playwright-a11y` configs both target this URL — newly-onboarded sites need the route to exist for either audit to pass. The template is intentionally generic (semantic landmarks + headings + a relative link); operator edits to an existing page are never clobbered.
|
|
129
135
|
|
|
130
|
-
### `init`
|
|
136
|
+
### `bump-deps` _(standalone — not part of `init`)_
|
|
137
|
+
|
|
138
|
+
Pre-flights that the site is on pnpm (refuses with a clear remediation if `package-lock.json` or `yarn.lock` is present without `pnpm-lock.yaml`), runs `pnpm install` to ensure the lockfile is current, then `pnpm outdated --json` to decide whether anything needs upgrading. If yes: creates a branch and runs `pnpm up` scoped to the requested `--group` (patch / minor / major), then commits the result. `noop` if nothing's out of date.
|
|
131
139
|
|
|
132
|
-
|
|
140
|
+
### `upgrade svelte-4-to-5` _(standalone — not part of `init`)_
|
|
141
|
+
|
|
142
|
+
The full 7-step Svelte 4 → 5 migration: bump framework versions, migrate `svelte.config.js`, run the official `svelte-migrate` codemod, run `@tailwindcss/upgrade`, apply gotcha codemods over `src/**/*.svelte`, verify with `pnpm install` + `pnpm run check`, and write a `MIGRATION_SVELTE_5.md` summary. Each step is its own commit; the file leaves a record of what ran and what may need manual review.
|
|
133
143
|
|
|
134
144
|
---
|
|
135
145
|
|
|
@@ -190,22 +200,30 @@ Pass `--fleet <path>` to run a command against multiple sites declared in an inv
|
|
|
190
200
|
|
|
191
201
|
## Library usage
|
|
192
202
|
|
|
193
|
-
The package's main entry exports every recipe and
|
|
203
|
+
The package's main entry exports every recipe, audit, and report function so you can wire them into custom tooling (CI jobs, scheduled scripts, alternative CLIs):
|
|
194
204
|
|
|
195
205
|
```ts
|
|
196
206
|
import {
|
|
197
207
|
// recipes
|
|
198
|
-
|
|
199
|
-
bumpDeps,
|
|
208
|
+
init,
|
|
200
209
|
convertToPnpm,
|
|
201
210
|
onboard,
|
|
211
|
+
syncConfigs,
|
|
202
212
|
svelteCodemods,
|
|
213
|
+
a11yFixturesPage,
|
|
214
|
+
bumpDeps,
|
|
203
215
|
upgradeSvelte4to5,
|
|
204
216
|
|
|
205
217
|
// audits
|
|
206
218
|
runAudits,
|
|
207
219
|
ALL_AUDIT_NAMES,
|
|
208
220
|
|
|
221
|
+
// reports (per-site maintenance / testing emails)
|
|
222
|
+
draftReportForSite,
|
|
223
|
+
sendApprovedReports,
|
|
224
|
+
renderReportHtml,
|
|
225
|
+
findDueReports,
|
|
226
|
+
|
|
209
227
|
// recipe registry
|
|
210
228
|
ALL_RECIPE_NAMES,
|
|
211
229
|
isRecipeName,
|
package/dist/cli/bin.js
CHANGED
|
@@ -284,7 +284,7 @@ var init_reports = __esm({
|
|
|
284
284
|
|
|
285
285
|
// src/reports/maintenance-email/assets/index.ts
|
|
286
286
|
import { readFile as readFile13 } from "fs/promises";
|
|
287
|
-
import { existsSync as
|
|
287
|
+
import { existsSync as existsSync3 } from "fs";
|
|
288
288
|
import { dirname as dirname2, join as join21 } from "path";
|
|
289
289
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
290
290
|
function resolveAssetsDir() {
|
|
@@ -292,12 +292,12 @@ function resolveAssetsDir() {
|
|
|
292
292
|
let dir = dirname2(fileURLToPath2(import.meta.url));
|
|
293
293
|
while (true) {
|
|
294
294
|
const srcCandidate = join21(dir, "src", "reports", "maintenance-email", "assets", "check.png");
|
|
295
|
-
if (
|
|
295
|
+
if (existsSync3(srcCandidate)) {
|
|
296
296
|
cachedAssetsDir = dirname2(srcCandidate);
|
|
297
297
|
return cachedAssetsDir;
|
|
298
298
|
}
|
|
299
299
|
const distCandidate = join21(dir, "dist", "reports", "maintenance-email", "assets", "check.png");
|
|
300
|
-
if (
|
|
300
|
+
if (existsSync3(distCandidate)) {
|
|
301
301
|
cachedAssetsDir = dirname2(distCandidate);
|
|
302
302
|
return cachedAssetsDir;
|
|
303
303
|
}
|
|
@@ -760,7 +760,7 @@ var init_orchestrate = __esm({
|
|
|
760
760
|
});
|
|
761
761
|
|
|
762
762
|
// src/cli/bin.ts
|
|
763
|
-
import { dirname as
|
|
763
|
+
import { dirname as dirname6 } from "path";
|
|
764
764
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
765
765
|
import { cac } from "cac";
|
|
766
766
|
|
|
@@ -1166,6 +1166,32 @@ async function readSiteConfig(sitePath) {
|
|
|
1166
1166
|
return out;
|
|
1167
1167
|
}
|
|
1168
1168
|
|
|
1169
|
+
// src/util/free-port.ts
|
|
1170
|
+
import { createServer } from "net";
|
|
1171
|
+
async function findFreePort() {
|
|
1172
|
+
return new Promise((resolve10, reject) => {
|
|
1173
|
+
const server = createServer();
|
|
1174
|
+
server.unref();
|
|
1175
|
+
server.on("error", reject);
|
|
1176
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1177
|
+
const addr = server.address();
|
|
1178
|
+
if (typeof addr === "object" && addr) {
|
|
1179
|
+
const port = addr.port;
|
|
1180
|
+
server.close(() => resolve10(port));
|
|
1181
|
+
} else {
|
|
1182
|
+
server.close();
|
|
1183
|
+
reject(new Error("findFreePort: could not determine assigned port from socket"));
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
function withFreePort(url, port) {
|
|
1189
|
+
const u = new URL(url);
|
|
1190
|
+
u.hostname = "localhost";
|
|
1191
|
+
u.port = String(port);
|
|
1192
|
+
return u.toString();
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1169
1195
|
// src/audits/lighthouse.ts
|
|
1170
1196
|
async function readJsonMaybe(path) {
|
|
1171
1197
|
try {
|
|
@@ -1206,13 +1232,19 @@ async function lighthouseAudit(ctx) {
|
|
|
1206
1232
|
const site = ctx.site;
|
|
1207
1233
|
const label = siteLabel(site);
|
|
1208
1234
|
const siteCfg = await readSiteConfig(site.path);
|
|
1209
|
-
const
|
|
1235
|
+
const port = await findFreePort();
|
|
1236
|
+
const baseUrl = siteCfg.lighthouseUrl ?? lighthouseConfig.ci.collect.url[0];
|
|
1237
|
+
const resolvedConfig = {
|
|
1210
1238
|
...lighthouseConfig,
|
|
1211
1239
|
ci: {
|
|
1212
1240
|
...lighthouseConfig.ci,
|
|
1213
|
-
collect: {
|
|
1241
|
+
collect: {
|
|
1242
|
+
...lighthouseConfig.ci.collect,
|
|
1243
|
+
url: [withFreePort(baseUrl, port)],
|
|
1244
|
+
startServerCommand: `npm run vite:dev -- --port ${port} --strictPort`
|
|
1245
|
+
}
|
|
1214
1246
|
}
|
|
1215
|
-
}
|
|
1247
|
+
};
|
|
1216
1248
|
const configDir = await mkdtemp(join4(tmpdir(), "reddoor-lhci-"));
|
|
1217
1249
|
const configPath = join4(configDir, "lighthouserc.json");
|
|
1218
1250
|
await writeFile(configPath, JSON.stringify(resolvedConfig), "utf-8");
|
|
@@ -1323,6 +1355,32 @@ async function readJsonMaybe2(path) {
|
|
|
1323
1355
|
return null;
|
|
1324
1356
|
}
|
|
1325
1357
|
}
|
|
1358
|
+
function buildPlaywrightConfig(port) {
|
|
1359
|
+
return `import { defineConfig } from "@playwright/test";
|
|
1360
|
+
|
|
1361
|
+
export default defineConfig({
|
|
1362
|
+
testDir: ".",
|
|
1363
|
+
testMatch: /.*\\.spec\\.ts$/,
|
|
1364
|
+
fullyParallel: true,
|
|
1365
|
+
forbidOnly: !!process.env.CI,
|
|
1366
|
+
retries: process.env.CI ? 2 : 0,
|
|
1367
|
+
reporter: process.env.CI ? "github" : "list",
|
|
1368
|
+
use: {
|
|
1369
|
+
baseURL: "http://localhost:${port}",
|
|
1370
|
+
trace: "on-first-retry",
|
|
1371
|
+
},
|
|
1372
|
+
webServer: {
|
|
1373
|
+
// --strictPort: refuse to bump to a different port if ours is taken,
|
|
1374
|
+
// so the audit fails loudly instead of probing a zombie.
|
|
1375
|
+
// reuseExistingServer:false: never reuse \u2014 we control the lifecycle.
|
|
1376
|
+
command: "npm run vite:dev -- --port ${port} --strictPort",
|
|
1377
|
+
url: "http://localhost:${port}/dev/a11y-fixtures",
|
|
1378
|
+
reuseExistingServer: false,
|
|
1379
|
+
timeout: 120_000,
|
|
1380
|
+
},
|
|
1381
|
+
});
|
|
1382
|
+
`;
|
|
1383
|
+
}
|
|
1326
1384
|
function buildSpec() {
|
|
1327
1385
|
return `import { test, expect } from "@playwright/test";
|
|
1328
1386
|
import AxeBuilder from "@axe-core/playwright";
|
|
@@ -1376,19 +1434,26 @@ async function a11yAudit(ctx) {
|
|
|
1376
1434
|
const specDir = await mkdtemp2(join5(tmpdir2(), "reddoor-a11y-spec-"));
|
|
1377
1435
|
const specPath = join5(specDir, "a11y.spec.ts");
|
|
1378
1436
|
await writeFile2(specPath, buildSpec(), "utf-8");
|
|
1437
|
+
const port = await findFreePort();
|
|
1438
|
+
const configPath = join5(specDir, "playwright.config.ts");
|
|
1439
|
+
await writeFile2(configPath, buildPlaywrightConfig(port), "utf-8");
|
|
1379
1440
|
const resultsPath = join5(site.path, RESULTS_REL);
|
|
1380
1441
|
await rm2(join5(site.path, ".reddoor-a11y"), { recursive: true, force: true });
|
|
1381
1442
|
let raw;
|
|
1382
1443
|
try {
|
|
1383
|
-
raw = await spawn2(
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1444
|
+
raw = await spawn2(
|
|
1445
|
+
"npx",
|
|
1446
|
+
["--yes", "playwright", "test", `--config=${configPath}`, "--reporter=line", specPath],
|
|
1447
|
+
{
|
|
1448
|
+
cwd: site.path,
|
|
1449
|
+
env: { ...process.env, REDDOOR_A11Y_OUTPUT: resultsPath },
|
|
1450
|
+
// playwright on a cold tree downloads Chrome, boots the site's dev
|
|
1451
|
+
// server, and runs axe over every configured route. The shared 30 s
|
|
1452
|
+
// default in runAudits is fine for deps/lint/security but starves
|
|
1453
|
+
// playwright (mirrors the lighthouse fix shipped earlier).
|
|
1454
|
+
timeoutMs: 5 * 6e4
|
|
1455
|
+
}
|
|
1456
|
+
);
|
|
1392
1457
|
} catch (err) {
|
|
1393
1458
|
await rm2(specDir, { recursive: true, force: true });
|
|
1394
1459
|
const e = err;
|
|
@@ -2633,6 +2698,20 @@ function findMatchingClose(source, openIdx) {
|
|
|
2633
2698
|
i = closeStr + 1;
|
|
2634
2699
|
continue;
|
|
2635
2700
|
}
|
|
2701
|
+
if (ch === "/") {
|
|
2702
|
+
const next = source[i + 1];
|
|
2703
|
+
if (next === "/") {
|
|
2704
|
+
const eol = source.indexOf("\n", i + 2);
|
|
2705
|
+
i = eol === -1 ? source.length : eol;
|
|
2706
|
+
continue;
|
|
2707
|
+
}
|
|
2708
|
+
if (next === "*") {
|
|
2709
|
+
const end = source.indexOf("*/", i + 2);
|
|
2710
|
+
if (end === -1) return -1;
|
|
2711
|
+
i = end + 2;
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2636
2715
|
if (ch === "{") depth++;
|
|
2637
2716
|
else if (ch === "}") {
|
|
2638
2717
|
depth--;
|
|
@@ -2965,15 +3044,25 @@ import { stat as stat4 } from "fs/promises";
|
|
|
2965
3044
|
import { join as join19 } from "path";
|
|
2966
3045
|
|
|
2967
3046
|
// src/util/self-version.ts
|
|
2968
|
-
import { readFileSync } from "fs";
|
|
3047
|
+
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
2969
3048
|
import { fileURLToPath } from "url";
|
|
2970
3049
|
import { dirname, join as join18 } from "path";
|
|
2971
3050
|
function selfPackageVersion(callerImportMetaUrl) {
|
|
2972
3051
|
try {
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
3052
|
+
let dir = dirname(fileURLToPath(callerImportMetaUrl));
|
|
3053
|
+
while (true) {
|
|
3054
|
+
const candidate = join18(dir, "package.json");
|
|
3055
|
+
if (existsSync2(candidate)) {
|
|
3056
|
+
const raw = readFileSync(candidate, "utf-8");
|
|
3057
|
+
const pkg = JSON.parse(raw);
|
|
3058
|
+
if (pkg.name === "@reddoorla/maintenance") {
|
|
3059
|
+
return pkg.version ?? "0.0.0";
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
const parent = dirname(dir);
|
|
3063
|
+
if (parent === dir) return "0.0.0";
|
|
3064
|
+
dir = parent;
|
|
3065
|
+
}
|
|
2977
3066
|
} catch {
|
|
2978
3067
|
return "0.0.0";
|
|
2979
3068
|
}
|
|
@@ -3515,20 +3604,31 @@ async function runInitCommand(site, opts) {
|
|
|
3515
3604
|
}
|
|
3516
3605
|
|
|
3517
3606
|
// src/cli/version.ts
|
|
3518
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
3519
|
-
import { join as join23 } from "path";
|
|
3607
|
+
import { readFileSync as readFileSync2, existsSync as existsSync4 } from "fs";
|
|
3608
|
+
import { dirname as dirname5, join as join23 } from "path";
|
|
3520
3609
|
function resolvePackageVersion(fromDir) {
|
|
3521
3610
|
try {
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3611
|
+
let dir = fromDir;
|
|
3612
|
+
while (true) {
|
|
3613
|
+
const candidate = join23(dir, "package.json");
|
|
3614
|
+
if (existsSync4(candidate)) {
|
|
3615
|
+
const raw = readFileSync2(candidate, "utf-8");
|
|
3616
|
+
const pkg = JSON.parse(raw);
|
|
3617
|
+
if (pkg.name === "@reddoorla/maintenance") {
|
|
3618
|
+
return pkg.version ?? "unknown";
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
const parent = dirname5(dir);
|
|
3622
|
+
if (parent === dir) return "unknown";
|
|
3623
|
+
dir = parent;
|
|
3624
|
+
}
|
|
3525
3625
|
} catch {
|
|
3526
3626
|
return "unknown";
|
|
3527
3627
|
}
|
|
3528
3628
|
}
|
|
3529
3629
|
|
|
3530
3630
|
// src/cli/bin.ts
|
|
3531
|
-
var here =
|
|
3631
|
+
var here = dirname6(fileURLToPath3(import.meta.url));
|
|
3532
3632
|
var version = resolvePackageVersion(here);
|
|
3533
3633
|
var AUDIT_DESCRIPTIONS = {
|
|
3534
3634
|
deps: "Diff site package.json against the bundled baseline version map.",
|