@polderlabs/bizar 10.2.0 → 10.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -8
- package/bizar-dash/CONTROL_SURFACES.md +0 -1
- package/bizar-dash/dist/assets/{ActivityFeed-JjmOY2AQ.js → ActivityFeed-DtRtg-JN.js} +1 -1
- package/bizar-dash/dist/assets/{ActivityFeed-JjmOY2AQ.js.map → ActivityFeed-DtRtg-JN.js.map} +1 -1
- package/bizar-dash/dist/assets/{ActivityView-BIIMvGWo.js → ActivityView-CoTXDyU_.js} +1 -1
- package/bizar-dash/dist/assets/{ActivityView-BIIMvGWo.js.map → ActivityView-CoTXDyU_.js.map} +1 -1
- package/bizar-dash/dist/assets/{AdminView-DRidJtkq.js → AdminView-DNc5Ll7L.js} +1 -1
- package/bizar-dash/dist/assets/{AdminView-DRidJtkq.js.map → AdminView-DNc5Ll7L.js.map} +1 -1
- package/bizar-dash/dist/assets/{AgentActivity-79l4SBMC.js → AgentActivity-CrX7LmQ6.js} +1 -1
- package/bizar-dash/dist/assets/{AgentActivity-79l4SBMC.js.map → AgentActivity-CrX7LmQ6.js.map} +1 -1
- package/bizar-dash/dist/assets/{AgentsView-0vT4uPSc.js → AgentsView-Du-G_skK.js} +3 -3
- package/bizar-dash/dist/assets/{AgentsView-0vT4uPSc.js.map → AgentsView-Du-G_skK.js.map} +1 -1
- package/bizar-dash/dist/assets/{Alert-CG5q_s75.js → Alert-B6DjfRFI.js} +1 -1
- package/bizar-dash/dist/assets/{Alert-CG5q_s75.js.map → Alert-B6DjfRFI.js.map} +1 -1
- package/bizar-dash/dist/assets/{ArtifactsView-DM8ZmpR9.js → ArtifactsView-znhN8GTR.js} +1 -1
- package/bizar-dash/dist/assets/{ArtifactsView-DM8ZmpR9.js.map → ArtifactsView-znhN8GTR.js.map} +1 -1
- package/bizar-dash/dist/assets/{AuthView-BveN94MC.js → AuthView-Rk8eKUPr.js} +1 -1
- package/bizar-dash/dist/assets/{AuthView-BveN94MC.js.map → AuthView-Rk8eKUPr.js.map} +1 -1
- package/bizar-dash/dist/assets/{Avatar-ulUPIYTd.js → Avatar-B41upSD1.js} +1 -1
- package/bizar-dash/dist/assets/{Avatar-ulUPIYTd.js.map → Avatar-B41upSD1.js.map} +1 -1
- package/bizar-dash/dist/assets/{BackgroundJobsView-BWErhqFo.js → BackgroundJobsView-BLWuN9qz.js} +1 -1
- package/bizar-dash/dist/assets/{BackgroundJobsView-BWErhqFo.js.map → BackgroundJobsView-BLWuN9qz.js.map} +1 -1
- package/bizar-dash/dist/assets/{BackupView-CnBjzG9N.js → BackupView-CqbtcnuQ.js} +1 -1
- package/bizar-dash/dist/assets/{BackupView-CnBjzG9N.js.map → BackupView-CqbtcnuQ.js.map} +1 -1
- package/bizar-dash/dist/assets/{Card-B0imhxMk.js → Card-rF59wHiO.js} +1 -1
- package/bizar-dash/dist/assets/{Card-B0imhxMk.js.map → Card-rF59wHiO.js.map} +1 -1
- package/bizar-dash/dist/assets/{ChatView-D6zGytzx.js → ChatView-CUriVEgU.js} +1 -1
- package/bizar-dash/dist/assets/{ChatView-D6zGytzx.js.map → ChatView-CUriVEgU.js.map} +1 -1
- package/bizar-dash/dist/assets/{Chip-wwZSGwLE.js → Chip-Dc2KmeWl.js} +1 -1
- package/bizar-dash/dist/assets/{Chip-wwZSGwLE.js.map → Chip-Dc2KmeWl.js.map} +1 -1
- package/bizar-dash/dist/assets/{ClaudeSessionsView-DK1sOLhi.js → ClaudeSessionsView-CiaHQdDZ.js} +1 -1
- package/bizar-dash/dist/assets/{ClaudeSessionsView-DK1sOLhi.js.map → ClaudeSessionsView-CiaHQdDZ.js.map} +1 -1
- package/bizar-dash/dist/assets/{ClipboardView-CZ2lYPr0.js → ClipboardView-DdP6aVso.js} +1 -1
- package/bizar-dash/dist/assets/{ClipboardView-CZ2lYPr0.js.map → ClipboardView-DdP6aVso.js.map} +1 -1
- package/bizar-dash/dist/assets/{ConfigView-D-zT9eNA.js → ConfigView-B2ysnTZU.js} +1 -1
- package/bizar-dash/dist/assets/{ConfigView-D-zT9eNA.js.map → ConfigView-B2ysnTZU.js.map} +1 -1
- package/bizar-dash/dist/assets/{DiagnosticsView-BocV68Lb.js → DiagnosticsView-88OZ5k0f.js} +1 -1
- package/bizar-dash/dist/assets/{DiagnosticsView-BocV68Lb.js.map → DiagnosticsView-88OZ5k0f.js.map} +1 -1
- package/bizar-dash/dist/assets/{DialogsView-BVjPI5x7.js → DialogsView-BcqDDCZQ.js} +1 -1
- package/bizar-dash/dist/assets/{DialogsView-BVjPI5x7.js.map → DialogsView-BcqDDCZQ.js.map} +1 -1
- package/bizar-dash/dist/assets/{DoctorView-84gJcZwM.js → DoctorView-BQwqvMAh.js} +1 -1
- package/bizar-dash/dist/assets/{DoctorView-84gJcZwM.js.map → DoctorView-BQwqvMAh.js.map} +1 -1
- package/bizar-dash/dist/assets/{EmptyState-BjgVOrcO.js → EmptyState-D65J5FQN.js} +1 -1
- package/bizar-dash/dist/assets/{EmptyState-BjgVOrcO.js.map → EmptyState-D65J5FQN.js.map} +1 -1
- package/bizar-dash/dist/assets/{EnvVarsView-BJ2TEzF7.js → EnvVarsView-DX-3oGH-.js} +2 -2
- package/bizar-dash/dist/assets/{EnvVarsView-BJ2TEzF7.js.map → EnvVarsView-DX-3oGH-.js.map} +1 -1
- package/bizar-dash/dist/assets/{ErrorState-BE3WBFqF.js → ErrorState-DULZnttJ.js} +1 -1
- package/bizar-dash/dist/assets/{ErrorState-BE3WBFqF.js.map → ErrorState-DULZnttJ.js.map} +1 -1
- package/bizar-dash/dist/assets/{EvalView-BJJ2P-kK.js → EvalView-CpdFu_ID.js} +1 -1
- package/bizar-dash/dist/assets/{EvalView-BJJ2P-kK.js.map → EvalView-CpdFu_ID.js.map} +1 -1
- package/bizar-dash/dist/assets/{GoalsView-pwROHApi.js → GoalsView-Br8oPPfb.js} +1 -1
- package/bizar-dash/dist/assets/{GoalsView-pwROHApi.js.map → GoalsView-Br8oPPfb.js.map} +1 -1
- package/bizar-dash/dist/assets/{Grid-qcvbhx6e.js → Grid-BSXhsCPC.js} +1 -1
- package/bizar-dash/dist/assets/{Grid-qcvbhx6e.js.map → Grid-BSXhsCPC.js.map} +1 -1
- package/bizar-dash/dist/assets/{HistoryView-C7SSOLJA.js → HistoryView-D0J3aoZF.js} +1 -1
- package/bizar-dash/dist/assets/{HistoryView-C7SSOLJA.js.map → HistoryView-D0J3aoZF.js.map} +1 -1
- package/bizar-dash/dist/assets/{Input-Bgzxf_Hu.js → Input-CZmRw5QX.js} +1 -1
- package/bizar-dash/dist/assets/{Input-Bgzxf_Hu.js.map → Input-CZmRw5QX.js.map} +1 -1
- package/bizar-dash/dist/assets/{LibrariesView-CbFFksox.js → LibrariesView-B1YX9wzp.js} +1 -1
- package/bizar-dash/dist/assets/{LibrariesView-CbFFksox.js.map → LibrariesView-B1YX9wzp.js.map} +1 -1
- package/bizar-dash/dist/assets/{LightRAGView-BqDrJbRq.js → LightRAGView-CykR0t9y.js} +1 -1
- package/bizar-dash/dist/assets/{LightRAGView-BqDrJbRq.js.map → LightRAGView-CykR0t9y.js.map} +1 -1
- package/bizar-dash/dist/assets/{ListHeader-TjPJrUfY.js → ListHeader-Dpun-r36.js} +1 -1
- package/bizar-dash/dist/assets/{ListHeader-TjPJrUfY.js.map → ListHeader-Dpun-r36.js.map} +1 -1
- package/bizar-dash/dist/assets/{MemoryView-BlTeHLhX.js → MemoryView-BbER5ZKt.js} +1 -1
- package/bizar-dash/dist/assets/{MemoryView-BlTeHLhX.js.map → MemoryView-BbER5ZKt.js.map} +1 -1
- package/bizar-dash/dist/assets/MiscView-DLNEGdML.js +1 -0
- package/bizar-dash/dist/assets/{MiscView-B8QY82Pm.js.map → MiscView-DLNEGdML.js.map} +1 -1
- package/bizar-dash/dist/assets/{ModsView-CvW1OWts.js → ModsView-P7gDeNHM.js} +2 -2
- package/bizar-dash/dist/assets/{ModsView-CvW1OWts.js.map → ModsView-P7gDeNHM.js.map} +1 -1
- package/bizar-dash/dist/assets/{NotificationsView-CsBt5U7x.js → NotificationsView-DAm2O0yf.js} +1 -1
- package/bizar-dash/dist/assets/{NotificationsView-CsBt5U7x.js.map → NotificationsView-DAm2O0yf.js.map} +1 -1
- package/bizar-dash/dist/assets/ObsidianView-Ci80gnwS.js +1 -0
- package/bizar-dash/dist/assets/{ObsidianView-BCTejEj2.js.map → ObsidianView-Ci80gnwS.js.map} +1 -1
- package/bizar-dash/dist/assets/{OverviewView-BsuPxwnB.js → OverviewView-rriw1GjA.js} +1 -1
- package/bizar-dash/dist/assets/{OverviewView-BsuPxwnB.js.map → OverviewView-rriw1GjA.js.map} +1 -1
- package/bizar-dash/dist/assets/{ProgressBar-DlZTETEn.js → ProgressBar-BahmeJhP.js} +1 -1
- package/bizar-dash/dist/assets/{ProgressBar-DlZTETEn.js.map → ProgressBar-BahmeJhP.js.map} +1 -1
- package/bizar-dash/dist/assets/{ProjectsView-Soxtmpet.js → ProjectsView-ChzM12bs.js} +1 -1
- package/bizar-dash/dist/assets/{ProjectsView-Soxtmpet.js.map → ProjectsView-ChzM12bs.js.map} +1 -1
- package/bizar-dash/dist/assets/{ProvidersView-DaCEV9pM.js → ProvidersView-BftGn8Cr.js} +1 -1
- package/bizar-dash/dist/assets/{ProvidersView-DaCEV9pM.js.map → ProvidersView-BftGn8Cr.js.map} +1 -1
- package/bizar-dash/dist/assets/{SchedulesView-DY2ByBj_.js → SchedulesView-CtNxtzN6.js} +1 -1
- package/bizar-dash/dist/assets/{SchedulesView-DY2ByBj_.js.map → SchedulesView-CtNxtzN6.js.map} +1 -1
- package/bizar-dash/dist/assets/{Select-Dbd88_n8.js → Select-0VcfTDeL.js} +1 -1
- package/bizar-dash/dist/assets/{Select-Dbd88_n8.js.map → Select-0VcfTDeL.js.map} +1 -1
- package/bizar-dash/dist/assets/{SettingsRow-CLMNd434.js → SettingsRow-BLic3DQl.js} +1 -1
- package/bizar-dash/dist/assets/{SettingsRow-CLMNd434.js.map → SettingsRow-BLic3DQl.js.map} +1 -1
- package/bizar-dash/dist/assets/{SettingsView-b24oFq9x.js → SettingsView-BjS3i4nr.js} +1 -1
- package/bizar-dash/dist/assets/{SettingsView-b24oFq9x.js.map → SettingsView-BjS3i4nr.js.map} +1 -1
- package/bizar-dash/dist/assets/{Sheet-DnDid58j.js → Sheet-BOProMs2.js} +1 -1
- package/bizar-dash/dist/assets/{Sheet-DnDid58j.js.map → Sheet-BOProMs2.js.map} +1 -1
- package/bizar-dash/dist/assets/{Slider-BJCvKI1I.js → Slider-B0eLEEUt.js} +1 -1
- package/bizar-dash/dist/assets/{Slider-BJCvKI1I.js.map → Slider-B0eLEEUt.js.map} +1 -1
- package/bizar-dash/dist/assets/{Sparkline-ktqQLdyb.js → Sparkline-SSrdgyW0.js} +1 -1
- package/bizar-dash/dist/assets/{Sparkline-ktqQLdyb.js.map → Sparkline-SSrdgyW0.js.map} +1 -1
- package/bizar-dash/dist/assets/{Switch-CwXfsDii.js → Switch-B3EQzb_q.js} +1 -1
- package/bizar-dash/dist/assets/{Switch-CwXfsDii.js.map → Switch-B3EQzb_q.js.map} +1 -1
- package/bizar-dash/dist/assets/{TasksView-BHmpzFCf.js → TasksView-SWjF_ZUQ.js} +1 -1
- package/bizar-dash/dist/assets/{TasksView-BHmpzFCf.js.map → TasksView-SWjF_ZUQ.js.map} +1 -1
- package/bizar-dash/dist/assets/{Textarea-BzIAwSlp.js → Textarea-QuofpMlq.js} +1 -1
- package/bizar-dash/dist/assets/{Textarea-BzIAwSlp.js.map → Textarea-QuofpMlq.js.map} +1 -1
- package/bizar-dash/dist/assets/{UpdateView-CSmX9NN0.js → UpdateView-CEHW2nd6.js} +1 -1
- package/bizar-dash/dist/assets/{UpdateView-CSmX9NN0.js.map → UpdateView-CEHW2nd6.js.map} +1 -1
- package/bizar-dash/dist/assets/{UsageView-COyr_Gfi.js → UsageView-BqOw6FBg.js} +1 -1
- package/bizar-dash/dist/assets/{UsageView-COyr_Gfi.js.map → UsageView-BqOw6FBg.js.map} +1 -1
- package/bizar-dash/dist/assets/{ViewHeader-BzYZ3m-e.js → ViewHeader-B6P-C4GD.js} +1 -1
- package/bizar-dash/dist/assets/{ViewHeader-BzYZ3m-e.js.map → ViewHeader-B6P-C4GD.js.map} +1 -1
- package/bizar-dash/dist/assets/{VoiceView-D33_xG5t.js → VoiceView-CwrPwWgi.js} +1 -1
- package/bizar-dash/dist/assets/{VoiceView-D33_xG5t.js.map → VoiceView-CwrPwWgi.js.map} +1 -1
- package/bizar-dash/dist/assets/{brain-CM-Vls8r.js → brain-CwZt9Kta.js} +1 -1
- package/bizar-dash/dist/assets/{brain-CM-Vls8r.js.map → brain-CwZt9Kta.js.map} +1 -1
- package/bizar-dash/dist/assets/{calendar-C0lgZk69.js → calendar-C5oK0_nM.js} +1 -1
- package/bizar-dash/dist/assets/{calendar-C0lgZk69.js.map → calendar-C5oK0_nM.js.map} +1 -1
- package/bizar-dash/dist/assets/{circle-Bx_hQEBC.js → circle-D1Lc_P2a.js} +1 -1
- package/bizar-dash/dist/assets/{circle-Bx_hQEBC.js.map → circle-D1Lc_P2a.js.map} +1 -1
- package/bizar-dash/dist/assets/{circle-alert-DHvsqpw5.js → circle-alert-HdHe7m8K.js} +1 -1
- package/bizar-dash/dist/assets/{circle-alert-DHvsqpw5.js.map → circle-alert-HdHe7m8K.js.map} +1 -1
- package/bizar-dash/dist/assets/{circle-check-Bx8wmXR9.js → circle-check-Cs-xoV7R.js} +1 -1
- package/bizar-dash/dist/assets/{circle-check-Bx8wmXR9.js.map → circle-check-Cs-xoV7R.js.map} +1 -1
- package/bizar-dash/dist/assets/{clock-CejGoecX.js → clock-DEIuPV60.js} +1 -1
- package/bizar-dash/dist/assets/{clock-CejGoecX.js.map → clock-DEIuPV60.js.map} +1 -1
- package/bizar-dash/dist/assets/{download-4cEUWue1.js → download-Clkq90Ct.js} +1 -1
- package/bizar-dash/dist/assets/{download-4cEUWue1.js.map → download-Clkq90Ct.js.map} +1 -1
- package/bizar-dash/dist/assets/{external-link-0NqUhI4i.js → external-link-D5tq2o_I.js} +1 -1
- package/bizar-dash/dist/assets/{external-link-0NqUhI4i.js.map → external-link-D5tq2o_I.js.map} +1 -1
- package/bizar-dash/dist/assets/{file-text-yEMkrSoZ.js → file-text-CQ12t7Oc.js} +1 -1
- package/bizar-dash/dist/assets/{file-text-yEMkrSoZ.js.map → file-text-CQ12t7Oc.js.map} +1 -1
- package/bizar-dash/dist/assets/{filter-BXt60zAS.js → filter-BcbE37wP.js} +1 -1
- package/bizar-dash/dist/assets/{filter-BXt60zAS.js.map → filter-BcbE37wP.js.map} +1 -1
- package/bizar-dash/dist/assets/{git-pull-request-BjG5lNtR.js → git-pull-request-D1qNDRia.js} +1 -1
- package/bizar-dash/dist/assets/{git-pull-request-BjG5lNtR.js.map → git-pull-request-D1qNDRia.js.map} +1 -1
- package/bizar-dash/dist/assets/{main-CokHAXWM.js → main-Cxeu4LhG.js} +28 -33
- package/bizar-dash/dist/assets/main-Cxeu4LhG.js.map +1 -0
- package/bizar-dash/dist/assets/network-DJdvVaHx.js +6 -0
- package/bizar-dash/dist/assets/network-DJdvVaHx.js.map +1 -0
- package/bizar-dash/dist/assets/{pause-CAmulU0F.js → pause-DUn9L5PB.js} +1 -1
- package/bizar-dash/dist/assets/{pause-CAmulU0F.js.map → pause-DUn9L5PB.js.map} +1 -1
- package/bizar-dash/dist/assets/{pencil-eq5IqBmU.js → pencil-DVrBlYV3.js} +1 -1
- package/bizar-dash/dist/assets/{pencil-eq5IqBmU.js.map → pencil-DVrBlYV3.js.map} +1 -1
- package/bizar-dash/dist/assets/{play-BoLrrcBA.js → play-CZjsr3XM.js} +1 -1
- package/bizar-dash/dist/assets/{play-BoLrrcBA.js.map → play-CZjsr3XM.js.map} +1 -1
- package/bizar-dash/dist/assets/{plug-DBrVM9lK.js → plug-BKuyIe5W.js} +1 -1
- package/bizar-dash/dist/assets/{plug-DBrVM9lK.js.map → plug-BKuyIe5W.js.map} +1 -1
- package/bizar-dash/dist/assets/{plus-DENm0hqV.js → plus-77h62zZm.js} +1 -1
- package/bizar-dash/dist/assets/{plus-DENm0hqV.js.map → plus-77h62zZm.js.map} +1 -1
- package/bizar-dash/dist/assets/{power-DLjXF7zE.js → power-DLmx-gCP.js} +1 -1
- package/bizar-dash/dist/assets/{power-DLjXF7zE.js.map → power-DLmx-gCP.js.map} +1 -1
- package/bizar-dash/dist/assets/{power-off-D47cOnG_.js → power-off-BFKzHXic.js} +1 -1
- package/bizar-dash/dist/assets/{power-off-D47cOnG_.js.map → power-off-BFKzHXic.js.map} +1 -1
- package/bizar-dash/dist/assets/{refresh-ccw-DNrXX9gB.js → refresh-ccw-DenGBOcq.js} +1 -1
- package/bizar-dash/dist/assets/{refresh-ccw-DNrXX9gB.js.map → refresh-ccw-DenGBOcq.js.map} +1 -1
- package/bizar-dash/dist/assets/{refresh-cw-CoAHDpeS.js → refresh-cw-ENBVR8hW.js} +1 -1
- package/bizar-dash/dist/assets/{refresh-cw-CoAHDpeS.js.map → refresh-cw-ENBVR8hW.js.map} +1 -1
- package/bizar-dash/dist/assets/{rotate-ccw-ObX5awaG.js → rotate-ccw-MxryaY7q.js} +1 -1
- package/bizar-dash/dist/assets/{rotate-ccw-ObX5awaG.js.map → rotate-ccw-MxryaY7q.js.map} +1 -1
- package/bizar-dash/dist/assets/{rotate-cw-xpPJw9LP.js → rotate-cw-CDOlRtUR.js} +1 -1
- package/bizar-dash/dist/assets/{rotate-cw-xpPJw9LP.js.map → rotate-cw-CDOlRtUR.js.map} +1 -1
- package/bizar-dash/dist/assets/{save-D1iKiJJl.js → save-Db1b-1Uq.js} +1 -1
- package/bizar-dash/dist/assets/{save-D1iKiJJl.js.map → save-Db1b-1Uq.js.map} +1 -1
- package/bizar-dash/dist/assets/{send-BRcAhZUm.js → send-yz6HFZM5.js} +1 -1
- package/bizar-dash/dist/assets/{send-BRcAhZUm.js.map → send-yz6HFZM5.js.map} +1 -1
- package/bizar-dash/dist/assets/{shield-alert-C6vEcK-g.js → shield-alert-DY6RAaC8.js} +1 -1
- package/bizar-dash/dist/assets/{shield-alert-C6vEcK-g.js.map → shield-alert-DY6RAaC8.js.map} +1 -1
- package/bizar-dash/dist/assets/{trash-2-DZDjEnNh.js → trash-2-Cv7U38jw.js} +1 -1
- package/bizar-dash/dist/assets/{trash-2-DZDjEnNh.js.map → trash-2-Cv7U38jw.js.map} +1 -1
- package/bizar-dash/dist/assets/{trending-up-DMj-AJVf.js → trending-up-CRPNagyX.js} +1 -1
- package/bizar-dash/dist/assets/{trending-up-DMj-AJVf.js.map → trending-up-CRPNagyX.js.map} +1 -1
- package/bizar-dash/dist/assets/{triangle-alert-_2xe_tl4.js → triangle-alert-tUTXbcEP.js} +1 -1
- package/bizar-dash/dist/assets/{triangle-alert-_2xe_tl4.js.map → triangle-alert-tUTXbcEP.js.map} +1 -1
- package/bizar-dash/dist/assets/{upload-DdDljAeV.js → upload-Cq7uTsrT.js} +1 -1
- package/bizar-dash/dist/assets/{upload-DdDljAeV.js.map → upload-Cq7uTsrT.js.map} +1 -1
- package/bizar-dash/dist/assets/{zap-mHsgh0FP.js → zap-5lAQBAPX.js} +1 -1
- package/bizar-dash/dist/assets/{zap-mHsgh0FP.js.map → zap-5lAQBAPX.js.map} +1 -1
- package/bizar-dash/dist/index.html +1 -1
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/api.mjs +5 -1
- package/bizar-dash/src/server/routes/update.mjs +48 -21
- package/bizar-dash/src/web/v8/App.tsx +0 -4
- package/bizar-dash/src/web/v8/__tests__/new-views.test.tsx +0 -17
- package/bizar-dash/src/web/v8/shell/Sidebar.tsx +3 -4
- package/bizar-dash/src/web/v8/views/Router.tsx +0 -5
- package/bizar-dash/tests/_update-test-helpers.mjs +13 -0
- package/bizar-dash/tests/update-check.test.mjs +18 -13
- package/bizar-dash/tests/update-packages.test.mjs +154 -0
- package/bizar-dash/tests/update-run.test.mjs +17 -80
- package/cli/banner.mjs +1 -1
- package/cli/bin.mjs +8 -1
- package/cli/bootstrap.mjs +1 -1
- package/cli/commands/claude-cmd.mjs +1 -1
- package/cli/commands/deploy/docker.mjs +0 -3
- package/cli/doctor.mjs +1 -1
- package/cli/install/banner.mjs +67 -0
- package/cli/install/banner.test.mjs +39 -0
- package/cli/install/index.mjs +32 -0
- package/cli/install/index.test.mjs +61 -0
- package/cli/install/paths.mjs +80 -0
- package/cli/install/paths.test.mjs +77 -0
- package/cli/install/plugin.mjs +161 -0
- package/cli/install/postinstall.mjs +111 -0
- package/cli/install.mjs +13 -600
- package/cli/migrate.mjs +157 -0
- package/cli/migrate.test.mjs +85 -0
- package/cli/provision.mjs +615 -1918
- package/cli/provision.test.mjs +1 -1
- package/cli/service-controller.test.mjs +0 -1
- package/cli/service-env.mjs +0 -1
- package/cli/utils.mjs +0 -1
- package/install.sh +54 -466
- package/package.json +1 -1
- package/packages/sdk/dist/mcp/server.js +41 -409
- package/packages/sdk/dist/router/q-learning-router.d.ts +3 -5
- package/packages/sdk/dist/router/q-learning-router.js +9 -5
- package/packages/sdk/package.json +1 -1
- package/scripts/install-curl.sh +17 -0
- package/bizar-dash/dist/assets/HeadroomView-D1UiTYbt.js +0 -11
- package/bizar-dash/dist/assets/HeadroomView-D1UiTYbt.js.map +0 -1
- package/bizar-dash/dist/assets/MiscView-B8QY82Pm.js +0 -1
- package/bizar-dash/dist/assets/ObsidianView-BCTejEj2.js +0 -1
- package/bizar-dash/dist/assets/main-CokHAXWM.js.map +0 -1
- package/bizar-dash/skills/headroom/SKILL.md +0 -94
- package/bizar-dash/src/server/headroom.mjs +0 -649
- package/bizar-dash/src/server/routes/headroom.mjs +0 -126
- package/bizar-dash/src/web/v8/views/Headroom/HeadroomView.tsx +0 -201
- package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +0 -90
- package/bizar-dash/tests/headroom-install.test.mjs +0 -173
- package/bizar-dash/tests/headroom-settings.test.mjs +0 -126
- package/bizar-dash/tests/headroom-status.test.mjs +0 -117
- package/bizar-dash/tests/v7-archive/mobile-overview.test.tsx +0 -214
- package/bizar-dash/tests/v7-archive/mobile-settings.test.tsx +0 -181
- package/bizar-dash/tests/v7-archive/navigation/CommandPalette.test.tsx +0 -105
- package/bizar-dash/tests/v7-archive/settings-nav.test.tsx +0 -128
- package/bizar-dash/tests/v7-archive/views/Overview.test.tsx +0 -413
- package/cli/commands/headroom.mjs +0 -204
- package/cli/provision-claude.mjs +0 -1176
package/cli/provision.mjs
CHANGED
|
@@ -1,33 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
/**
|
|
2
3
|
* cli/provision.mjs
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
+
* v10.0.0 — Unified installer + updater (Claude Code-native).
|
|
5
6
|
*
|
|
6
|
-
* `bizar install` and `bizar update`
|
|
7
|
-
*
|
|
8
|
-
* with massive overlap: both copied agents, both patched settings.json,
|
|
9
|
-
* both ran the plugin copy, both kicked off the service, both called
|
|
10
|
-
* `bizar doctor` at the end. The two paths diverged over time.
|
|
11
|
-
*
|
|
12
|
-
* This module is the single source of truth. Both `bizar install` and
|
|
13
|
-
* `bizar update` call `runProvision({ mode, ... })`. The differences
|
|
14
|
-
* between modes are explicit and small:
|
|
15
|
-
*
|
|
16
|
-
* install: bootstrap a fresh install. Detects what exists, installs
|
|
17
|
-
* everything that's missing, configures the service, runs
|
|
18
|
-
* doctor. Does NOT kill running instances (fresh install
|
|
19
|
-
* has none).
|
|
20
|
-
* update: refresh an existing install. Kills running instances,
|
|
21
|
-
* upgrades @polderlabs/bizar + @anthropic-ai/claude-code via
|
|
22
|
-
* npm, re-copies agent files / skills / commands, re-patches
|
|
23
|
-
* ~/.claude/settings.json (idempotent), restarts the
|
|
24
|
-
* dashboard, runs doctor.
|
|
25
|
-
*
|
|
26
|
-
* Both modes are safe to re-run — every step is idempotent and skips
|
|
27
|
-
* work that's already done.
|
|
7
|
+
* `bizar install` and `bizar update` are the same code path with
|
|
8
|
+
* different `mode` flags. Both are idempotent and safe to re-run.
|
|
28
9
|
*
|
|
29
10
|
* Public API:
|
|
30
|
-
* runProvision({ mode, dryRun, force, restart, ...
|
|
11
|
+
* runProvision({ mode, dryRun, force, yes, restart, ... })
|
|
31
12
|
* Runs the full provision flow for `mode` ('install' | 'update').
|
|
32
13
|
* detectState()
|
|
33
14
|
* Probe what's installed without modifying anything.
|
|
@@ -35,86 +16,68 @@
|
|
|
35
16
|
*/
|
|
36
17
|
|
|
37
18
|
import chalk from 'chalk';
|
|
38
|
-
import { execSync, spawn, spawnSync } from 'node:child_process';
|
|
39
|
-
import {
|
|
19
|
+
import { execFileSync, execSync, spawn, spawnSync } from 'node:child_process';
|
|
20
|
+
import {
|
|
21
|
+
chmodSync,
|
|
22
|
+
copyFileSync,
|
|
23
|
+
existsSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
readdirSync,
|
|
26
|
+
readFileSync,
|
|
27
|
+
rmSync,
|
|
28
|
+
statSync,
|
|
29
|
+
writeFileSync,
|
|
30
|
+
} from 'node:fs';
|
|
40
31
|
import { homedir } from 'node:os';
|
|
41
|
-
import { join,
|
|
32
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
42
33
|
import { fileURLToPath } from 'node:url';
|
|
43
|
-
import { bizarConfigDir } from './utils.mjs';
|
|
44
34
|
|
|
45
35
|
const __filename = fileURLToPath(import.meta.url);
|
|
46
36
|
const __dirname = dirname(__filename);
|
|
47
37
|
const HOME = homedir();
|
|
48
38
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
// one level up. This works both in source checkouts AND in global npm
|
|
52
|
-
// installs (`<npm root -g>/@polderlabs/bizar/cli/provision.mjs`).
|
|
53
|
-
export const REPO_ROOT = join(__dirname, '..');
|
|
39
|
+
// Repo root = `<pkg>/cli/provision.mjs` → one level up.
|
|
40
|
+
export const REPO_ROOT = resolve(__dirname, '..');
|
|
54
41
|
export const PKG_MAIN = '@polderlabs/bizar';
|
|
55
42
|
|
|
56
|
-
|
|
43
|
+
// ─── Paths ────────────────────────────────────────────────────────────────────
|
|
57
44
|
|
|
58
45
|
/**
|
|
59
46
|
* Resolve the Claude Code config directory.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* global config dir for `agents/`, `skills/`, `plugins/`, `commands/`,
|
|
63
|
-
* `hooks/`, and `settings.json`.
|
|
64
|
-
*
|
|
65
|
-
* Resolution order (matches Claude Code's configuration discovery):
|
|
66
|
-
* 1. `process.env.CLAUDE_CONFIG_DIR` (explicit override)
|
|
67
|
-
* 2. `$HOME/.claude` (the Claude Code default)
|
|
68
|
-
* 3. (Windows only) `%USERPROFILE%/.claude`
|
|
69
|
-
*
|
|
70
|
-
* `BIZAR_LEGACY_CLAUDE_DIR=1` opts back into the old Cline layout
|
|
71
|
-
* (`~/.config/cline/`) for users who deliberately want the legacy path.
|
|
72
|
-
* Removed in v6.3.0 — Claude Code is the only supported backend.
|
|
47
|
+
* 1. `process.env.CLAUDE_CONFIG_DIR`
|
|
48
|
+
* 2. `$HOME/.claude`
|
|
73
49
|
*/
|
|
74
|
-
function resolveClaudeDir() {
|
|
75
|
-
if (process.env.BIZAR_LEGACY_CLAUDE_DIR === '1') {
|
|
76
|
-
return join(
|
|
77
|
-
process.env.XDG_CONFIG_HOME || join(HOME, '.config'),
|
|
78
|
-
'cline',
|
|
79
|
-
);
|
|
80
|
-
}
|
|
50
|
+
export function resolveClaudeDir() {
|
|
81
51
|
if (process.env.CLAUDE_CONFIG_DIR && process.env.CLAUDE_CONFIG_DIR.trim()) {
|
|
82
52
|
return process.env.CLAUDE_CONFIG_DIR.trim();
|
|
83
53
|
}
|
|
84
54
|
if (process.platform === 'win32') {
|
|
85
|
-
return
|
|
55
|
+
return process.env.APPDATA
|
|
56
|
+
? join(process.env.APPDATA, 'Claude')
|
|
57
|
+
: join(HOME, '.claude');
|
|
86
58
|
}
|
|
87
59
|
return join(HOME, '.claude');
|
|
88
60
|
}
|
|
89
61
|
|
|
90
|
-
/**
|
|
91
|
-
* Compatibility export — many existing checks still reference this name.
|
|
92
|
-
* @deprecated Use `CLAUDE_DIR` instead.
|
|
93
|
-
*/
|
|
94
62
|
export const CLAUDE_DIR = resolveClaudeDir();
|
|
95
63
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
* @deprecated Use `CLAUDE_DIR` instead.
|
|
100
|
-
*/
|
|
101
|
-
export const CLINE_DIR = resolveClaudeDir();
|
|
64
|
+
/** `~/.config/bizar/` — Bizar runtime state (NOT under ~/.claude/). */
|
|
65
|
+
export const BIZAR_HOME = process.env.BIZAR_HOME
|
|
66
|
+
|| join(process.env.XDG_CONFIG_HOME || join(HOME, '.config'), 'bizar');
|
|
102
67
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
export const
|
|
110
|
-
process.env.XDG_CONFIG_HOME || join(HOME, '.config'),
|
|
111
|
-
'cline',
|
|
112
|
-
);
|
|
68
|
+
// Standard Claude Code subdirectories
|
|
69
|
+
export const CLAUDE_AGENTS_DIR = join(CLAUDE_DIR, 'agents');
|
|
70
|
+
export const CLAUDE_SKILLS_DIR = join(CLAUDE_DIR, 'skills');
|
|
71
|
+
export const CLAUDE_COMMANDS_DIR = join(CLAUDE_DIR, 'commands');
|
|
72
|
+
export const CLAUDE_HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
|
|
73
|
+
export const CLAUDE_RULES_DIR = join(CLAUDE_DIR, 'rules');
|
|
74
|
+
export const CLAUDE_PLUGINS_DIR = join(CLAUDE_DIR, 'plugins');
|
|
113
75
|
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const
|
|
76
|
+
// PID / port files
|
|
77
|
+
const SERVICE_PID_FILE = join(BIZAR_HOME, 'service.pid');
|
|
78
|
+
const DASHBOARD_PID_FILE = join(BIZAR_HOME, 'dashboard.pid');
|
|
79
|
+
const DASHBOARD_PORT_FILE = join(BIZAR_HOME, 'dashboard.port');
|
|
80
|
+
const INSTALL_MARKER_FILE = join(BIZAR_HOME, 'installed.json');
|
|
118
81
|
|
|
119
82
|
// ─── Tiny utilities ──────────────────────────────────────────────────────────
|
|
120
83
|
|
|
@@ -145,6 +108,25 @@ function readJsonSafe(file, fallback = null) {
|
|
|
145
108
|
}
|
|
146
109
|
}
|
|
147
110
|
|
|
111
|
+
function ensureDir(dir) {
|
|
112
|
+
mkdirSync(dir, { recursive: true });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function copyDirContents(src, dest) {
|
|
116
|
+
ensureDir(dest);
|
|
117
|
+
for (const name of readdirSync(src)) {
|
|
118
|
+
const sp = join(src, name);
|
|
119
|
+
const dp = join(dest, name);
|
|
120
|
+
const st = statSync(sp);
|
|
121
|
+
if (st.isDirectory()) copyDirContents(sp, dp);
|
|
122
|
+
else copyFileSync(sp, dp);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function rimrafSafe(p) {
|
|
127
|
+
try { rmSync(p, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
128
|
+
}
|
|
129
|
+
|
|
148
130
|
/** Read a PID file and return the live PID, or null if missing/stale. */
|
|
149
131
|
export function readLivePid(pidFile) {
|
|
150
132
|
if (!existsSync(pidFile)) return null;
|
|
@@ -208,19 +190,22 @@ export async function killAndWait(pid, { timeoutMs = 5000, label = 'process' } =
|
|
|
208
190
|
}
|
|
209
191
|
}
|
|
210
192
|
|
|
193
|
+
// ─── Logging helpers ─────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
function logOk(msg) { console.log(chalk.green(' ✓') + ' ' + msg); }
|
|
196
|
+
function logInfo(msg) { console.log(chalk.cyan(' →') + ' ' + msg); }
|
|
197
|
+
function logWarn(msg) { console.log(chalk.yellow(' ⚠') + ' ' + msg); }
|
|
198
|
+
function logErr(msg) { console.log(chalk.red(' ✗') + ' ' + msg); }
|
|
199
|
+
function section(title) { console.log('\n' + chalk.bold.cyan(`── ${title} ──`)); }
|
|
200
|
+
|
|
211
201
|
// ─── Install marker ──────────────────────────────────────────────────────────
|
|
212
202
|
|
|
213
|
-
/**
|
|
214
|
-
* Read the install marker file. Returns null if missing.
|
|
215
|
-
* Marker shape: { version, installedAt, repoPath, serviceUnit }
|
|
216
|
-
*/
|
|
203
|
+
/** Read the install marker file. Returns null if missing. */
|
|
217
204
|
export function readInstallMarker() {
|
|
218
205
|
return readJsonSafe(INSTALL_MARKER_FILE, null);
|
|
219
206
|
}
|
|
220
207
|
|
|
221
|
-
/**
|
|
222
|
-
* Write the install marker file.
|
|
223
|
-
*/
|
|
208
|
+
/** Write the install marker file. */
|
|
224
209
|
export function writeInstallMarker({ version, repoPath, serviceUnit }) {
|
|
225
210
|
const marker = {
|
|
226
211
|
version: version || currentVersion(PKG_MAIN) || 'unknown',
|
|
@@ -237,304 +222,7 @@ export function writeInstallMarker({ version, repoPath, serviceUnit }) {
|
|
|
237
222
|
}
|
|
238
223
|
}
|
|
239
224
|
|
|
240
|
-
|
|
241
|
-
* v6.2.5 - Register every `config/skills/<name>/SKILL.md` in
|
|
242
|
-
* `${agentsDir}/.skill-lock.json` so Cline's marketplace UI
|
|
243
|
-
* (`isMarketplaceSkillInstalled`) actually shows them as installed.
|
|
244
|
-
*
|
|
245
|
-
* Cline reads `~/.agents/.skill-lock.json` as its source of truth for
|
|
246
|
-
* skill installation state. The Bizar harness mirrors SKILL.md files
|
|
247
|
-
* to both `~/.cline/skills/` and `~/.agents/skills/`, but Cline still
|
|
248
|
-
* shows "No skills installed" in the Skills tab until each entry is
|
|
249
|
-
* registered here. Verified via `npx skills list -g` (writes the entry
|
|
250
|
-
* then the skill appears).
|
|
251
|
-
*
|
|
252
|
-
* Idempotent: entries already tagged `source: 'bizar/builtin'` only
|
|
253
|
-
* get their `updatedAt` refreshed; user-installed skills (any other
|
|
254
|
-
* source) are never touched.
|
|
255
|
-
*
|
|
256
|
-
* @param {object} opts
|
|
257
|
-
* @param {string} opts.skillsSrc - directory containing config/skills/<name>
|
|
258
|
-
* @param {string} opts.agentsDir - typically HOME/.agents
|
|
259
|
-
* @returns {{ok: boolean, count: number, path: string}}
|
|
260
|
-
*/
|
|
261
|
-
export function writeBizarSkillLock({ skillsSrc, agentsDir }) {
|
|
262
|
-
if (!existsSync(skillsSrc)) {
|
|
263
|
-
return { ok: true, count: 0, path: join(agentsDir, '.skill-lock.json') };
|
|
264
|
-
}
|
|
265
|
-
const lockPath = join(agentsDir, '.skill-lock.json');
|
|
266
|
-
let lock = { version: 3, skills: {} };
|
|
267
|
-
try {
|
|
268
|
-
const raw = readFileSync(lockPath, 'utf8');
|
|
269
|
-
lock = JSON.parse(raw);
|
|
270
|
-
if (!lock.skills || typeof lock.skills !== 'object') lock.skills = {};
|
|
271
|
-
if (typeof lock.version !== 'number') lock.version = 3;
|
|
272
|
-
} catch {
|
|
273
|
-
// first install - start fresh
|
|
274
|
-
}
|
|
275
|
-
const now = new Date().toISOString();
|
|
276
|
-
let count = 0;
|
|
277
|
-
for (const skillDir of readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
278
|
-
if (!skillDir.isDirectory()) continue;
|
|
279
|
-
const name = skillDir.name;
|
|
280
|
-
const existing = lock.skills[name];
|
|
281
|
-
if (existing && existing.source === 'bizar/builtin') {
|
|
282
|
-
existing.updatedAt = now;
|
|
283
|
-
count += 1;
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
lock.skills[name] = {
|
|
287
|
-
source: 'bizar/builtin',
|
|
288
|
-
sourceType: 'local',
|
|
289
|
-
sourceUrl: 'https://github.com/DrB0rk/BizarHarness',
|
|
290
|
-
skillPath: 'config/skills/' + name + '/SKILL.md',
|
|
291
|
-
skillFolderHash: 'bizar-managed',
|
|
292
|
-
pluginName: 'bizar',
|
|
293
|
-
installedAt: existing && existing.installedAt ? existing.installedAt : now,
|
|
294
|
-
updatedAt: now,
|
|
295
|
-
};
|
|
296
|
-
count += 1;
|
|
297
|
-
}
|
|
298
|
-
try {
|
|
299
|
-
mkdirSync(agentsDir, { recursive: true });
|
|
300
|
-
writeFileSync(lockPath, JSON.stringify(lock, null, 2));
|
|
301
|
-
return { ok: true, count, path: lockPath };
|
|
302
|
-
} catch (err) {
|
|
303
|
-
return { ok: false, count: 0, path: lockPath, error: err.message };
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// ─── State detection ──────────────────────────────────────────────────────────
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Probe the current state without modifying anything. Both install and
|
|
311
|
-
* update flows start here so they can decide what to skip.
|
|
312
|
-
*
|
|
313
|
-
* Returned shape:
|
|
314
|
-
* {
|
|
315
|
-
* pkgRoot: string, // <npm root -g>/@polderlabs/bizar
|
|
316
|
-
* pkgVersion: string|null, // installed @polderlabs/bizar version
|
|
317
|
-
* pkgLatest: string|null, // latest @polderlabs/bizar version on npm
|
|
318
|
-
* plugin: { sourceDir, destDir, installed, upToDate, symlink },
|
|
319
|
-
* clineJson: { path, hasPluginEntry, exists },
|
|
320
|
-
* service: { installed, running, unitPath },
|
|
321
|
-
* dashboard: { running, pid, port },
|
|
322
|
-
* claudeCli: { version, latest },
|
|
323
|
-
* headsUpState: { ok, blockerCount, warningCount },
|
|
324
|
-
* gitRepo: boolean, // are we running from a git checkout?
|
|
325
|
-
* }
|
|
326
|
-
*/
|
|
327
|
-
export function detectState({ cwd = process.cwd() } = {}) {
|
|
328
|
-
// ── npm-global package location ─────────────────────────────────────
|
|
329
|
-
// Try multiple strategies because `npm root -g` doesn't always
|
|
330
|
-
// respect NPM_CONFIG_PREFIX (notably when npm is installed by the
|
|
331
|
-
// system package manager on Debian/Ubuntu).
|
|
332
|
-
let globalRoot = null;
|
|
333
|
-
const npmRootCandidates = [];
|
|
334
|
-
|
|
335
|
-
// 1. Honour the user's explicit env override first.
|
|
336
|
-
if (process.env.NPM_CONFIG_PREFIX) {
|
|
337
|
-
npmRootCandidates.push(join(process.env.NPM_CONFIG_PREFIX, 'lib', 'node_modules'));
|
|
338
|
-
}
|
|
339
|
-
// 2. Honour NPM_CONFIG_GLOBAL_PREFIX (set by `npm -g`).
|
|
340
|
-
if (process.env.NPM_CONFIG_GLOBAL_PREFIX) {
|
|
341
|
-
npmRootCandidates.push(join(process.env.NPM_CONFIG_GLOBAL_PREFIX, 'lib', 'node_modules'));
|
|
342
|
-
}
|
|
343
|
-
// 3. The classic `npm root -g` query (works on most distros).
|
|
344
|
-
try {
|
|
345
|
-
const r = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
346
|
-
.toString().trim();
|
|
347
|
-
if (r) npmRootCandidates.push(r);
|
|
348
|
-
} catch { /* ignore */ }
|
|
349
|
-
// 4. Common distro defaults.
|
|
350
|
-
npmRootCandidates.push('/usr/local/lib/node_modules');
|
|
351
|
-
npmRootCandidates.push('/usr/lib/node_modules');
|
|
352
|
-
|
|
353
|
-
// Pick the first candidate that actually has @polderlabs/bizar.
|
|
354
|
-
for (const candidate of npmRootCandidates) {
|
|
355
|
-
if (existsSync(join(candidate, '@polderlabs', 'bizar'))) {
|
|
356
|
-
globalRoot = candidate;
|
|
357
|
-
break;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
// Fall back to the first candidate (even if it doesn't have the
|
|
361
|
-
// package) so downstream code can produce a clean error message.
|
|
362
|
-
if (!globalRoot) globalRoot = npmRootCandidates[0] ?? null;
|
|
363
|
-
|
|
364
|
-
const pkgRoot = globalRoot ? join(globalRoot, '@polderlabs', 'bizar') : null;
|
|
365
|
-
const pkgVersion = globalRoot ? currentVersion(PKG_MAIN) : null;
|
|
366
|
-
const pkgLatest = latestVersion(PKG_MAIN);
|
|
367
|
-
|
|
368
|
-
// ── Plugin copy (deployed to ${CLAUDE_DIR}/plugins/bizar) ──
|
|
369
|
-
const pluginSourceDir = pkgRoot ? join(pkgRoot, 'plugins', 'bizar') : null;
|
|
370
|
-
const pluginDestDir = join(CLAUDE_DIR, 'plugins', 'bizar');
|
|
371
|
-
let pluginInstalled = false;
|
|
372
|
-
let pluginUpToDate = false;
|
|
373
|
-
let pluginSymlink = false;
|
|
374
|
-
try {
|
|
375
|
-
const st = statSync(pluginDestDir);
|
|
376
|
-
pluginSymlink = st.isSymbolicLink();
|
|
377
|
-
pluginInstalled = true;
|
|
378
|
-
if (pluginSourceDir && existsSync(pluginSourceDir)) {
|
|
379
|
-
// Cheap freshness check: compare source vs dest mtime. The plugin
|
|
380
|
-
// source ships its bundled node_modules + compiled JS; if dest's
|
|
381
|
-
// mtime is older than source's, treat as out of date.
|
|
382
|
-
try {
|
|
383
|
-
const srcSt = statSync(pluginSourceDir);
|
|
384
|
-
const dstSt = statSync(pluginDestDir);
|
|
385
|
-
pluginUpToDate = srcSt.mtimeMs <= dstSt.mtimeMs;
|
|
386
|
-
} catch {
|
|
387
|
-
pluginUpToDate = false;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
} catch {
|
|
391
|
-
pluginInstalled = false;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// ── cline.json plugin entry ───────────────────────────────────────
|
|
395
|
-
// v6.3.0 — Read Claude Code settings.json (replaces cline.json).
|
|
396
|
-
const claudeSettingsPath = join(CLAUDE_DIR, 'settings.json');
|
|
397
|
-
const claudeSettings = readJsonSafe(claudeSettingsPath, null);
|
|
398
|
-
let hasMcpServerEntry = false;
|
|
399
|
-
if (claudeSettings && claudeSettings.mcpServers && typeof claudeSettings.mcpServers === 'object') {
|
|
400
|
-
hasMcpServerEntry = !!claudeSettings.mcpServers.bizar;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// ── Background service ─────────────────────────────────────────────
|
|
404
|
-
let serviceInstalled = false;
|
|
405
|
-
let serviceUnitPath = null;
|
|
406
|
-
if (pkgRoot) {
|
|
407
|
-
try {
|
|
408
|
-
const sc = require_safe(join(pkgRoot, 'cli', 'service-controller.mjs'));
|
|
409
|
-
serviceInstalled = sc?.isInstalled?.() ?? false;
|
|
410
|
-
serviceUnitPath = sc?.serviceUnitPath?.() ?? null;
|
|
411
|
-
} catch { /* ignore */ }
|
|
412
|
-
}
|
|
413
|
-
const servicePid = readLivePid(SERVICE_PID_FILE);
|
|
414
|
-
const serviceRunning = servicePid !== null;
|
|
415
|
-
|
|
416
|
-
// ── Dashboard process ──────────────────────────────────────────────
|
|
417
|
-
const dashboardPid = readLivePid(DASHBOARD_PID_FILE);
|
|
418
|
-
const dashboardPort = parseInt(readTextSafe(DASHBOARD_PORT_FILE, '').trim(), 10) || null;
|
|
419
|
-
|
|
420
|
-
// ── Claude Code CLI version ────────────────────────────────────────
|
|
421
|
-
const claudeCli = {
|
|
422
|
-
version: currentVersion('@anthropic-ai/claude-code'),
|
|
423
|
-
latest: latestVersion('@anthropic-ai/claude-code'),
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
// ── Heads-up gate (.bizar/PRE_PUSH_NOTES.md) ───────────────────────
|
|
427
|
-
let headsUpState = { ok: true, blockerCount: 0, warningCount: 0 };
|
|
428
|
-
try {
|
|
429
|
-
const { checkHeadsUps, findBizarDir } = require_safe('./heads-up.mjs');
|
|
430
|
-
const bizarDir = findBizarDir(cwd);
|
|
431
|
-
if (bizarDir && checkHeadsUps) {
|
|
432
|
-
headsUpState = checkHeadsUps(bizarDir);
|
|
433
|
-
}
|
|
434
|
-
} catch { /* ignore */ }
|
|
435
|
-
|
|
436
|
-
// ── Git checkout? ─────────────────────────────────────────────────
|
|
437
|
-
const gitRepo = existsSync(join(REPO_ROOT, '.git'));
|
|
438
|
-
|
|
439
|
-
// ── Installed mods ─────────────────────────────────────────────────
|
|
440
|
-
// v4.4.11 — `bizar install` and `bizar update` never install or
|
|
441
|
-
// upgrade mods. The list below is informational only. Use
|
|
442
|
-
// `bizar mod install <id>` to add a mod explicitly.
|
|
443
|
-
const MODS_DIR = join(BIZAR_HOME, 'mods');
|
|
444
|
-
const installedMods = listInstalledMods(MODS_DIR);
|
|
445
|
-
|
|
446
|
-
return {
|
|
447
|
-
pkgRoot,
|
|
448
|
-
pkgVersion,
|
|
449
|
-
pkgLatest,
|
|
450
|
-
plugin: {
|
|
451
|
-
sourceDir: pluginSourceDir,
|
|
452
|
-
destDir: pluginDestDir,
|
|
453
|
-
installed: pluginInstalled,
|
|
454
|
-
upToDate: pluginUpToDate,
|
|
455
|
-
symlink: pluginSymlink,
|
|
456
|
-
},
|
|
457
|
-
claudeSettings: {
|
|
458
|
-
path: claudeSettingsPath,
|
|
459
|
-
exists: !!claudeSettings,
|
|
460
|
-
hasMcpServerEntry,
|
|
461
|
-
},
|
|
462
|
-
service: {
|
|
463
|
-
installed: serviceInstalled,
|
|
464
|
-
running: serviceRunning,
|
|
465
|
-
pid: servicePid,
|
|
466
|
-
unitPath: serviceUnitPath,
|
|
467
|
-
},
|
|
468
|
-
dashboard: {
|
|
469
|
-
running: dashboardPid !== null,
|
|
470
|
-
pid: dashboardPid,
|
|
471
|
-
port: dashboardPort,
|
|
472
|
-
},
|
|
473
|
-
claudeCli,
|
|
474
|
-
headsUpState,
|
|
475
|
-
gitRepo,
|
|
476
|
-
installedMods,
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* v4.4.11 — Lightweight read-only scan of `~/.config/bizar/mods/`.
|
|
482
|
-
* Returns one entry per mod folder with the id + enabled flag parsed
|
|
483
|
-
* from mod.json. Never throws; mods with a missing or invalid mod.json
|
|
484
|
-
* are reported as `{id, error}` so the provisioner can surface them.
|
|
485
|
-
*/
|
|
486
|
-
function listInstalledMods(modsDir) {
|
|
487
|
-
const out = [];
|
|
488
|
-
if (!existsSync(modsDir)) return out;
|
|
489
|
-
let entries;
|
|
490
|
-
try {
|
|
491
|
-
entries = readdirSync(modsDir, { withFileTypes: true });
|
|
492
|
-
} catch {
|
|
493
|
-
return out;
|
|
494
|
-
}
|
|
495
|
-
for (const entry of entries) {
|
|
496
|
-
if (!entry.isDirectory()) continue;
|
|
497
|
-
const dir = join(modsDir, entry.name);
|
|
498
|
-
const manifestPath = join(dir, 'mod.json');
|
|
499
|
-
if (!existsSync(manifestPath)) {
|
|
500
|
-
out.push({ id: entry.name, error: 'missing mod.json' });
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
let manifest;
|
|
504
|
-
try {
|
|
505
|
-
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
506
|
-
} catch {
|
|
507
|
-
out.push({ id: entry.name, error: 'invalid mod.json' });
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
out.push({
|
|
511
|
-
id: manifest.id || entry.name,
|
|
512
|
-
name: manifest.name || entry.name,
|
|
513
|
-
version: manifest.version || '?',
|
|
514
|
-
enabled: manifest.enabled !== false,
|
|
515
|
-
installedAt: manifest.installedAt || null,
|
|
516
|
-
path: dir,
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
return out;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Lightweight CommonJS-ish require for ESM contexts. Used to detect the
|
|
524
|
-
* service-controller + heads-up modules without paying the static-import
|
|
525
|
-
* cost when those features aren't needed. Falls back gracefully if the
|
|
526
|
-
* import fails (e.g. during `bizar install` from a corrupted package).
|
|
527
|
-
*/
|
|
528
|
-
async function require_safe(spec) {
|
|
529
|
-
try {
|
|
530
|
-
const url = new URL(spec, `file://${__dirname}/`).href;
|
|
531
|
-
return await import(url);
|
|
532
|
-
} catch {
|
|
533
|
-
return null;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// ─── Version + npm helpers ───────────────────────────────────────────────────
|
|
225
|
+
// ─── Version helpers ─────────────────────────────────────────────────────────
|
|
538
226
|
|
|
539
227
|
export function currentVersion(pkg) {
|
|
540
228
|
try {
|
|
@@ -543,8 +231,7 @@ export function currentVersion(pkg) {
|
|
|
543
231
|
timeout: 15000,
|
|
544
232
|
}).toString();
|
|
545
233
|
const parsed = JSON.parse(out);
|
|
546
|
-
|
|
547
|
-
return deps[pkg]?.version ?? null;
|
|
234
|
+
return parsed.dependencies?.[pkg]?.version ?? null;
|
|
548
235
|
} catch {
|
|
549
236
|
return null;
|
|
550
237
|
}
|
|
@@ -562,675 +249,575 @@ export function latestVersion(pkg) {
|
|
|
562
249
|
}
|
|
563
250
|
}
|
|
564
251
|
|
|
565
|
-
|
|
566
|
-
* Print the installed-vs-latest version matrix in a stable column layout.
|
|
567
|
-
*/
|
|
568
|
-
export function printVersionMatrix(components) {
|
|
569
|
-
const colWidth = Math.max(8, ...components.map((c) => c.label.length));
|
|
570
|
-
for (const c of components) {
|
|
571
|
-
const cur = c.current ?? '(not installed)';
|
|
572
|
-
const lat = c.latest ?? '(unknown)';
|
|
573
|
-
const same = c.current && c.current === c.latest;
|
|
574
|
-
const marker = same ? chalk.green('✓ up to date') : chalk.yellow('⤵ update available');
|
|
575
|
-
console.log(` ${c.label.padEnd(colWidth)} ${cur.padEnd(15)} → ${lat} ${marker}`);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// ─── Step implementations ───────────────────────────────────────────────────
|
|
252
|
+
// ─── Build helpers ───────────────────────────────────────────────────────────
|
|
580
253
|
|
|
581
254
|
/**
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
* `mode`:
|
|
586
|
-
* 'install' — install if missing; never upgrade an existing install
|
|
587
|
-
* 'update' — install the latest version (upgrade or install)
|
|
255
|
+
* Build the SDK workspace so its compiled JS output lives at
|
|
256
|
+
* `<pkg>/packages/sdk/dist/`. The dashboard imports from `dist/memory/index.js`.
|
|
257
|
+
* Idempotent: skips when dist already exists. Non-fatal.
|
|
588
258
|
*/
|
|
589
|
-
export async function
|
|
590
|
-
const
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
if (mode === 'install' && current && !force) {
|
|
594
|
-
return { ok: true, message: `${pkg}@${current} already installed`, installed: current };
|
|
595
|
-
}
|
|
259
|
+
export async function buildSdk({ dryRun = false } = {}) {
|
|
260
|
+
const sdkRoot = join(REPO_ROOT, 'packages', 'sdk');
|
|
261
|
+
const distEntry = join(sdkRoot, 'dist', 'memory', 'index.js');
|
|
262
|
+
const srcEntry = join(sdkRoot, 'src', 'memory', 'index.ts');
|
|
596
263
|
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
}
|
|
264
|
+
if (!existsSync(srcEntry)) return { ok: true, skipped: true, message: `SDK src not found — skipping build` };
|
|
265
|
+
if (existsSync(distEntry)) return { ok: true, skipped: true, message: `SDK dist already present — skipping build` };
|
|
266
|
+
if (dryRun) return { ok: true, skipped: false, message: `would build SDK at ${sdkRoot}` };
|
|
600
267
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
268
|
+
const bunBin = join(process.env.HOME || '', '.bun', 'bin', 'bun');
|
|
269
|
+
let cmd, args, childEnv;
|
|
270
|
+
if (existsSync(bunBin)) {
|
|
271
|
+
cmd = bunBin; args = ['run', 'build:sdk'];
|
|
272
|
+
const pathDelim = process.platform === 'win32' ? ';' : ':';
|
|
273
|
+
let npmGlobalBin = '';
|
|
274
|
+
try {
|
|
275
|
+
const npmRootG = execSync('npm root -g', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
276
|
+
npmGlobalBin = join(dirname(npmRootG), '..', 'bin');
|
|
277
|
+
} catch {
|
|
278
|
+
npmGlobalBin = process.platform === 'win32' ? join(process.env.APPDATA || '', 'npm') : join(process.env.HOME || '', '.local', 'bin');
|
|
279
|
+
}
|
|
280
|
+
childEnv = { ...process.env, PATH: [dirname(bunBin), ...(npmGlobalBin && existsSync(npmGlobalBin) ? [npmGlobalBin] : []), process.env.PATH || ''].join(pathDelim) };
|
|
281
|
+
} else if (haveCmd('bun')) {
|
|
282
|
+
cmd = 'bun'; args = ['run', 'build:sdk']; childEnv = process.env;
|
|
283
|
+
} else {
|
|
284
|
+
cmd = 'npx'; args = ['--yes', 'tsc', '-p', 'packages/sdk/tsconfig.json']; childEnv = process.env;
|
|
607
285
|
}
|
|
608
286
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
const r = spawnSync('npm', npmArgs, { stdio: 'inherit', timeout: 600000 });
|
|
618
|
-
if (r.status === null && r.error?.code === 'ETIMEDOUT') {
|
|
619
|
-
return { ok: false, message: `${pkg} install timed out after 10 minutes`, installed: current };
|
|
620
|
-
}
|
|
621
|
-
if (r.status !== 0) {
|
|
622
|
-
return { ok: false, message: `${pkg} install failed`, installed: current };
|
|
287
|
+
try {
|
|
288
|
+
logInfo(`building SDK with \`${cmd} ${args.join(' ')}\``);
|
|
289
|
+
execFileSync(cmd, args, { cwd: REPO_ROOT, stdio: 'pipe', timeout: 180_000, env: childEnv });
|
|
290
|
+
if (existsSync(distEntry)) return { ok: true, skipped: false, message: 'SDK built (dist/memory/index.js present)' };
|
|
291
|
+
return { ok: false, message: `build completed but ${distEntry} still missing` };
|
|
292
|
+
} catch (err) {
|
|
293
|
+
const stderr = err.stderr ? err.stderr.toString().split('\n').slice(0, 5).join(' | ') : '(no stderr)';
|
|
294
|
+
return { ok: false, message: `SDK build failed: ${stderr}` };
|
|
623
295
|
}
|
|
624
|
-
return { ok: true, message: `${pkg} updated`, installed: latestVersion(pkg) };
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
export async function updateClineCli({ dryRun, force }) {
|
|
628
|
-
// v6.3.0 — Back-compat alias. The new code path upgrades the
|
|
629
|
-
// `@anthropic-ai/claude-code` npm package instead of `cline`.
|
|
630
|
-
return updateClaudeCodeCli({ dryRun, force });
|
|
631
296
|
}
|
|
632
297
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Build the Bizar plugin shim so `plugins/bizar/dist/index.js` exists.
|
|
300
|
+
* Idempotent. Non-fatal.
|
|
301
|
+
*/
|
|
302
|
+
export async function buildPlugin({ dryRun = false } = {}) {
|
|
303
|
+
const pluginRoot = join(REPO_ROOT, 'plugins', 'bizar');
|
|
304
|
+
const distEntry = join(pluginRoot, 'dist', 'index.js');
|
|
305
|
+
const srcEntry = join(pluginRoot, 'index.ts');
|
|
636
306
|
|
|
637
|
-
if (
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
message: `@anthropic-ai/claude-code@${current} up to date`,
|
|
641
|
-
installed: current,
|
|
642
|
-
};
|
|
643
|
-
}
|
|
307
|
+
if (!existsSync(srcEntry)) return { ok: true, skipped: true, message: `Plugin src not found — skipping build` };
|
|
308
|
+
if (existsSync(distEntry)) return { ok: true, skipped: true, message: `Plugin dist already present — skipping build` };
|
|
309
|
+
if (dryRun) return { ok: true, skipped: false, message: `would build plugin at ${pluginRoot}` };
|
|
644
310
|
|
|
645
|
-
|
|
646
|
-
|
|
311
|
+
const bunBin = join(process.env.HOME || '', '.bun', 'bin', 'bun');
|
|
312
|
+
let cmd, args, childEnv;
|
|
313
|
+
if (existsSync(bunBin)) {
|
|
314
|
+
cmd = bunBin; args = ['x', 'tsc', '-p', 'plugins/bizar/tsconfig.json'];
|
|
315
|
+
const pathDelim = process.platform === 'win32' ? ';' : ':';
|
|
316
|
+
let npmGlobalBin = '';
|
|
317
|
+
try {
|
|
318
|
+
const npmRootG = execSync('npm root -g', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
319
|
+
npmGlobalBin = join(dirname(npmRootG), '..', 'bin');
|
|
320
|
+
} catch {
|
|
321
|
+
npmGlobalBin = process.platform === 'win32' ? join(process.env.APPDATA || '', 'npm') : join(process.env.HOME || '', '.local', 'bin');
|
|
322
|
+
}
|
|
323
|
+
childEnv = { ...process.env, PATH: [dirname(bunBin), ...(npmGlobalBin && existsSync(npmGlobalBin) ? [npmGlobalBin] : []), process.env.PATH || ''].join(pathDelim) };
|
|
324
|
+
} else if (haveCmd('bun')) {
|
|
325
|
+
cmd = 'bun'; args = ['x', 'tsc', '-p', 'plugins/bizar/tsconfig.json']; childEnv = process.env;
|
|
326
|
+
} else {
|
|
327
|
+
cmd = 'npx'; args = ['--yes', 'tsc', '-p', 'plugins/bizar/tsconfig.json']; childEnv = process.env;
|
|
647
328
|
}
|
|
648
329
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
return {
|
|
658
|
-
ok: false,
|
|
659
|
-
message: '@anthropic-ai/claude-code install timed out after 10 minutes',
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
if (r.status !== 0) {
|
|
663
|
-
return { ok: false, message: '@anthropic-ai/claude-code install failed' };
|
|
330
|
+
try {
|
|
331
|
+
logInfo(`building plugin with \`${cmd} ${args.join(' ')}\``);
|
|
332
|
+
execFileSync(cmd, args, { cwd: REPO_ROOT, stdio: 'pipe', timeout: 180_000, env: childEnv });
|
|
333
|
+
if (existsSync(distEntry)) return { ok: true, skipped: false, message: 'Plugin built (dist/index.js present)' };
|
|
334
|
+
return { ok: false, message: `build completed but ${distEntry} still missing` };
|
|
335
|
+
} catch (err) {
|
|
336
|
+
const stderr = err.stderr ? err.stderr.toString().split('\n').slice(0, 5).join(' | ') : '(no stderr)';
|
|
337
|
+
return { ok: false, message: `Plugin build failed: ${stderr}` };
|
|
664
338
|
}
|
|
665
|
-
return {
|
|
666
|
-
ok: true,
|
|
667
|
-
message: '@anthropic-ai/claude-code updated',
|
|
668
|
-
installed: latestVersion('@anthropic-ai/claude-code'),
|
|
669
|
-
};
|
|
670
339
|
}
|
|
671
340
|
|
|
672
341
|
/**
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
* (set by `bizar dev-link`). Idempotent — safe to re-run.
|
|
676
|
-
*
|
|
677
|
-
* Bug history (v5.6.0-beta.12): the cp filter used to be
|
|
678
|
-
* `!p.includes('node_modules') && !p.includes('dist')`
|
|
679
|
-
* which always returned `false` for the SOURCE path itself because
|
|
680
|
-
* `pkgRoot = $(npm root -g)/@polderlabs/bizar` always contains
|
|
681
|
-
* `node_modules`. Net effect: 0 files were copied, the dest kept stale
|
|
682
|
-
* `index.ts` ("// fake plugin" leftovers from older installs), and the
|
|
683
|
-
* "up to date" mtime check then made subsequent installs skip the copy
|
|
684
|
-
* because `dest mtime >= src mtime`. Fixed by:
|
|
685
|
-
*
|
|
686
|
-
* 1. Filtering on RELATIVE path segments, not absolute substrings.
|
|
687
|
-
* 2. Adding content-hash comparison as a secondary freshness check.
|
|
688
|
-
* 3. Detecting the "fake plugin" stub and forcing a re-copy.
|
|
342
|
+
* Build the dashboard SPA so `bizar-dash/dist/index.html` exists.
|
|
343
|
+
* Idempotent. Non-fatal.
|
|
689
344
|
*/
|
|
690
|
-
export async function
|
|
691
|
-
const
|
|
692
|
-
const
|
|
693
|
-
const dest = state.plugin.destDir;
|
|
345
|
+
export async function buildDash({ dryRun = false } = {}) {
|
|
346
|
+
const dashDir = join(REPO_ROOT, 'bizar-dash');
|
|
347
|
+
const distIndex = join(dashDir, 'dist', 'index.html');
|
|
694
348
|
|
|
695
|
-
if (!src
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
message: `plugin source not found at ${src ?? '(unknown)'} — reinstall @polderlabs/bizar`,
|
|
699
|
-
};
|
|
700
|
-
}
|
|
349
|
+
if (!existsSync(join(dashDir, 'src', 'web', 'index.html'))) return { ok: true, skipped: true, message: `Dashboard src not found — skipping build` };
|
|
350
|
+
if (existsSync(distIndex)) return { ok: true, skipped: true, message: `Dashboard dist already present — skipping build` };
|
|
351
|
+
if (dryRun) return { ok: true, skipped: false, message: `would build dashboard at ${dashDir}` };
|
|
701
352
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
ok: false,
|
|
712
|
-
message:
|
|
713
|
-
`plugin source missing package.json at ${pkgJsonPath} — ` +
|
|
714
|
-
`rebuild @polderlabs/bizar`,
|
|
715
|
-
};
|
|
353
|
+
const bunBin = join(process.env.HOME || '', '.bun', 'bin', 'bun');
|
|
354
|
+
let cmd, args, childEnv;
|
|
355
|
+
if (existsSync(bunBin)) {
|
|
356
|
+
cmd = bunBin; args = ['run', 'build:dash'];
|
|
357
|
+
childEnv = { ...process.env, PATH: [dirname(bunBin), process.env.PATH || ''].join(process.platform === 'win32' ? ';' : ':') };
|
|
358
|
+
} else if (haveCmd('bun')) {
|
|
359
|
+
cmd = 'bun'; args = ['run', 'build:dash']; childEnv = process.env;
|
|
360
|
+
} else {
|
|
361
|
+
cmd = 'npx'; args = ['--yes', 'vite', 'build']; childEnv = process.env;
|
|
716
362
|
}
|
|
363
|
+
|
|
717
364
|
try {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
message: `plugin package.json at ${pkgJsonPath} is missing required 'name' field`,
|
|
723
|
-
};
|
|
724
|
-
}
|
|
365
|
+
logInfo(`building dashboard with \`${cmd} ${args.join(' ')}\``);
|
|
366
|
+
execFileSync(cmd, args, { cwd: REPO_ROOT, stdio: 'pipe', timeout: 240_000, env: childEnv });
|
|
367
|
+
if (existsSync(distIndex)) return { ok: true, skipped: false, message: 'Dashboard built (dist/index.html present)' };
|
|
368
|
+
return { ok: false, message: `build completed but ${distIndex} still missing` };
|
|
725
369
|
} catch (err) {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
message: `plugin package.json at ${pkgJsonPath} is invalid JSON: ${err.message}`,
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
if (state.plugin.symlink && !force) {
|
|
733
|
-
return {
|
|
734
|
-
ok: true,
|
|
735
|
-
message: `plugin dest is a dev symlink — skipping copy (use \`bizar dev-unlink\` to restore)`,
|
|
736
|
-
};
|
|
370
|
+
const stderr = err.stderr ? err.stderr.toString().split('\n').slice(0, 5).join(' | ') : '(no stderr)';
|
|
371
|
+
return { ok: false, message: `Dashboard build failed: ${stderr}` };
|
|
737
372
|
}
|
|
373
|
+
}
|
|
738
374
|
|
|
739
|
-
|
|
740
|
-
// behind by older broken installs that wrote `// fake plugin\n` and
|
|
741
|
-
// nothing else. Always overwrite those.
|
|
742
|
-
const destIndexTs = join(dest, 'index.ts');
|
|
743
|
-
if (existsSync(destIndexTs) && isFakePluginStub(destIndexTs)) {
|
|
744
|
-
if (!dryRun) {
|
|
745
|
-
try { rmSync(dest, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
746
|
-
}
|
|
747
|
-
state.plugin.installed = false;
|
|
748
|
-
state.plugin.upToDate = false;
|
|
749
|
-
}
|
|
375
|
+
// ─── Bizar HOME bootstrap ────────────────────────────────────────────────────
|
|
750
376
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
if (
|
|
754
|
-
|
|
755
|
-
}
|
|
377
|
+
/** Ensure `~/.config/bizar/` exists with memory-vault, loops, port file. Idempotent. */
|
|
378
|
+
export function ensureBizarHome({ dryRun = false } = {}) {
|
|
379
|
+
if (dryRun) return { ok: true, message: `[dry-run] would ensure ${BIZAR_HOME}` };
|
|
380
|
+
mkdirSync(BIZAR_HOME, { recursive: true });
|
|
381
|
+
mkdirSync(join(BIZAR_HOME, 'memory-vault'), { recursive: true });
|
|
382
|
+
mkdirSync(join(BIZAR_HOME, 'loops'), { recursive: true });
|
|
383
|
+
if (!existsSync(DASHBOARD_PORT_FILE)) writeFileSync(DASHBOARD_PORT_FILE, '4321\n');
|
|
384
|
+
return { ok: true, message: `${BIZAR_HOME} ready`, path: BIZAR_HOME };
|
|
385
|
+
}
|
|
756
386
|
|
|
757
|
-
|
|
758
|
-
return { ok: true, message: `[dry-run] would copy ${src} → ${dest}` };
|
|
759
|
-
}
|
|
387
|
+
// ─── Toolchain ───────────────────────────────────────────────────────────────
|
|
760
388
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
389
|
+
function checkToolchain() {
|
|
390
|
+
section('Toolchain');
|
|
391
|
+
if (haveCmd('node')) logOk(`node ${execSync('node --version').toString().trim()}`);
|
|
392
|
+
else { logErr('node not on PATH — install Node.js 18+'); process.exit(1); }
|
|
393
|
+
if (haveCmd('npm')) logOk(`npm ${execSync('npm --version').toString().trim()}`);
|
|
394
|
+
else logWarn('npm not on PATH');
|
|
395
|
+
if (haveCmd('git')) logOk(`git ${execSync('git --version').toString().trim().split(' ')[2]}`);
|
|
396
|
+
else logWarn('git not on PATH');
|
|
397
|
+
if (haveCmd('claude')) logOk(`claude ${execSync('claude --version').toString().trim().split('\n')[0]}`);
|
|
398
|
+
else logWarn('claude not on PATH — will install via npm');
|
|
399
|
+
}
|
|
764
400
|
|
|
765
|
-
|
|
401
|
+
/** Install Claude Code CLI globally via npm. Idempotent. */
|
|
402
|
+
function installClaudeCli({ force = false, dryRun = false } = {}) {
|
|
403
|
+
if (haveCmd('claude') && !force) return;
|
|
404
|
+
section('Installing Claude Code CLI');
|
|
405
|
+
logInfo('npm install -g @anthropic-ai/claude-agent-sdk @anthropic-ai/claude-code');
|
|
406
|
+
if (dryRun) return;
|
|
766
407
|
try {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
try { rmSync(dest, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
770
|
-
mkdirSync(dest, { recursive: true });
|
|
771
|
-
|
|
772
|
-
// Filter against RELATIVE path segments, not absolute substrings. This
|
|
773
|
-
// is the v5.6.0-beta.12 fix: the old filter excluded the src dir itself
|
|
774
|
-
// because npm-global paths contain `node_modules`.
|
|
775
|
-
//
|
|
776
|
-
// v5.6.0-beta.14: also skip `tests/` and `scripts/` (test fixtures &
|
|
777
|
-
// helper scripts aren't part of the runtime plugin) and `coverage/`
|
|
778
|
-
// (jest artifacts). The runtime only needs `index.ts`, `src/`,
|
|
779
|
-
// `package.json`, and the static docs.
|
|
780
|
-
const skipDirs = new Set([
|
|
781
|
-
'node_modules',
|
|
782
|
-
'dist',
|
|
783
|
-
'tests',
|
|
784
|
-
'scripts',
|
|
785
|
-
'coverage',
|
|
786
|
-
'.DS_Store',
|
|
787
|
-
]);
|
|
788
|
-
await cp(src, dest, {
|
|
789
|
-
recursive: true,
|
|
790
|
-
filter: (p) => {
|
|
791
|
-
const rel = p.slice(src.length).split(sep).filter(Boolean);
|
|
792
|
-
// Drop any path that has a skip segment.
|
|
793
|
-
return !rel.some((seg) => skipDirs.has(seg));
|
|
794
|
-
},
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
// Copy the SDK into the deployed plugin's node_modules so Bun can
|
|
798
|
-
// resolve @polderlabs/bizar-sdk when loading the plugin from
|
|
799
|
-
// ${CLAUDE_DIR}/plugins/bizar/.
|
|
800
|
-
const sdkSrc = join(state.pkgRoot, 'node_modules', '@polderlabs', 'bizar-sdk');
|
|
801
|
-
const sdkDst = join(dest, 'node_modules', '@polderlabs', 'bizar-sdk');
|
|
802
|
-
if (existsSync(sdkSrc)) {
|
|
803
|
-
mkdirSync(join(dest, 'node_modules', '@polderlabs'), { recursive: true });
|
|
804
|
-
await cp(sdkSrc, sdkDst, { recursive: true });
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// Wire runtime deps (`zod`, `@anthropic-ai/claude-agent-sdk`) into
|
|
808
|
-
// the deployed plugin's node_modules/ so Bun's module resolver
|
|
809
|
-
// finds them when loading the plugin from
|
|
810
|
-
// ${CLAUDE_DIR}/plugins/bizar/. Without this, the plugin throws
|
|
811
|
-
// "Cannot find module 'zod' (+2 more)" at startup.
|
|
812
|
-
//
|
|
813
|
-
// The resolution logic lives in `cli/plugin-runtime-deps.mjs` and is
|
|
814
|
-
// shared with the legacy installer (`cli/install.mjs`). It searches
|
|
815
|
-
// the Bizar npm pkg's own `node_modules/`, then the
|
|
816
|
-
// `@anthropic-ai/claude-code` npm pkg's `node_modules/`, then the
|
|
817
|
-
// dev source tree. Symlinks are preferred; recursive copy is the
|
|
818
|
-
// fallback.
|
|
819
|
-
//
|
|
820
|
-
// Only wire when the plugin entry is a `.ts` source file. If the
|
|
821
|
-
// plugin ships a pre-bundled `dist/index.js`, the runtime deps are
|
|
822
|
-
// already inlined and wiring is wasted work (and might shadow an
|
|
823
|
-
// intentional `node_modules/` layout in the bundle).
|
|
824
|
-
let needsWiring = true;
|
|
825
|
-
try {
|
|
826
|
-
const pkg = JSON.parse(readFileSync(join(dest, 'package.json'), 'utf8'));
|
|
827
|
-
if (typeof pkg.main === 'string' && !pkg.main.endsWith('.ts')) {
|
|
828
|
-
needsWiring = false;
|
|
829
|
-
}
|
|
830
|
-
} catch {
|
|
831
|
-
// missing/invalid package.json — proceed and let the wiring try.
|
|
832
|
-
}
|
|
833
|
-
if (needsWiring) {
|
|
834
|
-
const { wirePluginRuntimeDeps } = await import('./plugin-runtime-deps.mjs');
|
|
835
|
-
await wirePluginRuntimeDeps(dest, undefined, { pkgRoot: state.pkgRoot });
|
|
836
|
-
}
|
|
837
|
-
return { ok: true, message: `plugin copied to ${dest}` };
|
|
408
|
+
execSync('npm install -g @anthropic-ai/claude-agent-sdk @anthropic-ai/claude-code', { stdio: 'inherit' });
|
|
409
|
+
logOk('Claude Code CLI installed');
|
|
838
410
|
} catch (err) {
|
|
839
|
-
|
|
411
|
+
logErr(`npm install failed: ${err.message}`);
|
|
412
|
+
logWarn('retry manually: npm install -g @anthropic-ai/claude-code');
|
|
840
413
|
}
|
|
841
414
|
}
|
|
842
415
|
|
|
843
|
-
|
|
844
|
-
* Detect a "fake plugin" stub at the dest. These were emitted by old test
|
|
845
|
-
* suites and early install paths that wrote `// fake plugin\n` and never
|
|
846
|
-
* updated the file again. Anything under ~200 bytes is considered a stub.
|
|
847
|
-
*/
|
|
848
|
-
function isFakePluginStub(file) {
|
|
849
|
-
try {
|
|
850
|
-
const st = statSync(file);
|
|
851
|
-
if (st.size < 200) return true;
|
|
852
|
-
const head = readFileSync(file, 'utf8').slice(0, 200).toLowerCase();
|
|
853
|
-
if (head.includes('fake plugin')) return true;
|
|
854
|
-
return false;
|
|
855
|
-
} catch {
|
|
856
|
-
return true; // missing/unreadable → treat as bad
|
|
857
|
-
}
|
|
858
|
-
}
|
|
416
|
+
// ─── File sync helpers ──────────────────────────────────────────────────────
|
|
859
417
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
const dEntries = readdirSyncSafe(d);
|
|
872
|
-
if (!sEntries) return false;
|
|
873
|
-
// src should not have files missing from dest
|
|
874
|
-
for (const e of sEntries) {
|
|
875
|
-
if (skipDirs.has(e.name)) continue;
|
|
876
|
-
const sFull = join(s, e.name);
|
|
877
|
-
const dFull = join(d, e.name);
|
|
878
|
-
try {
|
|
879
|
-
const sSt = statSync(sFull);
|
|
880
|
-
if (sSt.isDirectory()) {
|
|
881
|
-
if (!walk(sFull, dFull)) return false;
|
|
882
|
-
} else {
|
|
883
|
-
const dSt = statSync(dFull);
|
|
884
|
-
if (dSt.size !== sSt.size) return false;
|
|
885
|
-
if (dSt.mtimeMs < sSt.mtimeMs) return false;
|
|
886
|
-
}
|
|
887
|
-
} catch {
|
|
888
|
-
return false;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
// dest may have extra files (legacy artefacts) — that's OK.
|
|
892
|
-
void dEntries;
|
|
893
|
-
return true;
|
|
418
|
+
function syncDir(srcDir, destDir, opts = {}) {
|
|
419
|
+
if (!existsSync(srcDir)) return { copied: 0, skipped: 0 };
|
|
420
|
+
ensureDir(destDir);
|
|
421
|
+
let copied = 0, skipped = 0;
|
|
422
|
+
for (const name of readdirSync(srcDir)) {
|
|
423
|
+
if (name.startsWith('.')) continue;
|
|
424
|
+
const sp = join(srcDir, name), dp = join(destDir, name);
|
|
425
|
+
const st = statSync(sp);
|
|
426
|
+
if (opts.filter && !opts.filter(name, sp)) { skipped++; continue; }
|
|
427
|
+
if (st.isDirectory()) { const r = syncDir(sp, dp, opts); copied += r.copied; skipped += r.skipped; }
|
|
428
|
+
else { copyFileSync(sp, dp); copied++; }
|
|
894
429
|
}
|
|
895
|
-
return
|
|
430
|
+
return { copied, skipped };
|
|
896
431
|
}
|
|
897
432
|
|
|
898
|
-
function
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
}
|
|
902
|
-
|
|
433
|
+
export async function syncAgentFiles({ dryRun = false } = {}) {
|
|
434
|
+
const src = join(REPO_ROOT, 'config', 'agents');
|
|
435
|
+
const dest = CLAUDE_AGENTS_DIR;
|
|
436
|
+
if (!existsSync(src)) return { ok: true, message: `no agents source at ${src}`, copied: 0, skipped: 0 };
|
|
437
|
+
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}` };
|
|
438
|
+
ensureDir(dest); ensureDir(join(dest, '_shared'));
|
|
439
|
+
let { copied, skipped } = syncDir(src, dest, { filter: n => n.endsWith('.md') });
|
|
440
|
+
const sharedSrc = join(src, '_shared');
|
|
441
|
+
if (existsSync(sharedSrc)) {
|
|
442
|
+
for (const entry of readdirSync(sharedSrc, { withFileTypes: true })) {
|
|
443
|
+
if (entry.isFile()) { copyFileSync(join(sharedSrc, entry.name), join(dest, '_shared', entry.name)); copied++; }
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return { ok: true, message: `${copied} agent(s) synced (${skipped} kept)`, copied, skipped };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export async function syncSkillFiles({ dryRun = false } = {}) {
|
|
450
|
+
const src = join(REPO_ROOT, 'config', 'skills');
|
|
451
|
+
const dest = CLAUDE_SKILLS_DIR;
|
|
452
|
+
if (!existsSync(src)) return { ok: true, message: `no skills source at ${src}`, copied: 0, skipped: 0 };
|
|
453
|
+
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}` };
|
|
454
|
+
ensureDir(dest); copyDirContents(src, dest);
|
|
455
|
+
const sharedBaseline = join(REPO_ROOT, 'config', 'agents', '_shared', 'AGENT_BASELINE.md');
|
|
456
|
+
if (existsSync(sharedBaseline)) {
|
|
457
|
+
const baselineDir = join(dest, 'agent-baseline');
|
|
458
|
+
ensureDir(baselineDir);
|
|
459
|
+
copyFileSync(sharedBaseline, join(baselineDir, 'SKILL.md'));
|
|
460
|
+
}
|
|
461
|
+
const count = readdirSync(dest).filter(n => { try { return statSync(join(dest, n)).isDirectory(); } catch { return false; } }).length;
|
|
462
|
+
return { ok: true, message: `${count} skill(s) synced`, copied: count, skipped: 0 };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function syncCommandFiles({ dryRun = false } = {}) {
|
|
466
|
+
const candidates = [join(REPO_ROOT, '.claude', 'commands'), join(REPO_ROOT, 'config', 'commands')];
|
|
467
|
+
let src = null;
|
|
468
|
+
for (const c of candidates) if (existsSync(c)) { src = c; break; }
|
|
469
|
+
if (!src) return { ok: true, message: 'no commands source found', copied: 0, skipped: 0 };
|
|
470
|
+
const dest = CLAUDE_COMMANDS_DIR;
|
|
471
|
+
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}` };
|
|
472
|
+
ensureDir(dest);
|
|
473
|
+
const { copied, skipped } = syncDir(src, dest, { filter: n => n.endsWith('.md') });
|
|
474
|
+
return { ok: true, message: `${copied} command(s) synced (${skipped} kept)`, copied, skipped };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export async function syncRulesFiles({ dryRun = false } = {}) {
|
|
478
|
+
const src = join(REPO_ROOT, 'config', 'rules');
|
|
479
|
+
const dest = CLAUDE_RULES_DIR;
|
|
480
|
+
if (!existsSync(src)) return { ok: true, message: `no rules source at ${src}`, copied: 0, skipped: 0 };
|
|
481
|
+
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}` };
|
|
482
|
+
ensureDir(dest);
|
|
483
|
+
const { copied, skipped } = syncDir(src, dest, { filter: n => n.endsWith('.md') || n.endsWith('.txt') });
|
|
484
|
+
return { ok: true, message: `${copied} rule(s) synced (${skipped} kept)`, copied, skipped };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export async function syncHookFiles({ dryRun = false } = {}) {
|
|
488
|
+
const src = join(REPO_ROOT, '.claude', 'hooks');
|
|
489
|
+
const dest = CLAUDE_HOOKS_DIR;
|
|
490
|
+
ensureDir(dest);
|
|
491
|
+
if (!existsSync(src)) return { ok: true, message: `no hooks source at ${src}`, copied: 0, skipped: 0 };
|
|
492
|
+
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}` };
|
|
493
|
+
copyDirContents(src, dest);
|
|
494
|
+
for (const name of readdirSync(dest)) {
|
|
495
|
+
const fp = join(dest, name);
|
|
496
|
+
try {
|
|
497
|
+
const st = statSync(fp);
|
|
498
|
+
if (st.isFile() && (name.endsWith('.sh') || name.endsWith('.mjs'))) chmodSync(fp, 0o755);
|
|
499
|
+
} catch { /* ignore */ }
|
|
903
500
|
}
|
|
501
|
+
return { ok: true, message: 'hook scripts installed', copied: readdirSync(dest).length, skipped: 0 };
|
|
904
502
|
}
|
|
905
503
|
|
|
906
|
-
|
|
907
|
-
* Ensure the Bizar plugin entry exists in `${CLAUDE_DIR}/cline.json`.
|
|
908
|
-
* Idempotent: if the entry already exists, no-op.
|
|
909
|
-
*/
|
|
910
|
-
export async function patchClineJson({ dryRun, force }) {
|
|
911
|
-
// v6.3.0 — Claude Code-native. The "plugin entry" model is gone;
|
|
912
|
-
// Claude Code uses an MCP server entry in `mcpServers` plus optional
|
|
913
|
-
// hooks in `hooks`. We still expose this function under the old name
|
|
914
|
-
// for back-compat with callers (e.g. update.mjs), but the body now
|
|
915
|
-
// patches ~/.claude/settings.json.
|
|
916
|
-
return patchClaudeSettings({ dryRun, force });
|
|
917
|
-
}
|
|
504
|
+
// ─── settings.json ──────────────────────────────────────────────────────────
|
|
918
505
|
|
|
919
|
-
export
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
$schema: 'https://json.schemastore.org/claude-code-settings.json',
|
|
934
|
-
mcpServers: {
|
|
935
|
-
bizar: {
|
|
936
|
-
command: 'bizar-mcp',
|
|
937
|
-
args: [],
|
|
506
|
+
export function writeClaudeSettings({ dryRun = false, force = false } = {}) {
|
|
507
|
+
const fp = join(CLAUDE_DIR, 'settings.json');
|
|
508
|
+
const existing = readJsonSafe(fp, {}) || {};
|
|
509
|
+
|
|
510
|
+
const bizarSettings = {
|
|
511
|
+
$schema: 'https://json.schemastore.org/claude-code-settings.json',
|
|
512
|
+
mcpServers: {
|
|
513
|
+
bizar: {
|
|
514
|
+
type: 'stdio',
|
|
515
|
+
command: 'npx',
|
|
516
|
+
args: ['-y', '@polderlabs/bizar-sdk', 'mcp'],
|
|
517
|
+
env: {
|
|
518
|
+
BIZAR_HOME,
|
|
519
|
+
BIZAR_MEMORY_VAULT: process.env.BIZAR_MEMORY_VAULT || join(BIZAR_HOME, 'memory-vault'),
|
|
938
520
|
},
|
|
939
521
|
},
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
522
|
+
},
|
|
523
|
+
permissions: {
|
|
524
|
+
allow: ['mcp__bizar__*'],
|
|
525
|
+
deny: [
|
|
526
|
+
'Read(./.env)', 'Read(./.env.*)',
|
|
527
|
+
'Bash(rm -rf /)', 'Bash(sudo *)',
|
|
528
|
+
'Write(./node_modules/**)', 'Write(./package-lock.json)',
|
|
529
|
+
],
|
|
530
|
+
},
|
|
531
|
+
env: {
|
|
532
|
+
BIZAR_HOME,
|
|
533
|
+
BIZAR_MEMORY_VAULT: process.env.BIZAR_MEMORY_VAULT || join(BIZAR_HOME, 'memory-vault'),
|
|
534
|
+
},
|
|
535
|
+
hooks: {
|
|
536
|
+
PreToolUse: [{ matcher: 'Write|Edit|MultiEdit|Bash', hooks: [{ type: 'command', command: join(CLAUDE_HOOKS_DIR, 'pretooluse-editwrite.mjs'), timeout: 10 }] }],
|
|
537
|
+
PostToolUse: [{ matcher: 'Edit|Write|MultiEdit', hooks: [{ type: 'command', command: join(CLAUDE_HOOKS_DIR, 'posttooluse-editwrite.mjs'), timeout: 10 }] }],
|
|
538
|
+
UserPromptSubmit: [{ hooks: [{ type: 'command', command: join(CLAUDE_HOOKS_DIR, 'userpromptsubmit-tag.mjs'), timeout: 10 }] }],
|
|
539
|
+
SessionStart: [{ hooks: [{ type: 'command', command: join(CLAUDE_HOOKS_DIR, 'sessionstart-prime.mjs'), timeout: 10 }] }],
|
|
540
|
+
SessionEnd: [{ hooks: [{ type: 'command', command: join(CLAUDE_HOOKS_DIR, 'sessionend-record.mjs'), timeout: 10 }] }],
|
|
541
|
+
},
|
|
542
|
+
};
|
|
950
543
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
// Per user request: "the installer should do nothing with providers,
|
|
961
|
-
// the user has to configure it themselves." The installer sets up the
|
|
962
|
-
// Bizar scaffolding (MCP server entry, schema, hooks) and stays out of
|
|
963
|
-
// provider configuration entirely.
|
|
964
|
-
//
|
|
965
|
-
// All patches are additive and idempotent. We never overwrite a value
|
|
966
|
-
// the user has set; the `force` flag bypasses that protection for the
|
|
967
|
-
// MCP server entry only.
|
|
968
|
-
let addedMcpServer = false;
|
|
969
|
-
let addedSchema = false;
|
|
970
|
-
let addedHooks = false;
|
|
971
|
-
|
|
972
|
-
const mcpServers = (cfg.mcpServers && typeof cfg.mcpServers === 'object')
|
|
973
|
-
? cfg.mcpServers : {};
|
|
974
|
-
if (!mcpServers.bizar) {
|
|
975
|
-
mcpServers.bizar = {
|
|
976
|
-
command: 'bizar-mcp',
|
|
977
|
-
args: [],
|
|
544
|
+
const merged = { ...existing };
|
|
545
|
+
if (force) { Object.assign(merged, bizarSettings); }
|
|
546
|
+
else {
|
|
547
|
+
merged.$schema = merged.$schema || bizarSettings.$schema;
|
|
548
|
+
merged.mcpServers = { ...(existing.mcpServers || {}), ...bizarSettings.mcpServers };
|
|
549
|
+
merged.permissions = {
|
|
550
|
+
allow: [...new Set([...(existing.permissions?.allow || []), ...bizarSettings.permissions.allow])],
|
|
551
|
+
deny: [...new Set([...(existing.permissions?.deny || []), ...bizarSettings.permissions.deny])],
|
|
552
|
+
ask: [...new Set([...(existing.permissions?.ask || [])])],
|
|
978
553
|
};
|
|
979
|
-
|
|
554
|
+
merged.env = { ...(existing.env || {}), ...bizarSettings.env };
|
|
555
|
+
merged.hooks = { ...(existing.hooks || {}), ...bizarSettings.hooks };
|
|
980
556
|
}
|
|
981
|
-
cfg.mcpServers = mcpServers;
|
|
982
557
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
558
|
+
if (dryRun) return { ok: true, message: `[dry-run] would write ${fp}` };
|
|
559
|
+
ensureDir(CLAUDE_DIR);
|
|
560
|
+
writeFileSync(fp, JSON.stringify(merged, null, 2) + '\n');
|
|
561
|
+
return { ok: true, message: `wrote ${fp}`, path: fp };
|
|
562
|
+
}
|
|
988
563
|
|
|
989
|
-
|
|
990
|
-
if (!cfg.hooks || (typeof cfg.hooks === 'object' && Object.keys(cfg.hooks).length === 0)) {
|
|
991
|
-
cfg.hooks = {
|
|
992
|
-
SessionStart: [
|
|
993
|
-
{
|
|
994
|
-
hooks: [
|
|
995
|
-
{
|
|
996
|
-
type: 'command',
|
|
997
|
-
command: 'cat .claude/hooks/bizar-tools.md',
|
|
998
|
-
},
|
|
999
|
-
],
|
|
1000
|
-
},
|
|
1001
|
-
],
|
|
1002
|
-
};
|
|
1003
|
-
addedHooks = true;
|
|
1004
|
-
}
|
|
564
|
+
// ─── CLAUDE.md mirror ──────────────────────────────────────────────────────
|
|
1005
565
|
|
|
1006
|
-
|
|
566
|
+
export function writeClaudeMdMirror({ dryRun = false, force = false } = {}) {
|
|
567
|
+
const srcCandidates = [join(REPO_ROOT, 'AGENTS.md'), join(REPO_ROOT, 'CLAUDE.md')];
|
|
568
|
+
const src = srcCandidates.find(p => existsSync(p));
|
|
569
|
+
const dest = join(CLAUDE_DIR, 'CLAUDE.md');
|
|
570
|
+
if (!src) return { ok: false, message: 'no CLAUDE.md / AGENTS.md found in repo root' };
|
|
571
|
+
if (existsSync(dest) && !force) return { ok: true, message: `${dest} already exists — pass --force to overwrite` };
|
|
572
|
+
if (dryRun) return { ok: true, message: `[dry-run] would mirror ${src} → ${dest}` };
|
|
573
|
+
ensureDir(CLAUDE_DIR);
|
|
574
|
+
const body = readFileSync(src, 'utf8');
|
|
575
|
+
const banner = `# CLAUDE.md — Mirror of AGENTS.md for Claude Code compatibility\n\n` +
|
|
576
|
+
`> Auto-generated by \`cli/provision.mjs:writeClaudeMdMirror\`.\n` +
|
|
577
|
+
`> DO NOT EDIT THIS FILE DIRECTLY — edit \`AGENTS.md\` and run \`make mirror-claude-md\`.\n\n` +
|
|
578
|
+
`---\n\n`;
|
|
579
|
+
writeFileSync(dest, banner + body);
|
|
580
|
+
return { ok: true, message: `mirrored ${src} → ${dest}`, path: dest };
|
|
581
|
+
}
|
|
1007
582
|
|
|
1008
|
-
|
|
1009
|
-
return { ok: true, message: 'settings.json already has Bizar MCP server entry' };
|
|
1010
|
-
}
|
|
583
|
+
// ─── MCP server + plugin registration ───────────────────────────────────────
|
|
1011
584
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
return { ok: true, message: `[dry-run] would patch settings.json with: ${added.join(', ')}` };
|
|
585
|
+
export function setupMcpServer({ dryRun = false } = {}) {
|
|
586
|
+
const settingsPath = join(CLAUDE_DIR, 'settings.json');
|
|
587
|
+
const existing = readJsonSafe(settingsPath, null);
|
|
588
|
+
if (existing && existing.mcpServers && existing.mcpServers.bizar) {
|
|
589
|
+
return { ok: true, message: `bizar MCP server already registered in ${settingsPath}` };
|
|
1018
590
|
}
|
|
591
|
+
if (!haveCmd('claude')) return { ok: false, message: 'claude CLI not on PATH' };
|
|
592
|
+
if (dryRun) return { ok: true, message: '[dry-run] would run: claude mcp add bizar -- npx -y @polderlabs/bizar-sdk mcp' };
|
|
593
|
+
const r = spawnSync('claude', ['mcp', 'add', '-f', '-s', 'user', 'bizar', '--', 'npx', '-y', '@polderlabs/bizar-sdk', 'mcp'], { stdio: 'inherit', timeout: 60_000 });
|
|
594
|
+
if (r.status !== 0) return { ok: false, message: `claude mcp add exited with code ${r.status}` };
|
|
595
|
+
return { ok: true, message: 'bizar MCP server registered' };
|
|
596
|
+
}
|
|
1019
597
|
|
|
1020
|
-
|
|
1021
|
-
const
|
|
1022
|
-
if (
|
|
1023
|
-
if (
|
|
1024
|
-
if (
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
? `settings.json patched: ${added.join(', ')}`
|
|
1029
|
-
: 'settings.json already complete',
|
|
1030
|
-
};
|
|
598
|
+
export function setupClaudePlugin({ dryRun = false } = {}) {
|
|
599
|
+
const manifestPath = join(REPO_ROOT, '.claude-plugin', 'plugin.json');
|
|
600
|
+
if (!existsSync(manifestPath)) return { ok: true, message: `no plugin manifest — skipping` };
|
|
601
|
+
if (!haveCmd('claude')) return { ok: false, message: 'claude CLI not on PATH' };
|
|
602
|
+
if (dryRun) return { ok: true, message: `[dry-run] would run: claude plugin install ${REPO_ROOT}` };
|
|
603
|
+
const r = spawnSync('claude', ['plugin', 'install', '.'], { stdio: 'inherit', timeout: 120_000, cwd: REPO_ROOT });
|
|
604
|
+
if (r.status !== 0) return { ok: false, message: `claude plugin install exited with code ${r.status}` };
|
|
605
|
+
return { ok: true, message: `claude plugin installed from ${REPO_ROOT}` };
|
|
1031
606
|
}
|
|
1032
607
|
|
|
1033
|
-
|
|
1034
|
-
* Copy `config/agents/*.md` into `${CLAUDE_DIR}/agents/`. Idempotent.
|
|
1035
|
-
* Doesn't overwrite existing files unless `force: true`.
|
|
1036
|
-
*/
|
|
1037
|
-
export async function syncAgentFiles({ dryRun, force }) {
|
|
1038
|
-
/**
|
|
1039
|
-
* v5.6.0-beta.12 — handle the Cline agent format.
|
|
1040
|
-
*
|
|
1041
|
-
* Cline reads agents from `~/.cline/agents/<name>.{yml,yaml}` with a
|
|
1042
|
-
* Zod-validated schema:
|
|
1043
|
-
* { name: string, description: string, tools?, skills?,
|
|
1044
|
-
* providerId?, modelId?, maxIterations? }
|
|
1045
|
-
*
|
|
1046
|
-
* Our source agents are written in OpenCode-flavored Markdown
|
|
1047
|
-
* (frontmatter + body). Both are valid: the body is the system prompt,
|
|
1048
|
-
* the frontmatter is config. To make agents loadable by the Cline CLI
|
|
1049
|
-
* we copy both `.md` (legacy / docs) and `.yaml` (Cline-loadable) into
|
|
1050
|
-
* `${CLAUDE_DIR}/agents/`. The YAML version is auto-generated if
|
|
1051
|
-
* missing, with the `name` field added from the filename.
|
|
1052
|
-
*/
|
|
1053
|
-
const srcDir = join(REPO_ROOT, 'config', 'agents');
|
|
1054
|
-
const dstDir = join(CLAUDE_DIR, 'agents');
|
|
1055
|
-
if (!existsSync(srcDir)) {
|
|
1056
|
-
return { ok: true, message: 'no bundled agents to sync' };
|
|
1057
|
-
}
|
|
1058
|
-
if (dryRun) {
|
|
1059
|
-
return { ok: true, message: `[dry-run] would sync ${srcDir} → ${dstDir}` };
|
|
1060
|
-
}
|
|
608
|
+
// ─── Dashboard service ─────────────────────────────────────────────────────
|
|
1061
609
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
if (entry.name === '_shared') continue;
|
|
1071
|
-
const src = join(srcDir, entry.name);
|
|
1072
|
-
const dst = join(dstDir, entry.name);
|
|
1073
|
-
|
|
1074
|
-
// Always copy the source file (md or yaml) verbatim.
|
|
1075
|
-
if (existsSync(dst) && !force) {
|
|
1076
|
-
skipped++;
|
|
1077
|
-
} else {
|
|
1078
|
-
copyFileSync(src, dst);
|
|
1079
|
-
copied++;
|
|
1080
|
-
}
|
|
610
|
+
function installDashboardService({ dryRun = false } = {}) {
|
|
611
|
+
const bin = join(REPO_ROOT, 'cli', 'bin.mjs');
|
|
612
|
+
if (!existsSync(bin)) return { ok: false, message: `cli/bin.mjs not found — service registration deferred` };
|
|
613
|
+
if (dryRun) return { ok: true, message: `[dry-run] would run: node ${bin} service install` };
|
|
614
|
+
const r = spawnSync('node', [bin, 'service', 'install'], { stdio: 'inherit' });
|
|
615
|
+
if (r.status !== 0) return { ok: false, message: `service install returned ${r.status}` };
|
|
616
|
+
return { ok: true, message: 'dashboard service registered' };
|
|
617
|
+
}
|
|
1081
618
|
|
|
1082
|
-
|
|
1083
|
-
// dest, generate one so Cline can load it.
|
|
1084
|
-
if (entry.name.endsWith('.md')) {
|
|
1085
|
-
const base = entry.name.slice(0, -3);
|
|
1086
|
-
const yamlDst = join(dstDir, base + '.yaml');
|
|
1087
|
-
if (!existsSync(yamlDst) || force) {
|
|
1088
|
-
const mdText = readFileSync(src, 'utf8');
|
|
1089
|
-
const yamlText = mdToClineAgentYaml(mdText, base);
|
|
1090
|
-
writeFileSync(yamlDst, yamlText);
|
|
1091
|
-
copied++;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
619
|
+
// ─── Detection ─────────────────────────────────────────────────────────────
|
|
1095
620
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
621
|
+
/**
|
|
622
|
+
* Probe the current state without modifying anything.
|
|
623
|
+
* Returned shape:
|
|
624
|
+
* {
|
|
625
|
+
* claudeDir, claudeCli, agentsDir, skillsDir, commandsDir,
|
|
626
|
+
* hooksDir, rulesDir, settingsFile, mcpServer, pluginManifest,
|
|
627
|
+
* bizarHome, gitRepo,
|
|
628
|
+
* }
|
|
629
|
+
*/
|
|
630
|
+
export function detectState() {
|
|
631
|
+
const settingsPath = join(CLAUDE_DIR, 'settings.json');
|
|
632
|
+
const settings = readJsonSafe(settingsPath, null);
|
|
633
|
+
let claudeVersion = null;
|
|
634
|
+
try {
|
|
635
|
+
claudeVersion = execFileSync('claude', ['--version'], { encoding: 'utf8', timeout: 5000 })
|
|
636
|
+
.toString().trim().split('\n')[0] || null;
|
|
637
|
+
} catch { /* not installed */ }
|
|
638
|
+
|
|
639
|
+
const countMd = (dir) => {
|
|
640
|
+
if (!existsSync(dir)) return 0;
|
|
641
|
+
try { return readdirSync(dir, { withFileTypes: true }).filter(e => e.isFile() && (e.name.endsWith('.md') || e.name.endsWith('.mjs'))).length; }
|
|
642
|
+
catch { return 0; }
|
|
643
|
+
};
|
|
644
|
+
const countSkillDirs = (dir) => {
|
|
645
|
+
if (!existsSync(dir)) return 0;
|
|
646
|
+
try { return readdirSync(dir, { withFileTypes: true }).filter(e => e.isDirectory()).length; }
|
|
647
|
+
catch { return 0; }
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const mcpServer = settings?.mcpServers?.bizar
|
|
651
|
+
? { registered: true, type: settings.mcpServers.bizar.type || 'stdio', command: settings.mcpServers.bizar.command || 'npx' }
|
|
652
|
+
: { registered: false };
|
|
1105
653
|
|
|
1106
654
|
return {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
655
|
+
claudeDir: CLAUDE_DIR,
|
|
656
|
+
claudeCli: { available: !!claudeVersion, version: claudeVersion },
|
|
657
|
+
agentsDir: { exists: existsSync(CLAUDE_AGENTS_DIR), count: countMd(CLAUDE_AGENTS_DIR) },
|
|
658
|
+
skillsDir: { exists: existsSync(CLAUDE_SKILLS_DIR), count: countSkillDirs(CLAUDE_SKILLS_DIR) },
|
|
659
|
+
commandsDir: { exists: existsSync(CLAUDE_COMMANDS_DIR), count: countMd(CLAUDE_COMMANDS_DIR) },
|
|
660
|
+
hooksDir: { exists: existsSync(CLAUDE_HOOKS_DIR), count: countMd(CLAUDE_HOOKS_DIR) },
|
|
661
|
+
rulesDir: { exists: existsSync(CLAUDE_RULES_DIR), count: countMd(CLAUDE_RULES_DIR) },
|
|
662
|
+
settingsFile: { exists: existsSync(settingsPath), parses: settings !== null && typeof settings === 'object', path: settingsPath },
|
|
663
|
+
mcpServer,
|
|
664
|
+
pluginManifest: { exists: existsSync(join(REPO_ROOT, '.claude-plugin', 'plugin.json')) },
|
|
665
|
+
bizarHome: { exists: existsSync(BIZAR_HOME), path: BIZAR_HOME },
|
|
666
|
+
gitRepo: existsSync(join(REPO_ROOT, '.git')),
|
|
1111
667
|
};
|
|
1112
668
|
}
|
|
1113
669
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
670
|
+
export function detectStateJson() {
|
|
671
|
+
return JSON.stringify(detectState(), null, 2);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ─── Top-level orchestration ─────────────────────────────────────────────────
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* The unified provision flow. `mode` is 'install' or 'update'.
|
|
678
|
+
* Every step is idempotent.
|
|
1118
679
|
*/
|
|
1119
|
-
function
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
// No frontmatter — write a minimal YAML with just the name + body.
|
|
1123
|
-
return [
|
|
1124
|
-
'name: ' + JSON.stringify(fallbackName),
|
|
1125
|
-
'description: ' + JSON.stringify(fallbackName),
|
|
1126
|
-
'',
|
|
1127
|
-
].join('\n');
|
|
1128
|
-
}
|
|
1129
|
-
// Wrap the fallback as a YAML mapping block too.
|
|
1130
|
-
const _fbFallback = '---\n' +
|
|
1131
|
-
'name: ' + JSON.stringify(fallbackName) + '\n' +
|
|
1132
|
-
'description: ' + JSON.stringify(fallbackName) + '\n' +
|
|
1133
|
-
'---\n\n' + mdText.trim() + '\n';
|
|
1134
|
-
if (!mdText.includes('---')) return _fbFallback;
|
|
1135
|
-
|
|
1136
|
-
const fm = m[1];
|
|
1137
|
-
const body = m[2];
|
|
1138
|
-
|
|
1139
|
-
// If frontmatter already has a `name:` line, leave it alone.
|
|
1140
|
-
let newFm = fm;
|
|
1141
|
-
if (!/^name:/m.test(newFm)) {
|
|
1142
|
-
newFm = `name: ${fallbackName}\n` + newFm;
|
|
1143
|
-
}
|
|
680
|
+
export async function runProvision(opts = {}) {
|
|
681
|
+
const { mode = 'install', dryRun = false, force = false, yes = false, start = true } = opts;
|
|
682
|
+
const effectiveMode = mode === 'update' ? 'update' : 'install';
|
|
1144
683
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
]);
|
|
1159
|
-
// Drop blocks whose top-level key isn't allowed (e.g. `permission:`).
|
|
1160
|
-
// An indented line whose top-level parent wasn't allowed is a sub-key of
|
|
1161
|
-
// a dropped block — skip it.
|
|
1162
|
-
const lines = newFm.split('\n');
|
|
1163
|
-
const filteredLines = [];
|
|
1164
|
-
let currentTopAllowed = false;
|
|
1165
|
-
for (const line of lines) {
|
|
1166
|
-
const isIndented = line.startsWith(' ') || line.startsWith('\t');
|
|
1167
|
-
if (line === '' || isIndented) {
|
|
1168
|
-
if (currentTopAllowed) filteredLines.push(line);
|
|
1169
|
-
continue;
|
|
1170
|
-
}
|
|
1171
|
-
const keyMatch = line.match(/^(\w[\w-]*)\s*:/);
|
|
1172
|
-
if (!keyMatch) {
|
|
1173
|
-
filteredLines.push(line);
|
|
1174
|
-
currentTopAllowed = false;
|
|
1175
|
-
continue;
|
|
1176
|
-
}
|
|
1177
|
-
if (ALLOWED.has(keyMatch[1])) {
|
|
1178
|
-
filteredLines.push(line);
|
|
1179
|
-
currentTopAllowed = true;
|
|
1180
|
-
} else {
|
|
1181
|
-
currentTopAllowed = false;
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
const filtered = filteredLines.join('\n');
|
|
684
|
+
console.log('');
|
|
685
|
+
console.log(chalk.bold.cyan(' ⚡ BizarHarness Provisioner v10.0.0 (Claude Code)'));
|
|
686
|
+
console.log(chalk.dim(` Mode: ${effectiveMode}${force ? ' (force)' : ''}${dryRun ? ' (dry-run)' : ''}`));
|
|
687
|
+
console.log('');
|
|
688
|
+
|
|
689
|
+
const state = detectState();
|
|
690
|
+
|
|
691
|
+
section('Bizar HOME');
|
|
692
|
+
const bizarHomeStep = ensureBizarHome({ dryRun });
|
|
693
|
+
if (bizarHomeStep.ok) logOk(bizarHomeStep.message); else logErr(bizarHomeStep.message);
|
|
694
|
+
|
|
695
|
+
checkToolchain();
|
|
696
|
+
installClaudeCli({ force, dryRun });
|
|
1185
697
|
|
|
1186
|
-
|
|
698
|
+
const stepResults = [];
|
|
699
|
+
const runStep = async (label, fn) => {
|
|
700
|
+
section(label);
|
|
701
|
+
const r = await fn();
|
|
702
|
+
console.log(` ${r.ok ? '✓' : '✗'} ${r.message}`);
|
|
703
|
+
stepResults.push({ label, ...r });
|
|
704
|
+
return r;
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
await runStep('Syncing agents', () => syncAgentFiles({ dryRun, force }));
|
|
708
|
+
await runStep('Syncing skills', () => syncSkillFiles({ dryRun, force }));
|
|
709
|
+
await runStep('Syncing commands', () => syncCommandFiles({ dryRun, force }));
|
|
710
|
+
await runStep('Syncing rules', () => syncRulesFiles({ dryRun, force }));
|
|
711
|
+
await runStep('Syncing hooks', () => syncHookFiles({ dryRun, force }));
|
|
712
|
+
await runStep('Building SDK', () => buildSdk({ dryRun }));
|
|
713
|
+
await runStep('Building plugin', () => buildPlugin({ dryRun }));
|
|
714
|
+
await runStep('Building dashboard', () => buildDash({ dryRun }));
|
|
715
|
+
|
|
716
|
+
section('Writing settings.json');
|
|
717
|
+
const settingsStep = writeClaudeSettings({ dryRun, force });
|
|
718
|
+
if (settingsStep.ok) logOk(settingsStep.message); else logErr(settingsStep.message);
|
|
719
|
+
stepResults.push({ label: 'settings.json', ...settingsStep });
|
|
720
|
+
|
|
721
|
+
section('MCP server (bizar)');
|
|
722
|
+
const mcpStep = setupMcpServer({ dryRun });
|
|
723
|
+
if (mcpStep.ok) logOk(mcpStep.message); else logWarn(mcpStep.message);
|
|
724
|
+
stepResults.push({ label: 'mcp-server', ...mcpStep });
|
|
725
|
+
|
|
726
|
+
section('Claude plugin');
|
|
727
|
+
const pluginStep = setupClaudePlugin({ dryRun });
|
|
728
|
+
if (pluginStep.ok) logOk(pluginStep.message); else logWarn(pluginStep.message);
|
|
729
|
+
stepResults.push({ label: 'claude-plugin', ...pluginStep });
|
|
730
|
+
|
|
731
|
+
section('CLAUDE.md mirror');
|
|
732
|
+
const claudeMdStep = writeClaudeMdMirror({ dryRun, force });
|
|
733
|
+
if (claudeMdStep.ok) logOk(claudeMdStep.message); else logWarn(claudeMdStep.message);
|
|
734
|
+
stepResults.push({ label: 'claude-md', ...claudeMdStep });
|
|
735
|
+
|
|
736
|
+
if (start && effectiveMode === 'install') {
|
|
737
|
+
section('Dashboard service');
|
|
738
|
+
const svcStep = installDashboardService({ dryRun });
|
|
739
|
+
if (svcStep.ok) logOk(svcStep.message); else logWarn(svcStep.message);
|
|
740
|
+
stepResults.push({ label: 'service', ...svcStep });
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
section('Provision complete');
|
|
744
|
+
console.log(chalk.dim(JSON.stringify(detectState(), null, 2)));
|
|
745
|
+
console.log('');
|
|
746
|
+
const anyFail = stepResults.some(r => !r.ok);
|
|
747
|
+
if (anyFail) console.log(chalk.yellow(' ⚠ Some steps had issues.'));
|
|
748
|
+
else { console.log(chalk.bold.green(' ✓ Bizar is ready.')); console.log(chalk.dim(' Next: restart your Claude Code session.')); }
|
|
749
|
+
console.log('');
|
|
750
|
+
return { ok: !anyFail, mode: effectiveMode, state: detectState(), stepResults };
|
|
1187
751
|
}
|
|
1188
752
|
|
|
753
|
+
// ─── Legacy / gap-closure helpers (from old provision.mjs) ───────────────────
|
|
754
|
+
|
|
1189
755
|
/**
|
|
1190
|
-
*
|
|
756
|
+
* LightRAG install via uv tool. Idempotent.
|
|
1191
757
|
*/
|
|
758
|
+
export async function installLightragProvision({ dryRun = false } = {}) {
|
|
759
|
+
if (dryRun) return { ok: true, message: '[dry-run] would run: uv tool install lightrag-hku[api]' };
|
|
760
|
+
if (haveCmd('lightrag-server')) return { ok: true, message: 'lightrag-server already on PATH' };
|
|
761
|
+
if (!haveCmd('uv')) return { ok: false, message: 'uv not found' };
|
|
762
|
+
try {
|
|
763
|
+
const r = spawnSync('uv', ['tool', 'install', 'lightrag-hku[api]'], { stdio: 'inherit', timeout: 120_000 });
|
|
764
|
+
return r.status === 0 || r.status === null
|
|
765
|
+
? { ok: true, message: 'lightrag-hku[api] installed via uv' }
|
|
766
|
+
: { ok: false, message: `uv tool install failed (exit ${r.status})` };
|
|
767
|
+
} catch (err) {
|
|
768
|
+
return { ok: false, message: `lightrag install error: ${err.message}` };
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
1192
772
|
/**
|
|
1193
|
-
*
|
|
1194
|
-
* Cline
|
|
1195
|
-
*
|
|
1196
|
-
* v5.6.0-beta.12 — complete rewrite:
|
|
1197
|
-
* - Iterates ALL subdirs of `config/skills/` dynamically instead of
|
|
1198
|
-
* hardcoding 3 names. Previously only `obsidian`, `glyph`, and
|
|
1199
|
-
* `read-the-damn-docs` were copied; the other 7 skills
|
|
1200
|
-
* (`bizar`, `cpp-coding-standards`, `cpp-testing`, `embedded-esp-idf`,
|
|
1201
|
-
* `lightrag`, `memory-protocol`, `self-improvement`) were silently
|
|
1202
|
-
* missing from the installed Cline.
|
|
1203
|
-
* - Each skill is copied as its OWN subdirectory under
|
|
1204
|
-
* `${CLAUDE_DIR}/skills/<name>/` (e.g. `skills/obsidian/SKILL.md`),
|
|
1205
|
-
* matching Cline's `resolveSkillsConfigSearchPaths` layout. Previously
|
|
1206
|
-
* all skills were flattened into one dir, causing `SKILL.md` from
|
|
1207
|
-
* each to overwrite the previous one — only the last copy survived.
|
|
1208
|
-
* - Same per-directory layout for `commands/`, `hooks/`, `workflows/`,
|
|
1209
|
-
* `rules/`.
|
|
1210
|
-
* - Idempotent: re-running refreshes content but never deletes user
|
|
1211
|
-
* additions.
|
|
1212
|
-
*
|
|
1213
|
-
* v6.0.1 — also syncs `config/rules/*.md` into `${CLAUDE_DIR}/rules/`.
|
|
1214
|
-
* Previously `config/rules/*.md` was only copied by the legacy
|
|
1215
|
-
* `cli/copy.mjs:installRules()` helper, never invoked by `bizar install`
|
|
1216
|
-
* or `bizar update` (which both go through `runProvision`). This meant
|
|
1217
|
-
* `general.md`, `git.md`, `javascript.md`, `python.md`, `testing.md`,
|
|
1218
|
-
* `thinking.md`, `uncertainty.md` were silently absent from the user's
|
|
1219
|
-
* `~/.cline/rules/`, breaking the always-on rules contract defined in
|
|
1220
|
-
* `AGENTS.md`. See https://docs.cline.bot/customization/cline-rules.
|
|
773
|
+
* v6.2.5 — Register every config/skills/<name>/SKILL.md in
|
|
774
|
+
* `~/.agents/.skill-lock.json` so Cline's marketplace UI shows them.
|
|
1221
775
|
*/
|
|
1222
|
-
export
|
|
1223
|
-
if (
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
776
|
+
export function writeBizarSkillLock({ skillsSrc, agentsDir }) {
|
|
777
|
+
if (!existsSync(skillsSrc)) return { ok: true, count: 0, path: join(agentsDir, '.skill-lock.json') };
|
|
778
|
+
const lockPath = join(agentsDir, '.skill-lock.json');
|
|
779
|
+
let lock = { version: 3, skills: {} };
|
|
780
|
+
try {
|
|
781
|
+
const raw = readFileSync(lockPath, 'utf8');
|
|
782
|
+
lock = JSON.parse(raw);
|
|
783
|
+
if (!lock.skills || typeof lock.skills !== 'object') lock.skills = {};
|
|
784
|
+
} catch { /* first install */ }
|
|
785
|
+
const now = new Date().toISOString();
|
|
786
|
+
let count = 0;
|
|
787
|
+
for (const skillDir of readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
788
|
+
if (!skillDir.isDirectory()) continue;
|
|
789
|
+
const name = skillDir.name;
|
|
790
|
+
const existing = lock.skills[name];
|
|
791
|
+
if (existing && existing.source === 'bizar/builtin') { existing.updatedAt = now; count++; continue; }
|
|
792
|
+
lock.skills[name] = {
|
|
793
|
+
source: 'bizar/builtin', sourceType: 'local',
|
|
794
|
+
sourceUrl: 'https://github.com/DrB0rk/BizarHarness',
|
|
795
|
+
skillPath: 'config/skills/' + name + '/SKILL.md',
|
|
796
|
+
skillFolderHash: 'bizar-managed', pluginName: 'bizar',
|
|
797
|
+
installedAt: existing && existing.installedAt ? existing.installedAt : now,
|
|
798
|
+
updatedAt: now,
|
|
1228
799
|
};
|
|
800
|
+
count++;
|
|
1229
801
|
}
|
|
802
|
+
try {
|
|
803
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
804
|
+
writeFileSync(lockPath, JSON.stringify(lock, null, 2));
|
|
805
|
+
return { ok: true, count, path: lockPath };
|
|
806
|
+
} catch (err) {
|
|
807
|
+
return { ok: false, count: 0, path: lockPath, error: err.message };
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Sync commands + skills + hooks + rules + workflows into CLAUDE_DIR.
|
|
813
|
+
* Combines the old syncAgentFiles / syncConfigExtras into one.
|
|
814
|
+
*/
|
|
815
|
+
export async function syncConfigExtras({ dryRun = false } = {}) {
|
|
816
|
+
if (dryRun) return { ok: true, message: '[dry-run] would sync commands + skills + hooks + rules + workflows', counts: { skills: 0, commands: 0, hooks: 0, rules: 0, workflows: 0 } };
|
|
1230
817
|
|
|
1231
818
|
const copyDirIfExists = async (srcDir, dstDir) => {
|
|
1232
819
|
if (!existsSync(srcDir)) return;
|
|
1233
|
-
|
|
820
|
+
ensureDir(dstDir);
|
|
1234
821
|
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
1235
822
|
const src = join(srcDir, entry.name);
|
|
1236
823
|
const dst = join(dstDir, entry.name);
|
|
@@ -1239,995 +826,105 @@ export async function syncConfigExtras({ dryRun }) {
|
|
|
1239
826
|
}
|
|
1240
827
|
};
|
|
1241
828
|
|
|
1242
|
-
// Track counts for the success message.
|
|
1243
829
|
const counts = { skills: 0, commands: 0, hooks: 0, rules: 0, workflows: 0 };
|
|
1244
830
|
|
|
1245
|
-
//
|
|
1246
|
-
// Cline searches `~/.cline/commands/` for command files (.md).
|
|
831
|
+
// Commands
|
|
1247
832
|
const commandsSrc = join(REPO_ROOT, 'config', 'commands');
|
|
1248
833
|
if (existsSync(commandsSrc)) {
|
|
1249
834
|
const commandsDst = join(CLAUDE_DIR, 'commands');
|
|
1250
|
-
|
|
1251
|
-
// One subdir per command (matching Cline's command loading pattern).
|
|
835
|
+
ensureDir(commandsDst);
|
|
1252
836
|
for (const cmd of readdirSync(commandsSrc, { withFileTypes: true })) {
|
|
1253
837
|
if (!cmd.isFile() && !cmd.isDirectory()) continue;
|
|
1254
838
|
const src = join(commandsSrc, cmd.name);
|
|
1255
839
|
const dst = join(commandsDst, cmd.name);
|
|
1256
|
-
if (cmd.isDirectory())
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
copyFileSync(src, dst);
|
|
1260
|
-
}
|
|
1261
|
-
counts.commands += 1;
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
// Legacy: also drop a copy under `commands-bizar/` for back-compat
|
|
1266
|
-
// with anyone referencing the v3.x command tree path.
|
|
1267
|
-
if (existsSync(commandsSrc)) {
|
|
1268
|
-
const commandsBizarDst = join(CLAUDE_DIR, 'commands-bizar');
|
|
1269
|
-
mkdirSync(commandsBizarDst, { recursive: true });
|
|
1270
|
-
for (const cmd of readdirSync(commandsSrc, { withFileTypes: true })) {
|
|
1271
|
-
if (!cmd.isFile()) continue;
|
|
1272
|
-
const src = join(commandsSrc, cmd.name);
|
|
1273
|
-
const dst = join(commandsBizarDst, cmd.name);
|
|
1274
|
-
copyFileSync(src, dst);
|
|
840
|
+
if (cmd.isDirectory()) await copyDirIfExists(src, dst);
|
|
841
|
+
else copyFileSync(src, dst);
|
|
842
|
+
counts.commands++;
|
|
1275
843
|
}
|
|
1276
844
|
}
|
|
1277
845
|
|
|
1278
|
-
//
|
|
1279
|
-
// Cline expects `<configDir>/skills/<name>/SKILL.md`. We sync every
|
|
1280
|
-
// subdir of `config/skills/` (no hardcoded list).
|
|
846
|
+
// Skills
|
|
1281
847
|
const skillsSrc = join(REPO_ROOT, 'config', 'skills');
|
|
1282
848
|
if (existsSync(skillsSrc)) {
|
|
1283
849
|
const skillsDst = join(CLAUDE_DIR, 'skills');
|
|
1284
|
-
|
|
1285
|
-
for (const skillDir of readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
1286
|
-
if (!skillDir.isDirectory()) continue;
|
|
1287
|
-
const src = join(skillsSrc, skillDir.name);
|
|
1288
|
-
const dst = join(skillsDst, skillDir.name);
|
|
1289
|
-
await copyDirIfExists(src, dst);
|
|
1290
|
-
counts.skills += 1;
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
// Also mirror to `~/.agents/skills/<name>/` since Cline checks that
|
|
1295
|
-
// path too (`resolveSkillsConfigSearchPaths`). Optional but cheap.
|
|
1296
|
-
const agentsSkillsDst = join(HOME, '.agents', 'skills');
|
|
1297
|
-
if (existsSync(skillsSrc) && counts.skills > 0) {
|
|
1298
|
-
mkdirSync(agentsSkillsDst, { recursive: true });
|
|
850
|
+
ensureDir(skillsDst);
|
|
1299
851
|
for (const skillDir of readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
1300
852
|
if (!skillDir.isDirectory()) continue;
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
await copyDirIfExists(src, dst);
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
// v6.2.5 - Register every Bizar skill in `~/.agents/.skill-lock.json`
|
|
1308
|
-
// so Cline's marketplace UI (`isMarketplaceSkillInstalled`) actually
|
|
1309
|
-
// shows them. Without this, users see "0 skills" in the Skills tab
|
|
1310
|
-
// even though the files are correctly mirrored. See the
|
|
1311
|
-
// `writeBizarSkillLock` JSDoc for the full rationale.
|
|
1312
|
-
if (existsSync(skillsSrc)) {
|
|
1313
|
-
const skillLock = writeBizarSkillLock({
|
|
1314
|
-
skillsSrc,
|
|
1315
|
-
agentsDir: join(HOME, '.agents'),
|
|
1316
|
-
});
|
|
1317
|
-
if (!skillLock.ok && skillLock.error) {
|
|
1318
|
-
console.warn(`bizar: skill lock write failed: ${skillLock.error}`);
|
|
853
|
+
await copyDirIfExists(join(skillsSrc, skillDir.name), join(skillsDst, skillDir.name));
|
|
854
|
+
counts.skills++;
|
|
1319
855
|
}
|
|
1320
856
|
}
|
|
1321
857
|
|
|
1322
|
-
//
|
|
1323
|
-
|
|
1324
|
-
// `~/.agents/skills/agent-baseline/SKILL.md`. The auto-loaded
|
|
1325
|
-
// `config/agents/_shared/AGENT_BASELINE.md` references skills by
|
|
1326
|
-
// `name: agent-baseline`, which the Cline skill loader resolves
|
|
1327
|
-
// via `~/.cline/skills/agent-baseline/SKILL.md` (and the
|
|
1328
|
-
// `npx skills` lookup uses `~/.agents/skills/...`). Without this
|
|
1329
|
-
// mirror the agent-baseline SKILL.md never appears in the
|
|
1330
|
-
// marketplace / loader, and the baseline fails to attach.
|
|
1331
|
-
const sharedAgentsDir = join(REPO_ROOT, 'config', 'agents', '_shared');
|
|
1332
|
-
if (existsSync(join(sharedAgentsDir, 'AGENT_BASELINE.md'))) {
|
|
1333
|
-
const baselineSkillDir1 = join(CLAUDE_DIR, 'skills', 'agent-baseline');
|
|
1334
|
-
const baselineSkillDir2 = join(HOME, '.agents', 'skills', 'agent-baseline');
|
|
1335
|
-
mkdirSync(baselineSkillDir1, { recursive: true });
|
|
1336
|
-
mkdirSync(baselineSkillDir2, { recursive: true });
|
|
1337
|
-
copyFileSync(
|
|
1338
|
-
join(sharedAgentsDir, 'AGENT_BASELINE.md'),
|
|
1339
|
-
join(baselineSkillDir1, 'SKILL.md'),
|
|
1340
|
-
);
|
|
1341
|
-
copyFileSync(
|
|
1342
|
-
join(sharedAgentsDir, 'AGENT_BASELINE.md'),
|
|
1343
|
-
join(baselineSkillDir2, 'SKILL.md'),
|
|
1344
|
-
);
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
// ── Hooks ────────────────────────────────────────────────────
|
|
1348
|
-
// v6.2.1 — Cline hooks are real executable scripts (PreToolUse,
|
|
1349
|
-
// PostToolUse, TaskStart, TaskResume, UserPromptSubmit). They live in
|
|
1350
|
-
// `~/.cline/hooks/` AND `~/Documents/Cline/Hooks/`. Cline only loads
|
|
1351
|
-
// hooks from the second path (the first is for additional runtime
|
|
1352
|
-
// hook injection via `--hooks-dir`), but we install to BOTH so:
|
|
1353
|
-
// 1. `bizar validate` can verify the hooks are on disk.
|
|
1354
|
-
// 2. Power users who pass `--hooks-dir ~/.cline/hooks` get them.
|
|
1355
|
-
// 3. The official `~/Documents/Cline/Hooks/` location is canonical
|
|
1356
|
-
// and works without any extra config.
|
|
1357
|
-
//
|
|
1358
|
-
// Hooks must be `chmod +x` executable (Cline silently skips
|
|
1359
|
-
// non-executable hook files per the Cline hooks contract).
|
|
1360
|
-
const hooksSrc = join(REPO_ROOT, 'config', 'hooks');
|
|
858
|
+
// Hooks
|
|
859
|
+
const hooksSrc = join(REPO_ROOT, '.claude', 'hooks');
|
|
1361
860
|
if (existsSync(hooksSrc)) {
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
.
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
const hooksDst1 = join(CLAUDE_DIR, 'hooks');
|
|
1369
|
-
mkdirSync(hooksDst1, { recursive: true });
|
|
1370
|
-
for (const hookFile of hookFiles) {
|
|
1371
|
-
const src = join(hooksSrc, hookFile);
|
|
1372
|
-
const dst = join(hooksDst1, hookFile);
|
|
1373
|
-
copyFileSync(src, dst);
|
|
1374
|
-
try { chmodSync(dst, 0o755); } catch { /* best-effort */ }
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// Also install to the canonical location: ~/Documents/Cline/Hooks/
|
|
1378
|
-
// (Cline's default global hooks directory per the official docs)
|
|
1379
|
-
const hooksDst2 = join(HOME, 'Documents', 'Cline', 'Hooks');
|
|
1380
|
-
mkdirSync(hooksDst2, { recursive: true });
|
|
1381
|
-
for (const hookFile of hookFiles) {
|
|
1382
|
-
const src = join(hooksSrc, hookFile);
|
|
1383
|
-
const dst = join(hooksDst2, hookFile);
|
|
1384
|
-
copyFileSync(src, dst);
|
|
1385
|
-
try { chmodSync(dst, 0o755); } catch { /* best-effort */ }
|
|
861
|
+
const hooksDst = join(CLAUDE_DIR, 'hooks');
|
|
862
|
+
ensureDir(hooksDst);
|
|
863
|
+
for (const entry of readdirSync(hooksSrc, { withFileTypes: true })) {
|
|
864
|
+
if (!entry.isFile()) continue;
|
|
865
|
+
copyFileSync(join(hooksSrc, entry.name), join(hooksDst, entry.name));
|
|
866
|
+
try { chmodSync(join(hooksDst, entry.name), 0o755); } catch { /* ignore */ }
|
|
1386
867
|
}
|
|
1387
|
-
|
|
1388
|
-
counts.hooks = hookFiles.length;
|
|
868
|
+
counts.hooks = readdirSync(hooksSrc, { withFileTypes: true }).filter(e => e.isFile()).length;
|
|
1389
869
|
}
|
|
1390
870
|
|
|
1391
|
-
//
|
|
1392
|
-
// Cline reads every `.md` and `.txt` file in `~/.cline/rules/` as a
|
|
1393
|
-
// rule (https://docs.cline.bot/customization/cline-rules). The Bizar
|
|
1394
|
-
// harness ships 7 always-on rule files in `config/rules/` (general,
|
|
1395
|
-
// git, javascript, python, testing, thinking, uncertainty). Copying
|
|
1396
|
-
// them here means `bizar install` and `bizar update` actually wire up
|
|
1397
|
-
// the always-on rules contract; before this change the directory
|
|
1398
|
-
// existed but was empty on a fresh install.
|
|
871
|
+
// Rules
|
|
1399
872
|
const rulesSrc = join(REPO_ROOT, 'config', 'rules');
|
|
1400
873
|
if (existsSync(rulesSrc)) {
|
|
1401
874
|
const rulesDst = join(CLAUDE_DIR, 'rules');
|
|
1402
|
-
|
|
875
|
+
ensureDir(rulesDst);
|
|
1403
876
|
let ruleCount = 0;
|
|
1404
877
|
for (const entry of readdirSync(rulesSrc, { withFileTypes: true })) {
|
|
1405
878
|
if (!entry.isFile()) continue;
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
const dst = join(rulesDst, name);
|
|
1410
|
-
copyFileSync(src, dst);
|
|
1411
|
-
ruleCount += 1;
|
|
879
|
+
if (!entry.name.endsWith('.md') && !entry.name.endsWith('.txt')) continue;
|
|
880
|
+
copyFileSync(join(rulesSrc, entry.name), join(rulesDst, entry.name));
|
|
881
|
+
ruleCount++;
|
|
1412
882
|
}
|
|
1413
883
|
counts.rules = ruleCount;
|
|
1414
884
|
}
|
|
1415
885
|
|
|
1416
|
-
//
|
|
886
|
+
// Workflows
|
|
1417
887
|
const workflowsSrc = join(REPO_ROOT, 'config', 'workflows');
|
|
1418
888
|
if (existsSync(workflowsSrc)) {
|
|
1419
889
|
const workflowsDst = join(CLAUDE_DIR, 'workflows');
|
|
1420
|
-
mkdirSync(workflowsDst, { recursive: true });
|
|
1421
890
|
await copyDirIfExists(workflowsSrc, workflowsDst);
|
|
1422
891
|
counts.workflows = 1;
|
|
1423
892
|
}
|
|
1424
893
|
|
|
1425
|
-
|
|
1426
|
-
// Old versions of this function flattened all skills into one dir.
|
|
1427
|
-
// That caused collisions (each skill wrote SKILL.md at the same
|
|
1428
|
-
// path). Clean up those legacy flat dirs so users on older installs
|
|
1429
|
-
// don't keep seeing the wrong skill.
|
|
1430
|
-
for (const legacy of ['skill']) {
|
|
1431
|
-
const p = join(CLAUDE_DIR, legacy);
|
|
1432
|
-
if (existsSync(p)) {
|
|
1433
|
-
// Only remove if it's flat (no subdirs). Real skill dirs under
|
|
1434
|
-
// skills/<name>/SKILL.md live in `skills/`, not `skill/`.
|
|
1435
|
-
const entries = readdirSync(p, { withFileTypes: true });
|
|
1436
|
-
const onlyFiles = entries.every((e) => e.isFile());
|
|
1437
|
-
if (onlyFiles) {
|
|
1438
|
-
rmSync(p, { recursive: true, force: true });
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
return {
|
|
1444
|
-
ok: true,
|
|
1445
|
-
message:
|
|
1446
|
-
`commands + skills + hooks + rules + workflows synced ` +
|
|
1447
|
-
`(${counts.commands} commands, ${counts.skills} skills, ` +
|
|
1448
|
-
`${counts.hooks} hooks, ${counts.rules} rules, ` +
|
|
1449
|
-
`${counts.workflows} workflows)`,
|
|
1450
|
-
counts,
|
|
1451
|
-
};
|
|
894
|
+
return { ok: true, message: `synced (${counts.commands} commands, ${counts.skills} skills, ${counts.hooks} hooks, ${counts.rules} rules, ${counts.workflows} workflows)`, counts };
|
|
1452
895
|
}
|
|
1453
896
|
|
|
1454
|
-
|
|
1455
|
-
* Run the system-deps + service-registration steps. These are shell-only
|
|
1456
|
-
* (need sudo + platform package manager). We shell to the bundled
|
|
1457
|
-
* install.sh which knows the platform.
|
|
1458
|
-
*/
|
|
1459
|
-
export async function ensureSystemDeps({ dryRun, mode }) {
|
|
1460
|
-
const installSh = join(REPO_ROOT, 'install.sh');
|
|
1461
|
-
if (!existsSync(installSh)) {
|
|
1462
|
-
return {
|
|
1463
|
-
ok: true,
|
|
1464
|
-
message: `install.sh not found at ${installSh} — skipping system deps`,
|
|
1465
|
-
};
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
if (dryRun) {
|
|
1469
|
-
return { ok: true, message: '[dry-run] would shell to install.sh for system deps' };
|
|
1470
|
-
}
|
|
897
|
+
// ─── CLI entry ──────────────────────────────────────────────────────────────
|
|
1471
898
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
if (
|
|
1479
|
-
|
|
899
|
+
function parseFlags(argv) {
|
|
900
|
+
const opts = { mode: 'install', dryRun: false, force: false, yes: false, start: true, update: false };
|
|
901
|
+
for (let i = 0; i < argv.length; i++) {
|
|
902
|
+
const a = argv[i];
|
|
903
|
+
if (a === '--mode=install') opts.mode = 'install';
|
|
904
|
+
else if (a === '--mode=update') opts.mode = 'update';
|
|
905
|
+
else if (a === '--mode=install-only-system') opts.mode = 'install-only-system';
|
|
906
|
+
else if (a === '--mode') {
|
|
907
|
+
const v = argv[++i];
|
|
908
|
+
if (v === 'install' || v === 'update' || v === 'install-only-system') opts.mode = v;
|
|
909
|
+
} else if (a === '--dry-run') opts.dryRun = true;
|
|
910
|
+
else if (a === '--force') opts.force = true;
|
|
911
|
+
else if (a === '--yes' || a === '-y') opts.yes = true;
|
|
912
|
+
else if (a === '--non-interactive') opts.yes = true;
|
|
913
|
+
else if (a === '--no-service') opts.start = false;
|
|
914
|
+
else if (a === '--update') opts.mode = 'update';
|
|
915
|
+
else if (a === '--help' || a === '-h') {
|
|
916
|
+
console.log('Usage: node cli/provision.mjs [--mode=install|update|install-only-system]\n [--dry-run] [--force] [--yes] [--non-interactive] [--no-service]');
|
|
917
|
+
process.exit(0);
|
|
1480
918
|
}
|
|
1481
|
-
return { ok: true, message: 'install.sh completed' };
|
|
1482
919
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
// system deps manually (Windows doesn't need apt/dnf for Node tooling).
|
|
1486
|
-
return {
|
|
1487
|
-
ok: true,
|
|
1488
|
-
message: 'Windows without WSL: skipping system deps (none required for Node tooling)',
|
|
1489
|
-
};
|
|
920
|
+
if (opts.mode === 'update') opts.update = true;
|
|
921
|
+
return opts;
|
|
1490
922
|
}
|
|
1491
923
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
const { runDoctor: doctorFn } = await import('./doctor.mjs');
|
|
1498
|
-
const result = await doctorFn({ silent });
|
|
1499
|
-
return result;
|
|
1500
|
-
} catch (err) {
|
|
1501
|
-
return {
|
|
1502
|
-
ok: false,
|
|
1503
|
-
failed: 1,
|
|
1504
|
-
results: [{ name: 'doctor', ok: false, message: err.message }],
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
/**
|
|
1510
|
-
* Run the post-install smoke test.
|
|
1511
|
-
*/
|
|
1512
|
-
export async function runSmokeTest({ silent = false } = {}) {
|
|
1513
|
-
try {
|
|
1514
|
-
const { runSmokeTest: smokeFn } = await import('./post-install-smoke.mjs');
|
|
1515
|
-
const result = await smokeFn();
|
|
1516
|
-
return result;
|
|
1517
|
-
} catch (err) {
|
|
1518
|
-
return {
|
|
1519
|
-
ok: false,
|
|
1520
|
-
checks: [{ name: 'smoke-test', ok: false, message: err.message }],
|
|
1521
|
-
passed: 0,
|
|
1522
|
-
failed: 1,
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
/**
|
|
1528
|
-
* v5.x — Install LightRAG via uv tool.
|
|
1529
|
-
* Called from the provisioner in case the shell script (install.sh)
|
|
1530
|
-
* was skipped (e.g. Windows without WSL). Idempotent — safe to re-run.
|
|
1531
|
-
* Fails gracefully if uv is not available.
|
|
1532
|
-
*/
|
|
1533
|
-
export async function installLightragProvision({ dryRun = false } = {}) {
|
|
1534
|
-
if (dryRun) {
|
|
1535
|
-
return { ok: true, message: '[dry-run] would run: uv tool install "lightrag-hku[api]"' };
|
|
1536
|
-
}
|
|
1537
|
-
// Check if already installed
|
|
1538
|
-
if (haveCmd('lightrag-server')) {
|
|
1539
|
-
return { ok: true, message: 'lightrag-server already on PATH' };
|
|
1540
|
-
}
|
|
1541
|
-
if (!haveCmd('uv')) {
|
|
1542
|
-
return { ok: false, message: 'uv not found — LightRAG not installed (install uv to enable)' };
|
|
1543
|
-
}
|
|
1544
|
-
try {
|
|
1545
|
-
const r = spawnSync('uv', ['tool', 'install', 'lightrag-hku[api]'], {
|
|
1546
|
-
stdio: 'inherit',
|
|
1547
|
-
timeout: 120_000,
|
|
1548
|
-
});
|
|
1549
|
-
if (r.status === 0 || r.status === null) {
|
|
1550
|
-
return { ok: true, message: 'lightrag-hku[api] installed via uv' };
|
|
1551
|
-
}
|
|
1552
|
-
return { ok: false, message: `uv tool install failed (exit ${r.status})` };
|
|
1553
|
-
} catch (err) {
|
|
1554
|
-
return { ok: false, message: `lightrag install error: ${err.message}` };
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
/**
|
|
1559
|
-
* v6.0.0 — Ensure `agent-browser` (native Rust CLI from vercel-labs) is
|
|
1560
|
-
* installed and the daemon is up. agent-browser replaces the v5.x
|
|
1561
|
-
* `browser-harness` (Python CDP wrapper). The CLI is installed via
|
|
1562
|
-
* `npm install -g agent-browser`; Chrome for Testing is downloaded by
|
|
1563
|
-
* `agent-browser install`; the daemon is started by
|
|
1564
|
-
* `agent-browser serve --headless --port <port> --profile <dir>`.
|
|
1565
|
-
*
|
|
1566
|
-
* Idempotent. Returns { ok, message, version?, daemonRunning? }.
|
|
1567
|
-
*/
|
|
1568
|
-
export async function ensureAgentBrowser({ dryRun = false, mode = 'install' } = {}) {
|
|
1569
|
-
const ab = await import('./agent-browser-update.mjs');
|
|
1570
|
-
const before = ab.detectState();
|
|
1571
|
-
if (dryRun) {
|
|
1572
|
-
return {
|
|
1573
|
-
ok: true,
|
|
1574
|
-
message: `[dry-run] agent-browser: ${before.installed ? 'installed' : 'not installed'}` +
|
|
1575
|
-
(before.installed ? ` (v${before.version})` : ''),
|
|
1576
|
-
version: before.version,
|
|
1577
|
-
daemonRunning: before.daemonRunning,
|
|
1578
|
-
};
|
|
1579
|
-
}
|
|
1580
|
-
try {
|
|
1581
|
-
const after = mode === 'update' && before.installed
|
|
1582
|
-
? ab.update({ silent: true, startDaemon: true })
|
|
1583
|
-
: ab.install({ silent: true, startDaemon: true });
|
|
1584
|
-
return {
|
|
1585
|
-
ok: true,
|
|
1586
|
-
message: `agent-browser: ${after.installed ? `v${after.version}` : 'install attempted'}` +
|
|
1587
|
-
` · daemon: ${after.daemonRunning ? 'running' : 'stopped'}`,
|
|
1588
|
-
version: after.version,
|
|
1589
|
-
daemonRunning: after.daemonRunning,
|
|
1590
|
-
};
|
|
1591
|
-
} catch (err) {
|
|
1592
|
-
return { ok: false, message: `agent-browser install failed: ${err.message}` };
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
/**
|
|
1597
|
-
* v4.4.11 — The mods step. By default, NEVER install or upgrade mods
|
|
1598
|
-
* during `bizar install` or `bizar update`. The step just reports the
|
|
1599
|
-
* current mod list so the user can see what's installed.
|
|
1600
|
-
*
|
|
1601
|
-
* To install a mod as part of the run, the user must opt in by:
|
|
1602
|
-
* - passing `--with-mods <id1,id2>` to `bizar install` / `bizar update`
|
|
1603
|
-
* - or setting the env var `BIZAR_MODS_AUTO_INSTALL=allow`
|
|
1604
|
-
*
|
|
1605
|
-
* When opted in, we shell to the running dashboard's `/api/mods`
|
|
1606
|
-
* endpoint (which already validates the mod) — we don't duplicate the
|
|
1607
|
-
* install logic here.
|
|
1608
|
-
*/
|
|
1609
|
-
export async function runModsStep({ mode, dryRun, force, withMods, state }) {
|
|
1610
|
-
// List the current mods (from the state we already detected).
|
|
1611
|
-
const installed = state?.installedMods ?? [];
|
|
1612
|
-
if (installed.length === 0) {
|
|
1613
|
-
console.log(chalk.dim(' Mods: 0 installed.'));
|
|
1614
|
-
return { ok: true, message: 'no mods installed', touched: false };
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
const enabled = installed.filter((m) => m.enabled).length;
|
|
1618
|
-
const disabled = installed.length - enabled;
|
|
1619
|
-
const summary = `${installed.length} installed (${enabled} enabled${disabled ? `, ${disabled} disabled` : ''})`;
|
|
1620
|
-
|
|
1621
|
-
// Resolve which mods the user asked us to install. The CLI parses
|
|
1622
|
-
// `--with-mods a,b,c` into a string array; the provisioner accepts
|
|
1623
|
-
// the same. We also accept a single env var as a comma-separated list.
|
|
1624
|
-
const envList = (process.env.BIZAR_MODS_AUTO_INSTALL || '')
|
|
1625
|
-
.split(',')
|
|
1626
|
-
.map((s) => s.trim())
|
|
1627
|
-
.filter(Boolean);
|
|
1628
|
-
const wantedMods = Array.isArray(withMods) && withMods.length > 0
|
|
1629
|
-
? withMods
|
|
1630
|
-
: (process.env.BIZAR_MODS_AUTO_INSTALL === 'allow' ? [] : envList);
|
|
1631
|
-
|
|
1632
|
-
// Default behavior: no install. Just print the list.
|
|
1633
|
-
if (!wantedMods || wantedMods.length === 0) {
|
|
1634
|
-
console.log(chalk.dim(` Mods: ${summary}. Not modified (use \`bizar mod install <id>\` to add one).`));
|
|
1635
|
-
// Surface mods with errors so the user knows they need attention.
|
|
1636
|
-
const broken = installed.filter((m) => m.error);
|
|
1637
|
-
for (const m of broken) {
|
|
1638
|
-
console.log(chalk.yellow(` ⚠ ${m.id}: ${m.error}`));
|
|
1639
|
-
}
|
|
1640
|
-
return { ok: true, message: `${summary}, not modified`, touched: false };
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
// Opt-in install path. Talk to the dashboard over HTTP — the
|
|
1644
|
-
// dashboard's `POST /api/mods` endpoint already does the actual
|
|
1645
|
-
// install + validation. If the dashboard isn't reachable, the
|
|
1646
|
-
// install fails loudly.
|
|
1647
|
-
console.log(chalk.cyan(` Installing ${wantedMods.length} mod(s) via dashboard API: ${wantedMods.join(', ')}`));
|
|
1648
|
-
const errors = [];
|
|
1649
|
-
for (const id of wantedMods) {
|
|
1650
|
-
try {
|
|
1651
|
-
const result = await installModViaDashboard(id, { dryRun });
|
|
1652
|
-
if (result.ok) {
|
|
1653
|
-
console.log(chalk.green(` ✓ ${id}: ${result.message}`));
|
|
1654
|
-
} else {
|
|
1655
|
-
console.log(chalk.red(` ✗ ${id}: ${result.message}`));
|
|
1656
|
-
errors.push(`${id}: ${result.message}`);
|
|
1657
|
-
}
|
|
1658
|
-
} catch (err) {
|
|
1659
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1660
|
-
console.log(chalk.red(` ✗ ${id}: ${msg}`));
|
|
1661
|
-
errors.push(`${id}: ${msg}`);
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
if (errors.length > 0) {
|
|
1665
|
-
return {
|
|
1666
|
-
ok: false,
|
|
1667
|
-
message: `mod install failed for ${errors.length} mod(s): ${errors.join('; ')}`,
|
|
1668
|
-
touched: true,
|
|
1669
|
-
};
|
|
1670
|
-
}
|
|
1671
|
-
return { ok: true, message: `${wantedMods.length} mod(s) installed`, touched: true };
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
/**
|
|
1675
|
-
* v4.4.11 — POST to the dashboard's /api/mods endpoint to install a mod.
|
|
1676
|
-
* We re-use the dashboard's own validation pipeline (mod-loader.mjs
|
|
1677
|
-
* runs the same manifest + route.mjs + permissions checks) so we
|
|
1678
|
-
* don't have to duplicate them here.
|
|
1679
|
-
*/
|
|
1680
|
-
async function installModViaDashboard(id, { dryRun }) {
|
|
1681
|
-
if (dryRun) {
|
|
1682
|
-
return { ok: true, message: '[dry-run] would install via dashboard' };
|
|
1683
|
-
}
|
|
1684
|
-
// Find the dashboard's port from BIZAR_HOME/dashboard.port.
|
|
1685
|
-
const portFile = join(BIZAR_HOME, 'dashboard.port');
|
|
1686
|
-
let port = 4321;
|
|
1687
|
-
try {
|
|
1688
|
-
if (existsSync(portFile)) {
|
|
1689
|
-
const parsed = parseInt(readTextSafe(portFile, '4321').trim(), 10);
|
|
1690
|
-
if (Number.isFinite(parsed) && parsed > 0) port = parsed;
|
|
1691
|
-
}
|
|
1692
|
-
} catch { /* ignore */ }
|
|
1693
|
-
// We need an auth token. The dashboard's install endpoint is under
|
|
1694
|
-
// /api/* which is auth-gated. Fetch the auth status + token via
|
|
1695
|
-
// /api/auth/status (skipped from auth via the skipPaths list in
|
|
1696
|
-
// server.mjs). If the dashboard has auth enabled, the user needs to
|
|
1697
|
-
// supply a token via BIZAR_DASHBOARD_TOKEN.
|
|
1698
|
-
const headers = { 'content-type': 'application/json' };
|
|
1699
|
-
const token = process.env.BIZAR_DASHBOARD_TOKEN;
|
|
1700
|
-
if (token) headers['authorization'] = `Basic ${Buffer.from(`cline:${token}`).toString('base64')}`;
|
|
1701
|
-
const url = `http://127.0.0.1:${port}/api/mods`;
|
|
1702
|
-
const res = await fetch(url, {
|
|
1703
|
-
method: 'POST',
|
|
1704
|
-
headers,
|
|
1705
|
-
body: JSON.stringify({ id }),
|
|
924
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
925
|
+
const opts = parseFlags(process.argv.slice(2));
|
|
926
|
+
runProvision(opts).catch((err) => {
|
|
927
|
+
logErr(`provision failed: ${err?.message || err}`);
|
|
928
|
+
process.exit(1);
|
|
1706
929
|
});
|
|
1707
|
-
if (!res.ok) {
|
|
1708
|
-
const text = await res.text().catch(() => '');
|
|
1709
|
-
return { ok: false, message: `dashboard returned ${res.status}: ${text.slice(0, 200) || '(no body)'}` };
|
|
1710
|
-
}
|
|
1711
|
-
const data = await res.json().catch(() => ({}));
|
|
1712
|
-
return { ok: true, message: data?.name ? `installed ${data.name}@${data.version}` : 'installed' };
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
// ─── Top-level orchestration ──────────────────────────────────────────────────
|
|
1716
|
-
|
|
1717
|
-
/**
|
|
1718
|
-
* Kill running Bizar instances before mutating npm-global files. Returns
|
|
1719
|
-
* the kill results so the caller can report them.
|
|
1720
|
-
*/
|
|
1721
|
-
export async function ensureInstancesKilled({ dryRun, force, instances }) {
|
|
1722
|
-
const live = instances ?? detectInstances();
|
|
1723
|
-
const running = [];
|
|
1724
|
-
if (live.service?.running) running.push({ kind: 'service', pid: live.service.pid });
|
|
1725
|
-
if (live.dashboard?.running) running.push({ kind: 'dashboard', pid: live.dashboard.pid });
|
|
1726
|
-
|
|
1727
|
-
if (running.length === 0) {
|
|
1728
|
-
return { killed: [], skipped: [] };
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
if (dryRun) {
|
|
1732
|
-
return {
|
|
1733
|
-
killed: running.map((r) => ({ ...r, ok: true, dryRun: true })),
|
|
1734
|
-
skipped: [],
|
|
1735
|
-
};
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
// Always kill (the npm update will replace on-disk files; running
|
|
1739
|
-
// processes have those files open). `--force` skips the prompt.
|
|
1740
|
-
// We don't prompt here — the caller (runProvision) is expected to
|
|
1741
|
-
// confirm before calling this function. If we ever want to make
|
|
1742
|
-
// it interactive, plumb `assumeYes` through here.
|
|
1743
|
-
void force;
|
|
1744
|
-
const killed = [];
|
|
1745
|
-
for (const r of running) {
|
|
1746
|
-
const label = r.kind === 'service' ? 'bizar service' : 'bizar-dash';
|
|
1747
|
-
const ok = await killAndWait(r.pid, { label });
|
|
1748
|
-
killed.push({ ...r, ok });
|
|
1749
|
-
if (ok) {
|
|
1750
|
-
try { rmSync(r.kind === 'service' ? SERVICE_PID_FILE : DASHBOARD_PID_FILE, { force: true }); } catch { /* ignore */ }
|
|
1751
|
-
if (r.kind === 'dashboard') {
|
|
1752
|
-
try { rmSync(DASHBOARD_PORT_FILE, { force: true }); } catch { /* ignore */ }
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
return { killed, skipped: [] };
|
|
1757
930
|
}
|
|
1758
|
-
|
|
1759
|
-
/**
|
|
1760
|
-
* Spawn a fresh dashboard detached. Used at the end of an `update` flow
|
|
1761
|
-
* to pick up the just-upgraded code.
|
|
1762
|
-
*/
|
|
1763
|
-
export function spawnFreshDashboard({ port } = {}) {
|
|
1764
|
-
const state = detectState();
|
|
1765
|
-
if (!state.pkgRoot) {
|
|
1766
|
-
return { ok: false, message: 'could not locate npm global root' };
|
|
1767
|
-
}
|
|
1768
|
-
const dashBin = join(state.pkgRoot, 'cli', 'bin.mjs');
|
|
1769
|
-
if (!existsSync(dashBin)) {
|
|
1770
|
-
return { ok: false, message: `dashboard binary not found at ${dashBin}` };
|
|
1771
|
-
}
|
|
1772
|
-
try {
|
|
1773
|
-
mkdirSync(BIZAR_HOME, { recursive: true });
|
|
1774
|
-
} catch { /* ignore */ }
|
|
1775
|
-
const args = [dashBin, 'start', '--bg'];
|
|
1776
|
-
if (port) args.push(`--port=${port}`);
|
|
1777
|
-
try {
|
|
1778
|
-
const child = spawn(process.execPath, args, {
|
|
1779
|
-
detached: true,
|
|
1780
|
-
stdio: 'ignore',
|
|
1781
|
-
env: { ...process.env, BIZAR_AUTO_RESPAWN: '1' },
|
|
1782
|
-
});
|
|
1783
|
-
child.on('error', () => { /* ignore */ });
|
|
1784
|
-
child.unref();
|
|
1785
|
-
return { ok: true, message: `dashboard re-spawned (pid ${child.pid})` };
|
|
1786
|
-
} catch (err) {
|
|
1787
|
-
return { ok: false, message: `dashboard re-spawn failed: ${err.message}` };
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
/**
|
|
1792
|
-
* The unified provision flow. `mode` is 'install' or 'update'.
|
|
1793
|
-
*
|
|
1794
|
-
* Steps performed:
|
|
1795
|
-
* 1. Detect state (no side effects).
|
|
1796
|
-
* 2. (update only) Show installed-vs-latest version matrix.
|
|
1797
|
-
* 3. (update only) Heads-up gate.
|
|
1798
|
-
* 4. Kill running instances (update only — installs have none).
|
|
1799
|
-
* 5. Upgrade npm packages (bizar + cline).
|
|
1800
|
-
* 6. Shell to install.sh for system-deps + service registration.
|
|
1801
|
-
* 7. Sync agent files, slash commands, skills, hooks, workflows.
|
|
1802
|
-
* 8. Copy plugin to ${CLAUDE_DIR}/plugins/bizar/.
|
|
1803
|
-
* 9. Patch cline.json with the Bizar plugin entry.
|
|
1804
|
-
* 10. (update only) Restart dashboard.
|
|
1805
|
-
* 11. Doctor health check.
|
|
1806
|
-
* 12. Summary.
|
|
1807
|
-
*
|
|
1808
|
-
* Every step is idempotent — running this twice is safe.
|
|
1809
|
-
*/
|
|
1810
|
-
export async function runProvision(opts = {}) {
|
|
1811
|
-
const {
|
|
1812
|
-
mode = 'install',
|
|
1813
|
-
dryRun = false,
|
|
1814
|
-
force = false,
|
|
1815
|
-
restart = mode === 'update',
|
|
1816
|
-
skipSystemDeps = false,
|
|
1817
|
-
skipHeadsUps = false,
|
|
1818
|
-
yes = false,
|
|
1819
|
-
withMods = null, // string[] — opt-in mod install. Default null = don't touch mods.
|
|
1820
|
-
} = opts;
|
|
1821
|
-
|
|
1822
|
-
const banner = mode === 'update'
|
|
1823
|
-
? chalk.bold.hex('#a855f7')('\n ᚦ BIZAR UPDATE ᚦ\n')
|
|
1824
|
-
: chalk.bold.hex('#6366f1')('\n ⚡ BIZAR INSTALL ᚦ\n');
|
|
1825
|
-
console.log(banner);
|
|
1826
|
-
if (dryRun) {
|
|
1827
|
-
console.log(chalk.dim(' --dry-run set: no installs, kills, or restarts will be performed.\n'));
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
// ── 0.5. Idempotency: detect existing install ─────────────────────────
|
|
1831
|
-
// v5.x — If a marker file exists and we're in 'install' mode (not 'update'),
|
|
1832
|
-
// treat this as a re-install. In non-interactive mode, auto-upgrade.
|
|
1833
|
-
// In interactive mode, prompt the user.
|
|
1834
|
-
const marker = readInstallMarker();
|
|
1835
|
-
if (mode === 'install' && marker && !dryRun) {
|
|
1836
|
-
if (yes) {
|
|
1837
|
-
// Non-interactive: auto-switch to update mode
|
|
1838
|
-
console.log(chalk.yellow(' ⚠ Existing install detected (marker found).'));
|
|
1839
|
-
console.log(chalk.yellow(' Auto-switching to update mode due to --yes flag.'));
|
|
1840
|
-
console.log('');
|
|
1841
|
-
// Re-call runProvision with update mode
|
|
1842
|
-
return runProvision({ ...opts, mode: 'update' });
|
|
1843
|
-
} else {
|
|
1844
|
-
// Interactive: warn and continue
|
|
1845
|
-
console.log(chalk.yellow(' ⚠ Existing install detected (marker found at ~/.config/bizar/installed.json).'));
|
|
1846
|
-
console.log(chalk.yellow(` Last installed: ${marker.installedAt || 'unknown'}`));
|
|
1847
|
-
console.log(chalk.yellow(' To update an existing install, use `bizar update` or re-run with --update.'));
|
|
1848
|
-
console.log('');
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
// ── 1. Detect state ──────────────────────────────────────────────────
|
|
1853
|
-
const state = detectState();
|
|
1854
|
-
|
|
1855
|
-
// ── 2. (update) Version matrix ───────────────────────────────────────
|
|
1856
|
-
if (mode === 'update') {
|
|
1857
|
-
console.log(' Installed vs. latest:');
|
|
1858
|
-
printVersionMatrix([
|
|
1859
|
-
{
|
|
1860
|
-
label: '@anthropic-ai/claude-code',
|
|
1861
|
-
current: state.claudeCli.version,
|
|
1862
|
-
latest: state.claudeCli.latest,
|
|
1863
|
-
},
|
|
1864
|
-
{ label: PKG_MAIN, current: state.pkgVersion, latest: state.pkgLatest },
|
|
1865
|
-
]);
|
|
1866
|
-
console.log('');
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
// ── 3. (update) Heads-up gate ────────────────────────────────────────
|
|
1870
|
-
if (mode === 'update' && !skipHeadsUps) {
|
|
1871
|
-
const h = state.headsUpState;
|
|
1872
|
-
if (h.blockerCount > 0) {
|
|
1873
|
-
if (dryRun) {
|
|
1874
|
-
console.log(chalk.yellow(` Dry-run: ${h.blockerCount} active blocker(s) — update would be blocked.`));
|
|
1875
|
-
} else if (yes || force) {
|
|
1876
|
-
console.log(chalk.yellow(` ⚠ ${h.blockerCount} active blocker(s) present — proceeding due to ${yes ? '--yes' : '--force'}.`));
|
|
1877
|
-
} else {
|
|
1878
|
-
console.error(chalk.red(` ✗ ${h.blockerCount} active blocker(s) in .bizar/PRE_PUSH_NOTES.md`));
|
|
1879
|
-
console.error(chalk.dim(' Archive with `bizar heads-up archive` or override with --force.'));
|
|
1880
|
-
process.exit(1);
|
|
1881
|
-
}
|
|
1882
|
-
} else if (h.warningCount > 0) {
|
|
1883
|
-
console.log(chalk.yellow(` ⚠ ${h.warningCount} warning(s) in active heads-ups.`));
|
|
1884
|
-
} else {
|
|
1885
|
-
console.log(chalk.green(' ✓ Heads-ups: clear'));
|
|
1886
|
-
}
|
|
1887
|
-
console.log('');
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
// ── 4. Kill running instances (update only) ─────────────────────────
|
|
1891
|
-
if (mode === 'update') {
|
|
1892
|
-
const kill = await ensureInstancesKilled({ dryRun, force, instances: {
|
|
1893
|
-
service: state.service.running ? { pid: state.service.pid } : null,
|
|
1894
|
-
dashboard: state.dashboard.running ? { pid: state.dashboard.pid } : null,
|
|
1895
|
-
} });
|
|
1896
|
-
if (kill.killed.length > 0) {
|
|
1897
|
-
const labels = kill.killed.map((k) => `${k.kind}${dryRun ? ' (dry-run)' : ''}`).join(', ');
|
|
1898
|
-
console.log(chalk.yellow(` ⚠ Stopped running instances: ${labels}`));
|
|
1899
|
-
for (const k of kill.killed) {
|
|
1900
|
-
const marker = k.ok ? chalk.green('✓') : chalk.red('✗');
|
|
1901
|
-
console.log(` ${marker} ${k.kind} ${k.ok ? 'stopped' : 'kill failed'}`);
|
|
1902
|
-
}
|
|
1903
|
-
// Give the kernel a moment to release file handles.
|
|
1904
|
-
if (!dryRun) await new Promise((r) => setTimeout(r, 500));
|
|
1905
|
-
} else {
|
|
1906
|
-
console.log(chalk.dim(' No running Bizar instances detected.'));
|
|
1907
|
-
}
|
|
1908
|
-
console.log('');
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
// ── 5. npm package upgrades ─────────────────────────────────────────
|
|
1912
|
-
const stepResults = [];
|
|
1913
|
-
const runStep = async (label, fn) => {
|
|
1914
|
-
console.log(chalk.bold(` → ${label}`));
|
|
1915
|
-
const r = await fn();
|
|
1916
|
-
console.log(` ${r.ok ? chalk.green('✓') : chalk.red('✗')} ${r.message}`);
|
|
1917
|
-
stepResults.push({ label, ...r });
|
|
1918
|
-
return r;
|
|
1919
|
-
};
|
|
1920
|
-
|
|
1921
|
-
if (mode === 'update') {
|
|
1922
|
-
await runStep('@anthropic-ai/claude-code', () =>
|
|
1923
|
-
updateClaudeCodeCli({ dryRun, force }),
|
|
1924
|
-
);
|
|
1925
|
-
}
|
|
1926
|
-
await runStep(PKG_MAIN, () => ensureNpmPackage(PKG_MAIN, { mode, dryRun, force }));
|
|
1927
|
-
|
|
1928
|
-
// ── 6. System deps + service registration (shell) ──────────────────
|
|
1929
|
-
if (!skipSystemDeps) {
|
|
1930
|
-
console.log('');
|
|
1931
|
-
await runStep('system-deps + service', () => ensureSystemDeps({ dryRun, mode }));
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
// ── 6.5. LightRAG install (Node-side, in case shell script skipped) ─
|
|
1935
|
-
await runStep('lightrag-server', () => installLightragProvision({ dryRun }));
|
|
1936
|
-
|
|
1937
|
-
// ── 7-9. Sync agent files, commands, skills, plugin, settings.json ─
|
|
1938
|
-
console.log('');
|
|
1939
|
-
await runStep('agent files + slash commands + skills', () =>
|
|
1940
|
-
Promise.all([
|
|
1941
|
-
syncAgentFiles({ dryRun, force }),
|
|
1942
|
-
syncConfigExtras({ dryRun }),
|
|
1943
|
-
]).then((results) => {
|
|
1944
|
-
const allOk = results.every((r) => r.ok);
|
|
1945
|
-
const msgs = results.map((r) => r.message).join('; ');
|
|
1946
|
-
return { ok: allOk, message: msgs || 'synced' };
|
|
1947
|
-
}),
|
|
1948
|
-
);
|
|
1949
|
-
await runStep('settings.json Bizar entry', () =>
|
|
1950
|
-
patchClaudeSettings({ dryRun, force }),
|
|
1951
|
-
);
|
|
1952
|
-
|
|
1953
|
-
// ── 9.5. v5.x — issue #7. Restart the system service so it picks up
|
|
1954
|
-
// the freshly-installed binary. We do this AFTER the npm upgrade
|
|
1955
|
-
// (so the new code is on disk) and AFTER the file sync (so the
|
|
1956
|
-
// service won't try to read stale files on boot). The service has
|
|
1957
|
-
// already been killed in step 4 (mode==='update' branch).
|
|
1958
|
-
// This is also called on first install: if the service was registered
|
|
1959
|
-
// before the npm upgrade, restart is a no-op (unit content matches
|
|
1960
|
-
// and the service is already running with the old code).
|
|
1961
|
-
if (mode === 'update' && !dryRun) {
|
|
1962
|
-
try {
|
|
1963
|
-
const { restartService } = await import('./service-controller.mjs');
|
|
1964
|
-
const restart = restartService({ force: false, dryRun: false });
|
|
1965
|
-
if (restart.ok) {
|
|
1966
|
-
console.log(chalk.green(' ✓ Background service restarted with new code.'));
|
|
1967
|
-
stepResults.push({ label: 'service-restart', ok: true, message: 'restarted' });
|
|
1968
|
-
} else {
|
|
1969
|
-
// Non-fatal: the service may have been killed but failed to
|
|
1970
|
-
// restart, or it may not have been installed. The OS will
|
|
1971
|
-
// attempt to start it on next login either way.
|
|
1972
|
-
console.log(chalk.yellow(` ⚠ Service restart: ${restart.error || 'unknown'}`));
|
|
1973
|
-
stepResults.push({ label: 'service-restart', ok: false, message: restart.error || 'restart failed' });
|
|
1974
|
-
}
|
|
1975
|
-
} catch (err) {
|
|
1976
|
-
console.log(chalk.yellow(` ⚠ Service restart skipped: ${err?.message || err}`));
|
|
1977
|
-
stepResults.push({ label: 'service-restart', ok: false, message: err?.message || 'skipped' });
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
// ── 10. (update) Restart dashboard ──────────────────────────────────
|
|
1982
|
-
if (mode === 'update' && restart && state.dashboard.running) {
|
|
1983
|
-
console.log('');
|
|
1984
|
-
console.log(chalk.cyan(' Restarting dashboard with the new code...'));
|
|
1985
|
-
const r = spawnFreshDashboard({ port: state.dashboard.port });
|
|
1986
|
-
console.log(` ${r.ok ? chalk.green('✓') : chalk.yellow('⚠')} ${r.message}`);
|
|
1987
|
-
stepResults.push({ label: 'dashboard-restart', ...r });
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
// ── 11. Mods (opt-in only) ─────────────────────────────────────────
|
|
1991
|
-
// v4.4.11 — `bizar install` and `bizar update` never install or
|
|
1992
|
-
// upgrade mods by default. The provisioner reports the current mod
|
|
1993
|
-
// list so the user can see what's installed, then exits the mod step
|
|
1994
|
-
// without touching anything. To install a mod, pass
|
|
1995
|
-
// `--with-mods <id1,id2>` or set `BIZAR_MODS_AUTO_INSTALL=allow`.
|
|
1996
|
-
console.log('');
|
|
1997
|
-
const modsStep = await runModsStep({ mode, dryRun, force, withMods, state });
|
|
1998
|
-
stepResults.push({ label: 'mods', ...modsStep });
|
|
1999
|
-
|
|
2000
|
-
// ── 11.5. Write install marker (v5.x idempotency) ──────────────────
|
|
2001
|
-
if (!dryRun) {
|
|
2002
|
-
const markerWrite = writeInstallMarker({
|
|
2003
|
-
version: state.pkgVersion || null,
|
|
2004
|
-
repoPath: REPO_ROOT,
|
|
2005
|
-
serviceUnit: state.service.unitPath,
|
|
2006
|
-
});
|
|
2007
|
-
if (markerWrite.ok) {
|
|
2008
|
-
console.log(chalk.dim(' ✓ install marker updated'));
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
// ── 12. Doctor health check ───────────────────────────────────────
|
|
2013
|
-
console.log('');
|
|
2014
|
-
const doctor = await runDoctor({ silent: true });
|
|
2015
|
-
if (doctor.failed > 0) {
|
|
2016
|
-
console.log(chalk.yellow(` ⚠ Post-${mode} health check found ${doctor.failed} issue(s):`));
|
|
2017
|
-
for (const r of (doctor.results || [])) {
|
|
2018
|
-
if (!r.ok) {
|
|
2019
|
-
console.log(chalk.red(` ✗ ${r.name}: ${r.message}`));
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
console.log(chalk.dim(` Run \`bizar doctor\` for details.`));
|
|
2023
|
-
} else {
|
|
2024
|
-
console.log(chalk.green(' ✓ Doctor: all checks passed'));
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
// ── 12.5. Smoke test ──────────────────────────────────────────────
|
|
2028
|
-
console.log('');
|
|
2029
|
-
console.log(chalk.bold(' → Smoke test'));
|
|
2030
|
-
const smoke = await runSmokeTest({ silent: false });
|
|
2031
|
-
stepResults.push({ label: 'smoke-test', ok: smoke.ok, message: `${smoke.passed} passed, ${smoke.failed} failed` });
|
|
2032
|
-
|
|
2033
|
-
// ── 12. Summary ─────────────────────────────────────────────────────
|
|
2034
|
-
console.log('');
|
|
2035
|
-
console.log(' Summary:');
|
|
2036
|
-
for (const r of stepResults) {
|
|
2037
|
-
const marker = r.ok ? chalk.green('✓') : chalk.red('✗');
|
|
2038
|
-
console.log(` ${marker} ${r.label.padEnd(36)} ${r.message}`);
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
const anyFail = stepResults.some((r) => !r.ok);
|
|
2042
|
-
if (anyFail) {
|
|
2043
|
-
console.log(chalk.yellow('\n Some steps had issues. See messages above.'));
|
|
2044
|
-
} else {
|
|
2045
|
-
console.log(chalk.green(`\n ✓ ${mode === 'update' ? 'Update' : 'Install'} complete\n`));
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
// ── 13. Provider bootstrap warning ───────────────────────────────
|
|
2049
|
-
// v6.3.0 — The installer no longer configures a provider. The user
|
|
2050
|
-
// must add their own. After a successful install, surface a clear
|
|
2051
|
-
// hint pointing them at Claude Code's settings.json.
|
|
2052
|
-
if (mode === 'install' && !dryRun && !anyFail) {
|
|
2053
|
-
const cfgPath = join(CLAUDE_DIR, 'settings.json');
|
|
2054
|
-
const cfg = readJsonSafe(cfgPath, null);
|
|
2055
|
-
// Claude Code doesn't have a "provider" config block; providers
|
|
2056
|
-
// are configured in `env` (per-session overrides) or via the
|
|
2057
|
-
// `claude` CLI's `/config` command. We only check for the env
|
|
2058
|
-
// presence here.
|
|
2059
|
-
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
|
|
2060
|
-
if (!hasAnthropicKey) {
|
|
2061
|
-
console.log(chalk.yellow(' ⚠ No ANTHROPIC_API_KEY in your environment.'));
|
|
2062
|
-
console.log(chalk.yellow(' `bizar install` no longer touches provider config (v6.2.2+).'));
|
|
2063
|
-
console.log(chalk.yellow(' You must set one yourself — see the "Provider setup" section below.'));
|
|
2064
|
-
console.log('');
|
|
2065
|
-
console.log(chalk.cyan(' ┌─ Provider setup ─────────────────────────────────────────────┐'));
|
|
2066
|
-
console.log(chalk.cyan(' │ 1. Export ANTHROPIC_API_KEY in your shell, or: │'));
|
|
2067
|
-
console.log(chalk.cyan(' │ 2. Run `claude /login` to authenticate via OAuth. │'));
|
|
2068
|
-
console.log(chalk.cyan(' │ 3. Or run `bizar connect` for an interactive TUI setup. │'));
|
|
2069
|
-
console.log(chalk.cyan(' └──────────────────────────────────────────────────────────────┘'));
|
|
2070
|
-
console.log('');
|
|
2071
|
-
console.log(chalk.dim(' Models available at http://localhost:20128/v1/models:'));
|
|
2072
|
-
console.log(chalk.dim(' minimax/MiniMax-M3, minimax/MiniMax-M2.7, nvidia/minimaxai/minimax-m3,'));
|
|
2073
|
-
console.log(chalk.dim(' nvidia/z-ai/glm-5.2, nvidia/deepseek-ai/deepseek-v4-pro, etc.'));
|
|
2074
|
-
console.log('');
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
return {
|
|
2079
|
-
ok: !anyFail,
|
|
2080
|
-
mode,
|
|
2081
|
-
state,
|
|
2082
|
-
stepResults,
|
|
2083
|
-
doctor,
|
|
2084
|
-
};
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
/**
|
|
2088
|
-
* CLI-flag-parsing entrypoint for `bizar update`. Kept here so that
|
|
2089
|
-
* `cli/update.mjs` (the bin.mjs-facing module) can stay a thin shim
|
|
2090
|
-
* without duplicating flag parsing. Accepts the legacy `subargs: string[]`
|
|
2091
|
-
* shape so any external callers keep working.
|
|
2092
|
-
*
|
|
2093
|
-
* Recognized flags (v4.4.14):
|
|
2094
|
-
* --check Only print current vs. latest, don't update.
|
|
2095
|
-
* --channel <name> npm dist-tag (stable | beta). Default: stable.
|
|
2096
|
-
* --no-restart Don't auto-restart the dashboard after update.
|
|
2097
|
-
* --dry-run Print what would happen, change nothing.
|
|
2098
|
-
* --force Override .bizar/PRE_PUSH_NOTES.md blockers.
|
|
2099
|
-
* --yes / -y Same as --force, but named for one-line scripts.
|
|
2100
|
-
* --with-mods <csv> Opt-in: install specific mods as part of the run.
|
|
2101
|
-
*
|
|
2102
|
-
* With --check, we print the version matrix and release notes between
|
|
2103
|
-
* current and latest, then return without touching the system.
|
|
2104
|
-
*/
|
|
2105
|
-
export async function runUpdate(subargs = []) {
|
|
2106
|
-
const args = Array.isArray(subargs) ? subargs : [];
|
|
2107
|
-
const checkOnly = args.includes('--check');
|
|
2108
|
-
|
|
2109
|
-
// --channel <name> — extract a single value. Accepts both
|
|
2110
|
-
// --channel=beta (equals form)
|
|
2111
|
-
// --channel beta (separate-arg form)
|
|
2112
|
-
// Default: 'stable'.
|
|
2113
|
-
let channel = 'stable';
|
|
2114
|
-
const eqArg = args.find((a) => a.startsWith('--channel='));
|
|
2115
|
-
if (eqArg) {
|
|
2116
|
-
const value = eqArg.slice('--channel='.length);
|
|
2117
|
-
if (value === 'stable' || value === 'beta') {
|
|
2118
|
-
channel = value;
|
|
2119
|
-
} else {
|
|
2120
|
-
console.log(chalk.yellow(` ⚠ Unknown channel "${value}". Using "stable".`));
|
|
2121
|
-
}
|
|
2122
|
-
} else {
|
|
2123
|
-
const channelIdx = args.indexOf('--channel');
|
|
2124
|
-
if (channelIdx >= 0) {
|
|
2125
|
-
const value = args[channelIdx + 1];
|
|
2126
|
-
if (!value || value.startsWith('--')) {
|
|
2127
|
-
console.log(chalk.yellow(' ⚠ --channel needs a value (stable | beta). Using "stable".'));
|
|
2128
|
-
} else if (value !== 'stable' && value !== 'beta') {
|
|
2129
|
-
console.log(chalk.yellow(` ⚠ Unknown channel "${value}". Using "stable".`));
|
|
2130
|
-
} else {
|
|
2131
|
-
channel = value;
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
if (checkOnly) {
|
|
2137
|
-
return runCheck(channel);
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
// Pass channel through to the provisioner. The provisioner does the
|
|
2141
|
-
// actual npm install; channel maps to the npm dist-tag suffix.
|
|
2142
|
-
return runProvision({
|
|
2143
|
-
mode: 'update',
|
|
2144
|
-
dryRun: args.includes('--dry-run'),
|
|
2145
|
-
force: args.includes('--force'),
|
|
2146
|
-
yes: args.includes('--yes') || args.includes('-y'),
|
|
2147
|
-
restart: !args.includes('--no-restart'),
|
|
2148
|
-
channel,
|
|
2149
|
-
});
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
/**
|
|
2153
|
-
* v4.4.14 — `bizar update --check`. Print the version matrix and (if
|
|
2154
|
-
* a newer version exists) the release notes between current and latest.
|
|
2155
|
-
* Does NOT touch the system. Exits non-zero when an update is available
|
|
2156
|
-
* so callers can use it in CI / pre-flight scripts.
|
|
2157
|
-
*/
|
|
2158
|
-
export async function runCheck(channel = 'stable') {
|
|
2159
|
-
console.log(chalk.bold.hex('#a855f7')('\n ᚦ BIZAR UPDATE — CHECK ᚦ\n'));
|
|
2160
|
-
console.log(chalk.dim(` Channel: ${channel}\n`));
|
|
2161
|
-
|
|
2162
|
-
const state = detectState();
|
|
2163
|
-
printVersionMatrix([
|
|
2164
|
-
{
|
|
2165
|
-
label: '@anthropic-ai/claude-code',
|
|
2166
|
-
current: state.claudeCli.version,
|
|
2167
|
-
latest: state.claudeCli.latest,
|
|
2168
|
-
},
|
|
2169
|
-
{ label: PKG_MAIN, current: state.pkgVersion, latest: state.pkgLatest },
|
|
2170
|
-
]);
|
|
2171
|
-
|
|
2172
|
-
const cur = state.pkgVersion;
|
|
2173
|
-
const lat = state.pkgLatest;
|
|
2174
|
-
if (cur && lat && cur !== lat) {
|
|
2175
|
-
console.log(chalk.dim(`\n Release notes (${cur} → ${lat}):`));
|
|
2176
|
-
const notes = fetchReleaseNotes(PKG_MAIN, cur, lat).catch((err) => {
|
|
2177
|
-
console.log(chalk.dim(` (could not fetch: ${err.message || err})`));
|
|
2178
|
-
return null;
|
|
2179
|
-
});
|
|
2180
|
-
const text = await notes;
|
|
2181
|
-
if (text) {
|
|
2182
|
-
// Trim to a sensible cap (first 30 lines) so the console stays
|
|
2183
|
-
// readable; the full notes are one `npm view` call away.
|
|
2184
|
-
const lines = text.split(/\r?\n/).slice(0, 30);
|
|
2185
|
-
for (const line of lines) console.log(` ${line}`);
|
|
2186
|
-
if (lines.length === 30) console.log(chalk.dim(' ... (truncated; run `npm view @polderlabs/bizar` for full)'));
|
|
2187
|
-
}
|
|
2188
|
-
console.log(chalk.yellow(`\n ⤵ Run \`bizar update\` to apply.\n`));
|
|
2189
|
-
// Non-zero exit so scripts can detect the update-available case.
|
|
2190
|
-
return { ok: true, updateAvailable: true, channel };
|
|
2191
|
-
}
|
|
2192
|
-
|
|
2193
|
-
console.log(chalk.green('\n ✓ Already on the latest version.\n'));
|
|
2194
|
-
return { ok: true, updateAvailable: false, channel };
|
|
2195
|
-
}
|
|
2196
|
-
|
|
2197
|
-
/**
|
|
2198
|
-
* Fetch the release notes between two versions from the npm registry.
|
|
2199
|
-
* Returns a plain-text summary (the registry's "description" field for
|
|
2200
|
-
* the latest version), or null on any failure. Never throws — failures
|
|
2201
|
-
* are surfaced to the caller as null so the check command stays useful
|
|
2202
|
-
* offline.
|
|
2203
|
-
*/
|
|
2204
|
-
async function fetchReleaseNotes(pkg, _from, _to) {
|
|
2205
|
-
// The npm registry does not expose a structured per-version release-
|
|
2206
|
-
// notes field. The best we can do is `npm view <pkg> description`
|
|
2207
|
-
// which is the package's README excerpt. We surface that as "notes".
|
|
2208
|
-
try {
|
|
2209
|
-
const { execFileSync } = await import('node:child_process');
|
|
2210
|
-
const out = execFileSync('npm', ['view', pkg, 'description'], {
|
|
2211
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
2212
|
-
timeout: 10000,
|
|
2213
|
-
encoding: 'utf8',
|
|
2214
|
-
}).toString().trim();
|
|
2215
|
-
return out || null;
|
|
2216
|
-
} catch {
|
|
2217
|
-
return null;
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
/**
|
|
2222
|
-
* CLI-flag-parsing entrypoint for `bizar install`. Mirrors `runUpdate`
|
|
2223
|
-
* so `cli/install.mjs` can stay a thin shim too.
|
|
2224
|
-
*/
|
|
2225
|
-
export async function runInstallerCli(opts = []) {
|
|
2226
|
-
const subargs = Array.isArray(opts) ? opts : [];
|
|
2227
|
-
return runProvision({
|
|
2228
|
-
mode: 'install',
|
|
2229
|
-
dryRun: subargs.includes('--dry-run'),
|
|
2230
|
-
force: subargs.includes('--force'),
|
|
2231
|
-
yes: subargs.includes('--yes') || subargs.includes('-y'),
|
|
2232
|
-
});
|
|
2233
|
-
}
|