bitboss-ui 3.0.0-beta.47 → 3.0.0-beta.49

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 (398) hide show
  1. package/README.md +24 -7
  2. package/bin/bitboss-ui-mcp.mjs +15 -3
  3. package/bin/bitboss-ui.mjs +167 -14
  4. package/dist/ai/BbAccordion.md +1 -1
  5. package/dist/ai/BbAvatar.md +1 -1
  6. package/dist/ai/BbBreadcrumbs.md +65 -65
  7. package/dist/ai/BbButton.md +28 -8
  8. package/dist/ai/BbCheckbox.md +0 -2
  9. package/dist/ai/BbCheckboxGroup.md +13 -20
  10. package/dist/ai/BbCollapsible.md +1 -1
  11. package/dist/ai/BbColorPalette.md +1 -1
  12. package/dist/ai/BbConfirm.md +8 -0
  13. package/dist/ai/BbConfirmPortal.md +1 -1
  14. package/dist/ai/BbDatePickerInput.md +4 -2
  15. package/dist/ai/BbDialog.md +1 -8
  16. package/dist/ai/BbDropdown.md +231 -220
  17. package/dist/ai/BbDropdownButton.md +65 -65
  18. package/dist/ai/BbDropdownGroup.md +2 -2
  19. package/dist/ai/BbDropzone.md +10 -10
  20. package/dist/ai/BbForm.md +43 -52
  21. package/dist/ai/BbHoverCard.md +19 -7
  22. package/dist/ai/BbHoverCardPortal.md +1 -1
  23. package/dist/ai/BbNumberInput.md +88 -21
  24. package/dist/ai/BbPopover.md +47 -40
  25. package/dist/ai/BbPopoverPortal.md +1 -1
  26. package/dist/ai/BbProgress.md +48 -22
  27. package/dist/ai/BbRadioGroup.md +13 -19
  28. package/dist/ai/BbSelect.md +16 -36
  29. package/dist/ai/BbSelectPopover.md +14 -30
  30. package/dist/ai/BbSmoothHeight.md +0 -2
  31. package/dist/ai/BbSwitchGroup.md +13 -20
  32. package/dist/ai/BbTable.md +11 -10
  33. package/dist/ai/BbTabs.md +107 -106
  34. package/dist/ai/BbTabsList.md +2 -2
  35. package/dist/ai/BbTabsPanels.md +1 -1
  36. package/dist/ai/BbTabsRoot.md +3 -3
  37. package/dist/ai/BbTooltip.md +29 -13
  38. package/dist/ai/BbTree.md +14 -11
  39. package/dist/ai/CommonPopover.md +1 -1
  40. package/dist/ai/changelog.json +297 -6
  41. package/dist/ai/components.json +50 -45
  42. package/dist/ai/composables/useBbDropdownContext.md +16 -16
  43. package/dist/ai/composables/useBbFormField.md +5 -1
  44. package/dist/ai/composables/useBbTabsContext.md +14 -14
  45. package/dist/ai/guides/agent-contract.md +27 -27
  46. package/dist/ai/guides/ai-router.md +1 -1
  47. package/dist/ai/guides/component-picker.md +3 -3
  48. package/dist/ai/guides/fetch-items-playbook.md +1 -1
  49. package/dist/ai/guides/icons-policy.md +10 -10
  50. package/dist/ai/guides/inertia-helpers.md +120 -67
  51. package/dist/ai/guides/installation-and-plugin-setup.md +45 -15
  52. package/dist/ai/guides/migration/components/bb-breadcrumbs.md +3 -2
  53. package/dist/ai/guides/migration/components/bb-checkbox-group.md +16 -16
  54. package/dist/ai/guides/migration/components/bb-dropdown-button.md +9 -8
  55. package/dist/ai/guides/migration/components/bb-dropdown.md +15 -14
  56. package/dist/ai/guides/migration/components/bb-radio-group.md +2 -1
  57. package/dist/ai/guides/migration/components/bb-select-popover.md +6 -1
  58. package/dist/ai/guides/migration/components/bb-select.md +10 -2
  59. package/dist/ai/guides/migration/components/bb-switch-group.md +2 -1
  60. package/dist/ai/guides/migration/components/bb-tabs.md +9 -8
  61. package/dist/ai/guides/migration/components/bb-tooltip.md +1 -1
  62. package/dist/ai/guides/migration/v2-to-v3.md +143 -28
  63. package/dist/ai/guides/migration/v3-beta-css-classes.md +29 -27
  64. package/dist/ai/guides/migration/v3-beta-tooling.md +11 -0
  65. package/dist/ai/guides/options-items-playbook.md +30 -24
  66. package/dist/ai/guides/passthrough.md +4 -4
  67. package/dist/ai/guides/preset-components.md +18 -3
  68. package/dist/ai/guides/typescript-generic-components.md +1 -1
  69. package/dist/ai/guides/validated-forms.md +11 -22
  70. package/dist/ai/guides/validation-providers.md +4 -3
  71. package/dist/ai/helpers/inertia/manifest.json +636 -0
  72. package/dist/ai/helpers/inertia/types.ts +15 -0
  73. package/dist/ai/helpers/inertia/useAsyncFn.ts +110 -0
  74. package/dist/ai/helpers/inertia/useConfirmOnRouteLeave.ts +102 -0
  75. package/dist/ai/helpers/inertia/useDeleteCmd.ts +30 -0
  76. package/dist/ai/helpers/inertia/useHasChanged.ts +36 -0
  77. package/dist/ai/helpers/inertia/useIndexById.ts +23 -0
  78. package/dist/ai/helpers/inertia/useLazyProp.ts +148 -0
  79. package/dist/ai/helpers/inertia/useLazyPropGetter.ts +50 -0
  80. package/dist/ai/helpers/inertia/useLoading.ts +20 -0
  81. package/dist/ai/helpers/inertia/usePaginatedResponseContext.ts +88 -0
  82. package/dist/ai/helpers/inertia/usePromiseForm.ts +62 -0
  83. package/dist/ai/helpers/inertia/useProp.ts +63 -0
  84. package/dist/ai/helpers/inertia/useQuery.ts +19 -0
  85. package/dist/ai/helpers/inertia/useQueryObject.ts +116 -0
  86. package/dist/ai/helpers/inertia/useResourceContext.ts +69 -0
  87. package/dist/ai/helpers/inertia/useRouteQuery.ts +54 -0
  88. package/dist/ai/helpers/inertia/useScreenSizes.ts +23 -0
  89. package/dist/ai/helpers/inertia/useTableContext.ts +34 -0
  90. package/dist/ai/helpers/inertia/useTableSelectionContext.ts +48 -0
  91. package/dist/ai/helpers/inertia/useUntil.ts +33 -0
  92. package/dist/ai/helpers/inertia/utils.ts +24 -0
  93. package/dist/ai/index.md +6 -6
  94. package/dist/ai/manifest/components/BbAccordion.json +1 -1
  95. package/dist/ai/manifest/components/BbBreadcrumbs.json +10 -6
  96. package/dist/ai/manifest/components/BbBreadcrumbs.tree.json +1 -1
  97. package/dist/ai/manifest/components/BbButton.json +2 -1
  98. package/dist/ai/manifest/components/BbButton.tree.json +1 -1
  99. package/dist/ai/manifest/components/BbCheckboxGroup.json +2 -2
  100. package/dist/ai/manifest/components/BbCollapsible.json +1 -1
  101. package/dist/ai/manifest/components/BbColorPalette.json +1 -1
  102. package/dist/ai/manifest/components/BbConfirmPortal.json +5 -2
  103. package/dist/ai/manifest/components/BbDropdown.json +23 -20
  104. package/dist/ai/manifest/components/BbDropdown.tree.json +2 -2
  105. package/dist/ai/manifest/components/BbDropdownButton.json +22 -19
  106. package/dist/ai/manifest/components/BbDropdownButton.tree.json +1 -1
  107. package/dist/ai/manifest/components/BbDropdownGroup.json +2 -2
  108. package/dist/ai/manifest/components/BbForm.json +1 -0
  109. package/dist/ai/manifest/components/BbHoverCard.json +1 -1
  110. package/dist/ai/manifest/components/BbHoverCardPortal.json +2 -2
  111. package/dist/ai/manifest/components/BbNumberInput.json +6 -1
  112. package/dist/ai/manifest/components/BbNumberInput.tree.json +2 -1
  113. package/dist/ai/manifest/components/BbPopover.json +2 -1
  114. package/dist/ai/manifest/components/BbPopoverPortal.json +2 -2
  115. package/dist/ai/manifest/components/BbProgress.json +6 -6
  116. package/dist/ai/manifest/components/BbProgress.tree.json +1 -0
  117. package/dist/ai/manifest/components/BbRadioGroup.json +2 -2
  118. package/dist/ai/manifest/components/BbSelect.json +2 -2
  119. package/dist/ai/manifest/components/BbSelect.tree.json +2 -2
  120. package/dist/ai/manifest/components/BbSelectPopover.json +2 -2
  121. package/dist/ai/manifest/components/BbSelectPopover.tree.json +1 -1
  122. package/dist/ai/manifest/components/BbSwitchGroup.json +2 -2
  123. package/dist/ai/manifest/components/BbTabs.json +8 -7
  124. package/dist/ai/manifest/components/BbTabsList.json +3 -3
  125. package/dist/ai/manifest/components/BbTabsPanels.json +1 -1
  126. package/dist/ai/manifest/components/BbTabsRoot.json +6 -5
  127. package/dist/ai/manifest/components/BbTag.tree.json +1 -1
  128. package/dist/ai/manifest/components/BbTooltip.json +2 -2
  129. package/dist/ai/manifest/components/ChipsBox.tree.json +1 -1
  130. package/dist/ai/manifest/components/CommonPopover.json +1 -1
  131. package/dist/ai/manifest/index.json +2 -2
  132. package/dist/ai/manifest/meta.json +5 -0
  133. package/dist/ai/manifest/types.advanced.json +10 -9
  134. package/dist/ai/manifest/types.api.json +8 -5
  135. package/dist/ai/manifest/types.wrapper.json +1 -0
  136. package/dist/ai/recipes/inertia/approvals-inbox.md +16 -18
  137. package/dist/ai/recipes/inertia/inline-edit-workspace.md +23 -33
  138. package/dist/ai/recipes/inertia/layout-scaffold.md +16 -16
  139. package/dist/ai/recipes/inertia/onboarding.md +6 -10
  140. package/dist/ai/recipes/inertia/ownership-atlas.md +15 -18
  141. package/dist/ai/recipes/inertia/record-form.md +11 -11
  142. package/dist/ai/recipes/inertia/records-workspace.md +38 -41
  143. package/dist/ai/recipes/inertia/settings-workspace.md +26 -38
  144. package/dist/ai/recipes/inertia/upload-center.md +31 -39
  145. package/dist/ai/recipes/inertia/wizard-form.md +10 -19
  146. package/dist/ai/recipes/nuxt/approvals-inbox.md +7 -8
  147. package/dist/ai/recipes/nuxt/inline-edit-workspace.md +14 -24
  148. package/dist/ai/recipes/nuxt/layout-scaffold.md +16 -16
  149. package/dist/ai/recipes/nuxt/onboarding.md +2 -6
  150. package/dist/ai/recipes/nuxt/ownership-atlas.md +7 -9
  151. package/dist/ai/recipes/nuxt/record-form.md +5 -5
  152. package/dist/ai/recipes/nuxt/records-workspace.md +29 -31
  153. package/dist/ai/recipes/nuxt/settings-workspace.md +22 -34
  154. package/dist/ai/recipes/nuxt/upload-center.md +22 -29
  155. package/dist/ai/recipes/nuxt/wizard-form.md +3 -11
  156. package/dist/ai/recipes/vue/approvals-inbox.md +7 -8
  157. package/dist/ai/recipes/vue/inline-edit-workspace.md +14 -24
  158. package/dist/ai/recipes/vue/layout-scaffold.md +16 -16
  159. package/dist/ai/recipes/vue/onboarding.md +2 -6
  160. package/dist/ai/recipes/vue/ownership-atlas.md +7 -9
  161. package/dist/ai/recipes/vue/record-form.md +5 -5
  162. package/dist/ai/recipes/vue/records-workspace.md +29 -31
  163. package/dist/ai/recipes/vue/settings-workspace.md +22 -34
  164. package/dist/ai/recipes/vue/upload-center.md +22 -29
  165. package/dist/ai/recipes/vue/wizard-form.md +3 -11
  166. package/dist/ai/source/BbAccordion.md +1 -1
  167. package/dist/ai/source/BbAlert.md +3 -3
  168. package/dist/ai/source/BbBadge.md +14 -10
  169. package/dist/ai/source/BbBadgeButton.md +17 -12
  170. package/dist/ai/source/BbBaseButton.md +1 -1
  171. package/dist/ai/source/BbBaseRadioIcon.md +1 -0
  172. package/dist/ai/source/BbBaseSwitchIcon.md +1 -0
  173. package/dist/ai/source/BbBreadcrumbs.md +63 -42
  174. package/dist/ai/source/BbButton.md +84 -38
  175. package/dist/ai/source/BbCalendar.md +17 -16
  176. package/dist/ai/source/BbCheckbox.md +3 -2
  177. package/dist/ai/source/BbCheckboxGroup.md +11 -4
  178. package/dist/ai/source/BbCollapsible.md +2 -1
  179. package/dist/ai/source/BbColorInput.md +10 -9
  180. package/dist/ai/source/BbColorPalette.md +2 -1
  181. package/dist/ai/source/BbConfirm.md +18 -1
  182. package/dist/ai/source/BbConfirmPortal.md +20 -6
  183. package/dist/ai/source/BbDatePicker.md +2 -2
  184. package/dist/ai/source/BbDatePickerInput.md +47 -20
  185. package/dist/ai/source/BbDialog.md +5 -5
  186. package/dist/ai/source/BbDropdown.md +231 -158
  187. package/dist/ai/source/BbDropdownButton.md +19 -12
  188. package/dist/ai/source/BbDropdownGroup.md +127 -93
  189. package/dist/ai/source/BbForm.md +25 -1
  190. package/dist/ai/source/BbHoverCard.md +30 -3
  191. package/dist/ai/source/BbHoverCardPortal.md +18 -3
  192. package/dist/ai/source/BbIndicator.md +4 -0
  193. package/dist/ai/source/BbNumberInput.md +855 -19
  194. package/dist/ai/source/BbOffCanvas.md +27 -11
  195. package/dist/ai/source/BbPagination.md +3 -3
  196. package/dist/ai/source/BbPopover.md +185 -12
  197. package/dist/ai/source/BbPopoverPortal.md +30 -3
  198. package/dist/ai/source/BbProgress.md +82 -18
  199. package/dist/ai/source/BbRadioGroup.md +12 -5
  200. package/dist/ai/source/BbRating.md +11 -9
  201. package/dist/ai/source/BbSelect.md +16 -10
  202. package/dist/ai/source/BbSelectPopover.md +29 -23
  203. package/dist/ai/source/BbSlider.md +12 -6
  204. package/dist/ai/source/BbSwitch.md +3 -2
  205. package/dist/ai/source/BbSwitchGroup.md +5 -2
  206. package/dist/ai/source/BbTable.md +102 -18
  207. package/dist/ai/source/BbTabs.md +30 -24
  208. package/dist/ai/source/BbTabsList.md +29 -23
  209. package/dist/ai/source/BbTabsPanels.md +29 -23
  210. package/dist/ai/source/BbTabsRoot.md +28 -22
  211. package/dist/ai/source/BbTag.md +10 -9
  212. package/dist/ai/source/BbTextInput.md +5 -4
  213. package/dist/ai/source/BbTextarea.md +6 -5
  214. package/dist/ai/source/BbTimePicker.md +1 -1
  215. package/dist/ai/source/BbTimePickerInput.md +46 -9
  216. package/dist/ai/source/BbToast.md +10 -7
  217. package/dist/ai/source/BbToastPortal.md +10 -7
  218. package/dist/ai/source/BbTooltip.md +88 -11
  219. package/dist/ai/source/BbTree.md +21 -3
  220. package/dist/ai/source/CommonField.md +6 -4
  221. package/dist/ai/source/CommonFieldInput.md +15 -10
  222. package/dist/ai/source/CommonPopover.md +8 -1
  223. package/dist/ai/source/CommonTimeSelector.md +5 -4
  224. package/dist/components/BbBadge/BbBadgeButton.vue_vue_type_script_setup_true_lang.js +10 -8
  225. package/dist/components/BbBaseButton/BbBaseButton.vue_vue_type_script_setup_true_lang.js +8 -8
  226. package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +45 -45
  227. package/dist/components/BbBreadcrumbs/types.d.ts +25 -17
  228. package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +88 -70
  229. package/dist/components/BbButton/toggleRoving.d.ts +6 -0
  230. package/dist/components/BbButton/toggleRoving.js +45 -0
  231. package/dist/components/BbButton/types.d.ts +12 -4
  232. package/dist/components/BbButton/types.js +5 -1
  233. package/dist/components/BbCalendar/BbCalendar.vue_vue_type_script_setup_true_lang.js +178 -178
  234. package/dist/components/BbCalendar/CalendarMonthPanel.vue_vue_type_script_setup_true_lang.js +15 -15
  235. package/dist/components/BbCalendar/CalendarYearPanel.vue_vue_type_script_setup_true_lang.js +28 -28
  236. package/dist/components/BbCalendar/useCalendarGrid.js +141 -139
  237. package/dist/components/BbCalendar/useDayjsLocale.d.ts +38 -15
  238. package/dist/components/BbCalendar/useDayjsLocale.js +31 -30
  239. package/dist/components/BbCheckbox/BbCheckbox.vue_vue_type_script_setup_true_lang.js +48 -48
  240. package/dist/components/BbCheckboxGroup/BbCheckboxGroup.vue_vue_type_script_setup_true_lang.js +81 -80
  241. package/dist/components/BbCheckboxGroup/types.d.ts +6 -2
  242. package/dist/components/BbCollapsible/types.d.ts +2 -1
  243. package/dist/components/BbColorInput/BbColorInput.vue_vue_type_script_setup_true_lang.js +69 -69
  244. package/dist/components/BbColorPalette/types.d.ts +2 -1
  245. package/dist/components/BbConfirm/BbConfirmPortal.vue.d.ts +2 -5
  246. package/dist/components/BbConfirm/types.d.ts +15 -0
  247. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +363 -349
  248. package/dist/components/BbDropdown/AdaptiveDropdown.vue_vue_type_script_setup_true_lang.js +91 -86
  249. package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +298 -289
  250. package/dist/components/BbDropdown/BbDropdownList.vue_vue_type_script_setup_true_lang.js +322 -356
  251. package/dist/components/BbDropdown/DropdownPipelineResolver.vue_vue_type_script_setup_true_lang.js +17 -15
  252. package/dist/components/BbDropdown/DropdownRow.vue.d.ts +40 -0
  253. package/dist/components/BbDropdown/DropdownRow.vue.js +5 -0
  254. package/dist/components/BbDropdown/DropdownRow.vue_vue_type_script_setup_true_lang.js +60 -0
  255. package/dist/components/BbDropdown/normalizeGroups.d.ts +8 -2
  256. package/dist/components/BbDropdown/normalizeGroups.js +60 -46
  257. package/dist/components/BbDropdown/types.d.ts +120 -88
  258. package/dist/components/BbDropdownButton/BbDropdownButton.vue_vue_type_script_setup_true_lang.js +11 -2
  259. package/dist/components/BbDropdownButton/types.d.ts +2 -2
  260. package/dist/components/BbForm/BbForm.vue.d.ts +9 -0
  261. package/dist/components/BbForm/BbForm.vue_vue_type_script_setup_true_lang.js +114 -105
  262. package/dist/components/BbForm/BbFormCustomField.js +1 -1
  263. package/dist/components/BbForm/bindFormField.js +2 -1
  264. package/dist/components/BbForm/formFields.d.ts +5 -0
  265. package/dist/components/BbHoverCard/BbHoverCard.vue_vue_type_script_setup_true_lang.js +145 -142
  266. package/dist/components/BbHoverCard/BbHoverCardPortal.vue.d.ts +2 -2
  267. package/dist/components/BbHoverCard/types.d.ts +14 -1
  268. package/dist/components/BbNumberInput/BbNumberInput.vue.d.ts +0 -1
  269. package/dist/components/BbNumberInput/BbNumberInput.vue_vue_type_script_setup_true_lang.js +452 -125
  270. package/dist/components/BbNumberInput/numberDisplay.d.ts +256 -0
  271. package/dist/components/BbNumberInput/numberDisplay.js +499 -0
  272. package/dist/components/BbNumberInput/types.d.ts +54 -1
  273. package/dist/components/BbNumberInput/warroom/corpus.d.ts +58 -0
  274. package/dist/components/BbNumberInput/warroom/currency-verify.rows.d.ts +28 -0
  275. package/dist/components/BbNumberInput/warroom/currency.rows.d.ts +2 -0
  276. package/dist/components/BbNumberInput/warroom/harness.d.ts +217 -0
  277. package/dist/components/BbNumberInput/warroom/oracle.d.ts +150 -0
  278. package/dist/components/BbPagination/BbPagination.vue_vue_type_script_setup_true_lang.js +7 -7
  279. package/dist/components/BbPopover/BbPopover.vue.d.ts +1 -0
  280. package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +207 -162
  281. package/dist/components/BbPopover/BbPopoverPortal.vue.d.ts +2 -2
  282. package/dist/components/BbPopover/types.d.ts +26 -1
  283. package/dist/components/BbProgress/BbProgress.vue_vue_type_script_setup_true_lang.js +15 -7
  284. package/dist/components/BbProgress/types.d.ts +23 -7
  285. package/dist/components/BbProgress/types.js +1 -1
  286. package/dist/components/BbRadioGroup/BbRadioGroup.vue_vue_type_script_setup_true_lang.js +102 -101
  287. package/dist/components/BbRadioGroup/types.d.ts +6 -2
  288. package/dist/components/BbRating/BbRating.vue_vue_type_script_setup_true_lang.js +63 -63
  289. package/dist/components/BbSelect/BbSelect.vue_vue_type_script_setup_true_lang.js +156 -154
  290. package/dist/components/BbSelect/types.d.ts +6 -2
  291. package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +2 -1
  292. package/dist/components/BbSelectPopover/types.d.ts +6 -2
  293. package/dist/components/BbSlider/BbSlider.vue_vue_type_script_setup_true_lang.js +153 -153
  294. package/dist/components/BbSwitch/BbSwitch.vue_vue_type_script_setup_true_lang.js +62 -62
  295. package/dist/components/BbSwitchGroup/BbSwitchGroup.vue_vue_type_script_setup_true_lang.js +131 -130
  296. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +711 -676
  297. package/dist/components/BbTabs/BbTabs.vue_vue_type_script_setup_true_lang.js +1 -1
  298. package/dist/components/BbTabs/BbTabsList.vue_vue_type_script_setup_true_lang.js +1 -1
  299. package/dist/components/BbTabs/types.d.ts +28 -22
  300. package/dist/components/BbTag/BbTag.vue_vue_type_script_setup_true_lang.js +76 -76
  301. package/dist/components/BbTextInput/BbTextInput.vue_vue_type_script_setup_true_lang.js +58 -58
  302. package/dist/components/BbTextarea/BbTextarea.vue_vue_type_script_setup_true_lang.js +82 -82
  303. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +246 -236
  304. package/dist/components/BbTooltip/BbTooltip.vue_vue_type_script_setup_true_lang.js +99 -86
  305. package/dist/components/BbTooltip/tooltipWarmup.d.ts +23 -0
  306. package/dist/components/BbTooltip/tooltipWarmup.js +7 -0
  307. package/dist/components/BbTooltip/types.d.ts +10 -3
  308. package/dist/components/BbTree/BbTree.vue_vue_type_script_setup_true_lang.js +29 -21
  309. package/dist/components/BbTree/BbTreeLevel.vue_vue_type_script_setup_true_lang.js +10 -7
  310. package/dist/components/CommonFieldInput/CommonFieldInput.vue_vue_type_script_setup_true_lang.js +37 -36
  311. package/dist/components/CommonPopover/types.d.ts +2 -1
  312. package/dist/components/CommonTimeSelector/CommonTimeSelector.vue_vue_type_script_setup_true_lang.js +9 -9
  313. package/dist/composables/formSubmitLock.d.ts +6 -0
  314. package/dist/composables/formSubmitLock.js +4 -0
  315. package/dist/composables/useBaseOptions.d.ts +7 -1
  316. package/dist/composables/useBaseOptions.js +32 -32
  317. package/dist/composables/useBbDropdownContext.d.ts +8 -8
  318. package/dist/composables/useBbFormField.d.ts +9 -0
  319. package/dist/composables/useBbFormField.js +5 -3
  320. package/dist/composables/useBbTabsContext.d.ts +10 -10
  321. package/dist/composables/useBbTabsContext.js +18 -18
  322. package/dist/composables/useFormBinding.d.ts +11 -0
  323. package/dist/composables/useFormBinding.js +15 -13
  324. package/dist/composables/useItemText.d.ts +5 -1
  325. package/dist/composables/useItemText.js +8 -3
  326. package/dist/composables/useItemValue.d.ts +5 -1
  327. package/dist/composables/useItemValue.js +8 -3
  328. package/dist/composables/useLocale.js +10 -9
  329. package/dist/deprecation/ai-deprecations.json.d.ts +130 -0
  330. package/dist/deprecation/ai-deprecations.json.js +1 -1
  331. package/dist/deprecation/removedProps.js +1 -1
  332. package/dist/directives/bbDropdown.js +1 -1
  333. package/dist/directives/directivePortal.d.ts +13 -7
  334. package/dist/directives/directivePortal.js +1 -4
  335. package/dist/i18n/index.d.ts +7 -12
  336. package/dist/i18n/locales/cs.json +11 -14
  337. package/dist/i18n/locales/da.json +13 -16
  338. package/dist/i18n/locales/de-CH.json +137 -0
  339. package/dist/i18n/locales/de.json +14 -17
  340. package/dist/i18n/locales/el.json +10 -13
  341. package/dist/i18n/locales/en-GB.json +137 -0
  342. package/dist/i18n/locales/en.json +8 -11
  343. package/dist/i18n/locales/es-MX.json +137 -0
  344. package/dist/i18n/locales/es.json +16 -19
  345. package/dist/i18n/locales/fi.json +8 -11
  346. package/dist/i18n/locales/fr.json +23 -26
  347. package/dist/i18n/locales/hu.json +8 -11
  348. package/dist/i18n/locales/it.json +12 -15
  349. package/dist/i18n/locales/ja.json +13 -16
  350. package/dist/i18n/locales/ko.json +10 -13
  351. package/dist/i18n/locales/nb.json +13 -16
  352. package/dist/i18n/locales/nl.json +21 -24
  353. package/dist/i18n/locales/pl.json +10 -13
  354. package/dist/i18n/locales/{pt.json → pt-BR.json} +19 -22
  355. package/dist/i18n/locales/pt-PT.json +137 -0
  356. package/dist/i18n/locales/ro.json +22 -25
  357. package/dist/i18n/locales/ru.json +11 -14
  358. package/dist/i18n/locales/sv.json +16 -19
  359. package/dist/i18n/locales/tr.json +14 -17
  360. package/dist/i18n/locales/uk.json +11 -14
  361. package/dist/i18n/locales/{zh-cn.json → zh-CN.json} +10 -13
  362. package/dist/i18n/locales/zh-HK.json +137 -0
  363. package/dist/i18n/locales/zh-TW.json +137 -0
  364. package/dist/i18n/regionHints.d.ts +16 -0
  365. package/dist/i18n/regionHints.js +49 -0
  366. package/dist/i18n/regions.d.ts +286 -0
  367. package/dist/i18n/regions.js +265 -0
  368. package/dist/index.d.ts +4 -4
  369. package/dist/index.js +4 -4
  370. package/dist/llms-full.txt +1914 -1714
  371. package/dist/llms-medium.txt +76 -46
  372. package/dist/llms.txt +7 -7
  373. package/dist/locale-blueprint.json +16 -13
  374. package/dist/styles.css +2 -2
  375. package/dist/types/passthrough.d.ts +1 -1
  376. package/dist/types/ptComponentMap.d.ts +2 -2
  377. package/dist/utilities/functions/slotKey.d.ts +2 -2
  378. package/dist/utils/versionCheck.js +1 -1
  379. package/dist/vite-plugin.d.ts +22 -19
  380. package/dist/vite.js +309 -297
  381. package/llms.txt +7 -7
  382. package/package.json +5 -3
  383. package/scripts/lib/agent-manifest.mjs +2 -1
  384. package/scripts/lib/eslint-disable.mjs +17 -4
  385. package/scripts/lib/eslint-plugin.d.ts +9 -1
  386. package/scripts/lib/eslint-plugin.mjs +256 -6
  387. package/scripts/lib/helper-canon.mjs +176 -0
  388. package/scripts/lib/helpers-add.mjs +1303 -0
  389. package/scripts/lib/local-components.mjs +33 -4
  390. package/scripts/lib/locale-check.mjs +165 -0
  391. package/scripts/lib/portal-names.mjs +458 -0
  392. package/scripts/lib/redundant-item-accessor.mjs +398 -0
  393. package/scripts/lib/renamed-item-field.mjs +343 -0
  394. package/scripts/lib/slot-key.mjs +2 -2
  395. package/scripts/lib/slot-renames.mjs +1 -1
  396. package/scripts/lib/unimported-components.mjs +5 -2
  397. package/scripts/lib/validate-bb-markup.mjs +123 -56
  398. package/dist/i18n/index.js +0 -27
@@ -0,0 +1,1303 @@
1
+ /**
2
+ * `bitboss-ui add <id…>` and `bitboss-ui helpers [list|diff|update]` (Q43.11):
3
+ * copy the Inertia helper folder into an app as files the app owns, and keep
4
+ * them current without ever destroying the app's own code.
5
+ *
6
+ * The rules (plans/reviews/q43-11-copy-in-2026-09-30/PLAN.md as amended by its
7
+ * owner rulings, and Q4-CANONICAL-HASH.md):
8
+ * - Only bytes bitboss-ui published may be replaced: a file is replaceable
9
+ * when its bytes, or its canonical code, match the package's published list.
10
+ * The lock never authorises a write.
11
+ * - A file the app edited is kept. It blocks only when bitboss-ui changed it
12
+ * too (`conflict`) or when it was never installed by us (`foreign`).
13
+ * - `add` only creates and adopts; a stale file sends the user to
14
+ * `helpers update`. Nothing is ever deleted, and there is no `--force`.
15
+ * - It never spawns a process, never loads the app's code or config, never
16
+ * writes outside the helper folder except `bitboss-ui.lock.json`, and
17
+ * refuses to run as a bitboss-ui other than the one the app installed.
18
+ *
19
+ * Exit codes: 0 done / nothing to do; 1 usage error, or `helpers diff` found
20
+ * a difference to act on; 2 refused (`add` wrote nothing; `update` wrote only
21
+ * the other groups); 3 a precondition failed, nothing written; 4 a write failed
22
+ * and was rolled back.
23
+ */
24
+ import { createHash, randomBytes } from 'node:crypto';
25
+ import {
26
+ chmodSync,
27
+ closeSync,
28
+ existsSync,
29
+ lstatSync,
30
+ mkdirSync,
31
+ openSync,
32
+ readdirSync,
33
+ readFileSync,
34
+ realpathSync,
35
+ renameSync,
36
+ rmdirSync,
37
+ unlinkSync,
38
+ writeSync,
39
+ } from 'node:fs';
40
+ import { dirname, join, sep } from 'node:path';
41
+ import {
42
+ canonDialect,
43
+ canonicalHash,
44
+ CANON_VERSION,
45
+ commentList,
46
+ lfHash,
47
+ } from './helper-canon.mjs';
48
+ import { detectPackageManager } from './mcp-config.mjs';
49
+
50
+ export const LOCK_FILE = 'bitboss-ui.lock.json';
51
+ const LOCK_VERSION = 1;
52
+ const LOCK_MAX_BYTES = 1024 * 1024;
53
+ const HELPERS_DIST = 'dist/ai/helpers/inertia';
54
+ const FILE_NAME = /^[A-Za-z][A-Za-z0-9]*\.ts$/;
55
+ const HELPER_ID = /^use[A-Za-z0-9]+$/;
56
+ const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
57
+ const DEVICE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
58
+ const CLI = 'npx bitboss-ui';
59
+
60
+ /** A refusal or failure with its exit code; `message` is complete for a human. */
61
+ export class HelperError extends Error {
62
+ constructor(code, message, next = null) {
63
+ super(message);
64
+ this.code = code;
65
+ this.next = next;
66
+ }
67
+ }
68
+
69
+ const fail = (code, message, next) => {
70
+ throw new HelperError(code, message, next);
71
+ };
72
+
73
+ const sha256 = (text) => createHash('sha256').update(text).digest('hex');
74
+
75
+ // ---- context -------------------------------------------------------------
76
+
77
+ /** Read the app's package.json, the installed manifest and the lock; refuse every precondition. */
78
+ export function loadContext({ cwd, packageRoot, command }) {
79
+ let root;
80
+ try {
81
+ root = realpathSync(cwd);
82
+ } catch {
83
+ fail(3, `cannot read the current directory ${cwd}`);
84
+ }
85
+ const pkgPath = join(root, 'package.json');
86
+ if (!existsSync(pkgPath))
87
+ fail(
88
+ 3,
89
+ 'no package.json here. Run it from the app root (the folder with package.json).'
90
+ );
91
+ let app;
92
+ try {
93
+ app = JSON.parse(readFileSync(pkgPath, 'utf-8'));
94
+ } catch (error) {
95
+ fail(3, `package.json does not parse: ${error.message}`);
96
+ }
97
+ const declared = new Set(
98
+ [
99
+ 'dependencies',
100
+ 'devDependencies',
101
+ 'peerDependencies',
102
+ 'optionalDependencies',
103
+ ].flatMap((key) => Object.keys(app?.[key] ?? {}))
104
+ );
105
+ const pm = detectPackageManager(root);
106
+ const localCommand = {
107
+ npm: 'npm exec -- bitboss-ui',
108
+ pnpm: 'pnpm exec bitboss-ui',
109
+ yarn: 'yarn bitboss-ui',
110
+ bun: 'bunx --no-install bitboss-ui',
111
+ }[pm];
112
+ if (!declared.has('bitboss-ui'))
113
+ fail(
114
+ 3,
115
+ "this package.json does not declare bitboss-ui. Run it in the app that depends on bitboss-ui (in a monorepo: that package's folder, not the root)."
116
+ );
117
+
118
+ // Version guard: this CLI must BE the app's installed bitboss-ui. A bare
119
+ // `npx` elsewhere runs whatever npm resolves (v2 is still `latest`).
120
+ const self = realpathSync(packageRoot);
121
+ const installed = findInstalled(root);
122
+ const ownVersion = readVersion(self);
123
+ if (!installed || realpathSync(installed) !== self)
124
+ fail(
125
+ 3,
126
+ installed
127
+ ? `this is bitboss-ui ${ownVersion} running from ${self}, but the app uses ${readVersion(realpathSync(installed))} at ${realpathSync(installed)}. Run the app's own copy: \`${localCommand} ${command}\`.`
128
+ : `the app has no node_modules/bitboss-ui (install its dependencies first), and this is bitboss-ui ${ownVersion} from ${self}. Then run \`${localCommand} ${command}\`.`
129
+ );
130
+ if (!declared.has('@inertiajs/vue3'))
131
+ fail(
132
+ 3,
133
+ 'this app does not declare @inertiajs/vue3. The helpers are for Inertia (Vue 3) apps.'
134
+ );
135
+ if (!existsSync(join(root, 'tsconfig.json')))
136
+ fail(
137
+ 3,
138
+ 'no tsconfig.json here. The helpers are TypeScript files; JavaScript apps are not supported.'
139
+ );
140
+
141
+ const manifest = loadManifest(self);
142
+ const lock = readLock(root);
143
+ return {
144
+ root,
145
+ self,
146
+ version: ownVersion,
147
+ manifest,
148
+ lock,
149
+ declared,
150
+ pm,
151
+ localCommand,
152
+ };
153
+ }
154
+
155
+ /** The first `node_modules/bitboss-ui` walking up from `root` (directory walk-up, like Node's resolution). */
156
+ function findInstalled(root) {
157
+ let dir = root;
158
+ for (;;) {
159
+ const candidate = join(dir, 'node_modules', 'bitboss-ui');
160
+ if (existsSync(candidate)) return candidate;
161
+ const parent = dirname(dir);
162
+ if (parent === dir) return null;
163
+ dir = parent;
164
+ }
165
+ }
166
+
167
+ function readVersion(dir) {
168
+ try {
169
+ return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8')).version;
170
+ } catch {
171
+ return 'unknown';
172
+ }
173
+ }
174
+
175
+ /** The shipped manifest, with every file's bytes read and checked against its hash. */
176
+ export function loadManifest(self) {
177
+ const base = join(self, HELPERS_DIST);
178
+ let manifest;
179
+ try {
180
+ manifest = JSON.parse(readFileSync(join(base, 'manifest.json'), 'utf-8'));
181
+ } catch {
182
+ fail(
183
+ 3,
184
+ `this bitboss-ui ships no helper manifest (${HELPERS_DIST}/manifest.json). Upgrade bitboss-ui.`
185
+ );
186
+ }
187
+ const realBase = realpathSync(base);
188
+ const bytes = new Map();
189
+ for (const name of Object.keys(manifest.files ?? {})) {
190
+ if (!FILE_NAME.test(name))
191
+ fail(
192
+ 3,
193
+ `the package manifest names an invalid file "${name}" (packaging bug).`
194
+ );
195
+ const path = join(base, name);
196
+ let text;
197
+ try {
198
+ if (!realpathSync(path).startsWith(realBase + sep))
199
+ fail(
200
+ 3,
201
+ `${HELPERS_DIST}/${name} resolves outside the package (packaging bug).`
202
+ );
203
+ text = readFileSync(path, 'utf-8');
204
+ } catch (error) {
205
+ if (error instanceof HelperError) throw error;
206
+ fail(
207
+ 3,
208
+ `${HELPERS_DIST}/${name} is missing from the package (packaging bug or a partial install).`
209
+ );
210
+ }
211
+ if (sha256(text) !== manifest.files[name].sha256)
212
+ fail(
213
+ 3,
214
+ `${HELPERS_DIST}/${name} does not match its manifest hash (packaging bug or a partial install). Reinstall bitboss-ui.`
215
+ );
216
+ bytes.set(name, text);
217
+ }
218
+ return { ...manifest, bytes };
219
+ }
220
+
221
+ /** The parsed lock, or null when there is none. Never "treated as empty" when broken. */
222
+ export function readLock(root) {
223
+ const path = join(root, LOCK_FILE);
224
+ let stats;
225
+ try {
226
+ stats = lstatSync(path);
227
+ } catch {
228
+ return null;
229
+ }
230
+ if (!stats.isFile())
231
+ fail(3, `${LOCK_FILE} is not a regular file (a link or a folder).`);
232
+ if (stats.size > LOCK_MAX_BYTES) fail(3, `${LOCK_FILE} is over 1 MB.`);
233
+ const text = readFileSync(path, 'utf-8');
234
+ let data;
235
+ try {
236
+ data = JSON.parse(text);
237
+ } catch (error) {
238
+ fail(
239
+ 3,
240
+ `${LOCK_FILE} does not parse (${error.message}). Fix it, or delete the broken lines: files that still match a bitboss-ui version are re-adopted on the next run, edited ones are reported, nothing is lost.`
241
+ );
242
+ }
243
+ if (!data || typeof data !== 'object' || Array.isArray(data))
244
+ fail(3, `${LOCK_FILE} is not a JSON object.`);
245
+ if (typeof data.lockVersion === 'number' && data.lockVersion > LOCK_VERSION)
246
+ fail(
247
+ 3,
248
+ `${LOCK_FILE} is lockVersion ${data.lockVersion}, newer than this bitboss-ui reads. Upgrade bitboss-ui.`
249
+ );
250
+ const helpers = Object.hasOwn(data, 'helpers') ? data.helpers : null;
251
+ const files = {};
252
+ if (helpers?.files && typeof helpers.files === 'object')
253
+ for (const [name, entry] of Object.entries(helpers.files))
254
+ if (FILE_NAME.test(name) && typeof entry?.sha256 === 'string')
255
+ files[name] = {
256
+ sha256: entry.sha256,
257
+ version: String(entry.version ?? ''),
258
+ };
259
+ return {
260
+ text,
261
+ data,
262
+ dir: typeof helpers?.dir === 'string' ? helpers.dir : null,
263
+ ids: Array.isArray(helpers?.ids)
264
+ ? helpers.ids.filter((id) => typeof id === 'string' && HELPER_ID.test(id))
265
+ : [],
266
+ files,
267
+ };
268
+ }
269
+
270
+ // ---- the destination folder ----------------------------------------------
271
+
272
+ /** One validator for `--dir` and the lock's `dir`, every run. */
273
+ export function validateDir(value, { fromArgv }) {
274
+ if (typeof value !== 'string' || !value)
275
+ fail(3, 'the helper folder is empty.');
276
+ let dir = value;
277
+ if (fromArgv && process.platform === 'win32') dir = dir.replaceAll('\\', '/');
278
+ dir = dir.replace(/\/+$/, '');
279
+ const where = fromArgv ? '--dir' : `the "dir" in ${LOCK_FILE}`;
280
+ if (dir.startsWith('/') || /^[A-Za-z]:/.test(dir))
281
+ fail(3, `${where} "${value}" must be a path relative to the app root.`);
282
+ const segments = dir.split('/');
283
+ if (segments.length > 12) fail(3, `${where} "${value}" is too deep.`);
284
+ for (const segment of segments)
285
+ if (!SEGMENT.test(segment) || segment.endsWith('.') || DEVICE.test(segment))
286
+ fail(
287
+ 3,
288
+ `${where} "${value}": "${segment}" is not allowed (letters, digits, ".", "_" and "-", no "..", no leading dot).`
289
+ );
290
+ if (['node_modules', '.git', 'vendor'].includes(segments[0]))
291
+ fail(3, `${where} "${value}" is inside ${segments[0]}/.`);
292
+ return dir;
293
+ }
294
+
295
+ /** First run: the folder from `--dir`, or the app's layout. */
296
+ export function detectDir(root, declared) {
297
+ const isDir = (p) => {
298
+ try {
299
+ const s = lstatSync(join(root, p));
300
+ return s.isDirectory() && !s.isSymbolicLink();
301
+ } catch {
302
+ return false;
303
+ }
304
+ };
305
+ if (
306
+ isDir('resources/js') &&
307
+ (existsSync(join(root, 'artisan')) || declared.has('laravel-vite-plugin'))
308
+ )
309
+ return {
310
+ dir: 'resources/js/composables/inertia-helpers',
311
+ why: 'Laravel layout: resources/js',
312
+ };
313
+ if (isDir('src'))
314
+ return { dir: 'src/composables/inertia-helpers', why: 'src/ layout' };
315
+ fail(
316
+ 3,
317
+ 'cannot tell where the helpers go (no resources/js in a Laravel app, no src/). Pass --dir <path>, e.g. --dir resources/js/composables/inertia-helpers.'
318
+ );
319
+ }
320
+
321
+ /**
322
+ * Every existing segment of `dir` under `root` must be a real directory (no
323
+ * link, no junction) spelled with exactly this case. Returns the segments
324
+ * that do not exist yet.
325
+ */
326
+ export function checkDirPath(root, dir) {
327
+ const segments = dir.split('/');
328
+ let current = root;
329
+ for (let i = 0; i < segments.length; i++) {
330
+ const segment = segments[i];
331
+ let names;
332
+ try {
333
+ names = readdirSync(current);
334
+ } catch {
335
+ return segments.slice(i);
336
+ }
337
+ if (!names.includes(segment)) {
338
+ const other = names.find(
339
+ (n) => n.toLowerCase() === segment.toLowerCase()
340
+ );
341
+ if (other)
342
+ fail(
343
+ 3,
344
+ `${segments.slice(0, i).concat(other).join('/')} exists with a different case than ${segments.slice(0, i + 1).join('/')}. On a case-sensitive disk (Linux CI) that would be a second folder. Use the existing spelling.`
345
+ );
346
+ return segments.slice(i);
347
+ }
348
+ const next = join(current, segment);
349
+ const stats = lstatSync(next);
350
+ if (stats.isSymbolicLink() || !stats.isDirectory())
351
+ fail(
352
+ 3,
353
+ `${segments.slice(0, i + 1).join('/')} is ${stats.isSymbolicLink() ? 'a link' : 'not a folder'}; bitboss-ui only writes into real folders.`
354
+ );
355
+ current = next;
356
+ }
357
+ return [];
358
+ }
359
+
360
+ // ---- canonical comparison --------------------------------------------------
361
+
362
+ /** Vue's Babel and the hashes it can compare against, or why not. */
363
+ export async function loadCanon(manifest) {
364
+ let babelParse;
365
+ try {
366
+ ({ babelParse } = await import('vue/compiler-sfc'));
367
+ } catch {
368
+ return { available: false, reason: 'vue/compiler-sfc could not be loaded' };
369
+ }
370
+ if (typeof babelParse !== 'function')
371
+ return { available: false, reason: 'vue/compiler-sfc has no babelParse' };
372
+ const dialect = canonDialect(babelParse);
373
+ if (!dialect)
374
+ return {
375
+ available: false,
376
+ reason: "Vue's Babel builds an unknown AST shape",
377
+ };
378
+ if (manifest.canon !== CANON_VERSION)
379
+ return {
380
+ available: false,
381
+ reason: `the manifest is ${manifest.canon}, this CLI reads ${CANON_VERSION}`,
382
+ };
383
+ const anyFile = Object.values(manifest.files ?? {})[0];
384
+ if (!anyFile?.published?.[dialect])
385
+ return {
386
+ available: false,
387
+ reason: `the package publishes no ${dialect} hashes`,
388
+ };
389
+ return { available: true, dialect, babelParse };
390
+ }
391
+
392
+ // ---- classification -------------------------------------------------------
393
+
394
+ /**
395
+ * The state of one helper file in the app:
396
+ * new | deleted | current | adopt | record | stale | edited | conflict | foreign
397
+ * (`reformatted` / `commentsDiffer` refine identical and stale files).
398
+ */
399
+ export function classify({ root, dir, name, manifest, lock, canon }) {
400
+ const pkg = manifest.bytes.get(name);
401
+ const entry = lock?.files?.[name] ?? null;
402
+ const path = join(root, dir, name);
403
+ let stats;
404
+ try {
405
+ stats = lstatSync(path);
406
+ } catch {
407
+ return { name, state: entry ? 'deleted' : 'new', disk: null };
408
+ }
409
+ if (!stats.isFile())
410
+ fail(
411
+ 3,
412
+ `${dir}/${name} is ${stats.isSymbolicLink() ? 'a link' : 'not a regular file'}; bitboss-ui does not write through it.`
413
+ );
414
+ const disk = readFileSync(path, 'utf-8');
415
+ const pkgHash = lfHash(pkg);
416
+ const diskHash = lfHash(disk);
417
+ const result = { name, disk, mode: stats.mode };
418
+ const identical = (extra) => {
419
+ const state = !entry
420
+ ? 'adopt'
421
+ : entry.sha256 === pkgHash
422
+ ? 'current'
423
+ : 'record';
424
+ return { ...result, state, ...extra };
425
+ };
426
+ if (disk === pkg || diskHash === pkgHash) return identical({});
427
+ const hashOf = (text) =>
428
+ canon.available ? canonicalHash(text, canon.babelParse) : null;
429
+ const diskCanon = hashOf(disk);
430
+ const differ = () => {
431
+ const a = canon.available ? commentList(disk, canon.babelParse) : null;
432
+ const b = canon.available ? commentList(pkg, canon.babelParse) : null;
433
+ return Boolean(a && b && a.join('\n\0') !== b.join('\n\0'));
434
+ };
435
+ if (diskCanon && diskCanon === hashOf(pkg))
436
+ return identical({ reformatted: true, commentsDiffer: differ() });
437
+ const published = manifest.files[name]?.published ?? {};
438
+ if (
439
+ (published.bytes ?? []).includes(diskHash) ||
440
+ (diskCanon && (published[canon.dialect] ?? []).includes(diskCanon))
441
+ )
442
+ return { ...result, state: 'stale', commentsDiffer: differ() };
443
+ if (!entry) return { ...result, state: 'foreign' };
444
+ return { ...result, state: entry.sha256 === pkgHash ? 'edited' : 'conflict' };
445
+ }
446
+
447
+ // ---- scope ----------------------------------------------------------------
448
+
449
+ function closureOf(manifest, id) {
450
+ return manifest.helpers[id].closure;
451
+ }
452
+
453
+ function checkIds(manifest, ids) {
454
+ for (const id of ids) {
455
+ if (!HELPER_ID.test(id) || !Object.hasOwn(manifest.helpers, id)) {
456
+ const known = Object.keys(manifest.helpers).sort();
457
+ fail(
458
+ 3,
459
+ `"${id}" is not a helper. Known: ${known.join(', ')}.`,
460
+ `${CLI} helpers list`
461
+ );
462
+ }
463
+ }
464
+ }
465
+
466
+ /** Installed helpers grouped by shared files: an update never splits a group. */
467
+ export function connectedGroups(manifest, ids) {
468
+ const groups = [];
469
+ for (const id of ids) {
470
+ const files = new Set(closureOf(manifest, id));
471
+ const merged = { ids: new Set([id]), files };
472
+ for (const group of [...groups]) {
473
+ if ([...group.files].some((f) => files.has(f))) {
474
+ group.ids.forEach((g) => merged.ids.add(g));
475
+ group.files.forEach((f) => merged.files.add(f));
476
+ groups.splice(groups.indexOf(group), 1);
477
+ }
478
+ }
479
+ groups.push(merged);
480
+ }
481
+ return groups.map((g) => ({
482
+ ids: [...g.ids].sort(),
483
+ files: [...g.files].sort(),
484
+ }));
485
+ }
486
+
487
+ /** Dependencies first, so a crash never leaves a new file next to an old dependency. */
488
+ function writeOrder(manifest, names) {
489
+ const order = [];
490
+ const seen = new Set();
491
+ const visit = (name) => {
492
+ if (seen.has(name)) return;
493
+ seen.add(name);
494
+ for (const dep of manifest.files[name]?.local ?? [])
495
+ if (names.has(dep)) visit(dep);
496
+ order.push(name);
497
+ };
498
+ [...names].sort().forEach(visit);
499
+ return order;
500
+ }
501
+
502
+ // ---- the write protocol ---------------------------------------------------
503
+
504
+ const RETRY = new Set(['EPERM', 'EBUSY', 'EACCES']);
505
+
506
+ function renameWithRetry(from, to) {
507
+ const deadline = Date.now() + (process.platform === 'win32' ? 3000 : 0);
508
+ for (;;) {
509
+ try {
510
+ renameSync(from, to);
511
+ return;
512
+ } catch (error) {
513
+ if (!RETRY.has(error?.code) || Date.now() >= deadline) throw error;
514
+ const until = Date.now() + 50;
515
+ while (Date.now() < until) {
516
+ // busy-wait: a rename blocked by an antivirus scan clears in milliseconds
517
+ }
518
+ }
519
+ }
520
+ }
521
+
522
+ function writeTemp(dirPath, name, text, mode) {
523
+ const temp = join(
524
+ dirPath,
525
+ `.${name}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`
526
+ );
527
+ const fd = openSync(temp, 'wx', 0o644);
528
+ try {
529
+ writeSync(fd, text);
530
+ } finally {
531
+ closeSync(fd);
532
+ }
533
+ if (mode !== undefined) chmodSync(temp, mode & 0o7777);
534
+ return temp;
535
+ }
536
+
537
+ function serialiseLock(lock) {
538
+ const files = Object.fromEntries(
539
+ Object.entries(lock.helpers.files).sort(([a], [b]) => a.localeCompare(b))
540
+ );
541
+ return `${JSON.stringify(
542
+ {
543
+ lockVersion: LOCK_VERSION,
544
+ helpers: {
545
+ dir: lock.helpers.dir,
546
+ ids: [...lock.helpers.ids].sort(),
547
+ files,
548
+ },
549
+ },
550
+ null,
551
+ 2
552
+ )}\n`;
553
+ }
554
+
555
+ /**
556
+ * Apply `writes` (`[{ name, text, kind: 'create'|'replace', observed, mode }]`)
557
+ * and then the lock, all or nothing: temp files first, every target re-checked
558
+ * right before its rename, renames in dependency order, rollback on any failure.
559
+ */
560
+ export function applyWrites({
561
+ root,
562
+ dir,
563
+ writes,
564
+ lockText,
565
+ previousLockText,
566
+ manifest,
567
+ testHooks = {},
568
+ }) {
569
+ const dirPath = join(root, dir);
570
+ const createdDirs = [];
571
+ const temps = [];
572
+ const done = [];
573
+ try {
574
+ // Create the missing folders (checked segment by segment already).
575
+ let current = root;
576
+ for (const segment of dir.split('/')) {
577
+ current = join(current, segment);
578
+ if (!existsSync(current)) {
579
+ mkdirSync(current);
580
+ createdDirs.push(current);
581
+ }
582
+ }
583
+ const byName = new Map(writes.map((w) => [w.name, w]));
584
+ for (const name of writeOrder(manifest, new Set(byName.keys()))) {
585
+ const w = byName.get(name);
586
+ temps.push({ w, temp: writeTemp(dirPath, name, w.text, w.mode) });
587
+ }
588
+ for (const { w, temp } of temps) {
589
+ checkDirPath(root, dir);
590
+ const target = join(dirPath, w.name);
591
+ let now = null;
592
+ try {
593
+ const stats = lstatSync(target);
594
+ if (!stats.isFile())
595
+ throw new HelperError(
596
+ 4,
597
+ `${dir}/${w.name} changed into a link or folder during the run`
598
+ );
599
+ now = readFileSync(target, 'utf-8');
600
+ } catch (error) {
601
+ if (error instanceof HelperError) throw error;
602
+ }
603
+ if (
604
+ (w.kind === 'create' && now !== null) ||
605
+ (w.kind === 'replace' && now !== w.observed)
606
+ )
607
+ throw new HelperError(
608
+ 4,
609
+ `${dir}/${w.name} changed while bitboss-ui was running (an editor or a formatter?). Nothing was kept; run the command again.`
610
+ );
611
+ testHooks.beforeRename?.(w.name);
612
+ renameWithRetry(temp, target);
613
+ temps.splice(
614
+ temps.findIndex((t) => t.temp === temp),
615
+ 1,
616
+ { w, temp: null }
617
+ );
618
+ done.push(w);
619
+ }
620
+ if (lockText !== null) {
621
+ const lockPath = join(root, LOCK_FILE);
622
+ testHooks.beforeLock?.();
623
+ let onDisk = null;
624
+ try {
625
+ onDisk = readFileSync(lockPath, 'utf-8');
626
+ } catch {
627
+ // absent
628
+ }
629
+ if (onDisk !== previousLockText)
630
+ throw new HelperError(
631
+ 4,
632
+ `${LOCK_FILE} changed while bitboss-ui was running (another run at the same time?). Nothing was kept; run the command again.`
633
+ );
634
+ const temp = writeTemp(root, LOCK_FILE, lockText);
635
+ temps.push({ w: null, temp });
636
+ renameWithRetry(temp, lockPath);
637
+ }
638
+ } catch (error) {
639
+ for (const { temp } of temps)
640
+ if (temp)
641
+ try {
642
+ unlinkSync(temp);
643
+ } catch {
644
+ // already gone
645
+ }
646
+ for (const w of done.reverse()) {
647
+ const target = join(dirPath, w.name);
648
+ try {
649
+ if (w.kind === 'create') unlinkSync(target);
650
+ else
651
+ renameWithRetry(
652
+ writeTemp(dirPath, w.name, w.observed, w.mode),
653
+ target
654
+ );
655
+ } catch {
656
+ // best effort: an untouched original is re-adopted by the next run
657
+ }
658
+ }
659
+ for (const created of createdDirs.reverse())
660
+ try {
661
+ rmdirSync(created);
662
+ } catch {
663
+ // not empty: something else lives there now
664
+ }
665
+ if (error instanceof HelperError) throw error;
666
+ throw new HelperError(
667
+ 4,
668
+ `a write failed (${error.code ?? error.message}); everything was rolled back.`
669
+ );
670
+ }
671
+ }
672
+
673
+ // ---- diff -----------------------------------------------------------------
674
+
675
+ /** A unified diff of two texts (small files: an LCS table is fine). */
676
+ export function unifiedDiff(a, b, labelA, labelB, context = 3) {
677
+ const x = a.replace(/\r\n/g, '\n').split('\n');
678
+ const y = b.replace(/\r\n/g, '\n').split('\n');
679
+ if (x.length * y.length > 4_000_000)
680
+ return `--- ${labelA}\n+++ ${labelB}\n(files too large to diff)\n`;
681
+ const lcs = Array.from(
682
+ { length: x.length + 1 },
683
+ () => new Uint32Array(y.length + 1)
684
+ );
685
+ for (let i = x.length - 1; i >= 0; i--)
686
+ for (let j = y.length - 1; j >= 0; j--)
687
+ lcs[i][j] =
688
+ x[i] === y[j]
689
+ ? lcs[i + 1][j + 1] + 1
690
+ : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
691
+ const ops = [];
692
+ let i = 0;
693
+ let j = 0;
694
+ while (i < x.length || j < y.length) {
695
+ if (i < x.length && j < y.length && x[i] === y[j])
696
+ ops.push([' ', x[i++], j++]);
697
+ else if (j < y.length && (i >= x.length || lcs[i][j + 1] >= lcs[i + 1][j]))
698
+ ops.push(['+', y[j++]]);
699
+ else ops.push(['-', x[i++]]);
700
+ }
701
+ const out = [`--- ${labelA}`, `+++ ${labelB}`];
702
+ let k = 0;
703
+ while (k < ops.length) {
704
+ if (ops[k][0] === ' ') {
705
+ k++;
706
+ continue;
707
+ }
708
+ const start = Math.max(0, k - context);
709
+ let end = k;
710
+ while (end < ops.length) {
711
+ if (ops[end][0] !== ' ') {
712
+ end++;
713
+ continue;
714
+ }
715
+ let run = end;
716
+ while (run < ops.length && ops[run][0] === ' ') run++;
717
+ if (run - end > context * 2 || run === ops.length) {
718
+ end = Math.min(run, end + context);
719
+ break;
720
+ }
721
+ end = run;
722
+ }
723
+ out.push('@@');
724
+ for (const [op, line] of ops.slice(start, end)) out.push(`${op}${line}`);
725
+ k = end;
726
+ }
727
+ return `${out.join('\n')}\n`;
728
+ }
729
+
730
+ /** Terminal-safe: control characters and bidi overrides become visible escapes. */
731
+ export function escapeForTerminal(text) {
732
+ return text.replace(
733
+ // eslint-disable-next-line no-control-regex -- matching control characters is the point
734
+ /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g,
735
+ (c) => `\\u{${c.codePointAt(0).toString(16)}}`
736
+ );
737
+ }
738
+
739
+ // ---- the commands -----------------------------------------------------------
740
+
741
+ function missingPackages(ctx, names) {
742
+ const runtime = new Set();
743
+ const types = new Set();
744
+ for (const name of names) {
745
+ (ctx.manifest.files[name]?.packages ?? []).forEach((p) => runtime.add(p));
746
+ (ctx.manifest.files[name]?.typePackages ?? []).forEach((p) => types.add(p));
747
+ }
748
+ runtime.forEach((p) => types.delete(p));
749
+ const missing = (set) => [...set].filter((p) => !ctx.declared.has(p)).sort();
750
+ const run = missing(runtime);
751
+ const dev = missing(types);
752
+ const verb = { npm: 'i', pnpm: 'add', yarn: 'add', bun: 'add' }[ctx.pm];
753
+ const install = [
754
+ ...(run.length ? [`${ctx.pm} ${verb} ${run.join(' ')}`] : []),
755
+ ...(dev.length ? [`${ctx.pm} ${verb} -D ${dev.join(' ')}`] : []),
756
+ ];
757
+ return { missing: [...run, ...dev], install };
758
+ }
759
+
760
+ const STATE_WORD = {
761
+ new: 'create',
762
+ deleted: 'create',
763
+ current: 'identical',
764
+ adopt: 'adopt',
765
+ record: 'record',
766
+ stale: 'stale',
767
+ edited: 'keep',
768
+ conflict: 'conflict',
769
+ foreign: 'foreign',
770
+ };
771
+
772
+ function note(file) {
773
+ const parts = [];
774
+ if (file.state === 'edited') parts.push('your edit is kept');
775
+ if (file.state === 'conflict')
776
+ parts.push('you edited it, and bitboss-ui changed it since your copy');
777
+ if (file.state === 'foreign')
778
+ parts.push('a file bitboss-ui did not install, with other content');
779
+ if (file.state === 'stale') parts.push('an older bitboss-ui version');
780
+ if (file.reformatted) parts.push('reformatted');
781
+ if (file.commentsDiffer)
782
+ parts.push(
783
+ file.state === 'stale'
784
+ ? 'your comments differ and would be replaced'
785
+ : 'your comments differ'
786
+ );
787
+ return parts.length ? ` (${parts.join('; ')})` : '';
788
+ }
789
+
790
+ /** Run one command. Returns `{ code, report, lines }`; never prints, never exits. */
791
+ export async function runHelpers({
792
+ command,
793
+ ids = [],
794
+ options = {},
795
+ cwd,
796
+ packageRoot,
797
+ testHooks,
798
+ }) {
799
+ const label = command === 'add' ? 'add' : `helpers ${command}`;
800
+ try {
801
+ const ctx = loadContext({ cwd, packageRoot, command: label });
802
+ const canon = await loadCanon(ctx.manifest);
803
+ if (command === 'list') return list(ctx, canon);
804
+ if (command === 'add') return add(ctx, canon, ids, options, testHooks);
805
+ if (command === 'diff') return diff(ctx, canon, ids);
806
+ if (command === 'update')
807
+ return update(ctx, canon, ids, options, testHooks);
808
+ fail(1, `unknown helpers command "${command}" (list, diff, update).`);
809
+ } catch (error) {
810
+ if (!(error instanceof HelperError)) throw error;
811
+ return {
812
+ code: error.code,
813
+ report: {
814
+ ok: false,
815
+ command: label,
816
+ error: error.message,
817
+ next: error.next,
818
+ },
819
+ lines: [
820
+ `[bitboss-ui] ${label}: ${error.message}`,
821
+ ...(error.next ? [` next: ${error.next}`] : []),
822
+ ],
823
+ stderr: true,
824
+ };
825
+ }
826
+ }
827
+
828
+ function resolveDir(ctx, options, { required }) {
829
+ const fromLock = ctx.lock?.dir
830
+ ? validateDir(ctx.lock.dir, { fromArgv: false })
831
+ : null;
832
+ if (options.dir !== undefined) {
833
+ const asked = validateDir(options.dir, { fromArgv: true });
834
+ if (fromLock && asked !== fromLock)
835
+ fail(
836
+ 3,
837
+ `--dir ${asked} differs from the folder in ${LOCK_FILE} (${fromLock}). To move the helpers, move the folder and edit "dir" in the lock.`
838
+ );
839
+ return { dir: asked, why: fromLock ? 'from the lock' : 'from --dir' };
840
+ }
841
+ if (fromLock) return { dir: fromLock, why: 'from the lock' };
842
+ if (!required) return null;
843
+ return detectDir(ctx.root, ctx.declared);
844
+ }
845
+
846
+ function header(ctx, label, dir, dryRun, canon) {
847
+ return [
848
+ `[bitboss-ui] ${label} · bitboss-ui ${ctx.version}${dryRun ? ' · DRY RUN, nothing written' : ''}`,
849
+ ...(dir ? [` dir ${dir.dir} (${dir.why})`] : []),
850
+ ...(canon.available
851
+ ? []
852
+ : [
853
+ ` note formatter-proof check unavailable: ${canon.reason}; reformatted helpers read as edited`,
854
+ ]),
855
+ ];
856
+ }
857
+
858
+ function fileLine(file) {
859
+ return ` ${STATE_WORD[file.state].padEnd(10)} ${file.name}${note(file)}`;
860
+ }
861
+
862
+ function newLock(ctx, dir, ids, files) {
863
+ const lock = {
864
+ helpers: {
865
+ dir,
866
+ ids: new Set(ctx.lock?.ids ?? []),
867
+ files: { ...(ctx.lock?.files ?? {}) },
868
+ },
869
+ };
870
+ ids.forEach((id) => lock.helpers.ids.add(id));
871
+ for (const file of files)
872
+ if (['new', 'deleted', 'adopt', 'record', 'stale'].includes(file.state))
873
+ lock.helpers.files[file.name] = {
874
+ sha256: lfHash(ctx.manifest.bytes.get(file.name)),
875
+ version: ctx.version,
876
+ };
877
+ const text = serialiseLock(lock);
878
+ const before = ctx.lock
879
+ ? serialiseLock({
880
+ helpers: {
881
+ dir: ctx.lock.dir,
882
+ ids: new Set(ctx.lock.ids),
883
+ files: ctx.lock.files,
884
+ },
885
+ })
886
+ : null;
887
+ return text === before ? null : text;
888
+ }
889
+
890
+ function writesFor(ctx, files) {
891
+ return files
892
+ .filter((f) => ['new', 'deleted', 'stale'].includes(f.state))
893
+ .map((f) => ({
894
+ name: f.name,
895
+ text: ctx.manifest.bytes.get(f.name),
896
+ kind: f.disk === null ? 'create' : 'replace',
897
+ observed: f.disk,
898
+ mode: f.mode,
899
+ }));
900
+ }
901
+
902
+ function packagesLines(pkgs) {
903
+ if (!pkgs.missing.length) return [];
904
+ return [
905
+ ` packages not in package.json: ${pkgs.missing.join(', ')}`,
906
+ ...pkgs.install.map((line) => ` ${line}`),
907
+ ];
908
+ }
909
+
910
+ function add(ctx, canon, named, options, testHooks) {
911
+ if (options.all && named.length)
912
+ fail(1, 'pass helper ids or --all, not both.');
913
+ const ids = options.all ? Object.keys(ctx.manifest.helpers).sort() : named;
914
+ if (!ids.length)
915
+ fail(
916
+ 1,
917
+ 'name at least one helper, e.g. `add useQuery`, or pass --all.',
918
+ `${CLI} helpers list`
919
+ );
920
+ checkIds(ctx.manifest, ids);
921
+ const dir = resolveDir(ctx, options, { required: true });
922
+ checkDirPath(ctx.root, dir.dir);
923
+ const names = [
924
+ ...new Set(ids.flatMap((id) => closureOf(ctx.manifest, id))),
925
+ ].sort();
926
+ const files = names.map((name) =>
927
+ classify({
928
+ root: ctx.root,
929
+ dir: dir.dir,
930
+ name,
931
+ manifest: ctx.manifest,
932
+ lock: ctx.lock,
933
+ canon,
934
+ })
935
+ );
936
+ const refused = files.filter((f) =>
937
+ ['stale', 'conflict', 'foreign'].includes(f.state)
938
+ );
939
+ const pkgs = missingPackages(ctx, names);
940
+ const lines = [
941
+ ...header(ctx, `add ${ids.join(' ')}`, dir, options.dryRun, canon),
942
+ ...files.map(fileLine),
943
+ ];
944
+ const report = {
945
+ ok: refused.length === 0,
946
+ command: 'add',
947
+ dryRun: Boolean(options.dryRun),
948
+ version: ctx.version,
949
+ dir: dir.dir,
950
+ files: files.map(({ name, state, reformatted, commentsDiffer }) => ({
951
+ file: name,
952
+ state,
953
+ ...(reformatted ? { reformatted } : {}),
954
+ ...(commentsDiffer ? { commentsDiffer } : {}),
955
+ })),
956
+ packages: pkgs,
957
+ canon: canon.available
958
+ ? { available: true }
959
+ : { available: false, reason: canon.reason },
960
+ };
961
+ if (refused.length) {
962
+ const stale = refused.every((f) => f.state === 'stale');
963
+ const next = stale
964
+ ? `${CLI} helpers update --dry-run`
965
+ : `${CLI} helpers diff ${refused.map((f) => f.name.replace(/\.ts$/, '')).join(' ')}`;
966
+ lines.push(
967
+ stale
968
+ ? '[bitboss-ui] add REFUSED, nothing written: an older bitboss-ui version of these files is installed. Update them first, then add:'
969
+ : "[bitboss-ui] add REFUSED, nothing written. Show the user the difference between their copy and bitboss-ui's:",
970
+ ` ${next}`
971
+ );
972
+ return {
973
+ code: 2,
974
+ report: { ...report, refused: refused.map((f) => f.name), next },
975
+ lines,
976
+ };
977
+ }
978
+ const writes = writesFor(ctx, files);
979
+ const lockText = newLock(ctx, dir.dir, ids, files);
980
+ lines.push(...packagesLines(pkgs));
981
+ if (!writes.length && lockText === null) {
982
+ lines.push('[bitboss-ui] nothing to do.');
983
+ return { code: 0, report: { ...report, next: null }, lines };
984
+ }
985
+ if (!options.dryRun)
986
+ applyWrites({
987
+ root: ctx.root,
988
+ dir: dir.dir,
989
+ writes,
990
+ lockText,
991
+ previousLockText: ctx.lock?.text ?? null,
992
+ manifest: ctx.manifest,
993
+ testHooks,
994
+ });
995
+ lines.push(
996
+ `[bitboss-ui] ${options.dryRun ? 'would write' : 'wrote'} ${writes.length} file(s)${lockText !== null ? ` and ${LOCK_FILE}` : ''}.`,
997
+ ...ids.map(
998
+ (id) =>
999
+ ` import { ${id} } from '@/${dir.dir.replace(/^(resources\/js|src)\//, '')}/${id}';`
1000
+ )
1001
+ );
1002
+ return {
1003
+ code: 0,
1004
+ report: {
1005
+ ...report,
1006
+ wrote: options.dryRun ? 0 : writes.length,
1007
+ next: null,
1008
+ },
1009
+ lines,
1010
+ };
1011
+ }
1012
+
1013
+ function installedIds(ctx) {
1014
+ return (ctx.lock?.ids ?? []).filter((id) =>
1015
+ Object.hasOwn(ctx.manifest.helpers, id)
1016
+ );
1017
+ }
1018
+
1019
+ function update(ctx, canon, ids, options, testHooks) {
1020
+ if (!ctx.lock?.dir) {
1021
+ return {
1022
+ code: 0,
1023
+ report: {
1024
+ ok: true,
1025
+ command: 'helpers update',
1026
+ installed: [],
1027
+ next: `${CLI} add <id>`,
1028
+ },
1029
+ lines: [
1030
+ `[bitboss-ui] helpers update: no helpers installed here (no ${LOCK_FILE}). Install one: ${CLI} add <id>`,
1031
+ ],
1032
+ };
1033
+ }
1034
+ checkIds(ctx.manifest, ids);
1035
+ const installed = installedIds(ctx);
1036
+ const notInstalled = ids.filter((id) => !installed.includes(id));
1037
+ if (notInstalled.length)
1038
+ fail(
1039
+ 3,
1040
+ `${notInstalled.join(', ')} ${notInstalled.length > 1 ? 'are' : 'is'} not installed here.`,
1041
+ `${CLI} add ${notInstalled.join(' ')}`
1042
+ );
1043
+ const dir = resolveDir(ctx, {}, { required: true });
1044
+ checkDirPath(ctx.root, dir.dir);
1045
+ const groups = connectedGroups(ctx.manifest, installed).filter(
1046
+ (g) => !ids.length || g.ids.some((id) => ids.includes(id))
1047
+ );
1048
+ const lines = [
1049
+ ...header(
1050
+ ctx,
1051
+ `helpers update${ids.length ? ` ${ids.join(' ')}` : ''}`,
1052
+ dir,
1053
+ options.dryRun,
1054
+ canon
1055
+ ),
1056
+ ];
1057
+ const applied = [];
1058
+ const refusedGroups = [];
1059
+ const allFiles = [];
1060
+ for (const group of groups) {
1061
+ const files = group.files.map((name) =>
1062
+ classify({
1063
+ root: ctx.root,
1064
+ dir: dir.dir,
1065
+ name,
1066
+ manifest: ctx.manifest,
1067
+ lock: ctx.lock,
1068
+ canon,
1069
+ })
1070
+ );
1071
+ allFiles.push(...files);
1072
+ lines.push(` group ${group.ids.join(', ')}`);
1073
+ lines.push(...files.map((f) => ` ${fileLine(f)}`));
1074
+ const blocked = files.filter((f) =>
1075
+ ['conflict', 'foreign'].includes(f.state)
1076
+ );
1077
+ if (blocked.length) refusedGroups.push({ group, blocked });
1078
+ else applied.push(...files);
1079
+ }
1080
+ const removed = Object.keys(ctx.lock.files).filter(
1081
+ (name) => !Object.hasOwn(ctx.manifest.files, name)
1082
+ );
1083
+ lines.push(
1084
+ ...removed.map(
1085
+ (name) =>
1086
+ ` ${'removed'.padEnd(10)} ${name} (no longer shipped; kept, never deleted)`
1087
+ )
1088
+ );
1089
+ const writes = writesFor(ctx, applied);
1090
+ const lockText = newLock(ctx, dir.dir, [], applied);
1091
+ const pkgs = missingPackages(ctx, [...new Set(applied.map((f) => f.name))]);
1092
+ lines.push(...packagesLines(pkgs));
1093
+ if ((writes.length || lockText !== null) && !options.dryRun)
1094
+ applyWrites({
1095
+ root: ctx.root,
1096
+ dir: dir.dir,
1097
+ writes,
1098
+ lockText,
1099
+ previousLockText: ctx.lock.text,
1100
+ manifest: ctx.manifest,
1101
+ testHooks,
1102
+ });
1103
+ const next = refusedGroups.length
1104
+ ? `${CLI} helpers diff ${refusedGroups.flatMap((r) => r.blocked.map((f) => f.name.replace(/\.ts$/, ''))).join(' ')}`
1105
+ : null;
1106
+ if (!writes.length && lockText === null && !refusedGroups.length)
1107
+ lines.push('[bitboss-ui] nothing to do.');
1108
+ else
1109
+ lines.push(
1110
+ `[bitboss-ui] ${options.dryRun ? 'would write' : 'wrote'} ${writes.length} file(s)${lockText !== null ? ` and ${LOCK_FILE}` : ''}.`
1111
+ );
1112
+ if (refusedGroups.length)
1113
+ lines.push(
1114
+ `[bitboss-ui] REFUSED ${refusedGroups.length} group(s), left untouched: ${refusedGroups.map((r) => r.group.ids.join(', ')).join(' | ')}. Show the user the difference:`,
1115
+ ` ${next}`
1116
+ );
1117
+ return {
1118
+ code: refusedGroups.length ? 2 : 0,
1119
+ report: {
1120
+ ok: !refusedGroups.length,
1121
+ command: 'helpers update',
1122
+ dryRun: Boolean(options.dryRun),
1123
+ version: ctx.version,
1124
+ dir: dir.dir,
1125
+ files: allFiles.map(({ name, state, reformatted, commentsDiffer }) => ({
1126
+ file: name,
1127
+ state,
1128
+ ...(reformatted ? { reformatted } : {}),
1129
+ ...(commentsDiffer ? { commentsDiffer } : {}),
1130
+ })),
1131
+ refused: refusedGroups.map((r) => ({
1132
+ ids: r.group.ids,
1133
+ files: r.blocked.map((f) => f.name),
1134
+ })),
1135
+ removed,
1136
+ packages: pkgs,
1137
+ wrote: options.dryRun ? 0 : writes.length,
1138
+ canon: canon.available
1139
+ ? { available: true }
1140
+ : { available: false, reason: canon.reason },
1141
+ next,
1142
+ },
1143
+ lines,
1144
+ };
1145
+ }
1146
+
1147
+ function diff(ctx, canon, ids) {
1148
+ checkIds(ctx.manifest, ids);
1149
+ if (!ctx.lock?.dir && !ids.length)
1150
+ return {
1151
+ code: 0,
1152
+ report: { ok: true, command: 'helpers diff', files: [], next: null },
1153
+ lines: [
1154
+ `[bitboss-ui] helpers diff: no helpers installed here (no ${LOCK_FILE}).`,
1155
+ ],
1156
+ };
1157
+ const dir = resolveDir(ctx, {}, { required: true });
1158
+ checkDirPath(ctx.root, dir.dir);
1159
+ const names = ids.length
1160
+ ? [...new Set(ids.flatMap((id) => closureOf(ctx.manifest, id)))].sort()
1161
+ : Object.keys(ctx.lock.files)
1162
+ .filter((n) => Object.hasOwn(ctx.manifest.files, n))
1163
+ .sort();
1164
+ const files = names.map((name) =>
1165
+ classify({
1166
+ root: ctx.root,
1167
+ dir: dir.dir,
1168
+ name,
1169
+ manifest: ctx.manifest,
1170
+ lock: ctx.lock,
1171
+ canon,
1172
+ })
1173
+ );
1174
+ const lines = [
1175
+ ...header(
1176
+ ctx,
1177
+ `helpers diff${ids.length ? ` ${ids.join(' ')}` : ''}`,
1178
+ dir,
1179
+ false,
1180
+ canon
1181
+ ),
1182
+ ];
1183
+ const report = [];
1184
+ for (const file of files) {
1185
+ lines.push(fileLine(file));
1186
+ const entry = { file: file.name, state: file.state };
1187
+ if (file.reformatted) entry.reformatted = true;
1188
+ if (file.commentsDiffer) entry.commentsDiffer = true;
1189
+ const show =
1190
+ ids.length &&
1191
+ file.disk !== null &&
1192
+ file.disk !== ctx.manifest.bytes.get(file.name);
1193
+ if (show) {
1194
+ const text = unifiedDiff(
1195
+ file.disk,
1196
+ ctx.manifest.bytes.get(file.name),
1197
+ `${dir.dir}/${file.name} (yours)`,
1198
+ `${file.name} (bitboss-ui ${ctx.version})`
1199
+ );
1200
+ entry.diff = text;
1201
+ if (file.reformatted)
1202
+ lines.push(
1203
+ ` ${file.commentsDiffer ? 'formatting and comments only' : 'formatting only'}`
1204
+ );
1205
+ lines.push(
1206
+ ...escapeForTerminal(text)
1207
+ .trimEnd()
1208
+ .split('\n')
1209
+ .map((l) => ` ${l}`)
1210
+ );
1211
+ }
1212
+ report.push(entry);
1213
+ }
1214
+ if (files.some((f) => f.state === 'stale' && f.commentsDiffer))
1215
+ lines.push(
1216
+ '[bitboss-ui] comments do not count as edits: `helpers update` replaces a stale file whole, so the comments shown above would be lost. Copy any you want to keep first.'
1217
+ );
1218
+ const act = files.filter((f) =>
1219
+ [
1220
+ 'stale',
1221
+ 'conflict',
1222
+ 'foreign',
1223
+ 'deleted',
1224
+ 'new',
1225
+ 'adopt',
1226
+ 'record',
1227
+ ].includes(f.state)
1228
+ );
1229
+ return {
1230
+ code: act.length ? 1 : 0,
1231
+ report: {
1232
+ ok: true,
1233
+ kind: act.length ? 'difference' : 'none',
1234
+ command: 'helpers diff',
1235
+ version: ctx.version,
1236
+ dir: dir.dir,
1237
+ files: report,
1238
+ canon: canon.available
1239
+ ? { available: true }
1240
+ : { available: false, reason: canon.reason },
1241
+ next: act.some((f) => f.state === 'stale')
1242
+ ? `${CLI} helpers update --dry-run`
1243
+ : null,
1244
+ },
1245
+ lines,
1246
+ };
1247
+ }
1248
+
1249
+ function list(ctx, canon) {
1250
+ const dir = ctx.lock?.dir ? resolveDir(ctx, {}, { required: false }) : null;
1251
+ if (dir) checkDirPath(ctx.root, dir.dir);
1252
+ const installed = new Set(installedIds(ctx));
1253
+ const lines = [...header(ctx, 'helpers list', dir, false, canon)];
1254
+ const helpers = [];
1255
+ for (const [id, helper] of Object.entries(ctx.manifest.helpers).sort(
1256
+ ([a], [b]) => a.localeCompare(b)
1257
+ )) {
1258
+ let state = null;
1259
+ if (dir && installed.has(id)) {
1260
+ const states = helper.closure.map(
1261
+ (name) =>
1262
+ classify({
1263
+ root: ctx.root,
1264
+ dir: dir.dir,
1265
+ name,
1266
+ manifest: ctx.manifest,
1267
+ lock: ctx.lock,
1268
+ canon,
1269
+ }).state
1270
+ );
1271
+ state =
1272
+ ['conflict', 'foreign', 'stale', 'deleted', 'edited'].find((s) =>
1273
+ states.includes(s)
1274
+ ) ?? 'current';
1275
+ }
1276
+ helpers.push({
1277
+ id,
1278
+ summary: helper.summary,
1279
+ files: helper.closure,
1280
+ packages: [...helper.packages, ...helper.typePackages],
1281
+ state,
1282
+ });
1283
+ lines.push(
1284
+ ` ${id.padEnd(30)} ${state ? `[${state}] ` : ''}${helper.summary}`
1285
+ );
1286
+ }
1287
+ lines.push(`[bitboss-ui] install one: ${CLI} add <id>`);
1288
+ return {
1289
+ code: 0,
1290
+ report: {
1291
+ ok: true,
1292
+ command: 'helpers list',
1293
+ version: ctx.version,
1294
+ dir: dir?.dir ?? null,
1295
+ testedWith: ctx.manifest.tested,
1296
+ helpers,
1297
+ },
1298
+ lines,
1299
+ };
1300
+ }
1301
+
1302
+ /** Exposed for tests. */
1303
+ export const _internal = { writeOrder, serialiseLock };