@reddoorla/maintenance 0.95.1 → 0.97.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 (316) hide show
  1. package/README.md +41 -7
  2. package/dist/{announce-CLQYX5I4.js → announce-5ZD2SBGD.js} +39 -31
  3. package/dist/announce-5ZD2SBGD.js.map +1 -0
  4. package/dist/attachments-JFGFQRXQ.js +10 -0
  5. package/dist/{blux-4KNXUP2K.js → blux-NXQLMS4W.js} +1 -1
  6. package/dist/{bump-deps-C5G2EFBB.js → bump-deps-775VLDYB.js} +6 -6
  7. package/dist/{chunk-FUWFLAWQ.js → chunk-3RLDX3Q2.js} +5 -5
  8. package/dist/{chunk-XHHMM5HX.js → chunk-43SW3FGW.js} +63 -1
  9. package/dist/chunk-43SW3FGW.js.map +1 -0
  10. package/dist/{chunk-R53VL3RI.js → chunk-5CJVUPJU.js} +1 -1
  11. package/dist/{chunk-R53VL3RI.js.map → chunk-5CJVUPJU.js.map} +1 -1
  12. package/dist/{chunk-S7RYFUIK.js → chunk-6PGXKN6F.js} +2 -2
  13. package/dist/{chunk-HTABUCF4.js → chunk-6THEZQLA.js} +3 -3
  14. package/dist/chunk-A5QKNOXW.js +72 -0
  15. package/dist/chunk-A5QKNOXW.js.map +1 -0
  16. package/dist/chunk-ASLDCWBF.js +41 -0
  17. package/dist/chunk-ASLDCWBF.js.map +1 -0
  18. package/dist/{chunk-CWXTNPNZ.js → chunk-AYDQQBNG.js} +3 -3
  19. package/dist/chunk-AYDQQBNG.js.map +1 -0
  20. package/dist/{chunk-WVNDEYXL.js → chunk-BINFZQ2T.js} +57 -6
  21. package/dist/chunk-BINFZQ2T.js.map +1 -0
  22. package/dist/{chunk-2NQUCS7O.js → chunk-BIYQN7FH.js} +3 -1
  23. package/dist/{chunk-2NQUCS7O.js.map → chunk-BIYQN7FH.js.map} +1 -1
  24. package/dist/{chunk-YLYXW5PH.js → chunk-CXICCLNM.js} +34 -4
  25. package/dist/chunk-CXICCLNM.js.map +1 -0
  26. package/dist/{chunk-BYGPUUCT.js → chunk-DRNZVORX.js} +66 -30
  27. package/dist/chunk-DRNZVORX.js.map +1 -0
  28. package/dist/{chunk-LM2NHRCJ.js → chunk-DYTYPKFN.js} +4 -4
  29. package/dist/chunk-DYTYPKFN.js.map +1 -0
  30. package/dist/{chunk-DFKBTBO3.js → chunk-EN6CTBUN.js} +35 -6
  31. package/dist/chunk-EN6CTBUN.js.map +1 -0
  32. package/dist/{chunk-5EEMTJMT.js → chunk-FHFXJ3EM.js} +8 -6
  33. package/dist/chunk-FHFXJ3EM.js.map +1 -0
  34. package/dist/{chunk-Z2K4JPQ3.js → chunk-FP6HOW6J.js} +10 -7
  35. package/dist/chunk-FP6HOW6J.js.map +1 -0
  36. package/dist/{chunk-2LYOAB2E.js → chunk-HSMN35LU.js} +2 -2
  37. package/dist/chunk-ICSRWW5Q.js +148 -0
  38. package/dist/chunk-ICSRWW5Q.js.map +1 -0
  39. package/dist/{chunk-FSGICFXB.js → chunk-IDBPII6G.js} +17 -5
  40. package/dist/chunk-IDBPII6G.js.map +1 -0
  41. package/dist/{chunk-C3IQQKXL.js → chunk-IOTOOTII.js} +121 -1
  42. package/dist/{chunk-C3IQQKXL.js.map → chunk-IOTOOTII.js.map} +1 -1
  43. package/dist/{chunk-BCKEP5S6.js → chunk-JHJNNJLG.js} +2 -2
  44. package/dist/{chunk-TO7DJYLZ.js → chunk-L3S3QZXL.js} +178 -39
  45. package/dist/chunk-L3S3QZXL.js.map +1 -0
  46. package/dist/{chunk-WSJ7RO54.js → chunk-LAG6IP7Y.js} +23 -5
  47. package/dist/chunk-LAG6IP7Y.js.map +1 -0
  48. package/dist/{chunk-WJMPCVOR.js → chunk-LAJ3FF54.js} +12 -4
  49. package/dist/chunk-LAJ3FF54.js.map +1 -0
  50. package/dist/{chunk-2ZAZ7ETT.js → chunk-LDHRIQUW.js} +32 -18
  51. package/dist/chunk-LDHRIQUW.js.map +1 -0
  52. package/dist/{chunk-6R56OKJZ.js → chunk-LF2TJN4X.js} +2 -2
  53. package/dist/chunk-LF2TJN4X.js.map +1 -0
  54. package/dist/{chunk-ZQV7UQNE.js → chunk-LUF52EIY.js} +2 -2
  55. package/dist/{chunk-5UINJNAJ.js → chunk-LZMAG73U.js} +34 -21
  56. package/dist/chunk-LZMAG73U.js.map +1 -0
  57. package/dist/{chunk-VSGMCCGQ.js → chunk-MMSMZC7S.js} +2 -2
  58. package/dist/chunk-MVKNU4QD.js +37 -0
  59. package/dist/chunk-MVKNU4QD.js.map +1 -0
  60. package/dist/{chunk-XPJQXSC4.js → chunk-P7323YVW.js} +27 -5
  61. package/dist/chunk-P7323YVW.js.map +1 -0
  62. package/dist/{chunk-KJRVMMM3.js → chunk-PARKXDXI.js} +36 -5
  63. package/dist/chunk-PARKXDXI.js.map +1 -0
  64. package/dist/{chunk-4763KQ6O.js → chunk-PXRN2MER.js} +2 -2
  65. package/dist/{chunk-WMBLR5VA.js → chunk-PZZ6HM5T.js} +3 -3
  66. package/dist/{chunk-TR3HFH6J.js → chunk-QFSL2L2B.js} +67 -10
  67. package/dist/chunk-QFSL2L2B.js.map +1 -0
  68. package/dist/chunk-RQJFGK3A.js +51 -0
  69. package/dist/chunk-RQJFGK3A.js.map +1 -0
  70. package/dist/{chunk-3Z6X3BHU.js → chunk-TC4GANMX.js} +53 -150
  71. package/dist/chunk-TC4GANMX.js.map +1 -0
  72. package/dist/{chunk-F3JWOPTG.js → chunk-TEMQPSCM.js} +45 -105
  73. package/dist/chunk-TEMQPSCM.js.map +1 -0
  74. package/dist/chunk-TJLKWQOX.js +52 -0
  75. package/dist/chunk-TJLKWQOX.js.map +1 -0
  76. package/dist/{chunk-CJ2RMTT7.js → chunk-TV76A6GN.js} +2 -2
  77. package/dist/{chunk-KZRR4NFG.js → chunk-TVZ6QCOE.js} +3 -3
  78. package/dist/{chunk-BVEDKMNG.js → chunk-U2ZI6G7H.js} +29 -11
  79. package/dist/chunk-U2ZI6G7H.js.map +1 -0
  80. package/dist/chunk-UPE6D4AT.js +69 -0
  81. package/dist/chunk-UPE6D4AT.js.map +1 -0
  82. package/dist/{chunk-R5IMFAFR.js → chunk-VMGHQUTV.js} +7 -16
  83. package/dist/chunk-VMGHQUTV.js.map +1 -0
  84. package/dist/chunk-VQMAAQ5M.js +53 -0
  85. package/dist/chunk-VQMAAQ5M.js.map +1 -0
  86. package/dist/{chunk-AVEQUR44.js → chunk-WMF5S2MB.js} +60 -17
  87. package/dist/chunk-WMF5S2MB.js.map +1 -0
  88. package/dist/{chunk-SVAAONYB.js → chunk-WQLNUEZL.js} +12 -2
  89. package/dist/{chunk-SVAAONYB.js.map → chunk-WQLNUEZL.js.map} +1 -1
  90. package/dist/{chunk-XN37VTDB.js → chunk-X56LAKPR.js} +3 -3
  91. package/dist/chunk-X56LAKPR.js.map +1 -0
  92. package/dist/{chunk-ZCL7C5LH.js → chunk-Y6FCXRIC.js} +1661 -387
  93. package/dist/chunk-Y6FCXRIC.js.map +1 -0
  94. package/dist/{chunk-2SLED3NB.js → chunk-YDZ7XGPZ.js} +1 -1
  95. package/dist/{chunk-2SLED3NB.js.map → chunk-YDZ7XGPZ.js.map} +1 -1
  96. package/dist/{chunk-IY6256GK.js → chunk-YJBSB4LP.js} +6 -1
  97. package/dist/chunk-YJBSB4LP.js.map +1 -0
  98. package/dist/{chunk-BI2VI6EM.js → chunk-YXWBM26P.js} +2 -2
  99. package/dist/{chunk-QF6IQQFP.js → chunk-Z7FDJ35L.js} +18 -12
  100. package/dist/chunk-Z7FDJ35L.js.map +1 -0
  101. package/dist/cli/bin.js +111 -51
  102. package/dist/cli/bin.js.map +1 -1
  103. package/dist/cli/commands/audit.d.ts +33 -3
  104. package/dist/cli/commands/audit.js +28 -25
  105. package/dist/cli/commands/audit.js.map +1 -1
  106. package/dist/client-DGTO65WB.js +11 -0
  107. package/dist/configs/playwright-a11y.d.ts +11 -1
  108. package/dist/configs/playwright-a11y.js +3 -1
  109. package/dist/{convert-to-pnpm-YTFREUJK.js → convert-to-pnpm-KLWA53N5.js} +6 -6
  110. package/dist/{db-XKZYDCKM.js → db-II5WAK4D.js} +45 -20
  111. package/dist/db-II5WAK4D.js.map +1 -0
  112. package/dist/deadletter-G4M3HSVM.js +17 -0
  113. package/dist/{digest-7UBGUBRK.js → digest-CWKK2DMR.js} +56 -32
  114. package/dist/digest-CWKK2DMR.js.map +1 -0
  115. package/dist/{digest-collectors-DJFN5ZDK.js → digest-collectors-CYTN2EUX.js} +17 -12
  116. package/dist/{digest-state-3KHZF7GD.js → digest-state-ZDPKR7MW.js} +18 -2
  117. package/dist/digest-state-ZDPKR7MW.js.map +1 -0
  118. package/dist/ensure-site-2QZHFVQ3.js +70 -0
  119. package/dist/ensure-site-2QZHFVQ3.js.map +1 -0
  120. package/dist/ensure-site-JEKV2ODE.js +105 -0
  121. package/dist/ensure-site-JEKV2ODE.js.map +1 -0
  122. package/dist/{ensure-site-OAAE5YE2.js → ensure-site-OO4UPWNQ.js} +49 -44
  123. package/dist/ensure-site-OO4UPWNQ.js.map +1 -0
  124. package/dist/fleet-events-CkOZpMVC.d.ts +435 -0
  125. package/dist/{fleet-state-UEVZ3X3J.js → fleet-state-RJROYXKO.js} +19 -7
  126. package/dist/{forms-notify-target-UC6PF3AE.js → forms-notify-target-X6UEOI2M.js} +19 -15
  127. package/dist/forms-notify-target-X6UEOI2M.js.map +1 -0
  128. package/dist/{freeze-A6MUCZU4.js → freeze-GUQ43EAU.js} +2 -2
  129. package/dist/{github-signals-TX5FPYP6.js → github-signals-QIRTHHET.js} +22 -17
  130. package/dist/github-signals-QIRTHHET.js.map +1 -0
  131. package/dist/{header-image-PSPT7CUM.js → header-image-H7ZJAV26.js} +15 -18
  132. package/dist/header-image-H7ZJAV26.js.map +1 -0
  133. package/dist/{health-endpoint-KVCJYWET.js → health-endpoint-BJ6BKRHF.js} +8 -7
  134. package/dist/{health-endpoint-KVCJYWET.js.map → health-endpoint-BJ6BKRHF.js.map} +1 -1
  135. package/dist/{health-mirror-XJAKA3EY.js → health-mirror-KU3G7V2Y.js} +10 -10
  136. package/dist/{import-airtable-TAKSN4NV.js → import-airtable-UBF5GYAL.js} +3 -3
  137. package/dist/index.d.ts +158 -419
  138. package/dist/index.js +91 -46
  139. package/dist/index.js.map +1 -1
  140. package/dist/{init-FFE4FCO2.js → init-GOWDA5QS.js} +19 -18
  141. package/dist/{init-FFE4FCO2.js.map → init-GOWDA5QS.js.map} +1 -1
  142. package/dist/{launch-UD36AHUN.js → launch-7N3X2MXB.js} +229 -79
  143. package/dist/launch-7N3X2MXB.js.map +1 -0
  144. package/dist/{lighthouse-VHI77PFB.js → lighthouse-52WSPZ5D.js} +2 -2
  145. package/dist/lighthouse-airtable-M45OPVRV.js +14 -0
  146. package/dist/match-harness-GBCDSVPS.js +417 -0
  147. package/dist/match-harness-GBCDSVPS.js.map +1 -0
  148. package/dist/migrate-XZ7U5DFC.js +7 -0
  149. package/dist/{notify-IISTHNBR.js → notify-K3WVH3DM.js} +4 -4
  150. package/dist/{onboard-TM2KESGF.js → onboard-JQQIMQW7.js} +6 -6
  151. package/dist/{orchestrate-HYVQPRCE.js → orchestrate-L2A5BZFY.js} +13 -10
  152. package/dist/parity-2JGVUCZD.js +14 -0
  153. package/dist/{pipeline-YROAY6DT.js → pipeline-UUPXOS2J.js} +3 -3
  154. package/dist/{preflight-RYIP54NY.js → preflight-O7Q6FWHU.js} +35 -11
  155. package/dist/preflight-O7Q6FWHU.js.map +1 -0
  156. package/dist/{prismic-ci-6GKKZGB5.js → prismic-ci-NAPWOXXD.js} +39 -9
  157. package/dist/prismic-ci-NAPWOXXD.js.map +1 -0
  158. package/dist/{prismic-models-P6FWWVRS.js → prismic-models-LL6ORTQ2.js} +20 -17
  159. package/dist/prismic-models-LL6ORTQ2.js.map +1 -0
  160. package/dist/prospect/types.d.ts +72 -0
  161. package/dist/prospect/types.js +1 -1
  162. package/dist/{prospect-audit-PRHU53HS.js → prospect-audit-FMPGQIRQ.js} +16 -7
  163. package/dist/prospect-audit-FMPGQIRQ.js.map +1 -0
  164. package/dist/{prospect-audits-R3LWF6U7.js → prospect-audits-IZVQY3OQ.js} +4 -2
  165. package/dist/protection-audit-YXJKFU6O.js +413 -0
  166. package/dist/protection-audit-YXJKFU6O.js.map +1 -0
  167. package/dist/recipes/sync-configs.js +3 -3
  168. package/dist/{render-YGNGTX3W.js → render-MHUUHPLN.js} +36 -9
  169. package/dist/render-MHUUHPLN.js.map +1 -0
  170. package/dist/{render-from-row-IFSBHWDH.js → render-from-row-75JTED2Z.js} +6 -4
  171. package/dist/{renovate-dispatch-U5Q52SKT.js → renovate-dispatch-2IHAZDVA.js} +14 -9
  172. package/dist/{renovate-dispatch-U5Q52SKT.js.map → renovate-dispatch-2IHAZDVA.js.map} +1 -1
  173. package/dist/{replay-PCSQOGCZ.js → replay-4FVFOMHH.js} +10 -23
  174. package/dist/replay-4FVFOMHH.js.map +1 -0
  175. package/dist/{report-LQS4ECTM.js → report-IAHXN5CW.js} +103 -57
  176. package/dist/report-IAHXN5CW.js.map +1 -0
  177. package/dist/{report-mirror-HDTOJE64.js → report-mirror-4QM57LMS.js} +34 -12
  178. package/dist/report-mirror-4QM57LMS.js.map +1 -0
  179. package/dist/roster-GDNSIRAK.js +19 -0
  180. package/dist/roster-GDNSIRAK.js.map +1 -0
  181. package/dist/{schema-A4WCPIF4.js → schema-KWUQEZ5O.js} +1 -1
  182. package/dist/schema-KWUQEZ5O.js.map +1 -0
  183. package/dist/{self-updating-MWXIZLL7.js → self-updating-ZUWPGYUP.js} +8 -7
  184. package/dist/{self-updating-MWXIZLL7.js.map → self-updating-ZUWPGYUP.js.map} +1 -1
  185. package/dist/{selftest-VXTHRPRN.js → selftest-G4W3LIBQ.js} +21 -17
  186. package/dist/{selftest-VXTHRPRN.js.map → selftest-G4W3LIBQ.js.map} +1 -1
  187. package/dist/site-create-ZU3ACBTO.js +45 -0
  188. package/dist/site-create-ZU3ACBTO.js.map +1 -0
  189. package/dist/site-lookup-DTOLJ4EA.js +27 -0
  190. package/dist/site-lookup-DTOLJ4EA.js.map +1 -0
  191. package/dist/{site-mirror-WON4FTJ2.js → site-mirror-WR7ACXOV.js} +26 -12
  192. package/dist/site-mirror-WR7ACXOV.js.map +1 -0
  193. package/dist/{smoke-suite-7DGITCTZ.js → smoke-suite-WAUPI6EJ.js} +8 -7
  194. package/dist/{smoke-suite-7DGITCTZ.js.map → smoke-suite-WAUPI6EJ.js.map} +1 -1
  195. package/dist/{submissions-OVZPVBYG.js → submissions-2SHCFOKD.js} +12 -7
  196. package/dist/submissions-2SHCFOKD.js.map +1 -0
  197. package/dist/{submissions-3I3GAQAX.js → submissions-JDZP35JM.js} +49 -9
  198. package/dist/submissions-JDZP35JM.js.map +1 -0
  199. package/dist/{svelte-codemods-WSV2FGNG.js → svelte-codemods-VOLOKKFI.js} +6 -6
  200. package/dist/{sync-K3PKHVLH.js → sync-F4USL4K5.js} +6 -6
  201. package/dist/{sync-configs-RRIHNLIJ.js → sync-configs-OXSWKVZ2.js} +6 -6
  202. package/dist/turso-KQKBBQCK.js +24 -0
  203. package/dist/turso-KQKBBQCK.js.map +1 -0
  204. package/dist/{upgrade-VTD6V4HS.js → upgrade-EVAL3JTO.js} +6 -6
  205. package/dist/util/git.d.ts +49 -10
  206. package/dist/util/git.js +1 -1
  207. package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
  208. package/dist/{websites-TB4TCAWM.js → websites-Y6FN2XFD.js} +27 -20
  209. package/dist/{write-audits-to-airtable-53EVXKMU.js → write-audits-to-airtable-TI3JMWSY.js} +7 -5
  210. package/package.json +3 -2
  211. package/dist/airtable-JGCPS7JH.js +0 -9
  212. package/dist/announce-CLQYX5I4.js.map +0 -1
  213. package/dist/attachments-NLZIJ5NU.js +0 -9
  214. package/dist/chunk-2ZAZ7ETT.js.map +0 -1
  215. package/dist/chunk-3Z6X3BHU.js.map +0 -1
  216. package/dist/chunk-5EEMTJMT.js.map +0 -1
  217. package/dist/chunk-5GUJ36TL.js +0 -53
  218. package/dist/chunk-5GUJ36TL.js.map +0 -1
  219. package/dist/chunk-5UINJNAJ.js.map +0 -1
  220. package/dist/chunk-6R56OKJZ.js.map +0 -1
  221. package/dist/chunk-AVEQUR44.js.map +0 -1
  222. package/dist/chunk-BVEDKMNG.js.map +0 -1
  223. package/dist/chunk-BYGPUUCT.js.map +0 -1
  224. package/dist/chunk-CWXTNPNZ.js.map +0 -1
  225. package/dist/chunk-DFKBTBO3.js.map +0 -1
  226. package/dist/chunk-F3JWOPTG.js.map +0 -1
  227. package/dist/chunk-FSGICFXB.js.map +0 -1
  228. package/dist/chunk-GASBX52O.js +0 -12
  229. package/dist/chunk-GASBX52O.js.map +0 -1
  230. package/dist/chunk-IY6256GK.js.map +0 -1
  231. package/dist/chunk-KJRVMMM3.js.map +0 -1
  232. package/dist/chunk-LM2NHRCJ.js.map +0 -1
  233. package/dist/chunk-QF6IQQFP.js.map +0 -1
  234. package/dist/chunk-R5IMFAFR.js.map +0 -1
  235. package/dist/chunk-TO7DJYLZ.js.map +0 -1
  236. package/dist/chunk-TR3HFH6J.js.map +0 -1
  237. package/dist/chunk-WJMPCVOR.js.map +0 -1
  238. package/dist/chunk-WSJ7RO54.js.map +0 -1
  239. package/dist/chunk-WVNDEYXL.js.map +0 -1
  240. package/dist/chunk-XHHMM5HX.js.map +0 -1
  241. package/dist/chunk-XN37VTDB.js.map +0 -1
  242. package/dist/chunk-XPJQXSC4.js.map +0 -1
  243. package/dist/chunk-YFAXOD7Z.js +0 -42
  244. package/dist/chunk-YFAXOD7Z.js.map +0 -1
  245. package/dist/chunk-YLYXW5PH.js.map +0 -1
  246. package/dist/chunk-Z2K4JPQ3.js.map +0 -1
  247. package/dist/chunk-ZCL7C5LH.js.map +0 -1
  248. package/dist/client-G2WGWPCA.js +0 -11
  249. package/dist/db-XKZYDCKM.js.map +0 -1
  250. package/dist/digest-7UBGUBRK.js.map +0 -1
  251. package/dist/digest-state-3KHZF7GD.js.map +0 -1
  252. package/dist/ensure-site-OAAE5YE2.js.map +0 -1
  253. package/dist/fleet-events-cF2Om88w.d.ts +0 -24
  254. package/dist/forms-notify-target-UC6PF3AE.js.map +0 -1
  255. package/dist/github-signals-TX5FPYP6.js.map +0 -1
  256. package/dist/header-image-PSPT7CUM.js.map +0 -1
  257. package/dist/launch-UD36AHUN.js.map +0 -1
  258. package/dist/lighthouse-airtable-YMH4UA7R.js +0 -12
  259. package/dist/match-harness-QRG6XZAR.js +0 -70
  260. package/dist/match-harness-QRG6XZAR.js.map +0 -1
  261. package/dist/migrate-Y7R4CESL.js +0 -7
  262. package/dist/parity-CAJ477PS.js +0 -14
  263. package/dist/preflight-RYIP54NY.js.map +0 -1
  264. package/dist/prismic-ci-6GKKZGB5.js.map +0 -1
  265. package/dist/prismic-models-P6FWWVRS.js.map +0 -1
  266. package/dist/prospect-audit-PRHU53HS.js.map +0 -1
  267. package/dist/protection-audit-SJO53MEQ.js +0 -168
  268. package/dist/protection-audit-SJO53MEQ.js.map +0 -1
  269. package/dist/render-YGNGTX3W.js.map +0 -1
  270. package/dist/replay-PCSQOGCZ.js.map +0 -1
  271. package/dist/report-LQS4ECTM.js.map +0 -1
  272. package/dist/report-mirror-HDTOJE64.js.map +0 -1
  273. package/dist/schema-A4WCPIF4.js.map +0 -1
  274. package/dist/site-mirror-WON4FTJ2.js.map +0 -1
  275. package/dist/submissions-3I3GAQAX.js.map +0 -1
  276. package/dist/submissions-OVZPVBYG.js.map +0 -1
  277. /package/dist/{airtable-JGCPS7JH.js.map → attachments-JFGFQRXQ.js.map} +0 -0
  278. /package/dist/{blux-4KNXUP2K.js.map → blux-NXQLMS4W.js.map} +0 -0
  279. /package/dist/{bump-deps-C5G2EFBB.js.map → bump-deps-775VLDYB.js.map} +0 -0
  280. /package/dist/{chunk-FUWFLAWQ.js.map → chunk-3RLDX3Q2.js.map} +0 -0
  281. /package/dist/{chunk-S7RYFUIK.js.map → chunk-6PGXKN6F.js.map} +0 -0
  282. /package/dist/{chunk-HTABUCF4.js.map → chunk-6THEZQLA.js.map} +0 -0
  283. /package/dist/{chunk-2LYOAB2E.js.map → chunk-HSMN35LU.js.map} +0 -0
  284. /package/dist/{chunk-BCKEP5S6.js.map → chunk-JHJNNJLG.js.map} +0 -0
  285. /package/dist/{chunk-ZQV7UQNE.js.map → chunk-LUF52EIY.js.map} +0 -0
  286. /package/dist/{chunk-VSGMCCGQ.js.map → chunk-MMSMZC7S.js.map} +0 -0
  287. /package/dist/{chunk-4763KQ6O.js.map → chunk-PXRN2MER.js.map} +0 -0
  288. /package/dist/{chunk-WMBLR5VA.js.map → chunk-PZZ6HM5T.js.map} +0 -0
  289. /package/dist/{chunk-CJ2RMTT7.js.map → chunk-TV76A6GN.js.map} +0 -0
  290. /package/dist/{chunk-KZRR4NFG.js.map → chunk-TVZ6QCOE.js.map} +0 -0
  291. /package/dist/{chunk-BI2VI6EM.js.map → chunk-YXWBM26P.js.map} +0 -0
  292. /package/dist/{attachments-NLZIJ5NU.js.map → client-DGTO65WB.js.map} +0 -0
  293. /package/dist/{convert-to-pnpm-YTFREUJK.js.map → convert-to-pnpm-KLWA53N5.js.map} +0 -0
  294. /package/dist/{client-G2WGWPCA.js.map → deadletter-G4M3HSVM.js.map} +0 -0
  295. /package/dist/{digest-collectors-DJFN5ZDK.js.map → digest-collectors-CYTN2EUX.js.map} +0 -0
  296. /package/dist/{fleet-state-UEVZ3X3J.js.map → fleet-state-RJROYXKO.js.map} +0 -0
  297. /package/dist/{freeze-A6MUCZU4.js.map → freeze-GUQ43EAU.js.map} +0 -0
  298. /package/dist/{health-mirror-XJAKA3EY.js.map → health-mirror-KU3G7V2Y.js.map} +0 -0
  299. /package/dist/{import-airtable-TAKSN4NV.js.map → import-airtable-UBF5GYAL.js.map} +0 -0
  300. /package/dist/{lighthouse-VHI77PFB.js.map → lighthouse-52WSPZ5D.js.map} +0 -0
  301. /package/dist/{lighthouse-airtable-YMH4UA7R.js.map → lighthouse-airtable-M45OPVRV.js.map} +0 -0
  302. /package/dist/{migrate-Y7R4CESL.js.map → migrate-XZ7U5DFC.js.map} +0 -0
  303. /package/dist/{notify-IISTHNBR.js.map → notify-K3WVH3DM.js.map} +0 -0
  304. /package/dist/{onboard-TM2KESGF.js.map → onboard-JQQIMQW7.js.map} +0 -0
  305. /package/dist/{orchestrate-HYVQPRCE.js.map → orchestrate-L2A5BZFY.js.map} +0 -0
  306. /package/dist/{parity-CAJ477PS.js.map → parity-2JGVUCZD.js.map} +0 -0
  307. /package/dist/{pipeline-YROAY6DT.js.map → pipeline-UUPXOS2J.js.map} +0 -0
  308. /package/dist/{prospect-audits-R3LWF6U7.js.map → prospect-audits-IZVQY3OQ.js.map} +0 -0
  309. /package/dist/{render-from-row-IFSBHWDH.js.map → render-from-row-75JTED2Z.js.map} +0 -0
  310. /package/dist/{svelte-codemods-WSV2FGNG.js.map → svelte-codemods-VOLOKKFI.js.map} +0 -0
  311. /package/dist/{sync-K3PKHVLH.js.map → sync-F4USL4K5.js.map} +0 -0
  312. /package/dist/{sync-configs-RRIHNLIJ.js.map → sync-configs-OXSWKVZ2.js.map} +0 -0
  313. /package/dist/{upgrade-VTD6V4HS.js.map → upgrade-EVAL3JTO.js.map} +0 -0
  314. /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
  315. /package/dist/{websites-TB4TCAWM.js.map → websites-Y6FN2XFD.js.map} +0 -0
  316. /package/dist/{write-audits-to-airtable-53EVXKMU.js.map → write-audits-to-airtable-TI3JMWSY.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/preflight.ts"],"sourcesContent":["// #646 step 4: preflight reads through the readers its caller hands it, so this\n// file no longer touches the Airtable layer at all — its row helpers come from\n// the vendor-neutral modules step 1 moved them to.\nimport { siteSlug } from \"../fleet/site-row.js\";\nimport type { WebsiteRow } from \"../fleet/site-row.js\";\nimport type { ReportRow } from \"./report-row.js\";\nimport { parseAddresses, isProbablyEmail } from \"./send/orchestrate.js\";\nimport { ELIGIBLE_STATUSES, reportPeriodKey } from \"./due.js\";\nimport type { ReportType } from \"./types.js\";\nimport { gatingHealth, isSendOverridden } from \"./checklist.js\";\n\nexport type PreflightLevel = \"fail\" | \"warn\" | \"info\";\n\nexport type PreflightFinding = {\n level: PreflightLevel;\n /** Stable machine-readable check id, e.g. \"recipients-missing\". */\n check: string;\n message: string;\n};\n\nexport type PreflightSiteResult = {\n site: string;\n findings: PreflightFinding[];\n};\n\n/** Domains whose addresses are the OPERATOR's, not a client's. A client site whose\n * resolved To contains one of these is almost always a test-send leftover — the\n * exact misconfiguration that would silently divert a client announcement. */\nconst OPERATOR_DOMAINS = [\"reddoorla.com\", \"tuckerlemos.com\"];\n\n/** Anchors older than this, WHEN the anchor is actually the schedule's base date\n * (no newer Sent-at — see nextDueDate's `lastSent ?? fallback`), mean the next\n * `report --due` run drafts a surprise back-dated report. */\nconst STALE_ANCHOR_DAYS = 396; // ~13 months\n\nfunction domainOf(addr: string): string {\n const at = addr.lastIndexOf(\"@\");\n return at === -1 ? \"\" : addr.slice(at + 1).toLowerCase();\n}\n\nfunction isOperatorSite(site: WebsiteRow): boolean {\n if (!site.url) return false;\n try {\n // Airtable `url` cells aren't guaranteed a scheme; retry with one before giving up.\n const host = new URL(\n /^[a-z][a-z0-9+.-]*:\\/\\//i.test(site.url) ? site.url : `https://${site.url}`,\n ).hostname.toLowerCase();\n return OPERATOR_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));\n } catch {\n return false;\n }\n}\n\nfunction lastSentForType(reports: ReportRow[], type: ReportType): string | null {\n const sent = reports\n .filter((r) => r.reportType === type && r.sentAt !== null)\n .map((r) => r.sentAt!)\n .sort();\n return sent[sent.length - 1] ?? null;\n}\n\nfunction checkFrequency(\n site: WebsiteRow,\n which: \"maintenance\" | \"testing\",\n reports: ReportRow[],\n findings: PreflightFinding[],\n now: Date,\n): void {\n // Validate the RAW Airtable cell, not the coerced Frequency: toFrequency trims\n // whitespace (so \"Monthly \" schedules), then coerces any still-unrecognized value\n // (typo, renamed option) to \"None\" with a console.warn. That warn only lands in\n // logs — this finding is the structured, surfaced version of the same failure.\n const raw = which === \"maintenance\" ? site.maintenanceFreqRaw : site.testingFreqRaw;\n const coerced = which === \"maintenance\" ? site.maintenanceFreq : site.testingFreq;\n if (raw !== null && raw !== \"\" && coerced === \"None\" && raw.trim() !== \"None\") {\n findings.push({\n level: \"fail\",\n check: \"frequency-unrecognized\",\n message: `${which} frequency cell is '${raw}' — not a recognized Monthly/Quarterly/Yearly/None (even after trimming), so the scheduler treats it as None and the site drops off the calendar; fix the Airtable value`,\n });\n return;\n }\n if (coerced === \"None\") return;\n\n const anchor = which === \"maintenance\" ? site.maintenanceDay : site.testingDay;\n if (!anchor) return;\n // nextDueDate uses `lastSent ?? anchor`: once anything has been sent, the anchor\n // is inert and an old one is normal on every healthy mature site — only warn when\n // the anchor is genuinely the base date the scheduler would use.\n const type: ReportType = which === \"maintenance\" ? \"Maintenance\" : \"Testing\";\n const lastSent = lastSentForType(reports, type);\n if (lastSent && new Date(lastSent).getTime() >= new Date(anchor).getTime()) return;\n const ageDays = (now.getTime() - new Date(anchor).getTime()) / 86_400_000;\n if (ageDays > STALE_ANCHOR_DAYS) {\n findings.push({\n level: \"warn\",\n check: \"anchor-stale\",\n message: `${which} day anchor is ${anchor} (>13 months old) and nothing newer has been sent — \\`report --due\\` will draft a back-dated overdue report; reset the anchor or clear the schedule`,\n });\n }\n}\n\n/**\n * Pure per-site preflight: every check that, left unfixed, makes a report send fail,\n * go to the wrong inbox, or surprise the operator. Read-only over data already\n * fetched; no Airtable handle, no network — trivially testable.\n *\n * Mirrors the send/draft-time validation (recipients + header image in\n * send/orchestrate.ts, Websites-row scores in draft.ts) so problems surface BEFORE\n * draft/approve instead of exploding at `report --send-ready`, and adds the checks\n * send time cannot do: operator-address leftovers, To-override shadowing,\n * pending-draft races, schedule hygiene against the RAW Airtable values.\n */\nexport function preflightSite(\n site: WebsiteRow,\n reports: ReportRow[],\n type: ReportType,\n now: Date,\n): PreflightSiteResult {\n const findings: PreflightFinding[] = [];\n\n // A site that isn't on the checked calendar never drafts, so its send\n // requirements (recipients / header image / scores) can't hurt anyone —\n // checking them anyway would drown the fleet run in fails for legacy hosting\n // rows. Frequency hygiene and pending-draft checks still run: a typo'd cell\n // is exactly HOW a site falls off the calendar, and an already-drafted report\n // can still be approved + sent regardless of schedule.\n const scheduledForType =\n type === \"Announcement\"\n ? true\n : type === \"Maintenance\"\n ? site.maintenanceFreq !== \"None\"\n : site.testingFreq !== \"None\";\n if (!scheduledForType) {\n findings.push({\n level: \"info\",\n check: \"not-scheduled\",\n message: `no ${type} schedule (frequency is None/blank) — send-requirement checks skipped; schedule + pending-draft checks still apply`,\n });\n }\n\n // --- Recipients: same resolution order as the send path (To override, else contact).\n const explicitTo = parseAddresses(site.reportRecipientsTo);\n const fallbackTo = parseAddresses(site.pointOfContact);\n const to = scheduledForType ? (explicitTo ?? fallbackTo ?? []) : [];\n if (scheduledForType && to.length === 0) {\n findings.push({\n level: \"fail\",\n check: \"recipients-missing\",\n message:\n \"no recipients — Report recipients (To) and point of contact are both empty; the send will throw\",\n });\n }\n for (const addr of to) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `recipient '${addr}' is malformed — use bare addresses only in Report recipients (To) / point of contact`,\n });\n }\n }\n const cc = scheduledForType ? (parseAddresses(site.reportRecipientsCc) ?? []) : [];\n for (const addr of cc) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `CC '${addr}' is malformed — fix Report recipients (CC)`,\n });\n }\n }\n if (!isOperatorSite(site)) {\n const operatorAddrs = to.filter((a) => OPERATOR_DOMAINS.includes(domainOf(a)));\n if (operatorAddrs.length > 0) {\n const allOperator = operatorAddrs.length === to.length;\n findings.push({\n level: \"warn\",\n check: \"recipient-operator-address\",\n message: allOperator\n ? `resolved To is ONLY operator address(es) ${operatorAddrs.join(\", \")} — probably a test-send leftover; the client will NOT receive this report`\n : `resolved To includes operator address(es) ${operatorAddrs.join(\", \")} alongside the client's — confirm that's intentional (the ops inbox is already CC'd on every send)`,\n });\n }\n }\n if (scheduledForType && explicitTo && fallbackTo) {\n const a = [...explicitTo]\n .map((x) => x.toLowerCase())\n .sort()\n .join(\",\");\n const b = [...fallbackTo]\n .map((x) => x.toLowerCase())\n .sort()\n .join(\",\");\n if (a !== b) {\n findings.push({\n level: \"info\",\n check: \"to-override-shadows-contact\",\n message: `Report recipients (To) [${explicitTo.join(\", \")}] overrides point of contact [${fallbackTo.join(\", \")}] — the contact won't receive this unless also listed`,\n });\n }\n }\n\n // --- Hard requirements of the draft/send paths, surfaced early.\n if (scheduledForType && !site.headerImage) {\n findings.push({\n level: \"fail\",\n check: \"header-image-missing\",\n message: \"no Header image on the Websites row — the send will throw\",\n });\n } else if (scheduledForType && site.headerImage && !site.headerImage.type.startsWith(\"image/\")) {\n findings.push({\n level: \"fail\",\n check: \"header-image-not-image\",\n message: `Header image attachment is '${site.headerImage.type}' (${site.headerImage.filename}) — not a decodable image; the send throws in prepareHeaderImage`,\n });\n }\n const scores = [site.pScore, site.rScore, site.bpScore, site.seoScore];\n if (scheduledForType && scores.some((s) => s === null)) {\n if (type === \"Announcement\") {\n // announce() skips score-less sites (skipped-no-scores) rather than throwing.\n findings.push({\n level: \"warn\",\n check: \"scores-missing\",\n message:\n \"Websites row is missing Lighthouse scores — `announce` will skip this site; run `audit lighthouse --write-back` first\",\n });\n } else {\n // report <site> / report --due HARD-THROW here (scoresFromWebsite in draft.ts).\n findings.push({\n level: \"fail\",\n check: \"scores-missing\",\n message:\n \"Websites row is missing Lighthouse scores — drafting will throw (scoresFromWebsite); run `audit lighthouse --write-back` first\",\n });\n }\n }\n if (type === \"Announcement\" && site.status !== \"maintained\") {\n findings.push({\n level: \"info\",\n check: \"status-not-maintenance\",\n message: `status is '${site.status ?? \"(blank)\"}' — \\`announce --all\\` only drafts for maintenance sites`,\n });\n }\n\n // --- Pending drafts. For an Announcement these always race (the queue tier rule\n // supersedes or blocks). For Maintenance/Testing, the CURRENT cycle's own draft\n // sitting ready on send day IS the payload — that's the steady state, not a\n // problem — so only genuinely stale/foreign drafts get the warn.\n const pending = reports.filter((r) => r.draftReady && r.sentAt === null);\n if (pending.length > 0) {\n const currentPeriod = reportPeriodKey(now);\n const isExpectedPayload = (r: ReportRow): boolean =>\n type !== \"Announcement\" && r.reportType === type && r.period === currentPeriod;\n const expected = pending.filter(isExpectedPayload);\n const stale = pending.filter((r) => !isExpectedPayload(r));\n if (expected.length > 0) {\n findings.push({\n level: \"info\",\n check: \"pending-drafts\",\n message: `current-cycle ${type} draft is queued${expected[0]!.approvedToSend ? \" and approved\" : \" (awaiting approval)\"} — this is the report \\`--send-ready\\` will deliver`,\n });\n }\n if (stale.length > 0) {\n const summary = stale\n .map(\n (r) =>\n `${r.reportType} ${r.period ?? \"(no period)\"}${r.approvedToSend ? \" [APPROVED]\" : \"\"}`,\n )\n .join(\", \");\n findings.push({\n level: \"warn\",\n check: \"pending-drafts\",\n message: `${stale.length} unsent draft(s) queued: ${summary} — resolve before sending so the client doesn't get the wrong report first`,\n });\n }\n }\n\n // --- Schedule hygiene (validates the RAW Airtable frequency cells).\n checkFrequency(site, \"maintenance\", reports, findings, now);\n checkFrequency(site, \"testing\", reports, findings, now);\n if (type === \"Announcement\") {\n const anySent = lastSentForType(reports, \"Maintenance\") ?? lastSentForType(reports, \"Testing\");\n if (!site.maintenanceDay && !site.testingDay && !anySent) {\n findings.push({\n level: \"info\",\n check: \"anchor-missing\",\n message:\n \"no maintenance/testing day anchors and nothing ever sent — set both anchors to the announcement send date (the Sonder playbook) so recurrence starts from the announcement\",\n });\n }\n }\n\n return { site: site.name, findings };\n}\n\n/**\n * Fleet-level heuristics that need the whole selection. Airtable column renames\n * don't error — the mapper reads null forever (its own header comment admits this).\n * A load-bearing column empty on EVERY selected site is far more likely a rename\n * than N coincidences; say so. Likewise two different sites resolving to one\n * recipient is worth one eyeball (it can be legitimate — same owner, two sites).\n */\nexport function preflightFleet(sites: WebsiteRow[]): PreflightFinding[] {\n if (sites.length < 3) return []; // too few rows to distinguish rename from data\n const findings: PreflightFinding[] = [];\n const allEmpty = (get: (s: WebsiteRow) => unknown, column: string): void => {\n if (sites.every((s) => get(s) === null || get(s) === \"\")) {\n findings.push({\n level: \"warn\",\n check: \"column-possibly-renamed\",\n message: `'${column}' is empty on all ${sites.length} selected sites — if that column was renamed in Airtable the code reads null silently; verify the column name`,\n });\n }\n };\n allEmpty((s) => s.pointOfContact, \"point of contact\");\n allEmpty((s) => s.maintenanceFreqRaw, \"maintenence freq\");\n allEmpty((s) => s.headerImage, \"Header image\");\n\n const byContact = new Map<string, string[]>();\n for (const s of sites) {\n const to = parseAddresses(s.reportRecipientsTo) ?? parseAddresses(s.pointOfContact) ?? [];\n for (const addr of to) {\n const key = addr.toLowerCase();\n byContact.set(key, [...(byContact.get(key) ?? []), s.name]);\n }\n }\n for (const [addr, names] of byContact) {\n if (names.length > 1 && !OPERATOR_DOMAINS.includes(domainOf(addr))) {\n findings.push({\n level: \"info\",\n check: \"duplicate-contact\",\n message: `${names.join(\" and \")} resolve to the same recipient (${addr}) — fine if one person owns both; otherwise a copy-paste error on one row`,\n });\n }\n }\n return findings;\n}\n\nexport type PreflightDeps = {\n /** The fleet roster. Since #646 step 4 preflight reads it from TURSO — read by\n * the command that calls this — so a `site_<ULID>` site is checked like any\n * other. Required, so nothing can reach a store this was not handed. */\n roster: () => Promise<WebsiteRow[]>;\n /** Every report row, on the same contract as `roster`. Called only when the\n * selection is non-empty (it was one whole-table read before, and still is). */\n allReports: () => Promise<ReportRow[]>;\n /** Slug of a single site (matched via siteSlug). Mutually exclusive with `all`. */\n site?: string;\n /** Fleet mode: for Announcement, maintenance-status sites (announce's own filter);\n * for Maintenance/Testing, everything `report --due` schedules — ELIGIBLE_STATUSES\n * plus null-status rows (due.ts treats null as active for legacy rows). */\n all?: boolean;\n type?: ReportType;\n now?: Date;\n};\n\nexport type PreflightResult = {\n results: PreflightSiteResult[];\n fleet: PreflightFinding[];\n};\n\n/**\n * Read-only rollout preflight over the fleet store. One roster read, one reports\n * read (the --due path does the same, and for the same reason: the whole table), then\n * {@link preflightSite} per selected site plus {@link preflightFleet} across the\n * selection. NEVER writes and NEVER sends.\n */\nexport async function preflight(deps: PreflightDeps): Promise<PreflightResult> {\n const type: ReportType = deps?.type ?? \"Announcement\";\n const now = deps?.now ?? new Date();\n\n const websites = await deps.roster();\n const selected = deps?.site\n ? websites.filter((w) => siteSlug(w.name) === siteSlug(deps.site!))\n : type === \"Announcement\"\n ? websites.filter((w) => w.status === \"maintained\")\n : websites.filter((w) => w.status === null || ELIGIBLE_STATUSES.has(w.status));\n\n const allReports = selected.length > 0 ? await deps.allReports() : [];\n const results = selected.map((site) =>\n preflightSite(\n site,\n allReports.filter((r) => r.siteId === site.id),\n type,\n now,\n ),\n );\n return { results, fleet: deps?.site ? [] : preflightFleet(selected) };\n}\n\n/**\n * Health-gate blockers for one report: every GATING field whose evidence is not `pass`/`n/a`\n * (i.e. `fail`, `unknown`, or absent) becomes a fail-level finding, keyed `health-gate`. PURE.\n * Folded into {@link approveBlockers} so health failures ride the existing send-blocked reason +\n * 409 + dashboard chip (the second gate — no third gate is added).\n *\n * A logged send-anyway override ({@link isSendOverridden}) suppresses these findings — but never\n * the REAL send blockers (recipients/header/scores), which are computed outside this function and\n * still gate an overridden send.\n */\nexport function healthBlockers(report: ReportRow): PreflightFinding[] {\n if (isSendOverridden(report)) return [];\n const findings: PreflightFinding[] = [];\n for (const { field, status } of gatingHealth({\n reportType: report.reportType,\n autoEvidence: report.autoEvidence ?? {},\n })) {\n if (status === \"pass\" || status === \"n/a\") continue;\n // `||`, not `??`: an explicit empty-string note (no message recorded) must\n // also fall back to the placeholder, not render as a trailing blank.\n const note = report.autoEvidence?.[field]?.note || \"no signal yet\";\n findings.push({\n level: \"fail\",\n check: \"health-gate\",\n message:\n status === \"fail\"\n ? `${field}: failing — ${note}`\n : `${field}: not yet green (${status}) — ${note}`,\n });\n }\n return findings;\n}\n\n/**\n * The send-blocking subset for ONE report, at approve time: exactly the\n * conditions that make `sendOne` throw (no recipients, malformed To/CC, no\n * header image, no report-level Lighthouse scores) plus the wrong-inbox warn\n * (operator address resolved as a client's To) plus every {@link healthBlockers}\n * finding (a gating checklist item whose auto-evidence isn't `pass`/`n/a` fails\n * the health gate). PURE — the approve gate, the dashboard's pending-row chip,\n * and the digest collector all call this one function so \"approvable\" can't\n * drift from \"sendable\".\n *\n * Deliberately narrower than {@link preflightSite}: schedule hygiene and\n * pending-draft races are fleet/site concerns — blocking THIS report's approval\n * on them would be over-gating.\n */\nexport function approveBlockers(site: WebsiteRow, report: ReportRow): PreflightFinding[] {\n const findings: PreflightFinding[] = [];\n\n const explicitTo = parseAddresses(site.reportRecipientsTo);\n const fallbackTo = parseAddresses(site.pointOfContact);\n const to = explicitTo ?? fallbackTo ?? [];\n if (to.length === 0) {\n findings.push({\n level: \"fail\",\n check: \"recipients-missing\",\n message:\n \"no recipients — Report recipients (To) and point of contact are both empty; the send will throw\",\n });\n }\n for (const addr of to) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `recipient '${addr}' is malformed — fix Report recipients (To) / point of contact in Airtable`,\n });\n }\n }\n for (const addr of parseAddresses(site.reportRecipientsCc) ?? []) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `CC '${addr}' is malformed — fix Report recipients (CC) in Airtable`,\n });\n }\n }\n if (!site.headerImage) {\n findings.push({\n level: \"fail\",\n check: \"header-image-missing\",\n message: \"no Header image on the Websites row — the send will throw\",\n });\n } else if (!site.headerImage.type.startsWith(\"image/\")) {\n findings.push({\n level: \"fail\",\n check: \"header-image-not-image\",\n message: `Header image attachment is '${site.headerImage.type}' (${site.headerImage.filename}) — not a decodable image; the send throws in prepareHeaderImage`,\n });\n }\n // sendOne throws on a null report.lighthouse (one blank/non-numeric cell nulls\n // all four) — the REPORT's snapshot, not the Websites-row scores.\n if (report.lighthouse === null) {\n findings.push({\n level: \"fail\",\n check: \"report-scores-missing\",\n message:\n \"the Reports row has no Lighthouse scores (one blank/non-numeric cell nulls all four) — the send will throw\",\n });\n }\n if (!isOperatorSite(site)) {\n const operatorAddrs = to.filter((a) => OPERATOR_DOMAINS.includes(domainOf(a)));\n if (operatorAddrs.length > 0 && operatorAddrs.length === to.length) {\n findings.push({\n level: \"warn\",\n check: \"recipient-operator-address\",\n message: `resolved To is ONLY operator address(es) ${operatorAddrs.join(\", \")} — the client will NOT receive this report`,\n });\n }\n }\n findings.push(...healthBlockers(report));\n return findings;\n}\n\n/** Convenience: just the fail-level blockers, formatted for gate messages. */\nexport function formatBlockers(findings: PreflightFinding[]): string[] {\n return findings.filter((f) => f.level === \"fail\").map((f) => `${f.check}: ${f.message}`);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,53 @@
1
+ import {
2
+ ACTIVE_STATUSES,
3
+ isPreLaunch,
4
+ siteSlug
5
+ } from "./chunk-ICSRWW5Q.js";
6
+ import {
7
+ isHttpUrl
8
+ } from "./chunk-RARREDQE.js";
9
+
10
+ // src/inventory/select.ts
11
+ function selectFleetSites(websites, workdir) {
12
+ return websites.filter(
13
+ (w) => w.status !== null && ACTIVE_STATUSES.has(w.status) && !isPreLaunch(w.status) && w.url.length > 0
14
+ ).flatMap((w) => {
15
+ const slug = siteSlug(w.name);
16
+ if (slug.length === 0) {
17
+ console.warn(
18
+ `[inventory] skipping "${w.name}" (row ${w.id}): Name has no slug-able characters (empty slug)`
19
+ );
20
+ return [];
21
+ }
22
+ const site = {
23
+ path: `${workdir}/${slug}`,
24
+ name: slug,
25
+ meta: { siteId: w.id, displayName: w.name }
26
+ };
27
+ if (isHttpUrl(w.url)) {
28
+ site.deployedUrl = w.url;
29
+ } else {
30
+ console.warn(
31
+ `[inventory] skipping deployed audit for "${w.name}": url is not http(s): ${JSON.stringify(w.url)}`
32
+ );
33
+ }
34
+ if (w.gitRepo) site.gitRepo = w.gitRepo;
35
+ if (w.netlifyId) site.netlifyId = w.netlifyId;
36
+ return [site];
37
+ });
38
+ }
39
+ function requireFleetWorkdir(explicit, provider) {
40
+ const workdir = explicit ?? process.env.REDDOOR_FLEET_WORKDIR;
41
+ if (!workdir) {
42
+ throw new Error(
43
+ `${provider} requires \`workdir\` option or REDDOOR_FLEET_WORKDIR env (sites need a local path)`
44
+ );
45
+ }
46
+ return workdir;
47
+ }
48
+
49
+ export {
50
+ selectFleetSites,
51
+ requireFleetWorkdir
52
+ };
53
+ //# sourceMappingURL=chunk-VQMAAQ5M.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/inventory/select.ts"],"sourcesContent":["import type { Site } from \"../types.js\";\nimport { siteSlug, ACTIVE_STATUSES, isPreLaunch, type WebsiteRow } from \"../fleet/site-row.js\";\nimport { isHttpUrl } from \"../util/url.js\";\n\n/**\n * THE fleet-sweep selection rule, over rows from either store (#646 step 4).\n *\n * Every `--fleet` sweep — the four nightly audits, the Prismic drift sweep,\n * `prismic-ci`, `init`, `sync-configs` and the rest — visits exactly the sites this\n * returns. It lived inside `fromAirtableBase` until step 4 moved the roster to\n * Turso; it is shared rather than copied so the Airtable and Turso providers\n * cannot drift, and `tests/inventory/selection-parity.test.ts` proves that the\n * two stores, fed the same rows, hand it identical input.\n *\n * Only LIVE `maintained` sites that have a `url` are included — pre-launch\n * stages (\"launching\" / \"building\", via isPreLaunch) are excluded so a\n * not-yet-live site is never audited as production (its deploy/domain/uptime/CMS\n * audits would fail against nothing and red its row) nor swept by other `--fleet`\n * ops; it re-enters the fleet when a Launch report flips its Status to\n * \"maintained\". The production URL is exposed as `Site.deployedUrl` so the\n * lighthouse audit can run against it with no checkout. `repoUrl` is\n * intentionally NOT set from `url` — a clone source must come from `gitRepo`\n * (`owner/repo`), never the production URL.\n *\n * The id is carried as `meta.siteId` and never interpreted: a `rec…` id and a\n * `site_<ULID>` id (#646 step 3) select identically.\n */\nexport function selectFleetSites(websites: readonly WebsiteRow[], workdir: string): Site[] {\n return websites\n .filter(\n (w) =>\n w.status !== null &&\n ACTIVE_STATUSES.has(w.status) &&\n !isPreLaunch(w.status) &&\n w.url.length > 0,\n )\n .flatMap((w) => {\n const slug = siteSlug(w.name);\n // An empty slug (a Name with no slug-able characters) can't form a stable\n // path and — fatally — can't be matched back to its row on write-back:\n // every empty-slug site would collapse under the \"\" key and mis-write or\n // fail. Skip it loudly rather than silently mis-map it.\n if (slug.length === 0) {\n console.warn(\n `[inventory] skipping \"${w.name}\" (row ${w.id}): Name has no slug-able characters (empty slug)`,\n );\n return [];\n }\n const site: Site = {\n path: `${workdir}/${slug}`,\n name: slug,\n meta: { siteId: w.id, displayName: w.name },\n };\n // Scheme-allowlist the `url` before exposing it as the deployed-audit\n // target (it's handed straight to Chrome/lhci). A `file://`/`gopher://`/\n // internal-host value would be a local-file read or SSRF — skip the\n // deployed audit for that site rather than trust it.\n if (isHttpUrl(w.url)) {\n site.deployedUrl = w.url;\n } else {\n console.warn(\n `[inventory] skipping deployed audit for \"${w.name}\": url is not http(s): ${JSON.stringify(w.url)}`,\n );\n }\n if (w.gitRepo) site.gitRepo = w.gitRepo;\n // Expose the Netlify site id (operator-set identity column) so the\n // netlify-deploy audit can query the API with no checkout. Absent → that\n // audit skips for this site. Not derived from the URL.\n if (w.netlifyId) site.netlifyId = w.netlifyId;\n return [site];\n });\n}\n\n/** The workdir every store-backed provider needs: sites carry a local path, and\n * no store holds checkout paths. */\nexport function requireFleetWorkdir(explicit: string | undefined, provider: string): string {\n const workdir = explicit ?? process.env.REDDOOR_FLEET_WORKDIR;\n if (!workdir) {\n throw new Error(\n `${provider} requires \\`workdir\\` option or REDDOOR_FLEET_WORKDIR env (sites need a local path)`,\n );\n }\n return workdir;\n}\n"],"mappings":";;;;;;;;;;AA2BO,SAAS,iBAAiB,UAAiC,SAAyB;AACzF,SAAO,SACJ;AAAA,IACC,CAAC,MACC,EAAE,WAAW,QACb,gBAAgB,IAAI,EAAE,MAAM,KAC5B,CAAC,YAAY,EAAE,MAAM,KACrB,EAAE,IAAI,SAAS;AAAA,EACnB,EACC,QAAQ,CAAC,MAAM;AACd,UAAM,OAAO,SAAS,EAAE,IAAI;AAK5B,QAAI,KAAK,WAAW,GAAG;AACrB,cAAQ;AAAA,QACN,yBAAyB,EAAE,IAAI,UAAU,EAAE,EAAE;AAAA,MAC/C;AACA,aAAO,CAAC;AAAA,IACV;AACA,UAAM,OAAa;AAAA,MACjB,MAAM,GAAG,OAAO,IAAI,IAAI;AAAA,MACxB,MAAM;AAAA,MACN,MAAM,EAAE,QAAQ,EAAE,IAAI,aAAa,EAAE,KAAK;AAAA,IAC5C;AAKA,QAAI,UAAU,EAAE,GAAG,GAAG;AACpB,WAAK,cAAc,EAAE;AAAA,IACvB,OAAO;AACL,cAAQ;AAAA,QACN,4CAA4C,EAAE,IAAI,0BAA0B,KAAK,UAAU,EAAE,GAAG,CAAC;AAAA,MACnG;AAAA,IACF;AACA,QAAI,EAAE,QAAS,MAAK,UAAU,EAAE;AAIhC,QAAI,EAAE,UAAW,MAAK,YAAY,EAAE;AACpC,WAAO,CAAC,IAAI;AAAA,EACd,CAAC;AACL;AAIO,SAAS,oBAAoB,UAA8B,UAA0B;AAC1F,QAAM,UAAU,YAAY,QAAQ,IAAI;AACxC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR,GAAG,QAAQ;AAAA,IACb;AAAA,EACF;AACA,SAAO;AACT;","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-WMF5S2MB.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 is not in the fleet roster yet (`--fleet turso` 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":[]}
@@ -1,8 +1,9 @@
1
1
  // src/db/freeze.ts
2
2
  var TURSO_IS_AUTHORITATIVE = true;
3
3
  async function mirrorWrite(label, run, strict = TURSO_IS_AUTHORITATIVE) {
4
+ let matched;
4
5
  try {
5
- await run();
6
+ matched = await run();
6
7
  } catch (err) {
7
8
  if (strict) {
8
9
  throw new Error(
@@ -11,6 +12,15 @@ async function mirrorWrite(label, run, strict = TURSO_IS_AUTHORITATIVE) {
11
12
  );
12
13
  }
13
14
  console.error(`[${label}] Turso mirror failed (sync will converge it): ${String(err)}`);
15
+ return;
16
+ }
17
+ if (matched === false) {
18
+ console.error(`[${label}] Turso mirror matched no row (mirrored=missed)`);
19
+ if (strict) {
20
+ throw new Error(
21
+ `[${label}] Turso mirror matched no row and Turso is authoritative \u2014 no such row in Turso, and nothing converges a row that was never inserted`
22
+ );
23
+ }
14
24
  }
15
25
  }
16
26
 
@@ -18,4 +28,4 @@ export {
18
28
  TURSO_IS_AUTHORITATIVE,
19
29
  mirrorWrite
20
30
  };
21
- //# sourceMappingURL=chunk-SVAAONYB.js.map
31
+ //# sourceMappingURL=chunk-WQLNUEZL.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/db/freeze.ts"],"sourcesContent":["/** #612 (#539 Phase 5): the one switch that says which store is allowed to fail.\n *\n * ## Why a switch exists at all\n *\n * Through Phase 5 the mirrors are best-effort by design: Airtable is\n * authoritative, so a Turso write that fails is caught, logged and swallowed,\n * and the hourly `fleet-db-sync` import converges whatever it missed. That is\n * correct — right up until the freeze stops the import.\n *\n * After that, nothing converges anything. The same swallowed failure becomes\n * permanent data loss announced only by a log line nobody is grepping. Three\n * outcomes change meaning at the flip:\n *\n * | outcome | before the freeze | after |\n * |-------------------|--------------------------------|------------------------------|\n * | `mirrored=0` | the sync will fix it | that write is gone |\n * | `mirrored=missed` | the site isn't imported yet | impossible, therefore a bug |\n * | `mirrored=absent` | no creds; Airtable still has it| every write was discarded |\n *\n * So the freeze is not a config change. It is: invert which store is allowed\n * to fail.\n *\n * ## Why a code constant rather than an env var\n *\n * The same built artifact runs in Netlify functions and in Actions runners. An\n * env var could be set in one and missed in the other, giving a PARTIAL freeze\n * — half the fleet treating Turso as authoritative and half not — which is a\n * worse state than either end. A constant is uniform by construction, and\n * flipping it is a one-line pull request with a diff a reviewer can see.\n *\n * This is the same shape the site-status vocabulary migration used: code\n * handles both worlds, then one reviewed commit flips the switch.\n *\n * ## Reading this correctly\n *\n * Consumers take it as a DEFAULT parameter rather than reading it inline, so\n * tests can exercise both sides as fixtures. Exactly one test asserts the\n * shipped value; every behavioural test injects. A suite that only ever read\n * the shipped constant would prove nothing about the state it is not in.\n */\n\n/** `true` since the freeze (flipped 2026-08-31, the same PR that retired the\n * hourly `fleet-db-sync` import — the two must move together, and did): Turso\n * is the store that must succeed. The Airtable write is kept as the shadow for\n * the one-week rollback window — still written, and DELIBERATELY still allowed\n * to fail its caller: a shadow you might roll back to is one you keep\n * trustworthy, so an Airtable outage still reds writes until Phase 6 deletes\n * the shadow and the Airtable client layer with it.\n *\n * Go/no-go recorded on #612: `FLEET_PARITY sites=44 health=44 schedule=44\n * reports=17 mismatches=0` immediately before the flip. */\nexport const TURSO_IS_AUTHORITATIVE = true;\n\n/**\n * Run one Turso mirror write with the error semantics the current world calls\n * for: swallowed while Airtable is authoritative, fatal once Turso is.\n *\n * The mirror FACTORIES (`makeSiteMirror`, `makeReportMirror`, the health mirrors)\n * already do this internally. The Netlify request handlers do not use those\n * factories — `approve-report`, `report-commentary`, `resend-webhook` and\n * `site-details` each call `mirrorReportPatch` / `mirrorSiteField` directly,\n * wrapped in a hand-rolled `try { … } catch { console.error }`. Four independent\n * copies of the swallow, none of which consulted the switch, each carrying a\n * comment saying \"the sync converges it\".\n *\n * **That comment stops being true at the freeze.** With the hourly import\n * stopped, a swallowed mirror failure on an approve, a commentary edit, a\n * delivery-status webhook or a site-detail edit is permanent data loss whose only\n * trace is a log line nobody greps — and the Airtable write it shadows will have\n * already succeeded, so the two stores diverge silently.\n *\n * Under `strict` the failure is raised instead. The caller's own error handling\n * decides what that means for the response; what it must not mean is \"logged and\n * forgotten\". `strict` is a parameter with the constant as its default so tests\n * exercise both worlds as fixtures rather than only the shipped one.\n */\nexport async function mirrorWrite(\n label: string,\n run: () => Promise<void>,\n strict: boolean = TURSO_IS_AUTHORITATIVE,\n): Promise<void> {\n try {\n await run();\n } catch (err) {\n if (strict) {\n throw new Error(\n `[${label}] Turso mirror failed and Turso is authoritative — ` +\n `the write is NOT recoverable by the hourly sync: ${String(err)}`,\n { cause: err },\n );\n }\n console.error(`[${label}] Turso mirror failed (sync will converge it): ${String(err)}`);\n }\n}\n"],"mappings":";AAmDO,IAAM,yBAAyB;AAyBtC,eAAsB,YACpB,OACA,KACA,SAAkB,wBACH;AACf,MAAI;AACF,UAAM,IAAI;AAAA,EACZ,SAAS,KAAK;AACZ,QAAI,QAAQ;AACV,YAAM,IAAI;AAAA,QACR,IAAI,KAAK,4GAC6C,OAAO,GAAG,CAAC;AAAA,QACjE,EAAE,OAAO,IAAI;AAAA,MACf;AAAA,IACF;AACA,YAAQ,MAAM,IAAI,KAAK,kDAAkD,OAAO,GAAG,CAAC,EAAE;AAAA,EACxF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/db/freeze.ts"],"sourcesContent":["/** #612 (#539 Phase 5): the one switch that says which store is allowed to fail.\n *\n * ## Why a switch exists at all\n *\n * Through Phase 5 the mirrors are best-effort by design: Airtable is\n * authoritative, so a Turso write that fails is caught, logged and swallowed,\n * and the hourly `fleet-db-sync` import converges whatever it missed. That is\n * correct — right up until the freeze stops the import.\n *\n * After that, nothing converges anything. The same swallowed failure becomes\n * permanent data loss announced only by a log line nobody is grepping. Three\n * outcomes change meaning at the flip:\n *\n * | outcome | before the freeze | after |\n * |-------------------|--------------------------------|------------------------------|\n * | `mirrored=0` | the sync will fix it | that write is gone |\n * | `mirrored=missed` | the site isn't imported yet | impossible, therefore a bug |\n * | `mirrored=absent` | no creds; Airtable still has it| every write was discarded |\n *\n * So the freeze is not a config change. It is: invert which store is allowed\n * to fail.\n *\n * ## Why a code constant rather than an env var\n *\n * The same built artifact runs in Netlify functions and in Actions runners. An\n * env var could be set in one and missed in the other, giving a PARTIAL freeze\n * — half the fleet treating Turso as authoritative and half not — which is a\n * worse state than either end. A constant is uniform by construction, and\n * flipping it is a one-line pull request with a diff a reviewer can see.\n *\n * This is the same shape the site-status vocabulary migration used: code\n * handles both worlds, then one reviewed commit flips the switch.\n *\n * ## Reading this correctly\n *\n * Consumers take it as a DEFAULT parameter rather than reading it inline, so\n * tests can exercise both sides as fixtures. Exactly one test asserts the\n * shipped value; every behavioural test injects. A suite that only ever read\n * the shipped constant would prove nothing about the state it is not in.\n */\n\n/** `true` since the freeze (flipped 2026-08-31, the same PR that retired the\n * hourly `fleet-db-sync` import — the two must move together, and did): Turso\n * is the store that must succeed. The Airtable write is kept as the shadow for\n * the one-week rollback window — still written, and DELIBERATELY still allowed\n * to fail its caller: a shadow you might roll back to is one you keep\n * trustworthy, so an Airtable outage still reds writes until Phase 6 deletes\n * the shadow and the Airtable client layer with it.\n *\n * Go/no-go recorded on #612: `FLEET_PARITY sites=44 health=44 schedule=44\n * reports=17 mismatches=0` immediately before the flip. */\nexport const TURSO_IS_AUTHORITATIVE = true;\n\n/**\n * Run one Turso mirror write with the error semantics the current world calls\n * for: swallowed while Airtable is authoritative, fatal once Turso is.\n *\n * The mirror FACTORIES (`makeSiteMirror`, `makeReportMirror`, the health mirrors)\n * already do this internally. The Netlify request handlers do not use those\n * factories — `approve-report`, `report-commentary`, `resend-webhook` and\n * `site-details` each call `mirrorReportPatch` / `mirrorSiteField` directly,\n * wrapped in a hand-rolled `try { … } catch { console.error }`. Four independent\n * copies of the swallow, none of which consulted the switch, each carrying a\n * comment saying \"the sync converges it\".\n *\n * **That comment stops being true at the freeze.** With the hourly import\n * stopped, a swallowed mirror failure on an approve, a commentary edit, a\n * delivery-status webhook or a site-detail edit is permanent data loss whose only\n * trace is a log line nobody greps — and the Airtable write it shadows will have\n * already succeeded, so the two stores diverge silently.\n *\n * Under `strict` the failure is raised instead. The caller's own error handling\n * decides what that means for the response; what it must not mean is \"logged and\n * forgotten\". `strict` is a parameter with the constant as its default so tests\n * exercise both worlds as fixtures rather than only the shipped one.\n */\nexport async function mirrorWrite(\n label: string,\n /** Resolves `false` when the write's UPDATE matched no row (#647) — the\n * writers that report a row count (`mirrorReportPatch`, `mirrorSiteFields`,\n * …) hand it straight through; a writer that reports nothing resolves void,\n * which counts as landed. */\n run: () => Promise<void | boolean>,\n strict: boolean = TURSO_IS_AUTHORITATIVE,\n): Promise<void> {\n let matched: void | boolean;\n try {\n matched = await run();\n } catch (err) {\n if (strict) {\n throw new Error(\n `[${label}] Turso mirror failed and Turso is authoritative — ` +\n `the write is NOT recoverable by the hourly sync: ${String(err)}`,\n { cause: err },\n );\n }\n console.error(`[${label}] Turso mirror failed (sync will converge it): ${String(err)}`);\n return;\n }\n // `missed` is its own outcome, distinct from both success and failure (#647):\n // the UPDATE ran fine and matched no row. Before the freeze the hourly sync\n // would import the row and the next write would land; after it nothing\n // converges the miss, so a green run here would be claiming a write that\n // never happened. Same rule `makeSiteMirror` already applies.\n if (matched === false) {\n console.error(`[${label}] Turso mirror matched no row (mirrored=missed)`);\n if (strict) {\n throw new Error(\n `[${label}] Turso mirror matched no row and Turso is authoritative — ` +\n `no such row in Turso, and nothing converges a row that was never inserted`,\n );\n }\n }\n}\n"],"mappings":";AAmDO,IAAM,yBAAyB;AAyBtC,eAAsB,YACpB,OAKA,KACA,SAAkB,wBACH;AACf,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,IAAI;AAAA,EACtB,SAAS,KAAK;AACZ,QAAI,QAAQ;AACV,YAAM,IAAI;AAAA,QACR,IAAI,KAAK,4GAC6C,OAAO,GAAG,CAAC;AAAA,QACjE,EAAE,OAAO,IAAI;AAAA,MACf;AAAA,IACF;AACA,YAAQ,MAAM,IAAI,KAAK,kDAAkD,OAAO,GAAG,CAAC,EAAE;AACtF;AAAA,EACF;AAMA,MAAI,YAAY,OAAO;AACrB,YAAQ,MAAM,IAAI,KAAK,iDAAiD;AACxE,QAAI,QAAQ;AACV,YAAM,IAAI;AAAA,QACR,IAAI,KAAK;AAAA,MAEX;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  siteSlug
3
- } from "./chunk-3Z6X3BHU.js";
3
+ } from "./chunk-ICSRWW5Q.js";
4
4
 
5
5
  // src/audits/lighthouse-airtable.ts
6
6
  import { readFile } from "fs/promises";
@@ -37,7 +37,7 @@ async function resolveSlugFromCwd(cwd) {
37
37
  return siteSlug(pkg.name);
38
38
  } catch (e) {
39
39
  throw new Error(
40
- `Could not derive site slug from ${cwd}/package.json: ${e.message}. Pass --write-airtable=<slug> explicitly.`,
40
+ `Could not derive site slug from ${cwd}/package.json: ${e.message}. Pass --write-back=<slug> explicitly.`,
41
41
  { cause: e }
42
42
  );
43
43
  }
@@ -48,4 +48,4 @@ export {
48
48
  lighthouseScoresFromResult,
49
49
  resolveSlugFromCwd
50
50
  };
51
- //# sourceMappingURL=chunk-XN37VTDB.js.map
51
+ //# sourceMappingURL=chunk-X56LAKPR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/audits/lighthouse-airtable.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult } from \"../types.js\";\nimport type { LighthouseScoreWriteback } from \"../reports/types.js\";\nimport { siteSlug } from \"../reports/airtable/websites.js\";\n\nconst LIGHTHOUSE_CATEGORIES = [\"performance\", \"accessibility\", \"best-practices\", \"seo\"] as const;\n\n/**\n * True when the result carries real lighthouse scores worth persisting.\n *\n * The `audit --write-back` policy used to refuse on any `status: \"fail\"`,\n * but that conflates two very different failure modes:\n * 1. Infrastructure failure (no lhr-*.json written, spawn timeout, etc.)\n * → `details.summary` empty → all-zeros would corrupt the dashboard\n * 2. Assertion failure (scores below threshold, e.g. best-practices < 0.9)\n * → `details.summary` has real numbers → tracking these IS the point\n *\n * The dashboard exists to surface drift over time. Refusing to write the\n * very numbers the dashboard wants to plot — just because one assertion\n * tripped — defeats the purpose. Write whenever real scores exist.\n */\nexport function hasRealScores(result: AuditResult): boolean {\n if (result.audit !== \"lighthouse\") return false;\n const details = (result.details ?? {}) as { summary?: Record<string, number> };\n const summary = details.summary ?? {};\n return LIGHTHOUSE_CATEGORIES.some(\n (k) => typeof summary[k] === \"number\" && !Number.isNaN(summary[k]),\n );\n}\n\n/**\n * Extract the four Lighthouse scores (as integer percentages) from a\n * `lighthouse` AuditResult. LHCI manifest summaries are floats in [0,1];\n * multiply + round. A category absent from the summary — e.g. Lighthouse\n * errored its audit (NO_LCP nulls the whole performance category) — yields\n * `null`, NOT 0, so the write path can clear the Airtable cell (\"—\") instead\n * of persisting a misleading zero. (`hasRealScores` still gates the write on\n * at least one real number, so an all-null infra failure is refused upstream.)\n */\nexport function lighthouseScoresFromResult(result: AuditResult): LighthouseScoreWriteback {\n if (result.audit !== \"lighthouse\") {\n throw new Error(`Expected a 'lighthouse' AuditResult, got '${result.audit}'`);\n }\n const details = (result.details ?? {}) as { summary?: Record<string, number> };\n const summary = details.summary ?? {};\n const toPct = (n: number | undefined): number | null =>\n typeof n === \"number\" && !Number.isNaN(n) ? Math.round(n * 100) : null;\n return {\n performance: toPct(summary[\"performance\"]),\n accessibility: toPct(summary[\"accessibility\"]),\n bestPractices: toPct(summary[\"best-practices\"]),\n seo: toPct(summary[\"seo\"]),\n };\n}\n\n/**\n * Derive a site slug from the cwd's package.json#name. Used by\n * `audit lighthouse --write-back` when the operator doesn't pass an\n * explicit slug — the cwd is the site checkout, so package.json#name is\n * usually the canonical site name.\n */\nexport async function resolveSlugFromCwd(cwd: string): Promise<string> {\n try {\n const pkgPath = join(cwd, \"package.json\");\n const raw = await readFile(pkgPath, \"utf-8\");\n const pkg = JSON.parse(raw) as { name?: string };\n if (!pkg.name) throw new Error(\"package.json has no 'name' field\");\n return siteSlug(pkg.name);\n } catch (e) {\n throw new Error(\n `Could not derive site slug from ${cwd}/package.json: ${(e as Error).message}. ` +\n `Pass --write-back=<slug> explicitly.`,\n { cause: e },\n );\n }\n}\n"],"mappings":";;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,YAAY;AAKrB,IAAM,wBAAwB,CAAC,eAAe,iBAAiB,kBAAkB,KAAK;AAgB/E,SAAS,cAAc,QAA8B;AAC1D,MAAI,OAAO,UAAU,aAAc,QAAO;AAC1C,QAAM,UAAW,OAAO,WAAW,CAAC;AACpC,QAAM,UAAU,QAAQ,WAAW,CAAC;AACpC,SAAO,sBAAsB;AAAA,IAC3B,CAAC,MAAM,OAAO,QAAQ,CAAC,MAAM,YAAY,CAAC,OAAO,MAAM,QAAQ,CAAC,CAAC;AAAA,EACnE;AACF;AAWO,SAAS,2BAA2B,QAA+C;AACxF,MAAI,OAAO,UAAU,cAAc;AACjC,UAAM,IAAI,MAAM,6CAA6C,OAAO,KAAK,GAAG;AAAA,EAC9E;AACA,QAAM,UAAW,OAAO,WAAW,CAAC;AACpC,QAAM,UAAU,QAAQ,WAAW,CAAC;AACpC,QAAM,QAAQ,CAAC,MACb,OAAO,MAAM,YAAY,CAAC,OAAO,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,GAAG,IAAI;AACpE,SAAO;AAAA,IACL,aAAa,MAAM,QAAQ,aAAa,CAAC;AAAA,IACzC,eAAe,MAAM,QAAQ,eAAe,CAAC;AAAA,IAC7C,eAAe,MAAM,QAAQ,gBAAgB,CAAC;AAAA,IAC9C,KAAK,MAAM,QAAQ,KAAK,CAAC;AAAA,EAC3B;AACF;AAQA,eAAsB,mBAAmB,KAA8B;AACrE,MAAI;AACF,UAAM,UAAU,KAAK,KAAK,cAAc;AACxC,UAAM,MAAM,MAAM,SAAS,SAAS,OAAO;AAC3C,UAAM,MAAM,KAAK,MAAM,GAAG;AAC1B,QAAI,CAAC,IAAI,KAAM,OAAM,IAAI,MAAM,kCAAkC;AACjE,WAAO,SAAS,IAAI,IAAI;AAAA,EAC1B,SAAS,GAAG;AACV,UAAM,IAAI;AAAA,MACR,mCAAmC,GAAG,kBAAmB,EAAY,OAAO;AAAA,MAE5E,EAAE,OAAO,EAAE;AAAA,IACb;AAAA,EACF;AACF;","names":[]}