@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.
Files changed (235) hide show
  1. package/README.md +31 -8
  2. package/bizar-dash/CONTROL_SURFACES.md +0 -1
  3. package/bizar-dash/dist/assets/{ActivityFeed-JjmOY2AQ.js → ActivityFeed-DtRtg-JN.js} +1 -1
  4. package/bizar-dash/dist/assets/{ActivityFeed-JjmOY2AQ.js.map → ActivityFeed-DtRtg-JN.js.map} +1 -1
  5. package/bizar-dash/dist/assets/{ActivityView-BIIMvGWo.js → ActivityView-CoTXDyU_.js} +1 -1
  6. package/bizar-dash/dist/assets/{ActivityView-BIIMvGWo.js.map → ActivityView-CoTXDyU_.js.map} +1 -1
  7. package/bizar-dash/dist/assets/{AdminView-DRidJtkq.js → AdminView-DNc5Ll7L.js} +1 -1
  8. package/bizar-dash/dist/assets/{AdminView-DRidJtkq.js.map → AdminView-DNc5Ll7L.js.map} +1 -1
  9. package/bizar-dash/dist/assets/{AgentActivity-79l4SBMC.js → AgentActivity-CrX7LmQ6.js} +1 -1
  10. package/bizar-dash/dist/assets/{AgentActivity-79l4SBMC.js.map → AgentActivity-CrX7LmQ6.js.map} +1 -1
  11. package/bizar-dash/dist/assets/{AgentsView-0vT4uPSc.js → AgentsView-Du-G_skK.js} +3 -3
  12. package/bizar-dash/dist/assets/{AgentsView-0vT4uPSc.js.map → AgentsView-Du-G_skK.js.map} +1 -1
  13. package/bizar-dash/dist/assets/{Alert-CG5q_s75.js → Alert-B6DjfRFI.js} +1 -1
  14. package/bizar-dash/dist/assets/{Alert-CG5q_s75.js.map → Alert-B6DjfRFI.js.map} +1 -1
  15. package/bizar-dash/dist/assets/{ArtifactsView-DM8ZmpR9.js → ArtifactsView-znhN8GTR.js} +1 -1
  16. package/bizar-dash/dist/assets/{ArtifactsView-DM8ZmpR9.js.map → ArtifactsView-znhN8GTR.js.map} +1 -1
  17. package/bizar-dash/dist/assets/{AuthView-BveN94MC.js → AuthView-Rk8eKUPr.js} +1 -1
  18. package/bizar-dash/dist/assets/{AuthView-BveN94MC.js.map → AuthView-Rk8eKUPr.js.map} +1 -1
  19. package/bizar-dash/dist/assets/{Avatar-ulUPIYTd.js → Avatar-B41upSD1.js} +1 -1
  20. package/bizar-dash/dist/assets/{Avatar-ulUPIYTd.js.map → Avatar-B41upSD1.js.map} +1 -1
  21. package/bizar-dash/dist/assets/{BackgroundJobsView-BWErhqFo.js → BackgroundJobsView-BLWuN9qz.js} +1 -1
  22. package/bizar-dash/dist/assets/{BackgroundJobsView-BWErhqFo.js.map → BackgroundJobsView-BLWuN9qz.js.map} +1 -1
  23. package/bizar-dash/dist/assets/{BackupView-CnBjzG9N.js → BackupView-CqbtcnuQ.js} +1 -1
  24. package/bizar-dash/dist/assets/{BackupView-CnBjzG9N.js.map → BackupView-CqbtcnuQ.js.map} +1 -1
  25. package/bizar-dash/dist/assets/{Card-B0imhxMk.js → Card-rF59wHiO.js} +1 -1
  26. package/bizar-dash/dist/assets/{Card-B0imhxMk.js.map → Card-rF59wHiO.js.map} +1 -1
  27. package/bizar-dash/dist/assets/{ChatView-D6zGytzx.js → ChatView-CUriVEgU.js} +1 -1
  28. package/bizar-dash/dist/assets/{ChatView-D6zGytzx.js.map → ChatView-CUriVEgU.js.map} +1 -1
  29. package/bizar-dash/dist/assets/{Chip-wwZSGwLE.js → Chip-Dc2KmeWl.js} +1 -1
  30. package/bizar-dash/dist/assets/{Chip-wwZSGwLE.js.map → Chip-Dc2KmeWl.js.map} +1 -1
  31. package/bizar-dash/dist/assets/{ClaudeSessionsView-DK1sOLhi.js → ClaudeSessionsView-CiaHQdDZ.js} +1 -1
  32. package/bizar-dash/dist/assets/{ClaudeSessionsView-DK1sOLhi.js.map → ClaudeSessionsView-CiaHQdDZ.js.map} +1 -1
  33. package/bizar-dash/dist/assets/{ClipboardView-CZ2lYPr0.js → ClipboardView-DdP6aVso.js} +1 -1
  34. package/bizar-dash/dist/assets/{ClipboardView-CZ2lYPr0.js.map → ClipboardView-DdP6aVso.js.map} +1 -1
  35. package/bizar-dash/dist/assets/{ConfigView-D-zT9eNA.js → ConfigView-B2ysnTZU.js} +1 -1
  36. package/bizar-dash/dist/assets/{ConfigView-D-zT9eNA.js.map → ConfigView-B2ysnTZU.js.map} +1 -1
  37. package/bizar-dash/dist/assets/{DiagnosticsView-BocV68Lb.js → DiagnosticsView-88OZ5k0f.js} +1 -1
  38. package/bizar-dash/dist/assets/{DiagnosticsView-BocV68Lb.js.map → DiagnosticsView-88OZ5k0f.js.map} +1 -1
  39. package/bizar-dash/dist/assets/{DialogsView-BVjPI5x7.js → DialogsView-BcqDDCZQ.js} +1 -1
  40. package/bizar-dash/dist/assets/{DialogsView-BVjPI5x7.js.map → DialogsView-BcqDDCZQ.js.map} +1 -1
  41. package/bizar-dash/dist/assets/{DoctorView-84gJcZwM.js → DoctorView-BQwqvMAh.js} +1 -1
  42. package/bizar-dash/dist/assets/{DoctorView-84gJcZwM.js.map → DoctorView-BQwqvMAh.js.map} +1 -1
  43. package/bizar-dash/dist/assets/{EmptyState-BjgVOrcO.js → EmptyState-D65J5FQN.js} +1 -1
  44. package/bizar-dash/dist/assets/{EmptyState-BjgVOrcO.js.map → EmptyState-D65J5FQN.js.map} +1 -1
  45. package/bizar-dash/dist/assets/{EnvVarsView-BJ2TEzF7.js → EnvVarsView-DX-3oGH-.js} +2 -2
  46. package/bizar-dash/dist/assets/{EnvVarsView-BJ2TEzF7.js.map → EnvVarsView-DX-3oGH-.js.map} +1 -1
  47. package/bizar-dash/dist/assets/{ErrorState-BE3WBFqF.js → ErrorState-DULZnttJ.js} +1 -1
  48. package/bizar-dash/dist/assets/{ErrorState-BE3WBFqF.js.map → ErrorState-DULZnttJ.js.map} +1 -1
  49. package/bizar-dash/dist/assets/{EvalView-BJJ2P-kK.js → EvalView-CpdFu_ID.js} +1 -1
  50. package/bizar-dash/dist/assets/{EvalView-BJJ2P-kK.js.map → EvalView-CpdFu_ID.js.map} +1 -1
  51. package/bizar-dash/dist/assets/{GoalsView-pwROHApi.js → GoalsView-Br8oPPfb.js} +1 -1
  52. package/bizar-dash/dist/assets/{GoalsView-pwROHApi.js.map → GoalsView-Br8oPPfb.js.map} +1 -1
  53. package/bizar-dash/dist/assets/{Grid-qcvbhx6e.js → Grid-BSXhsCPC.js} +1 -1
  54. package/bizar-dash/dist/assets/{Grid-qcvbhx6e.js.map → Grid-BSXhsCPC.js.map} +1 -1
  55. package/bizar-dash/dist/assets/{HistoryView-C7SSOLJA.js → HistoryView-D0J3aoZF.js} +1 -1
  56. package/bizar-dash/dist/assets/{HistoryView-C7SSOLJA.js.map → HistoryView-D0J3aoZF.js.map} +1 -1
  57. package/bizar-dash/dist/assets/{Input-Bgzxf_Hu.js → Input-CZmRw5QX.js} +1 -1
  58. package/bizar-dash/dist/assets/{Input-Bgzxf_Hu.js.map → Input-CZmRw5QX.js.map} +1 -1
  59. package/bizar-dash/dist/assets/{LibrariesView-CbFFksox.js → LibrariesView-B1YX9wzp.js} +1 -1
  60. package/bizar-dash/dist/assets/{LibrariesView-CbFFksox.js.map → LibrariesView-B1YX9wzp.js.map} +1 -1
  61. package/bizar-dash/dist/assets/{LightRAGView-BqDrJbRq.js → LightRAGView-CykR0t9y.js} +1 -1
  62. package/bizar-dash/dist/assets/{LightRAGView-BqDrJbRq.js.map → LightRAGView-CykR0t9y.js.map} +1 -1
  63. package/bizar-dash/dist/assets/{ListHeader-TjPJrUfY.js → ListHeader-Dpun-r36.js} +1 -1
  64. package/bizar-dash/dist/assets/{ListHeader-TjPJrUfY.js.map → ListHeader-Dpun-r36.js.map} +1 -1
  65. package/bizar-dash/dist/assets/{MemoryView-BlTeHLhX.js → MemoryView-BbER5ZKt.js} +1 -1
  66. package/bizar-dash/dist/assets/{MemoryView-BlTeHLhX.js.map → MemoryView-BbER5ZKt.js.map} +1 -1
  67. package/bizar-dash/dist/assets/MiscView-DLNEGdML.js +1 -0
  68. package/bizar-dash/dist/assets/{MiscView-B8QY82Pm.js.map → MiscView-DLNEGdML.js.map} +1 -1
  69. package/bizar-dash/dist/assets/{ModsView-CvW1OWts.js → ModsView-P7gDeNHM.js} +2 -2
  70. package/bizar-dash/dist/assets/{ModsView-CvW1OWts.js.map → ModsView-P7gDeNHM.js.map} +1 -1
  71. package/bizar-dash/dist/assets/{NotificationsView-CsBt5U7x.js → NotificationsView-DAm2O0yf.js} +1 -1
  72. package/bizar-dash/dist/assets/{NotificationsView-CsBt5U7x.js.map → NotificationsView-DAm2O0yf.js.map} +1 -1
  73. package/bizar-dash/dist/assets/ObsidianView-Ci80gnwS.js +1 -0
  74. package/bizar-dash/dist/assets/{ObsidianView-BCTejEj2.js.map → ObsidianView-Ci80gnwS.js.map} +1 -1
  75. package/bizar-dash/dist/assets/{OverviewView-BsuPxwnB.js → OverviewView-rriw1GjA.js} +1 -1
  76. package/bizar-dash/dist/assets/{OverviewView-BsuPxwnB.js.map → OverviewView-rriw1GjA.js.map} +1 -1
  77. package/bizar-dash/dist/assets/{ProgressBar-DlZTETEn.js → ProgressBar-BahmeJhP.js} +1 -1
  78. package/bizar-dash/dist/assets/{ProgressBar-DlZTETEn.js.map → ProgressBar-BahmeJhP.js.map} +1 -1
  79. package/bizar-dash/dist/assets/{ProjectsView-Soxtmpet.js → ProjectsView-ChzM12bs.js} +1 -1
  80. package/bizar-dash/dist/assets/{ProjectsView-Soxtmpet.js.map → ProjectsView-ChzM12bs.js.map} +1 -1
  81. package/bizar-dash/dist/assets/{ProvidersView-DaCEV9pM.js → ProvidersView-BftGn8Cr.js} +1 -1
  82. package/bizar-dash/dist/assets/{ProvidersView-DaCEV9pM.js.map → ProvidersView-BftGn8Cr.js.map} +1 -1
  83. package/bizar-dash/dist/assets/{SchedulesView-DY2ByBj_.js → SchedulesView-CtNxtzN6.js} +1 -1
  84. package/bizar-dash/dist/assets/{SchedulesView-DY2ByBj_.js.map → SchedulesView-CtNxtzN6.js.map} +1 -1
  85. package/bizar-dash/dist/assets/{Select-Dbd88_n8.js → Select-0VcfTDeL.js} +1 -1
  86. package/bizar-dash/dist/assets/{Select-Dbd88_n8.js.map → Select-0VcfTDeL.js.map} +1 -1
  87. package/bizar-dash/dist/assets/{SettingsRow-CLMNd434.js → SettingsRow-BLic3DQl.js} +1 -1
  88. package/bizar-dash/dist/assets/{SettingsRow-CLMNd434.js.map → SettingsRow-BLic3DQl.js.map} +1 -1
  89. package/bizar-dash/dist/assets/{SettingsView-b24oFq9x.js → SettingsView-BjS3i4nr.js} +1 -1
  90. package/bizar-dash/dist/assets/{SettingsView-b24oFq9x.js.map → SettingsView-BjS3i4nr.js.map} +1 -1
  91. package/bizar-dash/dist/assets/{Sheet-DnDid58j.js → Sheet-BOProMs2.js} +1 -1
  92. package/bizar-dash/dist/assets/{Sheet-DnDid58j.js.map → Sheet-BOProMs2.js.map} +1 -1
  93. package/bizar-dash/dist/assets/{Slider-BJCvKI1I.js → Slider-B0eLEEUt.js} +1 -1
  94. package/bizar-dash/dist/assets/{Slider-BJCvKI1I.js.map → Slider-B0eLEEUt.js.map} +1 -1
  95. package/bizar-dash/dist/assets/{Sparkline-ktqQLdyb.js → Sparkline-SSrdgyW0.js} +1 -1
  96. package/bizar-dash/dist/assets/{Sparkline-ktqQLdyb.js.map → Sparkline-SSrdgyW0.js.map} +1 -1
  97. package/bizar-dash/dist/assets/{Switch-CwXfsDii.js → Switch-B3EQzb_q.js} +1 -1
  98. package/bizar-dash/dist/assets/{Switch-CwXfsDii.js.map → Switch-B3EQzb_q.js.map} +1 -1
  99. package/bizar-dash/dist/assets/{TasksView-BHmpzFCf.js → TasksView-SWjF_ZUQ.js} +1 -1
  100. package/bizar-dash/dist/assets/{TasksView-BHmpzFCf.js.map → TasksView-SWjF_ZUQ.js.map} +1 -1
  101. package/bizar-dash/dist/assets/{Textarea-BzIAwSlp.js → Textarea-QuofpMlq.js} +1 -1
  102. package/bizar-dash/dist/assets/{Textarea-BzIAwSlp.js.map → Textarea-QuofpMlq.js.map} +1 -1
  103. package/bizar-dash/dist/assets/{UpdateView-CSmX9NN0.js → UpdateView-CEHW2nd6.js} +1 -1
  104. package/bizar-dash/dist/assets/{UpdateView-CSmX9NN0.js.map → UpdateView-CEHW2nd6.js.map} +1 -1
  105. package/bizar-dash/dist/assets/{UsageView-COyr_Gfi.js → UsageView-BqOw6FBg.js} +1 -1
  106. package/bizar-dash/dist/assets/{UsageView-COyr_Gfi.js.map → UsageView-BqOw6FBg.js.map} +1 -1
  107. package/bizar-dash/dist/assets/{ViewHeader-BzYZ3m-e.js → ViewHeader-B6P-C4GD.js} +1 -1
  108. package/bizar-dash/dist/assets/{ViewHeader-BzYZ3m-e.js.map → ViewHeader-B6P-C4GD.js.map} +1 -1
  109. package/bizar-dash/dist/assets/{VoiceView-D33_xG5t.js → VoiceView-CwrPwWgi.js} +1 -1
  110. package/bizar-dash/dist/assets/{VoiceView-D33_xG5t.js.map → VoiceView-CwrPwWgi.js.map} +1 -1
  111. package/bizar-dash/dist/assets/{brain-CM-Vls8r.js → brain-CwZt9Kta.js} +1 -1
  112. package/bizar-dash/dist/assets/{brain-CM-Vls8r.js.map → brain-CwZt9Kta.js.map} +1 -1
  113. package/bizar-dash/dist/assets/{calendar-C0lgZk69.js → calendar-C5oK0_nM.js} +1 -1
  114. package/bizar-dash/dist/assets/{calendar-C0lgZk69.js.map → calendar-C5oK0_nM.js.map} +1 -1
  115. package/bizar-dash/dist/assets/{circle-Bx_hQEBC.js → circle-D1Lc_P2a.js} +1 -1
  116. package/bizar-dash/dist/assets/{circle-Bx_hQEBC.js.map → circle-D1Lc_P2a.js.map} +1 -1
  117. package/bizar-dash/dist/assets/{circle-alert-DHvsqpw5.js → circle-alert-HdHe7m8K.js} +1 -1
  118. package/bizar-dash/dist/assets/{circle-alert-DHvsqpw5.js.map → circle-alert-HdHe7m8K.js.map} +1 -1
  119. package/bizar-dash/dist/assets/{circle-check-Bx8wmXR9.js → circle-check-Cs-xoV7R.js} +1 -1
  120. package/bizar-dash/dist/assets/{circle-check-Bx8wmXR9.js.map → circle-check-Cs-xoV7R.js.map} +1 -1
  121. package/bizar-dash/dist/assets/{clock-CejGoecX.js → clock-DEIuPV60.js} +1 -1
  122. package/bizar-dash/dist/assets/{clock-CejGoecX.js.map → clock-DEIuPV60.js.map} +1 -1
  123. package/bizar-dash/dist/assets/{download-4cEUWue1.js → download-Clkq90Ct.js} +1 -1
  124. package/bizar-dash/dist/assets/{download-4cEUWue1.js.map → download-Clkq90Ct.js.map} +1 -1
  125. package/bizar-dash/dist/assets/{external-link-0NqUhI4i.js → external-link-D5tq2o_I.js} +1 -1
  126. package/bizar-dash/dist/assets/{external-link-0NqUhI4i.js.map → external-link-D5tq2o_I.js.map} +1 -1
  127. package/bizar-dash/dist/assets/{file-text-yEMkrSoZ.js → file-text-CQ12t7Oc.js} +1 -1
  128. package/bizar-dash/dist/assets/{file-text-yEMkrSoZ.js.map → file-text-CQ12t7Oc.js.map} +1 -1
  129. package/bizar-dash/dist/assets/{filter-BXt60zAS.js → filter-BcbE37wP.js} +1 -1
  130. package/bizar-dash/dist/assets/{filter-BXt60zAS.js.map → filter-BcbE37wP.js.map} +1 -1
  131. package/bizar-dash/dist/assets/{git-pull-request-BjG5lNtR.js → git-pull-request-D1qNDRia.js} +1 -1
  132. package/bizar-dash/dist/assets/{git-pull-request-BjG5lNtR.js.map → git-pull-request-D1qNDRia.js.map} +1 -1
  133. package/bizar-dash/dist/assets/{main-CokHAXWM.js → main-Cxeu4LhG.js} +28 -33
  134. package/bizar-dash/dist/assets/main-Cxeu4LhG.js.map +1 -0
  135. package/bizar-dash/dist/assets/network-DJdvVaHx.js +6 -0
  136. package/bizar-dash/dist/assets/network-DJdvVaHx.js.map +1 -0
  137. package/bizar-dash/dist/assets/{pause-CAmulU0F.js → pause-DUn9L5PB.js} +1 -1
  138. package/bizar-dash/dist/assets/{pause-CAmulU0F.js.map → pause-DUn9L5PB.js.map} +1 -1
  139. package/bizar-dash/dist/assets/{pencil-eq5IqBmU.js → pencil-DVrBlYV3.js} +1 -1
  140. package/bizar-dash/dist/assets/{pencil-eq5IqBmU.js.map → pencil-DVrBlYV3.js.map} +1 -1
  141. package/bizar-dash/dist/assets/{play-BoLrrcBA.js → play-CZjsr3XM.js} +1 -1
  142. package/bizar-dash/dist/assets/{play-BoLrrcBA.js.map → play-CZjsr3XM.js.map} +1 -1
  143. package/bizar-dash/dist/assets/{plug-DBrVM9lK.js → plug-BKuyIe5W.js} +1 -1
  144. package/bizar-dash/dist/assets/{plug-DBrVM9lK.js.map → plug-BKuyIe5W.js.map} +1 -1
  145. package/bizar-dash/dist/assets/{plus-DENm0hqV.js → plus-77h62zZm.js} +1 -1
  146. package/bizar-dash/dist/assets/{plus-DENm0hqV.js.map → plus-77h62zZm.js.map} +1 -1
  147. package/bizar-dash/dist/assets/{power-DLjXF7zE.js → power-DLmx-gCP.js} +1 -1
  148. package/bizar-dash/dist/assets/{power-DLjXF7zE.js.map → power-DLmx-gCP.js.map} +1 -1
  149. package/bizar-dash/dist/assets/{power-off-D47cOnG_.js → power-off-BFKzHXic.js} +1 -1
  150. package/bizar-dash/dist/assets/{power-off-D47cOnG_.js.map → power-off-BFKzHXic.js.map} +1 -1
  151. package/bizar-dash/dist/assets/{refresh-ccw-DNrXX9gB.js → refresh-ccw-DenGBOcq.js} +1 -1
  152. package/bizar-dash/dist/assets/{refresh-ccw-DNrXX9gB.js.map → refresh-ccw-DenGBOcq.js.map} +1 -1
  153. package/bizar-dash/dist/assets/{refresh-cw-CoAHDpeS.js → refresh-cw-ENBVR8hW.js} +1 -1
  154. package/bizar-dash/dist/assets/{refresh-cw-CoAHDpeS.js.map → refresh-cw-ENBVR8hW.js.map} +1 -1
  155. package/bizar-dash/dist/assets/{rotate-ccw-ObX5awaG.js → rotate-ccw-MxryaY7q.js} +1 -1
  156. package/bizar-dash/dist/assets/{rotate-ccw-ObX5awaG.js.map → rotate-ccw-MxryaY7q.js.map} +1 -1
  157. package/bizar-dash/dist/assets/{rotate-cw-xpPJw9LP.js → rotate-cw-CDOlRtUR.js} +1 -1
  158. package/bizar-dash/dist/assets/{rotate-cw-xpPJw9LP.js.map → rotate-cw-CDOlRtUR.js.map} +1 -1
  159. package/bizar-dash/dist/assets/{save-D1iKiJJl.js → save-Db1b-1Uq.js} +1 -1
  160. package/bizar-dash/dist/assets/{save-D1iKiJJl.js.map → save-Db1b-1Uq.js.map} +1 -1
  161. package/bizar-dash/dist/assets/{send-BRcAhZUm.js → send-yz6HFZM5.js} +1 -1
  162. package/bizar-dash/dist/assets/{send-BRcAhZUm.js.map → send-yz6HFZM5.js.map} +1 -1
  163. package/bizar-dash/dist/assets/{shield-alert-C6vEcK-g.js → shield-alert-DY6RAaC8.js} +1 -1
  164. package/bizar-dash/dist/assets/{shield-alert-C6vEcK-g.js.map → shield-alert-DY6RAaC8.js.map} +1 -1
  165. package/bizar-dash/dist/assets/{trash-2-DZDjEnNh.js → trash-2-Cv7U38jw.js} +1 -1
  166. package/bizar-dash/dist/assets/{trash-2-DZDjEnNh.js.map → trash-2-Cv7U38jw.js.map} +1 -1
  167. package/bizar-dash/dist/assets/{trending-up-DMj-AJVf.js → trending-up-CRPNagyX.js} +1 -1
  168. package/bizar-dash/dist/assets/{trending-up-DMj-AJVf.js.map → trending-up-CRPNagyX.js.map} +1 -1
  169. package/bizar-dash/dist/assets/{triangle-alert-_2xe_tl4.js → triangle-alert-tUTXbcEP.js} +1 -1
  170. package/bizar-dash/dist/assets/{triangle-alert-_2xe_tl4.js.map → triangle-alert-tUTXbcEP.js.map} +1 -1
  171. package/bizar-dash/dist/assets/{upload-DdDljAeV.js → upload-Cq7uTsrT.js} +1 -1
  172. package/bizar-dash/dist/assets/{upload-DdDljAeV.js.map → upload-Cq7uTsrT.js.map} +1 -1
  173. package/bizar-dash/dist/assets/{zap-mHsgh0FP.js → zap-5lAQBAPX.js} +1 -1
  174. package/bizar-dash/dist/assets/{zap-mHsgh0FP.js.map → zap-5lAQBAPX.js.map} +1 -1
  175. package/bizar-dash/dist/index.html +1 -1
  176. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  177. package/bizar-dash/src/server/api.mjs +5 -1
  178. package/bizar-dash/src/server/routes/update.mjs +48 -21
  179. package/bizar-dash/src/web/v8/App.tsx +0 -4
  180. package/bizar-dash/src/web/v8/__tests__/new-views.test.tsx +0 -17
  181. package/bizar-dash/src/web/v8/shell/Sidebar.tsx +3 -4
  182. package/bizar-dash/src/web/v8/views/Router.tsx +0 -5
  183. package/bizar-dash/tests/_update-test-helpers.mjs +13 -0
  184. package/bizar-dash/tests/update-check.test.mjs +18 -13
  185. package/bizar-dash/tests/update-packages.test.mjs +154 -0
  186. package/bizar-dash/tests/update-run.test.mjs +17 -80
  187. package/cli/banner.mjs +1 -1
  188. package/cli/bin.mjs +8 -1
  189. package/cli/bootstrap.mjs +1 -1
  190. package/cli/commands/claude-cmd.mjs +1 -1
  191. package/cli/commands/deploy/docker.mjs +0 -3
  192. package/cli/doctor.mjs +1 -1
  193. package/cli/install/banner.mjs +67 -0
  194. package/cli/install/banner.test.mjs +39 -0
  195. package/cli/install/index.mjs +32 -0
  196. package/cli/install/index.test.mjs +61 -0
  197. package/cli/install/paths.mjs +80 -0
  198. package/cli/install/paths.test.mjs +77 -0
  199. package/cli/install/plugin.mjs +161 -0
  200. package/cli/install/postinstall.mjs +111 -0
  201. package/cli/install.mjs +13 -600
  202. package/cli/migrate.mjs +157 -0
  203. package/cli/migrate.test.mjs +85 -0
  204. package/cli/provision.mjs +615 -1918
  205. package/cli/provision.test.mjs +1 -1
  206. package/cli/service-controller.test.mjs +0 -1
  207. package/cli/service-env.mjs +0 -1
  208. package/cli/utils.mjs +0 -1
  209. package/install.sh +54 -466
  210. package/package.json +1 -1
  211. package/packages/sdk/dist/mcp/server.js +41 -409
  212. package/packages/sdk/dist/router/q-learning-router.d.ts +3 -5
  213. package/packages/sdk/dist/router/q-learning-router.js +9 -5
  214. package/packages/sdk/package.json +1 -1
  215. package/scripts/install-curl.sh +17 -0
  216. package/bizar-dash/dist/assets/HeadroomView-D1UiTYbt.js +0 -11
  217. package/bizar-dash/dist/assets/HeadroomView-D1UiTYbt.js.map +0 -1
  218. package/bizar-dash/dist/assets/MiscView-B8QY82Pm.js +0 -1
  219. package/bizar-dash/dist/assets/ObsidianView-BCTejEj2.js +0 -1
  220. package/bizar-dash/dist/assets/main-CokHAXWM.js.map +0 -1
  221. package/bizar-dash/skills/headroom/SKILL.md +0 -94
  222. package/bizar-dash/src/server/headroom.mjs +0 -649
  223. package/bizar-dash/src/server/routes/headroom.mjs +0 -126
  224. package/bizar-dash/src/web/v8/views/Headroom/HeadroomView.tsx +0 -201
  225. package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +0 -90
  226. package/bizar-dash/tests/headroom-install.test.mjs +0 -173
  227. package/bizar-dash/tests/headroom-settings.test.mjs +0 -126
  228. package/bizar-dash/tests/headroom-status.test.mjs +0 -117
  229. package/bizar-dash/tests/v7-archive/mobile-overview.test.tsx +0 -214
  230. package/bizar-dash/tests/v7-archive/mobile-settings.test.tsx +0 -181
  231. package/bizar-dash/tests/v7-archive/navigation/CommandPalette.test.tsx +0 -105
  232. package/bizar-dash/tests/v7-archive/settings-nav.test.tsx +0 -128
  233. package/bizar-dash/tests/v7-archive/views/Overview.test.tsx +0 -413
  234. package/cli/commands/headroom.mjs +0 -204
  235. 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
- * v6.3.0 — Unified installer + updater (Claude Code-native).
5
+ * v10.0.0 — Unified installer + updater (Claude Code-native).
5
6
  *
6
- * `bizar install` and `bizar update` used to be two separate code paths
7
- * (install.sh + cli/install.mjs for install, cli/update.mjs for update)
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, ...flags })
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 { existsSync, readFileSync, readdirSync, rmSync, writeFileSync, mkdirSync, statSync, copyFileSync, chmodSync } from 'node:fs';
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, dirname, sep } from 'node:path';
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
- // `cli/provision.mjs` lives at `<pkg>/cli/provision.mjs`. The repo root
50
- // (where `plugins/bizar/`, `bizar-dash/`, `package.json` etc. live) is
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
- export const BIZAR_HOME = bizarConfigDir();
43
+ // ─── Paths ────────────────────────────────────────────────────────────────────
57
44
 
58
45
  /**
59
46
  * Resolve the Claude Code config directory.
60
- *
61
- * Claude Code (v1.0+) uses `~/.claude/` (or `$CLAUDE_CONFIG_DIR`) as the
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 join(HOME, '.claude');
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
- * Back-compat alias used by other modules + scripts.
98
- * Same value as CLAUDE_DIR; kept so old imports still resolve.
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
- * Legacy Cline directory (the old OpenCode layout).
105
- *
106
- * Kept as an opt-in fallback. The installer warns but does not write
107
- * here under the v6.3.0 Claude Code-native flow.
108
- */
109
- export const LEGACY_CLAUDE_DIR = join(
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
- const SERVICE_PID_FILE = join(BIZAR_HOME, 'service.pid');
115
- const DASHBOARD_PID_FILE = join(BIZAR_HOME, 'dashboard.pid');
116
- const DASHBOARD_PORT_FILE = join(BIZAR_HOME, 'dashboard.port');
117
- const INSTALL_MARKER_FILE = join(BIZAR_HOME, 'installed.json');
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
- const deps = parsed.dependencies ?? {};
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
- * Ensure @polderlabs/bizar is installed via npm. Returns
583
- * `{ ok, message, installed: <version|null> }`.
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 ensureNpmPackage(pkg, { mode, dryRun, force }) {
590
- const current = currentVersion(pkg);
591
- const latest = latestVersion(pkg);
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 (current && current === latest && !force) {
598
- return { ok: true, message: `${pkg}@${current} already up to date`, installed: current };
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
- if (dryRun) {
602
- return {
603
- ok: true,
604
- message: `[dry-run] would run: npm install -g ${pkg}${latest ? `@${latest}` : '@latest'}`,
605
- installed: latest ?? current,
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
- // If we're inside a `bizar update`, the dashboard service / dashboard
610
- // processes are reading files inside the npm-global install dir. We
611
- // can't replace those files atomically while they're open. The caller
612
- // is expected to have already killed them via ensureInstancesKilled().
613
- const npmArgs = ['install', '-g', `${pkg}@latest`];
614
- if (process.env.NPM_CONFIG_PREFIX) {
615
- npmArgs.push('--prefix', process.env.NPM_CONFIG_PREFIX);
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
- export async function updateClaudeCodeCli({ dryRun, force }) {
634
- const current = currentVersion('@anthropic-ai/claude-code');
635
- const latest = latestVersion('@anthropic-ai/claude-code');
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 (current && current === latest && !force) {
638
- return {
639
- ok: true,
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
- if (dryRun) {
646
- return { ok: true, message: '[dry-run] @anthropic-ai/claude-code upgrade' };
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
- // Claude Code ships via npm; `claude upgrade` is not a built-in
650
- // subcommand. Always go through npm.
651
- const npmArgs = ['install', '-g', '@anthropic-ai/claude-code@latest'];
652
- if (process.env.NPM_CONFIG_PREFIX) {
653
- npmArgs.push('--prefix', process.env.NPM_CONFIG_PREFIX);
654
- }
655
- const r = spawnSync('npm', npmArgs, { stdio: 'inherit', timeout: 600000 });
656
- if (r.status === null && r.error?.code === 'ETIMEDOUT') {
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
- * Copy `plugins/bizar/` from the npm-installed package into
674
- * `${CLAUDE_DIR}/plugins/bizar/`. Skips if the dest is a dev symlink
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 copyPluginToCline({ dryRun, force }) {
691
- const state = detectState();
692
- const src = state.plugin.sourceDir;
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 || !existsSync(src)) {
696
- return {
697
- ok: false,
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
- // v6.3.0 Claude Code uses MCP servers, not the legacy Cline
703
- // plugin manifest. The Bizar SDK ships under packages/sdk/ and is
704
- // registered in ~/.claude/settings.json as `mcpServers.bizar`. The
705
- // plugins/bizar/ tree is now a thin shim that re-exports the SDK,
706
- // and we no longer copy it into ${CLAUDE_DIR}/plugins/bizar/.
707
- // We only verify that the source has a package.json (sanity check).
708
- const pkgJsonPath = join(src, 'package.json');
709
- if (!existsSync(pkgJsonPath)) {
710
- return {
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
- const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
719
- if (!pkg.name) {
720
- return {
721
- ok: false,
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
- return {
727
- ok: false,
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
- // Detect a stale "fake plugin" stub at the dest — these were left
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
- // Content-hash freshness check supersedes the dir-mtime check that
752
- // gave false positives for stale "fake plugin" dirs.
753
- if (state.plugin.installed && !force && pluginContentMatches(src, dest)) {
754
- return { ok: true, message: 'plugin copy is up to date (content verified)' };
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
- if (dryRun) {
758
- return { ok: true, message: `[dry-run] would copy ${src} → ${dest}` };
759
- }
387
+ // ─── Toolchain ───────────────────────────────────────────────────────────────
760
388
 
761
- if (state.plugin.symlink && force) {
762
- try { rmSync(dest, { force: true }); } catch { /* ignore */ }
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
- const { cp } = await import('node:fs/promises');
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
- // Start fresh wipe dest so leftover files from a previous version
768
- // don't linger (otherwise an older index.ts can shadow the new one).
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
- return { ok: false, message: `plugin copy failed: ${err.message}` };
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
- * Recursively compare the contents of `src` and `dest`, returning true if
862
- * every file in src exists in dest with the same content (size + mtime).
863
- *
864
- * Cheap O(n) walk; n is the plugin source size (typically <2k files).
865
- * Skips `node_modules/` and `dist/` in both sides.
866
- */
867
- function pluginContentMatches(src, dest) {
868
- const skipDirs = new Set(['node_modules', 'dist', '.DS_Store']);
869
- function walk(s, d) {
870
- const sEntries = readdirSyncSafe(s);
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 walk(src, dest);
430
+ return { copied, skipped };
896
431
  }
897
432
 
898
- function readdirSyncSafe(p) {
899
- try {
900
- return readdirSync(p, { withFileTypes: true });
901
- } catch {
902
- return null;
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 async function patchClaudeSettings({ dryRun, force }) {
920
- const cfgPath = join(CLAUDE_DIR, 'settings.json');
921
- if (!existsSync(cfgPath)) {
922
- if (dryRun) {
923
- return { ok: true, message: `[dry-run] would bootstrap ${cfgPath}` };
924
- }
925
- mkdirSync(CLAUDE_DIR, { recursive: true });
926
- const templateSrc = join(REPO_ROOT, 'config', 'claude.settings.json');
927
- if (existsSync(templateSrc)) {
928
- const { copyFileSync: cfs } = await import('node:fs');
929
- cfs(templateSrc, cfgPath);
930
- return { ok: true, message: `${cfgPath} bootstrapped from package template` };
931
- }
932
- writeFileSync(cfgPath, JSON.stringify({
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
- hooks: {},
941
- }, null, 2));
942
- return { ok: true, message: `${cfgPath} created` };
943
- }
944
-
945
- // File exists. Patch the Bizar config surface idempotently.
946
- const cfg = readJsonSafe(cfgPath, null);
947
- if (!cfg || typeof cfg !== 'object') {
948
- return { ok: false, message: `${cfgPath} is not valid JSON` };
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
- // v6.3.0 Multi-patch installer. On every install/update, ensure the
952
- // Bizar config surface is present in ~/.claude/settings.json:
953
- //
954
- // 1. mcpServers.bizar register the Bizar MCP server (the critical
955
- // entry; without this, Claude Code has no access to Bizar tools).
956
- // 2. $schema — add if missing (helps editors validate).
957
- // 3. hooks point at .claude/hooks/bizar-tools.md so Claude Code
958
- // fires them on every session.
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
- addedMcpServer = true;
554
+ merged.env = { ...(existing.env || {}), ...bizarSettings.env };
555
+ merged.hooks = { ...(existing.hooks || {}), ...bizarSettings.hooks };
980
556
  }
981
- cfg.mcpServers = mcpServers;
982
557
 
983
- // 2. $schema
984
- if (!cfg.$schema) {
985
- cfg.$schema = 'https://json.schemastore.org/claude-code-settings.json';
986
- addedSchema = true;
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
- // 3. hooks — point at the bundled Bizar hooks file.
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
- const anyAdded = addedMcpServer || addedSchema || addedHooks;
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
- if (!addedMcpServer && !force && !anyAdded) {
1009
- return { ok: true, message: 'settings.json already has Bizar MCP server entry' };
1010
- }
583
+ // ─── MCP server + plugin registration ───────────────────────────────────────
1011
584
 
1012
- if (dryRun) {
1013
- const added = [];
1014
- if (addedMcpServer) added.push('mcpServers.bizar');
1015
- if (addedSchema) added.push('$schema');
1016
- if (addedHooks) added.push('hooks');
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
- writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
1021
- const added = [];
1022
- if (addedMcpServer) added.push('mcpServers.bizar');
1023
- if (addedSchema) added.push('$schema');
1024
- if (addedHooks) added.push('hooks');
1025
- return {
1026
- ok: true,
1027
- message: added.length > 0
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
- mkdirSync(dstDir, { recursive: true });
1063
- mkdirSync(join(dstDir, '_shared'), { recursive: true });
1064
-
1065
- let copied = 0;
1066
- let skipped = 0;
1067
- const files = readdirSync(srcDir, { withFileTypes: true });
1068
- for (const entry of files) {
1069
- if (entry.isDirectory()) continue;
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
- // If the source is .md and there's no matching .yaml/.yml in the
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
- // _shared/ — always overwrite (it's tiny + ships agent defaults).
1097
- const sharedSrc = join(srcDir, '_shared');
1098
- if (existsSync(sharedSrc)) {
1099
- for (const entry of readdirSync(sharedSrc, { withFileTypes: true })) {
1100
- if (!entry.isFile()) continue;
1101
- copyFileSync(join(sharedSrc, entry.name), join(dstDir, '_shared', entry.name));
1102
- copied++;
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
- ok: true,
1108
- message: `agents synced (${copied} copied, ${skipped} kept)`,
1109
- copied,
1110
- skipped,
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
- * Convert an OpenCode-style agent markdown (frontmatter + body) into a
1116
- * Cline-loadable YAML agent. Cline's Zod schema requires `name` and
1117
- * `description`; the markdown body becomes the system prompt.
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 mdToClineAgentYaml(mdText, fallbackName) {
1120
- const m = mdText.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
1121
- if (!m) {
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
- // Cline uses YAML, but OpenCode uses a YAML superset. Strip keys Cline
1146
- // doesn't know about (`color`, `mode`, `permission`, etc.) — they were
1147
- // relevant for the OpenCode runtime, but they would only confuse the
1148
- // Cline Zod validator (it accepts only `name`, `description`, `tools`,
1149
- // `skills`, `providerId`, `modelId`, `maxIterations`).
1150
- const ALLOWED = new Set([
1151
- 'name',
1152
- 'description',
1153
- 'tools',
1154
- 'skills',
1155
- 'providerId',
1156
- 'modelId',
1157
- 'maxIterations',
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
- return '---\n' + filtered + '\n---\n\n' + body.trim() + '\n';
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
- * Copy slash commands + skills to the cline config dir.
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
- * Sync slash commands + skills + hooks + rules + workflows into the
1194
- * Cline config dir.
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 async function syncConfigExtras({ dryRun }) {
1223
- if (dryRun) {
1224
- return {
1225
- ok: true,
1226
- message: '[dry-run] would sync commands + skills + hooks + rules + workflows',
1227
- counts: { skills: 0, commands: 0, hooks: 0, rules: 0, workflows: 0 },
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
- mkdirSync(dstDir, { recursive: true });
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
- // ── Slash commands ───────────────────────────────────────────
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
- mkdirSync(commandsDst, { recursive: true });
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
- await copyDirIfExists(src, dst);
1258
- } else {
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
- // ── Skills ───────────────────────────────────────────────────
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
- mkdirSync(skillsDst, { recursive: true });
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
- const src = join(skillsSrc, skillDir.name);
1302
- const dst = join(agentsSkillsDst, skillDir.name);
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
- // v6.2.5 - Mirror `config/agents/_shared/AGENT_BASELINE.md` to both
1323
- // `~/.cline/skills/agent-baseline/SKILL.md` and
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 hookEntries = readdirSync(hooksSrc, { withFileTypes: true });
1363
- const hookFiles = hookEntries
1364
- .filter((e) => e.isFile() && !e.name.endsWith('.md') && !e.name.endsWith('.txt'))
1365
- .map((e) => e.name);
1366
-
1367
- // Install to ~/.cline/hooks/ (used by --hooks-dir override)
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
- // ── Rules ────────────────────────────────────────────────────
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
- mkdirSync(rulesDst, { recursive: true });
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
- const name = entry.name;
1407
- if (!name.endsWith('.md') && !name.endsWith('.txt')) continue;
1408
- const src = join(rulesSrc, name);
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
- // ── Workflows ────────────────────────────────────────────────
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
- // ── Legacy `skill/`, `skills/` (flat) ───────────────────────
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
- const useBash = process.platform !== 'win32' || process.env.WSL_DISTRO_NAME;
1473
- if (useBash) {
1474
- // install.sh accepts --mode and a few other flags. Pass --mode
1475
- // through so the bash script can skip what we already did in JS.
1476
- const args = [installSh, '--mode', mode, '--non-interactive'];
1477
- const r = spawnSync('bash', args, { stdio: 'inherit' });
1478
- if (r.status !== 0) {
1479
- return { ok: false, message: `install.sh exited with code ${r.status}` };
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
- // Windows without WSL — bash isn't available. The user can install
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
- * Run `bizar doctor` to verify the install.
1494
- */
1495
- export async function runDoctor({ silent = false } = {}) {
1496
- try {
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
- }