@reddoorla/maintenance 0.8.0 → 0.10.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/index.js CHANGED
@@ -335,9 +335,9 @@ async function securityAudit(ctx) {
335
335
  }
336
336
 
337
337
  // src/audits/lighthouse.ts
338
- import { readFile as readFile3, writeFile, mkdtemp, rm } from "fs/promises";
338
+ import { readFile as readFile4, writeFile, mkdtemp, rm } from "fs/promises";
339
339
  import { tmpdir } from "os";
340
- import { join as join3 } from "path";
340
+ import { join as join4 } from "path";
341
341
 
342
342
  // src/configs/lighthouse.ts
343
343
  var lighthouseConfig = {
@@ -370,10 +370,37 @@ var lighthouseConfig = {
370
370
  }
371
371
  };
372
372
 
373
+ // src/audits/util/site-config.ts
374
+ import { readFile as readFile3 } from "fs/promises";
375
+ import { join as join3 } from "path";
376
+ async function readSiteConfig(sitePath) {
377
+ let raw;
378
+ try {
379
+ raw = await readFile3(join3(sitePath, "package.json"), "utf-8");
380
+ } catch {
381
+ return {};
382
+ }
383
+ let pkg;
384
+ try {
385
+ pkg = JSON.parse(raw);
386
+ } catch {
387
+ return {};
388
+ }
389
+ if (!pkg || typeof pkg !== "object") return {};
390
+ const cfg = pkg.reddoor;
391
+ if (!cfg || typeof cfg !== "object") return {};
392
+ const out = {};
393
+ const url = cfg.lighthouseUrl;
394
+ if (typeof url === "string" && url.length > 0) {
395
+ out.lighthouseUrl = url;
396
+ }
397
+ return out;
398
+ }
399
+
373
400
  // src/audits/lighthouse.ts
374
401
  async function readJsonMaybe(path) {
375
402
  try {
376
- const raw = await readFile3(path, "utf-8");
403
+ const raw = await readFile4(path, "utf-8");
377
404
  return JSON.parse(raw);
378
405
  } catch {
379
406
  return null;
@@ -409,15 +436,28 @@ async function lighthouseAudit(ctx) {
409
436
  const spawn2 = ctx.spawn ?? defaultSpawn;
410
437
  const site = ctx.site;
411
438
  const label = siteLabel(site);
412
- const configDir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
413
- const configPath = join3(configDir, "lighthouserc.json");
414
- await writeFile(configPath, JSON.stringify(lighthouseConfig), "utf-8");
415
- const resultsDir = join3(site.path, ".lighthouseci");
439
+ const siteCfg = await readSiteConfig(site.path);
440
+ const resolvedConfig = siteCfg.lighthouseUrl ? {
441
+ ...lighthouseConfig,
442
+ ci: {
443
+ ...lighthouseConfig.ci,
444
+ collect: { ...lighthouseConfig.ci.collect, url: [siteCfg.lighthouseUrl] }
445
+ }
446
+ } : lighthouseConfig;
447
+ const configDir = await mkdtemp(join4(tmpdir(), "reddoor-lhci-"));
448
+ const configPath = join4(configDir, "lighthouserc.json");
449
+ await writeFile(configPath, JSON.stringify(resolvedConfig), "utf-8");
450
+ const resultsDir = join4(site.path, ".lighthouseci");
416
451
  await rm(resultsDir, { recursive: true, force: true });
417
452
  let raw;
418
453
  try {
419
454
  raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
420
- cwd: site.path
455
+ cwd: site.path,
456
+ // lhci autorun boots the site's dev server, downloads Chrome on first
457
+ // use, and runs the audit — easily 2–3 min on a cold tree. The shared
458
+ // 30 s default in runAudits is fine for deps/lint/security but starves
459
+ // lhci.
460
+ timeoutMs: 5 * 6e4
421
461
  });
422
462
  } catch (err) {
423
463
  await rm(configDir, { recursive: true, force: true });
@@ -433,7 +473,7 @@ async function lighthouseAudit(ctx) {
433
473
  throw err;
434
474
  }
435
475
  await rm(configDir, { recursive: true, force: true });
436
- const manifest = await readJsonMaybe(join3(resultsDir, "manifest.json"));
476
+ const manifest = await readJsonMaybe(join4(resultsDir, "manifest.json"));
437
477
  if (!manifest || manifest.length === 0) {
438
478
  return {
439
479
  audit: "lighthouse",
@@ -442,7 +482,7 @@ async function lighthouseAudit(ctx) {
442
482
  summary: `lighthouse: no manifest written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
443
483
  };
444
484
  }
445
- const assertionResults = await readJsonMaybe(join3(resultsDir, "assertion-results.json")) ?? [];
485
+ const assertionResults = await readJsonMaybe(join4(resultsDir, "assertion-results.json")) ?? [];
446
486
  const failed = assertionResults.filter((a) => !a.passed);
447
487
  const assertions = failed.map((a) => ({
448
488
  category: categoryFromAssertion(a),
@@ -468,9 +508,9 @@ async function lighthouseAudit(ctx) {
468
508
  }
469
509
 
470
510
  // src/audits/a11y.ts
471
- import { readFile as readFile4, writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
511
+ import { readFile as readFile5, writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
472
512
  import { tmpdir as tmpdir2 } from "os";
473
- import { join as join4 } from "path";
513
+ import { join as join5 } from "path";
474
514
 
475
515
  // src/configs/playwright-a11y.ts
476
516
  import { defineConfig, devices } from "@playwright/test";
@@ -508,7 +548,7 @@ var playwrightA11yConfig = defineConfig({
508
548
  var RESULTS_REL = ".reddoor-a11y/results.json";
509
549
  async function readJsonMaybe2(path) {
510
550
  try {
511
- const raw = await readFile4(path, "utf-8");
551
+ const raw = await readFile5(path, "utf-8");
512
552
  return JSON.parse(raw);
513
553
  } catch {
514
554
  return null;
@@ -564,11 +604,11 @@ async function a11yAudit(ctx) {
564
604
  const spawn2 = ctx.spawn ?? defaultSpawn;
565
605
  const site = ctx.site;
566
606
  const label = siteLabel(site);
567
- const specDir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-spec-"));
568
- const specPath = join4(specDir, "a11y.spec.ts");
607
+ const specDir = await mkdtemp2(join5(tmpdir2(), "reddoor-a11y-spec-"));
608
+ const specPath = join5(specDir, "a11y.spec.ts");
569
609
  await writeFile2(specPath, buildSpec(), "utf-8");
570
- const resultsPath = join4(site.path, RESULTS_REL);
571
- await rm2(join4(site.path, ".reddoor-a11y"), { recursive: true, force: true });
610
+ const resultsPath = join5(site.path, RESULTS_REL);
611
+ await rm2(join5(site.path, ".reddoor-a11y"), { recursive: true, force: true });
572
612
  let raw;
573
613
  try {
574
614
  raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=line", specPath], {
@@ -650,8 +690,8 @@ async function runAuditsAcross(sites, which) {
650
690
  }
651
691
 
652
692
  // src/recipes/sync-configs.ts
653
- import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
654
- import { join as join5 } from "path";
693
+ import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
694
+ import { join as join6 } from "path";
655
695
 
656
696
  // src/recipes/sync-configs/templates.ts
657
697
  var eslint = {
@@ -904,7 +944,7 @@ async function withRecipe(body) {
904
944
  var GITIGNORE_CONFIG = "gitignore";
905
945
  async function readMaybe(path) {
906
946
  try {
907
- return await readFile5(path, "utf-8");
947
+ return await readFile6(path, "utf-8");
908
948
  } catch {
909
949
  return null;
910
950
  }
@@ -912,13 +952,13 @@ async function readMaybe(path) {
912
952
  async function planTemplateDiffs(cwd, templates) {
913
953
  const diffs = [];
914
954
  for (const t of templates) {
915
- const existing = await readMaybe(join5(cwd, t.path));
955
+ const existing = await readMaybe(join6(cwd, t.path));
916
956
  if (existing !== t.contents) diffs.push(t);
917
957
  }
918
958
  return diffs;
919
959
  }
920
960
  async function planGitignore(cwd) {
921
- const existing = await readMaybe(join5(cwd, ".gitignore"));
961
+ const existing = await readMaybe(join6(cwd, ".gitignore"));
922
962
  const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);
923
963
  const tracked = await listTrackedFiles(cwd);
924
964
  const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);
@@ -926,7 +966,7 @@ async function planGitignore(cwd) {
926
966
  return { kind: "apply", content: merge.content, toUntrack, added: merge.added };
927
967
  }
928
968
  async function applyGitignore(cwd, plan) {
929
- await writeFile3(join5(cwd, ".gitignore"), plan.content, "utf-8");
969
+ await writeFile3(join6(cwd, ".gitignore"), plan.content, "utf-8");
930
970
  if (plan.toUntrack.length > 0) {
931
971
  await removeFromIndex(cwd, plan.toUntrack);
932
972
  }
@@ -949,7 +989,7 @@ async function syncConfigs(site, opts = {}) {
949
989
  },
950
990
  apply: async ({ templateDiffs, gitignorePlan }, { commit: commit2 }) => {
951
991
  for (const t of templateDiffs) {
952
- await writeFile3(join5(site.path, t.path), t.contents, "utf-8");
992
+ await writeFile3(join6(site.path, t.path), t.contents, "utf-8");
953
993
  await commit2(`chore: sync ${t.config} config from @reddoorla/maintenance`);
954
994
  }
955
995
  if (gitignorePlan.kind === "apply") {
@@ -963,7 +1003,7 @@ async function syncConfigs(site, opts = {}) {
963
1003
 
964
1004
  // src/recipes/bump-deps.ts
965
1005
  import { stat } from "fs/promises";
966
- import { join as join6 } from "path";
1006
+ import { join as join7 } from "path";
967
1007
  async function exists(path) {
968
1008
  try {
969
1009
  await stat(path);
@@ -992,10 +1032,10 @@ async function bumpDeps(site, opts = {}) {
992
1032
  // land on top of whatever else was in the tree.
993
1033
  checkTreeFirst: true,
994
1034
  plan: async () => {
995
- const hasPnpmLock = await exists(join6(site.path, "pnpm-lock.yaml"));
1035
+ const hasPnpmLock = await exists(join7(site.path, "pnpm-lock.yaml"));
996
1036
  if (!hasPnpmLock) {
997
- const hasNpmLock = await exists(join6(site.path, "package-lock.json"));
998
- const hasYarnLock = await exists(join6(site.path, "yarn.lock"));
1037
+ const hasNpmLock = await exists(join7(site.path, "package-lock.json"));
1038
+ const hasYarnLock = await exists(join7(site.path, "yarn.lock"));
999
1039
  if (hasNpmLock || hasYarnLock) {
1000
1040
  const competing = hasNpmLock ? "package-lock.json" : "yarn.lock";
1001
1041
  return {
@@ -1030,12 +1070,12 @@ async function bumpDeps(site, opts = {}) {
1030
1070
  }
1031
1071
 
1032
1072
  // src/recipes/svelte-5/index.ts
1033
- import { join as join12 } from "path";
1073
+ import { join as join13 } from "path";
1034
1074
 
1035
1075
  // src/util/pkg.ts
1036
- import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
1076
+ import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
1037
1077
  async function readPackageJson(path) {
1038
- const raw = await readFile6(path, "utf-8");
1078
+ const raw = await readFile7(path, "utf-8");
1039
1079
  return JSON.parse(raw);
1040
1080
  }
1041
1081
  function detectIndentFromContent(raw) {
@@ -1045,7 +1085,7 @@ function detectIndentFromContent(raw) {
1045
1085
  async function writePackageJson(path, pkg) {
1046
1086
  let indent = " ";
1047
1087
  try {
1048
- const existing = await readFile6(path, "utf-8");
1088
+ const existing = await readFile7(path, "utf-8");
1049
1089
  indent = detectIndentFromContent(existing);
1050
1090
  } catch {
1051
1091
  }
@@ -1079,7 +1119,7 @@ function bumpDep(pkg, name, version, opts = {}) {
1079
1119
  }
1080
1120
 
1081
1121
  // src/recipes/svelte-5/step-bump-versions.ts
1082
- import { join as join7 } from "path";
1122
+ import { join as join8 } from "path";
1083
1123
  var SVELTE_5_VERSIONS = {
1084
1124
  svelte: "^5.55.5",
1085
1125
  "@sveltejs/kit": "^2.59.0",
@@ -1092,7 +1132,7 @@ var SVELTE_5_VERSIONS = {
1092
1132
  "typescript-svelte-plugin": "^0.3.52"
1093
1133
  };
1094
1134
  async function bumpToSvelte5Versions(cwd) {
1095
- const pkgPath = join7(cwd, "package.json");
1135
+ const pkgPath = join8(cwd, "package.json");
1096
1136
  const pkg = await readPackageJson(pkgPath);
1097
1137
  let next = pkg;
1098
1138
  for (const [name, version] of Object.entries(SVELTE_5_VERSIONS)) {
@@ -1104,8 +1144,8 @@ async function bumpToSvelte5Versions(cwd) {
1104
1144
  }
1105
1145
 
1106
1146
  // src/recipes/svelte-5/step-svelte-config.ts
1107
- import { readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
1108
- import { join as join8 } from "path";
1147
+ import { readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
1148
+ import { join as join9 } from "path";
1109
1149
  var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
1110
1150
  var IMPORT_FROM_VITE_PLUGIN = new RegExp(
1111
1151
  String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
@@ -1146,10 +1186,10 @@ function dropPreprocessKey(source) {
1146
1186
  return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
1147
1187
  }
1148
1188
  async function migrateSvelteConfig(cwd) {
1149
- const path = join8(cwd, "svelte.config.js");
1189
+ const path = join9(cwd, "svelte.config.js");
1150
1190
  let src;
1151
1191
  try {
1152
- src = await readFile7(path, "utf-8");
1192
+ src = await readFile8(path, "utf-8");
1153
1193
  } catch {
1154
1194
  return false;
1155
1195
  }
@@ -1183,9 +1223,9 @@ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
1183
1223
  }
1184
1224
 
1185
1225
  // src/recipes/svelte-5/step-tailwind-upgrade.ts
1186
- import { join as join9 } from "path";
1226
+ import { join as join10 } from "path";
1187
1227
  async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
1188
- const pkg = await readPackageJson(join9(cwd, "package.json"));
1228
+ const pkg = await readPackageJson(join10(cwd, "package.json"));
1189
1229
  const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
1190
1230
  if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
1191
1231
  if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
@@ -1206,8 +1246,8 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
1206
1246
  }
1207
1247
 
1208
1248
  // src/recipes/svelte-5/step-gotchas.ts
1209
- import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
1210
- import { join as join10 } from "path";
1249
+ import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
1250
+ import { join as join11 } from "path";
1211
1251
  import { glob as glob2 } from "tinyglobby";
1212
1252
 
1213
1253
  // src/recipes/svelte-5/codemods/on-event-to-handler.ts
@@ -1539,8 +1579,8 @@ async function planGotchaCodemods(cwd) {
1539
1579
  const changes = [];
1540
1580
  const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
1541
1581
  for (const rel of relPaths) {
1542
- const path = join10(cwd, rel);
1543
- const before = await readFile8(path, "utf-8");
1582
+ const path = join11(cwd, rel);
1583
+ const before = await readFile9(path, "utf-8");
1544
1584
  const after = CODEMODS.reduce((s, fn) => fn(s), before);
1545
1585
  if (after !== before) changes.push({ rel, after });
1546
1586
  }
@@ -1549,7 +1589,7 @@ async function planGotchaCodemods(cwd) {
1549
1589
  async function applyGotchaCodemods(cwd) {
1550
1590
  const changes = await planGotchaCodemods(cwd);
1551
1591
  for (const c of changes) {
1552
- await writeFile6(join10(cwd, c.rel), c.after, "utf-8");
1592
+ await writeFile6(join11(cwd, c.rel), c.after, "utf-8");
1553
1593
  }
1554
1594
  return { filesChanged: changes.length };
1555
1595
  }
@@ -1573,7 +1613,7 @@ async function verifyMigration(cwd, spawn2 = defaultSpawn) {
1573
1613
 
1574
1614
  // src/recipes/svelte-5/step-summary.ts
1575
1615
  import { writeFile as writeFile7 } from "fs/promises";
1576
- import { join as join11 } from "path";
1616
+ import { join as join12 } from "path";
1577
1617
  async function writeMigrationSummary(input) {
1578
1618
  const lines = [
1579
1619
  `# Svelte 4 \u2192 5 migration summary`,
@@ -1590,7 +1630,7 @@ async function writeMigrationSummary(input) {
1590
1630
  `- Verify Playwright a11y tests still pass.`
1591
1631
  ];
1592
1632
  const content = lines.join("\n") + "\n";
1593
- const path = join11(input.cwd, "MIGRATION_SVELTE_5.md");
1633
+ const path = join12(input.cwd, "MIGRATION_SVELTE_5.md");
1594
1634
  await writeFile7(path, content, "utf-8");
1595
1635
  return path;
1596
1636
  }
@@ -1598,7 +1638,7 @@ async function writeMigrationSummary(input) {
1598
1638
  // src/recipes/svelte-5/index.ts
1599
1639
  async function alreadyOnSvelte5(cwd) {
1600
1640
  try {
1601
- const pkg = await readPackageJson(join12(cwd, "package.json"));
1641
+ const pkg = await readPackageJson(join13(cwd, "package.json"));
1602
1642
  const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
1603
1643
  return !!v && /^\^?5\./.test(v);
1604
1644
  } catch {
@@ -1653,7 +1693,7 @@ async function upgradeSvelte4to5(site, opts = {}) {
1653
1693
 
1654
1694
  // src/recipes/svelte-codemods.ts
1655
1695
  import { writeFile as writeFile8 } from "fs/promises";
1656
- import { join as join13 } from "path";
1696
+ import { join as join14 } from "path";
1657
1697
  async function svelteCodemods(site) {
1658
1698
  return withRecipe({
1659
1699
  name: "svelte-codemods",
@@ -1667,7 +1707,7 @@ async function svelteCodemods(site) {
1667
1707
  },
1668
1708
  apply: async (changes, { commit: commit2, cwd }) => {
1669
1709
  for (const c of changes) {
1670
- await writeFile8(join13(cwd, c.rel), c.after, "utf-8");
1710
+ await writeFile8(join14(cwd, c.rel), c.after, "utf-8");
1671
1711
  }
1672
1712
  await commit2(`refactor(svelte5): apply codemods (${changes.length} files)`);
1673
1713
  return { kind: "ok" };
@@ -1677,7 +1717,7 @@ async function svelteCodemods(site) {
1677
1717
 
1678
1718
  // src/recipes/convert-to-pnpm.ts
1679
1719
  import { rm as rm3, stat as stat2 } from "fs/promises";
1680
- import { join as join14 } from "path";
1720
+ import { join as join15 } from "path";
1681
1721
 
1682
1722
  // src/recipes/convert-to-pnpm/script-rewrites.ts
1683
1723
  function rewriteScriptForPnpm(script) {
@@ -1710,9 +1750,9 @@ async function exists2(path) {
1710
1750
  async function convertToPnpm(site, opts = {}) {
1711
1751
  const spawn2 = opts.spawn ?? defaultSpawn;
1712
1752
  const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;
1713
- const pnpmLockPath = join14(site.path, "pnpm-lock.yaml");
1714
- const npmLockPath = join14(site.path, "package-lock.json");
1715
- const yarnLockPath = join14(site.path, "yarn.lock");
1753
+ const pnpmLockPath = join15(site.path, "pnpm-lock.yaml");
1754
+ const npmLockPath = join15(site.path, "package-lock.json");
1755
+ const yarnLockPath = join15(site.path, "yarn.lock");
1716
1756
  return withRecipe({
1717
1757
  name: "convert-to-pnpm",
1718
1758
  site,
@@ -1735,7 +1775,7 @@ async function convertToPnpm(site, opts = {}) {
1735
1775
  if (hasYarnLock) await rm3(yarnLockPath, { force: true });
1736
1776
  const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
1737
1777
  await commit2(`chore(pnpm): remove ${sourceLock}`);
1738
- const pkgPath = join14(cwd, "package.json");
1778
+ const pkgPath = join15(cwd, "package.json");
1739
1779
  const pkg = await readPackageJson(pkgPath);
1740
1780
  const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
1741
1781
  if (pkg.scripts && typeof pkg.scripts === "object") {
@@ -1748,7 +1788,7 @@ async function convertToPnpm(site, opts = {}) {
1748
1788
  }
1749
1789
  await writePackageJson(pkgPath, next);
1750
1790
  await commit2("chore(pnpm): pin packageManager + rewrite npm scripts");
1751
- await rm3(join14(cwd, "node_modules"), { recursive: true, force: true });
1791
+ await rm3(join15(cwd, "node_modules"), { recursive: true, force: true });
1752
1792
  const installResult = await spawn2("pnpm", ["install"], { cwd, streaming: true });
1753
1793
  if (installResult.code !== 0) {
1754
1794
  return { kind: "failed", notes: `pnpm install failed (exit ${installResult.code})` };
@@ -1761,16 +1801,16 @@ async function convertToPnpm(site, opts = {}) {
1761
1801
 
1762
1802
  // src/recipes/onboard.ts
1763
1803
  import { stat as stat3 } from "fs/promises";
1764
- import { join as join16 } from "path";
1804
+ import { join as join17 } from "path";
1765
1805
 
1766
1806
  // src/util/self-version.ts
1767
1807
  import { readFileSync } from "fs";
1768
1808
  import { fileURLToPath } from "url";
1769
- import { dirname, join as join15 } from "path";
1809
+ import { dirname, join as join16 } from "path";
1770
1810
  function selfPackageVersion(callerImportMetaUrl) {
1771
1811
  try {
1772
1812
  const here2 = dirname(fileURLToPath(callerImportMetaUrl));
1773
- const raw = readFileSync(join15(here2, "..", "..", "package.json"), "utf-8");
1813
+ const raw = readFileSync(join16(here2, "..", "..", "package.json"), "utf-8");
1774
1814
  const pkg = JSON.parse(raw);
1775
1815
  return pkg.version ?? "0.0.0";
1776
1816
  } catch {
@@ -1820,13 +1860,13 @@ async function onboard(site, opts = {}) {
1820
1860
  name: "onboard",
1821
1861
  site,
1822
1862
  plan: async () => {
1823
- if (!await exists3(join16(site.path, "pnpm-lock.yaml"))) {
1863
+ if (!await exists3(join17(site.path, "pnpm-lock.yaml"))) {
1824
1864
  return {
1825
1865
  kind: "failed",
1826
1866
  notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
1827
1867
  };
1828
1868
  }
1829
- const pkgPath = join16(site.path, "package.json");
1869
+ const pkgPath = join17(site.path, "package.json");
1830
1870
  const pkg = await readPackageJson(pkgPath);
1831
1871
  const toAdd = [];
1832
1872
  if (!isDeclared(pkg, PACKAGE_NAME)) {
@@ -1846,7 +1886,7 @@ async function onboard(site, opts = {}) {
1846
1886
  return { kind: "apply", plan: { pkg, toAdd } };
1847
1887
  },
1848
1888
  apply: async ({ pkg, toAdd }, { commit: commit2, cwd }) => {
1849
- const pkgPath = join16(cwd, "package.json");
1889
+ const pkgPath = join17(cwd, "package.json");
1850
1890
  let next = pkg;
1851
1891
  for (const dep of toAdd) {
1852
1892
  next = bumpDep(next, dep.name, dep.version);
@@ -1868,6 +1908,116 @@ async function onboard(site, opts = {}) {
1868
1908
  });
1869
1909
  }
1870
1910
 
1911
+ // src/recipes/a11y-fixtures-page/index.ts
1912
+ import { access, mkdir, writeFile as writeFile9 } from "fs/promises";
1913
+ import { dirname as dirname2, join as join18 } from "path";
1914
+
1915
+ // src/recipes/a11y-fixtures-page/template.ts
1916
+ var A11Y_FIXTURES_PAGE_RELATIVE = "src/routes/dev/a11y-fixtures/+page.svelte";
1917
+ var A11Y_FIXTURES_PAGE_TEMPLATE = `<svelte:head>
1918
+ <title>a11y fixtures \u2014 Reddoor</title>
1919
+ <meta
1920
+ name="description"
1921
+ content="Reddoor accessibility fixtures \u2014 semantic landmarks, heading hierarchy, and a stable target for @lhci/cli and Playwright + axe-core coverage. Not linked from the public site."
1922
+ />
1923
+ </svelte:head>
1924
+
1925
+ <main>
1926
+ <header>
1927
+ <h1>Accessibility fixtures</h1>
1928
+ <p>
1929
+ This page exists so <code>@lhci/cli</code> and Playwright + axe-core have a
1930
+ stable target with predictable a11y characteristics. It is not linked from
1931
+ the public site.
1932
+ </p>
1933
+ </header>
1934
+
1935
+ <section aria-labelledby="landmarks-heading">
1936
+ <h2 id="landmarks-heading">Landmarks</h2>
1937
+ <p>
1938
+ A single <code>main</code> wraps the page; sections each declare
1939
+ <code>aria-labelledby</code> matched to their heading id so screen readers
1940
+ and axe both see a clean outline.
1941
+ </p>
1942
+ </section>
1943
+
1944
+ <section aria-labelledby="links-heading">
1945
+ <h2 id="links-heading">Links</h2>
1946
+ <p>
1947
+ <a href="/">Back to home</a> \u2014 relative link with descriptive visible text,
1948
+ so no <code>aria-label</code> override is needed.
1949
+ </p>
1950
+ </section>
1951
+ </main>
1952
+ `;
1953
+
1954
+ // src/recipes/a11y-fixtures-page/index.ts
1955
+ async function fileExists(path) {
1956
+ try {
1957
+ await access(path);
1958
+ return true;
1959
+ } catch {
1960
+ return false;
1961
+ }
1962
+ }
1963
+ async function a11yFixturesPage(site) {
1964
+ const target = join18(site.path, A11Y_FIXTURES_PAGE_RELATIVE);
1965
+ return withRecipe({
1966
+ name: "a11y-fixtures-page",
1967
+ site,
1968
+ plan: async () => {
1969
+ if (await fileExists(target)) {
1970
+ return { kind: "noop", notes: `${A11Y_FIXTURES_PAGE_RELATIVE} already exists` };
1971
+ }
1972
+ return { kind: "apply", plan: { target } };
1973
+ },
1974
+ apply: async (planned, { commit: commit2 }) => {
1975
+ await mkdir(dirname2(planned.target), { recursive: true });
1976
+ await writeFile9(planned.target, A11Y_FIXTURES_PAGE_TEMPLATE, "utf-8");
1977
+ await commit2("feat: add /dev/a11y-fixtures starter route");
1978
+ return { kind: "ok" };
1979
+ }
1980
+ });
1981
+ }
1982
+
1983
+ // src/recipes/init.ts
1984
+ function recipeStep(name, fn) {
1985
+ return {
1986
+ name,
1987
+ run: async (site) => ({ kind: "recipe", result: await fn(site) })
1988
+ };
1989
+ }
1990
+ var DEFAULT_INIT_STEPS = [
1991
+ recipeStep("convert-to-pnpm", convertToPnpm),
1992
+ recipeStep("onboard", onboard),
1993
+ recipeStep("sync-configs", syncConfigs),
1994
+ recipeStep("svelte-codemods", svelteCodemods),
1995
+ recipeStep("a11y-fixtures-page", a11yFixturesPage),
1996
+ {
1997
+ name: "audit",
1998
+ run: async (site) => ({ kind: "audit", results: await runAudits(site) })
1999
+ }
2000
+ ];
2001
+ async function init(site, opts = {}) {
2002
+ const steps = opts.steps ?? DEFAULT_INIT_STEPS;
2003
+ const out = [];
2004
+ for (const step of steps) {
2005
+ let result;
2006
+ try {
2007
+ result = await step.run(site);
2008
+ } catch (err) {
2009
+ const message = err instanceof Error ? err.message : String(err);
2010
+ out.push({ name: step.name, result: { kind: "error", message } });
2011
+ return { site: siteLabel(site), steps: out, complete: false };
2012
+ }
2013
+ out.push({ name: step.name, result });
2014
+ if (result.kind === "recipe" && result.result.status === "failed") {
2015
+ return { site: siteLabel(site), steps: out, complete: false };
2016
+ }
2017
+ }
2018
+ return { site: siteLabel(site), steps: out, complete: true };
2019
+ }
2020
+
1871
2021
  // src/recipes/index.ts
1872
2022
  var ALL_RECIPE_NAMES = [
1873
2023
  "sync-configs",
@@ -1875,7 +2025,9 @@ var ALL_RECIPE_NAMES = [
1875
2025
  "svelte-4-to-5",
1876
2026
  "svelte-codemods",
1877
2027
  "convert-to-pnpm",
1878
- "onboard"
2028
+ "onboard",
2029
+ "a11y-fixtures-page",
2030
+ "init"
1879
2031
  ];
1880
2032
  function isRecipeName(value) {
1881
2033
  return ALL_RECIPE_NAMES.includes(value);
@@ -1889,7 +2041,7 @@ function localPath(path, opts = {}) {
1889
2041
  }
1890
2042
 
1891
2043
  // src/inventory/json.ts
1892
- import { readFile as readFile9 } from "fs/promises";
2044
+ import { readFile as readFile10 } from "fs/promises";
1893
2045
  import { isAbsolute } from "path";
1894
2046
  function validate(raw) {
1895
2047
  if (!Array.isArray(raw)) {
@@ -1919,7 +2071,7 @@ function validate(raw) {
1919
2071
  }
1920
2072
  function fromJsonFile(path) {
1921
2073
  return async () => {
1922
- const raw = JSON.parse(await readFile9(path, "utf-8"));
2074
+ const raw = JSON.parse(await readFile10(path, "utf-8"));
1923
2075
  return validate(raw);
1924
2076
  };
1925
2077
  }
@@ -1987,23 +2139,23 @@ function fromAirtableBase(base, opts = {}) {
1987
2139
  }
1988
2140
 
1989
2141
  // src/reports/draft.ts
1990
- import { mkdir, writeFile as writeFile9 } from "fs/promises";
1991
- import { dirname as dirname3 } from "path";
2142
+ import { mkdir as mkdir2, writeFile as writeFile10 } from "fs/promises";
2143
+ import { dirname as dirname4 } from "path";
1992
2144
 
1993
2145
  // src/reports/render.ts
1994
2146
  import mjml2html from "mjml";
1995
2147
 
1996
2148
  // src/reports/maintenance-email/assets/index.ts
1997
- import { readFile as readFile10 } from "fs/promises";
1998
- import { dirname as dirname2, join as join17 } from "path";
2149
+ import { readFile as readFile11 } from "fs/promises";
2150
+ import { dirname as dirname3, join as join19 } from "path";
1999
2151
  import { fileURLToPath as fileURLToPath2 } from "url";
2000
- var here = dirname2(fileURLToPath2(import.meta.url));
2152
+ var here = dirname3(fileURLToPath2(import.meta.url));
2001
2153
  var CHECK_CID = "rd-check-png";
2002
2154
  var BLURRED_CID = "rd-blurred-tests-jpg";
2003
2155
  async function loadBundledImages() {
2004
2156
  const [check, blurred] = await Promise.all([
2005
- readFile10(join17(here, "check.png")),
2006
- readFile10(join17(here, "blurredTests.jpg"))
2157
+ readFile11(join19(here, "check.png")),
2158
+ readFile11(join19(here, "blurredTests.jpg"))
2007
2159
  ]);
2008
2160
  return {
2009
2161
  check: {
@@ -2367,8 +2519,8 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
2367
2519
  });
2368
2520
  if (options.previewOnly) {
2369
2521
  const path = options.previewPath ?? `reports/${slug}/draft.html`;
2370
- await mkdir(dirname3(path), { recursive: true });
2371
- await writeFile9(path, html, "utf-8");
2522
+ await mkdir2(dirname4(path), { recursive: true });
2523
+ await writeFile10(path, html, "utf-8");
2372
2524
  return { reportRow: null, htmlPath: path, html };
2373
2525
  }
2374
2526
  if (base === null) throw new Error("base required when previewOnly=false");
@@ -2634,7 +2786,9 @@ function findDueReports(websites, reports, today) {
2634
2786
  export {
2635
2787
  ALL_AUDIT_NAMES,
2636
2788
  ALL_RECIPE_NAMES,
2789
+ DEFAULT_INIT_STEPS,
2637
2790
  a11yAudit,
2791
+ a11yFixturesPage,
2638
2792
  bumpDeps,
2639
2793
  convertToPnpm,
2640
2794
  depsAudit,
@@ -2642,6 +2796,7 @@ export {
2642
2796
  findDueReports,
2643
2797
  fromAirtableBase,
2644
2798
  fromJsonFile,
2799
+ init,
2645
2800
  isRecipeName,
2646
2801
  lighthouseAudit,
2647
2802
  lintAudit,