@reddoorla/maintenance 0.95.0 → 0.96.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 (272) hide show
  1. package/README.md +37 -2
  2. package/dist/airtable-SJV4ZSMS.js +9 -0
  3. package/dist/{announce-PGT4SZZB.js → announce-45HVP4J4.js} +7 -7
  4. package/dist/{blux-4KNXUP2K.js → blux-RUFMNOUJ.js} +5 -5
  5. package/dist/{bump-deps-C5G2EFBB.js → bump-deps-EL3TKMIL.js} +6 -6
  6. package/dist/{chunk-XN37VTDB.js → chunk-37B4LVB3.js} +2 -2
  7. package/dist/{chunk-BVEDKMNG.js → chunk-3EA43S3R.js} +28 -10
  8. package/dist/chunk-3EA43S3R.js.map +1 -0
  9. package/dist/{chunk-XHHMM5HX.js → chunk-43SW3FGW.js} +63 -1
  10. package/dist/chunk-43SW3FGW.js.map +1 -0
  11. package/dist/{chunk-WMBLR5VA.js → chunk-44SDGPGQ.js} +3 -3
  12. package/dist/chunk-4UTJ4G6P.js +52 -0
  13. package/dist/chunk-4UTJ4G6P.js.map +1 -0
  14. package/dist/{chunk-R53VL3RI.js → chunk-5CJVUPJU.js} +1 -1
  15. package/dist/{chunk-R53VL3RI.js.map → chunk-5CJVUPJU.js.map} +1 -1
  16. package/dist/{chunk-4AIYWWKV.js → chunk-62YWOFVN.js} +36 -5
  17. package/dist/chunk-62YWOFVN.js.map +1 -0
  18. package/dist/{chunk-22GKJO5F.js → chunk-6INOBNRO.js} +2 -2
  19. package/dist/{chunk-BYGPUUCT.js → chunk-72FJZ6LO.js} +43 -11
  20. package/dist/chunk-72FJZ6LO.js.map +1 -0
  21. package/dist/{chunk-5GUJ36TL.js → chunk-74QRWE6G.js} +2 -2
  22. package/dist/chunk-A5QKNOXW.js +72 -0
  23. package/dist/chunk-A5QKNOXW.js.map +1 -0
  24. package/dist/{chunk-2NQUCS7O.js → chunk-BIYQN7FH.js} +3 -1
  25. package/dist/{chunk-2NQUCS7O.js.map → chunk-BIYQN7FH.js.map} +1 -1
  26. package/dist/{chunk-S7RYFUIK.js → chunk-CA6ADGOC.js} +2 -2
  27. package/dist/chunk-CGQZFQAY.js +38 -0
  28. package/dist/chunk-CGQZFQAY.js.map +1 -0
  29. package/dist/{chunk-YLYXW5PH.js → chunk-CXICCLNM.js} +34 -4
  30. package/dist/chunk-CXICCLNM.js.map +1 -0
  31. package/dist/{chunk-KZRR4NFG.js → chunk-D3CTOCUR.js} +3 -3
  32. package/dist/{chunk-CWXTNPNZ.js → chunk-DVJNYPFR.js} +3 -3
  33. package/dist/chunk-DVJNYPFR.js.map +1 -0
  34. package/dist/{chunk-AVEQUR44.js → chunk-EASYCD6F.js} +60 -17
  35. package/dist/chunk-EASYCD6F.js.map +1 -0
  36. package/dist/{chunk-BCKEP5S6.js → chunk-ECDJDHKT.js} +2 -2
  37. package/dist/{chunk-DMPHP7UP.js → chunk-EQQ4H6L3.js} +2 -2
  38. package/dist/{chunk-WVNDEYXL.js → chunk-GEUC5VGM.js} +11 -5
  39. package/dist/chunk-GEUC5VGM.js.map +1 -0
  40. package/dist/{chunk-Z2K4JPQ3.js → chunk-H445EH33.js} +2 -2
  41. package/dist/chunk-IHHAYXKY.js +351 -0
  42. package/dist/chunk-IHHAYXKY.js.map +1 -0
  43. package/dist/{chunk-FUWFLAWQ.js → chunk-IMYL67JV.js} +3 -3
  44. package/dist/{chunk-3Z6X3BHU.js → chunk-KN52PM63.js} +7 -2
  45. package/dist/{chunk-3Z6X3BHU.js.map → chunk-KN52PM63.js.map} +1 -1
  46. package/dist/{chunk-WJMPCVOR.js → chunk-LAJ3FF54.js} +12 -4
  47. package/dist/chunk-LAJ3FF54.js.map +1 -0
  48. package/dist/{chunk-5UINJNAJ.js → chunk-MZZDMBDT.js} +16 -3
  49. package/dist/chunk-MZZDMBDT.js.map +1 -0
  50. package/dist/{chunk-DFKBTBO3.js → chunk-NVLREB5R.js} +35 -6
  51. package/dist/chunk-NVLREB5R.js.map +1 -0
  52. package/dist/{chunk-AEUABPAJ.js → chunk-OTEVKRAH.js} +3 -3
  53. package/dist/{chunk-KQB2BX4V.js → chunk-P4A6D6HO.js} +4 -4
  54. package/dist/chunk-P7323YVW.js +134 -0
  55. package/dist/chunk-P7323YVW.js.map +1 -0
  56. package/dist/{chunk-4763KQ6O.js → chunk-PXRN2MER.js} +2 -2
  57. package/dist/{chunk-TR3HFH6J.js → chunk-QFSL2L2B.js} +67 -10
  58. package/dist/chunk-QFSL2L2B.js.map +1 -0
  59. package/dist/{chunk-2LYOAB2E.js → chunk-RRE74VWI.js} +2 -2
  60. package/dist/{chunk-LM2NHRCJ.js → chunk-SHGIDTYF.js} +4 -4
  61. package/dist/chunk-SHGIDTYF.js.map +1 -0
  62. package/dist/{chunk-ZQV7UQNE.js → chunk-SQ4FSNYL.js} +2 -2
  63. package/dist/{chunk-OCNSIBMP.js → chunk-T76MA2FH.js} +135 -1
  64. package/dist/{chunk-OCNSIBMP.js.map → chunk-T76MA2FH.js.map} +1 -1
  65. package/dist/{chunk-6R56OKJZ.js → chunk-UIITN37J.js} +2 -2
  66. package/dist/{chunk-CJ2RMTT7.js → chunk-VEIIUCYL.js} +2 -2
  67. package/dist/{chunk-SVAAONYB.js → chunk-WQLNUEZL.js} +12 -2
  68. package/dist/{chunk-SVAAONYB.js.map → chunk-WQLNUEZL.js.map} +1 -1
  69. package/dist/{chunk-WSJ7RO54.js → chunk-XED2DNAI.js} +23 -5
  70. package/dist/chunk-XED2DNAI.js.map +1 -0
  71. package/dist/{chunk-FSGICFXB.js → chunk-XG2A6GIG.js} +17 -5
  72. package/dist/chunk-XG2A6GIG.js.map +1 -0
  73. package/dist/{chunk-TO7DJYLZ.js → chunk-XY7YV2YQ.js} +178 -39
  74. package/dist/chunk-XY7YV2YQ.js.map +1 -0
  75. package/dist/{chunk-HTABUCF4.js → chunk-Y5BVR4HW.js} +3 -3
  76. package/dist/chunk-Y6FCXRIC.js +3972 -0
  77. package/dist/chunk-Y6FCXRIC.js.map +1 -0
  78. package/dist/{chunk-2SLED3NB.js → chunk-YDZ7XGPZ.js} +1 -1
  79. package/dist/{chunk-2SLED3NB.js.map → chunk-YDZ7XGPZ.js.map} +1 -1
  80. package/dist/{chunk-QF6IQQFP.js → chunk-Z7FDJ35L.js} +18 -12
  81. package/dist/chunk-Z7FDJ35L.js.map +1 -0
  82. package/dist/{chunk-5EEMTJMT.js → chunk-ZH5LOQET.js} +4 -4
  83. package/dist/cli/bin.js +49 -31
  84. package/dist/cli/bin.js.map +1 -1
  85. package/dist/cli/commands/audit.js +18 -18
  86. package/dist/client-L7NAUG6J.js +11 -0
  87. package/dist/configs/eslint.js +27 -1
  88. package/dist/configs/eslint.js.map +1 -1
  89. package/dist/configs/playwright-a11y.d.ts +11 -1
  90. package/dist/configs/playwright-a11y.js +3 -1
  91. package/dist/{convert-to-pnpm-YTFREUJK.js → convert-to-pnpm-4DON7IFS.js} +6 -6
  92. package/dist/{db-BBEM7CBX.js → db-5K4CDEGJ.js} +45 -20
  93. package/dist/db-5K4CDEGJ.js.map +1 -0
  94. package/dist/deadletter-G4M3HSVM.js +17 -0
  95. package/dist/{digest-65BOZ7AT.js → digest-5AJGWS33.js} +45 -22
  96. package/dist/digest-5AJGWS33.js.map +1 -0
  97. package/dist/{digest-collectors-EHFY2OQC.js → digest-collectors-MC5NWEUA.js} +12 -10
  98. package/dist/{digest-state-3KHZF7GD.js → digest-state-ZDPKR7MW.js} +18 -2
  99. package/dist/digest-state-ZDPKR7MW.js.map +1 -0
  100. package/dist/{ensure-site-WIDO6LGU.js → ensure-site-XESZHZEB.js} +39 -11
  101. package/dist/ensure-site-XESZHZEB.js.map +1 -0
  102. package/dist/{fleet-state-UEVZ3X3J.js → fleet-state-GJWGKTLD.js} +6 -4
  103. package/dist/{forms-notify-target-2LWF5C5B.js → forms-notify-target-4NPLX2UG.js} +5 -5
  104. package/dist/{freeze-A6MUCZU4.js → freeze-GUQ43EAU.js} +2 -2
  105. package/dist/{github-signals-GAHNOAHS.js → github-signals-DMU74SAX.js} +10 -10
  106. package/dist/{header-image-YN3AQRY3.js → header-image-BRTOLHJF.js} +6 -5
  107. package/dist/header-image-BRTOLHJF.js.map +1 -0
  108. package/dist/{health-endpoint-KVCJYWET.js → health-endpoint-UCLMSSCN.js} +8 -7
  109. package/dist/{health-endpoint-KVCJYWET.js.map → health-endpoint-UCLMSSCN.js.map} +1 -1
  110. package/dist/{health-mirror-EB76EERV.js → health-mirror-ONRT4DQ5.js} +7 -7
  111. package/dist/{import-airtable-TAKSN4NV.js → import-airtable-IIK2QUUF.js} +3 -3
  112. package/dist/index.d.ts +135 -4
  113. package/dist/index.js +68 -34
  114. package/dist/index.js.map +1 -1
  115. package/dist/{init-FFE4FCO2.js → init-3LMZTCVD.js} +19 -18
  116. package/dist/{init-FFE4FCO2.js.map → init-3LMZTCVD.js.map} +1 -1
  117. package/dist/{launch-4RVWPZGI.js → launch-XTDMC6LD.js} +209 -61
  118. package/dist/launch-XTDMC6LD.js.map +1 -0
  119. package/dist/{lighthouse-VHI77PFB.js → lighthouse-52WSPZ5D.js} +2 -2
  120. package/dist/{lighthouse-airtable-YMH4UA7R.js → lighthouse-airtable-3NOFB42B.js} +3 -3
  121. package/dist/{match-harness-2Q6ISEES.js → match-harness-OW45CB5H.js} +14 -7
  122. package/dist/match-harness-OW45CB5H.js.map +1 -0
  123. package/dist/migrate-NE3DUMTM.js +7 -0
  124. package/dist/{notify-IISTHNBR.js → notify-K3WVH3DM.js} +3 -3
  125. package/dist/{onboard-TM2KESGF.js → onboard-CZYBWVWH.js} +6 -6
  126. package/dist/{orchestrate-MWXENQOS.js → orchestrate-TGDFGY52.js} +8 -8
  127. package/dist/{parity-CAJ477PS.js → parity-AEKYSLN6.js} +5 -5
  128. package/dist/{pipeline-YROAY6DT.js → pipeline-UUPXOS2J.js} +3 -3
  129. package/dist/{preflight-4VQP7JJL.js → preflight-POZZI3GN.js} +9 -9
  130. package/dist/{prismic-ci-6GKKZGB5.js → prismic-ci-AHY5ULJR.js} +38 -8
  131. package/dist/prismic-ci-AHY5ULJR.js.map +1 -0
  132. package/dist/{prismic-models-W6CNLTWH.js → prismic-models-URYGODNY.js} +9 -9
  133. package/dist/prospect/types.d.ts +72 -0
  134. package/dist/prospect/types.js +1 -1
  135. package/dist/{prospect-audit-X4Z67LWW.js → prospect-audit-AJEE3Z4L.js} +16 -7
  136. package/dist/prospect-audit-AJEE3Z4L.js.map +1 -0
  137. package/dist/{prospect-audits-RVYEA4KH.js → prospect-audits-IZVQY3OQ.js} +12 -4
  138. package/dist/protection-audit-3J3UEOVT.js +413 -0
  139. package/dist/protection-audit-3J3UEOVT.js.map +1 -0
  140. package/dist/recipes/sync-configs.js +3 -3
  141. package/dist/{render-YGNGTX3W.js → render-MHUUHPLN.js} +36 -9
  142. package/dist/render-MHUUHPLN.js.map +1 -0
  143. package/dist/{render-from-row-IFSBHWDH.js → render-from-row-65DC6ZP3.js} +4 -4
  144. package/dist/{renovate-dispatch-U5Q52SKT.js → renovate-dispatch-ONJMBUHJ.js} +3 -3
  145. package/dist/{replay-PCSQOGCZ.js → replay-4FVFOMHH.js} +10 -23
  146. package/dist/replay-4FVFOMHH.js.map +1 -0
  147. package/dist/{report-4GFD4MW2.js → report-RC5LGPU3.js} +22 -20
  148. package/dist/report-RC5LGPU3.js.map +1 -0
  149. package/dist/{report-mirror-IP64R7B3.js → report-mirror-KKZZ6RMZ.js} +13 -9
  150. package/dist/{report-mirror-IP64R7B3.js.map → report-mirror-KKZZ6RMZ.js.map} +1 -1
  151. package/dist/{schema-2V7BGYVT.js → schema-KWUQEZ5O.js} +1 -1
  152. package/dist/schema-KWUQEZ5O.js.map +1 -0
  153. package/dist/{self-updating-MWXIZLL7.js → self-updating-RKKIYNV4.js} +8 -7
  154. package/dist/{self-updating-MWXIZLL7.js.map → self-updating-RKKIYNV4.js.map} +1 -1
  155. package/dist/{selftest-MAB2PBXQ.js → selftest-AFLR7YV7.js} +10 -10
  156. package/dist/site-lookup-DTOLJ4EA.js +27 -0
  157. package/dist/site-lookup-DTOLJ4EA.js.map +1 -0
  158. package/dist/{site-mirror-UKHREAD6.js → site-mirror-H2FYMN55.js} +23 -9
  159. package/dist/site-mirror-H2FYMN55.js.map +1 -0
  160. package/dist/{smoke-suite-7DGITCTZ.js → smoke-suite-BWHR3XX5.js} +8 -7
  161. package/dist/{smoke-suite-7DGITCTZ.js.map → smoke-suite-BWHR3XX5.js.map} +1 -1
  162. package/dist/{submissions-5GKJPCIH.js → submissions-II5J35QM.js} +4 -4
  163. package/dist/{submissions-3I3GAQAX.js → submissions-JDZP35JM.js} +49 -9
  164. package/dist/submissions-JDZP35JM.js.map +1 -0
  165. package/dist/{svelte-codemods-WSV2FGNG.js → svelte-codemods-QWGIRNN6.js} +6 -6
  166. package/dist/{sync-K3PKHVLH.js → sync-NKPDCMYW.js} +5 -5
  167. package/dist/{sync-configs-RRIHNLIJ.js → sync-configs-HGLQV55H.js} +6 -6
  168. package/dist/{upgrade-VTD6V4HS.js → upgrade-WZ44IARG.js} +6 -6
  169. package/dist/util/git.d.ts +48 -9
  170. package/dist/util/git.js +1 -1
  171. package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
  172. package/dist/{websites-TB4TCAWM.js → websites-6DHIHJ2D.js} +4 -2
  173. package/dist/{write-audits-to-airtable-53EVXKMU.js → write-audits-to-airtable-BIK6LNVC.js} +5 -5
  174. package/dist/write-audits-to-airtable-BIK6LNVC.js.map +1 -0
  175. package/package.json +3 -2
  176. package/dist/airtable-JGCPS7JH.js +0 -9
  177. package/dist/chunk-4AIYWWKV.js.map +0 -1
  178. package/dist/chunk-5UINJNAJ.js.map +0 -1
  179. package/dist/chunk-6VKYQATC.js +0 -1708
  180. package/dist/chunk-6VKYQATC.js.map +0 -1
  181. package/dist/chunk-7MKT4I5T.js +0 -62
  182. package/dist/chunk-7MKT4I5T.js.map +0 -1
  183. package/dist/chunk-AVEQUR44.js.map +0 -1
  184. package/dist/chunk-BVEDKMNG.js.map +0 -1
  185. package/dist/chunk-BYGPUUCT.js.map +0 -1
  186. package/dist/chunk-CWXTNPNZ.js.map +0 -1
  187. package/dist/chunk-DFKBTBO3.js.map +0 -1
  188. package/dist/chunk-FSGICFXB.js.map +0 -1
  189. package/dist/chunk-GASBX52O.js +0 -12
  190. package/dist/chunk-GASBX52O.js.map +0 -1
  191. package/dist/chunk-LM2NHRCJ.js.map +0 -1
  192. package/dist/chunk-QF6IQQFP.js.map +0 -1
  193. package/dist/chunk-TO7DJYLZ.js.map +0 -1
  194. package/dist/chunk-TR3HFH6J.js.map +0 -1
  195. package/dist/chunk-WJMPCVOR.js.map +0 -1
  196. package/dist/chunk-WSJ7RO54.js.map +0 -1
  197. package/dist/chunk-WVNDEYXL.js.map +0 -1
  198. package/dist/chunk-XHHMM5HX.js.map +0 -1
  199. package/dist/chunk-YLYXW5PH.js.map +0 -1
  200. package/dist/client-M2V6FHH5.js +0 -11
  201. package/dist/db-BBEM7CBX.js.map +0 -1
  202. package/dist/digest-65BOZ7AT.js.map +0 -1
  203. package/dist/digest-state-3KHZF7GD.js.map +0 -1
  204. package/dist/ensure-site-WIDO6LGU.js.map +0 -1
  205. package/dist/header-image-YN3AQRY3.js.map +0 -1
  206. package/dist/launch-4RVWPZGI.js.map +0 -1
  207. package/dist/match-harness-2Q6ISEES.js.map +0 -1
  208. package/dist/migrate-K4JETR36.js +0 -7
  209. package/dist/prismic-ci-6GKKZGB5.js.map +0 -1
  210. package/dist/prospect-audit-X4Z67LWW.js.map +0 -1
  211. package/dist/protection-audit-SJO53MEQ.js +0 -168
  212. package/dist/protection-audit-SJO53MEQ.js.map +0 -1
  213. package/dist/render-YGNGTX3W.js.map +0 -1
  214. package/dist/replay-PCSQOGCZ.js.map +0 -1
  215. package/dist/report-4GFD4MW2.js.map +0 -1
  216. package/dist/schema-2V7BGYVT.js.map +0 -1
  217. package/dist/site-mirror-UKHREAD6.js.map +0 -1
  218. package/dist/submissions-3I3GAQAX.js.map +0 -1
  219. /package/dist/{airtable-JGCPS7JH.js.map → airtable-SJV4ZSMS.js.map} +0 -0
  220. /package/dist/{announce-PGT4SZZB.js.map → announce-45HVP4J4.js.map} +0 -0
  221. /package/dist/{blux-4KNXUP2K.js.map → blux-RUFMNOUJ.js.map} +0 -0
  222. /package/dist/{bump-deps-C5G2EFBB.js.map → bump-deps-EL3TKMIL.js.map} +0 -0
  223. /package/dist/{chunk-XN37VTDB.js.map → chunk-37B4LVB3.js.map} +0 -0
  224. /package/dist/{chunk-WMBLR5VA.js.map → chunk-44SDGPGQ.js.map} +0 -0
  225. /package/dist/{chunk-22GKJO5F.js.map → chunk-6INOBNRO.js.map} +0 -0
  226. /package/dist/{chunk-5GUJ36TL.js.map → chunk-74QRWE6G.js.map} +0 -0
  227. /package/dist/{chunk-S7RYFUIK.js.map → chunk-CA6ADGOC.js.map} +0 -0
  228. /package/dist/{chunk-KZRR4NFG.js.map → chunk-D3CTOCUR.js.map} +0 -0
  229. /package/dist/{chunk-BCKEP5S6.js.map → chunk-ECDJDHKT.js.map} +0 -0
  230. /package/dist/{chunk-DMPHP7UP.js.map → chunk-EQQ4H6L3.js.map} +0 -0
  231. /package/dist/{chunk-Z2K4JPQ3.js.map → chunk-H445EH33.js.map} +0 -0
  232. /package/dist/{chunk-FUWFLAWQ.js.map → chunk-IMYL67JV.js.map} +0 -0
  233. /package/dist/{chunk-AEUABPAJ.js.map → chunk-OTEVKRAH.js.map} +0 -0
  234. /package/dist/{chunk-KQB2BX4V.js.map → chunk-P4A6D6HO.js.map} +0 -0
  235. /package/dist/{chunk-4763KQ6O.js.map → chunk-PXRN2MER.js.map} +0 -0
  236. /package/dist/{chunk-2LYOAB2E.js.map → chunk-RRE74VWI.js.map} +0 -0
  237. /package/dist/{chunk-ZQV7UQNE.js.map → chunk-SQ4FSNYL.js.map} +0 -0
  238. /package/dist/{chunk-6R56OKJZ.js.map → chunk-UIITN37J.js.map} +0 -0
  239. /package/dist/{chunk-CJ2RMTT7.js.map → chunk-VEIIUCYL.js.map} +0 -0
  240. /package/dist/{chunk-HTABUCF4.js.map → chunk-Y5BVR4HW.js.map} +0 -0
  241. /package/dist/{chunk-5EEMTJMT.js.map → chunk-ZH5LOQET.js.map} +0 -0
  242. /package/dist/{client-M2V6FHH5.js.map → client-L7NAUG6J.js.map} +0 -0
  243. /package/dist/{convert-to-pnpm-YTFREUJK.js.map → convert-to-pnpm-4DON7IFS.js.map} +0 -0
  244. /package/dist/{digest-collectors-EHFY2OQC.js.map → deadletter-G4M3HSVM.js.map} +0 -0
  245. /package/dist/{fleet-state-UEVZ3X3J.js.map → digest-collectors-MC5NWEUA.js.map} +0 -0
  246. /package/dist/{freeze-A6MUCZU4.js.map → fleet-state-GJWGKTLD.js.map} +0 -0
  247. /package/dist/{forms-notify-target-2LWF5C5B.js.map → forms-notify-target-4NPLX2UG.js.map} +0 -0
  248. /package/dist/{import-airtable-TAKSN4NV.js.map → freeze-GUQ43EAU.js.map} +0 -0
  249. /package/dist/{github-signals-GAHNOAHS.js.map → github-signals-DMU74SAX.js.map} +0 -0
  250. /package/dist/{health-mirror-EB76EERV.js.map → health-mirror-ONRT4DQ5.js.map} +0 -0
  251. /package/dist/{lighthouse-VHI77PFB.js.map → import-airtable-IIK2QUUF.js.map} +0 -0
  252. /package/dist/{lighthouse-airtable-YMH4UA7R.js.map → lighthouse-52WSPZ5D.js.map} +0 -0
  253. /package/dist/{migrate-K4JETR36.js.map → lighthouse-airtable-3NOFB42B.js.map} +0 -0
  254. /package/dist/{notify-IISTHNBR.js.map → migrate-NE3DUMTM.js.map} +0 -0
  255. /package/dist/{orchestrate-MWXENQOS.js.map → notify-K3WVH3DM.js.map} +0 -0
  256. /package/dist/{onboard-TM2KESGF.js.map → onboard-CZYBWVWH.js.map} +0 -0
  257. /package/dist/{parity-CAJ477PS.js.map → orchestrate-TGDFGY52.js.map} +0 -0
  258. /package/dist/{pipeline-YROAY6DT.js.map → parity-AEKYSLN6.js.map} +0 -0
  259. /package/dist/{prospect-audits-RVYEA4KH.js.map → pipeline-UUPXOS2J.js.map} +0 -0
  260. /package/dist/{preflight-4VQP7JJL.js.map → preflight-POZZI3GN.js.map} +0 -0
  261. /package/dist/{prismic-models-W6CNLTWH.js.map → prismic-models-URYGODNY.js.map} +0 -0
  262. /package/dist/{render-from-row-IFSBHWDH.js.map → prospect-audits-IZVQY3OQ.js.map} +0 -0
  263. /package/dist/{websites-TB4TCAWM.js.map → render-from-row-65DC6ZP3.js.map} +0 -0
  264. /package/dist/{renovate-dispatch-U5Q52SKT.js.map → renovate-dispatch-ONJMBUHJ.js.map} +0 -0
  265. /package/dist/{selftest-MAB2PBXQ.js.map → selftest-AFLR7YV7.js.map} +0 -0
  266. /package/dist/{submissions-5GKJPCIH.js.map → submissions-II5J35QM.js.map} +0 -0
  267. /package/dist/{svelte-codemods-WSV2FGNG.js.map → svelte-codemods-QWGIRNN6.js.map} +0 -0
  268. /package/dist/{sync-K3PKHVLH.js.map → sync-NKPDCMYW.js.map} +0 -0
  269. /package/dist/{sync-configs-RRIHNLIJ.js.map → sync-configs-HGLQV55H.js.map} +0 -0
  270. /package/dist/{upgrade-VTD6V4HS.js.map → upgrade-WZ44IARG.js.map} +0 -0
  271. /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
  272. /package/dist/{write-audits-to-airtable-53EVXKMU.js.map → websites-6DHIHJ2D.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db/deadletter.ts"],"sourcesContent":["import type { Db } from \"./client.js\";\nimport type { TurnstileVerification } from \"../forms/turnstile.js\";\n\n/** One captured-but-unresolved lead: the site lookup threw before the normal\n * pipeline could run (Phase 0 of #539 — persist before enrich). */\nexport type DeadLetterRow = {\n id: string;\n siteSlug: string;\n /** The raw wire payload, exactly as the handler received it. */\n payload: unknown;\n /** The Turnstile verification computed at receipt — tokens expire in 300s,\n * so replay reuses this answer rather than re-verifying. */\n turnstile: TurnstileVerification;\n error: string;\n receivedAt: string;\n};\n\nexport type DeadLetterInput = {\n siteSlug: string;\n payload: unknown;\n turnstile: TurnstileVerification;\n error: string;\n receivedAt: Date;\n};\n\n/** Opaque, collision-free id (mirrors newSubmissionId). */\nexport function newDeadLetterId(): string {\n return `dl_${crypto.randomUUID()}`;\n}\n\n/** Persist a lead the normal pipeline could not place. This is the LAST writer\n * on the ingest path — if it throws too, both stores are down and the caller's\n * 502 is honest. */\nexport async function createDeadLetter(db: Db, input: DeadLetterInput): Promise<{ id: string }> {\n const id = newDeadLetterId();\n await db\n .insertInto(\"submission_deadletter\")\n .values({\n id,\n site_slug: input.siteSlug,\n payload: JSON.stringify(input.payload),\n turnstile: JSON.stringify(input.turnstile),\n error: input.error,\n received_at: input.receivedAt.toISOString(),\n replayed_at: null,\n replay_outcome: null,\n replay_submission_id: null,\n })\n .execute();\n return { id };\n}\n\n/** Rows not yet replayed, oldest first — replay preserves arrival order so the\n * duplicate/velocity signals see submissions in the order they happened. */\nexport async function listUnreplayedDeadLetters(db: Db): Promise<DeadLetterRow[]> {\n const rows = await db\n .selectFrom(\"submission_deadletter\")\n .select([\"id\", \"site_slug\", \"payload\", \"turnstile\", \"error\", \"received_at\"])\n .where(\"replayed_at\", \"is\", null)\n // #786: an abandoned row is terminal by decision — it must leave the replay\n // queue, or the command it was meant to release stays at exit 1 forever.\n .where(\"abandoned_at\", \"is\", null)\n .orderBy(\"received_at\", \"asc\")\n .execute();\n return rows.map((r) => ({\n id: r.id,\n siteSlug: r.site_slug,\n payload: JSON.parse(r.payload) as unknown,\n turnstile: JSON.parse(r.turnstile) as TurnstileVerification,\n error: r.error,\n receivedAt: r.received_at,\n }));\n}\n\n/** Mark a row's replay TERMINAL — it will never be picked up again. Only call\n * for outcomes that re-running cannot improve (accepted, rejected). A replay\n * whose lookup threw again is left untouched so the next run retries it — and\n * since #645 so is `unknown-site`, because `ensure-site` can heal the missing\n * Turso row and make the same replay succeed. */\nexport async function markDeadLetterReplayed(\n db: Db,\n id: string,\n outcome: string,\n submissionId: string | null,\n now: Date,\n): Promise<void> {\n await db\n .updateTable(\"submission_deadletter\")\n .set({\n replayed_at: now.toISOString(),\n replay_outcome: outcome,\n replay_submission_id: submissionId,\n })\n .where(\"id\", \"=\", id)\n .execute();\n}\n\n/** Unreplayed rows per site slug — the input to the `deadletter` attention\n * collector (#645). Grouped in SQL rather than by listing and counting in JS:\n * the rows carry full lead payloads, and an alarm must never pull a client's\n * PII into a dashboard request just to learn how many rows there are. */\nexport async function countUnreplayedDeadLettersBySlug(\n db: Db,\n): Promise<ReadonlyMap<string, number>> {\n const rows = await db\n .selectFrom(\"submission_deadletter\")\n .select((eb) => [\"site_slug\", eb.fn.countAll<number>().as(\"n\")])\n .where(\"replayed_at\", \"is\", null)\n // #786: and out of the alarm count, which is the standing CRITICAL item.\n .where(\"abandoned_at\", \"is\", null)\n .groupBy(\"site_slug\")\n .execute();\n return new Map(rows.map((r) => [r.site_slug, Number(r.n)]));\n}\n\n/** Abandon dead letters as resolved-by-DECISION (#786).\n *\n * The escape hatch #785 left open. `unknown-site` is deliberately non-terminal\n * on replay so that healing and replaying are safe in either order — but that\n * means a slug which is genuinely dead, and still deployed and posting, grows\n * the queue without bound, holds `db replay-deadletters` at exit 1, and leaves\n * a standing CRITICAL cockpit item. The only alternative today is deleting\n * rows by hand, which destroys the very leads the decision was made about.\n *\n * Targets ONE slug or ONE row id, never \"everything\": a mistyped invocation\n * must not be able to write off the whole queue. Only rows that are still live\n * (neither replayed nor already abandoned) are touched, which makes the call\n * idempotent and keeps the FIRST decision — the one actually made — in place.\n * `reason` is required and non-blank, because an undocumented write-off is the\n * thing this exists to stop being necessary.\n *\n * Returns the ids it abandoned, so the CLI can name them rather than print a\n * bare count.\n */\nexport async function abandonDeadLetters(\n db: Db,\n opts: { slug?: string; id?: string; by: string; reason: string; now: Date },\n): Promise<string[]> {\n const bySlug = opts.slug !== undefined;\n const byId = opts.id !== undefined;\n if (bySlug === byId) {\n throw new Error(\"abandonDeadLetters: pass exactly one of slug or id\");\n }\n const reason = opts.reason.trim();\n if (reason === \"\") {\n throw new Error(\"abandonDeadLetters: a non-blank reason is required\");\n }\n\n // Read the target ids first so the caller can be told exactly what was\n // abandoned; the same predicate then guards the write.\n let q = db\n .selectFrom(\"submission_deadletter\")\n .select(\"id\")\n .where(\"replayed_at\", \"is\", null)\n .where(\"abandoned_at\", \"is\", null);\n q = bySlug ? q.where(\"site_slug\", \"=\", opts.slug!) : q.where(\"id\", \"=\", opts.id!);\n const ids = (await q.execute()).map((r) => r.id);\n if (ids.length === 0) return [];\n\n await db\n .updateTable(\"submission_deadletter\")\n .set({\n abandoned_at: opts.now.toISOString(),\n abandoned_by: opts.by,\n abandoned_reason: reason,\n })\n .where(\"id\", \"in\", ids)\n // Re-asserted on the write: between the read and here nothing else runs\n // (libSQL is single-writer), but the guard costs nothing and keeps the\n // write's meaning legible on its own.\n .where(\"replayed_at\", \"is\", null)\n .where(\"abandoned_at\", \"is\", null)\n .execute();\n return ids;\n}\n"],"mappings":";AA0BO,SAAS,kBAA0B;AACxC,SAAO,MAAM,OAAO,WAAW,CAAC;AAClC;AAKA,eAAsB,iBAAiB,IAAQ,OAAiD;AAC9F,QAAM,KAAK,gBAAgB;AAC3B,QAAM,GACH,WAAW,uBAAuB,EAClC,OAAO;AAAA,IACN;AAAA,IACA,WAAW,MAAM;AAAA,IACjB,SAAS,KAAK,UAAU,MAAM,OAAO;AAAA,IACrC,WAAW,KAAK,UAAU,MAAM,SAAS;AAAA,IACzC,OAAO,MAAM;AAAA,IACb,aAAa,MAAM,WAAW,YAAY;AAAA,IAC1C,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,sBAAsB;AAAA,EACxB,CAAC,EACA,QAAQ;AACX,SAAO,EAAE,GAAG;AACd;AAIA,eAAsB,0BAA0B,IAAkC;AAChF,QAAM,OAAO,MAAM,GAChB,WAAW,uBAAuB,EAClC,OAAO,CAAC,MAAM,aAAa,WAAW,aAAa,SAAS,aAAa,CAAC,EAC1E,MAAM,eAAe,MAAM,IAAI,EAG/B,MAAM,gBAAgB,MAAM,IAAI,EAChC,QAAQ,eAAe,KAAK,EAC5B,QAAQ;AACX,SAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACtB,IAAI,EAAE;AAAA,IACN,UAAU,EAAE;AAAA,IACZ,SAAS,KAAK,MAAM,EAAE,OAAO;AAAA,IAC7B,WAAW,KAAK,MAAM,EAAE,SAAS;AAAA,IACjC,OAAO,EAAE;AAAA,IACT,YAAY,EAAE;AAAA,EAChB,EAAE;AACJ;AAOA,eAAsB,uBACpB,IACA,IACA,SACA,cACA,KACe;AACf,QAAM,GACH,YAAY,uBAAuB,EACnC,IAAI;AAAA,IACH,aAAa,IAAI,YAAY;AAAA,IAC7B,gBAAgB;AAAA,IAChB,sBAAsB;AAAA,EACxB,CAAC,EACA,MAAM,MAAM,KAAK,EAAE,EACnB,QAAQ;AACb;AAMA,eAAsB,iCACpB,IACsC;AACtC,QAAM,OAAO,MAAM,GAChB,WAAW,uBAAuB,EAClC,OAAO,CAAC,OAAO,CAAC,aAAa,GAAG,GAAG,SAAiB,EAAE,GAAG,GAAG,CAAC,CAAC,EAC9D,MAAM,eAAe,MAAM,IAAI,EAE/B,MAAM,gBAAgB,MAAM,IAAI,EAChC,QAAQ,WAAW,EACnB,QAAQ;AACX,SAAO,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;AAC5D;AAqBA,eAAsB,mBACpB,IACA,MACmB;AACnB,QAAM,SAAS,KAAK,SAAS;AAC7B,QAAM,OAAO,KAAK,OAAO;AACzB,MAAI,WAAW,MAAM;AACnB,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE;AACA,QAAM,SAAS,KAAK,OAAO,KAAK;AAChC,MAAI,WAAW,IAAI;AACjB,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE;AAIA,MAAI,IAAI,GACL,WAAW,uBAAuB,EAClC,OAAO,IAAI,EACX,MAAM,eAAe,MAAM,IAAI,EAC/B,MAAM,gBAAgB,MAAM,IAAI;AACnC,MAAI,SAAS,EAAE,MAAM,aAAa,KAAK,KAAK,IAAK,IAAI,EAAE,MAAM,MAAM,KAAK,KAAK,EAAG;AAChF,QAAM,OAAO,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE;AAC/C,MAAI,IAAI,WAAW,EAAG,QAAO,CAAC;AAE9B,QAAM,GACH,YAAY,uBAAuB,EACnC,IAAI;AAAA,IACH,cAAc,KAAK,IAAI,YAAY;AAAA,IACnC,cAAc,KAAK;AAAA,IACnB,kBAAkB;AAAA,EACpB,CAAC,EACA,MAAM,MAAM,MAAM,GAAG,EAIrB,MAAM,eAAe,MAAM,IAAI,EAC/B,MAAM,gBAAgB,MAAM,IAAI,EAChC,QAAQ;AACX,SAAO;AACT;","names":[]}
@@ -46,6 +46,8 @@ async function readSiteConfig(sitePath) {
46
46
  const clean = routes.filter((r) => typeof r === "string").map((r) => r.trim()).filter((r) => r.length > 0);
47
47
  if (clean.length > 0) out.a11yRoutes = clean;
48
48
  }
49
+ const gate = cfg.gateServer;
50
+ if (gate === "dev" || gate === "preview") out.gateServer = gate;
49
51
  return out;
50
52
  }
51
53
 
@@ -240,4 +242,4 @@ export {
240
242
  readSiteConfig,
241
243
  lighthouseAudit
242
244
  };
243
- //# sourceMappingURL=chunk-2NQUCS7O.js.map
245
+ //# sourceMappingURL=chunk-BIYQN7FH.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/audits/lighthouse.ts","../src/audits/util/site-config.ts"],"sourcesContent":["import { readFile, writeFile, mkdtemp, rm, readdir } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AuditResult, Site } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { lighthouseConfig } from \"../configs/lighthouse.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { SpawnFn, SpawnResult } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { readSiteConfig } from \"./util/site-config.js\";\nimport { findFreePort, withFreePort } from \"../util/free-port.js\";\n\ntype ManifestEntry = {\n url: string;\n summary: Record<string, number>;\n htmlPath?: string;\n jsonPath?: string;\n};\n\ntype AssertionResult = {\n name: string;\n actual: number;\n expected: number;\n operator: string;\n passed: boolean;\n level: \"warn\" | \"error\";\n auditProperty?: string;\n auditId?: string;\n};\n\ntype NormalizedLhciResult = {\n summary: Record<string, number>;\n assertionsFailed: number;\n assertions: Array<{ category: string; level: \"warn\" | \"error\"; message: string }>;\n};\n\nasync function readJsonMaybe<T>(path: string): Promise<T | null> {\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\ntype LhrFile = {\n requestedUrl: string;\n finalUrl?: string;\n categories: Record<string, { score: number | null }>;\n};\n\n/**\n * Build manifest-equivalent entries by scanning the `.lighthouseci/` dir\n * for `lhr-*.json` files written by `lhci collect`. We used to read\n * `manifest.json` directly, but lhci 0.15+ no longer writes it — the\n * audit would silently return \"no manifest written\" against a perfectly\n * healthy run. Reproduced on caltex 2026-05-28 (0.10.5 dogfood).\n */\nasync function readLhrEntries(resultsDir: string): Promise<ManifestEntry[]> {\n const files = await readdir(resultsDir).catch(() => [] as string[]);\n const entries: ManifestEntry[] = [];\n for (const f of files) {\n if (!f.startsWith(\"lhr-\") || !f.endsWith(\".json\")) continue;\n const lhr = await readJsonMaybe<LhrFile>(join(resultsDir, f));\n if (!lhr || !lhr.categories) continue;\n const summary: Record<string, number> = {};\n for (const [k, v] of Object.entries(lhr.categories)) {\n if (typeof v?.score === \"number\") summary[k] = v.score;\n }\n entries.push({ url: lhr.requestedUrl, summary });\n }\n return entries;\n}\n\nfunction averageSummaries(entries: ManifestEntry[]): Record<string, number> {\n if (entries.length === 0) return {};\n const sums: Record<string, number> = {};\n const counts: Record<string, number> = {};\n for (const e of entries) {\n for (const [k, v] of Object.entries(e.summary ?? {})) {\n if (typeof v !== \"number\") continue;\n sums[k] = (sums[k] ?? 0) + v;\n counts[k] = (counts[k] ?? 0) + 1;\n }\n }\n const out: Record<string, number> = {};\n for (const k of Object.keys(sums)) {\n const total = sums[k] ?? 0;\n const count = counts[k] ?? 1;\n out[k] = total / count;\n }\n return out;\n}\n\nfunction categoryFromAssertion(a: AssertionResult): string {\n // `name` looks like \"categories:accessibility\" or \"audits:uses-http2\".\n const colonIdx = a.name.indexOf(\":\");\n return colonIdx >= 0 ? a.name.slice(colonIdx + 1) : a.name;\n}\n\nfunction messageForAssertion(a: AssertionResult): string {\n // `a.actual` is parsed from external lhci/Lighthouse JSON; a malformed or\n // missing value (not a number) would make `.toFixed` throw and crash the whole\n // audit. Guard it and fall back to a readable string instead.\n const actual = typeof a.actual === \"number\" ? a.actual.toFixed(2) : \"n/a\";\n return `${a.name} ${a.operator} ${a.expected} (actual: ${actual})`;\n}\n\n/** Shared tail: scan `.lighthouseci/` for lhr-*.json + assertion-results.json and\n * build the AuditResult. Identical for the checkout and deployed paths. */\nasync function parseLhciResults(\n resultsDir: string,\n label: string,\n raw: SpawnResult,\n): Promise<AuditResult> {\n const manifest = await readLhrEntries(resultsDir);\n\n if (manifest.length === 0) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"fail\",\n summary: `lighthouse: no lhr-*.json written (exit ${raw.code})${\n raw.stderr ? ` — ${raw.stderr.slice(0, 200)}` : \"\"\n }`,\n };\n }\n\n const assertionResults =\n (await readJsonMaybe<AssertionResult[]>(join(resultsDir, \"assertion-results.json\"))) ?? [];\n\n const failed = assertionResults.filter((a) => !a.passed);\n const assertions = failed.map((a) => ({\n category: categoryFromAssertion(a),\n level: a.level,\n message: messageForAssertion(a),\n }));\n\n const anyError = assertions.some((a) => a.level === \"error\");\n const anyWarn = assertions.some((a) => a.level === \"warn\");\n const status: AuditResult[\"status\"] = anyError ? \"fail\" : anyWarn ? \"warn\" : \"pass\";\n\n const normalized: NormalizedLhciResult = {\n summary: averageSummaries(manifest),\n assertionsFailed: failed.length,\n assertions,\n };\n\n const summary =\n status === \"pass\"\n ? \"lighthouse: all categories passing\"\n : `lighthouse: ${failed.length} assertion(s) failed`;\n\n return { audit: \"lighthouse\", site: label, status, summary, details: normalized };\n}\n\n/** Checkout mode (unchanged behavior): boot the site's vite dev server on a\n * pinned free port and audit the local fixtures/override URL. */\nasync function checkoutLighthouse(spawn: SpawnFn, site: Site, label: string): Promise<AuditResult> {\n const siteCfg = await readSiteConfig(site.path);\n // Allocate a free port + force vite to `--strictPort` so the spawned dev\n // server either binds the port we picked or fails loudly (caltex 2026-05-28\n // zombie-vite incident).\n const port = await findFreePort();\n const baseUrl = siteCfg.lighthouseUrl ?? lighthouseConfig.ci.collect.url[0];\n const resolvedConfig = {\n ...lighthouseConfig,\n ci: {\n ...lighthouseConfig.ci,\n collect: {\n ...lighthouseConfig.ci.collect,\n url: [withFreePort(baseUrl, port)],\n startServerCommand: `npm run vite:dev -- --port ${port} --strictPort`,\n },\n },\n };\n\n const configDir = await mkdtemp(join(tmpdir(), \"reddoor-lhci-\"));\n const configPath = join(configDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(site.path, \".lighthouseci\");\n await rm(resultsDir, { recursive: true, force: true });\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: site.path,\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(configDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n await rm(configDir, { recursive: true, force: true });\n\n return parseLhciResults(resultsDir, label, raw);\n}\n\n/** Deployed mode: audit a production URL directly — no checkout, no dev server.\n * Runs in a throwaway tmp cwd; uploads to the filesystem so fleet runs never\n * push 200 public reports to temporary-public-storage. */\nasync function deployedLighthouse(\n spawn: SpawnFn,\n deployedUrl: string,\n label: string,\n): Promise<AuditResult> {\n const workDir = await mkdtemp(join(tmpdir(), \"reddoor-lh-deployed-\"));\n const resolvedConfig = {\n ci: {\n // Deliberately NOT spread from lighthouseConfig.ci.collect: deployed mode\n // must omit startServerCommand and the dev-server settings entirely.\n collect: {\n url: [deployedUrl],\n // 3 runs to damp Lighthouse's run-to-run variance; parseLhciResults\n // averages the lhr files. (Median is a tracked future refinement.)\n numberOfRuns: 3,\n // Use devtools (applied) throttling instead of the desktop preset's\n // default `simulate` (Lantern). Lantern estimates LCP from the\n // dependency graph and can't model an LCP element revealed by a\n // JS-load opacity fade (e.g. gallerysonder's GridImage): on a fast\n // observed load it finds no LCP node and throws NO_LCP, nulling the\n // perf score, which the cockpit then coerces to a misleading 0.\n // devtools reads the real LCP paint event from the throttled trace\n // instead. Trade-off: slightly noisier run-to-run, damped by the\n // numberOfRuns:3 average above.\n settings: { preset: \"desktop\", throttlingMethod: \"devtools\", skipAudits: [\"uses-http2\"] },\n },\n assert: lighthouseConfig.ci.assert,\n upload: { target: \"filesystem\", outputDir: join(workDir, \"lhci-report\") },\n },\n };\n\n const configPath = join(workDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(workDir, \".lighthouseci\");\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: workDir,\n // 3 serial cold runs of a slow deployed site (lhci's own maxWaitForLoad\n // ~45-60s each) + first-use Chrome download can plausibly exceed 3 min →\n // SIGTERM → no lhr-*.json → spurious \"no scores\". Match the 5-min budget\n // the checkout path already gives (erp-industrials nightly flake,\n // morning-brief 2026-06-10 MEDIUM-F).\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(workDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n\n try {\n return await parseLhciResults(resultsDir, label, raw);\n } finally {\n await rm(workDir, { recursive: true, force: true });\n }\n}\n\nexport async function lighthouseAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n return site.deployedUrl\n ? deployedLighthouse(spawn, site.deployedUrl, label)\n : checkoutLighthouse(spawn, site, label);\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport type SiteConfig = {\n /** Override URL the lighthouse audit hits. Sites without the default\n * `/dev/a11y-fixtures` dev route set this to their homepage. */\n lighthouseUrl?: string;\n /**\n * Real site routes the a11y audit should axe-scan, IN ADDITION to the two\n * synthetic fixture pages it always covers. Opt-in per site and absent by\n * default: the audit runs with `--fail-on-violations` in the shared CI\n * workflow, and most of the fleet carries pre-existing a11y debt, so scanning\n * real routes everywhere at once would red every repo. A site adopts this once\n * its routes are clean. Omitted (never `[]`) when unset or unusable.\n */\n a11yRoutes?: string[];\n};\n\n/**\n * Read per-site overrides from `package.json#reddoor`. Returns `{}` on any\n * failure (missing file, malformed JSON, missing key, wrong type) so every\n * caller can safely fall back to its built-in default. Never throws.\n */\nexport async function readSiteConfig(sitePath: string): Promise<SiteConfig> {\n let raw: string;\n try {\n raw = await readFile(join(sitePath, \"package.json\"), \"utf-8\");\n } catch {\n return {};\n }\n let pkg: unknown;\n try {\n pkg = JSON.parse(raw);\n } catch {\n return {};\n }\n if (!pkg || typeof pkg !== \"object\") return {};\n const cfg = (pkg as { reddoor?: unknown }).reddoor;\n if (!cfg || typeof cfg !== \"object\") return {};\n\n const out: SiteConfig = {};\n const url = (cfg as { lighthouseUrl?: unknown }).lighthouseUrl;\n if (typeof url === \"string\" && url.length > 0) {\n out.lighthouseUrl = url;\n }\n const routes = (cfg as { a11yRoutes?: unknown }).a11yRoutes;\n if (Array.isArray(routes)) {\n // Junk entries are dropped rather than passed through — a `null` in the list\n // would become `page.goto(null)` inside the generated spec and fail the whole\n // audit for a typo. An all-junk list leaves the key omitted, so the caller's\n // \"no routes configured\" branch is the same shape as \"key absent\".\n const clean = routes\n .filter((r): r is string => typeof r === \"string\")\n .map((r) => r.trim())\n .filter((r) => r.length > 0);\n if (clean.length > 0) out.a11yRoutes = clean;\n }\n return out;\n}\n"],"mappings":"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","names":["readFile","join","readFile","join"]}
1
+ {"version":3,"sources":["../src/audits/lighthouse.ts","../src/audits/util/site-config.ts"],"sourcesContent":["import { readFile, writeFile, mkdtemp, rm, readdir } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AuditResult, Site } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { lighthouseConfig } from \"../configs/lighthouse.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { SpawnFn, SpawnResult } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { readSiteConfig } from \"./util/site-config.js\";\nimport { findFreePort, withFreePort } from \"../util/free-port.js\";\n\ntype ManifestEntry = {\n url: string;\n summary: Record<string, number>;\n htmlPath?: string;\n jsonPath?: string;\n};\n\ntype AssertionResult = {\n name: string;\n actual: number;\n expected: number;\n operator: string;\n passed: boolean;\n level: \"warn\" | \"error\";\n auditProperty?: string;\n auditId?: string;\n};\n\ntype NormalizedLhciResult = {\n summary: Record<string, number>;\n assertionsFailed: number;\n assertions: Array<{ category: string; level: \"warn\" | \"error\"; message: string }>;\n};\n\nasync function readJsonMaybe<T>(path: string): Promise<T | null> {\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\ntype LhrFile = {\n requestedUrl: string;\n finalUrl?: string;\n categories: Record<string, { score: number | null }>;\n};\n\n/**\n * Build manifest-equivalent entries by scanning the `.lighthouseci/` dir\n * for `lhr-*.json` files written by `lhci collect`. We used to read\n * `manifest.json` directly, but lhci 0.15+ no longer writes it — the\n * audit would silently return \"no manifest written\" against a perfectly\n * healthy run. Reproduced on caltex 2026-05-28 (0.10.5 dogfood).\n */\nasync function readLhrEntries(resultsDir: string): Promise<ManifestEntry[]> {\n const files = await readdir(resultsDir).catch(() => [] as string[]);\n const entries: ManifestEntry[] = [];\n for (const f of files) {\n if (!f.startsWith(\"lhr-\") || !f.endsWith(\".json\")) continue;\n const lhr = await readJsonMaybe<LhrFile>(join(resultsDir, f));\n if (!lhr || !lhr.categories) continue;\n const summary: Record<string, number> = {};\n for (const [k, v] of Object.entries(lhr.categories)) {\n if (typeof v?.score === \"number\") summary[k] = v.score;\n }\n entries.push({ url: lhr.requestedUrl, summary });\n }\n return entries;\n}\n\nfunction averageSummaries(entries: ManifestEntry[]): Record<string, number> {\n if (entries.length === 0) return {};\n const sums: Record<string, number> = {};\n const counts: Record<string, number> = {};\n for (const e of entries) {\n for (const [k, v] of Object.entries(e.summary ?? {})) {\n if (typeof v !== \"number\") continue;\n sums[k] = (sums[k] ?? 0) + v;\n counts[k] = (counts[k] ?? 0) + 1;\n }\n }\n const out: Record<string, number> = {};\n for (const k of Object.keys(sums)) {\n const total = sums[k] ?? 0;\n const count = counts[k] ?? 1;\n out[k] = total / count;\n }\n return out;\n}\n\nfunction categoryFromAssertion(a: AssertionResult): string {\n // `name` looks like \"categories:accessibility\" or \"audits:uses-http2\".\n const colonIdx = a.name.indexOf(\":\");\n return colonIdx >= 0 ? a.name.slice(colonIdx + 1) : a.name;\n}\n\nfunction messageForAssertion(a: AssertionResult): string {\n // `a.actual` is parsed from external lhci/Lighthouse JSON; a malformed or\n // missing value (not a number) would make `.toFixed` throw and crash the whole\n // audit. Guard it and fall back to a readable string instead.\n const actual = typeof a.actual === \"number\" ? a.actual.toFixed(2) : \"n/a\";\n return `${a.name} ${a.operator} ${a.expected} (actual: ${actual})`;\n}\n\n/** Shared tail: scan `.lighthouseci/` for lhr-*.json + assertion-results.json and\n * build the AuditResult. Identical for the checkout and deployed paths. */\nasync function parseLhciResults(\n resultsDir: string,\n label: string,\n raw: SpawnResult,\n): Promise<AuditResult> {\n const manifest = await readLhrEntries(resultsDir);\n\n if (manifest.length === 0) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"fail\",\n summary: `lighthouse: no lhr-*.json written (exit ${raw.code})${\n raw.stderr ? ` — ${raw.stderr.slice(0, 200)}` : \"\"\n }`,\n };\n }\n\n const assertionResults =\n (await readJsonMaybe<AssertionResult[]>(join(resultsDir, \"assertion-results.json\"))) ?? [];\n\n const failed = assertionResults.filter((a) => !a.passed);\n const assertions = failed.map((a) => ({\n category: categoryFromAssertion(a),\n level: a.level,\n message: messageForAssertion(a),\n }));\n\n const anyError = assertions.some((a) => a.level === \"error\");\n const anyWarn = assertions.some((a) => a.level === \"warn\");\n const status: AuditResult[\"status\"] = anyError ? \"fail\" : anyWarn ? \"warn\" : \"pass\";\n\n const normalized: NormalizedLhciResult = {\n summary: averageSummaries(manifest),\n assertionsFailed: failed.length,\n assertions,\n };\n\n const summary =\n status === \"pass\"\n ? \"lighthouse: all categories passing\"\n : `lighthouse: ${failed.length} assertion(s) failed`;\n\n return { audit: \"lighthouse\", site: label, status, summary, details: normalized };\n}\n\n/** Checkout mode (unchanged behavior): boot the site's vite dev server on a\n * pinned free port and audit the local fixtures/override URL. */\nasync function checkoutLighthouse(spawn: SpawnFn, site: Site, label: string): Promise<AuditResult> {\n const siteCfg = await readSiteConfig(site.path);\n // Allocate a free port + force vite to `--strictPort` so the spawned dev\n // server either binds the port we picked or fails loudly (caltex 2026-05-28\n // zombie-vite incident).\n const port = await findFreePort();\n const baseUrl = siteCfg.lighthouseUrl ?? lighthouseConfig.ci.collect.url[0];\n const resolvedConfig = {\n ...lighthouseConfig,\n ci: {\n ...lighthouseConfig.ci,\n collect: {\n ...lighthouseConfig.ci.collect,\n url: [withFreePort(baseUrl, port)],\n startServerCommand: `npm run vite:dev -- --port ${port} --strictPort`,\n },\n },\n };\n\n const configDir = await mkdtemp(join(tmpdir(), \"reddoor-lhci-\"));\n const configPath = join(configDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(site.path, \".lighthouseci\");\n await rm(resultsDir, { recursive: true, force: true });\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: site.path,\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(configDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n await rm(configDir, { recursive: true, force: true });\n\n return parseLhciResults(resultsDir, label, raw);\n}\n\n/** Deployed mode: audit a production URL directly — no checkout, no dev server.\n * Runs in a throwaway tmp cwd; uploads to the filesystem so fleet runs never\n * push 200 public reports to temporary-public-storage. */\nasync function deployedLighthouse(\n spawn: SpawnFn,\n deployedUrl: string,\n label: string,\n): Promise<AuditResult> {\n const workDir = await mkdtemp(join(tmpdir(), \"reddoor-lh-deployed-\"));\n const resolvedConfig = {\n ci: {\n // Deliberately NOT spread from lighthouseConfig.ci.collect: deployed mode\n // must omit startServerCommand and the dev-server settings entirely.\n collect: {\n url: [deployedUrl],\n // 3 runs to damp Lighthouse's run-to-run variance; parseLhciResults\n // averages the lhr files. (Median is a tracked future refinement.)\n numberOfRuns: 3,\n // Use devtools (applied) throttling instead of the desktop preset's\n // default `simulate` (Lantern). Lantern estimates LCP from the\n // dependency graph and can't model an LCP element revealed by a\n // JS-load opacity fade (e.g. gallerysonder's GridImage): on a fast\n // observed load it finds no LCP node and throws NO_LCP, nulling the\n // perf score, which the cockpit then coerces to a misleading 0.\n // devtools reads the real LCP paint event from the throttled trace\n // instead. Trade-off: slightly noisier run-to-run, damped by the\n // numberOfRuns:3 average above.\n settings: { preset: \"desktop\", throttlingMethod: \"devtools\", skipAudits: [\"uses-http2\"] },\n },\n assert: lighthouseConfig.ci.assert,\n upload: { target: \"filesystem\", outputDir: join(workDir, \"lhci-report\") },\n },\n };\n\n const configPath = join(workDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(workDir, \".lighthouseci\");\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: workDir,\n // 3 serial cold runs of a slow deployed site (lhci's own maxWaitForLoad\n // ~45-60s each) + first-use Chrome download can plausibly exceed 3 min →\n // SIGTERM → no lhr-*.json → spurious \"no scores\". Match the 5-min budget\n // the checkout path already gives (erp-industrials nightly flake,\n // morning-brief 2026-06-10 MEDIUM-F).\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(workDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n\n try {\n return await parseLhciResults(resultsDir, label, raw);\n } finally {\n await rm(workDir, { recursive: true, force: true });\n }\n}\n\nexport async function lighthouseAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n return site.deployedUrl\n ? deployedLighthouse(spawn, site.deployedUrl, label)\n : checkoutLighthouse(spawn, site, label);\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport type SiteConfig = {\n /** Override URL the lighthouse audit hits. Sites without the default\n * `/dev/a11y-fixtures` dev route set this to their homepage. */\n lighthouseUrl?: string;\n /**\n * Real site routes the a11y audit should axe-scan, IN ADDITION to the two\n * synthetic fixture pages it always covers. Opt-in per site and absent by\n * default: the audit runs with `--fail-on-violations` in the shared CI\n * workflow, and most of the fleet carries pre-existing a11y debt, so scanning\n * real routes everywhere at once would red every repo. A site adopts this once\n * its routes are clean. Omitted (never `[]`) when unset or unusable.\n */\n a11yRoutes?: string[];\n /**\n * Which server the browser gates run against: `\"dev\"` (the default — `vite\n * dev`) or `\"preview\"` (a real `vite build`, served by `vite preview`).\n *\n * Both gates measured the dev server, so the production bundle was built in\n * CI and then never opened by a browser — and dev does not merely fail to\n * reproduce some defects, it hides them. Module graph, code splitting,\n * minification and asset hashing are most of what \"hydration works\" means,\n * and a stylesheet can even be fetched under a different CSP directive in\n * each (#700).\n *\n * Opt-in, because a preview costs a `pnpm build` per run and because the\n * `/dev/*` fixture routes the axe scan targets are not guaranteed to survive\n * one — a site that flipped this without checking would trade a working gate\n * for one reporting every fixture as a missing route. Omitted (never\n * defaulted here) so each caller states its own default.\n *\n * Requires a `preview` script in the site's package.json; the starter has one.\n */\n gateServer?: \"dev\" | \"preview\";\n};\n\n/**\n * Read per-site overrides from `package.json#reddoor`. Returns `{}` on any\n * failure (missing file, malformed JSON, missing key, wrong type) so every\n * caller can safely fall back to its built-in default. Never throws.\n */\nexport async function readSiteConfig(sitePath: string): Promise<SiteConfig> {\n let raw: string;\n try {\n raw = await readFile(join(sitePath, \"package.json\"), \"utf-8\");\n } catch {\n return {};\n }\n let pkg: unknown;\n try {\n pkg = JSON.parse(raw);\n } catch {\n return {};\n }\n if (!pkg || typeof pkg !== \"object\") return {};\n const cfg = (pkg as { reddoor?: unknown }).reddoor;\n if (!cfg || typeof cfg !== \"object\") return {};\n\n const out: SiteConfig = {};\n const url = (cfg as { lighthouseUrl?: unknown }).lighthouseUrl;\n if (typeof url === \"string\" && url.length > 0) {\n out.lighthouseUrl = url;\n }\n const routes = (cfg as { a11yRoutes?: unknown }).a11yRoutes;\n if (Array.isArray(routes)) {\n // Junk entries are dropped rather than passed through — a `null` in the list\n // would become `page.goto(null)` inside the generated spec and fail the whole\n // audit for a typo. An all-junk list leaves the key omitted, so the caller's\n // \"no routes configured\" branch is the same shape as \"key absent\".\n const clean = routes\n .filter((r): r is string => typeof r === \"string\")\n .map((r) => r.trim())\n .filter((r) => r.length > 0);\n if (clean.length > 0) out.a11yRoutes = clean;\n }\n // Anything but the two known values reads as absent. This string reaches a\n // shell as part of a `webServer.command`, so a typo must fall back to the\n // default rather than be forwarded — `\"prod\"` would become `npm run prod`.\n const gate = (cfg as { gateServer?: unknown }).gateServer;\n if (gate === \"dev\" || gate === \"preview\") out.gateServer = gate;\n\n return out;\n}\n"],"mappings":"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","names":["readFile","join","readFile","join"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  withRecipe
3
- } from "./chunk-ZQV7UQNE.js";
3
+ } from "./chunk-SQ4FSNYL.js";
4
4
  import {
5
5
  bumpDep,
6
6
  readPackageJson,
@@ -150,4 +150,4 @@ export {
150
150
  selfCaretRange,
151
151
  onboard
152
152
  };
153
- //# sourceMappingURL=chunk-S7RYFUIK.js.map
153
+ //# sourceMappingURL=chunk-CA6ADGOC.js.map
@@ -0,0 +1,38 @@
1
+ // src/github/config.ts
2
+ import { execFileSync } from "child_process";
3
+ var defaultExecFileSync = (cmd, args, opts) => execFileSync(cmd, [...args], opts);
4
+ function ghAuthToken(exec = defaultExecFileSync) {
5
+ const { GITHUB_TOKEN: _github, GH_TOKEN: _gh, ...env } = process.env;
6
+ void _github;
7
+ void _gh;
8
+ try {
9
+ const out = exec("gh", ["auth", "token"], {
10
+ env,
11
+ encoding: "utf8",
12
+ stdio: ["ignore", "pipe", "ignore"],
13
+ timeout: 5e3
14
+ });
15
+ return out.trim() || null;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ function resolveToken(exec) {
21
+ const set = process.env.GITHUB_TOKEN;
22
+ if (set === void 0) return ghAuthToken(exec);
23
+ const trimmed = set.trim();
24
+ if (trimmed) return trimmed;
25
+ if (set === "") return null;
26
+ return ghAuthToken(exec);
27
+ }
28
+ function readGitHubConfig(opts = {}) {
29
+ const token = resolveToken(opts.execFileSync);
30
+ if (!token) return null;
31
+ const renovateToken = process.env.RENOVATE_TOKEN?.trim() || token;
32
+ return { token, renovateToken };
33
+ }
34
+
35
+ export {
36
+ readGitHubConfig
37
+ };
38
+ //# sourceMappingURL=chunk-CGQZFQAY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/github/config.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\n\nexport type GitHubConfig = {\n /** Broad PAT used by the tool's own `gh` calls (PRs, branch protection, secrets). */\n token: string;\n /** Narrow PAT stored per-repo as the RENOVATE_TOKEN secret. Falls back to `token`. */\n renovateToken: string;\n};\n\n/** The shape of `node:child_process`'s `execFileSync` that `ghAuthToken` needs —\n * injectable so a test never shells out (and never depends on the runner's\n * own keyring). */\nexport type ExecFileSyncFn = (\n cmd: string,\n args: readonly string[],\n opts: {\n env: NodeJS.ProcessEnv;\n encoding: \"utf8\";\n stdio: [\"ignore\", \"pipe\", \"ignore\"];\n timeout: number;\n },\n) => string;\n\nconst defaultExecFileSync: ExecFileSyncFn = (cmd, args, opts) => execFileSync(cmd, [...args], opts);\n\n/**\n * The token `gh` itself is logged in with (`gh auth token`), or null when `gh`\n * is missing, not logged in, or prints nothing. #665.\n *\n * Two traps, both from the issue:\n *\n * - `gh auth token` ECHOES `$GITHUB_TOKEN` / `$GH_TOKEN` back when either is\n * set, so the child runs with both stripped. Without that a dead file token\n * makes the good keyring token look dead as well — which is exactly how the\n * original failure was misdiagnosed once.\n * - The token is NOT validated by pinging an endpoint. A fine-grained token can\n * be refused by `/user` and still be fully working for the calls it is scoped\n * to; \"validating\" it there would declare working tokens dead.\n *\n * stderr is discarded (gh prints its \"not logged in\" hint there) and the call\n * is bounded so a wedged keychain prompt cannot hang a recipe.\n */\nexport function ghAuthToken(exec: ExecFileSyncFn = defaultExecFileSync): string | null {\n const { GITHUB_TOKEN: _github, GH_TOKEN: _gh, ...env } = process.env;\n void _github;\n void _gh;\n try {\n const out = exec(\"gh\", [\"auth\", \"token\"], {\n env,\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n timeout: 5_000,\n });\n return out.trim() || null;\n } catch {\n return null;\n }\n}\n\n/**\n * The broad token, from the environment or the keyring, or null for \"nobody has\n * one\". `GITHUB_TOKEN` has THREE states here, not two, and the third is the one\n * #665's fix collapsed by accident:\n *\n * - **Absent** — the recommended shape. Ask `gh auth token`.\n * - **Set to a value** — that value wins; `gh` is never asked.\n * - **Set to the EMPTY STRING** — `GITHUB_TOKEN=` in a shell, an env block, or\n * a test's `vi.stubEnv(\"GITHUB_TOKEN\", \"\")`. That is a DELIBERATE \"no token\",\n * and it must resolve to null rather than quietly reaching past the operator\n * to the keyring. Under the previous `?.trim() || ghAuthToken()` it did not:\n * `\"\"` is falsy, so an explicitly-disabled token fell through and a machine\n * that had run `gh auth login` silently resolved one anyway. The token gate\n * then read as configured on a developer's laptop and unconfigured in CI —\n * the same code taking different paths on ambient login state, which is how\n * `tests/cli/prismic-ci-command.test.ts` came to pass only where `gh` was\n * logged out.\n *\n * Whitespace-only is NOT the empty string and keeps falling through to the\n * keyring: `GITHUB_TOKEN=\" \"` is a mis-pasted `credentials.env` line, never\n * an instruction, and #665 exists precisely so a bad file value cannot override\n * a working keyring credential.\n */\nfunction resolveToken(exec?: ExecFileSyncFn): string | null {\n const set = process.env.GITHUB_TOKEN;\n if (set === undefined) return ghAuthToken(exec);\n const trimmed = set.trim();\n if (trimmed) return trimmed;\n if (set === \"\") return null; // explicitly disabled — do not consult the keyring\n return ghAuthToken(exec); // blank-but-not-empty: an accident, not a decision\n}\n\n/**\n * Read GitHub config from the environment (credentials.env is loaded into process.env by the CLI).\n *\n * `GITHUB_TOKEN` wins when set; when it is ABSENT the token comes from\n * `gh auth token` — the keyring `gh auth login` populated — so `GITHUB_TOKEN`\n * may be left OUT of `credentials.env` entirely, and should be: `gh.ts` hands\n * this token to `gh` as `GH_TOKEN`, so a set-but-dead file value actively\n * overrides a working keyring credential, and a file COPY of the keyring token\n * goes stale on the next `gh auth login/refresh/logout` (#665). Returns null\n * only when neither source has a token — the signal that git/GitHub features\n * aren't configured.\n *\n * `RENOVATE_TOKEN` falls back to the resolved token when unset (a narrower token\n * is recommended but optional).\n */\nexport function readGitHubConfig(\n opts: { execFileSync?: ExecFileSyncFn } = {},\n): GitHubConfig | null {\n const token = resolveToken(opts.execFileSync);\n if (!token) return null;\n const renovateToken = process.env.RENOVATE_TOKEN?.trim() || token;\n return { token, renovateToken };\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;AAuB7B,IAAM,sBAAsC,CAAC,KAAK,MAAM,SAAS,aAAa,KAAK,CAAC,GAAG,IAAI,GAAG,IAAI;AAmB3F,SAAS,YAAY,OAAuB,qBAAoC;AACrF,QAAM,EAAE,cAAc,SAAS,UAAU,KAAK,GAAG,IAAI,IAAI,QAAQ;AACjE,OAAK;AACL,OAAK;AACL,MAAI;AACF,UAAM,MAAM,KAAK,MAAM,CAAC,QAAQ,OAAO,GAAG;AAAA,MACxC;AAAA,MACA,UAAU;AAAA,MACV,OAAO,CAAC,UAAU,QAAQ,QAAQ;AAAA,MAClC,SAAS;AAAA,IACX,CAAC;AACD,WAAO,IAAI,KAAK,KAAK;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAyBA,SAAS,aAAa,MAAsC;AAC1D,QAAM,MAAM,QAAQ,IAAI;AACxB,MAAI,QAAQ,OAAW,QAAO,YAAY,IAAI;AAC9C,QAAM,UAAU,IAAI,KAAK;AACzB,MAAI,QAAS,QAAO;AACpB,MAAI,QAAQ,GAAI,QAAO;AACvB,SAAO,YAAY,IAAI;AACzB;AAiBO,SAAS,iBACd,OAA0C,CAAC,GACtB;AACrB,QAAM,QAAQ,aAAa,KAAK,YAAY;AAC5C,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,gBAAgB,QAAQ,IAAI,gBAAgB,KAAK,KAAK;AAC5D,SAAO,EAAE,OAAO,cAAc;AAChC;","names":[]}
@@ -1,5 +1,7 @@
1
1
  // src/configs/playwright-a11y.ts
2
2
  import { execFileSync } from "child_process";
3
+ import { readFileSync } from "fs";
4
+ import { join } from "path";
3
5
  import { defineConfig, devices } from "@playwright/test";
4
6
  var a11yRoutes = [
5
7
  { path: "/dev/a11y-fixtures", name: "a11y fixtures" },
@@ -29,6 +31,22 @@ function resolvePort() {
29
31
  return allocated;
30
32
  }
31
33
  var port = resolvePort();
34
+ function readGateServer(root) {
35
+ try {
36
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
37
+ const value = pkg?.reddoor?.gateServer;
38
+ return value === "preview" || value === "dev" ? value : "dev";
39
+ } catch {
40
+ return "dev";
41
+ }
42
+ }
43
+ function resolveGateServer() {
44
+ const fromEnv = process.env.REDDOOR_GATE_SERVER;
45
+ if (fromEnv === "preview" || fromEnv === "dev") return fromEnv;
46
+ return readGateServer(process.cwd());
47
+ }
48
+ var gateServer = resolveGateServer();
49
+ var previewing = gateServer === "preview";
32
50
  var playwrightA11yConfig = defineConfig({
33
51
  testDir: "tests",
34
52
  testMatch: /.*\.spec\.ts$/,
@@ -78,8 +96,15 @@ var playwrightA11yConfig = defineConfig({
78
96
  // --strictPort now only bites if the allocated port is taken in the window
79
97
  // between releasing and binding it, which is exactly the case worth failing
80
98
  // on.
81
- command: `npm run vite:dev -- --port ${port} --strictPort`,
82
- url: `http://localhost:${port}/dev/a11y-fixtures`,
99
+ //
100
+ // gateServer (#700): `preview` builds the site and serves the real bundle
101
+ // instead. The probe URL moves with it, and that move is the whole trap
102
+ // this had to avoid — `/dev/a11y-fixtures` is a dev fixture route with no
103
+ // guarantee of surviving a production build, so a flip that kept probing it
104
+ // would hang for the full webServer timeout on every opted-in site and
105
+ // report it as the site's failure. `/` is the one route every site serves.
106
+ command: previewing ? `npm run build && npm run preview -- --port ${port} --strictPort` : `npm run vite:dev -- --port ${port} --strictPort`,
107
+ url: previewing ? `http://localhost:${port}/` : `http://localhost:${port}/dev/a11y-fixtures`,
83
108
  // NEVER reuse (#524). This used to be `!process.env.CI`, so local runs
84
109
  // reused whatever answered the probe URL. The probe only asks "does this
85
110
  // respond?" — never "is this serving the code I am about to test?" — so a
@@ -95,7 +120,11 @@ var playwrightA11yConfig = defineConfig({
95
120
  // Because the port above is allocated rather than fixed, your own dev
96
121
  // server on 5173 keeps running untouched.
97
122
  reuseExistingServer: false,
98
- timeout: 12e4
123
+ // A vite boot is ~10-20s, so 120s covered it with room. A production build
124
+ // with prerendering is minutes, and the budget has to cover build + serve
125
+ // or the run dies on "Timed out waiting ... from config.webServer" while
126
+ // the build was still working.
127
+ timeout: previewing ? 5 * 6e4 : 12e4
99
128
  }
100
129
  });
101
130
  var playwright_a11y_default = playwrightA11yConfig;
@@ -103,6 +132,7 @@ var playwright_a11y_default = playwrightA11yConfig;
103
132
  export {
104
133
  a11yRoutes,
105
134
  smokeRoutes,
135
+ readGateServer,
106
136
  playwright_a11y_default
107
137
  };
108
- //# sourceMappingURL=chunk-YLYXW5PH.js.map
138
+ //# sourceMappingURL=chunk-CXICCLNM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/configs/playwright-a11y.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { defineConfig, devices, type PlaywrightTestConfig } from \"@playwright/test\";\n\nexport type A11yRoute = { path: string; name: string };\n\nexport const a11yRoutes: A11yRoute[] = [\n { path: \"/dev/a11y-fixtures\", name: \"a11y fixtures\" },\n { path: \"/dev/animate-in\", name: \"animate-in demo\" },\n];\n\n// Routes smoke-loaded for client-side (hydration) errors only — NOT axe-scanned.\n// Catches the class of bug where build + SSR succeed but client hydration throws\n// and blanks the page (data-dynamiq 2026-06-09: a Svelte 4->5 `run()` referenced\n// a `$state` declared after it → TDZ ReferenceError on hydrate). `/` is the one\n// route every site has; real routes carry a11y debt we don't gate on here, so we\n// assert only that they don't crash on hydrate.\nexport const smokeRoutes: A11yRoute[] = [{ path: \"/\", name: \"home\" }];\n\n// R1.1 (health-gate): the central `smoke` audit (src/audits/smoke.ts) allocates\n// a free port and passes it as REDDOOR_SMOKE_PORT so a zombie vite already\n// squatting the default 5173 can't silently hijack the run and green a stale\n// build. The per-site R1.1 config template honors it, but sites whose\n// playwright.config.ts merely re-exports this shared base (pre-R1.1 adopters\n// the smoke-suite recipe flags-but-never-rewrites) would otherwise ignore it —\n// so honor it here too and every re-exporter inherits the port binding on its\n// next package bump. Unset (local `pnpm test:smoke`) → the fixed 5173.\nconst smokePort = process.env.REDDOOR_SMOKE_PORT;\n\n/**\n * Allocate a free port SYNCHRONOUSLY, for the local path where nothing handed\n * us one. Same trick as src/util/free-port.ts (bind :0, read the assigned port,\n * release it) — but that is async, and this value is needed at module scope\n * while Playwright is still building the config object.\n *\n * It cannot be an async default export instead: sites consume this base by\n * SPREADING it (`{ ...base, use: { ...base.use } }` — see the smoke-suite\n * recipe template). Spreading a Promise yields none of its properties, so the\n * site would get a silently empty config — the exact false-green this whole\n * change exists to remove. The export must stay a plain object.\n *\n * A subprocess is the cost of that constraint: ~30-50ms, once per Playwright\n * run. On any failure we return null and the caller falls back to 5173, which\n * (with reuseExistingServer now false) degrades to a loud \"port already in use\"\n * rather than a silent wrong-server run.\n */\nfunction allocateFreePortSync(): string | null {\n try {\n const out = execFileSync(\n process.execPath,\n [\n \"-e\",\n 'const s=require(\"node:net\").createServer();s.on(\"error\",()=>process.exit(1));' +\n 's.listen(0,\"127.0.0.1\",()=>{const p=s.address().port;s.close(()=>process.stdout.write(String(p)))});',\n ],\n { encoding: \"utf8\", timeout: 5_000, stdio: [\"ignore\", \"pipe\", \"ignore\"] },\n ).trim();\n return /^\\d+$/.test(out) ? out : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the port this run binds to, ONCE per run rather than once per\n * evaluation of this file.\n *\n * REDDOOR_SMOKE_PORT (the central audit already allocated one) wins; otherwise\n * this run allocates its own. The old fallback was the fixed 5173 — the same\n * port a dev server sits on, which is what let `reuseExistingServer` silently\n * hijack local runs (#524).\n *\n * The allocation is pinned back into the environment, and that is the whole\n * point. Playwright re-evaluates the config file in EVERY worker process, not\n * just the main one. Allocating per evaluation gave each worker a different\n * port: the main process started the dev server on one, every worker aimed\n * `baseURL` at another, and the run died with ERR_CONNECTION_REFUSED against a\n * handful of ports nothing was ever serving. Workers are forked and inherit\n * this environment, so writing it back makes every later evaluation agree.\n *\n * It broke on the site suites and not on `audit --only a11y`, which writes its\n * own config and never reads this file — so nothing here caught it.\n */\nfunction resolvePort(): string {\n if (smokePort) return smokePort;\n const allocated = allocateFreePortSync() || \"5173\";\n process.env.REDDOOR_SMOKE_PORT = allocated;\n return allocated;\n}\n\nconst port = resolvePort();\n\nexport type GateServer = \"dev\" | \"preview\";\n\n/**\n * Which server this run puts under test, read from the site's own\n * `package.json#reddoor.gateServer`. Unknown values, a missing key, a missing\n * or malformed package.json all read as `\"dev\"` — this is evaluated in every\n * Playwright worker process at config load, so it must never throw, and the\n * value is interpolated into a shell command, so an unrecognized one must never\n * be forwarded (`\"prod\"` would run `npm run prod`).\n */\nexport function readGateServer(root: string): GateServer {\n try {\n const pkg = JSON.parse(readFileSync(join(root, \"package.json\"), \"utf-8\")) as {\n reddoor?: { gateServer?: unknown };\n };\n const value = pkg?.reddoor?.gateServer;\n return value === \"preview\" || value === \"dev\" ? value : \"dev\";\n } catch {\n return \"dev\";\n }\n}\n\n/**\n * REDDOOR_GATE_SERVER overrides the package.json key, so a one-off CI run can\n * exercise the built bundle without a commit — the same shape as\n * REDDOOR_SMOKE_PORT above. `process.cwd()` is the site root: Playwright is\n * invoked from there both by `pnpm test:smoke` and by the central smoke audit.\n */\nfunction resolveGateServer(): GateServer {\n const fromEnv = process.env.REDDOOR_GATE_SERVER;\n if (fromEnv === \"preview\" || fromEnv === \"dev\") return fromEnv;\n return readGateServer(process.cwd());\n}\n\nconst gateServer = resolveGateServer();\nconst previewing = gateServer === \"preview\";\n\n// NOTE: default export only — sites consume this as `import base from\n// \"@reddoorla/maintenance/configs/playwright-a11y\"` (or re-export the default).\n// The old `playwrightA11yConfig` named alias had zero importers and was removed.\nconst playwrightA11yConfig: PlaywrightTestConfig = defineConfig({\n testDir: \"tests\",\n testMatch: /.*\\.spec\\.ts$/,\n fullyParallel: true,\n forbidOnly: !!process.env.CI,\n retries: process.env.CI ? 2 : 0,\n reporter: process.env.CI ? \"github\" : \"list\",\n use: {\n baseURL: `http://localhost:${port}`,\n trace: \"on-first-retry\",\n // Emulate reduced motion fleet-wide: scrollIntoView lands instantly rather\n // than animating, so Playwright's actionability checks don't flake under\n // parallel load, and view transitions fall back to instant. Pairs with the\n // prefers-reduced-motion gate on scroll-behavior in every site's app.css.\n //\n // It MUST sit under `contextOptions` — `reducedMotion` is a\n // BrowserContextOptions member, not a top-level test option. reddoor-starter\n // carried it at the top level of `use` from 2026-06 until 2026-09-01, where\n // Playwright silently ignored it (unknown keys are dropped at runtime) and\n // `pnpm check` never flagged it because svelte-check does not typecheck\n // playwright.config.ts. The emulation was inert that whole time.\n contextOptions: { reducedMotion: \"reduce\" as const },\n },\n projects: [\n {\n name: \"chromium\",\n use: { ...devices[\"Desktop Chrome\"] },\n },\n ],\n webServer: {\n // Portable across pnpm and npm sites — pnpm respects `npm run` too.\n //\n // `--port ... --strictPort` in BOTH cases. It used to be applied only when\n // REDDOOR_SMOKE_PORT allocated one, on the reasoning that we should \"fail\n // loudly rather than let vite drift to a free port the baseURL doesn't\n // point at\" — but that argument covers the unset case just as well. 5173 is\n // equally a fixed port that `baseURL` and the readiness probe below are\n // pinned to, and vite left to itself drifts off it whenever something else\n // holds it.\n //\n // The symptom that exposed this: a non-vite process on 5173 sends vite to\n // 5174 while the probe keeps polling 5173, so the run dies on\n // \"Timed out waiting 120000ms from config.webServer\" — 120 seconds of\n // nothing, naming neither the port nor the squatter. With --strictPort it\n // is an immediate \"Port 5173 is already in use\".\n //\n // --strictPort now only bites if the allocated port is taken in the window\n // between releasing and binding it, which is exactly the case worth failing\n // on.\n //\n // gateServer (#700): `preview` builds the site and serves the real bundle\n // instead. The probe URL moves with it, and that move is the whole trap\n // this had to avoid — `/dev/a11y-fixtures` is a dev fixture route with no\n // guarantee of surviving a production build, so a flip that kept probing it\n // would hang for the full webServer timeout on every opted-in site and\n // report it as the site's failure. `/` is the one route every site serves.\n command: previewing\n ? `npm run build && npm run preview -- --port ${port} --strictPort`\n : `npm run vite:dev -- --port ${port} --strictPort`,\n url: previewing ? `http://localhost:${port}/` : `http://localhost:${port}/dev/a11y-fixtures`,\n // NEVER reuse (#524). This used to be `!process.env.CI`, so local runs\n // reused whatever answered the probe URL. The probe only asks \"does this\n // respond?\" — never \"is this serving the code I am about to test?\" — so a\n // dev server left open, or one whose tree changed under it after a\n // checkout, silently became the system under test. That fails in both\n // directions: a false red blamed on the code (beachfront 2026-08-12, where\n // it was investigated as a macOS-vs-Linux difference and written up as one\n // before being caught), and a false green where a passing suite ran against\n // an old build. CI already had it false, and that asymmetry is precisely\n // what made the failure read as a platform bug.\n //\n // The cost is a fresh vite boot per run (~10-20s against a ~2min suite).\n // Because the port above is allocated rather than fixed, your own dev\n // server on 5173 keeps running untouched.\n reuseExistingServer: false,\n // A vite boot is ~10-20s, so 120s covered it with room. A production build\n // with prerendering is minutes, and the budget has to cover build + serve\n // or the run dies on \"Timed out waiting ... from config.webServer\" while\n // the build was still working.\n timeout: previewing ? 5 * 60_000 : 120_000,\n },\n});\n\nexport default playwrightA11yConfig;\n"],"mappings":";AAAA,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,YAAY;AACrB,SAAS,cAAc,eAA0C;AAI1D,IAAM,aAA0B;AAAA,EACrC,EAAE,MAAM,sBAAsB,MAAM,gBAAgB;AAAA,EACpD,EAAE,MAAM,mBAAmB,MAAM,kBAAkB;AACrD;AAQO,IAAM,cAA2B,CAAC,EAAE,MAAM,KAAK,MAAM,OAAO,CAAC;AAUpE,IAAM,YAAY,QAAQ,IAAI;AAmB9B,SAAS,uBAAsC;AAC7C,MAAI;AACF,UAAM,MAAM;AAAA,MACV,QAAQ;AAAA,MACR;AAAA,QACE;AAAA,QACA;AAAA,MAEF;AAAA,MACA,EAAE,UAAU,QAAQ,SAAS,KAAO,OAAO,CAAC,UAAU,QAAQ,QAAQ,EAAE;AAAA,IAC1E,EAAE,KAAK;AACP,WAAO,QAAQ,KAAK,GAAG,IAAI,MAAM;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAsBA,SAAS,cAAsB;AAC7B,MAAI,UAAW,QAAO;AACtB,QAAM,YAAY,qBAAqB,KAAK;AAC5C,UAAQ,IAAI,qBAAqB;AACjC,SAAO;AACT;AAEA,IAAM,OAAO,YAAY;AAYlB,SAAS,eAAe,MAA0B;AACvD,MAAI;AACF,UAAM,MAAM,KAAK,MAAM,aAAa,KAAK,MAAM,cAAc,GAAG,OAAO,CAAC;AAGxE,UAAM,QAAQ,KAAK,SAAS;AAC5B,WAAO,UAAU,aAAa,UAAU,QAAQ,QAAQ;AAAA,EAC1D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQA,SAAS,oBAAgC;AACvC,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,YAAY,aAAa,YAAY,MAAO,QAAO;AACvD,SAAO,eAAe,QAAQ,IAAI,CAAC;AACrC;AAEA,IAAM,aAAa,kBAAkB;AACrC,IAAM,aAAa,eAAe;AAKlC,IAAM,uBAA6C,aAAa;AAAA,EAC9D,SAAS;AAAA,EACT,WAAW;AAAA,EACX,eAAe;AAAA,EACf,YAAY,CAAC,CAAC,QAAQ,IAAI;AAAA,EAC1B,SAAS,QAAQ,IAAI,KAAK,IAAI;AAAA,EAC9B,UAAU,QAAQ,IAAI,KAAK,WAAW;AAAA,EACtC,KAAK;AAAA,IACH,SAAS,oBAAoB,IAAI;AAAA,IACjC,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAYP,gBAAgB,EAAE,eAAe,SAAkB;AAAA,EACrD;AAAA,EACA,UAAU;AAAA,IACR;AAAA,MACE,MAAM;AAAA,MACN,KAAK,EAAE,GAAG,QAAQ,gBAAgB,EAAE;AAAA,IACtC;AAAA,EACF;AAAA,EACA,WAAW;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,IA2BT,SAAS,aACL,8CAA8C,IAAI,kBAClD,8BAA8B,IAAI;AAAA,IACtC,KAAK,aAAa,oBAAoB,IAAI,MAAM,oBAAoB,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAexE,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA,IAKrB,SAAS,aAAa,IAAI,MAAS;AAAA,EACrC;AACF,CAAC;AAED,IAAO,0BAAQ;","names":[]}
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  REPORT_LIST_COLUMNS
3
- } from "./chunk-WVNDEYXL.js";
3
+ } from "./chunk-GEUC5VGM.js";
4
4
  import {
5
5
  mapReportRecord,
6
6
  mapWebsiteRecord
7
- } from "./chunk-6R56OKJZ.js";
7
+ } from "./chunk-UIITN37J.js";
8
8
 
9
9
  // src/db/parity.ts
10
10
  var SKIP_COLUMNS = /* @__PURE__ */ new Set([
@@ -129,4 +129,4 @@ export {
129
129
  checkFleetParity,
130
130
  formatParityResult
131
131
  };
132
- //# sourceMappingURL=chunk-KZRR4NFG.js.map
132
+ //# sourceMappingURL=chunk-D3CTOCUR.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  withRecipe
3
- } from "./chunk-ZQV7UQNE.js";
3
+ } from "./chunk-SQ4FSNYL.js";
4
4
  import {
5
5
  readPackageJson,
6
6
  writePackageJson
@@ -32,7 +32,7 @@ function rewriteScriptsForPnpm(scripts) {
32
32
  }
33
33
 
34
34
  // src/recipes/convert-to-pnpm.ts
35
- var DEFAULT_PNPM_VERSION = "10.33.1";
35
+ var DEFAULT_PNPM_VERSION = "11.11.0";
36
36
  async function exists(path) {
37
37
  try {
38
38
  await stat(path);
@@ -96,4 +96,4 @@ async function convertToPnpm(site, opts = {}) {
96
96
  export {
97
97
  convertToPnpm
98
98
  };
99
- //# sourceMappingURL=chunk-CWXTNPNZ.js.map
99
+ //# sourceMappingURL=chunk-DVJNYPFR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/recipes/convert-to-pnpm.ts","../src/recipes/convert-to-pnpm/script-rewrites.ts"],"sourcesContent":["import { rm, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../types.js\";\nimport { readPackageJson, writePackageJson, type PackageJsonLike } from \"../util/pkg.js\";\nimport { defaultSpawn, type SpawnFn } from \"../audits/util/spawn.js\";\nimport { rewriteScriptsForPnpm } from \"./convert-to-pnpm/script-rewrites.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\nexport type ConvertToPnpmOptions = {\n spawn?: SpawnFn;\n /** Version string written into package.json's `packageManager` field.\n * Defaults to the version baked into this package's own pnpm setup. */\n pnpmVersion?: string;\n};\n\n/** Version this recipe stamps into a converted site's `packageManager` field.\n * Sites can override it per-recipe via `pnpmVersion`.\n *\n * Hand-typed, and therefore WATCHED: the guard in\n * `tests/recipes/convert-to-pnpm.test.ts` reads this package's own\n * `packageManager` field and fails the moment the two disagree. It is exported\n * for that guard — a constant nothing can read is a constant nothing can check.\n *\n * The comment this replaces asserted the value was \"kept in sync with\n * package.json\". It was not. It read `10.33.1` for roughly a year while this\n * repo and all 24 pinned fleet repos ran `pnpm@11.11.0`, so every site the\n * recipe converted was born a year behind the fleet (#835). Nothing caught it,\n * because a stale pin is a plausible number whose install succeeds. */\nexport const DEFAULT_PNPM_VERSION = \"11.11.0\";\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\ntype Plan = { hasNpmLock: boolean; hasYarnLock: boolean };\n\nexport async function convertToPnpm(\n site: Site,\n opts: ConvertToPnpmOptions = {},\n): Promise<RecipeResult> {\n const spawn = opts.spawn ?? defaultSpawn;\n const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;\n\n const pnpmLockPath = join(site.path, \"pnpm-lock.yaml\");\n const npmLockPath = join(site.path, \"package-lock.json\");\n const yarnLockPath = join(site.path, \"yarn.lock\");\n\n return withRecipe<Plan>({\n name: \"convert-to-pnpm\",\n site,\n plan: async () => {\n if (await exists(pnpmLockPath)) {\n return { kind: \"noop\", notes: \"site already has pnpm-lock.yaml\" };\n }\n const hasNpmLock = await exists(npmLockPath);\n const hasYarnLock = await exists(yarnLockPath);\n if (!hasNpmLock && !hasYarnLock) {\n return {\n kind: \"noop\",\n notes: \"no convertible lockfile (package-lock.json or yarn.lock) at site root\",\n };\n }\n return { kind: \"apply\", plan: { hasNpmLock, hasYarnLock } };\n },\n apply: async ({ hasNpmLock, hasYarnLock }, { commit, cwd }) => {\n // Step 1: remove the npm/yarn lockfile(s).\n if (hasNpmLock) await rm(npmLockPath, { force: true });\n if (hasYarnLock) await rm(yarnLockPath, { force: true });\n const sourceLock = hasNpmLock ? \"package-lock.json\" : \"yarn.lock\";\n await commit(`chore(pnpm): remove ${sourceLock}`);\n\n // Step 2: pin packageManager + rewrite scripts (single commit — they\n // both touch package.json).\n const pkgPath = join(cwd, \"package.json\");\n const pkg = await readPackageJson(pkgPath);\n const next: PackageJsonLike = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };\n\n if (pkg.scripts && typeof pkg.scripts === \"object\") {\n const { scripts: rewritten, changedCount } = rewriteScriptsForPnpm(\n pkg.scripts as Record<string, string>,\n );\n if (changedCount > 0) {\n next.scripts = rewritten;\n }\n }\n\n await writePackageJson(pkgPath, next);\n await commit(\"chore(pnpm): pin packageManager + rewrite npm scripts\");\n\n // Step 3: remove any existing flat node_modules from a prior npm/yarn run\n // before pnpm installs. Sharing a node_modules across package managers\n // produces phantom-dep resolution issues (pnpm's nested layout disagrees\n // with what's already on disk). node_modules is gitignored on every\n // reddoor site so this doesn't dirty the tree.\n await rm(join(cwd, \"node_modules\"), { recursive: true, force: true });\n\n // Step 4: run pnpm install to materialize pnpm-lock.yaml.\n const installResult = await spawn(\"pnpm\", [\"install\"], { cwd, streaming: true });\n if (installResult.code !== 0) {\n return { kind: \"failed\", notes: `pnpm install failed (exit ${installResult.code})` };\n }\n\n await commit(\"chore(pnpm): add pnpm-lock.yaml\");\n return { kind: \"ok\" };\n },\n });\n}\n","/**\n * Rewrite a single package.json script value to use pnpm equivalents\n * where the substitution is safe. Conservative on purpose: we only touch\n * patterns whose semantics are identical under pnpm.\n *\n * - `npm run <token>` → `pnpm run <token>` (identical behavior)\n * - `npx <token>` → `pnpm dlx <token>` (identical behavior in pnpm 7+)\n *\n * Intentionally NOT rewritten:\n * - `npm install`, `npm install <pkg>`, `npm install --save-dev <pkg>` —\n * subtle flag mapping (e.g. `--save-dev` → `-D`) and edge cases like\n * `--save-exact` / `--save-optional`. Better to leave for an operator\n * eyeball than to silently mis-translate.\n * - Hyphenated identifiers like `npm-check-updates` (word-boundary protected).\n * - `concurrently \"npm:scriptName\"` shorthand syntax — it isn't actually\n * running npm; it's a concurrently-specific script reference.\n */\nexport function rewriteScriptForPnpm(script: string): string {\n let out = script;\n // `npm run <name>` → `pnpm run <name>`. \\b before npm prevents\n // matching inside hyphenated identifiers. Lookahead `(?=\\s)` after run\n // ensures we don't match `runner`.\n out = out.replace(/\\bnpm run(?=\\s)/g, \"pnpm run\");\n // `npx ` → `pnpm dlx `. \\b before npx prevents matching `npx-something`.\n out = out.replace(/\\bnpx(?=\\s)/g, \"pnpm dlx\");\n return out;\n}\n\n/**\n * Rewrite every entry in a package.json `scripts` map. Returns the new\n * map alongside a count of scripts that were actually changed.\n */\nexport function rewriteScriptsForPnpm(scripts: Record<string, string>): {\n scripts: Record<string, string>;\n changedCount: number;\n} {\n const next: Record<string, string> = {};\n let changedCount = 0;\n for (const [name, value] of Object.entries(scripts)) {\n const rewritten = rewriteScriptForPnpm(value);\n next[name] = rewritten;\n if (rewritten !== value) changedCount++;\n }\n return { scripts: next, changedCount };\n}\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,IAAI,YAAY;AACzB,SAAS,YAAY;;;ACgBd,SAAS,qBAAqB,QAAwB;AAC3D,MAAI,MAAM;AAIV,QAAM,IAAI,QAAQ,oBAAoB,UAAU;AAEhD,QAAM,IAAI,QAAQ,gBAAgB,UAAU;AAC5C,SAAO;AACT;AAMO,SAAS,sBAAsB,SAGpC;AACA,QAAM,OAA+B,CAAC;AACtC,MAAI,eAAe;AACnB,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AACnD,UAAM,YAAY,qBAAqB,KAAK;AAC5C,SAAK,IAAI,IAAI;AACb,QAAI,cAAc,MAAO;AAAA,EAC3B;AACA,SAAO,EAAE,SAAS,MAAM,aAAa;AACvC;;;ADhBO,IAAM,uBAAuB;AAEpC,eAAe,OAAO,MAAgC;AACpD,MAAI;AACF,UAAM,KAAK,IAAI;AACf,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAIA,eAAsB,cACpB,MACA,OAA6B,CAAC,GACP;AACvB,QAAM,QAAQ,KAAK,SAAS;AAC5B,QAAM,cAAc,KAAK,eAAe;AAExC,QAAM,eAAe,KAAK,KAAK,MAAM,gBAAgB;AACrD,QAAM,cAAc,KAAK,KAAK,MAAM,mBAAmB;AACvD,QAAM,eAAe,KAAK,KAAK,MAAM,WAAW;AAEhD,SAAO,WAAiB;AAAA,IACtB,MAAM;AAAA,IACN;AAAA,IACA,MAAM,YAAY;AAChB,UAAI,MAAM,OAAO,YAAY,GAAG;AAC9B,eAAO,EAAE,MAAM,QAAQ,OAAO,kCAAkC;AAAA,MAClE;AACA,YAAM,aAAa,MAAM,OAAO,WAAW;AAC3C,YAAM,cAAc,MAAM,OAAO,YAAY;AAC7C,UAAI,CAAC,cAAc,CAAC,aAAa;AAC/B,eAAO;AAAA,UACL,MAAM;AAAA,UACN,OAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO,EAAE,MAAM,SAAS,MAAM,EAAE,YAAY,YAAY,EAAE;AAAA,IAC5D;AAAA,IACA,OAAO,OAAO,EAAE,YAAY,YAAY,GAAG,EAAE,QAAQ,IAAI,MAAM;AAE7D,UAAI,WAAY,OAAM,GAAG,aAAa,EAAE,OAAO,KAAK,CAAC;AACrD,UAAI,YAAa,OAAM,GAAG,cAAc,EAAE,OAAO,KAAK,CAAC;AACvD,YAAM,aAAa,aAAa,sBAAsB;AACtD,YAAM,OAAO,uBAAuB,UAAU,EAAE;AAIhD,YAAM,UAAU,KAAK,KAAK,cAAc;AACxC,YAAM,MAAM,MAAM,gBAAgB,OAAO;AACzC,YAAM,OAAwB,EAAE,GAAG,KAAK,gBAAgB,QAAQ,WAAW,GAAG;AAE9E,UAAI,IAAI,WAAW,OAAO,IAAI,YAAY,UAAU;AAClD,cAAM,EAAE,SAAS,WAAW,aAAa,IAAI;AAAA,UAC3C,IAAI;AAAA,QACN;AACA,YAAI,eAAe,GAAG;AACpB,eAAK,UAAU;AAAA,QACjB;AAAA,MACF;AAEA,YAAM,iBAAiB,SAAS,IAAI;AACpC,YAAM,OAAO,uDAAuD;AAOpE,YAAM,GAAG,KAAK,KAAK,cAAc,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAGpE,YAAM,gBAAgB,MAAM,MAAM,QAAQ,CAAC,SAAS,GAAG,EAAE,KAAK,WAAW,KAAK,CAAC;AAC/E,UAAI,cAAc,SAAS,GAAG;AAC5B,eAAO,EAAE,MAAM,UAAU,OAAO,6BAA6B,cAAc,IAAI,IAAI;AAAA,MACrF;AAEA,YAAM,OAAO,iCAAiC;AAC9C,aAAO,EAAE,MAAM,KAAK;AAAA,IACtB;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -1,6 +1,7 @@
1
1
  // src/util/git.ts
2
2
  import { execFile } from "child_process";
3
3
  import { promisify } from "util";
4
+ import { realpath } from "fs/promises";
4
5
  var exec = promisify(execFile);
5
6
  async function git(cwd, args) {
6
7
  return exec("git", args, { cwd, env: process.env });
@@ -80,37 +81,79 @@ function sameOwnerRepo(a, b) {
80
81
  if (na === null || nb === null) return false;
81
82
  return na.toLowerCase() === nb.toLowerCase();
82
83
  }
84
+ var GITHUB_REMOTE_HOSTS = /* @__PURE__ */ new Set(["github.com", "www.github.com", "ssh.github.com"]);
83
85
  function parseOwnerRepo(remoteUrl) {
84
- const trimmed = remoteUrl.trim().replace(/\.git$/, "").replace(/\/$/, "");
85
- const scp = trimmed.match(/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:(.+)$/);
86
- const path = scp ? scp[1] : trimmed.replace(/^https?:\/\/[^/]+\//, "");
87
- const segments = path.split("/").filter(Boolean);
88
- if (segments.length < 2) return null;
89
- return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`;
86
+ const raw = remoteUrl.trim();
87
+ if (raw.length === 0) return null;
88
+ if (raw.includes("..")) return null;
89
+ const scp = raw.match(/^(?:[A-Za-z0-9._-]+@)?([A-Za-z0-9._-]+):(?!\/)(.+)$/);
90
+ let url;
91
+ try {
92
+ url = new URL(scp ? `ssh://${scp[1]}/${scp[2]}` : raw);
93
+ } catch {
94
+ return null;
95
+ }
96
+ if (!GITHUB_REMOTE_HOSTS.has(url.hostname.toLowerCase())) return null;
97
+ const segments = url.pathname.replace(/\.git$/, "").split("/").filter(Boolean);
98
+ if (segments.length !== 2) return null;
99
+ const ownerRepo = `${segments[0]}/${segments[1]}`;
100
+ return isOwnerRepo(ownerRepo) ? ownerRepo : null;
90
101
  }
91
102
  async function getRemoteUrl(cwd) {
92
103
  const { stdout } = await git(cwd, ["remote", "get-url", "origin"]);
93
104
  return stdout.trim();
94
105
  }
106
+ function isNoSuchRemote(err) {
107
+ const e = err;
108
+ return e.code === 2 && /No such remote/i.test(e.stderr ?? "");
109
+ }
110
+ function isNotAWorkTree(err) {
111
+ const e = err;
112
+ return e.code === 128 && /not a git repository/i.test(e.stderr ?? "");
113
+ }
114
+ async function repoToplevel(cwd) {
115
+ try {
116
+ const { stdout } = await git(cwd, ["rev-parse", "--show-toplevel"]);
117
+ return await realpath(stdout.trim());
118
+ } catch (err) {
119
+ if (isNotAWorkTree(err)) return null;
120
+ throw err;
121
+ }
122
+ }
95
123
  async function resolveOwnerRepo(site) {
96
- if (site.gitRepo) {
97
- if (!isOwnerRepo(site.gitRepo)) {
124
+ const declared = site.gitRepo?.trim();
125
+ if (declared) {
126
+ if (!isOwnerRepo(declared)) {
98
127
  throw new Error(
99
128
  `refusing to act on malformed repo identity: expected "owner/repo", got ${JSON.stringify(site.gitRepo)}`
100
129
  );
101
130
  }
102
- return site.gitRepo;
131
+ return declared;
132
+ }
133
+ const toplevel = await repoToplevel(site.path);
134
+ if (toplevel === null) return null;
135
+ if (await realpath(site.path) !== toplevel) {
136
+ let enclosing = "";
137
+ try {
138
+ const id = parseOwnerRepo(await getRemoteUrl(site.path));
139
+ if (id !== null) enclosing = ` \u2014 ${id}`;
140
+ } catch {
141
+ }
142
+ throw new Error(
143
+ `refusing to derive a write identity from ${JSON.stringify(site.path)}: it is not the root of a git repository. git walks up to the checkout at ${toplevel}${enclosing}, which is not the directory that was asked about \u2014 cd to the repository root, or set the repo identity explicitly.`
144
+ );
103
145
  }
104
- let fromOrigin;
146
+ let originUrl;
105
147
  try {
106
- fromOrigin = parseOwnerRepo(await getRemoteUrl(site.path));
107
- } catch {
108
- return null;
148
+ originUrl = await getRemoteUrl(site.path);
149
+ } catch (err) {
150
+ if (isNoSuchRemote(err)) return null;
151
+ throw err;
109
152
  }
110
- if (fromOrigin === null) return null;
111
- if (!isOwnerRepo(fromOrigin)) {
153
+ const fromOrigin = parseOwnerRepo(originUrl);
154
+ if (fromOrigin === null) {
112
155
  throw new Error(
113
- `refusing to act on malformed repo identity from origin: ${JSON.stringify(fromOrigin)}`
156
+ `could not determine a GitHub owner/repo from origin ${JSON.stringify(originUrl)} \u2014 expected https://github.com/<owner>/<repo>, git@github.com:<owner>/<repo> or ssh://git@github.com/<owner>/<repo>; refusing to guess a write identity`
114
157
  );
115
158
  }
116
159
  return fromOrigin;
@@ -141,4 +184,4 @@ export {
141
184
  resolveOwnerRepo,
142
185
  push
143
186
  };
144
- //# sourceMappingURL=chunk-AVEQUR44.js.map
187
+ //# sourceMappingURL=chunk-EASYCD6F.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\";\nimport { realpath } from \"node:fs/promises\";\nimport type { Site } from \"../types.js\";\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 * The subset of `paths` that is NOT in HEAD's tree — what a fresh clone would\n * NOT get. POSITIVE evidence: a path counts as installed only when it is FOUND\n * in `git ls-tree`'s output. `git add -A` honours .gitignore and exits 0 either\n * way, so \"staging did not error\" proves nothing about what landed.\n *\n * `-z`, because `ls-tree` C-quotes any path outside core.quotePath's safe set\n * (`\"caf\\303\\251.txt\"`), which would read as missing. NOT `--full-tree`:\n * without it `ls-tree` reports paths relative to `cwd`, the same base the\n * caller's relative paths use.\n *\n * A git failure (no HEAD yet, not a repo) yields an EMPTY tree, so every path\n * comes back missing. An error may only ever DENY.\n */\nexport async function pathsMissingFromHead(\n cwd: string,\n paths: readonly string[],\n): Promise<string[]> {\n if (paths.length === 0) return [];\n let inTree: Set<string>;\n try {\n const { stdout } = await git(cwd, [\"ls-tree\", \"-r\", \"-z\", \"--name-only\", \"HEAD\"]);\n inTree = new Set(stdout.split(\"\\0\").filter((p) => p.length > 0));\n } catch {\n inTree = new Set();\n }\n return paths.filter((p) => !inTree.has(p));\n}\n\n/**\n * Why git is excluding `paths`: `<source>:<line>:<pattern>\\t<path>` rows from\n * `git check-ignore -v --no-index`. A path with no row is absent for some other\n * reason (a nested repository, say) and the caller must say so rather than\n * guess. Never throws: check-ignore exits 1 when NOTHING matches and 128 on\n * error, and both must read as \"no rule found\" — the caller is already\n * reporting a failure and must not lose it to a second one.\n */\nexport async function ignoreRulesFor(cwd: string, paths: readonly string[]): Promise<string[]> {\n if (paths.length === 0) return [];\n let out: string;\n try {\n ({ stdout: out } = await git(cwd, [\"check-ignore\", \"-v\", \"--no-index\", \"--\", ...paths]));\n } catch (err) {\n out = (err as { stdout?: string }).stdout ?? \"\";\n }\n return out.split(\"\\n\").filter((l) => l.length > 0);\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/**\n * The git remote hosts this project will derive a GitHub WRITE identity from.\n * The host is part of the identity: `https://gitlab.com/acme/site.git` names a\n * repository that exists on GitLab, and answering `acme/site` to a caller about\n * to write a repo secret on GitHub is a wrong-repo write, not a parse (#712).\n *\n * `ssh.github.com` is GitHub's documented port-443 SSH endpoint for networks\n * that block 22 — an origin real checkouts really carry; `www.github.com`\n * redirects to the apex and git follows it. Every other host resolves to\n * `null`, deliberately, so the caller refuses instead of guessing.\n */\nconst GITHUB_REMOTE_HOSTS = new Set([\"github.com\", \"www.github.com\", \"ssh.github.com\"]);\n\n/**\n * Derive `owner/repo` from a GITHUB git remote URL (https, ssh, git:// or\n * scp-style). `null` when the remote is not a GitHub repository URL: another\n * host, a traversal segment, a nested or too-short path, a local filesystem\n * path, anything unparseable.\n *\n * PARSED, not pattern-matched (#712). The previous implementation stripped\n * `^https?://[^/]+/` — throwing the HOST away — and returned the last two path\n * segments of whatever remained, so each of these yielded a confident,\n * correctly-shaped GitHub write target for a repository the code was not in:\n * `https://gitlab.com/acme/site.git` -> `acme/site`;\n * `https://user@evil.example.com/attacker/target` -> `attacker/target`;\n * `https://github.com/ok/repo/../../evil/target` -> `evil/target`;\n * `https://github.com/ok/repo?x=/evil/other` -> `evil/other`;\n * `/Users/me/Documents/GitHub/reddoor-starter` -> `GitHub/reddoor-starter`.\n *\n * A `..` anywhere in the raw remote is REFUSED rather than resolved: `new URL`\n * (like curl, and so git-over-https) collapses `ok/repo/../../evil/target` to\n * `/evil/target`, while ssh hands the literal path to the server. The two\n * transports disagree about which repository the remote names, so neither\n * answer is proven — and `..` is already inadmissible in an identity this\n * project accepts (see {@link isOwnerRepo}).\n */\nexport function parseOwnerRepo(remoteUrl: string): string | null {\n const raw = remoteUrl.trim();\n if (raw.length === 0) return null;\n if (raw.includes(\"..\")) return null;\n // scp-style `[user@]host:path` (git@github.com:owner/repo.git). The negative\n // lookahead keeps `https://…`, `ssh://…` and friends out of this branch —\n // their colon is followed by a slash.\n const scp = raw.match(/^(?:[A-Za-z0-9._-]+@)?([A-Za-z0-9._-]+):(?!\\/)(.+)$/);\n let url: URL;\n try {\n url = new URL(scp ? `ssh://${scp[1]!}/${scp[2]!}` : raw);\n } catch {\n return null; // not a URL at all — a local path, a typo, free text\n }\n if (!GITHUB_REMOTE_HOSTS.has(url.hostname.toLowerCase())) return null;\n // EXACTLY two segments. `/org/team/repo` is not a GitHub repository path, and\n // its last two name a repository that does not exist. `url.pathname` is the\n // path git resolves — a query string or fragment is not part of it.\n const segments = url.pathname\n .replace(/\\.git$/, \"\")\n .split(\"/\")\n .filter(Boolean);\n if (segments.length !== 2) return null;\n const ownerRepo = `${segments[0]!}/${segments[1]!}`;\n // Percent-encoding and anything else outside the identity's character class\n // ends here rather than reaching a `gh` path.\n return isOwnerRepo(ownerRepo) ? ownerRepo : null;\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/** What `execFile` rejects with: `code` is git's exit status, or a string errno\n * (`ENOENT`, `EACCES`) when the process could not be run at all. */\ntype GitFailure = { code?: number | string; stderr?: string };\n\n/** No `origin` remote is configured — exit 2, `error: No such remote 'origin'`\n * (measured, git 2.54). The one benign \"nothing wired\" failure. */\nfunction isNoSuchRemote(err: unknown): boolean {\n const e = err as GitFailure;\n return e.code === 2 && /No such remote/i.test(e.stderr ?? \"\");\n}\n\n/** `cwd` is not inside a git work tree — exit 128, `fatal: not a git repository`\n * (measured). Distinct from a dubious-ownership refusal, which also exits 128\n * but says something else and is NOT benign. */\nfunction isNotAWorkTree(err: unknown): boolean {\n const e = err as GitFailure;\n return e.code === 128 && /not a git repository/i.test(e.stderr ?? \"\");\n}\n\n/** Realpath'd `git rev-parse --show-toplevel` for `cwd`; `null` when `cwd` is\n * not inside a work tree. Every OTHER git failure throws — see\n * {@link resolveOwnerRepo} for why that distinction is load-bearing. */\nasync function repoToplevel(cwd: string): Promise<string | null> {\n try {\n const { stdout } = await git(cwd, [\"rev-parse\", \"--show-toplevel\"]);\n return await realpath(stdout.trim());\n } catch (err) {\n if (isNotAWorkTree(err)) return null;\n throw err;\n }\n}\n\n/**\n * Resolve the `owner/repo` a recipe will act on, and PROVE it rather than infer\n * it. An explicit `site.gitRepo` (Airtable, or a JSON inventory) wins;\n * otherwise it is derived from the checkout's `origin`.\n *\n * Returns `null` for the two states that genuinely mean \"nothing is wired\n * here\": `site.path` is not inside a git work tree, and the work tree has no\n * `origin`. Everything else THROWS, naming both sides, because callers write\n * repo secrets, branch-protection rulesets and pull requests at this identity\n * — a wrong one is not cosmetic (#712):\n *\n * - a value that is present but is not a clean `owner/repo`;\n * - an `origin` that is not a GitHub repository URL. A GitLab or impostor host\n * answered with a GitHub write target is the core of this class;\n * - a `site.path` that is not the ROOT of its work tree. `git remote get-url`\n * walks UP, so a site directory with no clone in it yet — exactly what the\n * positional `/new-site` route points at — resolved to whatever repository\n * happened to enclose it. Run a recipe against such a directory from under\n * the maintenance checkout and the token secret plus a ruleset landed on\n * `reddoorla/reddoor-maintenance`. No attacker required; a wrong `cd` was\n * enough. The cost is that \"run me from a subdirectory\" now refuses instead\n * of guessing — the refusal names the root, so the fix is one `cd`, or an\n * explicit identity;\n * - any git failure that is not one of the two benign shapes above. \"There is\n * no remote\" and \"git could not run\" used to be the same answer to the\n * caller, so `selfUpdating` printed \"add an origin remote\" for a missing\n * binary, an unreadable path and a dubious-ownership refusal alike.\n *\n * Extracted from `self-updating`'s private `resolveRepo` because `prismic-ci`\n * read `site.gitRepo` directly and therefore refused every POSITIONAL run with\n * \"no Git repo on this site\" — `localPath()` (src/inventory/local.ts:11) builds\n * `{ path, name }` and nothing else. A site being bootstrapped is exactly the\n * case that has no Airtable row yet (`--fleet airtable` filters pre-launch\n * statuses), so the positional path is the ONLY one `/new-site` can use.\n */\nexport async function resolveOwnerRepo(site: Site): Promise<string | null> {\n // Trimmed: a hand-typed Airtable cell with a trailing space is a typo, not a\n // malformed identity, and a blank cell means \"unset\" — fall through to origin\n // rather than fail closed with an alarming refusal.\n const declared = site.gitRepo?.trim();\n if (declared) {\n if (!isOwnerRepo(declared)) {\n throw new Error(\n `refusing to act on malformed repo identity: expected \"owner/repo\", got ${JSON.stringify(site.gitRepo)}`,\n );\n }\n return declared;\n }\n\n const toplevel = await repoToplevel(site.path);\n if (toplevel === null) return null; // not a work tree — nothing here to derive from\n if ((await realpath(site.path)) !== toplevel) {\n let enclosing = \"\";\n try {\n const id = parseOwnerRepo(await getRemoteUrl(site.path));\n if (id !== null) enclosing = ` — ${id}`;\n } catch {\n // Naming the enclosing repository is a courtesy. Failing to name it is\n // not a reason to stop refusing.\n }\n throw new Error(\n `refusing to derive a write identity from ${JSON.stringify(site.path)}: it is not the root ` +\n `of a git repository. git walks up to the checkout at ${toplevel}${enclosing}, which is ` +\n `not the directory that was asked about — cd to the repository root, or set the repo ` +\n `identity explicitly.`,\n );\n }\n\n let originUrl: string;\n try {\n originUrl = await getRemoteUrl(site.path);\n } catch (err) {\n if (isNoSuchRemote(err)) return null; // the one benign \"nothing wired\" failure\n throw err;\n }\n const fromOrigin = parseOwnerRepo(originUrl);\n if (fromOrigin === null) {\n throw new Error(\n `could not determine a GitHub owner/repo from origin ${JSON.stringify(originUrl)} — ` +\n `expected https://github.com/<owner>/<repo>, git@github.com:<owner>/<repo> or ` +\n `ssh://git@github.com/<owner>/<repo>; refusing to guess a write identity`,\n );\n }\n return fromOrigin;\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":[]}
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-J7SZQUCW.js";
4
4
  import {
5
5
  withRecipe
6
- } from "./chunk-ZQV7UQNE.js";
6
+ } from "./chunk-SQ4FSNYL.js";
7
7
  import {
8
8
  bumpDep,
9
9
  readPackageJson,
@@ -243,4 +243,4 @@ async function upgradeSvelte4to5(site, opts = {}) {
243
243
  export {
244
244
  upgradeSvelte4to5
245
245
  };
246
- //# sourceMappingURL=chunk-BCKEP5S6.js.map
246
+ //# sourceMappingURL=chunk-ECDJDHKT.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  openDb,
3
3
  readDbConfig
4
- } from "./chunk-22GKJO5F.js";
4
+ } from "./chunk-6INOBNRO.js";
5
5
 
6
6
  // src/db/fleet-events.ts
7
7
  async function recordFleetEvent(db, e) {
@@ -47,4 +47,4 @@ async function recordFleetEventsBestEffort(events, now, open = () => openDb(read
47
47
  export {
48
48
  recordFleetEventsBestEffort
49
49
  };
50
- //# sourceMappingURL=chunk-DMPHP7UP.js.map
50
+ //# sourceMappingURL=chunk-EQQ4H6L3.js.map