@rainbow-robotics/muscat-plugin-kit 1.10.1-dev.1 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +282 -282
  2. package/cli/README.md +274 -274
  3. package/cli/bin/create-muscat-plugin.mjs +8 -8
  4. package/cli/bin/muscat-pk.mjs +155 -155
  5. package/cli/bundle-templates.mjs +31 -31
  6. package/cli/docker/Dockerfile.cpp +28 -28
  7. package/cli/docker/Dockerfile.dev +30 -30
  8. package/cli/docker/Dockerfile.python +21 -21
  9. package/cli/m5-e2e.mjs +69 -69
  10. package/cli/prepack.mjs +10 -10
  11. package/cli/src/ai-rules.mjs +70 -70
  12. package/cli/src/build.mjs +447 -447
  13. package/cli/src/check-package.mjs +13 -13
  14. package/cli/src/completion.mjs +166 -166
  15. package/cli/src/cpp.mjs +78 -78
  16. package/cli/src/cppIde.mjs +75 -75
  17. package/cli/src/dev.mjs +165 -165
  18. package/cli/src/devBack.mjs +734 -727
  19. package/cli/src/devUi.mjs +247 -243
  20. package/cli/src/esbuild.mjs +56 -56
  21. package/cli/src/host_agent.py +223 -223
  22. package/cli/src/manifest.mjs +287 -287
  23. package/cli/src/mcp.mjs +1071 -1071
  24. package/cli/src/muscatLocal.mjs +209 -209
  25. package/cli/src/new.mjs +343 -343
  26. package/cli/src/pluginDevServer.mjs +111 -111
  27. package/cli/src/ports.mjs +193 -193
  28. package/cli/src/python.mjs +112 -112
  29. package/cli/src/ros2.mjs +109 -109
  30. package/cli/src/sdk.mjs +117 -117
  31. package/cli/src/storybook.mjs +168 -168
  32. package/cli/src/type.mjs +141 -141
  33. package/cli/src/uninstall.mjs +81 -81
  34. package/cli/src/update.mjs +295 -295
  35. package/cli/src/util.mjs +441 -441
  36. package/cli/src/zenohDev.mjs +83 -81
  37. package/cli/storybook-dist/_design-tokens.css +4957 -4957
  38. package/cli/storybook-dist/_stories-src/NxAvatar.stories.tsx +27 -27
  39. package/cli/storybook-dist/_stories-src/NxPagination.stories.tsx +28 -28
  40. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordion.stories.tsx +46 -46
  41. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordionGroup.stories.tsx +73 -73
  42. package/cli/storybook-dist/_stories-src/RBBadge/RBBadge.stories.tsx +93 -93
  43. package/cli/storybook-dist/_stories-src/RBBattery/RBBattery.stories.tsx +74 -74
  44. package/cli/storybook-dist/_stories-src/RBButton/RBCheckIconButton.stories.tsx +28 -28
  45. package/cli/storybook-dist/_stories-src/RBButton/RBCheckLabelButton.stories.tsx +84 -84
  46. package/cli/storybook-dist/_stories-src/RBButton/RBIconButton.stories.tsx +57 -57
  47. package/cli/storybook-dist/_stories-src/RBButton/RBInlineLabelButton.stories.tsx +41 -41
  48. package/cli/storybook-dist/_stories-src/RBButton/RBLabelButton.stories.tsx +60 -60
  49. package/cli/storybook-dist/_stories-src/RBDatePicker/RBDateInput.stories.tsx +44 -44
  50. package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +56 -56
  51. package/cli/storybook-dist/_stories-src/RBDialog/RBAlert.stories.tsx +51 -51
  52. package/cli/storybook-dist/_stories-src/RBDialog/RBConfirm.stories.tsx +55 -55
  53. package/cli/storybook-dist/_stories-src/RBDialog/RBPrompt.stories.tsx +52 -52
  54. package/cli/storybook-dist/_stories-src/RBDivider/RBDivider.stories.tsx +93 -93
  55. package/cli/storybook-dist/_stories-src/RBEmptyView/RBEmptyView.stories.tsx +49 -49
  56. package/cli/storybook-dist/_stories-src/RBIcons/RBColorIcons.stories.tsx +108 -108
  57. package/cli/storybook-dist/_stories-src/RBIcons/RBIcons.stories.tsx +106 -106
  58. package/cli/storybook-dist/_stories-src/RBInput/RBCheckBox.stories.tsx +89 -89
  59. package/cli/storybook-dist/_stories-src/RBInput/RBMultiRangeInput.stories.tsx +64 -64
  60. package/cli/storybook-dist/_stories-src/RBInput/RBRadioBox.stories.tsx +65 -65
  61. package/cli/storybook-dist/_stories-src/RBInput/RBRadioButton.stories.tsx +68 -68
  62. package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +216 -216
  63. package/cli/storybook-dist/_stories-src/RBInput/RBSideNumberInput.stories.tsx +90 -90
  64. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextArea.stories.tsx +45 -45
  65. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextInput.stories.tsx +50 -50
  66. package/cli/storybook-dist/_stories-src/RBInput/RBTextField.stories.tsx +102 -102
  67. package/cli/storybook-dist/_stories-src/RBJoyStick/RBJoyStick.stories.tsx +31 -31
  68. package/cli/storybook-dist/_stories-src/RBLight/RBLight.stories.tsx +36 -36
  69. package/cli/storybook-dist/_stories-src/RBPagination/RBPagination.stories.tsx +49 -49
  70. package/cli/storybook-dist/_stories-src/RBShadcn/RBCard/RBCard.stories.tsx +89 -89
  71. package/cli/storybook-dist/_stories-src/RBShadcn/RBCommand/RBCommand.stories.tsx +116 -116
  72. package/cli/storybook-dist/_stories-src/RBShadcn/RBDialog/RBDialog.stories.tsx +74 -74
  73. package/cli/storybook-dist/_stories-src/RBShadcn/RBDrawer/RBDrawer.stories.tsx +99 -99
  74. package/cli/storybook-dist/_stories-src/RBShadcn/RBDropdownMenu/RBDropdownMenu.stories.tsx +123 -123
  75. package/cli/storybook-dist/_stories-src/RBShadcn/RBMenubar/RBMenubar.stories.tsx +135 -135
  76. package/cli/storybook-dist/_stories-src/RBShadcn/RBPopover/RBPopover.stories.tsx +69 -69
  77. package/cli/storybook-dist/_stories-src/RBShadcn/RBResizable/RBResizable.stories.tsx +49 -49
  78. package/cli/storybook-dist/_stories-src/RBShadcn/RBSheet/RBSheet.stories.tsx +91 -91
  79. package/cli/storybook-dist/_stories-src/RBShadcn/RBSlider/RBSlider.stories.tsx +38 -38
  80. package/cli/storybook-dist/_stories-src/RBShadcn/RBSwitch/RBSwitch.stories.tsx +112 -112
  81. package/cli/storybook-dist/_stories-src/RBShadcn/RBTabs/RBTabs.stories.tsx +75 -75
  82. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggle/RBToggle.stories.tsx +51 -51
  83. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggleGroup/RBToggleGroup.stories.tsx +84 -84
  84. package/cli/storybook-dist/_stories-src/RBTab/RBChipTab.stories.tsx +102 -102
  85. package/cli/storybook-dist/_stories-src/RBTab/RBChipsBoxTab.stories.tsx +78 -78
  86. package/cli/storybook-dist/_stories-src/RBTab/RBTab.stories.tsx +57 -57
  87. package/cli/storybook-dist/_stories-src/RBTable/RBTable.stories.tsx +177 -177
  88. package/cli/storybook-dist/_stories-src/RBTable/RBTableRow.stories.tsx +64 -64
  89. package/cli/storybook-dist/_stories-src/RBToast/RBToast.stories.tsx +117 -117
  90. package/cli/storybook-dist/_stories-src/RBTooltip/RBTooltip.stories.tsx +50 -50
  91. package/cli/storybook-dist/_stories-src/loading/RBSpinner.stories.tsx +30 -30
  92. package/cli/storybook-dist/_stories-src/nx-button/NxButton.stories.tsx +54 -54
  93. package/cli/storybook-dist/_stories-src/nx-button/NxPointerHoldButton.stories.tsx +57 -57
  94. package/cli/storybook-dist/_stories-src/nx-icons/NxIcons.stories.tsx +48 -48
  95. package/cli/storybook-dist/_stories-src/nx-inputs/NxNumberInput.stories.tsx +103 -103
  96. package/cli/storybook-dist/_stories-src/nx-inputs/NxRangeInput.stories.tsx +27 -27
  97. package/cli/storybook-dist/_stories-src/nx-inputs/NxSelectBox.stories.tsx +67 -67
  98. package/cli/storybook-dist/_stories-src/nx-inputs/NxSlideRange.stories.tsx +45 -45
  99. package/cli/storybook-dist/_stories-src/nx-inputs/NxSwitch.stories.tsx +27 -27
  100. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextArea.stories.tsx +48 -48
  101. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextInput.stories.tsx +46 -46
  102. package/cli/storybook-dist/assets/{Color-YHDXOIA2-D6JqZzDJ.js → Color-YHDXOIA2-B4IER17l.js} +1 -1
  103. package/cli/storybook-dist/assets/{Docs-ESb7JAHg.js → Docs-C8GCHFtK.js} +3 -3
  104. package/cli/storybook-dist/assets/{Docs-6gD2kUo1.js → Docs-CgdEMPQP.js} +2 -2
  105. package/cli/storybook-dist/assets/{Docs-B9wd8Ok7.js → Docs-Ck3WHvk6.js} +3 -3
  106. package/cli/storybook-dist/assets/{Docs-CgFbWWXM.js → Docs-D9NHsoeZ.js} +2 -2
  107. package/cli/storybook-dist/assets/{Docs-DBQup8Bu.js → Docs-DNYQC7nl.js} +3 -3
  108. package/cli/storybook-dist/assets/{Docs-CMjovnSY.js → Docs-DUfeiM4I.js} +4 -4
  109. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-BOUI1AHi.js → DocsRenderer-CFRXHY34-C0WhyDoR.js} +1 -1
  110. package/cli/storybook-dist/assets/{NxAvatar.stories-DsYFMUh3.js → NxAvatar.stories-CwnK8AUF.js} +1 -1
  111. package/cli/storybook-dist/assets/{NxButton.stories-CHfNcqsc.js → NxButton.stories-ueYdosfQ.js} +8 -8
  112. package/cli/storybook-dist/assets/{NxNumberInput.stories-BFw_4vFE.js → NxNumberInput.stories-M5ZCLbrK.js} +1 -1
  113. package/cli/storybook-dist/assets/{NxPagination.stories-DVfxyw4M.js → NxPagination.stories-bxxUnHEL.js} +1 -1
  114. package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BzyxUWoZ.js → NxPointerHoldButton.stories-BHxB2kr5.js} +1 -1
  115. package/cli/storybook-dist/assets/{NxRangeInput.stories-DmRdzBc1.js → NxRangeInput.stories-BKfr-fp4.js} +6 -6
  116. package/cli/storybook-dist/assets/{NxSelectBox.stories-B1KkNUOf.js → NxSelectBox.stories-CG8FuTBq.js} +1 -1
  117. package/cli/storybook-dist/assets/{NxSlideRange.stories-CyOdvfQ6.js → NxSlideRange.stories-BRTFtMTL.js} +1 -1
  118. package/cli/storybook-dist/assets/{NxSwitch.stories-DrDBw0qp.js → NxSwitch.stories-Bwuyzn-R.js} +1 -1
  119. package/cli/storybook-dist/assets/{NxTextArea.stories-DTB2aqNS.js → NxTextArea.stories-COq2sGqu.js} +1 -1
  120. package/cli/storybook-dist/assets/{NxTextInput.stories-DU0rucmM.js → NxTextInput.stories-CjsBLz05.js} +1 -1
  121. package/cli/storybook-dist/assets/{RBAccordion.stories-DTfOtXtp.js → RBAccordion.stories-BARwuZ0Q.js} +3 -3
  122. package/cli/storybook-dist/assets/{RBAccordionGroup.stories-9y9BjrUe.js → RBAccordionGroup.stories-COrPxETW.js} +10 -10
  123. package/cli/storybook-dist/assets/{RBAlert.stories-DLTAv9fT.js → RBAlert.stories-BQrkjZPt.js} +3 -3
  124. package/cli/storybook-dist/assets/{RBBadge.stories-BNMACewA.js → RBBadge.stories-BTsLL-lK.js} +1 -1
  125. package/cli/storybook-dist/assets/{RBBattery.stories-B3kXMAt5.js → RBBattery.stories-DAeQdJbc.js} +1 -1
  126. package/cli/storybook-dist/assets/{RBCard.stories-Y-s8R2n7.js → RBCard.stories-HyhOE80R.js} +39 -39
  127. package/cli/storybook-dist/assets/{RBCheckBox.stories-DBuT9tL4.js → RBCheckBox.stories-KFk0WqX_.js} +1 -1
  128. package/cli/storybook-dist/assets/{RBCheckIconButton.stories-CWu1TL56.js → RBCheckIconButton.stories-CXbbktF5.js} +1 -1
  129. package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-sCu2MGCX.js → RBCheckLabelButton.stories-DWfYz_9Z.js} +17 -17
  130. package/cli/storybook-dist/assets/{RBChipTab.stories-BX9qLJkR.js → RBChipTab.stories-JPEgVFAS.js} +5 -5
  131. package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-35u1aJ-P.js → RBChipsBoxTab.stories-CTPuWXrQ.js} +1 -1
  132. package/cli/storybook-dist/assets/{RBColorIcons.stories-DAOZ5Ies.js → RBColorIcons.stories-DiFRZ5jk.js} +1 -1
  133. package/cli/storybook-dist/assets/RBCommand.stories-Pf1HAVIR.js +73 -0
  134. package/cli/storybook-dist/assets/{RBConfirm.stories-iMXcUh7Z.js → RBConfirm.stories-D1cEPUXk.js} +3 -3
  135. package/cli/storybook-dist/assets/{RBDateInput.stories-hK5rXECf.js → RBDateInput.stories-RPYpCjig.js} +1 -1
  136. package/cli/storybook-dist/assets/RBDialog.stories-DJH-0XSR.js +38 -0
  137. package/cli/storybook-dist/assets/{RBDivider.stories-BAK-iZ1Z.js → RBDivider.stories-uEj1vOgK.js} +1 -1
  138. package/cli/storybook-dist/assets/{RBDrawer.stories-B2nqf2Nh.js → RBDrawer.stories-BiMzElb2.js} +12 -12
  139. package/cli/storybook-dist/assets/RBDropdownMenu.stories-Da_TBkla.js +86 -0
  140. package/cli/storybook-dist/assets/{RBEmptyView.stories-nCenrgBh.js → RBEmptyView.stories-BHeKGfPS.js} +1 -1
  141. package/cli/storybook-dist/assets/{RBIconButton.stories-DRH7O3sp.js → RBIconButton.stories-NEsRFoYp.js} +1 -1
  142. package/cli/storybook-dist/assets/{RBIcons.stories-D3PyJXG8.js → RBIcons.stories-CcZ-TTLS.js} +1 -1
  143. package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-modQbt10.js → RBInlineLabelButton.stories-B0wpdQyR.js} +1 -1
  144. package/cli/storybook-dist/assets/{RBJoyStick.stories-BY14qVgR.js → RBJoyStick.stories-DEgBou3K.js} +1 -1
  145. package/cli/storybook-dist/assets/{RBLabelButton.stories-DzF7vGwm.js → RBLabelButton.stories-DT8drIsF.js} +1 -1
  146. package/cli/storybook-dist/assets/{RBLight.stories-lF4FRmVh.js → RBLight.stories-Dh-aoeyG.js} +1 -1
  147. package/cli/storybook-dist/assets/{RBMenubar.stories-XhrI7bsV.js → RBMenubar.stories-D-H7UqVh.js} +1 -1
  148. package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-DprllQDl.js → RBMultiRangeInput.stories-hlclv6jI.js} +1 -1
  149. package/cli/storybook-dist/assets/{RBPagination.stories-2v2gBJfC.js → RBPagination.stories-DWCeoZ_S.js} +1 -1
  150. package/cli/storybook-dist/assets/{RBPopover.stories-C8iLIZpv.js → RBPopover.stories-C514M09_.js} +1 -1
  151. package/cli/storybook-dist/assets/{RBPrompt.stories-Th_HrWHI.js → RBPrompt.stories-BSItvfiT.js} +1 -1
  152. package/cli/storybook-dist/assets/{RBRadioBox.stories-CoHuLaN3.js → RBRadioBox.stories-BBb4YbFU.js} +1 -1
  153. package/cli/storybook-dist/assets/{RBRadioButton.stories-DULxE_ff.js → RBRadioButton.stories-CXyOGJCv.js} +1 -1
  154. package/cli/storybook-dist/assets/{RBRangeDateInput.stories-D8A9KBSS.js → RBRangeDateInput.stories-qvhubu0e.js} +1 -1
  155. package/cli/storybook-dist/assets/{RBResizable.stories-C41QfrQ3.js → RBResizable.stories-CZKeyGjH.js} +14 -14
  156. package/cli/storybook-dist/assets/{RBSelectBox.stories-C1gNmwzw.js → RBSelectBox.stories-CRKP3ybf.js} +1 -1
  157. package/cli/storybook-dist/assets/RBSheet.stories-sCM3ZJUa.js +39 -0
  158. package/cli/storybook-dist/assets/{RBSideNumberInput.stories-9LlgQ8nh.js → RBSideNumberInput.stories-Bb7gw8RF.js} +3 -3
  159. package/cli/storybook-dist/assets/{RBSlider.stories-oUgo5K9d.js → RBSlider.stories-23OAQU5z.js} +3 -3
  160. package/cli/storybook-dist/assets/{RBSpinner.stories-DDpohbY2.js → RBSpinner.stories-2xcQdcmt.js} +1 -1
  161. package/cli/storybook-dist/assets/{RBSwitch.stories-gW-GN7QO.js → RBSwitch.stories-BACUaJm_.js} +7 -7
  162. package/cli/storybook-dist/assets/{RBTab.stories-CN-PV3HH.js → RBTab.stories-CDHE3sVs.js} +1 -1
  163. package/cli/storybook-dist/assets/{RBTable.stories-BAZLpxtM.js → RBTable.stories-C0Cfhfl7.js} +29 -29
  164. package/cli/storybook-dist/assets/{RBTableRow.stories-CwjVrszB.js → RBTableRow.stories-BF8kIkpp.js} +1 -1
  165. package/cli/storybook-dist/assets/{RBTabs.stories-JuEo2sUL.js → RBTabs.stories-BdJWRq1o.js} +7 -7
  166. package/cli/storybook-dist/assets/{RBTextArea.stories-D0_Pv4qZ.js → RBTextArea.stories-CGy2xeYH.js} +3 -3
  167. package/cli/storybook-dist/assets/{RBTextField.stories-CCsOvZqV.js → RBTextField.stories-IfUOzxCM.js} +15 -15
  168. package/cli/storybook-dist/assets/{RBTextInput.stories-Cccn3VNm.js → RBTextInput.stories-VjXNFo4R.js} +3 -3
  169. package/cli/storybook-dist/assets/{RBToast.stories-I5617t0K.js → RBToast.stories-BzcobblV.js} +1 -1
  170. package/cli/storybook-dist/assets/{RBToggle.stories-DsgKjGsJ.js → RBToggle.stories-DuLE5mKv.js} +4 -4
  171. package/cli/storybook-dist/assets/{RBToggleGroup.stories-BZEzSDuz.js → RBToggleGroup.stories-CuR0N0GJ.js} +5 -5
  172. package/cli/storybook-dist/assets/{RBTooltip.stories-4oSyAWsA.js → RBTooltip.stories--aAgNC8_.js} +1 -1
  173. package/cli/storybook-dist/assets/{SideNumberInput-DSKbd0bG.js → SideNumberInput-DyWQxYUj.js} +743 -743
  174. package/cli/storybook-dist/assets/{entry-preview-B35XREGs.js → entry-preview-D7IHsza_.js} +1 -1
  175. package/cli/storybook-dist/assets/{iframe-BB797lmL.js → iframe-CbaIp5I-.js} +2 -2
  176. package/cli/storybook-dist/assets/{index-BU2567EP.js → index-C6CBgMuG.js} +3 -3
  177. package/cli/storybook-dist/assets/{preview-C7DaSWyc.js → preview-B-iYLDuW.js} +2 -2
  178. package/cli/storybook-dist/assets/{preview-CbuRHit4.js → preview-CRWqc9DH.js} +1 -1
  179. package/cli/storybook-dist/assets/{react-resizable-panels.browser-ChIvlwo7.js → react-resizable-panels.browser-8hC8dr-6.js} +8 -8
  180. package/cli/storybook-dist/iframe.html +2 -2
  181. package/cli/storybook-dist/index.html +1 -1
  182. package/cli/storybook-dist/project.json +1 -1
  183. package/cli/templates/common/.mcp.json +8 -8
  184. package/cli/templates/common/README.md +1231 -1231
  185. package/cli/templates/common/assets/icon.svg +45 -45
  186. package/cli/templates/common/manifest.json +26 -26
  187. package/cli/templates/common/package.json +22 -22
  188. package/cli/templates/common/py/main.py +125 -125
  189. package/cli/templates/common/tsconfig.json +13 -13
  190. package/cli/templates/common/ui/content.tsx +216 -216
  191. package/cli/templates/common/ui/style.css +6 -6
  192. package/cli/templates/common/vite.config.ts +10 -10
  193. package/cli/templates/cpp/cpp/main.cpp +73 -73
  194. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +25 -25
  195. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/package.xml +10 -10
  196. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/setup.py +18 -18
  197. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -11
  198. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +23 -23
  199. package/cli/vite.d.mts +25 -25
  200. package/cli/vite.mjs +292 -292
  201. package/package.json +3 -3
  202. package/cli/storybook-dist/assets/RBCommand.stories-BSqXDkKw.js +0 -73
  203. package/cli/storybook-dist/assets/RBDialog.stories-BAktz-ku.js +0 -38
  204. package/cli/storybook-dist/assets/RBDropdownMenu.stories-DbW2fWAj.js +0 -86
  205. package/cli/storybook-dist/assets/RBSheet.stories-_6VH-RUm.js +0 -39
@@ -1,287 +1,287 @@
1
- // manifest.mjs — manifest.json schema definition and validation
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { resolve } from "node:path";
4
- import { manifestPortRanges, validatePorts } from "./ports.mjs";
5
-
6
- // 호스트가 이해하는 contribution point. plugin-ui의 CONTRIBUTION_POINTS와 동일해야 한다.
7
- export const CONTRIBUTION_POINTS = [
8
- "toolbar",
9
- "lnb",
10
- "move.panel",
11
- "program.panel",
12
- ];
13
- const ICON_SIZES = ["16", "48", "128"];
14
- // Reverse-domain id: "."-separated parts of lowercase letters/digits joined by single "-",
15
- // each part starting with a letter. Keeps the ROS namespace derived from it (rosNamespace)
16
- // valid and collision-free (no "_" in ids, so "." and "-" map to distinct characters).
17
- export const PLUGIN_ID_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)*$/;
18
-
19
- /** ROS namespace for a plugin id: "." → "/", "-" → "_" (com.example.my-plugin → /com/example/my_plugin). */
20
- export function rosNamespace(id) {
21
- return "/" + id.replace(/\./g, "/").replace(/-/g, "_");
22
- }
23
- const RUNTIME_TYPES = ["none", "python", "cpp", "ros2"];
24
- const ROS_CLIENTS = ["rclpy", "rclcpp"];
25
- const I18N_LANGS = ["ko", "en", "ja", "zh", "vi", "th", "es"];
26
- const TARGET_CATEGORIES = ["manipulate", "amr", "rby1", "vision"];
27
- const VARIABLE_TYPES = ["number", "string", "boolean", "array"];
28
- const VARIABLE_NAME_PREFIX = "RBP_";
29
- const VARIABLE_NAME_RE = /^RBP_[A-Za-z0-9_]+$/;
30
-
31
- function validateI18nLabel(value, field, errors) {
32
- if (value === undefined || value === null) {
33
- errors.push(`${field} is required`);
34
- return;
35
- }
36
- if (typeof value === "string") return;
37
- if (typeof value === "object") {
38
- if (typeof value.en !== "string")
39
- errors.push(`${field}: "en" is required when using object form`);
40
- const unknown = Object.keys(value).filter((k) => !I18N_LANGS.includes(k));
41
- if (unknown.length > 0)
42
- errors.push(`${field}: unsupported language key(s) — ${unknown.map((k) => `"${k}"`).join(", ")} (allowed: ${I18N_LANGS.join(", ")})`);
43
- return;
44
- }
45
- errors.push(`${field}: must be a string or { en (required), ko, ja, zh, vi, th, es }`);
46
- }
47
-
48
- export function readManifest(root) {
49
- const path = resolve(root, "manifest.json");
50
- if (!existsSync(path)) return {};
51
- try {
52
- return JSON.parse(readFileSync(path, "utf8"));
53
- } catch {
54
- return null;
55
- }
56
- }
57
-
58
- export function manifestRuntime(root) {
59
- return readManifest(root)?.runtime?.type ?? "python";
60
- }
61
-
62
- function validateIcons(icons, errors) {
63
- if (icons === undefined || icons === null) {
64
- errors.push("icons is required");
65
- return;
66
- }
67
- if (typeof icons === "string") {
68
- if (!icons.toLowerCase().endsWith(".svg"))
69
- errors.push(`icons: non-SVG images (PNG, JPG, etc.) must use the { "16": "...", "48": "...", "128": "..." } object form`);
70
- } else if (typeof icons === "object") {
71
- const missing = ICON_SIZES.filter((s) => typeof icons[s] !== "string" || icons[s].length === 0);
72
- if (missing.length > 0)
73
- errors.push(`icons: missing size(s) ${missing.map((s) => `"${s}"`).join(", ")} — all three (16, 48, 128) are required for image files`);
74
- } else {
75
- errors.push(`icons: must be a string (SVG path) or { "16", "48", "128" } object (PNG/JPG)`);
76
- }
77
- }
78
-
79
- function validateRuntime(runtime, errors) {
80
- if (runtime === undefined || runtime === null) {
81
- errors.push("runtime is required");
82
- return;
83
- }
84
- if (typeof runtime !== "object") {
85
- errors.push("runtime must be an object");
86
- return;
87
- }
88
- if (!RUNTIME_TYPES.includes(runtime.type))
89
- errors.push(`runtime.type must be one of ${RUNTIME_TYPES.map((t) => `"${t}"`).join(" | ")}`);
90
- if (runtime.type === "python" && !runtime.entry)
91
- errors.push("runtime.entry is required for python runtime");
92
- if (runtime.type === "cpp" && !runtime.entry)
93
- errors.push("runtime.entry is required for cpp runtime (the built executable name)");
94
- if (runtime.type === "ros2") {
95
- if (!runtime.package) errors.push("runtime.package is required for ros2 runtime");
96
- if (!runtime.launch) errors.push("runtime.launch is required for ros2 runtime");
97
- if (!ROS_CLIENTS.includes(runtime.rosClient))
98
- errors.push(`runtime.rosClient must be one of ${ROS_CLIENTS.map((c) => `"${c}"`).join(" | ")}`);
99
- }
100
- }
101
-
102
- function validateCommandInfo(info, prefix, errors) {
103
- if (typeof info.name !== "string" || info.name.length === 0)
104
- errors.push(`${prefix}.commandInfo.name is required`);
105
- if (typeof info.icon !== "string" || info.icon.length === 0)
106
- errors.push(`${prefix}.commandInfo.icon is required`);
107
- if (info.iconClassName !== undefined && typeof info.iconClassName !== "string")
108
- errors.push(`${prefix}.commandInfo.iconClassName must be a string`);
109
- if (info.funcName !== undefined && typeof info.funcName !== "string")
110
- errors.push(`${prefix}.commandInfo.funcName must be a string`);
111
- if (info.args !== undefined && (typeof info.args !== "object" || Array.isArray(info.args)))
112
- errors.push(`${prefix}.commandInfo.args must be an object`);
113
- for (const key of ["disabled", "syncGroupId", "disableCopy", "disableDelete", "requireChildren", "requireParent"]) {
114
- if (info[key] !== undefined && typeof info[key] !== "boolean")
115
- errors.push(`${prefix}.commandInfo.${key} must be a boolean`);
116
- }
117
- for (const key of ["allowChildren", "allowParents", "notAllowChildren"]) {
118
- if (info[key] !== undefined && (!Array.isArray(info[key]) || info[key].some((v) => typeof v !== "string")))
119
- errors.push(`${prefix}.commandInfo.${key} must be a string[]`);
120
- }
121
- if (info.childrenCommand !== undefined && typeof info.childrenCommand !== "string")
122
- errors.push(`${prefix}.commandInfo.childrenCommand must be a string`);
123
- }
124
-
125
- // variables: pyfm에 노출할 변수 선언. 없으면 UI가 사용 가능한 변수를 보여줄 수 없다.
126
- function validateVariables(variables, errors) {
127
- if (variables === undefined || variables === null) return;
128
- if (!Array.isArray(variables)) {
129
- errors.push(`"variables" must be an array`);
130
- return;
131
- }
132
- const names = variables.map((v) => v?.name).filter(Boolean);
133
- const duplicates = names.filter((n, i) => names.indexOf(n) !== i);
134
- if (duplicates.length > 0)
135
- errors.push(`variables: "name" must be unique (duplicates: ${[...new Set(duplicates)].map((n) => `"${n}"`).join(", ")})`);
136
-
137
- variables.forEach((v, i) => {
138
- if (typeof v !== "object" || v === null || Array.isArray(v)) {
139
- errors.push(`variables[${i}] must be an object ({ name, type, init, description? })`);
140
- return;
141
- }
142
- if (typeof v.name !== "string" || v.name.length === 0)
143
- errors.push(`variables[${i}].name is required`);
144
- else if (!v.name.startsWith(VARIABLE_NAME_PREFIX))
145
- errors.push(`variables[${i}].name: "${v.name}" must start with "${VARIABLE_NAME_PREFIX}" (e.g. "${VARIABLE_NAME_PREFIX}${v.name}")`);
146
- else if (!VARIABLE_NAME_RE.test(v.name))
147
- errors.push(`variables[${i}].name: "${v.name}" is not a valid identifier (after "${VARIABLE_NAME_PREFIX}": letters, digits, underscore only, at least one character)`);
148
- if (!VARIABLE_TYPES.includes(v.type))
149
- errors.push(`variables[${i}].type must be one of ${VARIABLE_TYPES.map((t) => `"${t}"`).join(" | ")}`);
150
- if (v.init === undefined)
151
- errors.push(`variables[${i}].init is required (initial value)`);
152
- else if (v.type === "number" && typeof v.init !== "number")
153
- errors.push(`variables[${i}].init must be a number when type is "number"`);
154
- else if (v.type === "string" && typeof v.init !== "string")
155
- errors.push(`variables[${i}].init must be a string when type is "string"`);
156
- else if (v.type === "boolean" && typeof v.init !== "boolean")
157
- errors.push(`variables[${i}].init must be a boolean when type is "boolean"`);
158
- else if (v.type === "array" && !Array.isArray(v.init))
159
- errors.push(`variables[${i}].init must be an array when type is "array"`);
160
- if (v.description !== undefined && v.description !== null)
161
- validateI18nLabel(v.description, `variables[${i}].description`, errors);
162
- });
163
- }
164
-
165
- // No contributes (missing or []) = a backend-only plugin: no screens, nothing for vite to build.
166
- // A plugin with screens still needs a toolbar entry — that is where the host lists it.
167
- function validateContributes(contributes, errors) {
168
- if (contributes === undefined || contributes === null) return;
169
- if (!Array.isArray(contributes)) {
170
- errors.push("contributes must be an array");
171
- return;
172
- }
173
- if (contributes.length === 0) return;
174
- if (!contributes.some((c) => c.point === "toolbar"))
175
- errors.push(`contributes: at least one "toolbar" entry is required`);
176
-
177
- const uiValues = contributes.map((c) => c.ui).filter(Boolean);
178
- const duplicateUi = uiValues.filter((v, i) => uiValues.indexOf(v) !== i);
179
- if (duplicateUi.length > 0)
180
- errors.push(`contributes: "ui" values must be unique across all entries (duplicates: ${[...new Set(duplicateUi)].map((v) => `"${v}"`).join(", ")})`);
181
-
182
- contributes.forEach((c, i) => {
183
- if (!CONTRIBUTION_POINTS.includes(c.point))
184
- errors.push(`contributes[${i}].point: unknown point ${JSON.stringify(c.point)} (allowed: ${CONTRIBUTION_POINTS.join(", ")})`);
185
- validateI18nLabel(c.label, `contributes[${i}].label`, errors);
186
- if (c.point === "program.panel") {
187
- if (!c.commandInfo)
188
- errors.push(`contributes[${i}]: "commandInfo" is required when point is "program.panel"`);
189
- else
190
- validateCommandInfo(c.commandInfo, `contributes[${i}]`, errors);
191
- }
192
- });
193
- }
194
-
195
- /**
196
- * Validate manifest.json. Returns an array of error messages (empty = valid).
197
- */
198
- export function validateManifest(manifest) {
199
- const errors = [];
200
-
201
- if (typeof manifest !== "object" || manifest === null)
202
- return ["manifest.json is not a valid JSON object"];
203
-
204
- for (const key of ["id", "name", "version", "minMuscatVersion"]) {
205
- if (typeof manifest[key] !== "string" || manifest[key].length === 0)
206
- errors.push(`"${key}" is required`);
207
- }
208
- if (typeof manifest.id === "string" && manifest.id && !PLUGIN_ID_RE.test(manifest.id))
209
- errors.push(`"id" must be "."-separated parts of lowercase letters/digits and single "-", each starting with a letter (e.g. com.example.my-plugin)`);
210
- if (typeof manifest.author !== "string" || manifest.author.length === 0)
211
- errors.push(`"author" is required`);
212
- if (typeof manifest.email !== "string" || manifest.email.length === 0)
213
- errors.push(`"email" is required`);
214
- if (manifest.company !== undefined && manifest.company !== null && typeof manifest.company !== "string")
215
- errors.push(`"company" must be a string`);
216
-
217
- const hasModels = manifest.targetModels !== undefined && manifest.targetModels !== null;
218
- const hasCategorys = manifest.targetCategorys !== undefined && manifest.targetCategorys !== null;
219
- if (!hasModels && !hasCategorys) {
220
- errors.push(`"targetModels" or "targetCategorys" is required`);
221
- } else {
222
- if (hasModels) {
223
- if (manifest.targetModels !== "all" && (!Array.isArray(manifest.targetModels) || manifest.targetModels.length === 0))
224
- errors.push(`"targetModels" must be "all" or a non-empty array of model strings`);
225
- else if (Array.isArray(manifest.targetModels) && manifest.targetModels.some((m) => typeof m !== "string"))
226
- errors.push(`"targetModels": all entries must be strings`);
227
- }
228
- if (hasCategorys) {
229
- if (manifest.targetCategorys !== "all" && (!Array.isArray(manifest.targetCategorys) || manifest.targetCategorys.length === 0))
230
- errors.push(`"targetCategorys" must be "all" or a non-empty array`);
231
- else if (Array.isArray(manifest.targetCategorys)) {
232
- const invalid = manifest.targetCategorys.filter((c) => !TARGET_CATEGORIES.includes(c));
233
- if (invalid.length > 0)
234
- errors.push(`"targetCategorys": invalid value(s) ${invalid.map((c) => `"${c}"`).join(", ")} — allowed: ${TARGET_CATEGORIES.join(", ")}`);
235
- }
236
- }
237
- }
238
- validateI18nLabel(manifest.description, "description", errors);
239
-
240
- validateIcons(manifest.icons, errors);
241
- validatePorts(manifest.ports, errors);
242
- validateRuntime(manifest.runtime, errors);
243
- validateContributes(manifest.contributes, errors);
244
- validateVariables(manifest.variables, errors);
245
-
246
- return errors;
247
- }
248
-
249
- /**
250
- * Read and validate manifest.json. Throws if any errors are found.
251
- */
252
- export function assertManifest(root) {
253
- const manifest = readManifest(root);
254
- if (manifest === null)
255
- throw new Error("manifest.json parse failed — check JSON syntax");
256
- if (Object.keys(manifest).length === 0)
257
- return manifest;
258
- const errors = validateManifest(manifest);
259
- if (errors.length > 0)
260
- throw new Error(`manifest.json errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
261
- return manifest;
262
- }
263
-
264
- export function assertManifestVariables(root) {
265
- const manifest = readManifest(root);
266
- if (manifest === null)
267
- throw new Error("manifest.json parse failed — check JSON syntax");
268
- if (Object.keys(manifest).length === 0)
269
- return;
270
- const errors = [];
271
- validateVariables(manifest.variables, errors);
272
- if (errors.length > 0)
273
- throw new Error(`manifest.json variable errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
274
- }
275
-
276
- export function assertManifestPorts(root) {
277
- const manifest = readManifest(root);
278
- if (manifest === null)
279
- throw new Error("manifest.json parse failed — check JSON syntax");
280
- if (Object.keys(manifest).length === 0)
281
- return [];
282
- const errors = [];
283
- validatePorts(manifest.ports, errors);
284
- if (errors.length > 0)
285
- throw new Error(`manifest.json port errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
286
- return manifestPortRanges(manifest);
287
- }
1
+ // manifest.mjs — manifest.json schema definition and validation
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { manifestPortRanges, validatePorts } from "./ports.mjs";
5
+
6
+ // 호스트가 이해하는 contribution point. plugin-ui의 CONTRIBUTION_POINTS와 동일해야 한다.
7
+ export const CONTRIBUTION_POINTS = [
8
+ "toolbar",
9
+ "lnb",
10
+ "move.panel",
11
+ "program.panel",
12
+ ];
13
+ const ICON_SIZES = ["16", "48", "128"];
14
+ // Reverse-domain id: "."-separated parts of lowercase letters/digits joined by single "-",
15
+ // each part starting with a letter. Keeps the ROS namespace derived from it (rosNamespace)
16
+ // valid and collision-free (no "_" in ids, so "." and "-" map to distinct characters).
17
+ export const PLUGIN_ID_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)*$/;
18
+
19
+ /** ROS namespace for a plugin id: "." → "/", "-" → "_" (com.example.my-plugin → /com/example/my_plugin). */
20
+ export function rosNamespace(id) {
21
+ return "/" + id.replace(/\./g, "/").replace(/-/g, "_");
22
+ }
23
+ const RUNTIME_TYPES = ["none", "python", "cpp", "ros2"];
24
+ const ROS_CLIENTS = ["rclpy", "rclcpp"];
25
+ const I18N_LANGS = ["ko", "en", "ja", "zh", "vi", "th", "es"];
26
+ const TARGET_CATEGORIES = ["manipulate", "amr", "rby1", "vision"];
27
+ const VARIABLE_TYPES = ["number", "string", "boolean", "array"];
28
+ const VARIABLE_NAME_PREFIX = "RBP_";
29
+ const VARIABLE_NAME_RE = /^RBP_[A-Za-z0-9_]+$/;
30
+
31
+ function validateI18nLabel(value, field, errors) {
32
+ if (value === undefined || value === null) {
33
+ errors.push(`${field} is required`);
34
+ return;
35
+ }
36
+ if (typeof value === "string") return;
37
+ if (typeof value === "object") {
38
+ if (typeof value.en !== "string")
39
+ errors.push(`${field}: "en" is required when using object form`);
40
+ const unknown = Object.keys(value).filter((k) => !I18N_LANGS.includes(k));
41
+ if (unknown.length > 0)
42
+ errors.push(`${field}: unsupported language key(s) — ${unknown.map((k) => `"${k}"`).join(", ")} (allowed: ${I18N_LANGS.join(", ")})`);
43
+ return;
44
+ }
45
+ errors.push(`${field}: must be a string or { en (required), ko, ja, zh, vi, th, es }`);
46
+ }
47
+
48
+ export function readManifest(root) {
49
+ const path = resolve(root, "manifest.json");
50
+ if (!existsSync(path)) return {};
51
+ try {
52
+ return JSON.parse(readFileSync(path, "utf8"));
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ export function manifestRuntime(root) {
59
+ return readManifest(root)?.runtime?.type ?? "python";
60
+ }
61
+
62
+ function validateIcons(icons, errors) {
63
+ if (icons === undefined || icons === null) {
64
+ errors.push("icons is required");
65
+ return;
66
+ }
67
+ if (typeof icons === "string") {
68
+ if (!icons.toLowerCase().endsWith(".svg"))
69
+ errors.push(`icons: non-SVG images (PNG, JPG, etc.) must use the { "16": "...", "48": "...", "128": "..." } object form`);
70
+ } else if (typeof icons === "object") {
71
+ const missing = ICON_SIZES.filter((s) => typeof icons[s] !== "string" || icons[s].length === 0);
72
+ if (missing.length > 0)
73
+ errors.push(`icons: missing size(s) ${missing.map((s) => `"${s}"`).join(", ")} — all three (16, 48, 128) are required for image files`);
74
+ } else {
75
+ errors.push(`icons: must be a string (SVG path) or { "16", "48", "128" } object (PNG/JPG)`);
76
+ }
77
+ }
78
+
79
+ function validateRuntime(runtime, errors) {
80
+ if (runtime === undefined || runtime === null) {
81
+ errors.push("runtime is required");
82
+ return;
83
+ }
84
+ if (typeof runtime !== "object") {
85
+ errors.push("runtime must be an object");
86
+ return;
87
+ }
88
+ if (!RUNTIME_TYPES.includes(runtime.type))
89
+ errors.push(`runtime.type must be one of ${RUNTIME_TYPES.map((t) => `"${t}"`).join(" | ")}`);
90
+ if (runtime.type === "python" && !runtime.entry)
91
+ errors.push("runtime.entry is required for python runtime");
92
+ if (runtime.type === "cpp" && !runtime.entry)
93
+ errors.push("runtime.entry is required for cpp runtime (the built executable name)");
94
+ if (runtime.type === "ros2") {
95
+ if (!runtime.package) errors.push("runtime.package is required for ros2 runtime");
96
+ if (!runtime.launch) errors.push("runtime.launch is required for ros2 runtime");
97
+ if (!ROS_CLIENTS.includes(runtime.rosClient))
98
+ errors.push(`runtime.rosClient must be one of ${ROS_CLIENTS.map((c) => `"${c}"`).join(" | ")}`);
99
+ }
100
+ }
101
+
102
+ function validateCommandInfo(info, prefix, errors) {
103
+ if (typeof info.name !== "string" || info.name.length === 0)
104
+ errors.push(`${prefix}.commandInfo.name is required`);
105
+ if (typeof info.icon !== "string" || info.icon.length === 0)
106
+ errors.push(`${prefix}.commandInfo.icon is required`);
107
+ if (info.iconClassName !== undefined && typeof info.iconClassName !== "string")
108
+ errors.push(`${prefix}.commandInfo.iconClassName must be a string`);
109
+ if (info.funcName !== undefined && typeof info.funcName !== "string")
110
+ errors.push(`${prefix}.commandInfo.funcName must be a string`);
111
+ if (info.args !== undefined && (typeof info.args !== "object" || Array.isArray(info.args)))
112
+ errors.push(`${prefix}.commandInfo.args must be an object`);
113
+ for (const key of ["disabled", "syncGroupId", "disableCopy", "disableDelete", "requireChildren", "requireParent"]) {
114
+ if (info[key] !== undefined && typeof info[key] !== "boolean")
115
+ errors.push(`${prefix}.commandInfo.${key} must be a boolean`);
116
+ }
117
+ for (const key of ["allowChildren", "allowParents", "notAllowChildren"]) {
118
+ if (info[key] !== undefined && (!Array.isArray(info[key]) || info[key].some((v) => typeof v !== "string")))
119
+ errors.push(`${prefix}.commandInfo.${key} must be a string[]`);
120
+ }
121
+ if (info.childrenCommand !== undefined && typeof info.childrenCommand !== "string")
122
+ errors.push(`${prefix}.commandInfo.childrenCommand must be a string`);
123
+ }
124
+
125
+ // variables: pyfm에 노출할 변수 선언. 없으면 UI가 사용 가능한 변수를 보여줄 수 없다.
126
+ function validateVariables(variables, errors) {
127
+ if (variables === undefined || variables === null) return;
128
+ if (!Array.isArray(variables)) {
129
+ errors.push(`"variables" must be an array`);
130
+ return;
131
+ }
132
+ const names = variables.map((v) => v?.name).filter(Boolean);
133
+ const duplicates = names.filter((n, i) => names.indexOf(n) !== i);
134
+ if (duplicates.length > 0)
135
+ errors.push(`variables: "name" must be unique (duplicates: ${[...new Set(duplicates)].map((n) => `"${n}"`).join(", ")})`);
136
+
137
+ variables.forEach((v, i) => {
138
+ if (typeof v !== "object" || v === null || Array.isArray(v)) {
139
+ errors.push(`variables[${i}] must be an object ({ name, type, init, description? })`);
140
+ return;
141
+ }
142
+ if (typeof v.name !== "string" || v.name.length === 0)
143
+ errors.push(`variables[${i}].name is required`);
144
+ else if (!v.name.startsWith(VARIABLE_NAME_PREFIX))
145
+ errors.push(`variables[${i}].name: "${v.name}" must start with "${VARIABLE_NAME_PREFIX}" (e.g. "${VARIABLE_NAME_PREFIX}${v.name}")`);
146
+ else if (!VARIABLE_NAME_RE.test(v.name))
147
+ errors.push(`variables[${i}].name: "${v.name}" is not a valid identifier (after "${VARIABLE_NAME_PREFIX}": letters, digits, underscore only, at least one character)`);
148
+ if (!VARIABLE_TYPES.includes(v.type))
149
+ errors.push(`variables[${i}].type must be one of ${VARIABLE_TYPES.map((t) => `"${t}"`).join(" | ")}`);
150
+ if (v.init === undefined)
151
+ errors.push(`variables[${i}].init is required (initial value)`);
152
+ else if (v.type === "number" && typeof v.init !== "number")
153
+ errors.push(`variables[${i}].init must be a number when type is "number"`);
154
+ else if (v.type === "string" && typeof v.init !== "string")
155
+ errors.push(`variables[${i}].init must be a string when type is "string"`);
156
+ else if (v.type === "boolean" && typeof v.init !== "boolean")
157
+ errors.push(`variables[${i}].init must be a boolean when type is "boolean"`);
158
+ else if (v.type === "array" && !Array.isArray(v.init))
159
+ errors.push(`variables[${i}].init must be an array when type is "array"`);
160
+ if (v.description !== undefined && v.description !== null)
161
+ validateI18nLabel(v.description, `variables[${i}].description`, errors);
162
+ });
163
+ }
164
+
165
+ // No contributes (missing or []) = a backend-only plugin: no screens, nothing for vite to build.
166
+ // A plugin with screens still needs a toolbar entry — that is where the host lists it.
167
+ function validateContributes(contributes, errors) {
168
+ if (contributes === undefined || contributes === null) return;
169
+ if (!Array.isArray(contributes)) {
170
+ errors.push("contributes must be an array");
171
+ return;
172
+ }
173
+ if (contributes.length === 0) return;
174
+ if (!contributes.some((c) => c.point === "toolbar"))
175
+ errors.push(`contributes: at least one "toolbar" entry is required`);
176
+
177
+ const uiValues = contributes.map((c) => c.ui).filter(Boolean);
178
+ const duplicateUi = uiValues.filter((v, i) => uiValues.indexOf(v) !== i);
179
+ if (duplicateUi.length > 0)
180
+ errors.push(`contributes: "ui" values must be unique across all entries (duplicates: ${[...new Set(duplicateUi)].map((v) => `"${v}"`).join(", ")})`);
181
+
182
+ contributes.forEach((c, i) => {
183
+ if (!CONTRIBUTION_POINTS.includes(c.point))
184
+ errors.push(`contributes[${i}].point: unknown point ${JSON.stringify(c.point)} (allowed: ${CONTRIBUTION_POINTS.join(", ")})`);
185
+ validateI18nLabel(c.label, `contributes[${i}].label`, errors);
186
+ if (c.point === "program.panel") {
187
+ if (!c.commandInfo)
188
+ errors.push(`contributes[${i}]: "commandInfo" is required when point is "program.panel"`);
189
+ else
190
+ validateCommandInfo(c.commandInfo, `contributes[${i}]`, errors);
191
+ }
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Validate manifest.json. Returns an array of error messages (empty = valid).
197
+ */
198
+ export function validateManifest(manifest) {
199
+ const errors = [];
200
+
201
+ if (typeof manifest !== "object" || manifest === null)
202
+ return ["manifest.json is not a valid JSON object"];
203
+
204
+ for (const key of ["id", "name", "version", "minMuscatVersion"]) {
205
+ if (typeof manifest[key] !== "string" || manifest[key].length === 0)
206
+ errors.push(`"${key}" is required`);
207
+ }
208
+ if (typeof manifest.id === "string" && manifest.id && !PLUGIN_ID_RE.test(manifest.id))
209
+ errors.push(`"id" must be "."-separated parts of lowercase letters/digits and single "-", each starting with a letter (e.g. com.example.my-plugin)`);
210
+ if (typeof manifest.author !== "string" || manifest.author.length === 0)
211
+ errors.push(`"author" is required`);
212
+ if (typeof manifest.email !== "string" || manifest.email.length === 0)
213
+ errors.push(`"email" is required`);
214
+ if (manifest.company !== undefined && manifest.company !== null && typeof manifest.company !== "string")
215
+ errors.push(`"company" must be a string`);
216
+
217
+ const hasModels = manifest.targetModels !== undefined && manifest.targetModels !== null;
218
+ const hasCategorys = manifest.targetCategorys !== undefined && manifest.targetCategorys !== null;
219
+ if (!hasModels && !hasCategorys) {
220
+ errors.push(`"targetModels" or "targetCategorys" is required`);
221
+ } else {
222
+ if (hasModels) {
223
+ if (manifest.targetModels !== "all" && (!Array.isArray(manifest.targetModels) || manifest.targetModels.length === 0))
224
+ errors.push(`"targetModels" must be "all" or a non-empty array of model strings`);
225
+ else if (Array.isArray(manifest.targetModels) && manifest.targetModels.some((m) => typeof m !== "string"))
226
+ errors.push(`"targetModels": all entries must be strings`);
227
+ }
228
+ if (hasCategorys) {
229
+ if (manifest.targetCategorys !== "all" && (!Array.isArray(manifest.targetCategorys) || manifest.targetCategorys.length === 0))
230
+ errors.push(`"targetCategorys" must be "all" or a non-empty array`);
231
+ else if (Array.isArray(manifest.targetCategorys)) {
232
+ const invalid = manifest.targetCategorys.filter((c) => !TARGET_CATEGORIES.includes(c));
233
+ if (invalid.length > 0)
234
+ errors.push(`"targetCategorys": invalid value(s) ${invalid.map((c) => `"${c}"`).join(", ")} — allowed: ${TARGET_CATEGORIES.join(", ")}`);
235
+ }
236
+ }
237
+ }
238
+ validateI18nLabel(manifest.description, "description", errors);
239
+
240
+ validateIcons(manifest.icons, errors);
241
+ validatePorts(manifest.ports, errors);
242
+ validateRuntime(manifest.runtime, errors);
243
+ validateContributes(manifest.contributes, errors);
244
+ validateVariables(manifest.variables, errors);
245
+
246
+ return errors;
247
+ }
248
+
249
+ /**
250
+ * Read and validate manifest.json. Throws if any errors are found.
251
+ */
252
+ export function assertManifest(root) {
253
+ const manifest = readManifest(root);
254
+ if (manifest === null)
255
+ throw new Error("manifest.json parse failed — check JSON syntax");
256
+ if (Object.keys(manifest).length === 0)
257
+ return manifest;
258
+ const errors = validateManifest(manifest);
259
+ if (errors.length > 0)
260
+ throw new Error(`manifest.json errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
261
+ return manifest;
262
+ }
263
+
264
+ export function assertManifestVariables(root) {
265
+ const manifest = readManifest(root);
266
+ if (manifest === null)
267
+ throw new Error("manifest.json parse failed — check JSON syntax");
268
+ if (Object.keys(manifest).length === 0)
269
+ return;
270
+ const errors = [];
271
+ validateVariables(manifest.variables, errors);
272
+ if (errors.length > 0)
273
+ throw new Error(`manifest.json variable errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
274
+ }
275
+
276
+ export function assertManifestPorts(root) {
277
+ const manifest = readManifest(root);
278
+ if (manifest === null)
279
+ throw new Error("manifest.json parse failed — check JSON syntax");
280
+ if (Object.keys(manifest).length === 0)
281
+ return [];
282
+ const errors = [];
283
+ validatePorts(manifest.ports, errors);
284
+ if (errors.length > 0)
285
+ throw new Error(`manifest.json port errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
286
+ return manifestPortRanges(manifest);
287
+ }