@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
@@ -1,79 +1,427 @@
1
1
  // muscat-pk dev:back — run the plugin back end during development.
2
- // python : create/refresh .venv, then run main.py with watchfiles hot-reload
3
- // (uvicorn's reloader). Watches ALL .py files, not just main.py.
4
- // ros2 : keep a Jazzy + Python 3.12 container warm, colcon build once with
5
- // --symlink-install, run the node, and restart on source changes —
6
- // i.e. develop as if inside the container.
2
+ // Every backend runs in a warm Docker container that matches the robot
3
+ // (Ubuntu 24.04), so dev behaves like production.
4
+ // python : Python 3.12 container, pip install, run main.py with watchfiles
5
+ // hot-reload. Shares the bridge container's netns (localhost:10000).
6
+ // cpp : Ubuntu + cmake container, build cpp/, run the binary, rebuild +
7
+ // restart on source changes.
8
+ // ros2 : Jazzy + Python 3.12 container, colcon build, run the node, restart
9
+ // on source changes (rclcpp rebuilds on change).
7
10
  import { spawn, spawnSync } from "node:child_process";
8
- import { existsSync, readFileSync, watch } from "node:fs";
11
+ import { existsSync, watch } from "node:fs";
9
12
  import { resolve } from "node:path";
10
- import { detectPythonCmd, hasBin, findPluginRoot, log, venvPython } from "./util.mjs";
13
+ import { ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection } from "./util.mjs";
14
+ import { ensureRemoteZenohEnv } from "./zenohDev.mjs";
15
+ import { assertManifestPorts, manifestRuntime, readManifest, validateManifest } from "./manifest.mjs";
16
+ import {
17
+ formatPortRanges,
18
+ portEnvFlags,
19
+ startPortContractMonitor,
20
+ } from "./ports.mjs";
21
+ import {
22
+ PYTHON_DEV_IMAGE,
23
+ assertDockerReady as pyAssertDockerReady,
24
+ ensureImage as pyEnsureImage,
25
+ } from "./python.mjs";
11
26
  import {
12
27
  ROS2_DEV_IMAGE,
13
28
  assertDockerReady,
14
29
  ensureImage,
15
30
  ros2PackageName,
16
31
  } from "./ros2.mjs";
32
+ import {
33
+ CPP_DEV_IMAGE,
34
+ assertDockerReady as cppAssertDockerReady,
35
+ ensureImage as cppEnsureImage,
36
+ } from "./cpp.mjs";
37
+
38
+ const MANAGED_DEV_ENV = "MUSCAT_PK_MANAGED_DEV";
17
39
 
18
- function manifestRuntime(root) {
19
- try {
20
- const m = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8"));
21
- return m.runtime?.type ?? "python";
22
- } catch {
23
- return "python";
40
+ function exitAfterCleanup(code = 0) {
41
+ if (process.env[MANAGED_DEV_ENV] !== "1") process.exit(code ?? 0);
42
+ }
43
+
44
+ export function stopDevBackContainers() {
45
+ for (const container of [
46
+ `${PYTHON_DEV_IMAGE}-run`,
47
+ `${ROS2_DEV_IMAGE}-run`,
48
+ `${CPP_DEV_IMAGE}-run`,
49
+ ]) {
50
+ spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
24
51
  }
25
52
  }
26
53
 
27
- // ── python ────────────────────────────────────────────────────────────────────
28
- function runPython(root) {
29
- const venv = resolve(root, ".venv");
30
- const py = venvPython(venv);
31
-
32
- if (!existsSync(py)) {
33
- log.step("creating .venv…");
34
- if (hasBin("uv")) {
35
- spawnSync("uv", ["venv", venv, "--python", "3.12", "--seed"], { cwd: root, stdio: "inherit" });
36
- } else if (hasBin("conda")) {
37
- spawnSync("conda", ["create", "-p", venv, "python=3.12", "-y", "--quiet"], { cwd: root, stdio: "inherit" });
38
- } else if (hasBin("python3.12") || hasBin("python3") || hasBin("python")) {
39
- const { cmd, is312 } = detectPythonCmd();
40
- if (!is312) log.warn("python3.12 not found; using fallback (robot targets 3.12).");
41
- spawnSync(cmd[0], [...cmd.slice(1), "-m", "venv", venv], { cwd: root, stdio: "inherit" });
42
- } else {
43
- throw new Error("Python not found (uv / conda / python3 none available). Install Python 3.12 to run the backend.");
54
+ // ── IDE tooling venv (best-effort, non-blocking) ────────────────────────────
55
+ // `muscat-pk new` already creates this up front (see new.mjs); this just
56
+ // refreshes it on every `dev`/`dev:back` in case requirements.txt changed.
57
+ // Docker remains the actual execution environment — this never affects it.
58
+ // Runs in the background so it never delays `dev:back` starting, and any
59
+ // failure here (no local Python toolchain, offline, ...) is silent.
60
+ function syncIdeVenv(root) {
61
+ setImmediate(() => {
62
+ try {
63
+ if (ensureIdeVenv(root)) {
64
+ log.info("IDE venv ready → .venv (point your editor's Python interpreter there for pylint/pyright)");
65
+ }
66
+ } catch {
67
+ /* best-effort only */
44
68
  }
45
- }
69
+ });
70
+ }
46
71
 
47
- const reqs = resolve(root, "requirements.txt");
48
- if (existsSync(reqs)) {
49
- log.step("pip install -r requirements.txt…");
50
- spawnSync(py, ["-m", "pip", "install", "-q", "-r", reqs], { cwd: root, stdio: "inherit" });
51
- }
72
+ // ── python ────────────────────────────────────────────────────────────────────
73
+ // Runs in a warm Ubuntu 24.04 + Python 3.12 container (matches the robot).
74
+ // Under `muscat-pk dev`, it shares the bridge container's netns, so the SDK
75
+ // reaches the Socket.IO bridge at the default localhost:10000.
76
+ async function runPython(root, portRanges) {
77
+ // Independent of Docker readiness — must still happen if Docker isn't
78
+ // installed/running, since that's exactly when local IDE tooling matters most.
79
+ syncIdeVenv(root);
80
+ pyAssertDockerReady();
81
+ await pyEnsureImage();
82
+
83
+ const manifest = readManifest(root);
84
+ const pluginId = manifest.id ?? "";
85
+ const container = `${PYTHON_DEV_IMAGE}-run`;
86
+ const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
87
+ const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
88
+ const zenohAdvertiseHost = process.env.MUSCAT_ZENOH_ADVERTISE_HOST ?? "";
89
+ const zenohPort = process.env.MUSCAT_ZENOH_PORT ?? "";
90
+ // See host_agent.py: a Docker Desktop container can't join the robot's
91
+ // Zenoh mesh, so it speaks a byte protocol to the host-side proxy instead.
92
+ const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT ?? "";
93
+ // Share the bridge container's network namespace so localhost:10000 is the
94
+ // bridge (set by `muscat-pk dev`). Standalone dev:back has no bridge to join.
95
+ const bridgeContainer = process.env.MUSCAT_PK_BRIDGE_CONTAINER;
96
+ const netFlags = bridgeContainer ? ["--network", `container:${bridgeContainer}`] : [];
97
+ // Only the container's OWN reverse-dial path needs to publish a listen port.
98
+ // With the byte-proxy, the container just connects out to host.docker.internal
99
+ // — nothing to publish.
100
+ const standaloneZenohPortFlags =
101
+ !bridgeContainer && !proxyEndpoint && zenohPort ? ["-p", `${zenohPort}:${zenohPort}`] : [];
102
+ const hostGatewayFlags = bridgeContainer
103
+ ? []
104
+ : ["--add-host", "host.docker.internal:host-gateway"];
105
+ // Without the bridge's netns, 127.0.0.1 (the SDK's gateway default) is the
106
+ // container itself, not the host — host.docker.internal reaches the host.
107
+ const gatewayFlags = bridgeContainer
108
+ ? []
109
+ : ["-e", "RB_PLUGIN_GATEWAY_URL=http://host.docker.internal:10000"];
110
+ const envFlags = [
111
+ // docker exec below has no -t (no pseudo-TTY), so Python defaults to
112
+ // block-buffered stdout — print() output wouldn't show up until the
113
+ // buffer fills or the process exits. Force unbuffered.
114
+ "-e", "PYTHONUNBUFFERED=1",
115
+ ...gatewayFlags,
116
+ ...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
117
+ ...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
118
+ ...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
119
+ // Byte-proxy mode: the container has no Zenoh session of its own — the
120
+ // entry script's shim speaks the raw query/put/subscribe protocol to the
121
+ // host_agent process at this address instead.
122
+ ...(proxyEndpoint ? ["-e", `MUSCAT_PROXY_ENDPOINT=${proxyEndpoint}`] : []),
123
+ // Fallback (no proxy): the container reverse-dials the robot itself.
124
+ ...(!proxyEndpoint && zenohAdvertiseHost ? ["-e", `MUSCAT_ZENOH_ADVERTISE_HOST=${zenohAdvertiseHost}`] : []),
125
+ ...(!proxyEndpoint && zenohPort ? ["-e", `MUSCAT_ZENOH_PORT=${zenohPort}`] : []),
126
+ ...portEnvFlags(portRanges),
127
+ ];
128
+ const docker = (args, opts = {}) =>
129
+ spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
130
+
131
+ // start a warm, long-lived container with the plugin mounted
132
+ docker(["rm", "-f", container], { stdio: "ignore" });
133
+ log.step("starting Python dev container…");
134
+ const up = docker(
135
+ ["run", "-d", "--name", container, ...netFlags, ...standaloneZenohPortFlags,
136
+ // host.docker.internal is automatic on Docker Desktop (macOS/Windows);
137
+ // this makes it resolve on Linux too, for the no-bridge gateway fallback above.
138
+ ...hostGatewayFlags,
139
+ "-e", "PIP_ROOT_USER_ACTION=ignore",
140
+ "-v", `${root}:/plugin`, "-w", "/plugin", PYTHON_DEV_IMAGE, "sleep", "infinity"],
141
+ { stdio: "inherit" },
142
+ );
143
+ if (up.status !== 0) throw new Error("failed to start dev container.");
144
+ const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log });
145
+
146
+ log.step("pip install requirements + watchfiles (in container)…");
147
+ docker(["exec", container, "bash", "-lc",
148
+ "pip install --break-system-packages --ignore-installed -q watchfiles" +
149
+ " && { [ -f requirements.txt ] && pip install --break-system-packages --ignore-installed -q -r requirements.txt || true; }"]);
150
+
151
+ docker(["exec", container, "bash", "-lc", `cat > /tmp/muscat-pk-python-dev-entry.py <<'PY'
152
+ import os
153
+
154
+ try:
155
+ proxy_endpoint = os.getenv("MUSCAT_PROXY_ENDPOINT")
156
+ if proxy_endpoint:
157
+ # Byte-proxy mode: this container has no Zenoh session of its own.
158
+ # ZenohClient's C++ core is replaced with a shim that speaks a raw
159
+ # query/put/subscribe byte protocol to host_agent.py instead.
160
+ import json
161
+ import socket
162
+ import struct
163
+ import threading
164
+
165
+ _proxy_host, _proxy_port_s = proxy_endpoint.rsplit(":", 1)
166
+ _proxy_port = int(_proxy_port_s)
167
+
168
+ def _send_frame(conn, lock, obj):
169
+ body = json.dumps(obj).encode()
170
+ with lock:
171
+ conn.sendall(struct.pack(">I", len(body)) + body)
172
+
173
+ def _recv_frame(conn):
174
+ hdr = b""
175
+ while len(hdr) < 4:
176
+ chunk = conn.recv(4 - len(hdr))
177
+ if not chunk:
178
+ return None
179
+ hdr += chunk
180
+ n = struct.unpack(">I", hdr)[0]
181
+ buf = b""
182
+ while len(buf) < n:
183
+ chunk = conn.recv(n - len(buf))
184
+ if not chunk:
185
+ return None
186
+ buf += chunk
187
+ return json.loads(buf.decode())
188
+
189
+ class _ProxyRawClient:
190
+ """Drop-in replacement for ZenohClient's C++ core, backed by the
191
+ host_agent byte-proxy instead of a local Zenoh session."""
192
+
193
+ sender = "RRS"
194
+ sender_id = ""
195
+
196
+ def __init__(self, host, port):
197
+ self._conn = socket.create_connection((host, port), timeout=10)
198
+ # create_connection() leaves the connect timeout applied to
199
+ # the socket permanently; the background read loop below does
200
+ # a long-lived blocking recv() for async "sample" pushes, so
201
+ # it must not time out on an idle connection.
202
+ self._conn.settimeout(None)
203
+ self._send_lock = threading.Lock()
204
+ self._req_id = 0
205
+ self._req_lock = threading.Lock()
206
+ self._pending = {}
207
+ self._subs = {} # sub_id -> callback
208
+ self._sub_by_keyexpr = {}
209
+ threading.Thread(target=self._read_loop, daemon=True).start()
210
+
211
+ def _next_id(self):
212
+ with self._req_lock:
213
+ self._req_id += 1
214
+ return self._req_id
215
+
216
+ def _read_loop(self):
217
+ while True:
218
+ frame = _recv_frame(self._conn)
219
+ if frame is None:
220
+ return
221
+ ftype = frame.get("type")
222
+ if ftype == "sample":
223
+ cb = self._sub_by_keyexpr.get(frame["keyexpr"])
224
+ if cb is not None:
225
+ try:
226
+ cb(frame["key"], bytes.fromhex(frame["payload_hex"]), frame.get("att", ""))
227
+ except Exception as exc:
228
+ print(f"[muscat-pk] proxy subscriber callback error: {exc}", flush=True)
229
+ elif "id" in frame:
230
+ ev = self._pending.pop(frame["id"], None)
231
+ if ev is not None:
232
+ ev[1] = frame
233
+ ev[0].set()
234
+
235
+ def _request(self, obj, timeout=30):
236
+ req_id = self._next_id()
237
+ obj["id"] = req_id
238
+ ev = threading.Event()
239
+ slot = [ev, None]
240
+ self._pending[req_id] = slot
241
+ _send_frame(self._conn, self._send_lock, obj)
242
+ if not ev.wait(timeout):
243
+ self._pending.pop(req_id, None)
244
+ raise TimeoutError(f"proxy request timed out: {obj.get('type')} {obj.get('keyexpr')}")
245
+ return slot[1]
246
+
247
+ def query(self, keyexpr, payload, timeout_ms, target):
248
+ resp = self._request({
249
+ "type": "query", "keyexpr": keyexpr,
250
+ "payload_hex": payload.hex() if payload else None,
251
+ "timeout_ms": timeout_ms, "target": target,
252
+ }, timeout=(timeout_ms / 1000) + 5)
253
+ if not resp.get("ok"):
254
+ raise RuntimeError(resp.get("error", "proxy query failed"))
255
+ return [
256
+ (r["key"], bytes.fromhex(r["payload_hex"]), r["att"], r["encoding"], r["is_error"])
257
+ for r in resp["replies"]
258
+ ]
259
+
260
+ def query_all(self, keyexpr, payload, timeout_ms, target):
261
+ return self.query(keyexpr, payload, timeout_ms, target)
262
+
263
+ def publish(self, topic, payload, att, congestion_control, priority, express):
264
+ resp = self._request({
265
+ "type": "put", "keyexpr": topic,
266
+ "payload_hex": bytes(payload).hex() if payload else None,
267
+ })
268
+ if not resp.get("ok"):
269
+ raise RuntimeError(resp.get("error", "proxy put failed"))
270
+
271
+ def has_subscriber(self, topic):
272
+ return topic in self._sub_by_keyexpr
273
+
274
+ def declare_subscriber(self, topic, callback, opts=None):
275
+ resp = self._request({"type": "subscribe", "keyexpr": topic})
276
+ if not resp.get("ok"):
277
+ raise RuntimeError(resp.get("error", "proxy subscribe failed"))
278
+ sub_id = self._next_id()
279
+ self._subs[sub_id] = topic
280
+ self._sub_by_keyexpr[topic] = callback
281
+ return sub_id
282
+
283
+ def undeclare_subscriber(self, sub_id):
284
+ topic = self._subs.pop(sub_id, None)
285
+ if topic is None:
286
+ return
287
+ self._sub_by_keyexpr.pop(topic, None)
288
+ self._request({"type": "unsubscribe", "keyexpr": topic})
289
+
290
+ def unsubscribe_topic(self, topic):
291
+ for sub_id, t in list(self._subs.items()):
292
+ if t == topic:
293
+ self.undeclare_subscriber(sub_id)
294
+
295
+ def unsubscribe_all(self):
296
+ for sub_id in list(self._subs):
297
+ self.undeclare_subscriber(sub_id)
298
+
299
+ def get_sub_stats(self, sub_id):
300
+ return {}
301
+
302
+ def declare_queryable(self, keyexpr, handler):
303
+ raise NotImplementedError("byte-proxy mode does not support declare_queryable")
304
+
305
+ def undeclare_queryable_by_topic(self, keyexpr):
306
+ pass
307
+
308
+ # ZenohClient is a process-wide singleton whose __init__ eagerly
309
+ # calls connect() on first construction. rainbow.rb_sdk.base
310
+ # transitively imports rainbow.rb_log.log, which constructs one at
311
+ # module level — so this patch must land before RBBaseSDK is
312
+ # imported, or that eager construction wins with a real session.
313
+ from rainbow.rb_zenoh.client import ZenohClient
314
+
315
+ def _proxy_connect(self, *, config_pairs=None, sender=None, max_attempts=8, initial_delay_s=0.5, max_delay_s=3.0):
316
+ if self._client is not None:
317
+ return
318
+ if sender is not None:
319
+ self._sender = sender
320
+ print(f"[muscat-pk] zenoh via host byte-proxy → {proxy_endpoint} (no local Zenoh session)", flush=True)
321
+ self._client = _ProxyRawClient(_proxy_host, _proxy_port)
322
+
323
+ ZenohClient.connect = _proxy_connect
324
+
325
+ from rainbow.rb_sdk.base import RBBaseSDK
52
326
 
53
- // watchfiles = uvicorn's reloader. --filter python watches every *.py in the
54
- // project (and ignores .venv/__pycache__/node_modules), so interdependent
55
- // modules trigger reloads — not just main.py.
56
- log.step("pip install -q watchfiles (dev reloader)…");
57
- spawnSync(py, ["-m", "pip", "install", "-q", "watchfiles"], { cwd: root, stdio: "inherit" });
327
+ async def _sdk_connect_via_proxy(self, *args, **kwargs):
328
+ # No HTTP registration with common — host_agent already handles
329
+ # that. Just make sure the (proxy-backed) shared ZenohClient exists.
330
+ self._ensure_zenoh_client_alive()
331
+ return None
58
332
 
59
- log.step("watchfiles → python main.py (reload on any .py change)");
333
+ async def _sdk_disconnect_via_proxy(self, *args, **kwargs):
334
+ return None
335
+
336
+ RBBaseSDK.connect = _sdk_connect_via_proxy
337
+ RBBaseSDK.disconnect = _sdk_disconnect_via_proxy
338
+ else:
339
+ # Fallback: no host byte-proxy — the container reverse-dials the robot itself.
340
+ advertise_host = os.getenv("MUSCAT_ZENOH_ADVERTISE_HOST")
341
+ zenoh_port = os.getenv("MUSCAT_ZENOH_PORT")
342
+ from rainbow.rb_sdk.base import RBBaseSDK
343
+ if advertise_host:
344
+ RBBaseSDK._detect_local_ip = staticmethod(lambda host: advertise_host)
345
+ if zenoh_port:
346
+ _orig_connect = RBBaseSDK.connect
347
+
348
+ async def _connect_with_dev_port(self, *args, **kwargs):
349
+ kwargs.setdefault("zenoh_port", int(zenoh_port))
350
+ return await _orig_connect(self, *args, **kwargs)
351
+
352
+ RBBaseSDK.connect = _connect_with_dev_port
353
+ except Exception as exc:
354
+ print(f"[muscat-pk] SDK dev network patch skipped: {exc}", flush=True)
355
+
356
+ import runpy
357
+ runpy.run_path("/plugin/py/main.py", run_name="__main__")
358
+ PY`]);
359
+
360
+ if (pluginId) log.step(`RB_PLUGIN_ID=${pluginId}`);
361
+ if (proxyEndpoint) log.step(`Zenoh via host byte-proxy → ${proxyEndpoint}`);
362
+ else if (zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
363
+ log.step("watchfiles → py/main.py (reload on any .py change in py/)");
60
364
  const child = spawn(
61
- py,
62
- ["-m", "watchfiles", "--filter", "python", "--target-type", "command", "python main.py", "."],
63
- { cwd: root, stdio: "inherit" },
365
+ "docker",
366
+ ["exec", ...envFlags, container, "bash", "-lc",
367
+ "python3 -m watchfiles --filter python --target-type command 'python3 /tmp/muscat-pk-python-dev-entry.py' py"],
368
+ { cwd: root, stdio: ["inherit", "inherit", "pipe"] },
64
369
  );
65
- child.on("exit", (code) => process.exit(code ?? 0));
370
+ // watchfiles prints a KeyboardInterrupt traceback on SIGTERM — suppress it
371
+ child.stderr.on("data", (buf) => {
372
+ const text = buf.toString();
373
+ if (!text.includes("KeyboardInterrupt") && !text.includes("raise_keyboard_interrupt")) {
374
+ process.stderr.write(buf);
375
+ }
376
+ });
377
+
378
+ let cleaned = false;
379
+ const cleanupResources = () => {
380
+ if (cleaned) return;
381
+ cleaned = true;
382
+ stopPortMonitor();
383
+ if (!child.killed && child.exitCode === null) child.kill();
384
+ spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
385
+ };
386
+ const cleanup = () => {
387
+ cleanupResources();
388
+ exitAfterCleanup(0);
389
+ };
390
+ child.on("exit", (code) => {
391
+ cleanupResources();
392
+ exitAfterCleanup(code ?? 0);
393
+ });
394
+ process.once("SIGINT", cleanup);
395
+ process.once("SIGTERM", cleanup);
66
396
  }
67
397
 
68
398
  // ── ros2 ───────────────────────────────────────────────────────────────────────
69
- function runRos2(root) {
399
+ async function runRos2(root, portRanges) {
400
+ const manifest = readManifest(root);
401
+ const isRclcpp = manifest.runtime?.rosClient === "rclcpp";
402
+ // Independent of Docker readiness — must still happen if Docker isn't
403
+ // installed/running, since that's exactly when local IDE tooling matters most.
404
+ if (!isRclcpp) syncIdeVenv(root); // rclpy is plain Python — rclcpp is C++, no venv needed
405
+
70
406
  assertDockerReady();
71
407
  const pkg = ros2PackageName(root);
72
408
  if (!pkg) throw new Error("no ROS2 package found under ros2_ws/src.");
73
409
 
74
- ensureImage();
410
+ await ensureImage();
75
411
 
412
+ const pluginId = manifest.id ?? "";
76
413
  const container = `${ROS2_DEV_IMAGE}-run`;
414
+ const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
415
+ const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
416
+ const envFlags = [
417
+ // Same rationale as runPython(): no -t on docker exec below means
418
+ // rclpy's Python process stdout is block-buffered, not line-buffered.
419
+ "-e", "PYTHONUNBUFFERED=1",
420
+ ...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
421
+ ...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
422
+ ...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
423
+ ...portEnvFlags(portRanges),
424
+ ];
77
425
  const docker = (args, opts = {}) =>
78
426
  spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
79
427
 
@@ -81,10 +429,11 @@ function runRos2(root) {
81
429
  docker(["rm", "-f", container], { stdio: "ignore" });
82
430
  log.step("starting ROS2 dev container…");
83
431
  const up = docker(
84
- ["run", "-d", "--name", container, "-v", `${root}:/plugin`, "-w", "/plugin", ROS2_DEV_IMAGE, "sleep", "infinity"],
432
+ ["run", "-d", "--name", container, "-e", "PIP_ROOT_USER_ACTION=ignore", "-v", `${root}:/plugin`, "-w", "/plugin", ROS2_DEV_IMAGE, "sleep", "infinity"],
85
433
  { stdio: "inherit" },
86
434
  );
87
435
  if (up.status !== 0) throw new Error("failed to start dev container.");
436
+ const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log });
88
437
 
89
438
  // one-time deps + symlink build (edits to .py reflect without rebuilding)
90
439
  log.step("pip install + colcon build --symlink-install (once)…");
@@ -97,21 +446,22 @@ function runRos2(root) {
97
446
  log.step(`ros2 run ${pkg} ${pkg}_node`);
98
447
  child = spawn(
99
448
  "docker",
100
- ["exec", container, "bash", "-lc",
449
+ ["exec", ...envFlags, container, "bash", "-lc",
101
450
  `cd ros2_ws && source install/setup.bash && ros2 run ${pkg} ${pkg}_node`],
102
451
  { cwd: root, stdio: "inherit" },
103
452
  );
104
453
  };
105
454
  startNode();
106
455
 
107
- // restart node on source change; rebuild only when package metadata changes
456
+ // restart node on source change. rclpy reflects .py edits live (symlink-install);
457
+ // rclcpp (C++) and package-metadata changes require a colcon rebuild.
108
458
  let timer = null;
109
459
  const onChange = (_event, filename) => {
110
460
  clearTimeout(timer);
111
461
  timer = setTimeout(() => {
112
462
  const structural = filename && /(setup\.py|package\.xml|CMakeLists\.txt)$/.test(String(filename));
113
- if (structural) {
114
- log.info("package metadata changed — colcon rebuild…");
463
+ if (structural || isRclcpp) {
464
+ log.info(structural ? "package metadata changed — colcon rebuild…" : "source changed — colcon rebuild…");
115
465
  docker(["exec", container, "bash", "-lc", "cd ros2_ws && colcon build --symlink-install"]);
116
466
  } else {
117
467
  log.info("source changed — restarting node…");
@@ -125,17 +475,115 @@ function runRos2(root) {
125
475
  }
126
476
 
127
477
  const cleanup = () => {
478
+ stopPortMonitor();
479
+ if (child) child.kill();
480
+ spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
481
+ exitAfterCleanup(0);
482
+ };
483
+ process.on("SIGINT", cleanup);
484
+ process.on("SIGTERM", cleanup);
485
+ }
486
+
487
+ // ── cpp ──────────────────────────────────────────────────────────────────────
488
+ async function runCpp(root, portRanges) {
489
+ cppAssertDockerReady();
490
+ const manifest = readManifest(root);
491
+ const entry = manifest.runtime?.entry;
492
+ if (!entry) throw new Error("runtime.entry (the built executable name) is required for cpp.");
493
+
494
+ await cppEnsureImage();
495
+
496
+ const pluginId = manifest.id ?? "";
497
+ const container = `${CPP_DEV_IMAGE}-run`;
498
+ const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
499
+ const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
500
+ const envFlags = [
501
+ ...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
502
+ ...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
503
+ ...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
504
+ ...portEnvFlags(portRanges),
505
+ ];
506
+ const docker = (args, opts = {}) =>
507
+ spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
508
+
509
+ // start a warm, long-lived container with the plugin mounted
510
+ docker(["rm", "-f", container], { stdio: "ignore" });
511
+ log.step("starting C++ dev container…");
512
+ const up = docker(
513
+ ["run", "-d", "--name", container, "-v", `${root}:/plugin`, "-w", "/plugin", CPP_DEV_IMAGE, "sleep", "infinity"],
514
+ { stdio: "inherit" },
515
+ );
516
+ if (up.status !== 0) throw new Error("failed to start dev container.");
517
+ const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log });
518
+
519
+ const build = () => {
520
+ log.step("cmake build…");
521
+ return docker(["exec", container, "bash", "-lc",
522
+ "cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=Debug && cmake --build cpp/build -j"]);
523
+ };
524
+ build();
525
+
526
+ let child = null;
527
+ const startBin = () => {
528
+ if (child) child.kill();
529
+ log.step(`run cpp/build/${entry}`);
530
+ child = spawn(
531
+ "docker",
532
+ ["exec", ...envFlags, container, "bash", "-lc", `./cpp/build/${entry}`],
533
+ { cwd: root, stdio: "inherit" },
534
+ );
535
+ };
536
+ startBin();
537
+
538
+ // rebuild + restart on C++ source changes; ignore the build/ output dir.
539
+ let timer = null;
540
+ const onChange = (_event, filename) => {
541
+ if (filename && String(filename).startsWith("build")) return;
542
+ clearTimeout(timer);
543
+ timer = setTimeout(() => {
544
+ log.info("source changed — rebuilding…");
545
+ const r = build();
546
+ if (r.status === 0) startBin();
547
+ else log.warn("build failed — keeping previous binary");
548
+ }, 300);
549
+ };
550
+ const cppDir = resolve(root, "cpp");
551
+ if (existsSync(cppDir)) watch(cppDir, { recursive: true }, onChange);
552
+
553
+ const cleanup = () => {
554
+ stopPortMonitor();
128
555
  if (child) child.kill();
129
556
  spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
130
- process.exit(0);
557
+ exitAfterCleanup(0);
131
558
  };
132
559
  process.on("SIGINT", cleanup);
133
560
  process.on("SIGTERM", cleanup);
134
561
  }
135
562
 
136
- export async function runDevBack() {
563
+ function warnManifest(root) {
564
+ const manifest = readManifest(root);
565
+ if (!manifest || Object.keys(manifest).length === 0) return;
566
+ const errors = validateManifest(manifest);
567
+ for (const e of errors) log.warn(`manifest.json: ${e}`);
568
+ }
569
+
570
+ export async function runDevBack(args = []) {
137
571
  const root = findPluginRoot();
572
+ warnManifest(root);
573
+ const portRanges = assertManifestPorts(root);
574
+ if (portRanges.length > 0 && process.env.MUSCAT_PK_PORTS_PRECHECKED !== "1")
575
+ log.step(`plugin backend ports declared: ${formatPortRanges(portRanges)}`);
576
+ ensureDocker();
577
+ const { robotIp, robotToken } = await promptRobotConnection(args);
578
+ if (robotIp) process.env.MUSCAT_ROBOT_HOST = robotIp;
579
+ if (robotToken) process.env.MUSCAT_ROBOT_TOKEN = robotToken;
580
+ // Standalone `dev:back` against a remote robot needs the Zenoh reverse-path
581
+ // advertise host/port too (idempotent — `muscat-pk dev` already set these).
582
+ // Byte-proxy mode (a host-side host_agent set MUSCAT_PROXY_ENDPOINT) makes
583
+ // the container speak the proxy protocol instead, so it needs no reverse-dial.
584
+ if (!process.env.MUSCAT_PROXY_ENDPOINT) await ensureRemoteZenohEnv(robotIp, { log });
138
585
  const type = manifestRuntime(root);
139
- if (type === "ros2") return runRos2(root);
140
- return runPython(root);
586
+ if (type === "ros2") return await runRos2(root, portRanges);
587
+ if (type === "cpp") return await runCpp(root, portRanges);
588
+ return await runPython(root, portRanges);
141
589
  }