@rainbow-robotics/muscat-plugin-kit 1.3.8-dev.2 → 1.10.1-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) 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 -153
  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 -0
  12. package/cli/src/build.mjs +447 -437
  13. package/cli/src/check-package.mjs +13 -13
  14. package/cli/src/completion.mjs +166 -166
  15. package/cli/src/cpp.mjs +78 -79
  16. package/cli/src/cppIde.mjs +75 -75
  17. package/cli/src/dev.mjs +165 -164
  18. package/cli/src/devBack.mjs +727 -670
  19. package/cli/src/devUi.mjs +243 -238
  20. package/cli/src/esbuild.mjs +56 -56
  21. package/cli/src/host_agent.py +223 -222
  22. package/cli/src/manifest.mjs +287 -283
  23. package/cli/src/mcp.mjs +1071 -1016
  24. package/cli/src/muscatLocal.mjs +209 -198
  25. package/cli/src/new.mjs +343 -338
  26. package/cli/src/pluginDevServer.mjs +111 -112
  27. package/cli/src/ports.mjs +193 -193
  28. package/cli/src/python.mjs +112 -113
  29. package/cli/src/ros2.mjs +109 -110
  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 -437
  36. package/cli/src/zenohDev.mjs +81 -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-DeirXVfl.js → Color-YHDXOIA2-D6JqZzDJ.js} +1 -1
  103. package/cli/storybook-dist/assets/{Docs-BLxyi8cP.js → Docs-6gD2kUo1.js} +2 -2
  104. package/cli/storybook-dist/assets/{Docs-NrBqEYvM.js → Docs-B9wd8Ok7.js} +3 -3
  105. package/cli/storybook-dist/assets/{Docs-wzFMK-XA.js → Docs-CMjovnSY.js} +4 -4
  106. package/cli/storybook-dist/assets/{Docs-Buyf7Djy.js → Docs-CgFbWWXM.js} +2 -2
  107. package/cli/storybook-dist/assets/{Docs-CnfIOwf8.js → Docs-DBQup8Bu.js} +3 -3
  108. package/cli/storybook-dist/assets/{Docs-BxmwdaYa.js → Docs-ESb7JAHg.js} +3 -3
  109. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-CWc43rTP.js → DocsRenderer-CFRXHY34-BOUI1AHi.js} +1 -1
  110. package/cli/storybook-dist/assets/{NxAvatar.stories-DfL0kqmN.js → NxAvatar.stories-DsYFMUh3.js} +1 -1
  111. package/cli/storybook-dist/assets/{NxButton.stories-WxSc_gsR.js → NxButton.stories-CHfNcqsc.js} +8 -8
  112. package/cli/storybook-dist/assets/{NxNumberInput.stories-C1D3oUiY.js → NxNumberInput.stories-BFw_4vFE.js} +1 -1
  113. package/cli/storybook-dist/assets/{NxPagination.stories-C1Qa88rn.js → NxPagination.stories-DVfxyw4M.js} +1 -1
  114. package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-CmteJX3t.js → NxPointerHoldButton.stories-BzyxUWoZ.js} +1 -1
  115. package/cli/storybook-dist/assets/{NxRangeInput.stories-B7E-mhyI.js → NxRangeInput.stories-DmRdzBc1.js} +6 -6
  116. package/cli/storybook-dist/assets/{NxSelectBox.stories-De67vMdU.js → NxSelectBox.stories-B1KkNUOf.js} +1 -1
  117. package/cli/storybook-dist/assets/{NxSlideRange.stories-DvCV1MGk.js → NxSlideRange.stories-CyOdvfQ6.js} +1 -1
  118. package/cli/storybook-dist/assets/{NxSwitch.stories-DuHDZ9-v.js → NxSwitch.stories-DrDBw0qp.js} +1 -1
  119. package/cli/storybook-dist/assets/{NxTextArea.stories-BT1o0ngg.js → NxTextArea.stories-DTB2aqNS.js} +1 -1
  120. package/cli/storybook-dist/assets/{NxTextInput.stories-CSLBT83Q.js → NxTextInput.stories-DU0rucmM.js} +1 -1
  121. package/cli/storybook-dist/assets/{RBAccordion.stories-QavkYgV0.js → RBAccordion.stories-DTfOtXtp.js} +3 -3
  122. package/cli/storybook-dist/assets/{RBAccordionGroup.stories-C0RPGnO7.js → RBAccordionGroup.stories-9y9BjrUe.js} +10 -10
  123. package/cli/storybook-dist/assets/{RBAlert.stories-BUCAT0Sd.js → RBAlert.stories-DLTAv9fT.js} +3 -3
  124. package/cli/storybook-dist/assets/{RBBadge.stories-CnBXTQ4J.js → RBBadge.stories-BNMACewA.js} +1 -1
  125. package/cli/storybook-dist/assets/{RBBattery.stories-5FbsZcdy.js → RBBattery.stories-B3kXMAt5.js} +1 -1
  126. package/cli/storybook-dist/assets/{RBCard.stories-FwTKjtCi.js → RBCard.stories-Y-s8R2n7.js} +39 -39
  127. package/cli/storybook-dist/assets/{RBCheckBox.stories-C0y5psMI.js → RBCheckBox.stories-DBuT9tL4.js} +1 -1
  128. package/cli/storybook-dist/assets/{RBCheckIconButton.stories-sJjAZBVh.js → RBCheckIconButton.stories-CWu1TL56.js} +1 -1
  129. package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-COJqEkDr.js → RBCheckLabelButton.stories-sCu2MGCX.js} +17 -17
  130. package/cli/storybook-dist/assets/{RBChipTab.stories-BnXj-Woa.js → RBChipTab.stories-BX9qLJkR.js} +5 -5
  131. package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-DE1yl3rz.js → RBChipsBoxTab.stories-35u1aJ-P.js} +1 -1
  132. package/cli/storybook-dist/assets/{RBColorIcons.stories-CYASERLQ.js → RBColorIcons.stories-DAOZ5Ies.js} +1 -1
  133. package/cli/storybook-dist/assets/RBCommand.stories-BSqXDkKw.js +73 -0
  134. package/cli/storybook-dist/assets/{RBConfirm.stories-XLLTJZRf.js → RBConfirm.stories-iMXcUh7Z.js} +3 -3
  135. package/cli/storybook-dist/assets/{RBDateInput.stories-DpVylrQl.js → RBDateInput.stories-hK5rXECf.js} +1 -1
  136. package/cli/storybook-dist/assets/RBDialog.stories-BAktz-ku.js +38 -0
  137. package/cli/storybook-dist/assets/{RBDivider.stories-NOTo-jJG.js → RBDivider.stories-BAK-iZ1Z.js} +1 -1
  138. package/cli/storybook-dist/assets/{RBDrawer.stories-CUZaQyw8.js → RBDrawer.stories-B2nqf2Nh.js} +12 -12
  139. package/cli/storybook-dist/assets/RBDropdownMenu.stories-DbW2fWAj.js +86 -0
  140. package/cli/storybook-dist/assets/{RBEmptyView.stories-BIRNPjwt.js → RBEmptyView.stories-nCenrgBh.js} +1 -1
  141. package/cli/storybook-dist/assets/{RBIconButton.stories-CJWeIQTt.js → RBIconButton.stories-DRH7O3sp.js} +1 -1
  142. package/cli/storybook-dist/assets/{RBIcons.stories-Cu0txCUp.js → RBIcons.stories-D3PyJXG8.js} +1 -1
  143. package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-DnGhYrxa.js → RBInlineLabelButton.stories-modQbt10.js} +1 -1
  144. package/cli/storybook-dist/assets/{RBJoyStick.stories-BlfvKcGb.js → RBJoyStick.stories-BY14qVgR.js} +1 -1
  145. package/cli/storybook-dist/assets/{RBLabelButton.stories-CFwmlhAE.js → RBLabelButton.stories-DzF7vGwm.js} +1 -1
  146. package/cli/storybook-dist/assets/{RBLight.stories-wMrFLJ7u.js → RBLight.stories-lF4FRmVh.js} +1 -1
  147. package/cli/storybook-dist/assets/{RBMenubar.stories-Bu5wtV-T.js → RBMenubar.stories-XhrI7bsV.js} +1 -1
  148. package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-Db1d940z.js → RBMultiRangeInput.stories-DprllQDl.js} +1 -1
  149. package/cli/storybook-dist/assets/{RBPagination.stories-NCpjg-2W.js → RBPagination.stories-2v2gBJfC.js} +1 -1
  150. package/cli/storybook-dist/assets/{RBPopover.stories-CqsM8byU.js → RBPopover.stories-C8iLIZpv.js} +1 -1
  151. package/cli/storybook-dist/assets/{RBPrompt.stories-TMQEaHk-.js → RBPrompt.stories-Th_HrWHI.js} +1 -1
  152. package/cli/storybook-dist/assets/{RBRadioBox.stories-pnsBvtzU.js → RBRadioBox.stories-CoHuLaN3.js} +1 -1
  153. package/cli/storybook-dist/assets/{RBRadioButton.stories-BMDBfq82.js → RBRadioButton.stories-DULxE_ff.js} +1 -1
  154. package/cli/storybook-dist/assets/{RBRangeDateInput.stories-ljAsdz0Q.js → RBRangeDateInput.stories-D8A9KBSS.js} +1 -1
  155. package/cli/storybook-dist/assets/{RBResizable.stories-B5YDW687.js → RBResizable.stories-C41QfrQ3.js} +14 -14
  156. package/cli/storybook-dist/assets/{RBSelectBox.stories-DPtyIwEr.js → RBSelectBox.stories-C1gNmwzw.js} +1 -1
  157. package/cli/storybook-dist/assets/RBSheet.stories-_6VH-RUm.js +39 -0
  158. package/cli/storybook-dist/assets/{RBSideNumberInput.stories-DLsS3mzL.js → RBSideNumberInput.stories-9LlgQ8nh.js} +3 -3
  159. package/cli/storybook-dist/assets/{RBSlider.stories-BjTYZ3ri.js → RBSlider.stories-oUgo5K9d.js} +3 -3
  160. package/cli/storybook-dist/assets/{RBSpinner.stories-BWBvOGUY.js → RBSpinner.stories-DDpohbY2.js} +1 -1
  161. package/cli/storybook-dist/assets/{RBSwitch.stories-tpthe98k.js → RBSwitch.stories-gW-GN7QO.js} +7 -7
  162. package/cli/storybook-dist/assets/{RBTab.stories-BijmaLIo.js → RBTab.stories-CN-PV3HH.js} +1 -1
  163. package/cli/storybook-dist/assets/{RBTable.stories-Dojsz28q.js → RBTable.stories-BAZLpxtM.js} +29 -29
  164. package/cli/storybook-dist/assets/{RBTableRow.stories-CQajMiEa.js → RBTableRow.stories-CwjVrszB.js} +1 -1
  165. package/cli/storybook-dist/assets/{RBTabs.stories-BslRXoCw.js → RBTabs.stories-JuEo2sUL.js} +7 -7
  166. package/cli/storybook-dist/assets/{RBTextArea.stories-N44Q45Xp.js → RBTextArea.stories-D0_Pv4qZ.js} +3 -3
  167. package/cli/storybook-dist/assets/{RBTextField.stories-scpiy0Kx.js → RBTextField.stories-CCsOvZqV.js} +15 -15
  168. package/cli/storybook-dist/assets/{RBTextInput.stories-D_suHJaA.js → RBTextInput.stories-Cccn3VNm.js} +3 -3
  169. package/cli/storybook-dist/assets/{RBToast.stories-B73JQcKo.js → RBToast.stories-I5617t0K.js} +1 -1
  170. package/cli/storybook-dist/assets/{RBToggle.stories-CmujM833.js → RBToggle.stories-DsgKjGsJ.js} +4 -4
  171. package/cli/storybook-dist/assets/{RBToggleGroup.stories-DhCAfiKu.js → RBToggleGroup.stories-BZEzSDuz.js} +5 -5
  172. package/cli/storybook-dist/assets/{RBTooltip.stories-BixEjKZy.js → RBTooltip.stories-4oSyAWsA.js} +1 -1
  173. package/cli/storybook-dist/assets/{SideNumberInput-B7fbx2jk.js → SideNumberInput-DSKbd0bG.js} +743 -743
  174. package/cli/storybook-dist/assets/{entry-preview-BeR5Td7C.js → entry-preview-B35XREGs.js} +1 -1
  175. package/cli/storybook-dist/assets/{iframe-D3pKf_6w.js → iframe-BB797lmL.js} +2 -2
  176. package/cli/storybook-dist/assets/{index-gXlU0WyC.js → index-BU2567EP.js} +3 -3
  177. package/cli/storybook-dist/assets/{preview-GwaHbm9r.js → preview-C7DaSWyc.js} +2 -2
  178. package/cli/storybook-dist/assets/preview-CbuRHit4.js +3 -0
  179. package/cli/storybook-dist/assets/preview-CigNHo-I.css +1 -0
  180. package/cli/storybook-dist/assets/react-resizable-panels-Dp9zi-o3.css +1 -0
  181. package/cli/storybook-dist/assets/{react-resizable-panels.browser-D49JKwKo.js → react-resizable-panels.browser-ChIvlwo7.js} +8 -8
  182. package/cli/storybook-dist/iframe.html +2 -2
  183. package/cli/storybook-dist/index.html +1 -1
  184. package/cli/storybook-dist/project.json +1 -1
  185. package/cli/templates/common/.mcp.json +8 -8
  186. package/cli/templates/common/README.md +1231 -1226
  187. package/cli/templates/common/assets/icon.svg +45 -45
  188. package/cli/templates/common/manifest.json +26 -26
  189. package/cli/templates/common/package.json +22 -22
  190. package/cli/templates/common/py/main.py +125 -125
  191. package/cli/templates/common/requirements.txt +4 -4
  192. package/cli/templates/common/tsconfig.json +13 -13
  193. package/cli/templates/common/ui/content.tsx +216 -216
  194. package/cli/templates/common/ui/style.css +6 -6
  195. package/cli/templates/common/vite.config.ts +10 -10
  196. package/cli/templates/cpp/cpp/main.cpp +73 -73
  197. package/cli/templates/readme-cpp/README.md +6 -1
  198. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +25 -25
  199. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/package.xml +10 -10
  200. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/setup.py +18 -18
  201. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -11
  202. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +23 -23
  203. package/cli/vite.d.mts +25 -25
  204. package/cli/vite.mjs +292 -292
  205. package/package.json +4 -4
  206. package/cli/src/templates-embedded.mjs +0 -4
  207. package/cli/storybook-dist/assets/RBCommand.stories-BYkWjdMG.js +0 -73
  208. package/cli/storybook-dist/assets/RBDialog.stories-DcPRAan8.js +0 -38
  209. package/cli/storybook-dist/assets/RBDropdownMenu.stories-5UC0ZA3H.js +0 -86
  210. package/cli/storybook-dist/assets/RBSheet.stories-DMTeF_hx.js +0 -39
  211. package/cli/storybook-dist/assets/preview-BYhMD55w.css +0 -1
  212. package/cli/storybook-dist/assets/preview-D-VpQB0_.js +0 -3
  213. package/cli/storybook-dist/assets/react-resizable-panels-DJMuB2U0.css +0 -1
@@ -1,112 +1,111 @@
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}`);
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
+ }
package/cli/src/ports.mjs CHANGED
@@ -1,193 +1,193 @@
1
- // Port contract helpers for plugin back ends.
2
- import { spawnSync } from "node:child_process";
3
- import { usesHostNetwork } from "./zenohDev.mjs";
4
-
5
- export const PLUGIN_PORT_MIN = 28600;
6
- export const PLUGIN_PORT_MAX = 28999;
7
- export const MEDIA_PORTS = [8554, 8889, 8189]; // MediaMTX: RTSP publish, WebRTC, WebRTC media
8
- export const INTERNAL_DEV_PORTS = new Set([10000, ...MEDIA_PORTS]);
9
-
10
- export function mediaPortFlags() {
11
- return [
12
- "-p", "127.0.0.1:8554:8554", "-p", "127.0.0.1:8889:8889",
13
- "-p", "127.0.0.1:8189:8189/udp", "-p", "127.0.0.1:8189:8189/tcp",
14
- ];
15
- }
16
- export const RESERVED_BACKEND_PORTS = new Map([
17
- [502, "Modbus TCP"],
18
- [4840, "OPC-UA"],
19
- [10000, "Muscat plugin gateway"],
20
- [2222, "EtherNet/IP Implicit"],
21
- [30004, "RTDE"],
22
- [34964, "ProfiNET"],
23
- [44818, "EtherNet/IP Explicit"],
24
- [50051, "gRPC"],
25
- ]);
26
- export const RESERVED_BACKEND_PORT_RANGES = [
27
- { from: 5170, to: 5179, name: "UI 517x servers" },
28
- { from: 8000, to: 8999, name: "Muscat/internal 8000 range" },
29
- { from: 50000, to: 50005, name: "robot TCP/UDP ASCII and REST interfaces" },
30
- ];
31
-
32
- function reservedPortReason(port) {
33
- if (RESERVED_BACKEND_PORTS.has(port)) return RESERVED_BACKEND_PORTS.get(port);
34
- const range = RESERVED_BACKEND_PORT_RANGES.find((item) => port >= item.from && port <= item.to);
35
- if (!range) return null;
36
- return `${range.name} (${range.from}-${range.to})`;
37
- }
38
-
39
- // function isPluginManagedPort(port) {
40
- // return reservedPortReason(port) || (port >= PLUGIN_PORT_MIN && port <= PLUGIN_PORT_MAX);
41
- // }
42
-
43
- function isInt(value) {
44
- return Number.isInteger(value);
45
- }
46
-
47
- function validatePort(port, field, errors) {
48
- if (!isInt(port)) {
49
- errors.push(`${field}: must be a port number`);
50
- return;
51
- }
52
- if (port < 1 || port > 65535) {
53
- errors.push(`${field}: port must be between 1 and 65535`);
54
- return;
55
- }
56
- const reserved = reservedPortReason(port);
57
- if (reserved) {
58
- errors.push(`${field}: ${port} is reserved for ${reserved}`);
59
- return;
60
- }
61
- if (port < PLUGIN_PORT_MIN || port > PLUGIN_PORT_MAX) {
62
- errors.push(
63
- `${field}: external plugin backend ports must be within ${PLUGIN_PORT_MIN}-${PLUGIN_PORT_MAX}`,
64
- );
65
- }
66
- }
67
-
68
- export function normalizePorts(ports, errors = []) {
69
- if (ports === undefined || ports === null) return [];
70
- if (!Array.isArray(ports)) {
71
- errors.push(`"ports" must be an array`);
72
- return [];
73
- }
74
- if (ports.length === 0) {
75
- errors.push(`"ports" must not be empty when provided`);
76
- return [];
77
- }
78
-
79
- const normalized = [];
80
- const seen = new Set();
81
- ports.forEach((port, i) => {
82
- validatePort(port, `ports[${i}]`, errors);
83
- if (!isInt(port)) return;
84
- if (seen.has(port)) {
85
- errors.push(`"ports" entries must be unique (duplicate: ${port})`);
86
- return;
87
- }
88
- seen.add(port);
89
- normalized.push(port);
90
- });
91
-
92
- return normalized;
93
- }
94
-
95
- export function validatePorts(ports, errors) {
96
- normalizePorts(ports, errors);
97
- }
98
-
99
- export function manifestPortRanges(manifest) {
100
- return normalizePorts(manifest?.ports, []);
101
- }
102
-
103
- export function formatPortRange(range) {
104
- return String(range);
105
- }
106
-
107
- export function formatPortRanges(ranges) {
108
- return ranges.map(formatPortRange).join(", ");
109
- }
110
-
111
- export function portInRanges(port, ranges) {
112
- return ranges.includes(port);
113
- }
114
-
115
- export function portEnvFlags(ranges) {
116
- if (ranges.length === 0) return [];
117
- return ["-e", `RB_PLUGIN_PORTS=${ranges.map(formatPortRange).join(",")}`];
118
- }
119
-
120
- /**
121
- * `docker run -p` flags for the plugin's declared ports, so macOS/Windows can
122
- * reach the backend's listeners (native Linux uses --network host and already
123
- * has them). Only valid on the container that owns the netns.
124
- */
125
- export function publishPortFlags(ranges = []) {
126
- return ranges.flatMap((port) => ["-p", `127.0.0.1:${port}:${port}`]);
127
- }
128
-
129
- // Skips OS-assigned loopback listeners (127.0.0.1:0, e.g. the SDK's Zenoh session).
130
- function listeningTcpPorts(container) {
131
- const script = `
132
- read -r eph_lo eph_hi < /proc/sys/net/ipv4/ip_local_port_range 2>/dev/null || { eph_lo=32768; eph_hi=60999; }
133
- for f in /proc/net/tcp /proc/net/tcp6; do
134
- [ -r "$f" ] || continue
135
- tail -n +2 "$f" | while read -r sl local_address rem_address st rest; do
136
- [ "$st" = "0A" ] || continue
137
- hex_ip="\${local_address%%:*}"
138
- port=$((0x\${local_address##*:}))
139
- case "$hex_ip" in
140
- 0100007F|00000000000000000000000001000000)
141
- [ "$port" -ge "$eph_lo" ] && [ "$port" -le "$eph_hi" ] && continue ;;
142
- esac
143
- echo "$port"
144
- done
145
- done | sort -n -u
146
- `;
147
- const r = spawnSync("docker", ["exec", container, "bash", "-lc", script], {
148
- encoding: "utf8",
149
- });
150
- if (r.status !== 0) return [];
151
- return String(r.stdout || "")
152
- .split(/\s+/)
153
- .filter(Boolean)
154
- .map((v) => Number(v))
155
- .filter((v) => Number.isInteger(v));
156
- }
157
-
158
- export function startPortContractMonitor({ container, ranges, log, intervalMs = 1000, allow = [] }) {
159
- // Under --network host (or a local Muscat container's netns) /proc/net/tcp shows every
160
- // host/robot service port, not just the plugin's — the contract can't be enforced.
161
- if (usesHostNetwork() || process.env.MUSCAT_PK_MUSCAT_CONTAINER) return () => false;
162
- const declared = ranges ?? [];
163
- // muscat-pk's own dev plumbing shares this netns — not the plugin's doing.
164
- const ignored = new Set([...INTERNAL_DEV_PORTS, ...allow.map(Number).filter(Number.isInteger)]);
165
- let reported = false;
166
- const timer = setInterval(() => {
167
- // Every listening port that isn't declared is a violation — a port outside
168
- // 28600-28999 can't be declared at all, so it must not be opened either.
169
- // Otherwise it silently escapes both this check and rby2's install-time
170
- // conflict check, and only collides once it's on a robot.
171
- const unexpected = listeningTcpPorts(container)
172
- .filter((port) => !ignored.has(port))
173
- .filter((port) => !portInRanges(port, declared));
174
- if (unexpected.length === 0) return;
175
-
176
- clearInterval(timer);
177
- reported = true;
178
- const declaredText = declared.length > 0 ? formatPortRanges(declared) : "(none)";
179
- log.error(
180
- `backend opened undeclared TCP port(s): ${unexpected.join(", ")}. ` +
181
- `Every listening port must be in manifest.json "ports" within ${PLUGIN_PORT_MIN}-${PLUGIN_PORT_MAX}. ` +
182
- `Declared: ${declaredText}`,
183
- );
184
- spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
185
- process.exit(1);
186
- }, intervalMs);
187
- timer.unref?.();
188
-
189
- return () => {
190
- clearInterval(timer);
191
- return reported;
192
- };
193
- }
1
+ // Port contract helpers for plugin back ends.
2
+ import { spawnSync } from "node:child_process";
3
+ import { usesHostNetwork } from "./zenohDev.mjs";
4
+
5
+ export const PLUGIN_PORT_MIN = 28600;
6
+ export const PLUGIN_PORT_MAX = 28999;
7
+ export const MEDIA_PORTS = [8554, 8889, 8189]; // MediaMTX: RTSP publish, WebRTC, WebRTC media
8
+ export const INTERNAL_DEV_PORTS = new Set([10000, ...MEDIA_PORTS]);
9
+
10
+ export function mediaPortFlags() {
11
+ return [
12
+ "-p", "127.0.0.1:8554:8554", "-p", "127.0.0.1:8889:8889",
13
+ "-p", "127.0.0.1:8189:8189/udp", "-p", "127.0.0.1:8189:8189/tcp",
14
+ ];
15
+ }
16
+ export const RESERVED_BACKEND_PORTS = new Map([
17
+ [502, "Modbus TCP"],
18
+ [4840, "OPC-UA"],
19
+ [10000, "Muscat plugin gateway"],
20
+ [2222, "EtherNet/IP Implicit"],
21
+ [30004, "RTDE"],
22
+ [34964, "ProfiNET"],
23
+ [44818, "EtherNet/IP Explicit"],
24
+ [50051, "gRPC"],
25
+ ]);
26
+ export const RESERVED_BACKEND_PORT_RANGES = [
27
+ { from: 5170, to: 5179, name: "UI 517x servers" },
28
+ { from: 8000, to: 8999, name: "Muscat/internal 8000 range" },
29
+ { from: 50000, to: 50005, name: "robot TCP/UDP ASCII and REST interfaces" },
30
+ ];
31
+
32
+ function reservedPortReason(port) {
33
+ if (RESERVED_BACKEND_PORTS.has(port)) return RESERVED_BACKEND_PORTS.get(port);
34
+ const range = RESERVED_BACKEND_PORT_RANGES.find((item) => port >= item.from && port <= item.to);
35
+ if (!range) return null;
36
+ return `${range.name} (${range.from}-${range.to})`;
37
+ }
38
+
39
+ // function isPluginManagedPort(port) {
40
+ // return reservedPortReason(port) || (port >= PLUGIN_PORT_MIN && port <= PLUGIN_PORT_MAX);
41
+ // }
42
+
43
+ function isInt(value) {
44
+ return Number.isInteger(value);
45
+ }
46
+
47
+ function validatePort(port, field, errors) {
48
+ if (!isInt(port)) {
49
+ errors.push(`${field}: must be a port number`);
50
+ return;
51
+ }
52
+ if (port < 1 || port > 65535) {
53
+ errors.push(`${field}: port must be between 1 and 65535`);
54
+ return;
55
+ }
56
+ const reserved = reservedPortReason(port);
57
+ if (reserved) {
58
+ errors.push(`${field}: ${port} is reserved for ${reserved}`);
59
+ return;
60
+ }
61
+ if (port < PLUGIN_PORT_MIN || port > PLUGIN_PORT_MAX) {
62
+ errors.push(
63
+ `${field}: external plugin backend ports must be within ${PLUGIN_PORT_MIN}-${PLUGIN_PORT_MAX}`,
64
+ );
65
+ }
66
+ }
67
+
68
+ export function normalizePorts(ports, errors = []) {
69
+ if (ports === undefined || ports === null) return [];
70
+ if (!Array.isArray(ports)) {
71
+ errors.push(`"ports" must be an array`);
72
+ return [];
73
+ }
74
+ if (ports.length === 0) {
75
+ errors.push(`"ports" must not be empty when provided`);
76
+ return [];
77
+ }
78
+
79
+ const normalized = [];
80
+ const seen = new Set();
81
+ ports.forEach((port, i) => {
82
+ validatePort(port, `ports[${i}]`, errors);
83
+ if (!isInt(port)) return;
84
+ if (seen.has(port)) {
85
+ errors.push(`"ports" entries must be unique (duplicate: ${port})`);
86
+ return;
87
+ }
88
+ seen.add(port);
89
+ normalized.push(port);
90
+ });
91
+
92
+ return normalized;
93
+ }
94
+
95
+ export function validatePorts(ports, errors) {
96
+ normalizePorts(ports, errors);
97
+ }
98
+
99
+ export function manifestPortRanges(manifest) {
100
+ return normalizePorts(manifest?.ports, []);
101
+ }
102
+
103
+ export function formatPortRange(range) {
104
+ return String(range);
105
+ }
106
+
107
+ export function formatPortRanges(ranges) {
108
+ return ranges.map(formatPortRange).join(", ");
109
+ }
110
+
111
+ export function portInRanges(port, ranges) {
112
+ return ranges.includes(port);
113
+ }
114
+
115
+ export function portEnvFlags(ranges) {
116
+ if (ranges.length === 0) return [];
117
+ return ["-e", `RB_PLUGIN_PORTS=${ranges.map(formatPortRange).join(",")}`];
118
+ }
119
+
120
+ /**
121
+ * `docker run -p` flags for the plugin's declared ports, so macOS/Windows can
122
+ * reach the backend's listeners (native Linux uses --network host and already
123
+ * has them). Only valid on the container that owns the netns.
124
+ */
125
+ export function publishPortFlags(ranges = []) {
126
+ return ranges.flatMap((port) => ["-p", `127.0.0.1:${port}:${port}`]);
127
+ }
128
+
129
+ // Skips OS-assigned loopback listeners (127.0.0.1:0, e.g. the SDK's Zenoh session).
130
+ function listeningTcpPorts(container) {
131
+ const script = `
132
+ read -r eph_lo eph_hi < /proc/sys/net/ipv4/ip_local_port_range 2>/dev/null || { eph_lo=32768; eph_hi=60999; }
133
+ for f in /proc/net/tcp /proc/net/tcp6; do
134
+ [ -r "$f" ] || continue
135
+ tail -n +2 "$f" | while read -r sl local_address rem_address st rest; do
136
+ [ "$st" = "0A" ] || continue
137
+ hex_ip="\${local_address%%:*}"
138
+ port=$((0x\${local_address##*:}))
139
+ case "$hex_ip" in
140
+ 0100007F|00000000000000000000000001000000)
141
+ [ "$port" -ge "$eph_lo" ] && [ "$port" -le "$eph_hi" ] && continue ;;
142
+ esac
143
+ echo "$port"
144
+ done
145
+ done | sort -n -u
146
+ `;
147
+ const r = spawnSync("docker", ["exec", container, "bash", "-lc", script], {
148
+ encoding: "utf8",
149
+ });
150
+ if (r.status !== 0) return [];
151
+ return String(r.stdout || "")
152
+ .split(/\s+/)
153
+ .filter(Boolean)
154
+ .map((v) => Number(v))
155
+ .filter((v) => Number.isInteger(v));
156
+ }
157
+
158
+ export function startPortContractMonitor({ container, ranges, log, intervalMs = 1000, allow = [] }) {
159
+ // Under --network host (or a local Muscat container's netns) /proc/net/tcp shows every
160
+ // host/robot service port, not just the plugin's — the contract can't be enforced.
161
+ if (usesHostNetwork() || process.env.MUSCAT_PK_MUSCAT_CONTAINER) return () => false;
162
+ const declared = ranges ?? [];
163
+ // muscat-pk's own dev plumbing shares this netns — not the plugin's doing.
164
+ const ignored = new Set([...INTERNAL_DEV_PORTS, ...allow.map(Number).filter(Number.isInteger)]);
165
+ let reported = false;
166
+ const timer = setInterval(() => {
167
+ // Every listening port that isn't declared is a violation — a port outside
168
+ // 28600-28999 can't be declared at all, so it must not be opened either.
169
+ // Otherwise it silently escapes both this check and rby2's install-time
170
+ // conflict check, and only collides once it's on a robot.
171
+ const unexpected = listeningTcpPorts(container)
172
+ .filter((port) => !ignored.has(port))
173
+ .filter((port) => !portInRanges(port, declared));
174
+ if (unexpected.length === 0) return;
175
+
176
+ clearInterval(timer);
177
+ reported = true;
178
+ const declaredText = declared.length > 0 ? formatPortRanges(declared) : "(none)";
179
+ log.error(
180
+ `backend opened undeclared TCP port(s): ${unexpected.join(", ")}. ` +
181
+ `Every listening port must be in manifest.json "ports" within ${PLUGIN_PORT_MIN}-${PLUGIN_PORT_MAX}. ` +
182
+ `Declared: ${declaredText}`,
183
+ );
184
+ spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
185
+ process.exit(1);
186
+ }, intervalMs);
187
+ timer.unref?.();
188
+
189
+ return () => {
190
+ clearInterval(timer);
191
+ return reported;
192
+ };
193
+ }