@reddoorla/maintenance 0.55.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +3 -1
  2. package/dist/airtable-2BJX4ATJ.js +9 -0
  3. package/dist/airtable-2BJX4ATJ.js.map +1 -0
  4. package/dist/announce-JEVCDDKL.js +171 -0
  5. package/dist/announce-JEVCDDKL.js.map +1 -0
  6. package/dist/bump-deps-O3SOLQYY.js +57 -0
  7. package/dist/bump-deps-O3SOLQYY.js.map +1 -0
  8. package/dist/chunk-2X322ZES.js +99 -0
  9. package/dist/chunk-2X322ZES.js.map +1 -0
  10. package/dist/chunk-4NAOR44V.js +153 -0
  11. package/dist/chunk-4NAOR44V.js.map +1 -0
  12. package/dist/chunk-4PY4I5OR.js +1167 -0
  13. package/dist/chunk-4PY4I5OR.js.map +1 -0
  14. package/dist/chunk-5WL3ET24.js +203 -0
  15. package/dist/chunk-5WL3ET24.js.map +1 -0
  16. package/dist/chunk-6LIWDIXU.js +38 -0
  17. package/dist/chunk-6LIWDIXU.js.map +1 -0
  18. package/dist/chunk-6VAL7XJT.js +52 -0
  19. package/dist/chunk-6VAL7XJT.js.map +1 -0
  20. package/dist/chunk-BWLUNY7L.js +585 -0
  21. package/dist/chunk-BWLUNY7L.js.map +1 -0
  22. package/dist/chunk-CASCWQQX.js +161 -0
  23. package/dist/chunk-CASCWQQX.js.map +1 -0
  24. package/dist/chunk-CLVTVDOU.js +274 -0
  25. package/dist/chunk-CLVTVDOU.js.map +1 -0
  26. package/dist/chunk-D3DQFC32.js +79 -0
  27. package/dist/chunk-D3DQFC32.js.map +1 -0
  28. package/dist/chunk-DIVB6ZF7.js +217 -0
  29. package/dist/chunk-DIVB6ZF7.js.map +1 -0
  30. package/dist/chunk-EYODPUHV.js +37 -0
  31. package/dist/chunk-EYODPUHV.js.map +1 -0
  32. package/dist/chunk-FDIFTIXC.js +70 -0
  33. package/dist/chunk-FDIFTIXC.js.map +1 -0
  34. package/dist/chunk-FUD4I2ZW.js +35 -0
  35. package/dist/chunk-FUD4I2ZW.js.map +1 -0
  36. package/dist/chunk-HG5DTFNF.js +62 -0
  37. package/dist/chunk-HG5DTFNF.js.map +1 -0
  38. package/dist/chunk-HN6I2XWI.js +65 -0
  39. package/dist/chunk-HN6I2XWI.js.map +1 -0
  40. package/dist/chunk-HVOOCK6L.js +84 -0
  41. package/dist/chunk-HVOOCK6L.js.map +1 -0
  42. package/dist/chunk-IMIR7SC2.js +51 -0
  43. package/dist/chunk-IMIR7SC2.js.map +1 -0
  44. package/dist/chunk-J3OPWFZE.js +64 -0
  45. package/dist/chunk-J3OPWFZE.js.map +1 -0
  46. package/dist/chunk-JNXCDUYB.js +211 -0
  47. package/dist/chunk-JNXCDUYB.js.map +1 -0
  48. package/dist/chunk-JQG222CI.js +49 -0
  49. package/dist/chunk-JQG222CI.js.map +1 -0
  50. package/dist/chunk-LKGVSM2O.js +43 -0
  51. package/dist/chunk-LKGVSM2O.js.map +1 -0
  52. package/dist/chunk-LYCKN7ZR.js +41 -0
  53. package/dist/chunk-LYCKN7ZR.js.map +1 -0
  54. package/dist/chunk-O65I7G7G.js +246 -0
  55. package/dist/chunk-O65I7G7G.js.map +1 -0
  56. package/dist/chunk-OCS7SZUZ.js +243 -0
  57. package/dist/chunk-OCS7SZUZ.js.map +1 -0
  58. package/dist/chunk-OGGRFBL4.js +14 -0
  59. package/dist/chunk-OGGRFBL4.js.map +1 -0
  60. package/dist/chunk-OQPMZ6BM.js +368 -0
  61. package/dist/chunk-OQPMZ6BM.js.map +1 -0
  62. package/dist/chunk-OTGB6YQO.js +52 -0
  63. package/dist/chunk-OTGB6YQO.js.map +1 -0
  64. package/dist/chunk-PF6BNYGE.js +162 -0
  65. package/dist/chunk-PF6BNYGE.js.map +1 -0
  66. package/dist/chunk-QTHKOIFJ.js +13 -0
  67. package/dist/chunk-QTHKOIFJ.js.map +1 -0
  68. package/dist/chunk-U5SZ4FOP.js +26 -0
  69. package/dist/chunk-U5SZ4FOP.js.map +1 -0
  70. package/dist/chunk-VXSX2FHO.js +269 -0
  71. package/dist/chunk-VXSX2FHO.js.map +1 -0
  72. package/dist/chunk-W3G7XFBZ.js +444 -0
  73. package/dist/chunk-W3G7XFBZ.js.map +1 -0
  74. package/dist/chunk-WEKL2HYJ.js +31 -0
  75. package/dist/chunk-WEKL2HYJ.js.map +1 -0
  76. package/dist/chunk-WVRPOXKH.js +74 -0
  77. package/dist/chunk-WVRPOXKH.js.map +1 -0
  78. package/dist/chunk-XKOZUVK2.js +121 -0
  79. package/dist/chunk-XKOZUVK2.js.map +1 -0
  80. package/dist/chunk-XO2RUYVM.js +138 -0
  81. package/dist/chunk-XO2RUYVM.js.map +1 -0
  82. package/dist/chunk-XTK5VIQB.js +97 -0
  83. package/dist/chunk-XTK5VIQB.js.map +1 -0
  84. package/dist/chunk-XXTZBPUY.js +9 -0
  85. package/dist/chunk-XXTZBPUY.js.map +1 -0
  86. package/dist/chunk-ZYBOT5PM.js +153 -0
  87. package/dist/chunk-ZYBOT5PM.js.map +1 -0
  88. package/dist/cli/bin.js +75 -7785
  89. package/dist/cli/bin.js.map +1 -1
  90. package/dist/cli/commands/audit.js +37 -2336
  91. package/dist/cli/commands/audit.js.map +1 -1
  92. package/dist/client-EPVO3FEH.js +10 -0
  93. package/dist/client-EPVO3FEH.js.map +1 -0
  94. package/dist/client-JA6QLEDW.js +48 -0
  95. package/dist/client-JA6QLEDW.js.map +1 -0
  96. package/dist/configs/lighthouse.js +4 -31
  97. package/dist/configs/lighthouse.js.map +1 -1
  98. package/dist/configs/playwright-a11y.js +6 -33
  99. package/dist/configs/playwright-a11y.js.map +1 -1
  100. package/dist/convert-to-pnpm-UR64X3OX.js +51 -0
  101. package/dist/convert-to-pnpm-UR64X3OX.js.map +1 -0
  102. package/dist/db-AXKMRTVA.js +23 -0
  103. package/dist/db-AXKMRTVA.js.map +1 -0
  104. package/dist/digest-YTO73BEO.js +408 -0
  105. package/dist/digest-YTO73BEO.js.map +1 -0
  106. package/dist/forms/index.js +3 -8
  107. package/dist/forms/index.js.map +1 -1
  108. package/dist/github-signals-YD64R2AF.js +122 -0
  109. package/dist/github-signals-YD64R2AF.js.map +1 -0
  110. package/dist/index.js +102 -4757
  111. package/dist/index.js.map +1 -1
  112. package/dist/init-JITQXFVL.js +85 -0
  113. package/dist/init-JITQXFVL.js.map +1 -0
  114. package/dist/launch-HXHEL6ID.js +207 -0
  115. package/dist/launch-HXHEL6ID.js.map +1 -0
  116. package/dist/lighthouse-airtable-4FKYKFH6.js +12 -0
  117. package/dist/lighthouse-airtable-4FKYKFH6.js.map +1 -0
  118. package/dist/migrate-P2LS4WXI.js +7 -0
  119. package/dist/migrate-P2LS4WXI.js.map +1 -0
  120. package/dist/onboard-TCQOP2W4.js +69 -0
  121. package/dist/onboard-TCQOP2W4.js.map +1 -0
  122. package/dist/orchestrate-RMSBHLQK.js +19 -0
  123. package/dist/orchestrate-RMSBHLQK.js.map +1 -0
  124. package/dist/recipes/sync-configs.js +9 -505
  125. package/dist/recipes/sync-configs.js.map +1 -1
  126. package/dist/renovate-dispatch-ZGLXNPEK.js +97 -0
  127. package/dist/renovate-dispatch-ZGLXNPEK.js.map +1 -0
  128. package/dist/report-SD3CNBOQ.js +240 -0
  129. package/dist/report-SD3CNBOQ.js.map +1 -0
  130. package/dist/resend-G2BJMAQV.js +7 -0
  131. package/dist/resend-G2BJMAQV.js.map +1 -0
  132. package/dist/self-updating-CIUMOMYI.js +61 -0
  133. package/dist/self-updating-CIUMOMYI.js.map +1 -0
  134. package/dist/svelte-codemods-BYRJAU7I.js +51 -0
  135. package/dist/svelte-codemods-BYRJAU7I.js.map +1 -0
  136. package/dist/sync-configs-JPRLAW5J.js +109 -0
  137. package/dist/sync-configs-JPRLAW5J.js.map +1 -0
  138. package/dist/upgrade-ERA45EVR.js +64 -0
  139. package/dist/upgrade-ERA45EVR.js.map +1 -0
  140. package/dist/util/git.js +19 -76
  141. package/dist/util/git.js.map +1 -1
  142. package/dist/util/pkg.js +5 -45
  143. package/dist/util/pkg.js.map +1 -1
  144. package/dist/websites-X5DLC2PC.js +43 -0
  145. package/dist/websites-X5DLC2PC.js.map +1 -0
  146. package/dist/write-audits-to-airtable-CE4POBAF.js +13 -0
  147. package/dist/write-audits-to-airtable-CE4POBAF.js.map +1 -0
  148. package/package.json +13 -12
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/due.ts"],"sourcesContent":["import type { WebsiteRow, Frequency, Status } from \"./airtable/websites.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport type { ReportType } from \"./types.js\";\n\n/** Statuses where reports are appropriate. Drops \"deprecated\" and\n * \"probably not our problem\" — even if the operator left a freq set, we don't\n * want to surface those sites in --due output. Sites with status=null pass\n * through (existing data is partial; better to surface than silently skip). */\nconst ELIGIBLE_STATUSES: ReadonlySet<Status> = new Set<Status>([\n \"in development\",\n \"launch period\",\n \"maintenance\",\n \"hosting\",\n]);\n\nexport type DueItem = {\n site: WebsiteRow;\n reportType: ReportType;\n /** Inclusive: the day the next report became due. */\n dueDate: Date;\n /** ISO date of the last `Sent at` for this (site, type), or null if there's never been one. */\n lastSent: string | null;\n};\n\nconst MONTHS: Record<Exclude<Frequency, \"None\">, number> = {\n Monthly: 1,\n Quarterly: 3,\n Yearly: 12,\n};\n\n/**\n * Add `n` calendar months in UTC, clamped to the last day of the target month.\n * Jan 31 + 1 month = Feb 28 (not Mar 3, which is what naive setMonth produces).\n * All-UTC accessors mean the result is timezone-independent.\n */\nfunction addMonths(d: Date, n: number): Date {\n const out = new Date(d);\n const day = out.getUTCDate();\n out.setUTCDate(1);\n out.setUTCMonth(out.getUTCMonth() + n);\n const lastDayOfTargetMonth = new Date(\n Date.UTC(out.getUTCFullYear(), out.getUTCMonth() + 1, 0),\n ).getUTCDate();\n out.setUTCDate(Math.min(day, lastDayOfTargetMonth));\n return out;\n}\n\n/** Truncate to UTC midnight. Avoids local-TZ skew when comparing Airtable date-only fields. */\nfunction startOfDay(d: Date): Date {\n const out = new Date(d);\n out.setUTCHours(0, 0, 0, 0);\n return out;\n}\n\nfunction lastSentForType(reports: ReportRow[], siteId: string, type: ReportType): string | null {\n const candidates = reports\n .filter((r) => r.siteId === siteId && r.reportType === type && r.sentAt !== null)\n .map((r) => r.sentAt!)\n .sort();\n return candidates[candidates.length - 1] ?? null;\n}\n\n/**\n * Computes which (site, type) pairs are due as of `today`.\n *\n * Algorithm per (site, type):\n * 1. If freq === \"None\", skip.\n * 2. baseDate = max(last Sent at for this type, site's `maintenance/testing day` fallback).\n * 3. If no baseDate exists at all, the site is due now.\n * 4. dueDate = baseDate + frequency months.\n * 5. Due iff startOfDay(today) >= startOfDay(dueDate).\n */\nexport function findDueReports(\n websites: WebsiteRow[],\n reports: ReportRow[],\n today: Date,\n): DueItem[] {\n const out: DueItem[] = [];\n const todayStart = startOfDay(today);\n\n for (const site of websites) {\n // Skip explicitly-non-active statuses (deprecated, \"probably not our problem\").\n // Null status is treated as active for backwards compat with rows that pre-date\n // the Status convention.\n if (site.status !== null && !ELIGIBLE_STATUSES.has(site.status)) continue;\n\n for (const type of [\"Maintenance\", \"Testing\"] as const) {\n const rawFreq = type === \"Maintenance\" ? site.maintenanceFreq : site.testingFreq;\n // Normalize obvious whitespace so a trailing-space typo (\"Quarterly \") still\n // schedules. The LOUD warning below is the real safety net for genuine\n // casing/spelling mistakes (\"monthly\", \"Quaterly\").\n const freq = (typeof rawFreq === \"string\" ? rawFreq.trim() : rawFreq) as Frequency;\n // Intentional silent skip — \"None\" (and the empty/blank default) means \"no\n // schedule\", not a mistake.\n if (freq === \"None\" || freq === (\"\" as Frequency)) continue;\n // A non-empty, non-None value that doesn't match a known schedule used to\n // silently produce no due date — the site just vanished from the loop. Warn\n // LOUDLY so a casing/typo Airtable value is fixable instead of invisible.\n if (!(freq in MONTHS)) {\n console.warn(\n `⚠ ${site.name}: unrecognized ${type === \"Maintenance\" ? \"maintenance\" : \"testing\"} frequency '${rawFreq}' — not scheduling; fix the Airtable value`,\n );\n continue;\n }\n\n const lastSent = lastSentForType(reports, site.id, type);\n const fallback = type === \"Maintenance\" ? site.maintenanceDay : site.testingDay;\n const baseIso = lastSent ?? fallback;\n\n if (!baseIso) {\n out.push({ site, reportType: type, dueDate: todayStart, lastSent });\n continue;\n }\n\n const dueDate = addMonths(new Date(baseIso), MONTHS[freq]);\n if (todayStart.getTime() >= startOfDay(dueDate).getTime()) {\n out.push({ site, reportType: type, dueDate, lastSent });\n }\n }\n }\n\n return out;\n}\n\n/**\n * The UTC `YYYY-MM` of a `dueDate` from {@link findDueReports} — the per-recurrence\n * idempotency key for drafting. Monthly recurrences land in distinct months; quarterly\n * and yearly land in distinct due-months too, so this uniquely names one draft per cycle.\n * UTC accessors keep it timezone-independent, consistent with the rest of this module.\n */\nexport function reportPeriodKey(dueDate: Date): string {\n if (Number.isNaN(dueDate.getTime())) throw new TypeError(\"reportPeriodKey: invalid Date\");\n const year = dueDate.getUTCFullYear();\n const month = String(dueDate.getUTCMonth() + 1).padStart(2, \"0\");\n return `${year}-${month}`;\n}\n"],"mappings":";AAQA,IAAM,oBAAyC,oBAAI,IAAY;AAAA,EAC7D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAWD,IAAM,SAAqD;AAAA,EACzD,SAAS;AAAA,EACT,WAAW;AAAA,EACX,QAAQ;AACV;AAOA,SAAS,UAAU,GAAS,GAAiB;AAC3C,QAAM,MAAM,IAAI,KAAK,CAAC;AACtB,QAAM,MAAM,IAAI,WAAW;AAC3B,MAAI,WAAW,CAAC;AAChB,MAAI,YAAY,IAAI,YAAY,IAAI,CAAC;AACrC,QAAM,uBAAuB,IAAI;AAAA,IAC/B,KAAK,IAAI,IAAI,eAAe,GAAG,IAAI,YAAY,IAAI,GAAG,CAAC;AAAA,EACzD,EAAE,WAAW;AACb,MAAI,WAAW,KAAK,IAAI,KAAK,oBAAoB,CAAC;AAClD,SAAO;AACT;AAGA,SAAS,WAAW,GAAe;AACjC,QAAM,MAAM,IAAI,KAAK,CAAC;AACtB,MAAI,YAAY,GAAG,GAAG,GAAG,CAAC;AAC1B,SAAO;AACT;AAEA,SAAS,gBAAgB,SAAsB,QAAgB,MAAiC;AAC9F,QAAM,aAAa,QAChB,OAAO,CAAC,MAAM,EAAE,WAAW,UAAU,EAAE,eAAe,QAAQ,EAAE,WAAW,IAAI,EAC/E,IAAI,CAAC,MAAM,EAAE,MAAO,EACpB,KAAK;AACR,SAAO,WAAW,WAAW,SAAS,CAAC,KAAK;AAC9C;AAYO,SAAS,eACd,UACA,SACA,OACW;AACX,QAAM,MAAiB,CAAC;AACxB,QAAM,aAAa,WAAW,KAAK;AAEnC,aAAW,QAAQ,UAAU;AAI3B,QAAI,KAAK,WAAW,QAAQ,CAAC,kBAAkB,IAAI,KAAK,MAAM,EAAG;AAEjE,eAAW,QAAQ,CAAC,eAAe,SAAS,GAAY;AACtD,YAAM,UAAU,SAAS,gBAAgB,KAAK,kBAAkB,KAAK;AAIrE,YAAM,OAAQ,OAAO,YAAY,WAAW,QAAQ,KAAK,IAAI;AAG7D,UAAI,SAAS,UAAU,SAAU,GAAkB;AAInD,UAAI,EAAE,QAAQ,SAAS;AACrB,gBAAQ;AAAA,UACN,UAAK,KAAK,IAAI,kBAAkB,SAAS,gBAAgB,gBAAgB,SAAS,eAAe,OAAO;AAAA,QAC1G;AACA;AAAA,MACF;AAEA,YAAM,WAAW,gBAAgB,SAAS,KAAK,IAAI,IAAI;AACvD,YAAM,WAAW,SAAS,gBAAgB,KAAK,iBAAiB,KAAK;AACrE,YAAM,UAAU,YAAY;AAE5B,UAAI,CAAC,SAAS;AACZ,YAAI,KAAK,EAAE,MAAM,YAAY,MAAM,SAAS,YAAY,SAAS,CAAC;AAClE;AAAA,MACF;AAEA,YAAM,UAAU,UAAU,IAAI,KAAK,OAAO,GAAG,OAAO,IAAI,CAAC;AACzD,UAAI,WAAW,QAAQ,KAAK,WAAW,OAAO,EAAE,QAAQ,GAAG;AACzD,YAAI,KAAK,EAAE,MAAM,YAAY,MAAM,SAAS,SAAS,CAAC;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAQO,SAAS,gBAAgB,SAAuB;AACrD,MAAI,OAAO,MAAM,QAAQ,QAAQ,CAAC,EAAG,OAAM,IAAI,UAAU,+BAA+B;AACxF,QAAM,OAAO,QAAQ,eAAe;AACpC,QAAM,QAAQ,OAAO,QAAQ,YAAY,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AAC/D,SAAO,GAAG,IAAI,IAAI,KAAK;AACzB;","names":[]}
@@ -0,0 +1,121 @@
1
+ import {
2
+ siteLabel
3
+ } from "./chunk-XXTZBPUY.js";
4
+ import {
5
+ branchName,
6
+ checkoutBranch,
7
+ commit,
8
+ createBranch,
9
+ currentBranch,
10
+ deleteBranch,
11
+ forceCheckoutBranch,
12
+ isWorkingTreeClean
13
+ } from "./chunk-XTK5VIQB.js";
14
+
15
+ // src/recipes/_with-recipe.ts
16
+ async function withRecipe(body) {
17
+ const label = siteLabel(body.site);
18
+ if (body.checkTreeFirst && !await isWorkingTreeClean(body.site.path)) {
19
+ throw new Error(`refusing to run: working tree is not clean at ${body.site.path}`);
20
+ }
21
+ const planned = await body.plan();
22
+ if (planned.kind === "noop") {
23
+ return {
24
+ recipe: body.name,
25
+ site: label,
26
+ status: "noop",
27
+ commits: [],
28
+ ...planned.notes ? { notes: planned.notes } : {}
29
+ };
30
+ }
31
+ if (planned.kind === "failed") {
32
+ return {
33
+ recipe: body.name,
34
+ site: label,
35
+ status: "failed",
36
+ commits: [],
37
+ notes: planned.notes
38
+ };
39
+ }
40
+ if (!body.checkTreeFirst && !await isWorkingTreeClean(body.site.path)) {
41
+ throw new Error(`refusing to run: working tree is not clean at ${body.site.path}`);
42
+ }
43
+ let original = null;
44
+ try {
45
+ original = await currentBranch(body.site.path);
46
+ } catch {
47
+ original = null;
48
+ }
49
+ const branch = branchName(body.name);
50
+ await createBranch(body.site.path, branch);
51
+ const restoreOriginal = async () => {
52
+ if (original === null || original === branch) return;
53
+ try {
54
+ await checkoutBranch(body.site.path, original);
55
+ } catch (err) {
56
+ console.warn(
57
+ `warning: could not restore branch ${original} after ${body.name}: ${err instanceof Error ? err.message : String(err)}`
58
+ );
59
+ }
60
+ };
61
+ const restoreAfterFailure = async () => {
62
+ if (original === null || original === branch) return;
63
+ try {
64
+ await forceCheckoutBranch(body.site.path, original);
65
+ } catch (err) {
66
+ console.warn(
67
+ `warning: could not force-restore branch ${original} after failed ${body.name}: ${err instanceof Error ? err.message : String(err)}`
68
+ );
69
+ return;
70
+ }
71
+ try {
72
+ await deleteBranch(body.site.path, branch);
73
+ } catch (err) {
74
+ console.warn(
75
+ `warning: could not delete recipe branch ${branch} after failed ${body.name}: ${err instanceof Error ? err.message : String(err)}`
76
+ );
77
+ }
78
+ };
79
+ const shas = [];
80
+ let result;
81
+ try {
82
+ result = await body.apply(planned.plan, {
83
+ cwd: body.site.path,
84
+ branch,
85
+ commit: async (msg) => {
86
+ const sha = await commit(body.site.path, msg);
87
+ if (sha) shas.push(sha);
88
+ return sha;
89
+ }
90
+ });
91
+ } catch (err) {
92
+ await restoreAfterFailure();
93
+ throw err;
94
+ }
95
+ if (result.kind === "failed") {
96
+ await restoreAfterFailure();
97
+ return {
98
+ recipe: body.name,
99
+ site: label,
100
+ status: "failed",
101
+ commits: shas,
102
+ notes: result.notes
103
+ };
104
+ }
105
+ if (shas.length === 0) {
106
+ await restoreOriginal();
107
+ }
108
+ const notes = result.notes ? `${result.notes}; branch: ${branch}` : `branch: ${branch}`;
109
+ return {
110
+ recipe: body.name,
111
+ site: label,
112
+ status: shas.length > 0 ? "applied" : "noop",
113
+ commits: shas,
114
+ notes
115
+ };
116
+ }
117
+
118
+ export {
119
+ withRecipe
120
+ };
121
+ //# sourceMappingURL=chunk-XKOZUVK2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/recipes/_with-recipe.ts"],"sourcesContent":["import type { RecipeName, RecipeResult, Site } from \"../types.js\";\nimport {\n branchName,\n checkoutBranch,\n commit as gitCommit,\n createBranch,\n currentBranch,\n deleteBranch,\n forceCheckoutBranch,\n isWorkingTreeClean,\n} from \"../util/git.js\";\nimport { siteLabel } from \"../util/site.js\";\n\n/** Outcome of the read-only planning phase. `noop` and `failed` short-circuit\n * without creating a branch; `apply` carries the recipe-specific plan data\n * forward to the apply phase. */\nexport type RecipePlan<P> =\n | { kind: \"noop\"; notes?: string }\n | { kind: \"failed\"; notes: string }\n | { kind: \"apply\"; plan: P };\n\nexport type RecipeApplyCtx = {\n /** Stage all current changes and commit. Returns the SHA, or null if\n * nothing was staged. The wrapper accumulates SHAs into the final\n * RecipeResult. */\n commit: (message: string) => Promise<string | null>;\n /** Branch name that was created for this run. */\n branch: string;\n /** Site path — same as `site.path`. */\n cwd: string;\n};\n\nexport type RecipeApplyResult = { kind: \"ok\"; notes?: string } | { kind: \"failed\"; notes: string };\n\nexport type RecipeBody<P> = {\n name: RecipeName;\n site: Site;\n /** Inspect the site and decide: noop, failed, or proceed (with plan data\n * passed to apply). Runs before the working-tree clean check unless\n * `checkTreeFirst: true` is set, so most recipes can noop on a dirty\n * tree without throwing. */\n plan: () => Promise<RecipePlan<P>>;\n /** Make the actual changes. Use `ctx.commit(msg)` for each logical step;\n * the wrapper collects SHAs into `RecipeResult.commits`. Return\n * `{ kind: \"failed\", notes }` to abort partway and surface the failure. */\n apply: (plan: P, ctx: RecipeApplyCtx) => Promise<RecipeApplyResult>;\n /** Check working tree clean BEFORE `plan()` runs. Use only when plan\n * itself mutates the tree (e.g. `bump-deps` runs `pnpm install` in plan\n * for an accurate outdated probe). Default false — clean check happens\n * after plan only if plan returns proceed, allowing noop-on-dirty for\n * read-only plans (a tree with stray edits + no recipe work to do\n * should not throw). */\n checkTreeFirst?: boolean;\n};\n\n/** Wrap a recipe's plan/apply phases. Centralises the siteLabel /\n * clean-tree check / branch creation / commit accumulation / RecipeResult\n * construction boilerplate that every recipe used to re-implement. */\nexport async function withRecipe<P>(body: RecipeBody<P>): Promise<RecipeResult> {\n const label = siteLabel(body.site);\n\n if (body.checkTreeFirst && !(await isWorkingTreeClean(body.site.path))) {\n throw new Error(`refusing to run: working tree is not clean at ${body.site.path}`);\n }\n\n const planned = await body.plan();\n\n if (planned.kind === \"noop\") {\n return {\n recipe: body.name,\n site: label,\n status: \"noop\",\n commits: [],\n ...(planned.notes ? { notes: planned.notes } : {}),\n };\n }\n if (planned.kind === \"failed\") {\n return {\n recipe: body.name,\n site: label,\n status: \"failed\",\n commits: [],\n notes: planned.notes,\n };\n }\n\n if (!body.checkTreeFirst && !(await isWorkingTreeClean(body.site.path))) {\n throw new Error(`refusing to run: working tree is not clean at ${body.site.path}`);\n }\n\n // Capture the operator's branch BEFORE we create the recipe branch, so we can\n // return them to it afterwards (#2) and so the failure path (#3) knows which\n // branch to force-restore to. Best-effort: if we can't read it (detached HEAD,\n // git error) we proceed with `original = null` and skip any force operations\n // rather than guess — we must NEVER force-discard/delete a branch we're unsure\n // of.\n let original: string | null = null;\n try {\n original = await currentBranch(body.site.path);\n } catch {\n original = null;\n }\n\n const branch = branchName(body.name);\n await createBranch(body.site.path, branch);\n\n /**\n * Best-effort restore to the operator's original branch. Never throws — a\n * restore failure must not turn an otherwise-clean recipe result into a\n * failure (#2). Skipped when we couldn't capture the original branch.\n *\n * IMPORTANT (composition): this is invoked only on the NOOP-from-apply path\n * (the recipe created a branch but committed nothing — leaving the operator\n * parked on an empty maint branch is pure downside). It is deliberately NOT\n * invoked on the APPLIED path: the fleet onboarding pipeline composes recipes\n * by running them in sequence against the SAME checkout, each building on the\n * prior's committed files in the working tree (convert-to-pnpm's lockfile →\n * onboard's deps → sync-configs → svelte-codemods). Restoring to the base\n * branch after an applied recipe would strip those files from the working\n * tree and break composition (verified live across the fleet). selfUpdating,\n * which PUSHES its branch, does its own post-push restore since its local\n * branch is disposable.\n */\n const restoreOriginal = async (): Promise<void> => {\n if (original === null || original === branch) return;\n try {\n await checkoutBranch(body.site.path, original);\n } catch (err) {\n // Leave the operator on the recipe branch rather than fail the result.\n console.warn(\n `warning: could not restore branch ${original} after ${body.name}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n };\n\n /**\n * Failure restore (#3): force the checkout back to the captured original\n * branch (discarding the recipe branch's uncommitted changes) and delete the\n * recipe branch, so a re-run starts clean. SAFETY: only ever force-checks-out\n * the captured `original` and only ever deletes `branch` (the recipe-created\n * branch); never deletes `original`, never runs `git clean`. If `original` is\n * unavailable we do nothing (the safe subset) — better to leave the operator\n * parked than to force anything we're unsure about. Best-effort: never throws.\n */\n const restoreAfterFailure = async (): Promise<void> => {\n if (original === null || original === branch) return;\n try {\n await forceCheckoutBranch(body.site.path, original);\n } catch (err) {\n console.warn(\n `warning: could not force-restore branch ${original} after failed ${body.name}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n // If we couldn't get off the recipe branch, deleting it would fail anyway;\n // and we must never delete the branch we're still on.\n return;\n }\n try {\n await deleteBranch(body.site.path, branch);\n } catch (err) {\n console.warn(\n `warning: could not delete recipe branch ${branch} after failed ${body.name}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n };\n\n const shas: string[] = [];\n let result: RecipeApplyResult;\n try {\n result = await body.apply(planned.plan, {\n cwd: body.site.path,\n branch,\n commit: async (msg) => {\n const sha = await gitCommit(body.site.path, msg);\n if (sha) shas.push(sha);\n return sha;\n },\n });\n } catch (err) {\n // Body threw mid-mutation: force-restore + delete the recipe branch so the\n // checkout is retriable, then re-throw (preserve the prior throw semantics —\n // callers like init treat an uncaught throw as an `error` step).\n await restoreAfterFailure();\n throw err;\n }\n\n if (result.kind === \"failed\") {\n await restoreAfterFailure();\n return {\n recipe: body.name,\n site: label,\n status: \"failed\",\n commits: shas,\n notes: result.notes,\n };\n }\n\n // NOOP-from-apply only: no commits to compose, so don't leave the operator\n // parked on an empty maint branch. The APPLIED path intentionally stays on the\n // maint branch so the onboarding pipeline can compose (see restoreOriginal).\n if (shas.length === 0) {\n await restoreOriginal();\n }\n\n const notes = result.notes ? `${result.notes}; branch: ${branch}` : `branch: ${branch}`;\n return {\n recipe: body.name,\n site: label,\n status: shas.length > 0 ? \"applied\" : \"noop\",\n commits: shas,\n notes,\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,138 @@
1
+ import {
2
+ svelteCodemods
3
+ } from "./chunk-FUD4I2ZW.js";
4
+ import {
5
+ onboard
6
+ } from "./chunk-ZYBOT5PM.js";
7
+ import {
8
+ convertToPnpm
9
+ } from "./chunk-2X322ZES.js";
10
+ import {
11
+ syncConfigs
12
+ } from "./chunk-JNXCDUYB.js";
13
+ import {
14
+ withRecipe
15
+ } from "./chunk-XKOZUVK2.js";
16
+ import {
17
+ runAudits
18
+ } from "./chunk-4PY4I5OR.js";
19
+ import {
20
+ siteLabel
21
+ } from "./chunk-XXTZBPUY.js";
22
+
23
+ // src/recipes/a11y-fixtures-page/index.ts
24
+ import { access, mkdir, writeFile } from "fs/promises";
25
+ import { dirname, join } from "path";
26
+
27
+ // src/recipes/a11y-fixtures-page/template.ts
28
+ var A11Y_FIXTURES_PAGE_RELATIVE = "src/routes/dev/a11y-fixtures/+page.svelte";
29
+ var A11Y_FIXTURES_PAGE_TEMPLATE = `<svelte:head>
30
+ <title>a11y fixtures \u2014 Reddoor</title>
31
+ <meta
32
+ name="description"
33
+ 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."
34
+ />
35
+ </svelte:head>
36
+
37
+ <main>
38
+ <header>
39
+ <h1>Accessibility fixtures</h1>
40
+ <p>
41
+ This page exists so <code>@lhci/cli</code> and Playwright + axe-core have a
42
+ stable target with predictable a11y characteristics. It is not linked from
43
+ the public site.
44
+ </p>
45
+ </header>
46
+
47
+ <section aria-labelledby="landmarks-heading">
48
+ <h2 id="landmarks-heading">Landmarks</h2>
49
+ <p>
50
+ A single <code>main</code> wraps the page; sections each declare
51
+ <code>aria-labelledby</code> matched to their heading id so screen readers
52
+ and axe both see a clean outline.
53
+ </p>
54
+ </section>
55
+
56
+ <section aria-labelledby="links-heading">
57
+ <h2 id="links-heading">Links</h2>
58
+ <p>
59
+ <a href="/">Back to home</a> \u2014 relative link with descriptive visible text,
60
+ so no <code>aria-label</code> override is needed.
61
+ </p>
62
+ </section>
63
+ </main>
64
+ `;
65
+
66
+ // src/recipes/a11y-fixtures-page/index.ts
67
+ async function fileExists(path) {
68
+ try {
69
+ await access(path);
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+ async function a11yFixturesPage(site) {
76
+ const target = join(site.path, A11Y_FIXTURES_PAGE_RELATIVE);
77
+ return withRecipe({
78
+ name: "a11y-fixtures-page",
79
+ site,
80
+ plan: async () => {
81
+ if (await fileExists(target)) {
82
+ return { kind: "noop", notes: `${A11Y_FIXTURES_PAGE_RELATIVE} already exists` };
83
+ }
84
+ return { kind: "apply", plan: { target } };
85
+ },
86
+ apply: async (planned, { commit }) => {
87
+ await mkdir(dirname(planned.target), { recursive: true });
88
+ await writeFile(planned.target, A11Y_FIXTURES_PAGE_TEMPLATE, "utf-8");
89
+ await commit("feat: add /dev/a11y-fixtures starter route");
90
+ return { kind: "ok" };
91
+ }
92
+ });
93
+ }
94
+
95
+ // src/recipes/init.ts
96
+ function recipeStep(name, fn) {
97
+ return {
98
+ name,
99
+ run: async (site) => ({ kind: "recipe", result: await fn(site) })
100
+ };
101
+ }
102
+ var DEFAULT_INIT_STEPS = [
103
+ recipeStep("convert-to-pnpm", convertToPnpm),
104
+ recipeStep("onboard", onboard),
105
+ recipeStep("sync-configs", syncConfigs),
106
+ recipeStep("svelte-codemods", svelteCodemods),
107
+ recipeStep("a11y-fixtures-page", a11yFixturesPage),
108
+ {
109
+ name: "audit",
110
+ run: async (site) => ({ kind: "audit", results: await runAudits(site) })
111
+ }
112
+ ];
113
+ async function init(site, opts = {}) {
114
+ const steps = opts.steps ?? DEFAULT_INIT_STEPS;
115
+ const out = [];
116
+ for (const step of steps) {
117
+ let result;
118
+ try {
119
+ result = await step.run(site);
120
+ } catch (err) {
121
+ const message = err instanceof Error ? err.message : String(err);
122
+ out.push({ name: step.name, result: { kind: "error", message } });
123
+ return { site: siteLabel(site), steps: out, complete: false };
124
+ }
125
+ out.push({ name: step.name, result });
126
+ if (result.kind === "recipe" && result.result.status === "failed") {
127
+ return { site: siteLabel(site), steps: out, complete: false };
128
+ }
129
+ }
130
+ return { site: siteLabel(site), steps: out, complete: true };
131
+ }
132
+
133
+ export {
134
+ a11yFixturesPage,
135
+ DEFAULT_INIT_STEPS,
136
+ init
137
+ };
138
+ //# sourceMappingURL=chunk-XO2RUYVM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/recipes/a11y-fixtures-page/index.ts","../src/recipes/a11y-fixtures-page/template.ts","../src/recipes/init.ts"],"sourcesContent":["import { access, mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { withRecipe } from \"../_with-recipe.js\";\nimport { A11Y_FIXTURES_PAGE_RELATIVE, A11Y_FIXTURES_PAGE_TEMPLATE } from \"./template.js\";\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Writes a starter `src/routes/dev/a11y-fixtures/+page.svelte` if the route\n * doesn't already exist. The hardcoded URL in `src/configs/lighthouse.ts` +\n * `src/configs/playwright-a11y.ts` targets this path — newly-onboarded sites\n * need the route to exist for either audit to pass. Operator edits to an\n * existing page are never clobbered (noop on existing file).\n */\nexport async function a11yFixturesPage(site: Site): Promise<RecipeResult> {\n const target = join(site.path, A11Y_FIXTURES_PAGE_RELATIVE);\n return withRecipe<{ target: string }>({\n name: \"a11y-fixtures-page\",\n site,\n plan: async () => {\n if (await fileExists(target)) {\n return { kind: \"noop\", notes: `${A11Y_FIXTURES_PAGE_RELATIVE} already exists` };\n }\n return { kind: \"apply\", plan: { target } };\n },\n apply: async (planned, { commit }) => {\n await mkdir(dirname(planned.target), { recursive: true });\n await writeFile(planned.target, A11Y_FIXTURES_PAGE_TEMPLATE, \"utf-8\");\n await commit(\"feat: add /dev/a11y-fixtures starter route\");\n return { kind: \"ok\" };\n },\n });\n}\n","/** Relative path inside a site where the a11y fixtures route lives. The\n * hardcoded URL in `src/configs/lighthouse.ts` + `src/configs/playwright-a11y.ts`\n * is `/dev/a11y-fixtures`, so a SvelteKit `+page.svelte` here resolves. */\nexport const A11Y_FIXTURES_PAGE_RELATIVE = \"src/routes/dev/a11y-fixtures/+page.svelte\";\n\n/** Stub `+page.svelte` for newly-onboarded sites. Generic on purpose —\n * landmarks, heading hierarchy, and a relative link cover the axe-core +\n * lhci defaults without committing the operator to any specific fixture\n * shape. Replace with site-specific patterns over time. */\nexport const A11Y_FIXTURES_PAGE_TEMPLATE = `<svelte:head>\n <title>a11y fixtures — Reddoor</title>\n <meta\n name=\"description\"\n content=\"Reddoor accessibility fixtures — semantic landmarks, heading hierarchy, and a stable target for @lhci/cli and Playwright + axe-core coverage. Not linked from the public site.\"\n />\n</svelte:head>\n\n<main>\n <header>\n <h1>Accessibility fixtures</h1>\n <p>\n This page exists so <code>@lhci/cli</code> and Playwright + axe-core have a\n stable target with predictable a11y characteristics. It is not linked from\n the public site.\n </p>\n </header>\n\n <section aria-labelledby=\"landmarks-heading\">\n <h2 id=\"landmarks-heading\">Landmarks</h2>\n <p>\n A single <code>main</code> wraps the page; sections each declare\n <code>aria-labelledby</code> matched to their heading id so screen readers\n and axe both see a clean outline.\n </p>\n </section>\n\n <section aria-labelledby=\"links-heading\">\n <h2 id=\"links-heading\">Links</h2>\n <p>\n <a href=\"/\">Back to home</a> — relative link with descriptive visible text,\n so no <code>aria-label</code> override is needed.\n </p>\n </section>\n</main>\n`;\n","import type { AuditResult, RecipeResult, Site } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { convertToPnpm } from \"./convert-to-pnpm.js\";\nimport { onboard } from \"./onboard.js\";\nimport { syncConfigs } from \"./sync-configs.js\";\nimport { svelteCodemods } from \"./svelte-codemods.js\";\nimport { a11yFixturesPage } from \"./a11y-fixtures-page/index.js\";\nimport { runAudits } from \"../audits/index.js\";\n\nexport type InitStepResult =\n | { kind: \"recipe\"; result: RecipeResult }\n | { kind: \"audit\"; results: AuditResult[] }\n | { kind: \"error\"; message: string };\n\nexport type InitStep = {\n name: string;\n run: (site: Site) => Promise<InitStepResult>;\n};\n\nexport type InitResult = {\n site: string;\n steps: Array<{ name: string; result: InitStepResult }>;\n /** True if every step ran; false if an `error` or `failed` recipe result\n * short-circuited the chain. `noop` recipes do not break completeness. */\n complete: boolean;\n};\n\nexport type InitOptions = {\n /** Override the default step list. Tests inject mocked steps; production\n * code relies on the default. */\n steps?: InitStep[];\n};\n\nfunction recipeStep(name: string, fn: (site: Site) => Promise<RecipeResult>): InitStep {\n return {\n name,\n run: async (site) => ({ kind: \"recipe\", result: await fn(site) }),\n };\n}\n\n/** convert-to-pnpm → onboard → sync-configs → svelte-codemods →\n * a11y-fixtures-page → audit. Order is deliberate — every step depends on\n * the prior one's output (pnpm before onboard's installs, onboard's deps\n * before sync-configs writes lighthouserc, fixtures page before audit\n * actually has a route to hit). */\nexport const DEFAULT_INIT_STEPS: InitStep[] = [\n recipeStep(\"convert-to-pnpm\", convertToPnpm),\n recipeStep(\"onboard\", onboard),\n recipeStep(\"sync-configs\", syncConfigs),\n recipeStep(\"svelte-codemods\", svelteCodemods),\n recipeStep(\"a11y-fixtures-page\", a11yFixturesPage),\n {\n name: \"audit\",\n run: async (site) => ({ kind: \"audit\", results: await runAudits(site) }),\n },\n];\n\n/**\n * One-shot guided onboarding. Runs the default step sequence against a\n * site, collecting per-step results into an InitResult. Each underlying\n * recipe still creates its own branch — init is a thin orchestrator, not\n * a branch-collapser; the operator ends up with one stack of branches per\n * mutated step (recipes that noop don't branch).\n *\n * Stops the chain on the first uncaught error or `failed` recipe result.\n * `noop` results are expected (e.g. running init twice) and continue the\n * chain. The final audit pass runs if no prior step errored.\n */\nexport async function init(site: Site, opts: InitOptions = {}): Promise<InitResult> {\n const steps = opts.steps ?? DEFAULT_INIT_STEPS;\n const out: Array<{ name: string; result: InitStepResult }> = [];\n\n for (const step of steps) {\n let result: InitStepResult;\n try {\n result = await step.run(site);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n out.push({ name: step.name, result: { kind: \"error\", message } });\n return { site: siteLabel(site), steps: out, complete: false };\n }\n out.push({ name: step.name, result });\n if (result.kind === \"recipe\" && result.result.status === \"failed\") {\n return { site: siteLabel(site), steps: out, complete: false };\n }\n }\n\n return { site: siteLabel(site), steps: out, complete: true };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,QAAQ,OAAO,iBAAiB;AACzC,SAAS,SAAS,YAAY;;;ACEvB,IAAM,8BAA8B;AAMpC,IAAM,8BAA8B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ADH3C,eAAe,WAAW,MAAgC;AACxD,MAAI;AACF,UAAM,OAAO,IAAI;AACjB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASA,eAAsB,iBAAiB,MAAmC;AACxE,QAAM,SAAS,KAAK,KAAK,MAAM,2BAA2B;AAC1D,SAAO,WAA+B;AAAA,IACpC,MAAM;AAAA,IACN;AAAA,IACA,MAAM,YAAY;AAChB,UAAI,MAAM,WAAW,MAAM,GAAG;AAC5B,eAAO,EAAE,MAAM,QAAQ,OAAO,GAAG,2BAA2B,kBAAkB;AAAA,MAChF;AACA,aAAO,EAAE,MAAM,SAAS,MAAM,EAAE,OAAO,EAAE;AAAA,IAC3C;AAAA,IACA,OAAO,OAAO,SAAS,EAAE,OAAO,MAAM;AACpC,YAAM,MAAM,QAAQ,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;AACxD,YAAM,UAAU,QAAQ,QAAQ,6BAA6B,OAAO;AACpE,YAAM,OAAO,4CAA4C;AACzD,aAAO,EAAE,MAAM,KAAK;AAAA,IACtB;AAAA,EACF,CAAC;AACH;;;AEPA,SAAS,WAAW,MAAc,IAAqD;AACrF,SAAO;AAAA,IACL;AAAA,IACA,KAAK,OAAO,UAAU,EAAE,MAAM,UAAU,QAAQ,MAAM,GAAG,IAAI,EAAE;AAAA,EACjE;AACF;AAOO,IAAM,qBAAiC;AAAA,EAC5C,WAAW,mBAAmB,aAAa;AAAA,EAC3C,WAAW,WAAW,OAAO;AAAA,EAC7B,WAAW,gBAAgB,WAAW;AAAA,EACtC,WAAW,mBAAmB,cAAc;AAAA,EAC5C,WAAW,sBAAsB,gBAAgB;AAAA,EACjD;AAAA,IACE,MAAM;AAAA,IACN,KAAK,OAAO,UAAU,EAAE,MAAM,SAAS,SAAS,MAAM,UAAU,IAAI,EAAE;AAAA,EACxE;AACF;AAaA,eAAsB,KAAK,MAAY,OAAoB,CAAC,GAAwB;AAClF,QAAM,QAAQ,KAAK,SAAS;AAC5B,QAAM,MAAuD,CAAC;AAE9D,aAAW,QAAQ,OAAO;AACxB,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,KAAK,IAAI,IAAI;AAAA,IAC9B,SAAS,KAAK;AACZ,YAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,UAAI,KAAK,EAAE,MAAM,KAAK,MAAM,QAAQ,EAAE,MAAM,SAAS,QAAQ,EAAE,CAAC;AAChE,aAAO,EAAE,MAAM,UAAU,IAAI,GAAG,OAAO,KAAK,UAAU,MAAM;AAAA,IAC9D;AACA,QAAI,KAAK,EAAE,MAAM,KAAK,MAAM,OAAO,CAAC;AACpC,QAAI,OAAO,SAAS,YAAY,OAAO,OAAO,WAAW,UAAU;AACjE,aAAO,EAAE,MAAM,UAAU,IAAI,GAAG,OAAO,KAAK,UAAU,MAAM;AAAA,IAC9D;AAAA,EACF;AAEA,SAAO,EAAE,MAAM,UAAU,IAAI,GAAG,OAAO,KAAK,UAAU,KAAK;AAC7D;","names":[]}
@@ -0,0 +1,97 @@
1
+ // src/util/git.ts
2
+ import { execFile } from "child_process";
3
+ import { promisify } from "util";
4
+ var exec = promisify(execFile);
5
+ async function git(cwd, args) {
6
+ return exec("git", args, { cwd, env: process.env });
7
+ }
8
+ function branchName(recipe, when = /* @__PURE__ */ new Date()) {
9
+ const compact = when.toISOString().replace(/[-:.]/g, "");
10
+ return `maint/${recipe}-${compact}`;
11
+ }
12
+ async function currentBranch(cwd) {
13
+ const { stdout } = await git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
14
+ return stdout.trim();
15
+ }
16
+ async function isWorkingTreeClean(cwd) {
17
+ const { stdout } = await git(cwd, ["status", "--porcelain"]);
18
+ return stdout.trim().length === 0;
19
+ }
20
+ async function createBranch(cwd, name) {
21
+ await git(cwd, ["checkout", "-b", name]);
22
+ }
23
+ async function checkoutBranch(cwd, name) {
24
+ await git(cwd, ["checkout", name]);
25
+ }
26
+ async function forceCheckoutBranch(cwd, name) {
27
+ await git(cwd, ["checkout", "-f", name]);
28
+ }
29
+ async function deleteBranch(cwd, name) {
30
+ await git(cwd, ["branch", "-D", name]);
31
+ }
32
+ async function stageAll(cwd) {
33
+ await git(cwd, ["add", "-A"]);
34
+ }
35
+ async function listTrackedFiles(cwd) {
36
+ const { stdout } = await git(cwd, ["ls-files"]);
37
+ return stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
38
+ }
39
+ async function removeFromIndex(cwd, paths) {
40
+ if (paths.length === 0) return;
41
+ await git(cwd, ["rm", "-r", "--cached", "--", ...paths]);
42
+ }
43
+ async function commit(cwd, message) {
44
+ await stageAll(cwd);
45
+ const { stdout: status } = await git(cwd, ["status", "--porcelain"]);
46
+ if (status.trim().length === 0) return null;
47
+ await git(cwd, ["commit", "-m", message]);
48
+ const { stdout: sha } = await git(cwd, ["rev-parse", "HEAD"]);
49
+ return sha.trim();
50
+ }
51
+ var OWNER_REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
52
+ function isOwnerRepo(repo) {
53
+ if (repo.includes("..")) return false;
54
+ return OWNER_REPO_RE.test(repo);
55
+ }
56
+ function sameOwnerRepo(a, b) {
57
+ const na = isOwnerRepo(a.trim()) ? a.trim() : parseOwnerRepo(a);
58
+ const nb = isOwnerRepo(b.trim()) ? b.trim() : parseOwnerRepo(b);
59
+ if (na === null || nb === null) return false;
60
+ return na.toLowerCase() === nb.toLowerCase();
61
+ }
62
+ function parseOwnerRepo(remoteUrl) {
63
+ const trimmed = remoteUrl.trim().replace(/\.git$/, "").replace(/\/$/, "");
64
+ const scp = trimmed.match(/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:(.+)$/);
65
+ const path = scp ? scp[1] : trimmed.replace(/^https?:\/\/[^/]+\//, "");
66
+ const segments = path.split("/").filter(Boolean);
67
+ if (segments.length < 2) return null;
68
+ return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`;
69
+ }
70
+ async function getRemoteUrl(cwd) {
71
+ const { stdout } = await git(cwd, ["remote", "get-url", "origin"]);
72
+ return stdout.trim();
73
+ }
74
+ async function push(cwd, branch) {
75
+ await git(cwd, ["push", "-u", "origin", branch]);
76
+ }
77
+
78
+ export {
79
+ branchName,
80
+ currentBranch,
81
+ isWorkingTreeClean,
82
+ createBranch,
83
+ checkoutBranch,
84
+ forceCheckoutBranch,
85
+ deleteBranch,
86
+ stageAll,
87
+ listTrackedFiles,
88
+ removeFromIndex,
89
+ commit,
90
+ OWNER_REPO_RE,
91
+ isOwnerRepo,
92
+ sameOwnerRepo,
93
+ parseOwnerRepo,
94
+ getRemoteUrl,
95
+ push
96
+ };
97
+ //# sourceMappingURL=chunk-XTK5VIQB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/git.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst exec = promisify(execFile);\n\nasync function git(cwd: string, args: string[]): Promise<{ stdout: string; stderr: string }> {\n return exec(\"git\", args, { cwd, env: process.env });\n}\n\nexport function branchName(recipe: string, when: Date = new Date()): string {\n // ISO with millisecond precision: 2026-05-20T10:30:00.123Z → 20260520T103000123Z.\n // Millis (vs. second-precision) shrinks the collision window for parallel runs.\n const compact = when.toISOString().replace(/[-:.]/g, \"\");\n return `maint/${recipe}-${compact}`;\n}\n\nexport async function currentBranch(cwd: string): Promise<string> {\n const { stdout } = await git(cwd, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n return stdout.trim();\n}\n\nexport async function isWorkingTreeClean(cwd: string): Promise<boolean> {\n const { stdout } = await git(cwd, [\"status\", \"--porcelain\"]);\n return stdout.trim().length === 0;\n}\n\nexport async function createBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", \"-b\", name]);\n}\n\n/** Check out an existing branch. Throws (via git) if the branch is missing or\n * the checkout is blocked (e.g. uncommitted changes that would be overwritten). */\nexport async function checkoutBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", name]);\n}\n\n/**\n * Force-check-out an existing branch, DISCARDING any uncommitted changes on the\n * current branch. Used by the recipe failure path to return the operator to\n * their original branch even when a recipe left the work-in-progress branch\n * dirty. Only ever called with the operator's ORIGINAL branch as `name`. Does\n * not run `git clean`, so untracked operator files are left untouched.\n */\nexport async function forceCheckoutBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", \"-f\", name]);\n}\n\n/**\n * Delete a local branch with `-D` (force). Used by the recipe failure path to\n * remove the branch the recipe itself created so a re-run starts clean. Callers\n * MUST only ever pass the recipe-created branch here, never the operator's\n * original branch.\n */\nexport async function deleteBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"branch\", \"-D\", name]);\n}\n\nexport async function stageAll(cwd: string): Promise<void> {\n await git(cwd, [\"add\", \"-A\"]);\n}\n\nexport async function listTrackedFiles(cwd: string): Promise<string[]> {\n const { stdout } = await git(cwd, [\"ls-files\"]);\n return stdout\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n}\n\nexport async function removeFromIndex(cwd: string, paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n await git(cwd, [\"rm\", \"-r\", \"--cached\", \"--\", ...paths]);\n}\n\n/**\n * Stages all current changes and commits with `message`. Returns the commit SHA,\n * or `null` if there was nothing to commit.\n */\nexport async function commit(cwd: string, message: string): Promise<string | null> {\n await stageAll(cwd);\n const { stdout: status } = await git(cwd, [\"status\", \"--porcelain\"]);\n if (status.trim().length === 0) return null;\n await git(cwd, [\"commit\", \"-m\", message]);\n const { stdout: sha } = await git(cwd, [\"rev-parse\", \"HEAD\"]);\n return sha.trim();\n}\n\n/**\n * Strict GitHub repo identity: exactly two `[A-Za-z0-9._-]` segments separated\n * by a single slash. Rejects a scheme, host, extra path segment, traversal\n * (`..`), whitespace, or an argv flag — anything that could retarget a `gh`\n * write at an unintended (attacker/typo-controlled) repo.\n */\nexport const OWNER_REPO_RE = /^[A-Za-z0-9._-]+\\/[A-Za-z0-9._-]+$/;\n\n/** True when `repo` is a clean `owner/repo` (see {@link OWNER_REPO_RE}). The\n * explicit `..` reject covers traversal segments the char-class would otherwise\n * admit (`.` is a legal repo char, so `../evil` matches the shape regex). */\nexport function isOwnerRepo(repo: string): boolean {\n if (repo.includes(\"..\")) return false;\n return OWNER_REPO_RE.test(repo);\n}\n\n/**\n * True when two repo references name the same `owner/repo`. Each side may be a\n * full remote URL (https or scp-style), or already an `owner/repo`. Used to\n * verify an existing checkout's `origin` matches the site's expected repo\n * before reusing it. Returns false if either side is unparseable.\n */\nexport function sameOwnerRepo(a: string, b: string): boolean {\n const na = isOwnerRepo(a.trim()) ? a.trim() : parseOwnerRepo(a);\n const nb = isOwnerRepo(b.trim()) ? b.trim() : parseOwnerRepo(b);\n if (na === null || nb === null) return false;\n return na.toLowerCase() === nb.toLowerCase();\n}\n\n/** Derive `owner/repo` from a git remote URL (https or scp-style). Null if unparseable. */\nexport function parseOwnerRepo(remoteUrl: string): string | null {\n const trimmed = remoteUrl\n .trim()\n .replace(/\\.git$/, \"\")\n .replace(/\\/$/, \"\");\n // scp-style: git@github.com:owner/repo\n const scp = trimmed.match(/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:(.+)$/);\n const path = scp ? scp[1]! : trimmed.replace(/^https?:\\/\\/[^/]+\\//, \"\");\n const segments = path.split(\"/\").filter(Boolean);\n if (segments.length < 2) return null;\n return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`;\n}\n\n/** `origin` remote URL for a checkout, trimmed. Throws (via git) if there's no origin. */\nexport async function getRemoteUrl(cwd: string): Promise<string> {\n const { stdout } = await git(cwd, [\"remote\", \"get-url\", \"origin\"]);\n return stdout.trim();\n}\n\n/** Push a branch to origin, setting upstream. Throws on non-zero (execFile rejects). */\nexport async function push(cwd: string, branch: string): Promise<void> {\n await git(cwd, [\"push\", \"-u\", \"origin\", branch]);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,9 @@
1
+ // src/util/site.ts
2
+ function siteLabel(site) {
3
+ return site.name || site.path;
4
+ }
5
+
6
+ export {
7
+ siteLabel
8
+ };
9
+ //# sourceMappingURL=chunk-XXTZBPUY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/site.ts"],"sourcesContent":["import type { Site } from \"../types.js\";\n\n/** Human-friendly label for log/output formatting. Prefer the inventory's\n * `name` when present (e.g. \"caltex-landing\") and fall back to the\n * filesystem `path` when unnamed. Every audit + recipe uses this.\n *\n * Uses `||` (not `??`) deliberately: an Airtable Name that slugs to the EMPTY\n * string (`siteSlug(\"!!!\")` → \"\") is `\"\"`, not null/undefined, so `??` would let\n * it through and render a blank label. `||` falls back to the path. */\nexport function siteLabel(site: Site): string {\n return site.name || site.path;\n}\n"],"mappings":";AASO,SAAS,UAAU,MAAoB;AAC5C,SAAO,KAAK,QAAQ,KAAK;AAC3B;","names":[]}
@@ -0,0 +1,153 @@
1
+ import {
2
+ withRecipe
3
+ } from "./chunk-XKOZUVK2.js";
4
+ import {
5
+ bumpDep,
6
+ readPackageJson,
7
+ writePackageJson
8
+ } from "./chunk-OTGB6YQO.js";
9
+ import {
10
+ baselineVersions
11
+ } from "./chunk-LKGVSM2O.js";
12
+ import {
13
+ defaultSpawn
14
+ } from "./chunk-HVOOCK6L.js";
15
+
16
+ // src/util/self-version.ts
17
+ import { readFileSync, existsSync } from "fs";
18
+ import { fileURLToPath } from "url";
19
+ import { dirname, join } from "path";
20
+ function selfPackageVersion(callerImportMetaUrl) {
21
+ try {
22
+ let dir = dirname(fileURLToPath(callerImportMetaUrl));
23
+ while (true) {
24
+ const candidate = join(dir, "package.json");
25
+ if (existsSync(candidate)) {
26
+ const raw = readFileSync(candidate, "utf-8");
27
+ const pkg = JSON.parse(raw);
28
+ if (pkg.name === "@reddoorla/maintenance") {
29
+ return pkg.version ?? "0.0.0";
30
+ }
31
+ }
32
+ const parent = dirname(dir);
33
+ if (parent === dir) return "0.0.0";
34
+ dir = parent;
35
+ }
36
+ } catch {
37
+ return "0.0.0";
38
+ }
39
+ }
40
+ function selfCaretRange(callerImportMetaUrl) {
41
+ return `^${selfPackageVersion(callerImportMetaUrl)}`;
42
+ }
43
+
44
+ // src/recipes/onboard.ts
45
+ import { stat } from "fs/promises";
46
+ import { join as join2 } from "path";
47
+ var PACKAGE_NAME = "@reddoorla/maintenance";
48
+ var AUDIT_DEP_NAMES = {
49
+ lighthouse: ["@lhci/cli"],
50
+ a11y: ["@playwright/test", "@axe-core/playwright"]
51
+ };
52
+ var FRAMEWORK_DEP_NAMES = ["@sveltejs/adapter-netlify"];
53
+ var FRAMEWORK_DEPS = FRAMEWORK_DEP_NAMES.map(
54
+ (name) => {
55
+ const version = baselineVersions[name];
56
+ if (!version) {
57
+ throw new Error(
58
+ `baseline-versions is missing framework dep "${name}" \u2014 add it to src/configs/baseline-versions.ts`
59
+ );
60
+ }
61
+ return { name, version };
62
+ }
63
+ );
64
+ var AUDIT_DEPS = Object.fromEntries(
65
+ Object.entries(AUDIT_DEP_NAMES).map(([audit, names]) => [
66
+ audit,
67
+ names.map((name) => {
68
+ const version = baselineVersions[name];
69
+ if (!version) {
70
+ throw new Error(
71
+ `baseline-versions is missing audit dep "${name}" \u2014 add it to src/configs/baseline-versions.ts`
72
+ );
73
+ }
74
+ return { name, version };
75
+ })
76
+ ])
77
+ );
78
+ async function exists(path) {
79
+ try {
80
+ await stat(path);
81
+ return true;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+ function isDeclared(pkg, name) {
87
+ return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
88
+ }
89
+ async function onboard(site, opts = {}) {
90
+ const spawn = opts.spawn ?? defaultSpawn;
91
+ const audits = opts.audits ?? ["lighthouse", "a11y"];
92
+ const packageVersion = opts.packageVersion ?? selfCaretRange(import.meta.url);
93
+ return withRecipe({
94
+ name: "onboard",
95
+ site,
96
+ plan: async () => {
97
+ if (!await exists(join2(site.path, "pnpm-lock.yaml"))) {
98
+ return {
99
+ kind: "failed",
100
+ notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
101
+ };
102
+ }
103
+ const pkgPath = join2(site.path, "package.json");
104
+ const pkg = await readPackageJson(pkgPath);
105
+ const toAdd = [];
106
+ if (!isDeclared(pkg, PACKAGE_NAME)) {
107
+ toAdd.push({ name: PACKAGE_NAME, version: packageVersion });
108
+ }
109
+ for (const dep of FRAMEWORK_DEPS) {
110
+ if (!isDeclared(pkg, dep.name)) toAdd.push(dep);
111
+ }
112
+ for (const audit of audits) {
113
+ for (const dep of AUDIT_DEPS[audit]) {
114
+ if (!isDeclared(pkg, dep.name)) toAdd.push(dep);
115
+ }
116
+ }
117
+ if (toAdd.length === 0) {
118
+ return {
119
+ kind: "noop",
120
+ notes: `site already has ${PACKAGE_NAME}, framework deps, and audit deps (${audits.join("+")})`
121
+ };
122
+ }
123
+ return { kind: "apply", plan: { pkg, toAdd } };
124
+ },
125
+ apply: async ({ pkg, toAdd }, { commit, cwd }) => {
126
+ const pkgPath = join2(cwd, "package.json");
127
+ let next = pkg;
128
+ for (const dep of toAdd) {
129
+ next = bumpDep(next, dep.name, dep.version);
130
+ }
131
+ await writePackageJson(pkgPath, next);
132
+ const installResult = await spawn("pnpm", ["install"], { cwd, streaming: true });
133
+ if (installResult.code !== 0) {
134
+ return {
135
+ kind: "failed",
136
+ notes: `pnpm install failed (exit ${installResult.code})`
137
+ };
138
+ }
139
+ await commit(`chore(reddoor): onboard with ${PACKAGE_NAME} ${packageVersion}`);
140
+ return {
141
+ kind: "ok",
142
+ notes: `Added ${toAdd.length} dep(s): ${toAdd.map((d) => d.name).join(", ")}`
143
+ };
144
+ }
145
+ });
146
+ }
147
+
148
+ export {
149
+ selfPackageVersion,
150
+ selfCaretRange,
151
+ onboard
152
+ };
153
+ //# sourceMappingURL=chunk-ZYBOT5PM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/self-version.ts","../src/recipes/onboard.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * Read this package's own version at runtime so recipe defaults don't go\n * stale at each minor bump.\n *\n * Pass `import.meta.url` from the calling file. Walks UP from the caller\n * looking for the first `package.json` whose `name` matches this package\n * (`@reddoorla/maintenance`). The older \"two levels up\" shortcut held for\n * `src/X/Y.ts` and `dist/cli/bin.js` (both happen to be 2 dirs deep) but\n * broke for `dist/index.js` (only 1 dir deep) — silently returned \"0.0.0\"\n * and pinned consumers to `^0.0.0`. Same bug class as the 0.10.1 bundled-\n * assets ENOENT (2026-05-27). Walk-up is robust regardless of bundling\n * layout.\n *\n * Returns \"0.0.0\" if no matching package.json is reachable (defensive\n * fallback; callers should treat that as a signal to either override\n * explicitly or fail loudly).\n */\nexport function selfPackageVersion(callerImportMetaUrl: string): string {\n try {\n let dir = dirname(fileURLToPath(callerImportMetaUrl));\n while (true) {\n const candidate = join(dir, \"package.json\");\n if (existsSync(candidate)) {\n const raw = readFileSync(candidate, \"utf-8\");\n const pkg = JSON.parse(raw) as { name?: string; version?: string };\n // Only accept OUR package.json — keep walking past random ancestor\n // package.jsons (the consumer's own, anything in node_modules) that\n // happen to sit above the bundle.\n if (pkg.name === \"@reddoorla/maintenance\") {\n return pkg.version ?? \"0.0.0\";\n }\n }\n const parent = dirname(dir);\n if (parent === dir) return \"0.0.0\";\n dir = parent;\n }\n } catch {\n return \"0.0.0\";\n }\n}\n\n/** Caret-pinned range against this package's own version: e.g. \"^0.6.2\". */\nexport function selfCaretRange(callerImportMetaUrl: string): string {\n return `^${selfPackageVersion(callerImportMetaUrl)}`;\n}\n","import { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../types.js\";\nimport { readPackageJson, writePackageJson, bumpDep, type PackageJsonLike } from \"../util/pkg.js\";\nimport { defaultSpawn, type SpawnFn } from \"../audits/util/spawn.js\";\nimport { selfCaretRange } from \"../util/self-version.js\";\nimport { baselineVersions } from \"../configs/baseline-versions.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\nexport type OnboardAudit = \"lighthouse\" | \"a11y\";\n\nexport type OnboardOptions = {\n spawn?: SpawnFn;\n /** Which audit-related deps to ensure. Defaults to all known audits. */\n audits?: OnboardAudit[];\n /** Version range to pin for @reddoorla/maintenance. Defaults to a caret\n * range against this package's own version at runtime — no manual\n * syncing required at each minor bump. */\n packageVersion?: string;\n};\n\nconst PACKAGE_NAME = \"@reddoorla/maintenance\";\n\nconst AUDIT_DEP_NAMES: Record<OnboardAudit, string[]> = {\n lighthouse: [\"@lhci/cli\"],\n a11y: [\"@playwright/test\", \"@axe-core/playwright\"],\n};\n\n/** Framework deps onboard ensures for every site, independent of which audits\n * are requested. The sync-configs svelte.config.js template does\n * `import adapter from \"@sveltejs/adapter-netlify\"`, so a site that lacks the\n * adapter declared can't build once configs are synced — onboard closes that\n * gap at the same time it adds the maintenance package. */\nconst FRAMEWORK_DEP_NAMES = [\"@sveltejs/adapter-netlify\"];\n\n/** Resolve framework dep versions from baselineVersions at module load so they\n * can't drift from the single source of truth — mirrors AUDIT_DEPS. Throws at\n * import time if a name is missing there (a programming error). */\nexport const FRAMEWORK_DEPS: Array<{ name: string; version: string }> = FRAMEWORK_DEP_NAMES.map(\n (name) => {\n const version = baselineVersions[name];\n if (!version) {\n throw new Error(\n `baseline-versions is missing framework dep \"${name}\" — add it to src/configs/baseline-versions.ts`,\n );\n }\n return { name, version };\n },\n);\n\n/** Look up each audit dep's version in baselineVersions at module load so\n * AUDIT_DEPS can't drift from the single source of truth across releases.\n * Throws at import time if baseline-versions is missing an audit dep —\n * which would be a programming error (every audit dep name above must\n * appear in baselineVersions). */\nexport const AUDIT_DEPS: Record<\n OnboardAudit,\n Array<{ name: string; version: string }>\n> = Object.fromEntries(\n (Object.entries(AUDIT_DEP_NAMES) as Array<[OnboardAudit, string[]]>).map(([audit, names]) => [\n audit,\n names.map((name) => {\n const version = baselineVersions[name];\n if (!version) {\n throw new Error(\n `baseline-versions is missing audit dep \"${name}\" — add it to src/configs/baseline-versions.ts`,\n );\n }\n return { name, version };\n }),\n ]),\n) as Record<OnboardAudit, Array<{ name: string; version: string }>>;\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction isDeclared(pkg: PackageJsonLike, name: string): boolean {\n return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);\n}\n\ntype Plan = {\n pkg: PackageJsonLike;\n toAdd: Array<{ name: string; version: string }>;\n};\n\nexport async function onboard(site: Site, opts: OnboardOptions = {}): Promise<RecipeResult> {\n const spawn = opts.spawn ?? defaultSpawn;\n const audits = opts.audits ?? ([\"lighthouse\", \"a11y\"] as OnboardAudit[]);\n const packageVersion = opts.packageVersion ?? selfCaretRange(import.meta.url);\n\n return withRecipe<Plan>({\n name: \"onboard\",\n site,\n plan: async () => {\n // Pre-flight: site must already be on pnpm. We don't auto-convert here;\n // that's the convert-to-pnpm recipe's job, and combining them would\n // hide the package-manager transition inside a bigger PR.\n if (!(await exists(join(site.path, \"pnpm-lock.yaml\")))) {\n return {\n kind: \"failed\",\n notes: \"no pnpm-lock.yaml at site root — run convert-to-pnpm first\",\n };\n }\n\n const pkgPath = join(site.path, \"package.json\");\n const pkg = await readPackageJson(pkgPath);\n\n // Determine what's missing. Anything already declared (even at a wildly\n // different version) is left alone — onboard never downgrades.\n const toAdd: Array<{ name: string; version: string }> = [];\n if (!isDeclared(pkg, PACKAGE_NAME)) {\n toAdd.push({ name: PACKAGE_NAME, version: packageVersion });\n }\n for (const dep of FRAMEWORK_DEPS) {\n if (!isDeclared(pkg, dep.name)) toAdd.push(dep);\n }\n for (const audit of audits) {\n for (const dep of AUDIT_DEPS[audit]) {\n if (!isDeclared(pkg, dep.name)) toAdd.push(dep);\n }\n }\n\n if (toAdd.length === 0) {\n return {\n kind: \"noop\",\n notes: `site already has ${PACKAGE_NAME}, framework deps, and audit deps (${audits.join(\"+\")})`,\n };\n }\n return { kind: \"apply\", plan: { pkg, toAdd } };\n },\n apply: async ({ pkg, toAdd }, { commit, cwd }) => {\n const pkgPath = join(cwd, \"package.json\");\n let next: PackageJsonLike = pkg;\n for (const dep of toAdd) {\n next = bumpDep(next, dep.name, dep.version);\n }\n await writePackageJson(pkgPath, next);\n\n // Run pnpm install so the lockfile reflects the new deps before we commit.\n // Stream output — install on a real site can take 30s+.\n const installResult = await spawn(\"pnpm\", [\"install\"], { cwd, streaming: true });\n if (installResult.code !== 0) {\n return {\n kind: \"failed\",\n notes: `pnpm install failed (exit ${installResult.code})`,\n };\n }\n\n await commit(`chore(reddoor): onboard with ${PACKAGE_NAME} ${packageVersion}`);\n return {\n kind: \"ok\",\n notes: `Added ${toAdd.length} dep(s): ${toAdd.map((d) => d.name).join(\", \")}`,\n };\n },\n });\n}\n"],"mappings":"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","names":["join","join"]}