@recombine-ai/bosun 0.16.2 → 0.17.1

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 (524) hide show
  1. package/README.md +24 -0
  2. package/bin/bosun.js +153 -20
  3. package/bin/git-mode.js +1139 -0
  4. package/build/client/_app/immutable/assets/0.D9HpHezk.css +1 -0
  5. package/build/client/_app/immutable/assets/0.D9HpHezk.css.br +0 -0
  6. package/build/client/_app/immutable/assets/0.D9HpHezk.css.gz +0 -0
  7. package/build/client/_app/immutable/assets/{2.DtxQDOXx.css → 2.Bc0Sf3YD.css} +1 -1
  8. package/build/client/_app/immutable/assets/2.Bc0Sf3YD.css.br +0 -0
  9. package/build/client/_app/immutable/assets/2.Bc0Sf3YD.css.gz +0 -0
  10. package/build/client/_app/immutable/chunks/{DneQQ7D5.js → -3ZOIHDA.js} +1 -1
  11. package/build/client/_app/immutable/chunks/-3ZOIHDA.js.br +0 -0
  12. package/build/client/_app/immutable/chunks/-3ZOIHDA.js.gz +0 -0
  13. package/build/client/_app/immutable/chunks/7hV1SZxK.js +1 -0
  14. package/build/client/_app/immutable/chunks/7hV1SZxK.js.br +2 -0
  15. package/build/client/_app/immutable/chunks/7hV1SZxK.js.gz +0 -0
  16. package/build/client/_app/immutable/chunks/B-dTD_wm.js +1 -0
  17. package/build/client/_app/immutable/chunks/B-dTD_wm.js.br +0 -0
  18. package/build/client/_app/immutable/chunks/B-dTD_wm.js.gz +0 -0
  19. package/build/client/_app/immutable/chunks/{UqjCSZmA.js → B-iasAJv.js} +1 -1
  20. package/build/client/_app/immutable/chunks/B-iasAJv.js.br +0 -0
  21. package/build/client/_app/immutable/chunks/B-iasAJv.js.gz +0 -0
  22. package/build/client/_app/immutable/chunks/BDijIo6D.js +33 -0
  23. package/build/client/_app/immutable/chunks/BDijIo6D.js.br +0 -0
  24. package/build/client/_app/immutable/chunks/BDijIo6D.js.gz +0 -0
  25. package/build/client/_app/immutable/chunks/BHB0Pj1h.js +2 -0
  26. package/build/client/_app/immutable/chunks/BHB0Pj1h.js.br +0 -0
  27. package/build/client/_app/immutable/chunks/BHB0Pj1h.js.gz +0 -0
  28. package/build/client/_app/immutable/chunks/{Bog028VR.js → BKfmN1Rj.js} +1 -1
  29. package/build/client/_app/immutable/chunks/BKfmN1Rj.js.br +0 -0
  30. package/build/client/_app/immutable/chunks/BKfmN1Rj.js.gz +0 -0
  31. package/build/client/_app/immutable/chunks/{pPpIn6tB.js → BlFOg-Sd.js} +1 -1
  32. package/build/client/_app/immutable/chunks/BlFOg-Sd.js.br +0 -0
  33. package/build/client/_app/immutable/chunks/BlFOg-Sd.js.gz +0 -0
  34. package/build/client/_app/immutable/chunks/{CJcZ2GVC.js → Byg3jr1u.js} +1 -1
  35. package/build/client/_app/immutable/chunks/Byg3jr1u.js.br +0 -0
  36. package/build/client/_app/immutable/chunks/Byg3jr1u.js.gz +0 -0
  37. package/build/client/_app/immutable/chunks/{Bmd5zhWC.js → BzxxLgHi.js} +1 -1
  38. package/build/client/_app/immutable/chunks/BzxxLgHi.js.br +0 -0
  39. package/build/client/_app/immutable/chunks/BzxxLgHi.js.gz +0 -0
  40. package/build/client/_app/immutable/chunks/{CEHsomMo.js → C4UC6AgE.js} +1 -1
  41. package/build/client/_app/immutable/chunks/C4UC6AgE.js.br +0 -0
  42. package/build/client/_app/immutable/chunks/{CEHsomMo.js.gz → C4UC6AgE.js.gz} +0 -0
  43. package/build/client/_app/immutable/chunks/C60AG6QQ.js +1 -0
  44. package/build/client/_app/immutable/chunks/C60AG6QQ.js.br +0 -0
  45. package/build/client/_app/immutable/chunks/C60AG6QQ.js.gz +0 -0
  46. package/build/client/_app/immutable/chunks/CAIfVN23.js +1 -0
  47. package/build/client/_app/immutable/chunks/CAIfVN23.js.br +0 -0
  48. package/build/client/_app/immutable/chunks/CAIfVN23.js.gz +0 -0
  49. package/build/client/_app/immutable/chunks/{JcG4w2gF.js → CB9iDxwI.js} +1 -1
  50. package/build/client/_app/immutable/chunks/CB9iDxwI.js.br +0 -0
  51. package/build/client/_app/immutable/chunks/CB9iDxwI.js.gz +0 -0
  52. package/build/client/_app/immutable/chunks/{DoD3q5w5.js → CD_KdWs9.js} +1 -1
  53. package/build/client/_app/immutable/chunks/CD_KdWs9.js.br +0 -0
  54. package/build/client/_app/immutable/chunks/CD_KdWs9.js.gz +0 -0
  55. package/build/client/_app/immutable/chunks/CDy1LBR5.js +1 -0
  56. package/build/client/_app/immutable/chunks/CDy1LBR5.js.br +0 -0
  57. package/build/client/_app/immutable/chunks/CDy1LBR5.js.gz +0 -0
  58. package/build/client/_app/immutable/chunks/{DCXjBDmY.js → CMFNmSvo.js} +1 -1
  59. package/build/client/_app/immutable/chunks/CMFNmSvo.js.br +0 -0
  60. package/build/client/_app/immutable/chunks/CMFNmSvo.js.gz +0 -0
  61. package/build/client/_app/immutable/chunks/{Bv1mNXs0.js → CNIn4L55.js} +1 -1
  62. package/build/client/_app/immutable/chunks/CNIn4L55.js.br +0 -0
  63. package/build/client/_app/immutable/chunks/CNIn4L55.js.gz +0 -0
  64. package/build/client/_app/immutable/chunks/CUKoVL36.js +1 -0
  65. package/build/client/_app/immutable/chunks/CUKoVL36.js.br +0 -0
  66. package/build/client/_app/immutable/chunks/CUKoVL36.js.gz +0 -0
  67. package/build/client/_app/immutable/chunks/CYGbCr1i.js +3 -0
  68. package/build/client/_app/immutable/chunks/CYGbCr1i.js.br +0 -0
  69. package/build/client/_app/immutable/chunks/CYGbCr1i.js.gz +0 -0
  70. package/build/client/_app/immutable/chunks/CYr0FGgn.js +1 -0
  71. package/build/client/_app/immutable/chunks/CYr0FGgn.js.br +0 -0
  72. package/build/client/_app/immutable/chunks/CYr0FGgn.js.gz +0 -0
  73. package/build/client/_app/immutable/chunks/{DtXXy94i.js → Ce6a22qU.js} +1 -1
  74. package/build/client/_app/immutable/chunks/Ce6a22qU.js.br +0 -0
  75. package/build/client/_app/immutable/chunks/Ce6a22qU.js.gz +0 -0
  76. package/build/client/_app/immutable/chunks/{DfcDjAv-.js → CeoQ-OhK.js} +1 -1
  77. package/build/client/_app/immutable/chunks/CeoQ-OhK.js.br +0 -0
  78. package/build/client/_app/immutable/chunks/CeoQ-OhK.js.gz +0 -0
  79. package/build/client/_app/immutable/chunks/{D8mA-uZ4.js → CupXtwVS.js} +1 -1
  80. package/build/client/_app/immutable/chunks/CupXtwVS.js.br +0 -0
  81. package/build/client/_app/immutable/chunks/CupXtwVS.js.gz +0 -0
  82. package/build/client/_app/immutable/chunks/{B5zf3x62.js → Cy2Uze-c.js} +1 -1
  83. package/build/client/_app/immutable/chunks/Cy2Uze-c.js.br +0 -0
  84. package/build/client/_app/immutable/chunks/Cy2Uze-c.js.gz +0 -0
  85. package/build/client/_app/immutable/chunks/{Dg0A8wPA.js → D8Lmn3S5.js} +1 -1
  86. package/build/client/_app/immutable/chunks/D8Lmn3S5.js.br +0 -0
  87. package/build/client/_app/immutable/chunks/D8Lmn3S5.js.gz +0 -0
  88. package/build/client/_app/immutable/chunks/D8doM1Y6.js +1 -0
  89. package/build/client/_app/immutable/chunks/D8doM1Y6.js.br +2 -0
  90. package/build/client/_app/immutable/chunks/D8doM1Y6.js.gz +0 -0
  91. package/build/client/_app/immutable/chunks/{BHHfrjbV.js → D9BWOD7V.js} +1 -1
  92. package/build/client/_app/immutable/chunks/D9BWOD7V.js.br +3 -0
  93. package/build/client/_app/immutable/chunks/D9BWOD7V.js.gz +0 -0
  94. package/build/client/_app/immutable/chunks/{Cp_st-64.js → DBB88ZB7.js} +1 -1
  95. package/build/client/_app/immutable/chunks/DBB88ZB7.js.br +2 -0
  96. package/build/client/_app/immutable/chunks/DBB88ZB7.js.gz +0 -0
  97. package/build/client/_app/immutable/chunks/{Dm49pcMm.js → DC7lPrn4.js} +1 -1
  98. package/build/client/_app/immutable/chunks/DC7lPrn4.js.br +0 -0
  99. package/build/client/_app/immutable/chunks/DC7lPrn4.js.gz +0 -0
  100. package/build/client/_app/immutable/chunks/DCwWNTpf.js +9 -0
  101. package/build/client/_app/immutable/chunks/DCwWNTpf.js.br +0 -0
  102. package/build/client/_app/immutable/chunks/DCwWNTpf.js.gz +0 -0
  103. package/build/client/_app/immutable/chunks/{wcTQUnZO.js → DEnUeQNt.js} +1 -1
  104. package/build/client/_app/immutable/chunks/DEnUeQNt.js.br +0 -0
  105. package/build/client/_app/immutable/chunks/DEnUeQNt.js.gz +0 -0
  106. package/build/client/_app/immutable/chunks/D_laxq3h.js +1 -0
  107. package/build/client/_app/immutable/chunks/D_laxq3h.js.br +0 -0
  108. package/build/client/_app/immutable/chunks/D_laxq3h.js.gz +0 -0
  109. package/build/client/_app/immutable/chunks/{Ddv5LgEc.js → DfkYlKBk.js} +1 -1
  110. package/build/client/_app/immutable/chunks/DfkYlKBk.js.br +0 -0
  111. package/build/client/_app/immutable/chunks/DfkYlKBk.js.gz +0 -0
  112. package/build/client/_app/immutable/chunks/DnMS2X6M.js +2 -0
  113. package/build/client/_app/immutable/chunks/DnMS2X6M.js.br +0 -0
  114. package/build/client/_app/immutable/chunks/DnMS2X6M.js.gz +0 -0
  115. package/build/client/_app/immutable/chunks/{B1HdnQRQ.js → DrE1ro9S.js} +1 -1
  116. package/build/client/_app/immutable/chunks/DrE1ro9S.js.br +0 -0
  117. package/build/client/_app/immutable/chunks/DrE1ro9S.js.gz +0 -0
  118. package/build/client/_app/immutable/chunks/DzY_RPb9.js +1 -0
  119. package/build/client/_app/immutable/chunks/DzY_RPb9.js.br +0 -0
  120. package/build/client/_app/immutable/chunks/DzY_RPb9.js.gz +0 -0
  121. package/build/client/_app/immutable/chunks/PrCSpCPg.js +1 -0
  122. package/build/client/_app/immutable/chunks/PrCSpCPg.js.br +0 -0
  123. package/build/client/_app/immutable/chunks/PrCSpCPg.js.gz +0 -0
  124. package/build/client/_app/immutable/chunks/{CN555stc.js → eMofpOrR.js} +1 -1
  125. package/build/client/_app/immutable/chunks/eMofpOrR.js.br +0 -0
  126. package/build/client/_app/immutable/chunks/eMofpOrR.js.gz +0 -0
  127. package/build/client/_app/immutable/entry/app.BQUuHkSR.js +2 -0
  128. package/build/client/_app/immutable/entry/app.BQUuHkSR.js.br +0 -0
  129. package/build/client/_app/immutable/entry/app.BQUuHkSR.js.gz +0 -0
  130. package/build/client/_app/immutable/entry/start.DoeTycdu.js +1 -0
  131. package/build/client/_app/immutable/entry/start.DoeTycdu.js.br +2 -0
  132. package/build/client/_app/immutable/entry/start.DoeTycdu.js.gz +0 -0
  133. package/build/client/_app/immutable/nodes/0.DBnFpwRM.js +4 -0
  134. package/build/client/_app/immutable/nodes/0.DBnFpwRM.js.br +0 -0
  135. package/build/client/_app/immutable/nodes/0.DBnFpwRM.js.gz +0 -0
  136. package/build/client/_app/immutable/nodes/1.D-kI-fLd.js +1 -0
  137. package/build/client/_app/immutable/nodes/1.D-kI-fLd.js.br +0 -0
  138. package/build/client/_app/immutable/nodes/1.D-kI-fLd.js.gz +0 -0
  139. package/build/client/_app/immutable/nodes/10.DWb5H7vX.js +28 -0
  140. package/build/client/_app/immutable/nodes/10.DWb5H7vX.js.br +0 -0
  141. package/build/client/_app/immutable/nodes/10.DWb5H7vX.js.gz +0 -0
  142. package/build/client/_app/immutable/nodes/{15.D2I0y3ys.js → 15.CGc3_07c.js} +1 -1
  143. package/build/client/_app/immutable/nodes/15.CGc3_07c.js.br +3 -0
  144. package/build/client/_app/immutable/nodes/15.CGc3_07c.js.gz +0 -0
  145. package/build/client/_app/immutable/nodes/17.BIba62hW.js +3 -0
  146. package/build/client/_app/immutable/nodes/17.BIba62hW.js.br +0 -0
  147. package/build/client/_app/immutable/nodes/17.BIba62hW.js.gz +0 -0
  148. package/build/client/_app/immutable/nodes/18.Clxr6FwI.js +11 -0
  149. package/build/client/_app/immutable/nodes/18.Clxr6FwI.js.br +0 -0
  150. package/build/client/_app/immutable/nodes/18.Clxr6FwI.js.gz +0 -0
  151. package/build/client/_app/immutable/nodes/2.cbpnR8be.js +35 -0
  152. package/build/client/_app/immutable/nodes/2.cbpnR8be.js.br +0 -0
  153. package/build/client/_app/immutable/nodes/2.cbpnR8be.js.gz +0 -0
  154. package/build/client/_app/immutable/nodes/3.DsP759xk.js +1 -0
  155. package/build/client/_app/immutable/nodes/3.DsP759xk.js.br +0 -0
  156. package/build/client/_app/immutable/nodes/3.DsP759xk.js.gz +0 -0
  157. package/build/client/_app/immutable/nodes/{4.C8VxVbVn.js → 4.BIGLAIDe.js} +1 -1
  158. package/build/client/_app/immutable/nodes/4.BIGLAIDe.js.br +0 -0
  159. package/build/client/_app/immutable/nodes/4.BIGLAIDe.js.gz +0 -0
  160. package/build/client/_app/immutable/nodes/{5.BjrnKYaA.js → 5.zMHOyhDv.js} +1 -1
  161. package/build/client/_app/immutable/nodes/5.zMHOyhDv.js.br +0 -0
  162. package/build/client/_app/immutable/nodes/5.zMHOyhDv.js.gz +0 -0
  163. package/build/client/_app/immutable/nodes/6.DFQ1IZsQ.js +1 -0
  164. package/build/client/_app/immutable/nodes/6.DFQ1IZsQ.js.br +0 -0
  165. package/build/client/_app/immutable/nodes/6.DFQ1IZsQ.js.gz +0 -0
  166. package/build/client/_app/immutable/nodes/8.CTgEdr7L.js +2 -0
  167. package/build/client/_app/immutable/nodes/8.CTgEdr7L.js.br +0 -0
  168. package/build/client/_app/immutable/nodes/8.CTgEdr7L.js.gz +0 -0
  169. package/build/client/_app/immutable/nodes/9.BidF5Uvq.js +2 -0
  170. package/build/client/_app/immutable/nodes/9.BidF5Uvq.js.br +0 -0
  171. package/build/client/_app/immutable/nodes/9.BidF5Uvq.js.gz +0 -0
  172. package/build/client/_app/version.json +1 -1
  173. package/build/client/_app/version.json.br +0 -0
  174. package/build/client/_app/version.json.gz +0 -0
  175. package/build/server/chunks/0-BZOFUIfF.js +9 -0
  176. package/build/server/chunks/0-BZOFUIfF.js.map +1 -0
  177. package/build/server/chunks/1-XwGU5VVN.js +9 -0
  178. package/build/server/chunks/{1-C0HdTvXW.js.map → 1-XwGU5VVN.js.map} +1 -1
  179. package/build/server/chunks/10-BgFSx4ib.js +9 -0
  180. package/build/server/chunks/10-BgFSx4ib.js.map +1 -0
  181. package/build/server/chunks/15-BjTTC1jf.js +9 -0
  182. package/build/server/chunks/{15-BQcb9C_y.js.map → 15-BjTTC1jf.js.map} +1 -1
  183. package/build/server/chunks/17-BEkWCfzc.js +9 -0
  184. package/build/server/chunks/17-BEkWCfzc.js.map +1 -0
  185. package/build/server/chunks/18-C-S0wKdp.js +9 -0
  186. package/build/server/chunks/18-C-S0wKdp.js.map +1 -0
  187. package/build/server/chunks/2-o-cIkIjs.js +9 -0
  188. package/build/server/chunks/2-o-cIkIjs.js.map +1 -0
  189. package/build/server/chunks/3-B-ABlyFO.js +9 -0
  190. package/build/server/chunks/3-B-ABlyFO.js.map +1 -0
  191. package/build/server/chunks/4-joAFA_Y8.js +9 -0
  192. package/build/server/chunks/4-joAFA_Y8.js.map +1 -0
  193. package/build/server/chunks/5-DIhC9k4l.js +9 -0
  194. package/build/server/chunks/5-DIhC9k4l.js.map +1 -0
  195. package/build/server/chunks/6-CMuAQIcW.js +9 -0
  196. package/build/server/chunks/6-CMuAQIcW.js.map +1 -0
  197. package/build/server/chunks/8-CbETTVRG.js +9 -0
  198. package/build/server/chunks/8-CbETTVRG.js.map +1 -0
  199. package/build/server/chunks/9-nMV90uVU.js +9 -0
  200. package/build/server/chunks/9-nMV90uVU.js.map +1 -0
  201. package/build/server/chunks/{AgentSidebar-BWBnqHnV.js → AgentSidebar-BEbSdZdN.js} +4 -4
  202. package/build/server/chunks/{AgentSidebar-BWBnqHnV.js.map → AgentSidebar-BEbSdZdN.js.map} +1 -1
  203. package/build/server/chunks/{Button-DH7gdU0s.js → Button-Dc9CR4Wh.js} +2 -2
  204. package/build/server/chunks/{Button-DH7gdU0s.js.map → Button-Dc9CR4Wh.js.map} +1 -1
  205. package/build/server/chunks/{ChatStepTrace-BivoNYOY.js → ChatStepTrace-OM96X4g8.js} +18 -7
  206. package/build/server/chunks/ChatStepTrace-OM96X4g8.js.map +1 -0
  207. package/build/server/chunks/{Dropdown-DnARl86Q.js → Dropdown-Cgd7pG-k.js} +2 -2
  208. package/build/server/chunks/{Dropdown-DnARl86Q.js.map → Dropdown-Cgd7pG-k.js.map} +1 -1
  209. package/build/server/chunks/GitControlsButton-BLcNzrDt.js +81 -0
  210. package/build/server/chunks/GitControlsButton-BLcNzrDt.js.map +1 -0
  211. package/build/server/chunks/{Icon-DP1YKCRV.js → Icon-Bogvl2n9.js} +2 -2
  212. package/build/server/chunks/{Icon-DP1YKCRV.js.map → Icon-Bogvl2n9.js.map} +1 -1
  213. package/build/server/chunks/{Input-CUJ9Cg7b.js → Input-7hpw1WLW.js} +2 -2
  214. package/build/server/chunks/{Input-CUJ9Cg7b.js.map → Input-7hpw1WLW.js.map} +1 -1
  215. package/build/server/chunks/{Modal-CGJuNpZ0.js → Modal-7IIIfFEG.js} +5 -5
  216. package/build/server/chunks/{Modal-CGJuNpZ0.js.map → Modal-7IIIfFEG.js.map} +1 -1
  217. package/build/server/chunks/{Panel-zvjxe036.js → Panel-Cj1mXx9-.js} +2 -2
  218. package/build/server/chunks/{Panel-zvjxe036.js.map → Panel-Cj1mXx9-.js.map} +1 -1
  219. package/build/server/chunks/{Tab-CUkyLqbN.js → Tab-bf4ZDs5y.js} +4 -4
  220. package/build/server/chunks/{Tab-CUkyLqbN.js.map → Tab-bf4ZDs5y.js.map} +1 -1
  221. package/build/server/chunks/{_layout.svelte-BnrWYuLv.js → _layout.svelte-C71luIwv.js} +10 -10
  222. package/build/server/chunks/{_layout.svelte-BnrWYuLv.js.map → _layout.svelte-C71luIwv.js.map} +1 -1
  223. package/build/server/chunks/{_layout.svelte-wi6o5Ecu.js → _layout.svelte-CFhHgXI_.js} +24 -18
  224. package/build/server/chunks/_layout.svelte-CFhHgXI_.js.map +1 -0
  225. package/build/server/chunks/_layout.svelte-CJd_pmkx.js +431 -0
  226. package/build/server/chunks/_layout.svelte-CJd_pmkx.js.map +1 -0
  227. package/build/server/chunks/{_layout.svelte-B88WMyk0.js → _layout.svelte-D6rhwm98.js} +445 -70
  228. package/build/server/chunks/_layout.svelte-D6rhwm98.js.map +1 -0
  229. package/build/server/chunks/{_page.svelte-CeS_21Ho.js → _page.svelte-BBvDMiTV.js} +25 -21
  230. package/build/server/chunks/_page.svelte-BBvDMiTV.js.map +1 -0
  231. package/build/server/chunks/{_page.svelte-h_J4lNT9.js → _page.svelte-BQs9dEuq.js} +8 -7
  232. package/build/server/chunks/{_page.svelte-h_J4lNT9.js.map → _page.svelte-BQs9dEuq.js.map} +1 -1
  233. package/build/server/chunks/{_page.svelte-DhFNFMnl.js → _page.svelte-BoO2LWVX.js} +34 -14
  234. package/build/server/chunks/_page.svelte-BoO2LWVX.js.map +1 -0
  235. package/build/server/chunks/{_page.svelte-CHgQjBQj.js → _page.svelte-DUCTSdv-.js} +2 -2
  236. package/build/server/chunks/{_page.svelte-CHgQjBQj.js.map → _page.svelte-DUCTSdv-.js.map} +1 -1
  237. package/build/server/chunks/_page.svelte-FXYLKyMw.js +110 -0
  238. package/build/server/chunks/_page.svelte-FXYLKyMw.js.map +1 -0
  239. package/build/server/chunks/{_page.svelte-57pyhszV.js → _page.svelte-KA1hqKj2.js} +20 -107
  240. package/build/server/chunks/_page.svelte-KA1hqKj2.js.map +1 -0
  241. package/build/server/chunks/{_page.svelte-BSVJ2d2G.js → _page.svelte-y8CZFL8a.js} +210 -106
  242. package/build/server/chunks/_page.svelte-y8CZFL8a.js.map +1 -0
  243. package/build/server/chunks/{_page.svelte-DHSYVLoj.js → _page.svelte-yIHSIEW6.js} +21 -15
  244. package/build/server/chunks/_page.svelte-yIHSIEW6.js.map +1 -0
  245. package/build/server/chunks/_server.ts-Cp9DgLcE.js +336 -0
  246. package/build/server/chunks/_server.ts-Cp9DgLcE.js.map +1 -0
  247. package/build/server/chunks/{agents-context-CbFsZmi5.js → agents-context-owgusUPR.js} +2 -2
  248. package/build/server/chunks/{agents-context-CbFsZmi5.js.map → agents-context-owgusUPR.js.map} +1 -1
  249. package/build/server/chunks/{client-DO-hqMbT.js → client-P-7bDULt.js} +4 -4
  250. package/build/server/chunks/{client-DO-hqMbT.js.map → client-P-7bDULt.js.map} +1 -1
  251. package/build/server/chunks/{conversation-view-memory-CtbUZd_L.js → conversation-view-memory-SdpdqTR7.js} +2 -2
  252. package/build/server/chunks/{conversation-view-memory-CtbUZd_L.js.map → conversation-view-memory-SdpdqTR7.js.map} +1 -1
  253. package/build/server/chunks/{edit-workspace-context-tyHBPujC.js → edit-workspace-context-hanFnXR3.js} +2 -2
  254. package/build/server/chunks/{edit-workspace-context-tyHBPujC.js.map → edit-workspace-context-hanFnXR3.js.map} +1 -1
  255. package/build/server/chunks/error.svelte-yI9RS6H4.js +15 -0
  256. package/build/server/chunks/{error.svelte-DmKKN1RU.js.map → error.svelte-yI9RS6H4.js.map} +1 -1
  257. package/build/server/chunks/git-mode-context-BpNsAUGF.js +18 -0
  258. package/build/server/chunks/git-mode-context-BpNsAUGF.js.map +1 -0
  259. package/build/server/chunks/{index-LMsd1ih2.js → index-DGtbUu8Y.js} +2 -2
  260. package/build/server/chunks/{index-LMsd1ih2.js.map → index-DGtbUu8Y.js.map} +1 -1
  261. package/build/server/chunks/{index-server-BRr5mZEq.js → index-server-l8-WsVn4.js} +2 -2
  262. package/build/server/chunks/{index-server-BRr5mZEq.js.map → index-server-l8-WsVn4.js.map} +1 -1
  263. package/build/server/chunks/{index2-ClgfzZ89.js → index2-CtgLiJVO.js} +3 -3
  264. package/build/server/chunks/{index2-ClgfzZ89.js.map → index2-CtgLiJVO.js.map} +1 -1
  265. package/build/server/chunks/{index3-BdRUAffj.js → index3-4hbbCqW0.js} +2 -2
  266. package/build/server/chunks/{index3-BdRUAffj.js.map → index3-4hbbCqW0.js.map} +1 -1
  267. package/build/server/chunks/{root-4q2Pr7ck.js → root-sWPnlpi3.js} +2 -2
  268. package/build/server/chunks/{root-4q2Pr7ck.js.map → root-sWPnlpi3.js.map} +1 -1
  269. package/build/server/chunks/{source-selection-CcN1cgXc.js → source-selection-DSkaPGfZ.js} +7 -2
  270. package/build/server/chunks/source-selection-DSkaPGfZ.js.map +1 -0
  271. package/build/server/chunks/{state.svelte-Bd1i-u49.js → state.svelte-BkvaElc6.js} +3 -3
  272. package/build/server/chunks/{state.svelte-Bd1i-u49.js.map → state.svelte-BkvaElc6.js.map} +1 -1
  273. package/build/server/chunks/trpc-client-Cr8RJ4UO.js +260 -0
  274. package/build/server/chunks/trpc-client-Cr8RJ4UO.js.map +1 -0
  275. package/build/server/index.js +4 -4
  276. package/build/server/index.js.map +1 -1
  277. package/build/server/manifest.js +15 -15
  278. package/build/server/manifest.js.map +1 -1
  279. package/package.json +1 -1
  280. package/src/backend/git-mode/command.spec.ts +172 -0
  281. package/src/backend/git-mode/command.ts +405 -0
  282. package/src/backend/git-mode/config.spec.ts +45 -0
  283. package/src/backend/git-mode/config.ts +90 -0
  284. package/src/backend/git-mode/index.ts +20 -0
  285. package/src/backend/git-mode/repository-write-gate.spec.ts +116 -0
  286. package/src/backend/git-mode/repository-write-gate.ts +260 -0
  287. package/src/backend/git-mode/router.ts +59 -0
  288. package/src/backend/git-mode/service.spec.ts +1653 -0
  289. package/src/backend/git-mode/service.ts +2183 -0
  290. package/src/backend/git-mode/types.ts +82 -0
  291. package/src/backend/git-mode/write-tracker.spec.ts +46 -0
  292. package/src/backend/git-mode/write-tracker.ts +39 -0
  293. package/src/backend/lib/backend-restart.spec.ts +27 -0
  294. package/src/backend/lib/backend-restart.ts +16 -2
  295. package/src/backend/lib/middleware/cors.spec.ts +67 -0
  296. package/src/backend/lib/middleware/cors.ts +40 -12
  297. package/src/backend/roleplay/agent-loader.ts +49 -0
  298. package/src/backend/roleplay/old-server.ts +106 -56
  299. package/src/backend/roleplay/prompt-edit-proposals.ts +38 -4
  300. package/src/backend/roleplay/prompt-file.ts +59 -0
  301. package/src/backend/roleplay/router.spec.ts +75 -0
  302. package/src/backend/roleplay/router.ts +123 -69
  303. package/src/backend/roleplay/suggest-file-edit.ts +160 -0
  304. package/src/backend/roleplay/suggest-prompt-edit.ts +9 -138
  305. package/src/backend/roleplay/suggest-test-fix.spec.ts +50 -0
  306. package/src/backend/roleplay/suggest-test-fix.ts +65 -0
  307. package/src/backend/roleplay/test-fix-proposal-acceptance.spec.ts +253 -0
  308. package/src/backend/server.ts +116 -13
  309. package/src/backend/tests/academy.ts +17 -3
  310. package/src/backend/tests/data.spec.ts +81 -2
  311. package/src/backend/tests/data.ts +74 -4
  312. package/src/backend/tests/router.ts +101 -36
  313. package/src/backend/tests/suggest-test-history-edit.spec.ts +20 -0
  314. package/src/backend/tests/suggest-test-history-edit.ts +52 -0
  315. package/src/backend/tests/test-edit-source.spec.ts +61 -0
  316. package/src/backend/tests/test-edit-source.ts +34 -0
  317. package/src/backend/tests/test-history-edit-proposals.ts +69 -0
  318. package/src/backend/tests/test-history-overrides.spec.ts +24 -0
  319. package/src/backend/tests/test-selection.ts +27 -0
  320. package/src/lib/api-telescope/source-selection.spec.ts +44 -0
  321. package/src/lib/api-telescope/source-selection.ts +28 -0
  322. package/src/lib/components/Modal.svelte +1 -1
  323. package/src/lib/edit/edit-workspace-context.ts +33 -3
  324. package/src/lib/edit/test-ai-edit.spec.ts +31 -0
  325. package/src/lib/edit/test-ai-edit.ts +52 -0
  326. package/src/lib/git-mode/BlockingGitDialog.svelte +118 -0
  327. package/src/lib/git-mode/GitControlsButton.svelte +96 -0
  328. package/src/lib/git-mode/client-admission.spec.ts +304 -0
  329. package/src/lib/git-mode/client-admission.ts +343 -0
  330. package/src/lib/git-mode/errors.ts +2 -0
  331. package/src/lib/git-mode/git-mode-context.ts +70 -0
  332. package/src/lib/git-mode/refresh-policy.spec.ts +95 -0
  333. package/src/lib/git-mode/refresh-policy.ts +39 -0
  334. package/src/lib/git-mode/status-recovery.spec.ts +86 -0
  335. package/src/lib/git-mode/status-recovery.ts +33 -0
  336. package/src/lib/monaco/bosun-theme.ts +6 -83
  337. package/src/lib/monaco/editor-appearance.spec.ts +84 -0
  338. package/src/lib/monaco/editor-appearance.ts +154 -0
  339. package/src/lib/monaco/editor-decorations.spec.ts +91 -0
  340. package/src/lib/monaco/editor-decorations.ts +133 -0
  341. package/src/lib/tests/assertion-types.ts +74 -0
  342. package/src/lib/trpc-client.ts +15 -1
  343. package/src/lib/ws-client.spec.ts +208 -0
  344. package/src/lib/ws-client.ts +196 -14
  345. package/src/lib/ws-message.ts +24 -0
  346. package/build/client/_app/immutable/assets/0.Rrc0O--C.css +0 -1
  347. package/build/client/_app/immutable/assets/0.Rrc0O--C.css.br +0 -0
  348. package/build/client/_app/immutable/assets/0.Rrc0O--C.css.gz +0 -0
  349. package/build/client/_app/immutable/assets/2.DtxQDOXx.css.br +0 -0
  350. package/build/client/_app/immutable/assets/2.DtxQDOXx.css.gz +0 -0
  351. package/build/client/_app/immutable/chunks/B1HdnQRQ.js.br +0 -0
  352. package/build/client/_app/immutable/chunks/B1HdnQRQ.js.gz +0 -0
  353. package/build/client/_app/immutable/chunks/B5zf3x62.js.br +0 -0
  354. package/build/client/_app/immutable/chunks/B5zf3x62.js.gz +0 -0
  355. package/build/client/_app/immutable/chunks/BHHfrjbV.js.br +0 -2
  356. package/build/client/_app/immutable/chunks/BHHfrjbV.js.gz +0 -0
  357. package/build/client/_app/immutable/chunks/BU7aeYn4.js +0 -1
  358. package/build/client/_app/immutable/chunks/BU7aeYn4.js.br +0 -0
  359. package/build/client/_app/immutable/chunks/BU7aeYn4.js.gz +0 -0
  360. package/build/client/_app/immutable/chunks/BbGYcfPY.js +0 -3
  361. package/build/client/_app/immutable/chunks/BbGYcfPY.js.br +0 -0
  362. package/build/client/_app/immutable/chunks/BbGYcfPY.js.gz +0 -0
  363. package/build/client/_app/immutable/chunks/Bmd5zhWC.js.br +0 -0
  364. package/build/client/_app/immutable/chunks/Bmd5zhWC.js.gz +0 -0
  365. package/build/client/_app/immutable/chunks/BnqH7p3U.js +0 -33
  366. package/build/client/_app/immutable/chunks/BnqH7p3U.js.br +0 -0
  367. package/build/client/_app/immutable/chunks/BnqH7p3U.js.gz +0 -0
  368. package/build/client/_app/immutable/chunks/Bog028VR.js.br +0 -0
  369. package/build/client/_app/immutable/chunks/Bog028VR.js.gz +0 -0
  370. package/build/client/_app/immutable/chunks/Bv1mNXs0.js.br +0 -0
  371. package/build/client/_app/immutable/chunks/Bv1mNXs0.js.gz +0 -0
  372. package/build/client/_app/immutable/chunks/BvjXTrOz.js +0 -1
  373. package/build/client/_app/immutable/chunks/BvjXTrOz.js.br +0 -0
  374. package/build/client/_app/immutable/chunks/BvjXTrOz.js.gz +0 -0
  375. package/build/client/_app/immutable/chunks/C1R-s4aD.js +0 -1
  376. package/build/client/_app/immutable/chunks/C1R-s4aD.js.br +0 -0
  377. package/build/client/_app/immutable/chunks/C1R-s4aD.js.gz +0 -0
  378. package/build/client/_app/immutable/chunks/CC1OcqhR.js +0 -1
  379. package/build/client/_app/immutable/chunks/CC1OcqhR.js.br +0 -0
  380. package/build/client/_app/immutable/chunks/CC1OcqhR.js.gz +0 -0
  381. package/build/client/_app/immutable/chunks/CEHsomMo.js.br +0 -0
  382. package/build/client/_app/immutable/chunks/CJcZ2GVC.js.br +0 -0
  383. package/build/client/_app/immutable/chunks/CJcZ2GVC.js.gz +0 -0
  384. package/build/client/_app/immutable/chunks/CN555stc.js.br +0 -0
  385. package/build/client/_app/immutable/chunks/CN555stc.js.gz +0 -0
  386. package/build/client/_app/immutable/chunks/CQg2Mme0.js +0 -9
  387. package/build/client/_app/immutable/chunks/CQg2Mme0.js.br +0 -0
  388. package/build/client/_app/immutable/chunks/CQg2Mme0.js.gz +0 -0
  389. package/build/client/_app/immutable/chunks/Cp_st-64.js.br +0 -0
  390. package/build/client/_app/immutable/chunks/Cp_st-64.js.gz +0 -0
  391. package/build/client/_app/immutable/chunks/D8mA-uZ4.js.br +0 -0
  392. package/build/client/_app/immutable/chunks/D8mA-uZ4.js.gz +0 -0
  393. package/build/client/_app/immutable/chunks/DCXjBDmY.js.br +0 -0
  394. package/build/client/_app/immutable/chunks/DCXjBDmY.js.gz +0 -0
  395. package/build/client/_app/immutable/chunks/DSLyVE1P.js +0 -1
  396. package/build/client/_app/immutable/chunks/DSLyVE1P.js.br +0 -0
  397. package/build/client/_app/immutable/chunks/DSLyVE1P.js.gz +0 -0
  398. package/build/client/_app/immutable/chunks/Ddv5LgEc.js.br +0 -0
  399. package/build/client/_app/immutable/chunks/Ddv5LgEc.js.gz +0 -0
  400. package/build/client/_app/immutable/chunks/DfcDjAv-.js.br +0 -0
  401. package/build/client/_app/immutable/chunks/DfcDjAv-.js.gz +0 -0
  402. package/build/client/_app/immutable/chunks/Dg0A8wPA.js.br +0 -0
  403. package/build/client/_app/immutable/chunks/Dg0A8wPA.js.gz +0 -0
  404. package/build/client/_app/immutable/chunks/Dm49pcMm.js.br +0 -0
  405. package/build/client/_app/immutable/chunks/Dm49pcMm.js.gz +0 -0
  406. package/build/client/_app/immutable/chunks/DneQQ7D5.js.br +0 -0
  407. package/build/client/_app/immutable/chunks/DneQQ7D5.js.gz +0 -0
  408. package/build/client/_app/immutable/chunks/DoD3q5w5.js.br +0 -0
  409. package/build/client/_app/immutable/chunks/DoD3q5w5.js.gz +0 -0
  410. package/build/client/_app/immutable/chunks/DtXXy94i.js.br +0 -0
  411. package/build/client/_app/immutable/chunks/DtXXy94i.js.gz +0 -0
  412. package/build/client/_app/immutable/chunks/Dy2CFyD6.js +0 -2
  413. package/build/client/_app/immutable/chunks/Dy2CFyD6.js.br +0 -0
  414. package/build/client/_app/immutable/chunks/Dy2CFyD6.js.gz +0 -0
  415. package/build/client/_app/immutable/chunks/DyBwTDJp.js +0 -2
  416. package/build/client/_app/immutable/chunks/DyBwTDJp.js.br +0 -0
  417. package/build/client/_app/immutable/chunks/DyBwTDJp.js.gz +0 -0
  418. package/build/client/_app/immutable/chunks/ILTCveYH.js +0 -1
  419. package/build/client/_app/immutable/chunks/ILTCveYH.js.br +0 -0
  420. package/build/client/_app/immutable/chunks/ILTCveYH.js.gz +0 -0
  421. package/build/client/_app/immutable/chunks/JcG4w2gF.js.br +0 -0
  422. package/build/client/_app/immutable/chunks/JcG4w2gF.js.gz +0 -0
  423. package/build/client/_app/immutable/chunks/UqjCSZmA.js.br +0 -0
  424. package/build/client/_app/immutable/chunks/UqjCSZmA.js.gz +0 -0
  425. package/build/client/_app/immutable/chunks/_YcrHZ6P.js +0 -1
  426. package/build/client/_app/immutable/chunks/_YcrHZ6P.js.br +0 -2
  427. package/build/client/_app/immutable/chunks/_YcrHZ6P.js.gz +0 -0
  428. package/build/client/_app/immutable/chunks/i6ygh6h8.js +0 -1
  429. package/build/client/_app/immutable/chunks/i6ygh6h8.js.br +0 -2
  430. package/build/client/_app/immutable/chunks/i6ygh6h8.js.gz +0 -0
  431. package/build/client/_app/immutable/chunks/oRi68ch3.js +0 -1
  432. package/build/client/_app/immutable/chunks/oRi68ch3.js.br +0 -0
  433. package/build/client/_app/immutable/chunks/oRi68ch3.js.gz +0 -0
  434. package/build/client/_app/immutable/chunks/pPpIn6tB.js.br +0 -0
  435. package/build/client/_app/immutable/chunks/pPpIn6tB.js.gz +0 -0
  436. package/build/client/_app/immutable/chunks/r0sHOWrM.js +0 -1
  437. package/build/client/_app/immutable/chunks/r0sHOWrM.js.br +0 -0
  438. package/build/client/_app/immutable/chunks/r0sHOWrM.js.gz +0 -0
  439. package/build/client/_app/immutable/chunks/wcTQUnZO.js.br +0 -0
  440. package/build/client/_app/immutable/chunks/wcTQUnZO.js.gz +0 -0
  441. package/build/client/_app/immutable/entry/app.DdO0gkPg.js +0 -2
  442. package/build/client/_app/immutable/entry/app.DdO0gkPg.js.br +0 -0
  443. package/build/client/_app/immutable/entry/app.DdO0gkPg.js.gz +0 -0
  444. package/build/client/_app/immutable/entry/start.eAjDq0c9.js +0 -1
  445. package/build/client/_app/immutable/entry/start.eAjDq0c9.js.br +0 -2
  446. package/build/client/_app/immutable/entry/start.eAjDq0c9.js.gz +0 -0
  447. package/build/client/_app/immutable/nodes/0.nG3DdvIX.js +0 -3
  448. package/build/client/_app/immutable/nodes/0.nG3DdvIX.js.br +0 -0
  449. package/build/client/_app/immutable/nodes/0.nG3DdvIX.js.gz +0 -0
  450. package/build/client/_app/immutable/nodes/1.Ceu0XS6v.js +0 -1
  451. package/build/client/_app/immutable/nodes/1.Ceu0XS6v.js.br +0 -0
  452. package/build/client/_app/immutable/nodes/1.Ceu0XS6v.js.gz +0 -0
  453. package/build/client/_app/immutable/nodes/10.dm2dgy31.js +0 -13
  454. package/build/client/_app/immutable/nodes/10.dm2dgy31.js.br +0 -0
  455. package/build/client/_app/immutable/nodes/10.dm2dgy31.js.gz +0 -0
  456. package/build/client/_app/immutable/nodes/15.D2I0y3ys.js.br +0 -0
  457. package/build/client/_app/immutable/nodes/15.D2I0y3ys.js.gz +0 -0
  458. package/build/client/_app/immutable/nodes/17.CKxGp0E7.js +0 -3
  459. package/build/client/_app/immutable/nodes/17.CKxGp0E7.js.br +0 -0
  460. package/build/client/_app/immutable/nodes/17.CKxGp0E7.js.gz +0 -0
  461. package/build/client/_app/immutable/nodes/18.DHJxyQpw.js +0 -11
  462. package/build/client/_app/immutable/nodes/18.DHJxyQpw.js.br +0 -0
  463. package/build/client/_app/immutable/nodes/18.DHJxyQpw.js.gz +0 -0
  464. package/build/client/_app/immutable/nodes/2.DesR2keg.js +0 -35
  465. package/build/client/_app/immutable/nodes/2.DesR2keg.js.br +0 -0
  466. package/build/client/_app/immutable/nodes/2.DesR2keg.js.gz +0 -0
  467. package/build/client/_app/immutable/nodes/3.BUKrXGR_.js +0 -1
  468. package/build/client/_app/immutable/nodes/3.BUKrXGR_.js.br +0 -0
  469. package/build/client/_app/immutable/nodes/3.BUKrXGR_.js.gz +0 -0
  470. package/build/client/_app/immutable/nodes/4.C8VxVbVn.js.br +0 -0
  471. package/build/client/_app/immutable/nodes/4.C8VxVbVn.js.gz +0 -0
  472. package/build/client/_app/immutable/nodes/5.BjrnKYaA.js.br +0 -0
  473. package/build/client/_app/immutable/nodes/5.BjrnKYaA.js.gz +0 -0
  474. package/build/client/_app/immutable/nodes/6.C_hEzPrU.js +0 -1
  475. package/build/client/_app/immutable/nodes/6.C_hEzPrU.js.br +0 -0
  476. package/build/client/_app/immutable/nodes/6.C_hEzPrU.js.gz +0 -0
  477. package/build/client/_app/immutable/nodes/8.BTYaOiDC.js +0 -2
  478. package/build/client/_app/immutable/nodes/8.BTYaOiDC.js.br +0 -0
  479. package/build/client/_app/immutable/nodes/8.BTYaOiDC.js.gz +0 -0
  480. package/build/client/_app/immutable/nodes/9.DVspuDYY.js +0 -2
  481. package/build/client/_app/immutable/nodes/9.DVspuDYY.js.br +0 -0
  482. package/build/client/_app/immutable/nodes/9.DVspuDYY.js.gz +0 -0
  483. package/build/server/chunks/0-D_duQDWy.js +0 -9
  484. package/build/server/chunks/0-D_duQDWy.js.map +0 -1
  485. package/build/server/chunks/1-C0HdTvXW.js +0 -9
  486. package/build/server/chunks/10-DcfhCR4R.js +0 -9
  487. package/build/server/chunks/10-DcfhCR4R.js.map +0 -1
  488. package/build/server/chunks/15-BQcb9C_y.js +0 -9
  489. package/build/server/chunks/17-DuVCTioj.js +0 -9
  490. package/build/server/chunks/17-DuVCTioj.js.map +0 -1
  491. package/build/server/chunks/18-CT0VFW_p.js +0 -9
  492. package/build/server/chunks/18-CT0VFW_p.js.map +0 -1
  493. package/build/server/chunks/2-DgTzKQAP.js +0 -9
  494. package/build/server/chunks/2-DgTzKQAP.js.map +0 -1
  495. package/build/server/chunks/3-eQMSMzXP.js +0 -9
  496. package/build/server/chunks/3-eQMSMzXP.js.map +0 -1
  497. package/build/server/chunks/4-OaAJtDqP.js +0 -9
  498. package/build/server/chunks/4-OaAJtDqP.js.map +0 -1
  499. package/build/server/chunks/5-CBdMutbv.js +0 -9
  500. package/build/server/chunks/5-CBdMutbv.js.map +0 -1
  501. package/build/server/chunks/6-FoEi7ggm.js +0 -9
  502. package/build/server/chunks/6-FoEi7ggm.js.map +0 -1
  503. package/build/server/chunks/8-B44yRkfX.js +0 -9
  504. package/build/server/chunks/8-B44yRkfX.js.map +0 -1
  505. package/build/server/chunks/9-eUjFpHzI.js +0 -9
  506. package/build/server/chunks/9-eUjFpHzI.js.map +0 -1
  507. package/build/server/chunks/ChatStepTrace-BivoNYOY.js.map +0 -1
  508. package/build/server/chunks/_layout.svelte-B-0DHjLB.js +0 -45
  509. package/build/server/chunks/_layout.svelte-B-0DHjLB.js.map +0 -1
  510. package/build/server/chunks/_layout.svelte-B88WMyk0.js.map +0 -1
  511. package/build/server/chunks/_layout.svelte-wi6o5Ecu.js.map +0 -1
  512. package/build/server/chunks/_page.svelte-57pyhszV.js.map +0 -1
  513. package/build/server/chunks/_page.svelte-BSVJ2d2G.js.map +0 -1
  514. package/build/server/chunks/_page.svelte-BoHt2cpr.js +0 -61
  515. package/build/server/chunks/_page.svelte-BoHt2cpr.js.map +0 -1
  516. package/build/server/chunks/_page.svelte-CeS_21Ho.js.map +0 -1
  517. package/build/server/chunks/_page.svelte-DHSYVLoj.js.map +0 -1
  518. package/build/server/chunks/_page.svelte-DhFNFMnl.js.map +0 -1
  519. package/build/server/chunks/_server.ts-D_eo_OAK.js +0 -139
  520. package/build/server/chunks/_server.ts-D_eo_OAK.js.map +0 -1
  521. package/build/server/chunks/error.svelte-DmKKN1RU.js +0 -15
  522. package/build/server/chunks/source-selection-CcN1cgXc.js.map +0 -1
  523. package/build/server/chunks/trpc-client-D8L6vWf4.js +0 -14
  524. package/build/server/chunks/trpc-client-D8L6vWf4.js.map +0 -1
@@ -0,0 +1,1139 @@
1
+ // @ts-check
2
+ import { randomUUID } from 'crypto'
3
+ import {
4
+ existsSync,
5
+ linkSync,
6
+ lstatSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ realpathSync,
10
+ renameSync,
11
+ rmSync,
12
+ unlinkSync,
13
+ writeFileSync
14
+ } from 'fs'
15
+ import { platform, tmpdir } from 'os'
16
+ import { basename, dirname, join, resolve } from 'path'
17
+ import { spawnSync } from 'child_process'
18
+
19
+ export const GIT_ACTIVATION_RESTART_EXIT_CODE = 76
20
+
21
+ const DEFAULT_ACTIVATION_IDLE_MINUTES = 10
22
+ const REPOSITORY_LEASE_VERSION = 3
23
+ const REPOSITORY_LEASE_FILENAME = 'bosun-launcher.lease.json'
24
+ const GIT_MODE_ENV_NAMES = [
25
+ 'BOSUN_GIT_MODE',
26
+ 'BOSUN_CURRENT_BRANCH',
27
+ 'BOSUN_PERSONAL_BRANCH',
28
+ 'BOSUN_GIT_ACTIVATION_IDLE_MINUTES',
29
+ 'BOSUN_GIT_ACTIVATION_IDLE_MS',
30
+ 'BOSUN_GIT_REPOSITORY_ROOT',
31
+ 'BOSUN_GIT_PERSONAL_UPSTREAM',
32
+ 'BOSUN_GIT_PERSONAL_UPSTREAM_REMOTE',
33
+ 'BOSUN_GIT_PERSONAL_UPSTREAM_BRANCH',
34
+ 'BOSUN_GIT_UPSTREAM',
35
+ 'BOSUN_GIT_UPSTREAM_REMOTE',
36
+ 'BOSUN_GIT_UPSTREAM_BRANCH',
37
+ 'BOSUN_GIT_CURRENT_UPSTREAM',
38
+ 'BOSUN_GIT_CURRENT_UPSTREAM_REMOTE',
39
+ 'BOSUN_GIT_CURRENT_UPSTREAM_BRANCH',
40
+ 'BOSUN_GIT_LAUNCH_ID',
41
+ 'BOSUN_GIT_RUNTIME_STATE_PATH',
42
+ 'BOSUN_GIT_COMMAND_STATE_PATH',
43
+ 'BOSUN_GIT_LEASE_ID',
44
+ 'BOSUN_GIT_ACTIVATION_RESTART_EXIT_CODE',
45
+ 'BOSUN_SHUTDOWN_ACK_PATH',
46
+ 'BOSUN_SHUTDOWN_ACK_TOKEN'
47
+ ]
48
+
49
+ // These variables can redirect an otherwise ordinary `git` command away from cwd. Git mode owns
50
+ // the validated repository root, so neither a parent shell nor the project .env may override it.
51
+ const GIT_REPOSITORY_ENV_NAMES = [
52
+ 'GIT_DIR',
53
+ 'GIT_WORK_TREE',
54
+ 'GIT_COMMON_DIR',
55
+ 'GIT_INDEX_FILE',
56
+ 'GIT_OBJECT_DIRECTORY',
57
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
58
+ 'GIT_NAMESPACE',
59
+ 'GIT_CEILING_DIRECTORIES',
60
+ 'GIT_DISCOVERY_ACROSS_FILESYSTEM'
61
+ ]
62
+
63
+ const IN_PROGRESS_MARKERS = [
64
+ ['MERGE_HEAD', 'merge'],
65
+ ['rebase-merge', 'rebase'],
66
+ ['rebase-apply', 'rebase or am operation'],
67
+ ['CHERRY_PICK_HEAD', 'cherry-pick'],
68
+ ['REVERT_HEAD', 'revert'],
69
+ ['BISECT_LOG', 'bisect'],
70
+ ['sequencer', 'sequenced Git operation']
71
+ ]
72
+
73
+ /**
74
+ * @typedef {{ currentBranch: string, personalBranch: string, activationIdleMs: number }} ProjectGitModeConfig
75
+ * @typedef {{ status: number | null, stdout: string, stderr: string, error?: Error }} GitResult
76
+ * @typedef {(args: string[]) => GitResult} GitRunner
77
+ * @typedef {{
78
+ * currentBranch: string,
79
+ * personalBranch: string,
80
+ * activationIdleMs: number,
81
+ * repositoryRoot: string,
82
+ * personalUpstream: string,
83
+ * personalUpstreamRemote: string,
84
+ * personalUpstreamBranch: string,
85
+ * currentUpstream: string,
86
+ * currentUpstreamRemote: string,
87
+ * currentUpstreamBranch: string,
88
+ * gitDirectory: string,
89
+ * launchId: string,
90
+ * runtimeStatePath: string,
91
+ * runtimeDirectory: string,
92
+ * leasePath: string,
93
+ * leaseId: string,
94
+ * commandStatePath: string,
95
+ * childStatePath: string
96
+ * }} GitModeRuntime
97
+ * @typedef {{
98
+ * version: typeof REPOSITORY_LEASE_VERSION,
99
+ * leaseId: string,
100
+ * launchId: string,
101
+ * pid: number,
102
+ * repositoryRoot: string,
103
+ * commandStatePath: string,
104
+ * childStatePath?: string,
105
+ * createdAt: string
106
+ * }} RepositoryLease
107
+ * @typedef {{
108
+ * leasePath: string,
109
+ * leaseId: string,
110
+ * commandStatePath: string,
111
+ * childStatePath: string
112
+ * }} RepositoryLeaseClaim
113
+ * @typedef {{
114
+ * pid: number,
115
+ * processGroupId: number,
116
+ * executable: string,
117
+ * startedAt: string
118
+ * }} RepositoryCommandProcess
119
+ * @typedef {{
120
+ * version: 1,
121
+ * leaseId: string,
122
+ * launchId: string,
123
+ * repositoryRoot: string,
124
+ * processes: RepositoryCommandProcess[]
125
+ * }} RepositoryCommandState
126
+ * @typedef {{ pid: number, name: string, startedAt: string }} RepositoryChildProcess
127
+ * @typedef {{
128
+ * version: 1,
129
+ * leaseId: string,
130
+ * launchId: string,
131
+ * processes: RepositoryChildProcess[]
132
+ * }} RepositoryChildState
133
+ */
134
+
135
+ /**
136
+ * Read the opt-in only from the already-parsed project .env object. In particular, do not pass
137
+ * process.env here: an inherited shell variable must never enable Git mode.
138
+ *
139
+ * @param {Record<string, string | undefined>} projectEnv
140
+ * @returns {ProjectGitModeConfig | null}
141
+ */
142
+ export function parseProjectGitMode(projectEnv) {
143
+ const mode = projectEnv.BOSUN_GIT_MODE?.trim().toLowerCase()
144
+ if (mode === undefined || mode === '' || mode === 'false') return null
145
+ if (mode !== 'true') {
146
+ throw new Error('BOSUN_GIT_MODE in the project .env must be either true or false.')
147
+ }
148
+
149
+ const currentBranch = projectEnv.BOSUN_CURRENT_BRANCH?.trim()
150
+ if (!currentBranch) {
151
+ throw new Error('Bosun Git mode requires BOSUN_CURRENT_BRANCH to be set in the project .env.')
152
+ }
153
+ const personalBranch = projectEnv.BOSUN_PERSONAL_BRANCH?.trim()
154
+ if (!personalBranch) {
155
+ throw new Error('Bosun Git mode requires BOSUN_PERSONAL_BRANCH to be set in the project .env.')
156
+ }
157
+ if (personalBranch === currentBranch) {
158
+ throw new Error('BOSUN_PERSONAL_BRANCH and BOSUN_CURRENT_BRANCH must name different branches.')
159
+ }
160
+
161
+ const idleMinutesValue =
162
+ projectEnv.BOSUN_GIT_ACTIVATION_IDLE_MINUTES?.trim() || String(DEFAULT_ACTIVATION_IDLE_MINUTES)
163
+ const idleMinutes = Number(idleMinutesValue)
164
+ const activationIdleMs = Math.max(1, Math.round(idleMinutes * 60_000))
165
+ if (
166
+ !Number.isFinite(idleMinutes) ||
167
+ idleMinutes <= 0 ||
168
+ !Number.isSafeInteger(activationIdleMs) ||
169
+ activationIdleMs <= 0
170
+ ) {
171
+ throw new Error(
172
+ 'BOSUN_GIT_ACTIVATION_IDLE_MINUTES in the project .env must be a positive number.'
173
+ )
174
+ }
175
+
176
+ return { currentBranch, personalBranch, activationIdleMs }
177
+ }
178
+
179
+ /**
180
+ * Validate the repository before either Bosun child process starts.
181
+ *
182
+ * @param {{ cwd: string, config: ProjectGitModeConfig, runGit?: GitRunner }} options
183
+ */
184
+ export function validateGitModeStartup({ cwd, config, runGit = createGitRunner(cwd) }) {
185
+ const repositoryRootOutput = requiredGitOutput(
186
+ runGit,
187
+ ['rev-parse', '--show-toplevel'],
188
+ 'Bosun Git mode must be started from the root of a Git repository.'
189
+ )
190
+ const repositoryRoot = realpathSync(repositoryRootOutput)
191
+ const launchDirectory = realpathSync(cwd)
192
+ if (repositoryRoot !== launchDirectory) {
193
+ throw new Error(
194
+ `Bosun Git mode must be started from the repository root (${repositoryRoot}), not ${launchDirectory}.`
195
+ )
196
+ }
197
+ const gitMarkerPath = join(repositoryRoot, '.git')
198
+ try {
199
+ const marker = lstatSync(gitMarkerPath)
200
+ if (
201
+ !marker.isDirectory() ||
202
+ marker.isSymbolicLink() ||
203
+ realpathSync(gitMarkerPath) !== gitMarkerPath
204
+ ) {
205
+ throw new Error('.git is not a standalone directory')
206
+ }
207
+ } catch (error) {
208
+ throw new Error(
209
+ 'Bosun Git mode requires a primary checkout with a standalone .git directory. Separate Git directories, a linked Git worktree, and submodule checkouts are not supported.',
210
+ { cause: error }
211
+ )
212
+ }
213
+
214
+ const superprojectRoot = optionalGitOutput(runGit, [
215
+ 'rev-parse',
216
+ '--show-superproject-working-tree'
217
+ ])
218
+ if (superprojectRoot) {
219
+ throw new Error(
220
+ `Bosun Git mode cannot run from a Git submodule checkout (${repositoryRoot}). Start it from a standalone repository.`
221
+ )
222
+ }
223
+
224
+ const trackedFiles = requiredGitResult(
225
+ runGit,
226
+ ['ls-files', '--stage', '-z'],
227
+ 'Bosun could not inspect the repository for tracked submodules.'
228
+ ).stdout
229
+ if (/(?:^|\0)160000 /.test(trackedFiles)) {
230
+ throw new Error(
231
+ 'Bosun Git mode cannot run in a repository containing tracked Git submodules. Remove the submodules or disable Git mode.'
232
+ )
233
+ }
234
+
235
+ const gitDirectory = realpathSync(
236
+ requiredGitOutput(
237
+ runGit,
238
+ ['rev-parse', '--absolute-git-dir'],
239
+ 'Bosun could not resolve the repository Git directory.'
240
+ )
241
+ )
242
+ const commonGitDirectory = realpathSync(
243
+ resolve(
244
+ cwd,
245
+ requiredGitOutput(
246
+ runGit,
247
+ ['rev-parse', '--git-common-dir'],
248
+ 'Bosun could not resolve the repository common Git directory.'
249
+ )
250
+ )
251
+ )
252
+ if (gitDirectory !== commonGitDirectory) {
253
+ throw new Error(
254
+ 'Bosun Git mode cannot run from a linked Git worktree. Start Bosun from the primary checkout.'
255
+ )
256
+ }
257
+
258
+ for (const [marker, operation] of IN_PROGRESS_MARKERS) {
259
+ const markerPath = resolve(
260
+ cwd,
261
+ requiredGitOutput(
262
+ runGit,
263
+ ['rev-parse', '--git-path', marker],
264
+ 'Bosun could not inspect the current Git operation.'
265
+ )
266
+ )
267
+ if (existsSync(markerPath)) {
268
+ throw new Error(
269
+ `Bosun Git mode cannot start while a ${operation} is in progress. Finish or abort it, then restart Bosun.`
270
+ )
271
+ }
272
+ }
273
+
274
+ const checkedOutBranch = optionalGitOutput(runGit, ['symbolic-ref', '--quiet', '--short', 'HEAD'])
275
+ if (!checkedOutBranch) {
276
+ throw new Error(
277
+ 'Bosun Git mode cannot start from a detached HEAD. Switch to the configured personal branch and restart Bosun.'
278
+ )
279
+ }
280
+
281
+ if (!gitSucceeded(runGit, ['rev-parse', '--verify', '--quiet', 'HEAD^{commit}'])) {
282
+ throw new Error(
283
+ `Bosun Git mode cannot start because branch ${quoteForMessage(checkedOutBranch)} has no commits yet.`
284
+ )
285
+ }
286
+
287
+ if (!gitSucceeded(runGit, ['check-ref-format', '--branch', config.currentBranch])) {
288
+ throw new Error(
289
+ `BOSUN_CURRENT_BRANCH is not a valid Git branch name: ${quoteForMessage(config.currentBranch)}.`
290
+ )
291
+ }
292
+ if (!gitSucceeded(runGit, ['check-ref-format', '--branch', config.personalBranch])) {
293
+ throw new Error(
294
+ `BOSUN_PERSONAL_BRANCH is not a valid Git branch name: ${quoteForMessage(config.personalBranch)}.`
295
+ )
296
+ }
297
+
298
+ if (checkedOutBranch !== config.personalBranch) {
299
+ throw new Error(
300
+ `Bosun Git mode requires personal branch ${quoteForMessage(config.personalBranch)}, but the repository is on ${quoteForMessage(checkedOutBranch)}.\n\n` +
301
+ `Run this command and restart Bosun:\n git switch ${quoteForShell(config.personalBranch)}`
302
+ )
303
+ }
304
+
305
+ if (
306
+ !gitSucceeded(runGit, ['show-ref', '--verify', '--quiet', `refs/heads/${config.currentBranch}`])
307
+ ) {
308
+ throw new Error(
309
+ `Configured current branch ${quoteForMessage(config.currentBranch)} does not exist locally. Create and publish it before starting Bosun.`
310
+ )
311
+ }
312
+
313
+ if (
314
+ !gitSucceeded(runGit, [
315
+ 'rev-parse',
316
+ '--verify',
317
+ '--quiet',
318
+ `refs/heads/${config.currentBranch}^{commit}`
319
+ ])
320
+ ) {
321
+ throw new Error(
322
+ `Configured current branch ${quoteForMessage(config.currentBranch)} does not resolve to a commit.`
323
+ )
324
+ }
325
+ const worktreeState = requiredGitResult(
326
+ runGit,
327
+ ['worktree', 'list', '--porcelain'],
328
+ 'Bosun could not inspect linked Git worktrees.'
329
+ ).stdout
330
+ if (worktreeState.split('\n').includes(`branch refs/heads/${config.currentBranch}`)) {
331
+ throw new Error(
332
+ `Configured current branch ${quoteForMessage(config.currentBranch)} is checked out in another Git worktree. Remove that worktree or switch it to another branch, then restart Bosun.`
333
+ )
334
+ }
335
+
336
+ const personalUpstream = validateBranchUpstream(runGit, config.personalBranch)
337
+ const currentUpstream = validateBranchUpstream(runGit, config.currentBranch)
338
+ if (personalUpstream.remote !== currentUpstream.remote) {
339
+ throw new Error(
340
+ `Personal branch ${quoteForMessage(config.personalBranch)} and current branch ${quoteForMessage(config.currentBranch)} must track the same remote so Bosun can publish a release atomically.`
341
+ )
342
+ }
343
+ validateSingleRemoteDestination(runGit, personalUpstream.remote)
344
+ if (personalUpstream.branch === currentUpstream.branch) {
345
+ throw new Error(
346
+ `Personal branch ${quoteForMessage(config.personalBranch)} and current branch ${quoteForMessage(config.currentBranch)} must track different upstream branches.`
347
+ )
348
+ }
349
+
350
+ return {
351
+ repositoryRoot,
352
+ currentBranch: config.currentBranch,
353
+ personalBranch: config.personalBranch,
354
+ personalUpstream: personalUpstream.name,
355
+ personalUpstreamRemote: personalUpstream.remote,
356
+ personalUpstreamBranch: personalUpstream.branch,
357
+ currentUpstream: currentUpstream.name,
358
+ currentUpstreamRemote: currentUpstream.remote,
359
+ currentUpstreamBranch: currentUpstream.branch,
360
+ gitDirectory
361
+ }
362
+ }
363
+
364
+ /** @param {GitRunner} runGit @param {string} branch */
365
+ function validateBranchUpstream(runGit, branch) {
366
+ const upstreamReference = `${branch}@{upstream}`
367
+ const upstream = requiredGitOutput(
368
+ runGit,
369
+ ['rev-parse', '--abbrev-ref', '--symbolic-full-name', upstreamReference],
370
+ `Branch ${quoteForMessage(branch)} has no upstream. Configure its upstream and restart Bosun.`
371
+ )
372
+ if (
373
+ !gitSucceeded(runGit, ['rev-parse', '--verify', '--quiet', `${upstreamReference}^{commit}`])
374
+ ) {
375
+ throw new Error(
376
+ `The upstream ${quoteForMessage(upstream)} for branch ${quoteForMessage(branch)} does not resolve to a commit. Fetch or repair it, then restart Bosun.`
377
+ )
378
+ }
379
+
380
+ const remote = requiredGitOutput(
381
+ runGit,
382
+ ['config', '--get', `branch.${branch}.remote`],
383
+ `Bosun could not determine the upstream remote for ${quoteForMessage(branch)}.`
384
+ )
385
+ if (remote === '.') {
386
+ throw new Error(
387
+ `Branch ${quoteForMessage(branch)} tracks another local branch. Bosun Git mode requires an upstream remote it can pull from and push to.`
388
+ )
389
+ }
390
+ const upstreamMergeRef = requiredGitOutput(
391
+ runGit,
392
+ ['config', '--get', `branch.${branch}.merge`],
393
+ `Bosun could not determine the upstream branch for ${quoteForMessage(branch)}.`
394
+ )
395
+ const headsPrefix = 'refs/heads/'
396
+ if (!upstreamMergeRef.startsWith(headsPrefix) || upstreamMergeRef.length === headsPrefix.length) {
397
+ throw new Error(
398
+ `Bosun Git mode requires an upstream branch for ${quoteForMessage(branch)}, but found ${quoteForMessage(upstreamMergeRef)}.`
399
+ )
400
+ }
401
+
402
+ return {
403
+ name: upstream,
404
+ remote,
405
+ branch: upstreamMergeRef.slice(headsPrefix.length)
406
+ }
407
+ }
408
+
409
+ /** @param {GitRunner} runGit @param {string} remote */
410
+ function validateSingleRemoteDestination(runGit, remote) {
411
+ /** @param {string[]} args @param {string} kind */
412
+ const readUrls = (args, kind) =>
413
+ requiredGitResult(
414
+ runGit,
415
+ args,
416
+ `The upstream remote ${quoteForMessage(remote)} has no usable ${kind} URL. Repair it and restart Bosun.`
417
+ )
418
+ .stdout.split(/\r?\n/)
419
+ .map((url) => url.trim())
420
+ .filter(Boolean)
421
+ const fetchUrls = readUrls(['remote', 'get-url', '--all', remote], 'fetch')
422
+ const pushUrls = readUrls(['remote', 'get-url', '--push', '--all', remote], 'push')
423
+ if (fetchUrls.length !== 1 || pushUrls.length !== 1 || fetchUrls[0] !== pushUrls[0]) {
424
+ throw new Error(
425
+ `Upstream remote ${quoteForMessage(remote)} must have exactly one fetch URL and one identical push URL so Bosun can publish and recover a release safely.`
426
+ )
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Create launch-scoped state. The launcher retains this object across managed backend restarts;
432
+ * a full launcher restart intentionally gets a new ID and state path.
433
+ *
434
+ * @param {{ cwd: string, projectEnv: Record<string, string | undefined> }} options
435
+ * @returns {GitModeRuntime | null}
436
+ */
437
+ export function prepareGitModeRuntime({ cwd, projectEnv }) {
438
+ const config = parseProjectGitMode(projectEnv)
439
+ if (!config) return null
440
+ if (platform() === 'win32') {
441
+ throw new Error(
442
+ 'Bosun Git mode is not supported on Windows because Bosun cannot safely own and verify an entire Git or pnpm process tree there. Use macOS, Linux, or WSL, or disable Git mode.'
443
+ )
444
+ }
445
+
446
+ const repository = validateGitModeStartup({ cwd, config })
447
+ const launchId = randomUUID()
448
+ const lease = acquireRepositoryLease({
449
+ gitDirectory: repository.gitDirectory,
450
+ repositoryRoot: repository.repositoryRoot,
451
+ launchId
452
+ })
453
+
454
+ try {
455
+ initializeRepositoryCommandState(lease.commandStatePath, {
456
+ version: 1,
457
+ leaseId: lease.leaseId,
458
+ launchId,
459
+ repositoryRoot: repository.repositoryRoot,
460
+ processes: []
461
+ })
462
+ initializeRepositoryChildState(lease.childStatePath, {
463
+ version: 1,
464
+ leaseId: lease.leaseId,
465
+ launchId,
466
+ processes: []
467
+ })
468
+ const runtimeDirectory = mkdtempSync(join(tmpdir(), 'bosun-git-mode-'))
469
+ return {
470
+ ...config,
471
+ ...repository,
472
+ ...lease,
473
+ launchId,
474
+ runtimeDirectory,
475
+ runtimeStatePath: join(runtimeDirectory, 'runtime-state.json')
476
+ }
477
+ } catch (error) {
478
+ try {
479
+ unlinkSync(lease.commandStatePath)
480
+ } catch (cleanupError) {
481
+ if (!isNodeError(cleanupError, 'ENOENT')) {
482
+ // Keep the lease when command-state cleanup is uncertain.
483
+ throw new Error(
484
+ `Bosun could not safely clean up Git-mode startup state at ${lease.commandStatePath}. The repository lease was retained.`,
485
+ { cause: cleanupError }
486
+ )
487
+ }
488
+ }
489
+ try {
490
+ unlinkSync(lease.childStatePath)
491
+ } catch (cleanupError) {
492
+ if (!isNodeError(cleanupError, 'ENOENT')) {
493
+ throw new Error(
494
+ `Bosun could not clean up child-process state at ${lease.childStatePath}. The repository lease was retained.`,
495
+ { cause: cleanupError }
496
+ )
497
+ }
498
+ }
499
+ releaseRepositoryLease(lease.leasePath, lease.leaseId)
500
+ throw error
501
+ }
502
+ }
503
+
504
+ /** @param {GitModeRuntime | null} runtime */
505
+ export function cleanupGitModeRuntime(runtime) {
506
+ if (!runtime) return
507
+ const commandState = readRepositoryCommandStateIfPresent(runtime.commandStatePath)
508
+ if (commandState && commandState.leaseId !== runtime.leaseId) {
509
+ throw new Error(
510
+ `Bosun cannot release the repository lease because ${runtime.commandStatePath} belongs to another launch.`
511
+ )
512
+ }
513
+ const liveProcesses = commandState?.processes.filter(isRepositoryCommandProcessAlive) ?? []
514
+ if (liveProcesses.length > 0) {
515
+ const processIds = liveProcesses.map((entry) => entry.pid).join(', ')
516
+ throw new Error(
517
+ `Bosun retained the repository lease because Git or pnpm is still running (PIDs: ${processIds}).`
518
+ )
519
+ }
520
+ const childState = readRepositoryChildStateIfPresent(runtime.childStatePath)
521
+ if (childState && childState.leaseId !== runtime.leaseId) {
522
+ throw new Error(
523
+ `Bosun cannot release the repository lease because ${runtime.childStatePath} belongs to another launch.`
524
+ )
525
+ }
526
+ const liveChildren = childState?.processes.filter((entry) => isProcessAlive(entry.pid)) ?? []
527
+ if (liveChildren.length > 0) {
528
+ throw new Error(
529
+ `Bosun retained the repository lease because child processes are still running (PIDs: ${liveChildren.map((entry) => entry.pid).join(', ')}).`
530
+ )
531
+ }
532
+ rmSync(runtime.runtimeDirectory, { force: true, recursive: true })
533
+ try {
534
+ unlinkSync(runtime.commandStatePath)
535
+ } catch (error) {
536
+ if (!isNodeError(error, 'ENOENT')) throw error
537
+ }
538
+ try {
539
+ unlinkSync(runtime.childStatePath)
540
+ } catch (error) {
541
+ if (!isNodeError(error, 'ENOENT')) throw error
542
+ }
543
+ if (!releaseRepositoryLease(runtime.leasePath, runtime.leaseId)) {
544
+ throw new Error('Bosun could not confirm ownership while releasing its repository lease.')
545
+ }
546
+ }
547
+
548
+ /** @param {GitModeRuntime | null} runtime @param {number | undefined} pid @param {string} name */
549
+ export function registerGitModeChildProcess(runtime, pid, name) {
550
+ if (!runtime || !Number.isSafeInteger(pid) || Number(pid) <= 0) return
551
+ const state = readRepositoryChildState(runtime.childStatePath)
552
+ assertRepositoryChildStateOwner(state, runtime)
553
+ state.processes = state.processes.filter((entry) => entry.pid !== pid)
554
+ state.processes.push({ pid: Number(pid), name, startedAt: new Date().toISOString() })
555
+ writeRepositoryChildState(runtime.childStatePath, state)
556
+ }
557
+
558
+ /** @param {GitModeRuntime | null} runtime @param {number | undefined} pid */
559
+ export function unregisterGitModeChildProcess(runtime, pid) {
560
+ if (!runtime || !Number.isSafeInteger(pid) || Number(pid) <= 0) return
561
+ const state = readRepositoryChildState(runtime.childStatePath)
562
+ assertRepositoryChildStateOwner(state, runtime)
563
+ state.processes = state.processes.filter((entry) => entry.pid !== pid)
564
+ writeRepositoryChildState(runtime.childStatePath, state)
565
+ }
566
+
567
+ /** @param {RepositoryChildState} state @param {GitModeRuntime} runtime */
568
+ function assertRepositoryChildStateOwner(state, runtime) {
569
+ if (state.leaseId !== runtime.leaseId || state.launchId !== runtime.launchId) {
570
+ throw new Error(`Bosun cannot update child-process state at ${runtime.childStatePath}.`)
571
+ }
572
+ }
573
+
574
+ /**
575
+ * Atomically claim the one launcher lease for a primary repository. A dead launcher does not make
576
+ * the repository unusable: recover its lease automatically unless its durable command ledger
577
+ * identifies a Git/pnpm process group that is still alive.
578
+ *
579
+ * @param {{ gitDirectory: string, repositoryRoot: string, launchId: string }} input
580
+ * @returns {RepositoryLeaseClaim}
581
+ */
582
+ export function acquireRepositoryLease(input) {
583
+ const leasePath = join(input.gitDirectory, REPOSITORY_LEASE_FILENAME)
584
+ const leaseId = randomUUID()
585
+ const commandStatePath = join(input.gitDirectory, `bosun-launcher.commands.${leaseId}.json`)
586
+ const childStatePath = join(input.gitDirectory, `bosun-launcher.children.${leaseId}.json`)
587
+ /** @type {RepositoryLease & { childStatePath: string }} */
588
+ const lease = {
589
+ version: REPOSITORY_LEASE_VERSION,
590
+ leaseId,
591
+ launchId: input.launchId,
592
+ pid: process.pid,
593
+ repositoryRoot: input.repositoryRoot,
594
+ commandStatePath,
595
+ childStatePath,
596
+ createdAt: new Date().toISOString()
597
+ }
598
+
599
+ assertNoRepositoryRecovery(leasePath, input.repositoryRoot)
600
+ if (tryCreateLeaseFile(leasePath, lease)) {
601
+ return { leasePath, leaseId, commandStatePath, childStatePath }
602
+ }
603
+
604
+ const existing = readRepositoryLease(leasePath)
605
+ if (existing === null) {
606
+ throw new Error(
607
+ 'Bosun could not acquire its repository launcher lease because it changed during startup. Restart Bosun.'
608
+ )
609
+ }
610
+ assertLeaseMatchesRepository(existing, input.repositoryRoot, leasePath)
611
+ if (isProcessAlive(existing.pid)) throw activeLauncherError(existing)
612
+ return recoverDeadRepositoryLease({ leasePath, existing, replacement: lease })
613
+ }
614
+
615
+ /**
616
+ * Keep the old lease present while inspecting its command ledger, then replace it by ownership ID.
617
+ * The short-lived recovery lease serializes simultaneous restart attempts.
618
+ *
619
+ * @param {{ leasePath: string, existing: RepositoryLease, replacement: RepositoryLease & { childStatePath: string } }} input
620
+ * @returns {RepositoryLeaseClaim}
621
+ */
622
+ function recoverDeadRepositoryLease({ leasePath, existing, replacement }) {
623
+ assertNoLiveRepositoryCommands(existing)
624
+ assertNoLiveRepositoryChildren(existing)
625
+ const recoveryPath = `${leasePath}.recovery`
626
+ const recoveryId = randomUUID()
627
+ /** @type {RepositoryLease} */
628
+ const recovery = {
629
+ ...replacement,
630
+ leaseId: recoveryId,
631
+ launchId: `recovery:${replacement.launchId}`
632
+ }
633
+ if (!tryCreateLeaseFile(recoveryPath, recovery)) {
634
+ throw repositoryRecoveryError(recoveryPath)
635
+ }
636
+
637
+ try {
638
+ const current = readRepositoryLease(leasePath)
639
+ if (!current || current.leaseId !== existing.leaseId) {
640
+ if (current) throw activeLauncherError(current)
641
+ throw new Error('The Bosun repository launcher lease changed during recovery.')
642
+ }
643
+ assertNoLiveRepositoryCommands(current)
644
+ assertNoLiveRepositoryChildren(current)
645
+
646
+ try {
647
+ unlinkSync(existing.commandStatePath)
648
+ } catch (error) {
649
+ if (!isNodeError(error, 'ENOENT')) {
650
+ throw new Error(
651
+ `Bosun could not remove the stale command ledger at ${existing.commandStatePath}. Restart Bosun and try again.`,
652
+ { cause: error }
653
+ )
654
+ }
655
+ }
656
+ if (existing.childStatePath) {
657
+ try {
658
+ unlinkSync(existing.childStatePath)
659
+ } catch (error) {
660
+ if (!isNodeError(error, 'ENOENT')) {
661
+ throw new Error(
662
+ `Bosun could not remove stale child-process state at ${existing.childStatePath}. Restart Bosun and try again.`,
663
+ { cause: error }
664
+ )
665
+ }
666
+ }
667
+ }
668
+
669
+ if (!releaseRepositoryLease(leasePath, existing.leaseId)) {
670
+ throw new Error('The Bosun repository launcher lease changed while it was being recovered.')
671
+ }
672
+ if (!tryCreateLeaseFile(leasePath, replacement)) {
673
+ const winner = readRepositoryLease(leasePath)
674
+ if (winner) throw activeLauncherError(winner)
675
+ throw new Error('Bosun could not claim the repository lease after recovery.')
676
+ }
677
+ return {
678
+ leasePath,
679
+ leaseId: replacement.leaseId,
680
+ commandStatePath: replacement.commandStatePath,
681
+ childStatePath: replacement.childStatePath
682
+ }
683
+ } finally {
684
+ releaseRepositoryLease(recoveryPath, recoveryId)
685
+ }
686
+ }
687
+
688
+ /** @param {string} leasePath @param {string} repositoryRoot */
689
+ function assertNoRepositoryRecovery(leasePath, repositoryRoot) {
690
+ const recoveryPath = `${leasePath}.recovery`
691
+ const recovery = readRepositoryLease(recoveryPath)
692
+ if (!recovery) return
693
+ assertLeaseMatchesRepository(recovery, repositoryRoot, recoveryPath)
694
+ if (isProcessAlive(recovery.pid)) throw repositoryRecoveryError(recoveryPath, recovery)
695
+ if (!releaseRepositoryLease(recoveryPath, recovery.leaseId)) {
696
+ throw new Error(`Bosun could not clear a stale repository recovery marker at ${recoveryPath}.`)
697
+ }
698
+ }
699
+
700
+ /** @param {string} recoveryPath @param {RepositoryLease} [recovery] */
701
+ function repositoryRecoveryError(recoveryPath, recovery) {
702
+ if (recovery && isProcessAlive(recovery.pid)) {
703
+ return new Error(
704
+ `Another Bosun launcher (PID ${recovery.pid}) is already recovering this repository. Wait for it to finish, then restart Bosun.`
705
+ )
706
+ }
707
+ return new Error(`Bosun could not recover a stale repository marker at ${recoveryPath}.`)
708
+ }
709
+
710
+ /**
711
+ * Write complete contents to a unique same-directory file, then hard-link it into the exclusive
712
+ * lease name. Readers can therefore never observe a partially written lease.
713
+ *
714
+ * @param {string} leasePath
715
+ * @param {RepositoryLease} lease
716
+ */
717
+ function tryCreateLeaseFile(leasePath, lease) {
718
+ const temporaryPath = `${leasePath}.${lease.leaseId}.tmp`
719
+ try {
720
+ writeFileSync(temporaryPath, `${JSON.stringify(lease)}\n`, {
721
+ encoding: 'utf8',
722
+ flag: 'wx',
723
+ mode: 0o600
724
+ })
725
+ try {
726
+ linkSync(temporaryPath, leasePath)
727
+ return true
728
+ } catch (error) {
729
+ if (isNodeError(error, 'EEXIST')) return false
730
+ throw error
731
+ }
732
+ } finally {
733
+ try {
734
+ unlinkSync(temporaryPath)
735
+ } catch (error) {
736
+ if (!isNodeError(error, 'ENOENT')) throw error
737
+ }
738
+ }
739
+ }
740
+
741
+ /** @param {string} leasePath @returns {RepositoryLease | null} */
742
+ function readRepositoryLease(leasePath) {
743
+ let raw
744
+ try {
745
+ raw = readFileSync(leasePath, 'utf8')
746
+ } catch (error) {
747
+ if (isNodeError(error, 'ENOENT')) return null
748
+ throw error
749
+ }
750
+
751
+ let parsed
752
+ try {
753
+ parsed = JSON.parse(raw)
754
+ } catch {
755
+ throw unreadableLeaseError(leasePath)
756
+ }
757
+ if (!isRepositoryLease(parsed)) throw unreadableLeaseError(leasePath)
758
+ return parsed
759
+ }
760
+
761
+ /** @param {unknown} value @returns {value is RepositoryLease} */
762
+ function isRepositoryLease(value) {
763
+ if (!value || typeof value !== 'object') return false
764
+ const lease = /** @type {Partial<RepositoryLease>} */ (value)
765
+ return (
766
+ lease.version === REPOSITORY_LEASE_VERSION &&
767
+ typeof lease.leaseId === 'string' &&
768
+ lease.leaseId.length > 0 &&
769
+ typeof lease.launchId === 'string' &&
770
+ Number.isSafeInteger(lease.pid) &&
771
+ Number(lease.pid) > 0 &&
772
+ typeof lease.repositoryRoot === 'string' &&
773
+ typeof lease.commandStatePath === 'string' &&
774
+ (lease.childStatePath === undefined || typeof lease.childStatePath === 'string') &&
775
+ typeof lease.createdAt === 'string'
776
+ )
777
+ }
778
+
779
+ /** @param {RepositoryLease} lease @param {string} repositoryRoot @param {string} leasePath */
780
+ function assertLeaseMatchesRepository(lease, repositoryRoot, leasePath) {
781
+ const commandStateFilename = basename(lease.commandStatePath)
782
+ const childStateFilename = lease.childStatePath ? basename(lease.childStatePath) : null
783
+ if (
784
+ lease.repositoryRoot !== repositoryRoot ||
785
+ dirname(resolve(lease.commandStatePath)) !== dirname(resolve(leasePath)) ||
786
+ !commandStateFilename.startsWith('bosun-launcher.commands.') ||
787
+ !commandStateFilename.endsWith('.json') ||
788
+ (lease.childStatePath !== undefined &&
789
+ (dirname(resolve(lease.childStatePath)) !== dirname(resolve(leasePath)) ||
790
+ !childStateFilename?.startsWith('bosun-launcher.children.') ||
791
+ !childStateFilename.endsWith('.json')))
792
+ ) {
793
+ throw unreadableLeaseError(leasePath)
794
+ }
795
+ }
796
+
797
+ /** @param {number} pid */
798
+ function isProcessAlive(pid) {
799
+ try {
800
+ process.kill(pid, 0)
801
+ return true
802
+ } catch (error) {
803
+ return !isNodeError(error, 'ESRCH')
804
+ }
805
+ }
806
+
807
+ /**
808
+ * Remove a lease only while the on-disk ownership ID still matches the caller.
809
+ *
810
+ * @param {string} leasePath
811
+ * @param {string} leaseId
812
+ */
813
+ function releaseRepositoryLease(leasePath, leaseId) {
814
+ let current
815
+ try {
816
+ current = readRepositoryLease(leasePath)
817
+ } catch {
818
+ return false
819
+ }
820
+ if (!current || current.leaseId !== leaseId) return false
821
+ try {
822
+ unlinkSync(leasePath)
823
+ return true
824
+ } catch (error) {
825
+ if (isNodeError(error, 'ENOENT')) return false
826
+ return false
827
+ }
828
+ }
829
+
830
+ /** @param {RepositoryLease} lease */
831
+ function activeLauncherError(lease) {
832
+ return new Error(
833
+ `Bosun Git mode is already running for this repository (launcher PID ${lease.pid}). Close the other Bosun instance, then restart Bosun.`
834
+ )
835
+ }
836
+
837
+ /** @param {RepositoryLease} lease */
838
+ function assertNoLiveRepositoryCommands(lease) {
839
+ const commandState = readRepositoryCommandStateIfPresent(lease.commandStatePath)
840
+ if (!commandState) return
841
+ const liveProcesses = commandState.processes.filter(isRepositoryCommandProcessAlive)
842
+ if (liveProcesses.length === 0) return
843
+ throw new Error(
844
+ `A previous Bosun process ended, but Git or pnpm is still running for this repository (PIDs: ${liveProcesses.map((entry) => entry.pid).join(', ')}). Wait for it to finish or stop it, then restart Bosun.`
845
+ )
846
+ }
847
+
848
+ /** @param {RepositoryLease} lease */
849
+ function assertNoLiveRepositoryChildren(lease) {
850
+ if (!lease.childStatePath) return
851
+ const childState = readRepositoryChildStateIfPresent(lease.childStatePath)
852
+ if (!childState) return
853
+ if (childState.leaseId !== lease.leaseId || childState.launchId !== lease.launchId) {
854
+ throw new Error(`Bosun cannot verify child-process ownership at ${lease.childStatePath}.`)
855
+ }
856
+ const liveChildren = childState.processes.filter((entry) => isProcessAlive(entry.pid))
857
+ if (liveChildren.length === 0) return
858
+ throw new Error(
859
+ `A previous Bosun launcher ended, but its ${liveChildren.map((entry) => `${entry.name} process ${entry.pid}`).join(' and ')} is still running. Wait for it to exit or stop it, then restart Bosun.`
860
+ )
861
+ }
862
+
863
+ /** @param {RepositoryCommandProcess} commandProcess */
864
+ function isRepositoryCommandProcessAlive(commandProcess) {
865
+ if (isProcessAlive(commandProcess.pid)) return true
866
+ if (platform() === 'win32') return false
867
+ try {
868
+ process.kill(-commandProcess.processGroupId, 0)
869
+ return true
870
+ } catch (error) {
871
+ return !isNodeError(error, 'ESRCH')
872
+ }
873
+ }
874
+
875
+ /** @param {string} leasePath */
876
+ function unreadableLeaseError(leasePath) {
877
+ return new Error(
878
+ `Bosun found an unreadable repository launcher lease at ${leasePath}. To avoid overlapping repository operations, confirm that no Bosun, Git, or pnpm process for this repository is still running. Then remove that lease and its associated bosun-launcher.commands.*.json ledger in ${dirname(leasePath)}, and restart Bosun.`
879
+ )
880
+ }
881
+
882
+ /** @param {string} commandStatePath @param {RepositoryCommandState} state */
883
+ function initializeRepositoryCommandState(commandStatePath, state) {
884
+ writeFileSync(commandStatePath, `${JSON.stringify(state)}\n`, {
885
+ encoding: 'utf8',
886
+ flag: 'wx',
887
+ mode: 0o600
888
+ })
889
+ }
890
+
891
+ /** @param {string} childStatePath @param {RepositoryChildState} state */
892
+ function initializeRepositoryChildState(childStatePath, state) {
893
+ writeFileSync(childStatePath, `${JSON.stringify(state)}\n`, {
894
+ encoding: 'utf8',
895
+ flag: 'wx',
896
+ mode: 0o600
897
+ })
898
+ }
899
+
900
+ /** @param {string} childStatePath @returns {RepositoryChildState} */
901
+ function readRepositoryChildState(childStatePath) {
902
+ let parsed
903
+ try {
904
+ parsed = JSON.parse(readFileSync(childStatePath, 'utf8'))
905
+ } catch (error) {
906
+ throw new Error(`Bosun cannot read child-process state at ${childStatePath}.`, {
907
+ cause: error
908
+ })
909
+ }
910
+ if (!isRepositoryChildState(parsed)) {
911
+ throw new Error(`Bosun found invalid child-process state at ${childStatePath}.`)
912
+ }
913
+ return parsed
914
+ }
915
+
916
+ /** @param {string} childStatePath @returns {RepositoryChildState | null} */
917
+ function readRepositoryChildStateIfPresent(childStatePath) {
918
+ try {
919
+ return readRepositoryChildState(childStatePath)
920
+ } catch (error) {
921
+ if (error instanceof Error && error.cause && isNodeError(error.cause, 'ENOENT')) return null
922
+ throw error
923
+ }
924
+ }
925
+
926
+ /** @param {string} childStatePath @param {RepositoryChildState} state */
927
+ function writeRepositoryChildState(childStatePath, state) {
928
+ const temporaryPath = `${childStatePath}.${process.pid}.${randomUUID()}.tmp`
929
+ try {
930
+ writeFileSync(temporaryPath, `${JSON.stringify(state)}\n`, {
931
+ encoding: 'utf8',
932
+ flag: 'wx',
933
+ mode: 0o600
934
+ })
935
+ renameSync(temporaryPath, childStatePath)
936
+ } finally {
937
+ try {
938
+ unlinkSync(temporaryPath)
939
+ } catch (error) {
940
+ if (!isNodeError(error, 'ENOENT')) throw error
941
+ }
942
+ }
943
+ }
944
+
945
+ /** @param {unknown} value @returns {value is RepositoryChildState} */
946
+ function isRepositoryChildState(value) {
947
+ if (!value || typeof value !== 'object') return false
948
+ const state = /** @type {Partial<RepositoryChildState>} */ (value)
949
+ return (
950
+ state.version === 1 &&
951
+ typeof state.leaseId === 'string' &&
952
+ typeof state.launchId === 'string' &&
953
+ Array.isArray(state.processes) &&
954
+ state.processes.every(
955
+ (entry) =>
956
+ entry &&
957
+ typeof entry === 'object' &&
958
+ Number.isSafeInteger(entry.pid) &&
959
+ entry.pid > 0 &&
960
+ typeof entry.name === 'string' &&
961
+ typeof entry.startedAt === 'string'
962
+ )
963
+ )
964
+ }
965
+
966
+ /** @param {string} commandStatePath @returns {RepositoryCommandState} */
967
+ function readRepositoryCommandState(commandStatePath) {
968
+ let parsed
969
+ try {
970
+ parsed = JSON.parse(readFileSync(commandStatePath, 'utf8'))
971
+ } catch (error) {
972
+ throw new Error(
973
+ `Bosun cannot verify repository command state at ${commandStatePath}; the repository lease was retained.`,
974
+ { cause: error }
975
+ )
976
+ }
977
+ if (!isRepositoryCommandState(parsed)) {
978
+ throw new Error(
979
+ `Bosun found invalid repository command state at ${commandStatePath}; the repository lease was retained.`
980
+ )
981
+ }
982
+ return parsed
983
+ }
984
+
985
+ /** @param {string} commandStatePath @returns {RepositoryCommandState | null} */
986
+ function readRepositoryCommandStateIfPresent(commandStatePath) {
987
+ try {
988
+ return readRepositoryCommandState(commandStatePath)
989
+ } catch (error) {
990
+ if (error instanceof Error && error.cause && isNodeError(error.cause, 'ENOENT')) return null
991
+ throw error
992
+ }
993
+ }
994
+
995
+ /** @param {unknown} value @returns {value is RepositoryCommandState} */
996
+ function isRepositoryCommandState(value) {
997
+ if (!value || typeof value !== 'object') return false
998
+ const state = /** @type {Partial<RepositoryCommandState>} */ (value)
999
+ return (
1000
+ state.version === 1 &&
1001
+ typeof state.leaseId === 'string' &&
1002
+ typeof state.launchId === 'string' &&
1003
+ typeof state.repositoryRoot === 'string' &&
1004
+ Array.isArray(state.processes) &&
1005
+ state.processes.every(
1006
+ (process) =>
1007
+ process &&
1008
+ typeof process === 'object' &&
1009
+ Number.isSafeInteger(process.pid) &&
1010
+ process.pid > 0 &&
1011
+ Number.isSafeInteger(process.processGroupId) &&
1012
+ process.processGroupId > 0 &&
1013
+ typeof process.executable === 'string' &&
1014
+ typeof process.startedAt === 'string'
1015
+ )
1016
+ )
1017
+ }
1018
+
1019
+ /** @param {unknown} error @param {string} code */
1020
+ function isNodeError(error, code) {
1021
+ return error instanceof Error && 'code' in error && error.code === code
1022
+ }
1023
+
1024
+ /**
1025
+ * Strip every inherited/raw Git-mode setting, then add only launcher-validated values. The exact
1026
+ * frontend origin is always launcher-owned, whether Git mode is enabled or disabled.
1027
+ *
1028
+ * @param {NodeJS.ProcessEnv} baseEnv
1029
+ * @param {GitModeRuntime | null} runtime
1030
+ * @param {string} allowedOrigin
1031
+ */
1032
+ export function createBosunChildEnvironment(baseEnv, runtime, allowedOrigin) {
1033
+ const childEnv = { ...baseEnv }
1034
+ for (const name of GIT_MODE_ENV_NAMES) delete childEnv[name]
1035
+ childEnv.BOSUN_ALLOWED_ORIGIN = allowedOrigin
1036
+
1037
+ if (!runtime) return childEnv
1038
+ for (const name of GIT_REPOSITORY_ENV_NAMES) delete childEnv[name]
1039
+
1040
+ Object.assign(childEnv, {
1041
+ BOSUN_GIT_MODE: 'true',
1042
+ BOSUN_CURRENT_BRANCH: runtime.currentBranch,
1043
+ BOSUN_PERSONAL_BRANCH: runtime.personalBranch,
1044
+ BOSUN_GIT_ACTIVATION_IDLE_MS: String(runtime.activationIdleMs),
1045
+ BOSUN_GIT_REPOSITORY_ROOT: runtime.repositoryRoot,
1046
+ BOSUN_GIT_PERSONAL_UPSTREAM: runtime.personalUpstream,
1047
+ BOSUN_GIT_PERSONAL_UPSTREAM_REMOTE: runtime.personalUpstreamRemote,
1048
+ BOSUN_GIT_PERSONAL_UPSTREAM_BRANCH: runtime.personalUpstreamBranch,
1049
+ BOSUN_GIT_CURRENT_UPSTREAM: runtime.currentUpstream,
1050
+ BOSUN_GIT_CURRENT_UPSTREAM_REMOTE: runtime.currentUpstreamRemote,
1051
+ BOSUN_GIT_CURRENT_UPSTREAM_BRANCH: runtime.currentUpstreamBranch,
1052
+ BOSUN_GIT_LAUNCH_ID: runtime.launchId,
1053
+ BOSUN_GIT_RUNTIME_STATE_PATH: runtime.runtimeStatePath,
1054
+ BOSUN_GIT_COMMAND_STATE_PATH: runtime.commandStatePath,
1055
+ BOSUN_GIT_LEASE_ID: runtime.leaseId,
1056
+ BOSUN_GIT_ACTIVATION_RESTART_EXIT_CODE: String(GIT_ACTIVATION_RESTART_EXIT_CODE)
1057
+ })
1058
+
1059
+ return childEnv
1060
+ }
1061
+
1062
+ /** @param {string} cwd */
1063
+ function createGitRunner(cwd) {
1064
+ /** @type {GitRunner} */
1065
+ return (args) => {
1066
+ /** @type {NodeJS.ProcessEnv} */
1067
+ const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' }
1068
+ for (const name of GIT_REPOSITORY_ENV_NAMES) delete gitEnv[name]
1069
+ const result = spawnSync('git', args, {
1070
+ cwd,
1071
+ encoding: 'utf8',
1072
+ env: gitEnv,
1073
+ stdio: ['ignore', 'pipe', 'pipe']
1074
+ })
1075
+ return {
1076
+ status: result.status,
1077
+ stdout: result.stdout ?? '',
1078
+ stderr: result.stderr ?? '',
1079
+ error: result.error
1080
+ }
1081
+ }
1082
+ }
1083
+
1084
+ /** @param {GitRunner} runGit @param {string[]} args */
1085
+ function gitSucceeded(runGit, args) {
1086
+ const result = runGit(args)
1087
+ if (result.error) throw gitExecutionError(result.error)
1088
+ return result.status === 0
1089
+ }
1090
+
1091
+ /** @param {GitRunner} runGit @param {string[]} args */
1092
+ function optionalGitOutput(runGit, args) {
1093
+ const result = runGit(args)
1094
+ if (result.error) throw gitExecutionError(result.error)
1095
+ return result.status === 0 ? result.stdout.trim() : ''
1096
+ }
1097
+
1098
+ /** @param {GitRunner} runGit @param {string[]} args @param {string} errorMessage */
1099
+ function requiredGitResult(runGit, args, errorMessage) {
1100
+ const result = runGit(args)
1101
+ if (result.error) throw gitExecutionError(result.error)
1102
+ if (result.status !== 0) {
1103
+ const detail = result.stderr.trim()
1104
+ throw new Error(detail ? `${errorMessage}\nGit reported: ${detail}` : errorMessage)
1105
+ }
1106
+ return result
1107
+ }
1108
+
1109
+ /** @param {GitRunner} runGit @param {string[]} args @param {string} errorMessage */
1110
+ function requiredGitOutput(runGit, args, errorMessage) {
1111
+ const result = runGit(args)
1112
+ if (result.error) throw gitExecutionError(result.error)
1113
+ const output = result.stdout.trim()
1114
+ if (result.status !== 0 || !output) {
1115
+ const detail = result.stderr.trim()
1116
+ throw new Error(detail ? `${errorMessage}\nGit reported: ${detail}` : errorMessage)
1117
+ }
1118
+ return output
1119
+ }
1120
+
1121
+ /** @param {Error} error */
1122
+ function gitExecutionError(error) {
1123
+ const isMissingGit = 'code' in error && error.code === 'ENOENT'
1124
+ return new Error(
1125
+ isMissingGit
1126
+ ? 'Bosun Git mode requires Git to be installed and available on PATH.'
1127
+ : `Bosun could not run Git: ${error.message}`
1128
+ )
1129
+ }
1130
+
1131
+ /** @param {string} value */
1132
+ function quoteForMessage(value) {
1133
+ return JSON.stringify(value)
1134
+ }
1135
+
1136
+ /** @param {string} value */
1137
+ function quoteForShell(value) {
1138
+ return `'${value.replaceAll("'", `'"'"'`)}'`
1139
+ }