@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.9 → 1.0.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 (272) hide show
  1. package/README.md +113 -49
  2. package/cli/README.md +191 -5
  3. package/cli/bin/create-muscat-plugin.mjs +1 -1
  4. package/cli/bin/muscat-pk.mjs +44 -26
  5. package/cli/bundle-templates.mjs +28 -0
  6. package/cli/docker/Dockerfile.bridge +17 -0
  7. package/cli/docker/Dockerfile.cpp +22 -0
  8. package/cli/docker/Dockerfile.dev +1 -0
  9. package/cli/docker/Dockerfile.python +20 -0
  10. package/cli/docker/bridge-package.json +8 -0
  11. package/cli/prepack.mjs +10 -0
  12. package/cli/src/__pycache__/host_agent.cpython-37.pyc +0 -0
  13. package/cli/src/bridge.mjs +95 -0
  14. package/cli/src/build.mjs +345 -85
  15. package/cli/src/check-package.mjs +8 -23
  16. package/cli/src/cpp.mjs +61 -0
  17. package/cli/src/dev.mjs +156 -4
  18. package/cli/src/devBack.mjs +505 -57
  19. package/cli/src/devUi.mjs +167 -22
  20. package/cli/src/esbuild.mjs +27 -10
  21. package/cli/src/host_agent.py +162 -0
  22. package/cli/src/manifest.mjs +208 -0
  23. package/cli/src/mcp.mjs +617 -0
  24. package/cli/src/new.mjs +170 -67
  25. package/cli/src/pluginDevServer.mjs +112 -0
  26. package/cli/src/ports.mjs +160 -0
  27. package/cli/src/python.mjs +109 -0
  28. package/cli/src/ros2.mjs +43 -18
  29. package/cli/src/sdk.mjs +11 -9
  30. package/cli/src/socketBridge.mjs +133 -0
  31. package/cli/src/storybook.mjs +168 -0
  32. package/cli/src/type.mjs +52 -29
  33. package/cli/src/uninstall.mjs +98 -0
  34. package/cli/src/update.mjs +259 -30
  35. package/cli/src/util.mjs +123 -9
  36. package/cli/src/zenohDev.mjs +75 -0
  37. package/cli/storybook-dist/_design-tokens.css +4957 -0
  38. package/cli/storybook-dist/_stories-src/NxAvatar.stories.tsx +27 -0
  39. package/cli/storybook-dist/_stories-src/NxPagination.stories.tsx +28 -0
  40. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordion.stories.tsx +46 -0
  41. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordionGroup.stories.tsx +73 -0
  42. package/cli/storybook-dist/_stories-src/RBBadge/RBBadge.stories.tsx +93 -0
  43. package/cli/storybook-dist/_stories-src/RBBattery/RBBattery.stories.tsx +74 -0
  44. package/cli/storybook-dist/_stories-src/RBButton/RBCheckIconButton.stories.tsx +28 -0
  45. package/cli/storybook-dist/_stories-src/RBButton/RBCheckLabelButton.stories.tsx +84 -0
  46. package/cli/storybook-dist/_stories-src/RBButton/RBIconButton.stories.tsx +57 -0
  47. package/cli/storybook-dist/_stories-src/RBButton/RBInlineLabelButton.stories.tsx +41 -0
  48. package/cli/storybook-dist/_stories-src/RBButton/RBLabelButton.stories.tsx +60 -0
  49. package/cli/storybook-dist/_stories-src/RBDatePicker/RBDateInput.stories.tsx +44 -0
  50. package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +44 -0
  51. package/cli/storybook-dist/_stories-src/RBDialog/RBAlert.stories.tsx +51 -0
  52. package/cli/storybook-dist/_stories-src/RBDialog/RBConfirm.stories.tsx +55 -0
  53. package/cli/storybook-dist/_stories-src/RBDialog/RBPrompt.stories.tsx +52 -0
  54. package/cli/storybook-dist/_stories-src/RBDivider/RBDivider.stories.tsx +93 -0
  55. package/cli/storybook-dist/_stories-src/RBEmptyView/RBEmptyView.stories.tsx +49 -0
  56. package/cli/storybook-dist/_stories-src/RBIcons/RBColorIcons.stories.tsx +108 -0
  57. package/cli/storybook-dist/_stories-src/RBIcons/RBIcons.stories.tsx +106 -0
  58. package/cli/storybook-dist/_stories-src/RBIcons/RBMenuTreeIcons.stories.tsx +108 -0
  59. package/cli/storybook-dist/_stories-src/RBIcons/RBProgramIcons.stories.tsx +116 -0
  60. package/cli/storybook-dist/_stories-src/RBInput/RBCheckBox.stories.tsx +89 -0
  61. package/cli/storybook-dist/_stories-src/RBInput/RBMultiRangeInput.stories.tsx +64 -0
  62. package/cli/storybook-dist/_stories-src/RBInput/RBRadioBox.stories.tsx +65 -0
  63. package/cli/storybook-dist/_stories-src/RBInput/RBRadioButton.stories.tsx +68 -0
  64. package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +119 -0
  65. package/cli/storybook-dist/_stories-src/RBInput/RBSideNumberInput.stories.tsx +90 -0
  66. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextArea.stories.tsx +45 -0
  67. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextInput.stories.tsx +50 -0
  68. package/cli/storybook-dist/_stories-src/RBInput/RBTextField.stories.tsx +102 -0
  69. package/cli/storybook-dist/_stories-src/RBJoyStick/RBJoyStick.stories.tsx +31 -0
  70. package/cli/storybook-dist/_stories-src/RBLight/RBLight.stories.tsx +36 -0
  71. package/cli/storybook-dist/_stories-src/RBPagination/RBPagination.stories.tsx +49 -0
  72. package/cli/storybook-dist/_stories-src/RBShadcn/RBCard/RBCard.stories.tsx +89 -0
  73. package/cli/storybook-dist/_stories-src/RBShadcn/RBCommand/RBCommand.stories.tsx +116 -0
  74. package/cli/storybook-dist/_stories-src/RBShadcn/RBDialog/RBDialog.stories.tsx +74 -0
  75. package/cli/storybook-dist/_stories-src/RBShadcn/RBDrawer/RBDrawer.stories.tsx +99 -0
  76. package/cli/storybook-dist/_stories-src/RBShadcn/RBDropdownMenu/RBDropdownMenu.stories.tsx +123 -0
  77. package/cli/storybook-dist/_stories-src/RBShadcn/RBMenubar/RBMenubar.stories.tsx +135 -0
  78. package/cli/storybook-dist/_stories-src/RBShadcn/RBPopover/RBPopover.stories.tsx +69 -0
  79. package/cli/storybook-dist/_stories-src/RBShadcn/RBResizable/RBResizable.stories.tsx +49 -0
  80. package/cli/storybook-dist/_stories-src/RBShadcn/RBSheet/RBSheet.stories.tsx +91 -0
  81. package/cli/storybook-dist/_stories-src/RBShadcn/RBSlider/RBSlider.stories.tsx +38 -0
  82. package/cli/storybook-dist/_stories-src/RBShadcn/RBSwitch/RBSwitch.stories.tsx +112 -0
  83. package/cli/storybook-dist/_stories-src/RBShadcn/RBTabs/RBTabs.stories.tsx +75 -0
  84. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggle/RBToggle.stories.tsx +51 -0
  85. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggleGroup/RBToggleGroup.stories.tsx +84 -0
  86. package/cli/storybook-dist/_stories-src/RBTab/RBChipTab.stories.tsx +102 -0
  87. package/cli/storybook-dist/_stories-src/RBTab/RBChipsBoxTab.stories.tsx +78 -0
  88. package/cli/storybook-dist/_stories-src/RBTab/RBTab.stories.tsx +57 -0
  89. package/cli/storybook-dist/_stories-src/RBTable/RBTable.stories.tsx +177 -0
  90. package/cli/storybook-dist/_stories-src/RBTable/RBTableRow.stories.tsx +64 -0
  91. package/cli/storybook-dist/_stories-src/RBToast/RBToast.stories.tsx +117 -0
  92. package/cli/storybook-dist/_stories-src/RBTooltip/RBTooltip.stories.tsx +50 -0
  93. package/cli/storybook-dist/_stories-src/loading/RBSpinner.stories.tsx +30 -0
  94. package/cli/storybook-dist/_stories-src/nx-button/NxButton.stories.tsx +54 -0
  95. package/cli/storybook-dist/_stories-src/nx-button/NxPointerHoldButton.stories.tsx +57 -0
  96. package/cli/storybook-dist/_stories-src/nx-icons/NxIcons.stories.tsx +48 -0
  97. package/cli/storybook-dist/_stories-src/nx-inputs/NxNumberInput.stories.tsx +103 -0
  98. package/cli/storybook-dist/_stories-src/nx-inputs/NxRangeInput.stories.tsx +27 -0
  99. package/cli/storybook-dist/_stories-src/nx-inputs/NxSelectBox.stories.tsx +67 -0
  100. package/cli/storybook-dist/_stories-src/nx-inputs/NxSlideRange.stories.tsx +45 -0
  101. package/cli/storybook-dist/_stories-src/nx-inputs/NxSwitch.stories.tsx +27 -0
  102. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextArea.stories.tsx +48 -0
  103. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextInput.stories.tsx +46 -0
  104. package/cli/storybook-dist/assets/Color-YHDXOIA2-ttZJDaKn.js +1 -0
  105. package/cli/storybook-dist/assets/Docs-B1jnlXHd.js +12 -0
  106. package/cli/storybook-dist/assets/Docs-B7dRsHh7.js +8 -0
  107. package/cli/storybook-dist/assets/Docs-C68t3mnn.js +20 -0
  108. package/cli/storybook-dist/assets/Docs-D7rPMMhK.js +7 -0
  109. package/cli/storybook-dist/assets/Docs-DM98U-ZG.js +7 -0
  110. package/cli/storybook-dist/assets/Docs-guWsANnn.js +8 -0
  111. package/cli/storybook-dist/assets/DocsRenderer-CFRXHY34-t12O9QfY.js +2 -0
  112. package/cli/storybook-dist/assets/NxAvatar.stories-ikdLTtys.js +13 -0
  113. package/cli/storybook-dist/assets/NxButton.stories-DL3S2KEV.js +20 -0
  114. package/cli/storybook-dist/assets/NxIcons.stories-Ca7qpAaO.js +3 -0
  115. package/cli/storybook-dist/assets/NxNumberInput.stories-D6NVMTos.js +26 -0
  116. package/cli/storybook-dist/assets/NxPagination.stories-BDKSlHiQ.js +11 -0
  117. package/cli/storybook-dist/assets/NxPointerHoldButton.stories-BI544zqB.js +14 -0
  118. package/cli/storybook-dist/assets/NxRangeInput.stories-BigBBkE7.js +17 -0
  119. package/cli/storybook-dist/assets/NxSelectBox.stories-Ci0IyUWd.js +17 -0
  120. package/cli/storybook-dist/assets/NxSlideRange.stories-sjnPAjFy.js +14 -0
  121. package/cli/storybook-dist/assets/NxSwitch.stories-CRxPDlJm.js +11 -0
  122. package/cli/storybook-dist/assets/NxTextArea.stories-Jh09n24O.js +11 -0
  123. package/cli/storybook-dist/assets/NxTextInput.stories-BeGCjxES.js +11 -0
  124. package/cli/storybook-dist/assets/PretendardVariable-CJuje-Rk.woff2 +0 -0
  125. package/cli/storybook-dist/assets/RBAccordion.stories-CnC95OwM.js +17 -0
  126. package/cli/storybook-dist/assets/RBAccordionGroup.stories-og-6ePox.js +33 -0
  127. package/cli/storybook-dist/assets/RBAlert.stories-BcE9-UOT.js +17 -0
  128. package/cli/storybook-dist/assets/RBBadge.stories-DYZMXb-e.js +1 -0
  129. package/cli/storybook-dist/assets/RBBattery.stories-B7YAUvlM.js +10 -0
  130. package/cli/storybook-dist/assets/RBCard.stories-DTL_7n6I.js +45 -0
  131. package/cli/storybook-dist/assets/RBCheckBox.stories-D9KC6hTg.js +1 -0
  132. package/cli/storybook-dist/assets/RBCheckIconButton.stories-BR8aIkOC.js +1 -0
  133. package/cli/storybook-dist/assets/RBCheckLabelButton.stories-C1ABYhEr.js +24 -0
  134. package/cli/storybook-dist/assets/RBChipTab.stories-Ceo_vnNi.js +21 -0
  135. package/cli/storybook-dist/assets/RBChipsBoxTab.stories-CB_bfK8M.js +1 -0
  136. package/cli/storybook-dist/assets/RBColorIcons.stories-DHZKezld.js +3 -0
  137. package/cli/storybook-dist/assets/RBCommand.stories-DjuCDn5h.js +73 -0
  138. package/cli/storybook-dist/assets/RBConfirm.stories-HMmUPkB1.js +20 -0
  139. package/cli/storybook-dist/assets/RBDateInput.stories-BRfpYtJ4.js +15 -0
  140. package/cli/storybook-dist/assets/RBDialog.stories-Ln23Gyhk.js +38 -0
  141. package/cli/storybook-dist/assets/RBDivider.stories-Bhc_D5Cm.js +1 -0
  142. package/cli/storybook-dist/assets/RBDrawer.stories-CEEhamr_.js +14 -0
  143. package/cli/storybook-dist/assets/RBDropdownMenu.stories-DBvwDCnf.js +86 -0
  144. package/cli/storybook-dist/assets/RBEmptyView.stories-BY5QtXVc.js +12 -0
  145. package/cli/storybook-dist/assets/RBIconButton.stories-DyMFWpIV.js +1 -0
  146. package/cli/storybook-dist/assets/RBIcons.stories-DisU8G0d.js +3 -0
  147. package/cli/storybook-dist/assets/RBInlineLabelButton.stories-Cx0KJqxD.js +1 -0
  148. package/cli/storybook-dist/assets/RBJoyStick.stories-D1THM1Du.js +11 -0
  149. package/cli/storybook-dist/assets/RBLabelButton.stories-BDktlyzc.js +1 -0
  150. package/cli/storybook-dist/assets/RBLight.stories-C5qpnM6J.js +1 -0
  151. package/cli/storybook-dist/assets/RBMenuTreeIcons.stories-DUCeQPKJ.js +3 -0
  152. package/cli/storybook-dist/assets/RBMenubar.stories-ClyM9v37.js +3 -0
  153. package/cli/storybook-dist/assets/RBMultiRangeInput.stories--xSn64ww.js +14 -0
  154. package/cli/storybook-dist/assets/RBPagination.stories-C_uTj83z.js +17 -0
  155. package/cli/storybook-dist/assets/RBPopover.stories-DWmb9Por.js +11 -0
  156. package/cli/storybook-dist/assets/RBProgramIcons.stories-B8ZjEtvw.js +3 -0
  157. package/cli/storybook-dist/assets/RBPrompt.stories-BFKfyYtl.js +20 -0
  158. package/cli/storybook-dist/assets/RBRadioBox.stories-DUz8a27t.js +1 -0
  159. package/cli/storybook-dist/assets/RBRadioButton.stories-DKF61xvC.js +11 -0
  160. package/cli/storybook-dist/assets/RBRangeDateInput.stories-GhMVDyMr.js +15 -0
  161. package/cli/storybook-dist/assets/RBResizable.stories-V0iisR-x.js +20 -0
  162. package/cli/storybook-dist/assets/RBSelectBox.stories-DhvooAcg.js +6 -0
  163. package/cli/storybook-dist/assets/RBSheet.stories-C93Hpmvn.js +39 -0
  164. package/cli/storybook-dist/assets/RBSideNumberInput.stories-Y547mUsB.js +5 -0
  165. package/cli/storybook-dist/assets/RBSlider.stories-BJZyHiLO.js +5 -0
  166. package/cli/storybook-dist/assets/RBSpinner.stories-6wkbu-bU.js +14 -0
  167. package/cli/storybook-dist/assets/RBSwitch.stories-T9InolEP.js +65 -0
  168. package/cli/storybook-dist/assets/RBTab.stories-CjGzZODx.js +1 -0
  169. package/cli/storybook-dist/assets/RBTable.stories-CaolKsVG.js +62 -0
  170. package/cli/storybook-dist/assets/RBTableRow.stories-D13urGfs.js +11 -0
  171. package/cli/storybook-dist/assets/RBTabs.stories-Cv2iIxvP.js +21 -0
  172. package/cli/storybook-dist/assets/RBTextArea.stories-CmrPeit2.js +5 -0
  173. package/cli/storybook-dist/assets/RBTextField.stories-ByW-EKCD.js +45 -0
  174. package/cli/storybook-dist/assets/RBTextInput.stories-DSPlr3uh.js +5 -0
  175. package/cli/storybook-dist/assets/RBToast.stories-DJNZ9tku.js +11 -0
  176. package/cli/storybook-dist/assets/RBToggle.stories-BDQW2WO1.js +12 -0
  177. package/cli/storybook-dist/assets/RBToggleGroup.stories-CjwH6YbI.js +17 -0
  178. package/cli/storybook-dist/assets/RBTooltip.stories-Cp1oL68L.js +9 -0
  179. package/cli/storybook-dist/assets/RainbowUiProvider-BYlbsa69.css +1 -0
  180. package/cli/storybook-dist/assets/RainbowUiProvider-B_piZBLb.js +31 -0
  181. package/cli/storybook-dist/assets/browserUtils-DeopUpDy.js +1 -0
  182. package/cli/storybook-dist/assets/chunk-XP5HYGXS-BHXC9YeQ.js +1 -0
  183. package/cli/storybook-dist/assets/client-DTf7SId2.js +1 -0
  184. package/cli/storybook-dist/assets/createConveyorBeltLayer-769P8deX.js +6257 -0
  185. package/cli/storybook-dist/assets/createConveyorBeltLayer-Ds5AOper.css +21 -0
  186. package/cli/storybook-dist/assets/entry-preview-CNdFC92L.js +2 -0
  187. package/cli/storybook-dist/assets/entry-preview-docs-b8bpVBXy.js +46 -0
  188. package/cli/storybook-dist/assets/iframe-CLUgOMp4.js +211 -0
  189. package/cli/storybook-dist/assets/index-B4FGfHer.js +1 -0
  190. package/cli/storybook-dist/assets/index-BRq5TIsn.js +24 -0
  191. package/cli/storybook-dist/assets/index-C1OzuK6-.js +240 -0
  192. package/cli/storybook-dist/assets/index-CT3U1fyk.js +1 -0
  193. package/cli/storybook-dist/assets/index-ChsGqxH_.js +9 -0
  194. package/cli/storybook-dist/assets/index-DKFBREX_.js +1 -0
  195. package/cli/storybook-dist/assets/index-DgH-xKnr.js +11 -0
  196. package/cli/storybook-dist/assets/index-Dmy2JGbm.js +1 -0
  197. package/cli/storybook-dist/assets/index-DrFu-skq.js +6 -0
  198. package/cli/storybook-dist/assets/index-mT46TdHs.js +575 -0
  199. package/cli/storybook-dist/assets/jsx-runtime-BTJTZTIL.js +9 -0
  200. package/cli/storybook-dist/assets/keymapTileIndex.worker-BsNYNbqs.js +1 -0
  201. package/cli/storybook-dist/assets/preview-B8lJiyuQ.js +34 -0
  202. package/cli/storybook-dist/assets/preview-BBWR9nbA.js +1 -0
  203. package/cli/storybook-dist/assets/preview-BWzBA1C2.js +396 -0
  204. package/cli/storybook-dist/assets/preview-CILRoSnB.js +1 -0
  205. package/cli/storybook-dist/assets/preview-CVgpLj3b.js +1 -0
  206. package/cli/storybook-dist/assets/preview-Com4scch.css +1 -0
  207. package/cli/storybook-dist/assets/preview-CvbIS5ZJ.js +1 -0
  208. package/cli/storybook-dist/assets/preview-CvycU0Br.js +2 -0
  209. package/cli/storybook-dist/assets/preview-CzhIqee1.js +3 -0
  210. package/cli/storybook-dist/assets/preview-DD_OYowb.js +1 -0
  211. package/cli/storybook-dist/assets/preview-DGUiP6tS.js +7 -0
  212. package/cli/storybook-dist/assets/preview-DHQbi4pV.js +1 -0
  213. package/cli/storybook-dist/assets/react-18-hB3smqWJ.js +1 -0
  214. package/cli/storybook-dist/assets/react-resizable-panels.browser-CvAJUcrm.js +72 -0
  215. package/cli/storybook-dist/assets/test-utils-BQudk2xt.js +9 -0
  216. package/cli/storybook-dist/favicon.svg +1 -0
  217. package/cli/storybook-dist/iframe.html +666 -0
  218. package/cli/storybook-dist/index.html +190 -0
  219. package/cli/storybook-dist/index.json +1 -0
  220. package/cli/storybook-dist/nunito-sans-bold-italic.woff2 +0 -0
  221. package/cli/storybook-dist/nunito-sans-bold.woff2 +0 -0
  222. package/cli/storybook-dist/nunito-sans-italic.woff2 +0 -0
  223. package/cli/storybook-dist/nunito-sans-regular.woff2 +0 -0
  224. package/cli/storybook-dist/project.json +1 -0
  225. package/cli/storybook-dist/sb-addons/chromatic-com-storybook-10/manager-bundle.js +331 -0
  226. package/cli/storybook-dist/sb-addons/chromatic-com-storybook-10/manager-bundle.js.LEGAL.txt +51 -0
  227. package/cli/storybook-dist/sb-addons/essentials-actions-3/manager-bundle.js +3 -0
  228. package/cli/storybook-dist/sb-addons/essentials-backgrounds-5/manager-bundle.js +12 -0
  229. package/cli/storybook-dist/sb-addons/essentials-controls-2/manager-bundle.js +405 -0
  230. package/cli/storybook-dist/sb-addons/essentials-docs-4/manager-bundle.js +245 -0
  231. package/cli/storybook-dist/sb-addons/essentials-measure-8/manager-bundle.js +3 -0
  232. package/cli/storybook-dist/sb-addons/essentials-outline-9/manager-bundle.js +3 -0
  233. package/cli/storybook-dist/sb-addons/essentials-toolbars-7/manager-bundle.js +3 -0
  234. package/cli/storybook-dist/sb-addons/essentials-viewport-6/manager-bundle.js +3 -0
  235. package/cli/storybook-dist/sb-addons/links-1/manager-bundle.js +3 -0
  236. package/cli/storybook-dist/sb-addons/storybook-13/manager-bundle.js +3 -0
  237. package/cli/storybook-dist/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js +3 -0
  238. package/cli/storybook-dist/sb-addons/storybook-experimental-addon-test-12/manager-bundle.js +223 -0
  239. package/cli/storybook-dist/sb-addons/themes-11/manager-bundle.js +3 -0
  240. package/cli/storybook-dist/sb-common-assets/favicon.svg +1 -0
  241. package/cli/storybook-dist/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
  242. package/cli/storybook-dist/sb-common-assets/nunito-sans-bold.woff2 +0 -0
  243. package/cli/storybook-dist/sb-common-assets/nunito-sans-italic.woff2 +0 -0
  244. package/cli/storybook-dist/sb-common-assets/nunito-sans-regular.woff2 +0 -0
  245. package/cli/storybook-dist/sb-manager/globals-module-info.js +1052 -0
  246. package/cli/storybook-dist/sb-manager/globals-runtime.js +41775 -0
  247. package/cli/storybook-dist/sb-manager/globals.js +48 -0
  248. package/cli/storybook-dist/sb-manager/runtime.js +12048 -0
  249. package/cli/templates/common/.mcp.json +8 -0
  250. package/cli/templates/common/README.md +644 -81
  251. package/cli/templates/common/assets/icon.svg +44 -2
  252. package/cli/templates/common/manifest.json +13 -13
  253. package/cli/templates/common/package.json +11 -4
  254. package/cli/templates/common/py/main.py +98 -0
  255. package/cli/templates/common/requirements.txt +4 -3
  256. package/cli/templates/common/tsconfig.json +1 -1
  257. package/cli/templates/common/ui/content.tsx +216 -0
  258. package/cli/templates/common/ui/style.css +6 -0
  259. package/cli/templates/common/vite.config.ts +6 -25
  260. package/cli/templates/cpp/cpp/CMakeLists.txt +14 -0
  261. package/cli/templates/cpp/cpp/main.cpp +26 -0
  262. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +1 -1
  263. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +17 -0
  264. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -0
  265. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +22 -0
  266. package/cli/vite.d.mts +25 -0
  267. package/cli/vite.mjs +292 -0
  268. package/package.json +32 -5
  269. package/cli/src/pack.mjs +0 -61
  270. package/cli/src/preview.mjs +0 -91
  271. package/cli/templates/common/main.py +0 -114
  272. package/cli/templates/common/src/content.tsx +0 -33
package/cli/src/new.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // muscat-pk new — scaffold a new plugin project from templates.
2
- import { execFileSync, execSync } from "node:child_process";
2
+ import { execSync } from "node:child_process";
3
3
  import {
4
4
  cpSync,
5
5
  existsSync,
@@ -7,22 +7,79 @@ import {
7
7
  readFileSync,
8
8
  readdirSync,
9
9
  renameSync,
10
+ rmSync,
10
11
  statSync,
11
12
  writeFileSync,
12
13
  } from "node:fs";
13
- import { createInterface } from "node:readline/promises";
14
- import { basename, resolve } from "node:path";
15
- import { detectPythonCmd, hasBin, parseArgs, templatesDir, log, venvPython } from "./util.mjs";
14
+ import { basename, dirname, resolve } from "node:path";
15
+ import { color, ensureIdeVenv, hasBin, kitVersion, parseArgs, prompt, templatesDir, log } from "./util.mjs";
16
16
 
17
- const TYPES = ["python", "ros2"];
17
+ // Like cpSync(src, dst, {recursive:true}) but reads from the embedded snapshot
18
+ // when the templates dir doesn't exist on disk (compiled binary mode).
19
+ // ponytail: import() is module-cached by the runtime — no manual cache needed.
20
+ async function copyTemplate(srcDir, dst) {
21
+ if (existsSync(templatesDir)) { cpSync(srcDir, dst, { recursive: true }); return; }
22
+ const { TEMPLATES } = await import("./templates-embedded.mjs");
23
+ const prefix = srcDir.slice(templatesDir.length + 1).replace(/\\/g, "/") + "/";
24
+ for (const [key, b64] of Object.entries(TEMPLATES)) {
25
+ if (!key.startsWith(prefix)) continue;
26
+ const dest = resolve(dst, key.slice(prefix.length));
27
+ mkdirSync(dirname(dest), { recursive: true });
28
+ writeFileSync(dest, Buffer.from(b64, "base64"));
29
+ }
30
+ }
31
+
32
+ // `--type` values map to a runtime descriptor. ros2 has a python (rclpy) and a
33
+ // C++ (rclcpp) flavor; we expose them as distinct type choices for clarity.
34
+ export const TYPE_CHOICES = {
35
+ python: { type: "python" },
36
+ cpp: { type: "cpp" },
37
+ ros2: { type: "ros2", rosClient: "rclpy" },
38
+ "ros2-cpp":{ type: "ros2", rosClient: "rclcpp" },
39
+ };
40
+ const TYPES = Object.keys(TYPE_CHOICES);
18
41
  const PMS = ["npm", "yarn", "pnpm", "bun"];
19
42
 
20
- async function prompt(question, fallback) {
21
- if (!process.stdin.isTTY) return fallback;
22
- const rl = createInterface({ input: process.stdin, output: process.stdout });
23
- const answer = (await rl.question(question)).trim();
24
- rl.close();
25
- return answer || fallback;
43
+ function printNewSuccess({ targetName, type, pm }) {
44
+ const mark = [
45
+ " /\\",
46
+ " / \\_",
47
+ " ____||__",
48
+ " / \\__/ \\",
49
+ " __/\\__/ \\__/\\",
50
+ " / \\__/\\__/\\__/\\",
51
+ " \\__/ \\__/ \\__/",
52
+ " \\__/ \\__/",
53
+ " \\__/",
54
+ ].map((line) => color.green(line)).join("\n");
55
+
56
+ log.info("");
57
+ log.info(mark);
58
+ log.info(color.bold(color.green("Welcome to Muscat Plugin Kit!")));
59
+ log.info(color.dim("A fresh plugin workspace is ready."));
60
+ log.info("");
61
+ log.ok(`created ${color.bold(targetName)} ${color.dim(`(type: ${type}, pm: ${pm})`)}`);
62
+ log.info("");
63
+ log.info(color.bold("Next steps"));
64
+ log.info(` ${color.cyan("cd")} ${targetName}`);
65
+ log.info(` ${color.cyan("muscat-pk dev")} ${color.dim("# preview in mini-Muscat + back end")}`);
66
+ log.info(` ${color.cyan("muscat-pk build")} ${color.dim("# build + distributable ZIP")}`);
67
+ log.info("");
68
+ }
69
+
70
+ // Build the manifest runtime object for a given type choice + plugin slug.
71
+ export function runtimeForChoice(choice, slug) {
72
+ switch (choice.type) {
73
+ case "cpp":
74
+ // entry = built executable name (matches add_executable in cpp/CMakeLists.txt)
75
+ return { type: "cpp", entry: slug };
76
+ case "ros2":
77
+ // package = colcon package name; launch = node executable run on the robot
78
+ return { type: "ros2", rosClient: choice.rosClient, package: slug, launch: `${slug}_node` };
79
+ case "python":
80
+ default:
81
+ return { type: "python", entry: "main.py" };
82
+ }
26
83
  }
27
84
 
28
85
  export function applyTokens(dir, tokens) {
@@ -74,16 +131,25 @@ function renameSlugPaths(dir, slug) {
74
131
  // Copy the ros2_ws/ overlay into a plugin and substitute the package slug into
75
132
  // every path segment. The dev Dockerfile is NOT scaffolded here — it lives in
76
133
  // the kit (cli/docker/) so authors can't edit it. Caller runs applyTokens after.
77
- export function overlayRos2(target, slug) {
78
- cpSync(resolve(templatesDir, "ros2"), target, { recursive: true });
134
+ // rclpy → templates/ros2 (ament_python)
135
+ // rclcpp → templates/ros2-cpp (ament_cmake)
136
+ export async function overlayRos2(target, slug, rosClient = "rclpy") {
137
+ const tmpl = rosClient === "rclcpp" ? "ros2-cpp" : "ros2";
138
+ await copyTemplate(resolve(templatesDir, tmpl), target);
79
139
  renameSlugPaths(resolve(target, "ros2_ws"), slug);
80
140
  }
81
141
 
142
+ // Copy the cpp/ overlay (CMake project) into a plugin. Caller runs applyTokens after.
143
+ export async function overlayCpp(target) {
144
+ await copyTemplate(resolve(templatesDir, "cpp"), target);
145
+ }
146
+
82
147
  export async function runNew(args) {
83
148
  const opts = parseArgs(args, { booleans: ["yes", "no-install", "help"] });
84
149
  if (opts.help) {
85
150
  console.log(
86
- "muscat-pk new [dir] [--type python|ros2] [--pm npm|yarn|pnpm|bun]\n" +
151
+ "muscat-pk new [dir] [--type python|cpp|ros2|ros2-cpp] [--pm npm|yarn|pnpm|bun]\n" +
152
+ " [--author <name>] [--email <email>] [--description <text>]\n" +
87
153
  " [--yes] [--no-install]",
88
154
  );
89
155
  return;
@@ -91,25 +157,53 @@ export async function runNew(args) {
91
157
 
92
158
  const dirArg = opts._[0];
93
159
  const targetName =
94
- dirArg || (await prompt("Plugin directory name: ", "my-plugin"));
160
+ dirArg || (await prompt(color.cyan("Plugin directory name: "), "my-plugin"));
95
161
  const target = resolve(process.cwd(), targetName);
96
162
  if (existsSync(target) && readdirSync(target).length > 0) {
97
163
  throw new Error(`directory not empty: ${target}`);
98
164
  }
99
165
 
166
+ let authorArg = opts.author;
167
+ let emailArg = opts.email;
168
+ let descriptionArg = opts.description;
169
+
170
+ if (!opts.yes) {
171
+ if (!authorArg) authorArg = await prompt(color.cyan("Author name: "), "");
172
+ if (!emailArg) emailArg = await prompt(`${color.cyan("Email")} ${color.yellow("(required)")}: `, "");
173
+ if (!descriptionArg) descriptionArg = await prompt(color.cyan("Description (English): "), "");
174
+ }
175
+
100
176
  let type = opts.type;
101
177
  if (!type && !opts.yes) {
102
- console.log("\n플러그인 타입을 선택하세요:");
103
- console.log(" 1) Python");
104
- console.log(" 2) ROS2(rclpy)");
105
- const choice = await prompt("\n선택 [1/2, default: 1]: ", "1");
106
- type = choice === "2" ? "ros2" : "python";
178
+ console.log(`\n${color.bold(color.cyan("Select the plugin type:"))}`);
179
+ console.log(` ${color.green("1)") } ${color.bold("Python")}`);
180
+ console.log(` ${color.green("2)") } ${color.bold("C++")}`);
181
+ console.log(` ${color.green("3)") } ${color.bold("ROS2")} ${color.dim("(rclpy / Python)")}`);
182
+ console.log(` ${color.green("4)") } ${color.bold("ROS2")} ${color.dim("(rclcpp / C++)")}`);
183
+ const choice = await prompt(`\n${color.cyan("선택")} ${color.dim("[1/2/3/4, default: 1]")}: `, "1");
184
+ type = { "2": "cpp", "3": "ros2", "4": "ros2-cpp" }[choice] ?? "python";
107
185
  }
108
186
  type = TYPES.includes(type) ? type : "python";
187
+ const runtimeChoice = TYPE_CHOICES[type];
188
+
189
+ // Auto-detect yarn workspace: if .yarnrc.yml exists anywhere up the tree, default to yarn.
190
+ function isInsideYarnWorkspace(dir) {
191
+ let cur = dir;
192
+ while (true) {
193
+ if (existsSync(resolve(cur, ".yarnrc.yml"))) return true;
194
+ const parent = resolve(cur, "..");
195
+ if (parent === cur) return false;
196
+ cur = parent;
197
+ }
198
+ }
109
199
 
110
200
  let pm = opts.pm;
111
201
  if (!pm && !opts.yes) {
112
- pm = await prompt(`Package manager (${PMS.join("/")}) [npm]: `, "npm");
202
+ const defaultPm = isInsideYarnWorkspace(process.cwd()) ? "yarn" : "npm";
203
+ pm = await prompt(
204
+ `${color.cyan("Front-end package manager")} ${color.dim(`(${PMS.join("/")})`)} ${color.dim(`[${defaultPm}]`)}: `,
205
+ defaultPm,
206
+ );
113
207
  }
114
208
  pm = PMS.includes(pm) ? pm : "npm";
115
209
 
@@ -117,11 +211,18 @@ export async function runNew(args) {
117
211
 
118
212
  mkdirSync(target, { recursive: true });
119
213
 
120
- // common(UI + main.py + requirements.txt + manifest/config)은 항상 포함.
121
- // ros2면 ros2_ws/ 오버레이만 추가로 얹는다. python은 ros2_ws/가 없을 뿐 구조는 동일.
214
+ // common(UI + manifest/config)은 항상 포함. 백엔드 오버레이는 타입별로 얹는다.
215
+ // python → common의 py/ 사용
216
+ // cpp → cpp/ 오버레이, py/·requirements.txt 제거
217
+ // ros2(*) → ros2_ws/ 오버레이 (rclpy=ament_python, rclcpp=ament_cmake)
122
218
  log.step(`scaffolding ${type} plugin → ${target}`);
123
- cpSync(resolve(templatesDir, "common"), target, { recursive: true });
124
- if (type === "ros2") overlayRos2(target, slug);
219
+ await copyTemplate(resolve(templatesDir, "common"), target);
220
+ if (runtimeChoice.type === "ros2") await overlayRos2(target, slug, runtimeChoice.rosClient);
221
+ if (runtimeChoice.type === "cpp") {
222
+ await overlayCpp(target);
223
+ rmSync(resolve(target, "py"), { recursive: true, force: true });
224
+ rmSync(resolve(target, "requirements.txt"), { force: true });
225
+ }
125
226
 
126
227
  applyTokens(target, {
127
228
  __PLUGIN_ID__: pluginId,
@@ -129,14 +230,24 @@ export async function runNew(args) {
129
230
  __PLUGIN_DIR__: dir,
130
231
  __PLUGIN_SLUG__: slug,
131
232
  __PLUGIN_CLASS__: pluginClass,
233
+ __PLUGIN_AUTHOR__: authorArg || "",
234
+ __PLUGIN_EMAIL__: emailArg || "",
132
235
  __PM__: pm,
133
- __RUNTIME_TYPE__: type,
236
+ __RUNTIME_TYPE__: runtimeChoice.type,
237
+ __RB_VERSION__: kitVersion(),
134
238
  });
135
239
 
240
+ // Write the correct runtime block into manifest.json (per-type fields).
241
+ const manifestPath = resolve(target, "manifest.json");
242
+ const manifestJson = JSON.parse(readFileSync(manifestPath, "utf8"));
243
+ manifestJson.runtime = runtimeForChoice(runtimeChoice, slug);
244
+ if (descriptionArg) manifestJson.description = { en: descriptionArg };
245
+ writeFileSync(manifestPath, JSON.stringify(manifestJson, null, 2) + "\n");
246
+
136
247
  // gitignore (templates can't ship a literal .gitignore via npm reliably)
137
248
  writeFileSync(
138
249
  resolve(target, ".gitignore"),
139
- ["node_modules", "dist", ".venv", "*.zip", "ros2_ws/install", ""].join("\n"),
250
+ ["node_modules", "dist", ".muscat-dev", ".venv", "*.zip", "ros2_ws/install", "cpp/build", ""].join("\n"),
140
251
  );
141
252
 
142
253
  if (!opts["no-install"]) {
@@ -146,61 +257,53 @@ export async function runNew(args) {
146
257
  } catch {
147
258
  log.warn(`'${pm} install' failed; run it manually in ${targetName}`);
148
259
  }
149
- }
150
-
151
- let noPython = false;
152
- let venvFailed = false;
153
260
 
154
- if (!opts["no-install"]) {
155
- // Python venv (robot targets 3.12) — cross-platform.
156
- // uv is preferred: handles uv-managed Pythons that break standard ensurepip.
157
- try {
158
- const venv = resolve(target, ".venv");
159
- log.step("creating .venv + installing requirements…");
160
- if (hasBin("uv")) {
161
- execFileSync("uv", ["venv", venv, "--python", "3.12", "--seed"], { cwd: target, stdio: "inherit" });
162
- } else if (hasBin("conda")) {
163
- execFileSync("conda", ["create", "-p", venv, "python=3.12", "-y", "--quiet"], { cwd: target, stdio: "inherit" });
164
- } else if (hasBin("python3.12") || hasBin("python3") || hasBin("python")) {
165
- const { cmd, is312 } = detectPythonCmd();
166
- if (!is312) log.warn("python3.12 not found; venv uses fallback python (robot targets 3.12).");
167
- execFileSync(cmd[0], [...cmd.slice(1), "-m", "venv", venv], { cwd: target, stdio: "inherit" });
261
+ // Local .venv is for your editor/linter (pylint, pyright, ...) only — the
262
+ // backend actually RUNS in Docker (see README), this never affects that.
263
+ // rclcpp is C++, so no venv applies there.
264
+ const needsIdeVenv =
265
+ runtimeChoice.type === "python" ||
266
+ (runtimeChoice.type === "ros2" && runtimeChoice.rosClient === "rclpy");
267
+ if (needsIdeVenv) {
268
+ log.step("creating .venv for your editor (pylint/pyright)…");
269
+ if (ensureIdeVenv(target)) {
270
+ log.ok(".venv ready — point your editor's Python interpreter there.");
168
271
  } else {
169
- noPython = true;
170
- }
171
- if (!noPython) {
172
- const py = venvPython(venv);
173
- if (existsSync(resolve(target, "requirements.txt"))) {
174
- execFileSync(py, ["-m", "pip", "install", "-q", "-r", "requirements.txt"], { cwd: target, stdio: "inherit" });
175
- }
272
+ log.warn("Could not create .venv (no local Python/uv?) — skipping, Docker dev still works.");
176
273
  }
274
+ }
275
+ }
276
+
277
+ // git init (best-effort). Always runs, even nested inside another repo —
278
+ // delete target/.git if you don't want it.
279
+ if (hasBin("git")) {
280
+ try {
281
+ execSync("git init", { cwd: target, stdio: "ignore" });
282
+ execSync("git add -A", { cwd: target, stdio: "ignore" });
283
+ execSync('git commit -m "Initial commit from muscat-pk new" --no-verify', { cwd: target, stdio: "ignore" });
177
284
  } catch {
178
- venvFailed = true;
285
+ log.warn("git init failed — run it manually if you want version control here.");
179
286
  }
180
- } else {
181
- noPython = !hasBin("uv") && !hasBin("conda") && !hasBin("python3.12") && !hasBin("python3") && !hasBin("python");
182
287
  }
183
288
 
184
- log.ok(`created ${targetName} (type: ${type}, pm: ${pm})`);
185
- log.info(`\n cd ${targetName}`);
186
- log.info(` muscat-pk dev # preview in mini-Muscat + back end`);
187
- log.info(` muscat-pk pack # build a distributable ZIP\n`);
289
+ // The back end (python / cpp / ros2) runs in a Docker container that matches
290
+ // the robot — no local Python/toolchain setup needed. Only `none` skips it.
291
+ const needsDocker = runtimeChoice.type !== "none";
292
+ const noDocker = needsDocker && !hasBin("docker");
293
+
294
+ printNewSuccess({ targetName, type, pm });
188
295
 
189
296
  // ── post-scaffold environment warnings ───────────────────────────────────
190
297
  const noNode = !hasBin("node");
191
- if (noNode || noPython || venvFailed) {
298
+ if (noNode || noDocker) {
192
299
  log.info("");
193
300
  if (noNode) {
194
301
  log.warn("Node.js not found. Install Node.js 20+ to run JS deps and dev:ui.");
195
302
  log.warn(" https://nodejs.org");
196
303
  }
197
- if (noPython) {
198
- log.warn("Python not found. Install Python 3.12 to run the backend.");
199
- log.warn(" https://www.python.org/downloads/ (or: brew install python@3.12 / uv / conda)");
200
- log.warn(` Then: cd ${targetName} && python3.12 -m venv .venv && pip install -r requirements.txt`);
201
- } else if (venvFailed) {
202
- log.warn("venv setup failed. Create it manually:");
203
- log.warn(` cd ${targetName} && python3.12 -m venv .venv && pip install -r requirements.txt`);
304
+ if (noDocker) {
305
+ log.warn("Docker not found. The plugin back end runs in a container (matches the robot env).");
306
+ log.warn(" https://docs.docker.com/get-docker/");
204
307
  }
205
308
  log.info("");
206
309
  }
@@ -0,0 +1,112 @@
1
+
2
+ import { createRequire } from "node:module";
3
+ import { writeFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ const pluginRoot = resolve(process.argv[2]);
8
+ const port = Number(process.argv[3] || "4181");
9
+ const readyFile = process.argv[4] ? resolve(process.argv[4]) : "";
10
+
11
+
12
+ process.chdir(pluginRoot);
13
+
14
+ const require = createRequire(resolve(pluginRoot, "package.json"));
15
+
16
+ const viteMod = await import(pathToFileURL(require.resolve("vite")));
17
+ const createServer = viteMod.createServer ?? viteMod.default?.createServer;
18
+
19
+ const BOOTSTRAP_PREFIX = "\0rb-bootstrap:";
20
+ const PREAMBLE_ID = "\0rb-preamble";
21
+ const bootstrapPlugin = {
22
+ name: "rb-plugin-bootstrap",
23
+ resolveId(id) {
24
+ if (id.startsWith("rb-bootstrap:")) return "\0" + id;
25
+ if (id === "rb-preamble") return PREAMBLE_ID;
26
+ },
27
+ load(id) {
28
+ if (id === PREAMBLE_ID) {
29
+ return `
30
+ import RefreshRuntime from "/@react-refresh";
31
+ RefreshRuntime.injectIntoGlobalHook(window);
32
+ window.$RefreshReg$ = () => {};
33
+ window.$RefreshSig$ = () => (type) => type;
34
+ window.__vite_plugin_react_preamble_installed__ = true;
35
+ `;
36
+ }
37
+ if (!id.startsWith(BOOTSTRAP_PREFIX)) return;
38
+ const ui = id.slice(BOOTSTRAP_PREFIX.length) || "content";
39
+ const entry = JSON.stringify(`muscat-ui-entry:${ui}`);
40
+ return `
41
+ import "rb-preamble";
42
+ import { createElement } from "react";
43
+ import { createRoot } from "react-dom/client";
44
+ import { RbContext } from "@rainbow-robotics/plugin-ui/sdk";
45
+ import { setHostServices } from "@rainbow-robotics/plugin-ui/host-ui";
46
+ import { RbWs, createLazyRbSdk } from "@rainbow-robotics/plugin-ui/ws";
47
+ import App from ${entry};
48
+
49
+ // HMR passes the new module to the accept callback but doesn't rebind the
50
+ // static \`App\` import, so we track the latest at module scope. Without this,
51
+ // navigating away and back (a fresh mount() on this cached module) renders the
52
+ // stale App from first import. \`activeRender\` points at the mounted root so
53
+ // live edits re-render in place.
54
+ let latestApp = App;
55
+ let activeRender = null;
56
+ if (import.meta.hot) {
57
+ import.meta.hot.accept(${entry}, (m) => {
58
+ if (!m || !m.default) return;
59
+ latestApp = m.default;
60
+ activeRender?.(latestApp);
61
+ });
62
+ }
63
+
64
+ export function mount(container, { socketUrl, pluginId, hostServices, data, onSave }) {
65
+ container.setAttribute("data-muscat-plugin-root", "");
66
+ container.setAttribute("data-muscat-plugin-id", pluginId);
67
+ setHostServices(hostServices);
68
+ // Lazy connect: don't block the first render on the WebSocket handshake
69
+ // (there may be no backend at all in dev, e.g. no robot reachable) — each rb
70
+ // method awaits this promise internally instead.
71
+ const wsPromise = RbWs.connect(socketUrl, pluginId);
72
+ const rb = createLazyRbSdk(wsPromise, hostServices, { data, onSave });
73
+ const root = createRoot(container);
74
+ let mounted = true;
75
+ const render = (Component) => {
76
+ if (!mounted) return;
77
+ root.render(createElement(RbContext.Provider, { value: rb }, createElement(Component)));
78
+ };
79
+ const boundRender = (Component) => queueMicrotask(() => render(Component));
80
+ activeRender = boundRender;
81
+ render(latestApp);
82
+ return () => {
83
+ mounted = false;
84
+ // Don't clobber a newer mount's render (host defers unmount a microtask).
85
+ if (activeRender === boundRender) activeRender = null;
86
+ root.unmount();
87
+ };
88
+ }
89
+ `;
90
+ },
91
+ };
92
+
93
+ const server = await createServer({
94
+ root: pluginRoot,
95
+ configFile: resolve(pluginRoot, "vite.config.ts"),
96
+ mode: "development",
97
+ plugins: [bootstrapPlugin],
98
+ server: {
99
+ port,
100
+ strictPort: false,
101
+ cors: true,
102
+ hmr: { host: "localhost", protocol: "ws" },
103
+ },
104
+ });
105
+
106
+ await server.listen();
107
+ const address = server.httpServer?.address();
108
+ const actualPort = typeof address === "object" && address ? address.port : port;
109
+ if (readyFile) {
110
+ writeFileSync(readyFile, JSON.stringify({ port: actualPort }), "utf8");
111
+ }
112
+ console.log(`[plugin-dev] vite dev server → http://localhost:${actualPort}`);
@@ -0,0 +1,160 @@
1
+ // Port contract helpers for plugin back ends.
2
+ import { spawnSync } from "node:child_process";
3
+
4
+ export const PLUGIN_PORT_MIN = 28600;
5
+ export const PLUGIN_PORT_MAX = 28999;
6
+ export const INTERNAL_DEV_PORTS = new Set([10000]);
7
+ export const RESERVED_BACKEND_PORTS = new Map([
8
+ [502, "Modbus TCP"],
9
+ [4840, "OPC-UA"],
10
+ [10000, "muscat-pk Socket.IO bridge"],
11
+ [2222, "EtherNet/IP Implicit"],
12
+ [30004, "RTDE"],
13
+ [34964, "ProfiNET"],
14
+ [44818, "EtherNet/IP Explicit"],
15
+ [50051, "gRPC"],
16
+ ]);
17
+ export const RESERVED_BACKEND_PORT_RANGES = [
18
+ { from: 5170, to: 5179, name: "UI 517x servers" },
19
+ { from: 8000, to: 8999, name: "Muscat/internal 8000 range" },
20
+ { from: 50000, to: 50005, name: "robot TCP/UDP ASCII and REST interfaces" },
21
+ ];
22
+
23
+ function reservedPortReason(port) {
24
+ if (RESERVED_BACKEND_PORTS.has(port)) return RESERVED_BACKEND_PORTS.get(port);
25
+ const range = RESERVED_BACKEND_PORT_RANGES.find((item) => port >= item.from && port <= item.to);
26
+ if (!range) return null;
27
+ return `${range.name} (${range.from}-${range.to})`;
28
+ }
29
+
30
+ function isPluginManagedPort(port) {
31
+ return reservedPortReason(port) || (port >= PLUGIN_PORT_MIN && port <= PLUGIN_PORT_MAX);
32
+ }
33
+
34
+ function isInt(value) {
35
+ return Number.isInteger(value);
36
+ }
37
+
38
+ function validatePort(port, field, errors) {
39
+ if (!isInt(port)) {
40
+ errors.push(`${field}: must be a port number`);
41
+ return;
42
+ }
43
+ if (port < 1 || port > 65535) {
44
+ errors.push(`${field}: port must be between 1 and 65535`);
45
+ return;
46
+ }
47
+ const reserved = reservedPortReason(port);
48
+ if (reserved) {
49
+ errors.push(`${field}: ${port} is reserved for ${reserved}`);
50
+ return;
51
+ }
52
+ if (port < PLUGIN_PORT_MIN || port > PLUGIN_PORT_MAX) {
53
+ errors.push(
54
+ `${field}: external plugin backend ports must be within ${PLUGIN_PORT_MIN}-${PLUGIN_PORT_MAX}`,
55
+ );
56
+ }
57
+ }
58
+
59
+ export function normalizePorts(ports, errors = []) {
60
+ if (ports === undefined || ports === null) return [];
61
+ if (!Array.isArray(ports)) {
62
+ errors.push(`"ports" must be an array`);
63
+ return [];
64
+ }
65
+ if (ports.length === 0) {
66
+ errors.push(`"ports" must not be empty when provided`);
67
+ return [];
68
+ }
69
+
70
+ const normalized = [];
71
+ const seen = new Set();
72
+ ports.forEach((port, i) => {
73
+ validatePort(port, `ports[${i}]`, errors);
74
+ if (!isInt(port)) return;
75
+ if (seen.has(port)) {
76
+ errors.push(`"ports" entries must be unique (duplicate: ${port})`);
77
+ return;
78
+ }
79
+ seen.add(port);
80
+ normalized.push(port);
81
+ });
82
+
83
+ return normalized;
84
+ }
85
+
86
+ export function validatePorts(ports, errors) {
87
+ normalizePorts(ports, errors);
88
+ }
89
+
90
+ export function manifestPortRanges(manifest) {
91
+ return normalizePorts(manifest?.ports, []);
92
+ }
93
+
94
+ export function formatPortRange(range) {
95
+ return String(range);
96
+ }
97
+
98
+ export function formatPortRanges(ranges) {
99
+ return ranges.map(formatPortRange).join(", ");
100
+ }
101
+
102
+ export function portInRanges(port, ranges) {
103
+ return ranges.includes(port);
104
+ }
105
+
106
+ export function portEnvFlags(ranges) {
107
+ if (ranges.length === 0) return [];
108
+ return ["-e", `RB_PLUGIN_PORTS=${ranges.map(formatPortRange).join(",")}`];
109
+ }
110
+
111
+ function listeningTcpPorts(container) {
112
+ const script = `
113
+ for f in /proc/net/tcp /proc/net/tcp6; do
114
+ [ -r "$f" ] || continue
115
+ tail -n +2 "$f" | while read -r sl local_address rem_address st rest; do
116
+ [ "$st" = "0A" ] || continue
117
+ hex_port="\${local_address##*:}"
118
+ printf "%d\\n" "0x$hex_port"
119
+ done
120
+ done | sort -n -u
121
+ `;
122
+ const r = spawnSync("docker", ["exec", container, "bash", "-lc", script], {
123
+ encoding: "utf8",
124
+ });
125
+ if (r.status !== 0) return [];
126
+ return String(r.stdout || "")
127
+ .split(/\s+/)
128
+ .filter(Boolean)
129
+ .map((v) => Number(v))
130
+ .filter((v) => Number.isInteger(v));
131
+ }
132
+
133
+ export function startPortContractMonitor({ container, ranges, log, intervalMs = 1000 }) {
134
+ const declared = ranges ?? [];
135
+ let reported = false;
136
+ const timer = setInterval(() => {
137
+ const ports = listeningTcpPorts(container)
138
+ .filter((port) => !INTERNAL_DEV_PORTS.has(port))
139
+ .filter((port) => isPluginManagedPort(port));
140
+ const unexpected = ports.filter((port) => reservedPortReason(port) || !portInRanges(port, declared));
141
+ if (unexpected.length === 0) return;
142
+
143
+ clearInterval(timer);
144
+ reported = true;
145
+ const declaredText = declared.length > 0 ? formatPortRanges(declared) : "(none)";
146
+ log.error(
147
+ `backend opened reserved or undeclared plugin TCP port(s): ${unexpected.join(", ")}. ` +
148
+ `Add them to manifest.json "ports" within ${PLUGIN_PORT_MIN}-${PLUGIN_PORT_MAX}. ` +
149
+ `Declared: ${declaredText}`,
150
+ );
151
+ spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
152
+ process.exit(1);
153
+ }, intervalMs);
154
+ timer.unref?.();
155
+
156
+ return () => {
157
+ clearInterval(timer);
158
+ return reported;
159
+ };
160
+ }