@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.
- package/README.md +24 -0
- package/bin/bosun.js +153 -20
- package/bin/git-mode.js +1139 -0
- package/build/client/_app/immutable/assets/0.D9HpHezk.css +1 -0
- package/build/client/_app/immutable/assets/0.D9HpHezk.css.br +0 -0
- package/build/client/_app/immutable/assets/0.D9HpHezk.css.gz +0 -0
- package/build/client/_app/immutable/assets/{2.DtxQDOXx.css → 2.Bc0Sf3YD.css} +1 -1
- package/build/client/_app/immutable/assets/2.Bc0Sf3YD.css.br +0 -0
- package/build/client/_app/immutable/assets/2.Bc0Sf3YD.css.gz +0 -0
- package/build/client/_app/immutable/chunks/{DneQQ7D5.js → -3ZOIHDA.js} +1 -1
- package/build/client/_app/immutable/chunks/-3ZOIHDA.js.br +0 -0
- package/build/client/_app/immutable/chunks/-3ZOIHDA.js.gz +0 -0
- package/build/client/_app/immutable/chunks/7hV1SZxK.js +1 -0
- package/build/client/_app/immutable/chunks/7hV1SZxK.js.br +2 -0
- package/build/client/_app/immutable/chunks/7hV1SZxK.js.gz +0 -0
- package/build/client/_app/immutable/chunks/B-dTD_wm.js +1 -0
- package/build/client/_app/immutable/chunks/B-dTD_wm.js.br +0 -0
- package/build/client/_app/immutable/chunks/B-dTD_wm.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{UqjCSZmA.js → B-iasAJv.js} +1 -1
- package/build/client/_app/immutable/chunks/B-iasAJv.js.br +0 -0
- package/build/client/_app/immutable/chunks/B-iasAJv.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BDijIo6D.js +33 -0
- package/build/client/_app/immutable/chunks/BDijIo6D.js.br +0 -0
- package/build/client/_app/immutable/chunks/BDijIo6D.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BHB0Pj1h.js +2 -0
- package/build/client/_app/immutable/chunks/BHB0Pj1h.js.br +0 -0
- package/build/client/_app/immutable/chunks/BHB0Pj1h.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Bog028VR.js → BKfmN1Rj.js} +1 -1
- package/build/client/_app/immutable/chunks/BKfmN1Rj.js.br +0 -0
- package/build/client/_app/immutable/chunks/BKfmN1Rj.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{pPpIn6tB.js → BlFOg-Sd.js} +1 -1
- package/build/client/_app/immutable/chunks/BlFOg-Sd.js.br +0 -0
- package/build/client/_app/immutable/chunks/BlFOg-Sd.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CJcZ2GVC.js → Byg3jr1u.js} +1 -1
- package/build/client/_app/immutable/chunks/Byg3jr1u.js.br +0 -0
- package/build/client/_app/immutable/chunks/Byg3jr1u.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Bmd5zhWC.js → BzxxLgHi.js} +1 -1
- package/build/client/_app/immutable/chunks/BzxxLgHi.js.br +0 -0
- package/build/client/_app/immutable/chunks/BzxxLgHi.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CEHsomMo.js → C4UC6AgE.js} +1 -1
- package/build/client/_app/immutable/chunks/C4UC6AgE.js.br +0 -0
- package/build/client/_app/immutable/chunks/{CEHsomMo.js.gz → C4UC6AgE.js.gz} +0 -0
- package/build/client/_app/immutable/chunks/C60AG6QQ.js +1 -0
- package/build/client/_app/immutable/chunks/C60AG6QQ.js.br +0 -0
- package/build/client/_app/immutable/chunks/C60AG6QQ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CAIfVN23.js +1 -0
- package/build/client/_app/immutable/chunks/CAIfVN23.js.br +0 -0
- package/build/client/_app/immutable/chunks/CAIfVN23.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{JcG4w2gF.js → CB9iDxwI.js} +1 -1
- package/build/client/_app/immutable/chunks/CB9iDxwI.js.br +0 -0
- package/build/client/_app/immutable/chunks/CB9iDxwI.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DoD3q5w5.js → CD_KdWs9.js} +1 -1
- package/build/client/_app/immutable/chunks/CD_KdWs9.js.br +0 -0
- package/build/client/_app/immutable/chunks/CD_KdWs9.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CDy1LBR5.js +1 -0
- package/build/client/_app/immutable/chunks/CDy1LBR5.js.br +0 -0
- package/build/client/_app/immutable/chunks/CDy1LBR5.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DCXjBDmY.js → CMFNmSvo.js} +1 -1
- package/build/client/_app/immutable/chunks/CMFNmSvo.js.br +0 -0
- package/build/client/_app/immutable/chunks/CMFNmSvo.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Bv1mNXs0.js → CNIn4L55.js} +1 -1
- package/build/client/_app/immutable/chunks/CNIn4L55.js.br +0 -0
- package/build/client/_app/immutable/chunks/CNIn4L55.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CUKoVL36.js +1 -0
- package/build/client/_app/immutable/chunks/CUKoVL36.js.br +0 -0
- package/build/client/_app/immutable/chunks/CUKoVL36.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CYGbCr1i.js +3 -0
- package/build/client/_app/immutable/chunks/CYGbCr1i.js.br +0 -0
- package/build/client/_app/immutable/chunks/CYGbCr1i.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CYr0FGgn.js +1 -0
- package/build/client/_app/immutable/chunks/CYr0FGgn.js.br +0 -0
- package/build/client/_app/immutable/chunks/CYr0FGgn.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DtXXy94i.js → Ce6a22qU.js} +1 -1
- package/build/client/_app/immutable/chunks/Ce6a22qU.js.br +0 -0
- package/build/client/_app/immutable/chunks/Ce6a22qU.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DfcDjAv-.js → CeoQ-OhK.js} +1 -1
- package/build/client/_app/immutable/chunks/CeoQ-OhK.js.br +0 -0
- package/build/client/_app/immutable/chunks/CeoQ-OhK.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{D8mA-uZ4.js → CupXtwVS.js} +1 -1
- package/build/client/_app/immutable/chunks/CupXtwVS.js.br +0 -0
- package/build/client/_app/immutable/chunks/CupXtwVS.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{B5zf3x62.js → Cy2Uze-c.js} +1 -1
- package/build/client/_app/immutable/chunks/Cy2Uze-c.js.br +0 -0
- package/build/client/_app/immutable/chunks/Cy2Uze-c.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Dg0A8wPA.js → D8Lmn3S5.js} +1 -1
- package/build/client/_app/immutable/chunks/D8Lmn3S5.js.br +0 -0
- package/build/client/_app/immutable/chunks/D8Lmn3S5.js.gz +0 -0
- package/build/client/_app/immutable/chunks/D8doM1Y6.js +1 -0
- package/build/client/_app/immutable/chunks/D8doM1Y6.js.br +2 -0
- package/build/client/_app/immutable/chunks/D8doM1Y6.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{BHHfrjbV.js → D9BWOD7V.js} +1 -1
- package/build/client/_app/immutable/chunks/D9BWOD7V.js.br +3 -0
- package/build/client/_app/immutable/chunks/D9BWOD7V.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Cp_st-64.js → DBB88ZB7.js} +1 -1
- package/build/client/_app/immutable/chunks/DBB88ZB7.js.br +2 -0
- package/build/client/_app/immutable/chunks/DBB88ZB7.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Dm49pcMm.js → DC7lPrn4.js} +1 -1
- package/build/client/_app/immutable/chunks/DC7lPrn4.js.br +0 -0
- package/build/client/_app/immutable/chunks/DC7lPrn4.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DCwWNTpf.js +9 -0
- package/build/client/_app/immutable/chunks/DCwWNTpf.js.br +0 -0
- package/build/client/_app/immutable/chunks/DCwWNTpf.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{wcTQUnZO.js → DEnUeQNt.js} +1 -1
- package/build/client/_app/immutable/chunks/DEnUeQNt.js.br +0 -0
- package/build/client/_app/immutable/chunks/DEnUeQNt.js.gz +0 -0
- package/build/client/_app/immutable/chunks/D_laxq3h.js +1 -0
- package/build/client/_app/immutable/chunks/D_laxq3h.js.br +0 -0
- package/build/client/_app/immutable/chunks/D_laxq3h.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Ddv5LgEc.js → DfkYlKBk.js} +1 -1
- package/build/client/_app/immutable/chunks/DfkYlKBk.js.br +0 -0
- package/build/client/_app/immutable/chunks/DfkYlKBk.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DnMS2X6M.js +2 -0
- package/build/client/_app/immutable/chunks/DnMS2X6M.js.br +0 -0
- package/build/client/_app/immutable/chunks/DnMS2X6M.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{B1HdnQRQ.js → DrE1ro9S.js} +1 -1
- package/build/client/_app/immutable/chunks/DrE1ro9S.js.br +0 -0
- package/build/client/_app/immutable/chunks/DrE1ro9S.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DzY_RPb9.js +1 -0
- package/build/client/_app/immutable/chunks/DzY_RPb9.js.br +0 -0
- package/build/client/_app/immutable/chunks/DzY_RPb9.js.gz +0 -0
- package/build/client/_app/immutable/chunks/PrCSpCPg.js +1 -0
- package/build/client/_app/immutable/chunks/PrCSpCPg.js.br +0 -0
- package/build/client/_app/immutable/chunks/PrCSpCPg.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CN555stc.js → eMofpOrR.js} +1 -1
- package/build/client/_app/immutable/chunks/eMofpOrR.js.br +0 -0
- package/build/client/_app/immutable/chunks/eMofpOrR.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.BQUuHkSR.js +2 -0
- package/build/client/_app/immutable/entry/app.BQUuHkSR.js.br +0 -0
- package/build/client/_app/immutable/entry/app.BQUuHkSR.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.DoeTycdu.js +1 -0
- package/build/client/_app/immutable/entry/start.DoeTycdu.js.br +2 -0
- package/build/client/_app/immutable/entry/start.DoeTycdu.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.DBnFpwRM.js +4 -0
- package/build/client/_app/immutable/nodes/0.DBnFpwRM.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.DBnFpwRM.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.D-kI-fLd.js +1 -0
- package/build/client/_app/immutable/nodes/1.D-kI-fLd.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.D-kI-fLd.js.gz +0 -0
- package/build/client/_app/immutable/nodes/10.DWb5H7vX.js +28 -0
- package/build/client/_app/immutable/nodes/10.DWb5H7vX.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.DWb5H7vX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{15.D2I0y3ys.js → 15.CGc3_07c.js} +1 -1
- package/build/client/_app/immutable/nodes/15.CGc3_07c.js.br +3 -0
- package/build/client/_app/immutable/nodes/15.CGc3_07c.js.gz +0 -0
- package/build/client/_app/immutable/nodes/17.BIba62hW.js +3 -0
- package/build/client/_app/immutable/nodes/17.BIba62hW.js.br +0 -0
- package/build/client/_app/immutable/nodes/17.BIba62hW.js.gz +0 -0
- package/build/client/_app/immutable/nodes/18.Clxr6FwI.js +11 -0
- package/build/client/_app/immutable/nodes/18.Clxr6FwI.js.br +0 -0
- package/build/client/_app/immutable/nodes/18.Clxr6FwI.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.cbpnR8be.js +35 -0
- package/build/client/_app/immutable/nodes/2.cbpnR8be.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.cbpnR8be.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.DsP759xk.js +1 -0
- package/build/client/_app/immutable/nodes/3.DsP759xk.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.DsP759xk.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{4.C8VxVbVn.js → 4.BIGLAIDe.js} +1 -1
- package/build/client/_app/immutable/nodes/4.BIGLAIDe.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.BIGLAIDe.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{5.BjrnKYaA.js → 5.zMHOyhDv.js} +1 -1
- package/build/client/_app/immutable/nodes/5.zMHOyhDv.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.zMHOyhDv.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.DFQ1IZsQ.js +1 -0
- package/build/client/_app/immutable/nodes/6.DFQ1IZsQ.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.DFQ1IZsQ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.CTgEdr7L.js +2 -0
- package/build/client/_app/immutable/nodes/8.CTgEdr7L.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.CTgEdr7L.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.BidF5Uvq.js +2 -0
- package/build/client/_app/immutable/nodes/9.BidF5Uvq.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.BidF5Uvq.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/0-BZOFUIfF.js +9 -0
- package/build/server/chunks/0-BZOFUIfF.js.map +1 -0
- package/build/server/chunks/1-XwGU5VVN.js +9 -0
- package/build/server/chunks/{1-C0HdTvXW.js.map → 1-XwGU5VVN.js.map} +1 -1
- package/build/server/chunks/10-BgFSx4ib.js +9 -0
- package/build/server/chunks/10-BgFSx4ib.js.map +1 -0
- package/build/server/chunks/15-BjTTC1jf.js +9 -0
- package/build/server/chunks/{15-BQcb9C_y.js.map → 15-BjTTC1jf.js.map} +1 -1
- package/build/server/chunks/17-BEkWCfzc.js +9 -0
- package/build/server/chunks/17-BEkWCfzc.js.map +1 -0
- package/build/server/chunks/18-C-S0wKdp.js +9 -0
- package/build/server/chunks/18-C-S0wKdp.js.map +1 -0
- package/build/server/chunks/2-o-cIkIjs.js +9 -0
- package/build/server/chunks/2-o-cIkIjs.js.map +1 -0
- package/build/server/chunks/3-B-ABlyFO.js +9 -0
- package/build/server/chunks/3-B-ABlyFO.js.map +1 -0
- package/build/server/chunks/4-joAFA_Y8.js +9 -0
- package/build/server/chunks/4-joAFA_Y8.js.map +1 -0
- package/build/server/chunks/5-DIhC9k4l.js +9 -0
- package/build/server/chunks/5-DIhC9k4l.js.map +1 -0
- package/build/server/chunks/6-CMuAQIcW.js +9 -0
- package/build/server/chunks/6-CMuAQIcW.js.map +1 -0
- package/build/server/chunks/8-CbETTVRG.js +9 -0
- package/build/server/chunks/8-CbETTVRG.js.map +1 -0
- package/build/server/chunks/9-nMV90uVU.js +9 -0
- package/build/server/chunks/9-nMV90uVU.js.map +1 -0
- package/build/server/chunks/{AgentSidebar-BWBnqHnV.js → AgentSidebar-BEbSdZdN.js} +4 -4
- package/build/server/chunks/{AgentSidebar-BWBnqHnV.js.map → AgentSidebar-BEbSdZdN.js.map} +1 -1
- package/build/server/chunks/{Button-DH7gdU0s.js → Button-Dc9CR4Wh.js} +2 -2
- package/build/server/chunks/{Button-DH7gdU0s.js.map → Button-Dc9CR4Wh.js.map} +1 -1
- package/build/server/chunks/{ChatStepTrace-BivoNYOY.js → ChatStepTrace-OM96X4g8.js} +18 -7
- package/build/server/chunks/ChatStepTrace-OM96X4g8.js.map +1 -0
- package/build/server/chunks/{Dropdown-DnARl86Q.js → Dropdown-Cgd7pG-k.js} +2 -2
- package/build/server/chunks/{Dropdown-DnARl86Q.js.map → Dropdown-Cgd7pG-k.js.map} +1 -1
- package/build/server/chunks/GitControlsButton-BLcNzrDt.js +81 -0
- package/build/server/chunks/GitControlsButton-BLcNzrDt.js.map +1 -0
- package/build/server/chunks/{Icon-DP1YKCRV.js → Icon-Bogvl2n9.js} +2 -2
- package/build/server/chunks/{Icon-DP1YKCRV.js.map → Icon-Bogvl2n9.js.map} +1 -1
- package/build/server/chunks/{Input-CUJ9Cg7b.js → Input-7hpw1WLW.js} +2 -2
- package/build/server/chunks/{Input-CUJ9Cg7b.js.map → Input-7hpw1WLW.js.map} +1 -1
- package/build/server/chunks/{Modal-CGJuNpZ0.js → Modal-7IIIfFEG.js} +5 -5
- package/build/server/chunks/{Modal-CGJuNpZ0.js.map → Modal-7IIIfFEG.js.map} +1 -1
- package/build/server/chunks/{Panel-zvjxe036.js → Panel-Cj1mXx9-.js} +2 -2
- package/build/server/chunks/{Panel-zvjxe036.js.map → Panel-Cj1mXx9-.js.map} +1 -1
- package/build/server/chunks/{Tab-CUkyLqbN.js → Tab-bf4ZDs5y.js} +4 -4
- package/build/server/chunks/{Tab-CUkyLqbN.js.map → Tab-bf4ZDs5y.js.map} +1 -1
- package/build/server/chunks/{_layout.svelte-BnrWYuLv.js → _layout.svelte-C71luIwv.js} +10 -10
- package/build/server/chunks/{_layout.svelte-BnrWYuLv.js.map → _layout.svelte-C71luIwv.js.map} +1 -1
- package/build/server/chunks/{_layout.svelte-wi6o5Ecu.js → _layout.svelte-CFhHgXI_.js} +24 -18
- package/build/server/chunks/_layout.svelte-CFhHgXI_.js.map +1 -0
- package/build/server/chunks/_layout.svelte-CJd_pmkx.js +431 -0
- package/build/server/chunks/_layout.svelte-CJd_pmkx.js.map +1 -0
- package/build/server/chunks/{_layout.svelte-B88WMyk0.js → _layout.svelte-D6rhwm98.js} +445 -70
- package/build/server/chunks/_layout.svelte-D6rhwm98.js.map +1 -0
- package/build/server/chunks/{_page.svelte-CeS_21Ho.js → _page.svelte-BBvDMiTV.js} +25 -21
- package/build/server/chunks/_page.svelte-BBvDMiTV.js.map +1 -0
- package/build/server/chunks/{_page.svelte-h_J4lNT9.js → _page.svelte-BQs9dEuq.js} +8 -7
- package/build/server/chunks/{_page.svelte-h_J4lNT9.js.map → _page.svelte-BQs9dEuq.js.map} +1 -1
- package/build/server/chunks/{_page.svelte-DhFNFMnl.js → _page.svelte-BoO2LWVX.js} +34 -14
- package/build/server/chunks/_page.svelte-BoO2LWVX.js.map +1 -0
- package/build/server/chunks/{_page.svelte-CHgQjBQj.js → _page.svelte-DUCTSdv-.js} +2 -2
- package/build/server/chunks/{_page.svelte-CHgQjBQj.js.map → _page.svelte-DUCTSdv-.js.map} +1 -1
- package/build/server/chunks/_page.svelte-FXYLKyMw.js +110 -0
- package/build/server/chunks/_page.svelte-FXYLKyMw.js.map +1 -0
- package/build/server/chunks/{_page.svelte-57pyhszV.js → _page.svelte-KA1hqKj2.js} +20 -107
- package/build/server/chunks/_page.svelte-KA1hqKj2.js.map +1 -0
- package/build/server/chunks/{_page.svelte-BSVJ2d2G.js → _page.svelte-y8CZFL8a.js} +210 -106
- package/build/server/chunks/_page.svelte-y8CZFL8a.js.map +1 -0
- package/build/server/chunks/{_page.svelte-DHSYVLoj.js → _page.svelte-yIHSIEW6.js} +21 -15
- package/build/server/chunks/_page.svelte-yIHSIEW6.js.map +1 -0
- package/build/server/chunks/_server.ts-Cp9DgLcE.js +336 -0
- package/build/server/chunks/_server.ts-Cp9DgLcE.js.map +1 -0
- package/build/server/chunks/{agents-context-CbFsZmi5.js → agents-context-owgusUPR.js} +2 -2
- package/build/server/chunks/{agents-context-CbFsZmi5.js.map → agents-context-owgusUPR.js.map} +1 -1
- package/build/server/chunks/{client-DO-hqMbT.js → client-P-7bDULt.js} +4 -4
- package/build/server/chunks/{client-DO-hqMbT.js.map → client-P-7bDULt.js.map} +1 -1
- package/build/server/chunks/{conversation-view-memory-CtbUZd_L.js → conversation-view-memory-SdpdqTR7.js} +2 -2
- package/build/server/chunks/{conversation-view-memory-CtbUZd_L.js.map → conversation-view-memory-SdpdqTR7.js.map} +1 -1
- package/build/server/chunks/{edit-workspace-context-tyHBPujC.js → edit-workspace-context-hanFnXR3.js} +2 -2
- package/build/server/chunks/{edit-workspace-context-tyHBPujC.js.map → edit-workspace-context-hanFnXR3.js.map} +1 -1
- package/build/server/chunks/error.svelte-yI9RS6H4.js +15 -0
- package/build/server/chunks/{error.svelte-DmKKN1RU.js.map → error.svelte-yI9RS6H4.js.map} +1 -1
- package/build/server/chunks/git-mode-context-BpNsAUGF.js +18 -0
- package/build/server/chunks/git-mode-context-BpNsAUGF.js.map +1 -0
- package/build/server/chunks/{index-LMsd1ih2.js → index-DGtbUu8Y.js} +2 -2
- package/build/server/chunks/{index-LMsd1ih2.js.map → index-DGtbUu8Y.js.map} +1 -1
- package/build/server/chunks/{index-server-BRr5mZEq.js → index-server-l8-WsVn4.js} +2 -2
- package/build/server/chunks/{index-server-BRr5mZEq.js.map → index-server-l8-WsVn4.js.map} +1 -1
- package/build/server/chunks/{index2-ClgfzZ89.js → index2-CtgLiJVO.js} +3 -3
- package/build/server/chunks/{index2-ClgfzZ89.js.map → index2-CtgLiJVO.js.map} +1 -1
- package/build/server/chunks/{index3-BdRUAffj.js → index3-4hbbCqW0.js} +2 -2
- package/build/server/chunks/{index3-BdRUAffj.js.map → index3-4hbbCqW0.js.map} +1 -1
- package/build/server/chunks/{root-4q2Pr7ck.js → root-sWPnlpi3.js} +2 -2
- package/build/server/chunks/{root-4q2Pr7ck.js.map → root-sWPnlpi3.js.map} +1 -1
- package/build/server/chunks/{source-selection-CcN1cgXc.js → source-selection-DSkaPGfZ.js} +7 -2
- package/build/server/chunks/source-selection-DSkaPGfZ.js.map +1 -0
- package/build/server/chunks/{state.svelte-Bd1i-u49.js → state.svelte-BkvaElc6.js} +3 -3
- package/build/server/chunks/{state.svelte-Bd1i-u49.js.map → state.svelte-BkvaElc6.js.map} +1 -1
- package/build/server/chunks/trpc-client-Cr8RJ4UO.js +260 -0
- package/build/server/chunks/trpc-client-Cr8RJ4UO.js.map +1 -0
- package/build/server/index.js +4 -4
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +15 -15
- package/build/server/manifest.js.map +1 -1
- package/package.json +1 -1
- package/src/backend/git-mode/command.spec.ts +172 -0
- package/src/backend/git-mode/command.ts +405 -0
- package/src/backend/git-mode/config.spec.ts +45 -0
- package/src/backend/git-mode/config.ts +90 -0
- package/src/backend/git-mode/index.ts +20 -0
- package/src/backend/git-mode/repository-write-gate.spec.ts +116 -0
- package/src/backend/git-mode/repository-write-gate.ts +260 -0
- package/src/backend/git-mode/router.ts +59 -0
- package/src/backend/git-mode/service.spec.ts +1653 -0
- package/src/backend/git-mode/service.ts +2183 -0
- package/src/backend/git-mode/types.ts +82 -0
- package/src/backend/git-mode/write-tracker.spec.ts +46 -0
- package/src/backend/git-mode/write-tracker.ts +39 -0
- package/src/backend/lib/backend-restart.spec.ts +27 -0
- package/src/backend/lib/backend-restart.ts +16 -2
- package/src/backend/lib/middleware/cors.spec.ts +67 -0
- package/src/backend/lib/middleware/cors.ts +40 -12
- package/src/backend/roleplay/agent-loader.ts +49 -0
- package/src/backend/roleplay/old-server.ts +106 -56
- package/src/backend/roleplay/prompt-edit-proposals.ts +38 -4
- package/src/backend/roleplay/prompt-file.ts +59 -0
- package/src/backend/roleplay/router.spec.ts +75 -0
- package/src/backend/roleplay/router.ts +123 -69
- package/src/backend/roleplay/suggest-file-edit.ts +160 -0
- package/src/backend/roleplay/suggest-prompt-edit.ts +9 -138
- package/src/backend/roleplay/suggest-test-fix.spec.ts +50 -0
- package/src/backend/roleplay/suggest-test-fix.ts +65 -0
- package/src/backend/roleplay/test-fix-proposal-acceptance.spec.ts +253 -0
- package/src/backend/server.ts +116 -13
- package/src/backend/tests/academy.ts +17 -3
- package/src/backend/tests/data.spec.ts +81 -2
- package/src/backend/tests/data.ts +74 -4
- package/src/backend/tests/router.ts +101 -36
- package/src/backend/tests/suggest-test-history-edit.spec.ts +20 -0
- package/src/backend/tests/suggest-test-history-edit.ts +52 -0
- package/src/backend/tests/test-edit-source.spec.ts +61 -0
- package/src/backend/tests/test-edit-source.ts +34 -0
- package/src/backend/tests/test-history-edit-proposals.ts +69 -0
- package/src/backend/tests/test-history-overrides.spec.ts +24 -0
- package/src/backend/tests/test-selection.ts +27 -0
- package/src/lib/api-telescope/source-selection.spec.ts +44 -0
- package/src/lib/api-telescope/source-selection.ts +28 -0
- package/src/lib/components/Modal.svelte +1 -1
- package/src/lib/edit/edit-workspace-context.ts +33 -3
- package/src/lib/edit/test-ai-edit.spec.ts +31 -0
- package/src/lib/edit/test-ai-edit.ts +52 -0
- package/src/lib/git-mode/BlockingGitDialog.svelte +118 -0
- package/src/lib/git-mode/GitControlsButton.svelte +96 -0
- package/src/lib/git-mode/client-admission.spec.ts +304 -0
- package/src/lib/git-mode/client-admission.ts +343 -0
- package/src/lib/git-mode/errors.ts +2 -0
- package/src/lib/git-mode/git-mode-context.ts +70 -0
- package/src/lib/git-mode/refresh-policy.spec.ts +95 -0
- package/src/lib/git-mode/refresh-policy.ts +39 -0
- package/src/lib/git-mode/status-recovery.spec.ts +86 -0
- package/src/lib/git-mode/status-recovery.ts +33 -0
- package/src/lib/monaco/bosun-theme.ts +6 -83
- package/src/lib/monaco/editor-appearance.spec.ts +84 -0
- package/src/lib/monaco/editor-appearance.ts +154 -0
- package/src/lib/monaco/editor-decorations.spec.ts +91 -0
- package/src/lib/monaco/editor-decorations.ts +133 -0
- package/src/lib/tests/assertion-types.ts +74 -0
- package/src/lib/trpc-client.ts +15 -1
- package/src/lib/ws-client.spec.ts +208 -0
- package/src/lib/ws-client.ts +196 -14
- package/src/lib/ws-message.ts +24 -0
- package/build/client/_app/immutable/assets/0.Rrc0O--C.css +0 -1
- package/build/client/_app/immutable/assets/0.Rrc0O--C.css.br +0 -0
- package/build/client/_app/immutable/assets/0.Rrc0O--C.css.gz +0 -0
- package/build/client/_app/immutable/assets/2.DtxQDOXx.css.br +0 -0
- package/build/client/_app/immutable/assets/2.DtxQDOXx.css.gz +0 -0
- package/build/client/_app/immutable/chunks/B1HdnQRQ.js.br +0 -0
- package/build/client/_app/immutable/chunks/B1HdnQRQ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/B5zf3x62.js.br +0 -0
- package/build/client/_app/immutable/chunks/B5zf3x62.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BHHfrjbV.js.br +0 -2
- package/build/client/_app/immutable/chunks/BHHfrjbV.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BU7aeYn4.js +0 -1
- package/build/client/_app/immutable/chunks/BU7aeYn4.js.br +0 -0
- package/build/client/_app/immutable/chunks/BU7aeYn4.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BbGYcfPY.js +0 -3
- package/build/client/_app/immutable/chunks/BbGYcfPY.js.br +0 -0
- package/build/client/_app/immutable/chunks/BbGYcfPY.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Bmd5zhWC.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bmd5zhWC.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BnqH7p3U.js +0 -33
- package/build/client/_app/immutable/chunks/BnqH7p3U.js.br +0 -0
- package/build/client/_app/immutable/chunks/BnqH7p3U.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Bog028VR.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bog028VR.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Bv1mNXs0.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bv1mNXs0.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BvjXTrOz.js +0 -1
- package/build/client/_app/immutable/chunks/BvjXTrOz.js.br +0 -0
- package/build/client/_app/immutable/chunks/BvjXTrOz.js.gz +0 -0
- package/build/client/_app/immutable/chunks/C1R-s4aD.js +0 -1
- package/build/client/_app/immutable/chunks/C1R-s4aD.js.br +0 -0
- package/build/client/_app/immutable/chunks/C1R-s4aD.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CC1OcqhR.js +0 -1
- package/build/client/_app/immutable/chunks/CC1OcqhR.js.br +0 -0
- package/build/client/_app/immutable/chunks/CC1OcqhR.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CEHsomMo.js.br +0 -0
- package/build/client/_app/immutable/chunks/CJcZ2GVC.js.br +0 -0
- package/build/client/_app/immutable/chunks/CJcZ2GVC.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CN555stc.js.br +0 -0
- package/build/client/_app/immutable/chunks/CN555stc.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CQg2Mme0.js +0 -9
- package/build/client/_app/immutable/chunks/CQg2Mme0.js.br +0 -0
- package/build/client/_app/immutable/chunks/CQg2Mme0.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Cp_st-64.js.br +0 -0
- package/build/client/_app/immutable/chunks/Cp_st-64.js.gz +0 -0
- package/build/client/_app/immutable/chunks/D8mA-uZ4.js.br +0 -0
- package/build/client/_app/immutable/chunks/D8mA-uZ4.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DCXjBDmY.js.br +0 -0
- package/build/client/_app/immutable/chunks/DCXjBDmY.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DSLyVE1P.js +0 -1
- package/build/client/_app/immutable/chunks/DSLyVE1P.js.br +0 -0
- package/build/client/_app/immutable/chunks/DSLyVE1P.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Ddv5LgEc.js.br +0 -0
- package/build/client/_app/immutable/chunks/Ddv5LgEc.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DfcDjAv-.js.br +0 -0
- package/build/client/_app/immutable/chunks/DfcDjAv-.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Dg0A8wPA.js.br +0 -0
- package/build/client/_app/immutable/chunks/Dg0A8wPA.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Dm49pcMm.js.br +0 -0
- package/build/client/_app/immutable/chunks/Dm49pcMm.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DneQQ7D5.js.br +0 -0
- package/build/client/_app/immutable/chunks/DneQQ7D5.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DoD3q5w5.js.br +0 -0
- package/build/client/_app/immutable/chunks/DoD3q5w5.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DtXXy94i.js.br +0 -0
- package/build/client/_app/immutable/chunks/DtXXy94i.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Dy2CFyD6.js +0 -2
- package/build/client/_app/immutable/chunks/Dy2CFyD6.js.br +0 -0
- package/build/client/_app/immutable/chunks/Dy2CFyD6.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DyBwTDJp.js +0 -2
- package/build/client/_app/immutable/chunks/DyBwTDJp.js.br +0 -0
- package/build/client/_app/immutable/chunks/DyBwTDJp.js.gz +0 -0
- package/build/client/_app/immutable/chunks/ILTCveYH.js +0 -1
- package/build/client/_app/immutable/chunks/ILTCveYH.js.br +0 -0
- package/build/client/_app/immutable/chunks/ILTCveYH.js.gz +0 -0
- package/build/client/_app/immutable/chunks/JcG4w2gF.js.br +0 -0
- package/build/client/_app/immutable/chunks/JcG4w2gF.js.gz +0 -0
- package/build/client/_app/immutable/chunks/UqjCSZmA.js.br +0 -0
- package/build/client/_app/immutable/chunks/UqjCSZmA.js.gz +0 -0
- package/build/client/_app/immutable/chunks/_YcrHZ6P.js +0 -1
- package/build/client/_app/immutable/chunks/_YcrHZ6P.js.br +0 -2
- package/build/client/_app/immutable/chunks/_YcrHZ6P.js.gz +0 -0
- package/build/client/_app/immutable/chunks/i6ygh6h8.js +0 -1
- package/build/client/_app/immutable/chunks/i6ygh6h8.js.br +0 -2
- package/build/client/_app/immutable/chunks/i6ygh6h8.js.gz +0 -0
- package/build/client/_app/immutable/chunks/oRi68ch3.js +0 -1
- package/build/client/_app/immutable/chunks/oRi68ch3.js.br +0 -0
- package/build/client/_app/immutable/chunks/oRi68ch3.js.gz +0 -0
- package/build/client/_app/immutable/chunks/pPpIn6tB.js.br +0 -0
- package/build/client/_app/immutable/chunks/pPpIn6tB.js.gz +0 -0
- package/build/client/_app/immutable/chunks/r0sHOWrM.js +0 -1
- package/build/client/_app/immutable/chunks/r0sHOWrM.js.br +0 -0
- package/build/client/_app/immutable/chunks/r0sHOWrM.js.gz +0 -0
- package/build/client/_app/immutable/chunks/wcTQUnZO.js.br +0 -0
- package/build/client/_app/immutable/chunks/wcTQUnZO.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.DdO0gkPg.js +0 -2
- package/build/client/_app/immutable/entry/app.DdO0gkPg.js.br +0 -0
- package/build/client/_app/immutable/entry/app.DdO0gkPg.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.eAjDq0c9.js +0 -1
- package/build/client/_app/immutable/entry/start.eAjDq0c9.js.br +0 -2
- package/build/client/_app/immutable/entry/start.eAjDq0c9.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.nG3DdvIX.js +0 -3
- package/build/client/_app/immutable/nodes/0.nG3DdvIX.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.nG3DdvIX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.Ceu0XS6v.js +0 -1
- package/build/client/_app/immutable/nodes/1.Ceu0XS6v.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.Ceu0XS6v.js.gz +0 -0
- package/build/client/_app/immutable/nodes/10.dm2dgy31.js +0 -13
- package/build/client/_app/immutable/nodes/10.dm2dgy31.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.dm2dgy31.js.gz +0 -0
- package/build/client/_app/immutable/nodes/15.D2I0y3ys.js.br +0 -0
- package/build/client/_app/immutable/nodes/15.D2I0y3ys.js.gz +0 -0
- package/build/client/_app/immutable/nodes/17.CKxGp0E7.js +0 -3
- package/build/client/_app/immutable/nodes/17.CKxGp0E7.js.br +0 -0
- package/build/client/_app/immutable/nodes/17.CKxGp0E7.js.gz +0 -0
- package/build/client/_app/immutable/nodes/18.DHJxyQpw.js +0 -11
- package/build/client/_app/immutable/nodes/18.DHJxyQpw.js.br +0 -0
- package/build/client/_app/immutable/nodes/18.DHJxyQpw.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.DesR2keg.js +0 -35
- package/build/client/_app/immutable/nodes/2.DesR2keg.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.DesR2keg.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.BUKrXGR_.js +0 -1
- package/build/client/_app/immutable/nodes/3.BUKrXGR_.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.BUKrXGR_.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.C8VxVbVn.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.C8VxVbVn.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.BjrnKYaA.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.BjrnKYaA.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.C_hEzPrU.js +0 -1
- package/build/client/_app/immutable/nodes/6.C_hEzPrU.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.C_hEzPrU.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.BTYaOiDC.js +0 -2
- package/build/client/_app/immutable/nodes/8.BTYaOiDC.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.BTYaOiDC.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.DVspuDYY.js +0 -2
- package/build/client/_app/immutable/nodes/9.DVspuDYY.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.DVspuDYY.js.gz +0 -0
- package/build/server/chunks/0-D_duQDWy.js +0 -9
- package/build/server/chunks/0-D_duQDWy.js.map +0 -1
- package/build/server/chunks/1-C0HdTvXW.js +0 -9
- package/build/server/chunks/10-DcfhCR4R.js +0 -9
- package/build/server/chunks/10-DcfhCR4R.js.map +0 -1
- package/build/server/chunks/15-BQcb9C_y.js +0 -9
- package/build/server/chunks/17-DuVCTioj.js +0 -9
- package/build/server/chunks/17-DuVCTioj.js.map +0 -1
- package/build/server/chunks/18-CT0VFW_p.js +0 -9
- package/build/server/chunks/18-CT0VFW_p.js.map +0 -1
- package/build/server/chunks/2-DgTzKQAP.js +0 -9
- package/build/server/chunks/2-DgTzKQAP.js.map +0 -1
- package/build/server/chunks/3-eQMSMzXP.js +0 -9
- package/build/server/chunks/3-eQMSMzXP.js.map +0 -1
- package/build/server/chunks/4-OaAJtDqP.js +0 -9
- package/build/server/chunks/4-OaAJtDqP.js.map +0 -1
- package/build/server/chunks/5-CBdMutbv.js +0 -9
- package/build/server/chunks/5-CBdMutbv.js.map +0 -1
- package/build/server/chunks/6-FoEi7ggm.js +0 -9
- package/build/server/chunks/6-FoEi7ggm.js.map +0 -1
- package/build/server/chunks/8-B44yRkfX.js +0 -9
- package/build/server/chunks/8-B44yRkfX.js.map +0 -1
- package/build/server/chunks/9-eUjFpHzI.js +0 -9
- package/build/server/chunks/9-eUjFpHzI.js.map +0 -1
- package/build/server/chunks/ChatStepTrace-BivoNYOY.js.map +0 -1
- package/build/server/chunks/_layout.svelte-B-0DHjLB.js +0 -45
- package/build/server/chunks/_layout.svelte-B-0DHjLB.js.map +0 -1
- package/build/server/chunks/_layout.svelte-B88WMyk0.js.map +0 -1
- package/build/server/chunks/_layout.svelte-wi6o5Ecu.js.map +0 -1
- package/build/server/chunks/_page.svelte-57pyhszV.js.map +0 -1
- package/build/server/chunks/_page.svelte-BSVJ2d2G.js.map +0 -1
- package/build/server/chunks/_page.svelte-BoHt2cpr.js +0 -61
- package/build/server/chunks/_page.svelte-BoHt2cpr.js.map +0 -1
- package/build/server/chunks/_page.svelte-CeS_21Ho.js.map +0 -1
- package/build/server/chunks/_page.svelte-DHSYVLoj.js.map +0 -1
- package/build/server/chunks/_page.svelte-DhFNFMnl.js.map +0 -1
- package/build/server/chunks/_server.ts-D_eo_OAK.js +0 -139
- package/build/server/chunks/_server.ts-D_eo_OAK.js.map +0 -1
- package/build/server/chunks/error.svelte-DmKKN1RU.js +0 -15
- package/build/server/chunks/source-selection-CcN1cgXc.js.map +0 -1
- package/build/server/chunks/trpc-client-D8L6vWf4.js +0 -14
- package/build/server/chunks/trpc-client-D8L6vWf4.js.map +0 -1
package/bin/git-mode.js
ADDED
|
@@ -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
|
+
}
|