@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.8 → 0.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 (274) hide show
  1. package/README.ko.md +1186 -0
  2. package/README.md +1137 -57
  3. package/cli/README.md +239 -7
  4. package/cli/bin/create-muscat-plugin.mjs +1 -1
  5. package/cli/bin/muscat-pk.mjs +123 -27
  6. package/cli/bundle-templates.mjs +31 -0
  7. package/cli/docker/Dockerfile.cpp +28 -0
  8. package/cli/docker/Dockerfile.dev +17 -0
  9. package/cli/docker/Dockerfile.python +21 -0
  10. package/cli/prepack.mjs +10 -0
  11. package/cli/src/__pycache__/host_agent.cpython-37.pyc +0 -0
  12. package/cli/src/ai-rules.mjs +107 -0
  13. package/cli/src/build.mjs +333 -93
  14. package/cli/src/check-package.mjs +8 -23
  15. package/cli/src/completion.mjs +168 -0
  16. package/cli/src/cpp.mjs +78 -0
  17. package/cli/src/cppIde.mjs +76 -0
  18. package/cli/src/dev.mjs +195 -6
  19. package/cli/src/devBack.mjs +671 -60
  20. package/cli/src/devId.mjs +54 -0
  21. package/cli/src/devSession.mjs +67 -0
  22. package/cli/src/devUi.mjs +214 -25
  23. package/cli/src/esbuild.mjs +27 -10
  24. package/cli/src/host_agent.py +223 -0
  25. package/cli/src/manifest.mjs +288 -0
  26. package/cli/src/mcp.mjs +1153 -0
  27. package/cli/src/mcpProxy.mjs +110 -0
  28. package/cli/src/muscatLocal.mjs +292 -0
  29. package/cli/src/new.mjs +205 -68
  30. package/cli/src/pluginDevServer.mjs +111 -0
  31. package/cli/src/ports.mjs +193 -0
  32. package/cli/src/projectCheck.mjs +102 -0
  33. package/cli/src/python.mjs +112 -0
  34. package/cli/src/ros2.mjs +62 -18
  35. package/cli/src/sdk.mjs +74 -24
  36. package/cli/src/sim.mjs +177 -0
  37. package/cli/src/storybook.mjs +150 -0
  38. package/cli/src/templates-embedded.mjs +4 -0
  39. package/cli/src/type.mjs +93 -34
  40. package/cli/src/uninstall.mjs +81 -0
  41. package/cli/src/update.mjs +364 -35
  42. package/cli/src/util.mjs +341 -11
  43. package/cli/src/zenohDev.mjs +83 -0
  44. package/cli/src/zip.mjs +121 -0
  45. package/cli/storybook-dist/_design-tokens.css +4957 -0
  46. package/cli/storybook-dist/_stories-src/NxAvatar.stories.tsx +27 -0
  47. package/cli/storybook-dist/_stories-src/NxPagination.stories.tsx +28 -0
  48. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordion.stories.tsx +46 -0
  49. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordionGroup.stories.tsx +73 -0
  50. package/cli/storybook-dist/_stories-src/RBBadge/RBBadge.stories.tsx +93 -0
  51. package/cli/storybook-dist/_stories-src/RBBattery/RBBattery.stories.tsx +74 -0
  52. package/cli/storybook-dist/_stories-src/RBButton/RBCheckIconButton.stories.tsx +28 -0
  53. package/cli/storybook-dist/_stories-src/RBButton/RBCheckLabelButton.stories.tsx +84 -0
  54. package/cli/storybook-dist/_stories-src/RBButton/RBIconButton.stories.tsx +57 -0
  55. package/cli/storybook-dist/_stories-src/RBButton/RBInlineLabelButton.stories.tsx +41 -0
  56. package/cli/storybook-dist/_stories-src/RBButton/RBLabelButton.stories.tsx +60 -0
  57. package/cli/storybook-dist/_stories-src/RBDatePicker/RBDateInput.stories.tsx +44 -0
  58. package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +56 -0
  59. package/cli/storybook-dist/_stories-src/RBDialog/RBAlert.stories.tsx +51 -0
  60. package/cli/storybook-dist/_stories-src/RBDialog/RBConfirm.stories.tsx +55 -0
  61. package/cli/storybook-dist/_stories-src/RBDialog/RBPrompt.stories.tsx +52 -0
  62. package/cli/storybook-dist/_stories-src/RBDivider/RBDivider.stories.tsx +93 -0
  63. package/cli/storybook-dist/_stories-src/RBEmptyView/RBEmptyView.stories.tsx +49 -0
  64. package/cli/storybook-dist/_stories-src/RBIcons/RBColorIcons.stories.tsx +108 -0
  65. package/cli/storybook-dist/_stories-src/RBIcons/RBIcons.stories.tsx +106 -0
  66. package/cli/storybook-dist/_stories-src/RBInput/RBCheckBox.stories.tsx +89 -0
  67. package/cli/storybook-dist/_stories-src/RBInput/RBMultiRangeInput.stories.tsx +64 -0
  68. package/cli/storybook-dist/_stories-src/RBInput/RBRadioBox.stories.tsx +65 -0
  69. package/cli/storybook-dist/_stories-src/RBInput/RBRadioButton.stories.tsx +68 -0
  70. package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +216 -0
  71. package/cli/storybook-dist/_stories-src/RBInput/RBSideNumberInput.stories.tsx +90 -0
  72. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextArea.stories.tsx +45 -0
  73. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextInput.stories.tsx +50 -0
  74. package/cli/storybook-dist/_stories-src/RBInput/RBTextField.stories.tsx +102 -0
  75. package/cli/storybook-dist/_stories-src/RBJoyStick/RBJoyStick.stories.tsx +31 -0
  76. package/cli/storybook-dist/_stories-src/RBLight/RBLight.stories.tsx +36 -0
  77. package/cli/storybook-dist/_stories-src/RBPagination/RBPagination.stories.tsx +49 -0
  78. package/cli/storybook-dist/_stories-src/RBShadcn/RBCard/RBCard.stories.tsx +89 -0
  79. package/cli/storybook-dist/_stories-src/RBShadcn/RBCommand/RBCommand.stories.tsx +116 -0
  80. package/cli/storybook-dist/_stories-src/RBShadcn/RBDialog/RBDialog.stories.tsx +74 -0
  81. package/cli/storybook-dist/_stories-src/RBShadcn/RBDrawer/RBDrawer.stories.tsx +99 -0
  82. package/cli/storybook-dist/_stories-src/RBShadcn/RBDropdownMenu/RBDropdownMenu.stories.tsx +123 -0
  83. package/cli/storybook-dist/_stories-src/RBShadcn/RBMenubar/RBMenubar.stories.tsx +135 -0
  84. package/cli/storybook-dist/_stories-src/RBShadcn/RBPopover/RBPopover.stories.tsx +69 -0
  85. package/cli/storybook-dist/_stories-src/RBShadcn/RBResizable/RBResizable.stories.tsx +49 -0
  86. package/cli/storybook-dist/_stories-src/RBShadcn/RBSheet/RBSheet.stories.tsx +91 -0
  87. package/cli/storybook-dist/_stories-src/RBShadcn/RBSlider/RBSlider.stories.tsx +38 -0
  88. package/cli/storybook-dist/_stories-src/RBShadcn/RBSwitch/RBSwitch.stories.tsx +112 -0
  89. package/cli/storybook-dist/_stories-src/RBShadcn/RBTabs/RBTabs.stories.tsx +75 -0
  90. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggle/RBToggle.stories.tsx +51 -0
  91. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggleGroup/RBToggleGroup.stories.tsx +84 -0
  92. package/cli/storybook-dist/_stories-src/RBTab/RBChipTab.stories.tsx +102 -0
  93. package/cli/storybook-dist/_stories-src/RBTab/RBChipsBoxTab.stories.tsx +78 -0
  94. package/cli/storybook-dist/_stories-src/RBTab/RBTab.stories.tsx +57 -0
  95. package/cli/storybook-dist/_stories-src/RBTable/RBTable.stories.tsx +177 -0
  96. package/cli/storybook-dist/_stories-src/RBTable/RBTableRow.stories.tsx +64 -0
  97. package/cli/storybook-dist/_stories-src/RBToast/RBToast.stories.tsx +117 -0
  98. package/cli/storybook-dist/_stories-src/RBTooltip/RBTooltip.stories.tsx +50 -0
  99. package/cli/storybook-dist/_stories-src/loading/RBSpinner.stories.tsx +30 -0
  100. package/cli/storybook-dist/_stories-src/nx-button/NxButton.stories.tsx +54 -0
  101. package/cli/storybook-dist/_stories-src/nx-button/NxPointerHoldButton.stories.tsx +57 -0
  102. package/cli/storybook-dist/_stories-src/nx-icons/NxIcons.stories.tsx +48 -0
  103. package/cli/storybook-dist/_stories-src/nx-inputs/NxNumberInput.stories.tsx +103 -0
  104. package/cli/storybook-dist/_stories-src/nx-inputs/NxRangeInput.stories.tsx +27 -0
  105. package/cli/storybook-dist/_stories-src/nx-inputs/NxSelectBox.stories.tsx +67 -0
  106. package/cli/storybook-dist/_stories-src/nx-inputs/NxSlideRange.stories.tsx +45 -0
  107. package/cli/storybook-dist/_stories-src/nx-inputs/NxSwitch.stories.tsx +27 -0
  108. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextArea.stories.tsx +48 -0
  109. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextInput.stories.tsx +46 -0
  110. package/cli/storybook-dist/assets/Color-YHDXOIA2-pCKrjUal.js +1 -0
  111. package/cli/storybook-dist/assets/Docs-B-Y7I1GA.js +7 -0
  112. package/cli/storybook-dist/assets/Docs-BFJcE_VS.js +8 -0
  113. package/cli/storybook-dist/assets/Docs-B_QaTtz8.js +8 -0
  114. package/cli/storybook-dist/assets/Docs-CryDGmfh.js +20 -0
  115. package/cli/storybook-dist/assets/Docs-Cw2AVNKx.js +7 -0
  116. package/cli/storybook-dist/assets/Docs-Q6r1fBeL.js +12 -0
  117. package/cli/storybook-dist/assets/DocsRenderer-CFRXHY34-_OmZBCn8.js +2 -0
  118. package/cli/storybook-dist/assets/NxAvatar.stories-Ctf5G2FQ.js +13 -0
  119. package/cli/storybook-dist/assets/NxButton.stories-DeLObVSj.js +20 -0
  120. package/cli/storybook-dist/assets/NxIcons.stories-DeqrVzVs.js +3 -0
  121. package/cli/storybook-dist/assets/NxNumberInput.stories-LqoY0ed1.js +26 -0
  122. package/cli/storybook-dist/assets/NxPagination.stories-DI6bXuJM.js +11 -0
  123. package/cli/storybook-dist/assets/NxPointerHoldButton.stories-3HSV1XmN.js +14 -0
  124. package/cli/storybook-dist/assets/NxRangeInput.stories-1pjgQVaG.js +17 -0
  125. package/cli/storybook-dist/assets/NxSelectBox.stories-bcQPZEuF.js +17 -0
  126. package/cli/storybook-dist/assets/NxSlideRange.stories-BosX4axk.js +14 -0
  127. package/cli/storybook-dist/assets/NxSwitch.stories-DxV6dS33.js +11 -0
  128. package/cli/storybook-dist/assets/NxTextArea.stories-CO545-LP.js +11 -0
  129. package/cli/storybook-dist/assets/NxTextInput.stories-BP0npai1.js +11 -0
  130. package/cli/storybook-dist/assets/PretendardVariable-CJuje-Rk.woff2 +0 -0
  131. package/cli/storybook-dist/assets/RBAccordion.stories-fVR4Va2a.js +17 -0
  132. package/cli/storybook-dist/assets/RBAccordionGroup.stories-Du5bsckD.js +33 -0
  133. package/cli/storybook-dist/assets/RBAlert.stories-DNkMXUu6.js +17 -0
  134. package/cli/storybook-dist/assets/RBBadge.stories-C44pXt8D.js +1 -0
  135. package/cli/storybook-dist/assets/RBBattery.stories-DE2JHvfL.js +10 -0
  136. package/cli/storybook-dist/assets/RBCard.stories-CuIda7NI.js +45 -0
  137. package/cli/storybook-dist/assets/RBCheckBox.stories-D1qM1ayr.js +1 -0
  138. package/cli/storybook-dist/assets/RBCheckIconButton.stories-e4gkliBO.js +1 -0
  139. package/cli/storybook-dist/assets/RBCheckLabelButton.stories-CDzCtsMI.js +24 -0
  140. package/cli/storybook-dist/assets/RBChipTab.stories-DBrc_3Mp.js +21 -0
  141. package/cli/storybook-dist/assets/RBChipsBoxTab.stories-hlrsHjKY.js +1 -0
  142. package/cli/storybook-dist/assets/RBColorIcons.stories-BHUvpkdu.js +3 -0
  143. package/cli/storybook-dist/assets/RBCommand.stories-azAF41NP.js +73 -0
  144. package/cli/storybook-dist/assets/RBConfirm.stories-BMNjwbmT.js +20 -0
  145. package/cli/storybook-dist/assets/RBDateInput.stories-D0VezDzv.js +15 -0
  146. package/cli/storybook-dist/assets/RBDialog.stories-AOJ975Fy.js +38 -0
  147. package/cli/storybook-dist/assets/RBDivider.stories-NSvnj43Z.js +1 -0
  148. package/cli/storybook-dist/assets/RBDrawer.stories-Du0oTvD1.js +14 -0
  149. package/cli/storybook-dist/assets/RBDropdownMenu.stories-Hsxve4PW.js +86 -0
  150. package/cli/storybook-dist/assets/RBEmptyView.stories-BlwHx3zp.js +12 -0
  151. package/cli/storybook-dist/assets/RBIconButton.stories-DAPDY63x.js +1 -0
  152. package/cli/storybook-dist/assets/RBIcons.stories-W5ZUV2L1.js +3 -0
  153. package/cli/storybook-dist/assets/RBInlineLabelButton.stories-C99j0Y3g.js +1 -0
  154. package/cli/storybook-dist/assets/RBJoyStick.stories-Du0H1iAS.js +11 -0
  155. package/cli/storybook-dist/assets/RBLabelButton.stories-BlvHaU_Z.js +1 -0
  156. package/cli/storybook-dist/assets/RBLight.stories-BY6DKrre.js +1 -0
  157. package/cli/storybook-dist/assets/RBMenubar.stories-D2PEGpM9.js +3 -0
  158. package/cli/storybook-dist/assets/RBMultiRangeInput.stories-Bm2qSmjq.js +14 -0
  159. package/cli/storybook-dist/assets/RBPagination.stories-DDleJa5N.js +17 -0
  160. package/cli/storybook-dist/assets/RBPopover.stories-27YxPSFG.js +11 -0
  161. package/cli/storybook-dist/assets/RBPrompt.stories-BFaIOctr.js +20 -0
  162. package/cli/storybook-dist/assets/RBRadioBox.stories-BroDar0t.js +1 -0
  163. package/cli/storybook-dist/assets/RBRadioButton.stories-M3O-wX7V.js +11 -0
  164. package/cli/storybook-dist/assets/RBRangeDateInput.stories-CWlM_D5X.js +15 -0
  165. package/cli/storybook-dist/assets/RBResizable.stories-ui8X1qnc.js +20 -0
  166. package/cli/storybook-dist/assets/RBSelectBox.stories-C8T6cfA0.js +74 -0
  167. package/cli/storybook-dist/assets/RBSheet.stories-BCOo_Zcc.js +39 -0
  168. package/cli/storybook-dist/assets/RBSideNumberInput.stories-CU2_MVlJ.js +5 -0
  169. package/cli/storybook-dist/assets/RBSlider.stories-DgOp1TFc.js +5 -0
  170. package/cli/storybook-dist/assets/RBSpinner.stories-qLScY3zZ.js +14 -0
  171. package/cli/storybook-dist/assets/RBSwitch.stories-SXSY1QMb.js +65 -0
  172. package/cli/storybook-dist/assets/RBTab.stories-BzXXTsXP.js +1 -0
  173. package/cli/storybook-dist/assets/RBTable.stories-DHTeRNVk.js +62 -0
  174. package/cli/storybook-dist/assets/RBTableRow.stories-DRNRsd15.js +11 -0
  175. package/cli/storybook-dist/assets/RBTabs.stories-DP0noBLp.js +21 -0
  176. package/cli/storybook-dist/assets/RBTextArea.stories-TkaejfxJ.js +5 -0
  177. package/cli/storybook-dist/assets/RBTextField.stories-B6VpCGyt.js +45 -0
  178. package/cli/storybook-dist/assets/RBTextInput.stories-D3hZ08e-.js +5 -0
  179. package/cli/storybook-dist/assets/RBToast.stories-BQ2wS2U0.js +11 -0
  180. package/cli/storybook-dist/assets/RBToggle.stories-GVOjGqo3.js +12 -0
  181. package/cli/storybook-dist/assets/RBToggleGroup.stories-pzx0btOD.js +17 -0
  182. package/cli/storybook-dist/assets/RBTooltip.stories-jufB6fa1.js +9 -0
  183. package/cli/storybook-dist/assets/SideNumberInput-BYlmNHEw.js +1109 -0
  184. package/cli/storybook-dist/assets/SideNumberInput-SAkhydsn.css +21 -0
  185. package/cli/storybook-dist/assets/browserUtils-CQM9Za7e.js +1 -0
  186. package/cli/storybook-dist/assets/chunk-XP5HYGXS-BHXC9YeQ.js +1 -0
  187. package/cli/storybook-dist/assets/client-DTf7SId2.js +1 -0
  188. package/cli/storybook-dist/assets/entry-preview-CAkU-e0D.js +2 -0
  189. package/cli/storybook-dist/assets/entry-preview-docs-b8bpVBXy.js +46 -0
  190. package/cli/storybook-dist/assets/iframe-B-BmTsxV.js +211 -0
  191. package/cli/storybook-dist/assets/index-B4FGfHer.js +1 -0
  192. package/cli/storybook-dist/assets/index-BRq5TIsn.js +24 -0
  193. package/cli/storybook-dist/assets/index-CT3U1fyk.js +1 -0
  194. package/cli/storybook-dist/assets/index-ChsGqxH_.js +9 -0
  195. package/cli/storybook-dist/assets/index-CkB0GfaI.js +575 -0
  196. package/cli/storybook-dist/assets/index-ClAc6FBW.js +240 -0
  197. package/cli/storybook-dist/assets/index-DKFBREX_.js +1 -0
  198. package/cli/storybook-dist/assets/index-DgH-xKnr.js +11 -0
  199. package/cli/storybook-dist/assets/index-Dmy2JGbm.js +1 -0
  200. package/cli/storybook-dist/assets/index-DrFu-skq.js +6 -0
  201. package/cli/storybook-dist/assets/jsx-runtime-BTJTZTIL.js +9 -0
  202. package/cli/storybook-dist/assets/preview-B8lJiyuQ.js +34 -0
  203. package/cli/storybook-dist/assets/preview-BBWR9nbA.js +1 -0
  204. package/cli/storybook-dist/assets/preview-BVJwlGUF.js +1 -0
  205. package/cli/storybook-dist/assets/preview-BWzBA1C2.js +396 -0
  206. package/cli/storybook-dist/assets/preview-CVgpLj3b.js +1 -0
  207. package/cli/storybook-dist/assets/preview-CvbIS5ZJ.js +1 -0
  208. package/cli/storybook-dist/assets/preview-CznQwwbo.js +3 -0
  209. package/cli/storybook-dist/assets/preview-DD_OYowb.js +1 -0
  210. package/cli/storybook-dist/assets/preview-DGUiP6tS.js +7 -0
  211. package/cli/storybook-dist/assets/preview-DHQbi4pV.js +1 -0
  212. package/cli/storybook-dist/assets/preview-M77JRg1A.js +2 -0
  213. package/cli/storybook-dist/assets/preview-kTZPgP6D.css +1 -0
  214. package/cli/storybook-dist/assets/react-18-hB3smqWJ.js +1 -0
  215. package/cli/storybook-dist/assets/react-resizable-panels-HsFbxPbn.css +1 -0
  216. package/cli/storybook-dist/assets/react-resizable-panels.browser-DuxpZLkF.js +120 -0
  217. package/cli/storybook-dist/assets/test-utils-BQudk2xt.js +9 -0
  218. package/cli/storybook-dist/favicon.svg +1 -0
  219. package/cli/storybook-dist/iframe.html +666 -0
  220. package/cli/storybook-dist/index.html +190 -0
  221. package/cli/storybook-dist/index.json +1 -0
  222. package/cli/storybook-dist/nunito-sans-bold-italic.woff2 +0 -0
  223. package/cli/storybook-dist/nunito-sans-bold.woff2 +0 -0
  224. package/cli/storybook-dist/nunito-sans-italic.woff2 +0 -0
  225. package/cli/storybook-dist/nunito-sans-regular.woff2 +0 -0
  226. package/cli/storybook-dist/project.json +1 -0
  227. package/cli/storybook-dist/sb-addons/chromatic-com-storybook-10/manager-bundle.js +331 -0
  228. package/cli/storybook-dist/sb-addons/chromatic-com-storybook-10/manager-bundle.js.LEGAL.txt +51 -0
  229. package/cli/storybook-dist/sb-addons/essentials-actions-3/manager-bundle.js +3 -0
  230. package/cli/storybook-dist/sb-addons/essentials-backgrounds-5/manager-bundle.js +12 -0
  231. package/cli/storybook-dist/sb-addons/essentials-controls-2/manager-bundle.js +405 -0
  232. package/cli/storybook-dist/sb-addons/essentials-docs-4/manager-bundle.js +245 -0
  233. package/cli/storybook-dist/sb-addons/essentials-measure-8/manager-bundle.js +3 -0
  234. package/cli/storybook-dist/sb-addons/essentials-outline-9/manager-bundle.js +3 -0
  235. package/cli/storybook-dist/sb-addons/essentials-toolbars-7/manager-bundle.js +3 -0
  236. package/cli/storybook-dist/sb-addons/essentials-viewport-6/manager-bundle.js +3 -0
  237. package/cli/storybook-dist/sb-addons/links-1/manager-bundle.js +3 -0
  238. package/cli/storybook-dist/sb-addons/storybook-13/manager-bundle.js +3 -0
  239. package/cli/storybook-dist/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js +3 -0
  240. package/cli/storybook-dist/sb-addons/storybook-experimental-addon-test-12/manager-bundle.js +223 -0
  241. package/cli/storybook-dist/sb-addons/themes-11/manager-bundle.js +3 -0
  242. package/cli/storybook-dist/sb-common-assets/favicon.svg +1 -0
  243. package/cli/storybook-dist/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
  244. package/cli/storybook-dist/sb-common-assets/nunito-sans-bold.woff2 +0 -0
  245. package/cli/storybook-dist/sb-common-assets/nunito-sans-italic.woff2 +0 -0
  246. package/cli/storybook-dist/sb-common-assets/nunito-sans-regular.woff2 +0 -0
  247. package/cli/storybook-dist/sb-manager/globals-module-info.js +1052 -0
  248. package/cli/storybook-dist/sb-manager/globals-runtime.js +41775 -0
  249. package/cli/storybook-dist/sb-manager/globals.js +48 -0
  250. package/cli/storybook-dist/sb-manager/runtime.js +12048 -0
  251. package/cli/templates/common/README.md +1070 -83
  252. package/cli/templates/common/assets/icon.svg +44 -2
  253. package/cli/templates/common/manifest.json +14 -13
  254. package/cli/templates/common/package.json +11 -4
  255. package/cli/templates/common/py/main.py +125 -0
  256. package/cli/templates/common/requirements.txt +4 -3
  257. package/cli/templates/common/tsconfig.json +1 -1
  258. package/cli/templates/common/ui/content.tsx +216 -0
  259. package/cli/templates/common/ui/style.css +6 -0
  260. package/cli/templates/common/vite.config.ts +6 -25
  261. package/cli/templates/cpp/cpp/CMakeLists.txt +14 -0
  262. package/cli/templates/cpp/cpp/main.cpp +73 -0
  263. package/cli/templates/readme-cpp/README.md +1012 -0
  264. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +12 -1
  265. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +22 -0
  266. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -0
  267. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +41 -0
  268. package/cli/vite.d.mts +25 -0
  269. package/cli/vite.mjs +292 -0
  270. package/package.json +34 -7
  271. package/cli/src/pack.mjs +0 -61
  272. package/cli/src/preview.mjs +0 -91
  273. package/cli/templates/common/main.py +0 -114
  274. package/cli/templates/common/src/content.tsx +0 -33
@@ -1,117 +1,588 @@
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 local Muscat 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, readFileSync, watch, writeFileSync } from "node:fs";
12
+ import { isIP } from "node:net";
9
13
  import { resolve } from "node:path";
10
- import { detectPythonCmd, hasBin, findPluginRoot, log, venvPython } from "./util.mjs";
14
+ import { dockerUserFlags, ensureDevDir, ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection, realtimeFlags } from "./util.mjs";
15
+ import { ensureRemoteZenohEnv, getFreePort, isRemoteRobot, usesHostNetwork } from "./zenohDev.mjs";
16
+ import { claimDevSession, DEV_CONTAINER, stopDevBackContainers } from "./devSession.mjs";
17
+ import { GATEWAY_PORT, localMuscatContainer, robotHasMuscat } from "./muscatLocal.mjs";
18
+ import { devPluginId } from "./devId.mjs";
19
+ import { PLUGIN_ID_RE, assertManifestPorts, assertManifestVariables, manifestRuntime, readManifest, rosNamespace, validateManifest } from "./manifest.mjs";
20
+ import {
21
+ portEnvFlags,
22
+ publishPortFlags,
23
+ startPortContractMonitor,
24
+ } from "./ports.mjs";
25
+ import {
26
+ PYTHON_DEV_IMAGE,
27
+ assertDockerReady as pyAssertDockerReady,
28
+ ensureImage as pyEnsureImage,
29
+ } from "./python.mjs";
11
30
  import {
12
31
  ROS2_DEV_IMAGE,
13
32
  assertDockerReady,
33
+ colconBuild,
14
34
  ensureImage,
15
35
  ros2PackageName,
16
36
  } from "./ros2.mjs";
37
+ import {
38
+ CPP_DEV_IMAGE,
39
+ assertDockerReady as cppAssertDockerReady,
40
+ cmakeBuild,
41
+ ensureImage as cppEnsureImage,
42
+ } from "./cpp.mjs";
43
+
44
+
45
+ export { stopDevBackContainers };
46
+
47
+ // Where the backend's 127.0.0.1 points. A local Muscat container (muscat-pk-muscat, muscat-dev):
48
+ // join its netns so the gateway and MediaMTX resolve like on the robot. Otherwise its own
49
+ // netns, gateway via the host.
50
+ // IS_DEV Muscat (muscat-dev): common is a zenoh-router client and opens no 7447 listener,
51
+ // so the plugin must join the router too or `execute` queries never reach it.
52
+ function muscatZenohEnv(container) {
53
+ const r = spawnSync("docker", ["inspect", "-f", "{{range .Config.Env}}{{println .}}{{end}}", container], { encoding: "utf8" });
54
+ return /^IS_DEV=(true|1)$/im.test(r.stdout || "")
55
+ ? ["-e", "ZENOH_PEER_CONNECT_ENDPOINTS=tcp/zenoh-router:7447"]
56
+ : [];
57
+ }
58
+
59
+ function backendNetwork(portRanges, robotGateway) {
60
+ const muscat = process.env.MUSCAT_PK_MUSCAT_CONTAINER;
61
+ // The SDK listens on 127.0.0.1 by default; simulators in their own containers need a reachable
62
+ // address to link back, or their queryables never answer the plugin.
63
+ if (muscat) return { run: ["--network", `container:${muscat}`, "--ipc", "host"], env: ["-e", "ZENOH_PEER_LISTEN_ENDPOINTS=tcp/0.0.0.0:0", ...muscatZenohEnv(muscat)], shared: true };
64
+ // The robot runs Muscat: its gateway, own netns (host network on Linux), declared ports published.
65
+ if (robotGateway) {
66
+ const run = usesHostNetwork() ? ["--network", "host"] : publishPortFlags(portRanges);
67
+ return { run, env: ["-e", `RB_PLUGIN_GATEWAY_URL=${robotGateway}`], shared: usesHostNetwork() };
68
+ }
69
+ if (usesHostNetwork()) return { run: ["--network", "host"], env: [], shared: true };
70
+ return {
71
+ // host.docker.internal is automatic on Docker Desktop; this makes it resolve on Linux too.
72
+ run: [...publishPortFlags(portRanges), "--add-host", "host.docker.internal:host-gateway"],
73
+ env: ["-e", "RB_PLUGIN_GATEWAY_URL=http://host.docker.internal:10000"],
74
+ shared: false,
75
+ };
76
+ }
77
+
78
+ // rb.rosPeers in dev: answer muscat/plugin/<id>/ros_peers/{get,set} on the gateway the way
79
+ // Muscat (common) does on the robot — keep the list in .muscat-dev/ and restart the node
80
+ // with ROS_STATIC_PEERS.
81
+ const ROS_PEERS_FILE = ".muscat-dev/ros-peers.json";
17
82
 
18
- function manifestRuntime(root) {
83
+ function readRosPeers(root) {
19
84
  try {
20
- const m = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8"));
21
- return m.runtime?.type ?? "python";
85
+ return JSON.parse(readFileSync(resolve(root, ROS_PEERS_FILE), "utf8"));
22
86
  } catch {
23
- return "python";
87
+ return [];
24
88
  }
25
89
  }
26
90
 
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.");
91
+ async function serveRosPeers(root, pluginId, restart) {
92
+ if (!pluginId) return;
93
+ const { RbWs } = await import("@rainbow-robotics/plugin-ui/ws");
94
+ const ws = await Promise.race([
95
+ RbWs.connect("http://127.0.0.1:10000", pluginId).catch(() => null),
96
+ new Promise((r) => setTimeout(() => r(null), 5000)),
97
+ ]);
98
+ if (!ws) return log.warn("rb.rosPeers unavailable in dev: plugin gateway not reachable on :10000");
99
+ const base = `/muscat/plugin/${pluginId}/ros_peers`;
100
+ await ws.serve(`${base}/get`, () => ({ peers: readRosPeers(root) }));
101
+ await ws.serve(`${base}/set`, (payload) => {
102
+ const peers = payload?.peers;
103
+ if (!Array.isArray(peers) || !peers.every((p) => typeof p === "string" && isIP(p))) {
104
+ return { ok: false, error: "peers must be a list of IP addresses" };
44
105
  }
45
- }
106
+ ensureDevDir(root);
107
+ const file = resolve(root, ROS_PEERS_FILE);
108
+ writeFileSync(file, JSON.stringify(peers) + "\n");
109
+ log.step(`ROS_STATIC_PEERS → ${peers.join(";") || "(none)"} — restarting node`);
110
+ restart();
111
+ return { ok: true, peers };
112
+ });
113
+ }
46
114
 
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
- }
115
+ // ── IDE tooling venv (best-effort, non-blocking) ────────────────────────────
116
+ // `muscat-pk new` already creates this up front (see new.mjs); this just
117
+ // refreshes it on every `dev`/`dev:back` in case requirements.txt changed.
118
+ // Docker remains the actual execution environment — this never affects it.
119
+ // Runs in the background so it never delays `dev:back` starting, and any
120
+ // failure here (no local Python toolchain, offline, ...) is silent.
121
+ function syncIdeVenv(root) {
122
+ setImmediate(() => {
123
+ try {
124
+ ensureIdeVenv(root);
125
+ } catch {
126
+ /* best-effort only */
127
+ }
128
+ });
129
+ }
130
+
131
+ // ── python ────────────────────────────────────────────────────────────────────
132
+ // Runs in a warm Ubuntu 24.04 + Python 3.12 container (matches the robot).
133
+ // Under `muscat-pk dev`, it shares the local Muscat container's netns, so the SDK
134
+ // reaches the plugin gateway at the default localhost:10000.
135
+ async function runPython(root, portRanges, robotGateway) {
136
+ // Independent of Docker readiness — must still happen if Docker isn't
137
+ // installed/running, since that's exactly when local IDE tooling matters most.
138
+ syncIdeVenv(root);
139
+ pyAssertDockerReady();
140
+ await pyEnsureImage();
141
+
142
+ const manifest = readManifest(root);
143
+ const pluginId = process.env.MUSCAT_PK_DEV_PLUGIN_ID || (manifest.id ?? "");
144
+ const container = DEV_CONTAINER;
145
+ const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
146
+ const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
147
+ const zenohAdvertiseHost = process.env.MUSCAT_ZENOH_ADVERTISE_HOST ?? "";
148
+ const zenohPort = process.env.MUSCAT_ZENOH_PORT ?? "";
149
+ // See host_agent.py: a Docker Desktop container can't join the robot's
150
+ // Zenoh mesh, so it speaks a byte protocol to the host-side proxy instead.
151
+ const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT ?? "";
152
+ // Native Linux (not WSL, not Docker Desktop) has real host networking: the
153
+ // container shares the host's network stack, so it reaches the gateway at
154
+ // localhost:10000 and the robot's Zenoh router / reverse-dial path directly
155
+ // — no netns sharing, port publishing, host.docker.internal, or byte-proxy.
156
+ const net = backendNetwork(portRanges, robotGateway);
157
+ // Only the container's OWN reverse-dial path needs to publish a listen port.
158
+ // With a shared netns the port is already reachable; with the byte-proxy
159
+ // the container just connects out — neither needs -p.
160
+ const standaloneZenohPortFlags =
161
+ !net.shared && !proxyEndpoint && zenohPort ? ["-p", `${zenohPort}:${zenohPort}`] : [];
162
+ // C++ SDK (rainbow-rb-sdk >= 0.10) can't use the byte shim or the fallback patch: its Zenoh session
163
+ // is native. It joins the robot's mesh itself on this host port (see the entry script); inside,
164
+ // the port + 1 accepts. With or without host_agent (dev vs standalone dev:back).
165
+ const sdkAdvertiseHost = proxyEndpoint ? proxyEndpoint.split(":")[0] : zenohAdvertiseHost;
166
+ const sdkPeerPort = sdkAdvertiseHost && !net.shared && zenohPort ? await getFreePort(Number(zenohPort) + 1) : "";
167
+ const sdkPeerFlags = sdkPeerPort
168
+ ? ["--sysctl", "net.ipv4.ip_nonlocal_bind=1", "-p", `${sdkPeerPort}:${Number(sdkPeerPort) + 1}`]
169
+ : [];
170
+ const envFlags = [
171
+ // docker exec below has no -t (no pseudo-TTY), so Python defaults to
172
+ // block-buffered stdout — print() output wouldn't show up until the
173
+ // buffer fills or the process exits. Force unbuffered.
174
+ "-e", "PYTHONUNBUFFERED=1",
175
+ ...net.env,
176
+ ...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
177
+ ...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
178
+ ...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
179
+ // Byte-proxy mode: the container has no Zenoh session of its own — the
180
+ // entry script's shim speaks the raw query/put/subscribe protocol to the
181
+ // host_agent process at this address instead.
182
+ ...(proxyEndpoint ? ["-e", `MUSCAT_PROXY_ENDPOINT=${proxyEndpoint}`] : []),
183
+ ...(sdkPeerPort ? ["-e", `MUSCAT_SDK_PEER_PORT=${sdkPeerPort}`, "-e", `MUSCAT_SDK_ADVERTISE_HOST=${sdkAdvertiseHost}`] : []),
184
+ // Fallback (no proxy): the container reverse-dials the robot itself.
185
+ ...(!proxyEndpoint && zenohAdvertiseHost ? ["-e", `MUSCAT_ZENOH_ADVERTISE_HOST=${zenohAdvertiseHost}`] : []),
186
+ ...(!proxyEndpoint && zenohPort ? ["-e", `MUSCAT_ZENOH_PORT=${zenohPort}`] : []),
187
+ ...portEnvFlags(portRanges),
188
+ ];
189
+ const docker = (args, opts = {}) =>
190
+ spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
191
+
192
+ // start a warm, long-lived container with the plugin mounted.
193
+ // 호스트 uid 로 돌린다(dockerUserFlags) — root 로 쓴 build/·install/ 은 사용자가 지우지 못한다.
194
+ docker(["rm", "-f", container], { stdio: "ignore" });
195
+ const up = docker(
196
+ ["run", "-d", "--name", container, ...net.run, ...standaloneZenohPortFlags, ...sdkPeerFlags, ...realtimeFlags(), ...dockerUserFlags(),
197
+ "-e", "PIP_ROOT_USER_ACTION=ignore",
198
+ "-v", `${root}:/plugin`, "-w", "/plugin", PYTHON_DEV_IMAGE, "sleep", "infinity"],
199
+ { stdio: ["ignore", "ignore", "inherit"] }, // stdout is just the container id
200
+ );
201
+ if (up.status !== 0) throw new Error("failed to start dev container.");
202
+ const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [zenohPort, sdkPeerPort, sdkPeerPort && String(Number(sdkPeerPort) + 1)] });
203
+
204
+ docker(["exec", container, "bash", "-lc",
205
+ "pip install --break-system-packages --ignore-installed -q watchfiles" +
206
+ " && { [ -f requirements.txt ] && pip install --break-system-packages --ignore-installed -q -r requirements.txt || true; }"]);
207
+
208
+ docker(["exec", container, "bash", "-lc", `cat > /tmp/muscat-pk-python-dev-entry.py <<'PY'
209
+ import os
210
+
211
+ try:
212
+ import glob
213
+ import importlib.util
214
+
215
+ proxy_endpoint = os.getenv("MUSCAT_PROXY_ENDPOINT")
216
+ # Located without importing: importing rainbow.rb_sdk opens the SDK's Zenoh session.
217
+ _rainbow = importlib.util.find_spec("rainbow")
218
+ cpp_sdk = bool(_rainbow and any(glob.glob(os.path.join(p, "rb_sdk", "rb_sdk_cpp*.so")) for p in _rainbow.submodule_search_locations))
219
+ if cpp_sdk and os.getenv("MUSCAT_SDK_PEER_PORT"):
220
+ # C++ SDK (rainbow-rb-sdk >= 0.10): its Zenoh session is native, so the byte shim below
221
+ # can't reach it, and connect() would register the container's own unreachable IP.
222
+ # Robot peers dial each remote peer at the locators it gossips, so the session must
223
+ # advertise the host's ip:port: listen there (ip_nonlocal_bind; never accepts) plus on
224
+ # port + 1, where Docker forwards the host port. Then register like host_agent does.
225
+ import json
226
+ import threading
227
+ import time
228
+ import urllib.request
229
+
230
+ from rainbow.rb_sdk import rb_sdk_cpp
231
+
232
+ _port = int(os.environ["MUSCAT_SDK_PEER_PORT"])
233
+ _advertise = os.environ["MUSCAT_SDK_ADVERTISE_HOST"]
234
+ os.environ["ZENOH_PEER_LISTEN_ENDPOINTS"] = f"tcp/{_advertise}:{_port},tcp/0.0.0.0:{_port + 1}"
235
+ for _cls in list(vars(rb_sdk_cpp).values()):
236
+ if isinstance(_cls, type) and issubclass(_cls, rb_sdk_cpp.RBBaseSDK) and "connect" in _cls.__dict__:
237
+ _cls.connect = lambda self, *args, **kwargs: None
238
+
239
+ def _peer_http(method, path, body=None):
240
+ headers = {"Content-Type": "application/json"}
241
+ if os.getenv("MUSCAT_ROBOT_TOKEN"):
242
+ headers["Authorization"] = "Bearer " + os.environ["MUSCAT_ROBOT_TOKEN"]
243
+ req = urllib.request.Request(f"http://{os.environ['MUSCAT_ROBOT_HOST']}:8000{path}",
244
+ data=json.dumps(body).encode() if body is not None else None,
245
+ headers=headers, method=method)
246
+ with urllib.request.urlopen(req, timeout=10) as resp:
247
+ raw = resp.read()
248
+ return json.loads(raw) if raw else {}
249
+
250
+ def _peer_keepalive():
251
+ advertise = _advertise
252
+ peer_id = None
253
+ while True:
254
+ try:
255
+ if peer_id is None:
256
+ peer_id = _peer_http("POST", "/zenoh/peer", {"ip": advertise, "port": _port, "ttl": 30})["peer_id"]
257
+ print(f"[muscat-pk] C++ SDK zenoh peer {advertise}:{_port} registered with the robot", flush=True)
258
+ elif not _peer_http("POST", f"/zenoh/peer/{peer_id}/heartbeat", {"ttl": 30}).get("ok"):
259
+ peer_id = None
260
+ continue
261
+ except Exception as exc:
262
+ print(f"[muscat-pk] zenoh peer registration failed ({exc}); retrying", flush=True)
263
+ peer_id = None
264
+ time.sleep(15 if peer_id else 3)
265
+
266
+ threading.Thread(target=_peer_keepalive, daemon=True).start()
267
+ elif proxy_endpoint:
268
+ # Byte-proxy mode: this container has no Zenoh session of its own.
269
+ # ZenohClient's C++ core is replaced with a shim that speaks a raw
270
+ # query/put/subscribe byte protocol to host_agent.py instead.
271
+ import json
272
+ import socket
273
+ import struct
274
+ import threading
275
+
276
+ _proxy_host, _proxy_port_s = proxy_endpoint.rsplit(":", 1)
277
+ _proxy_port = int(_proxy_port_s)
278
+
279
+ def _send_frame(conn, lock, obj):
280
+ body = json.dumps(obj).encode()
281
+ with lock:
282
+ conn.sendall(struct.pack(">I", len(body)) + body)
283
+
284
+ def _recv_frame(conn):
285
+ hdr = b""
286
+ while len(hdr) < 4:
287
+ chunk = conn.recv(4 - len(hdr))
288
+ if not chunk:
289
+ return None
290
+ hdr += chunk
291
+ n = struct.unpack(">I", hdr)[0]
292
+ buf = b""
293
+ while len(buf) < n:
294
+ chunk = conn.recv(n - len(buf))
295
+ if not chunk:
296
+ return None
297
+ buf += chunk
298
+ return json.loads(buf.decode())
299
+
300
+ class _ProxyRawClient:
301
+ """Drop-in replacement for ZenohClient's C++ core, backed by the
302
+ host_agent byte-proxy instead of a local Zenoh session."""
303
+
304
+ sender = "RRS"
305
+ sender_id = ""
306
+
307
+ def __init__(self, host, port):
308
+ self._conn = socket.create_connection((host, port), timeout=10)
309
+ # create_connection() leaves the connect timeout applied to
310
+ # the socket permanently; the background read loop below does
311
+ # a long-lived blocking recv() for async "sample" pushes, so
312
+ # it must not time out on an idle connection.
313
+ self._conn.settimeout(None)
314
+ self._send_lock = threading.Lock()
315
+ self._req_id = 0
316
+ self._req_lock = threading.Lock()
317
+ self._pending = {}
318
+ self._subs = {} # sub_id -> callback
319
+ self._sub_by_keyexpr = {}
320
+ threading.Thread(target=self._read_loop, daemon=True).start()
321
+
322
+ def _next_id(self):
323
+ with self._req_lock:
324
+ self._req_id += 1
325
+ return self._req_id
326
+
327
+ def _read_loop(self):
328
+ while True:
329
+ frame = _recv_frame(self._conn)
330
+ if frame is None:
331
+ return
332
+ ftype = frame.get("type")
333
+ if ftype == "sample":
334
+ cb = self._sub_by_keyexpr.get(frame["keyexpr"])
335
+ if cb is not None:
336
+ try:
337
+ cb(frame["key"], bytes.fromhex(frame["payload_hex"]), frame.get("att", ""))
338
+ except Exception as exc:
339
+ print(f"[muscat-pk] proxy subscriber callback error: {exc}", flush=True)
340
+ elif "id" in frame:
341
+ ev = self._pending.pop(frame["id"], None)
342
+ if ev is not None:
343
+ ev[1] = frame
344
+ ev[0].set()
345
+
346
+ def _request(self, obj, timeout=30):
347
+ req_id = self._next_id()
348
+ obj["id"] = req_id
349
+ ev = threading.Event()
350
+ slot = [ev, None]
351
+ self._pending[req_id] = slot
352
+ _send_frame(self._conn, self._send_lock, obj)
353
+ if not ev.wait(timeout):
354
+ self._pending.pop(req_id, None)
355
+ raise TimeoutError(f"proxy request timed out: {obj.get('type')} {obj.get('keyexpr')}")
356
+ return slot[1]
357
+
358
+ def query(self, keyexpr, payload, timeout_ms, target):
359
+ resp = self._request({
360
+ "type": "query", "keyexpr": keyexpr,
361
+ "payload_hex": payload.hex() if payload else None,
362
+ "timeout_ms": timeout_ms, "target": target,
363
+ }, timeout=(timeout_ms / 1000) + 5)
364
+ if not resp.get("ok"):
365
+ raise RuntimeError(resp.get("error", "proxy query failed"))
366
+ return [
367
+ (r["key"], bytes.fromhex(r["payload_hex"]), r["att"], r["encoding"], r["is_error"])
368
+ for r in resp["replies"]
369
+ ]
370
+
371
+ def query_all(self, keyexpr, payload, timeout_ms, target):
372
+ return self.query(keyexpr, payload, timeout_ms, target)
373
+
374
+ def publish(self, topic, payload, att, congestion_control, priority, express):
375
+ resp = self._request({
376
+ "type": "put", "keyexpr": topic,
377
+ "payload_hex": bytes(payload).hex() if payload else None,
378
+ })
379
+ if not resp.get("ok"):
380
+ raise RuntimeError(resp.get("error", "proxy put failed"))
381
+
382
+ def has_subscriber(self, topic):
383
+ return topic in self._sub_by_keyexpr
384
+
385
+ def declare_subscriber(self, topic, callback, opts=None):
386
+ resp = self._request({"type": "subscribe", "keyexpr": topic})
387
+ if not resp.get("ok"):
388
+ raise RuntimeError(resp.get("error", "proxy subscribe failed"))
389
+ sub_id = self._next_id()
390
+ self._subs[sub_id] = topic
391
+ self._sub_by_keyexpr[topic] = callback
392
+ return sub_id
393
+
394
+ def undeclare_subscriber(self, sub_id):
395
+ topic = self._subs.pop(sub_id, None)
396
+ if topic is None:
397
+ return
398
+ self._sub_by_keyexpr.pop(topic, None)
399
+ self._request({"type": "unsubscribe", "keyexpr": topic})
400
+
401
+ def unsubscribe_topic(self, topic):
402
+ for sub_id, t in list(self._subs.items()):
403
+ if t == topic:
404
+ self.undeclare_subscriber(sub_id)
405
+
406
+ def unsubscribe_all(self):
407
+ for sub_id in list(self._subs):
408
+ self.undeclare_subscriber(sub_id)
409
+
410
+ def get_sub_stats(self, sub_id):
411
+ return {}
412
+
413
+ def declare_queryable(self, keyexpr, handler):
414
+ raise NotImplementedError("byte-proxy mode does not support declare_queryable")
415
+
416
+ def undeclare_queryable_by_topic(self, keyexpr):
417
+ pass
418
+
419
+ # ZenohClient is a process-wide singleton whose __init__ eagerly
420
+ # calls connect() on first construction. rainbow.rb_sdk.base
421
+ # transitively imports rainbow.rb_log.log, which constructs one at
422
+ # module level — so this patch must land before RBBaseSDK is
423
+ # imported, or that eager construction wins with a real session.
424
+ from rainbow.rb_zenoh.client import ZenohClient
425
+
426
+ def _proxy_connect(self, *, config_pairs=None, sender=None, max_attempts=8, initial_delay_s=0.5, max_delay_s=3.0):
427
+ if self._client is not None:
428
+ return
429
+ if sender is not None:
430
+ self._sender = sender
431
+ print(f"[muscat-pk] zenoh via host byte-proxy → {proxy_endpoint} (no local Zenoh session)", flush=True)
432
+ self._client = _ProxyRawClient(_proxy_host, _proxy_port)
52
433
 
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" });
434
+ ZenohClient.connect = _proxy_connect
58
435
 
59
- log.step("watchfiles → python main.py (reload on any .py change)");
436
+ from rainbow.rb_sdk.base import RBBaseSDK
437
+
438
+ async def _sdk_connect_via_proxy(self, *args, **kwargs):
439
+ # No HTTP registration with common — host_agent already handles
440
+ # that. Just make sure the (proxy-backed) shared ZenohClient exists.
441
+ self._ensure_zenoh_client_alive()
442
+ return None
443
+
444
+ async def _sdk_disconnect_via_proxy(self, *args, **kwargs):
445
+ return None
446
+
447
+ RBBaseSDK.connect = _sdk_connect_via_proxy
448
+ RBBaseSDK.disconnect = _sdk_disconnect_via_proxy
449
+ else:
450
+ # Fallback: no host byte-proxy — the container reverse-dials the robot itself.
451
+ advertise_host = os.getenv("MUSCAT_ZENOH_ADVERTISE_HOST")
452
+ zenoh_port = os.getenv("MUSCAT_ZENOH_PORT")
453
+ from rainbow.rb_sdk.base import RBBaseSDK
454
+ if advertise_host:
455
+ RBBaseSDK._detect_local_ip = staticmethod(lambda host: advertise_host)
456
+ if zenoh_port:
457
+ _orig_connect = RBBaseSDK.connect
458
+
459
+ async def _connect_with_dev_port(self, *args, **kwargs):
460
+ kwargs.setdefault("zenoh_port", int(zenoh_port))
461
+ return await _orig_connect(self, *args, **kwargs)
462
+
463
+ RBBaseSDK.connect = _connect_with_dev_port
464
+ except Exception as exc:
465
+ print(f"[muscat-pk] SDK dev network patch skipped: {exc}", flush=True)
466
+
467
+ import runpy
468
+ runpy.run_path("/plugin/py/main.py", run_name="__main__")
469
+ PY`]);
470
+
471
+ if (usesHostNetwork()) log.step("network: host (native Linux — direct robot/gateway access)");
472
+ if (proxyEndpoint) log.step(`Zenoh via host byte-proxy → ${proxyEndpoint}`);
473
+ else if (zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
60
474
  const child = spawn(
61
- py,
62
- ["-m", "watchfiles", "--filter", "python", "--target-type", "command", "python main.py", "."],
63
- { cwd: root, stdio: "inherit" },
475
+ "docker",
476
+ ["exec", ...envFlags, container, "bash", "-lc",
477
+ "python3 -m watchfiles --filter python --target-type command 'python3 /tmp/muscat-pk-python-dev-entry.py' py"],
478
+ { cwd: root, stdio: ["inherit", "inherit", "pipe"] },
64
479
  );
65
- child.on("exit", (code) => process.exit(code ?? 0));
480
+ // watchfiles prints a KeyboardInterrupt traceback on SIGTERM — suppress it
481
+ child.stderr.on("data", (buf) => {
482
+ const text = buf.toString();
483
+ if (!text.includes("KeyboardInterrupt") && !text.includes("raise_keyboard_interrupt")) {
484
+ process.stderr.write(buf);
485
+ }
486
+ });
487
+
488
+ let cleaned = false;
489
+ const cleanupResources = () => {
490
+ if (cleaned) return;
491
+ cleaned = true;
492
+ stopPortMonitor();
493
+ if (!child.killed && child.exitCode === null) child.kill();
494
+ spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
495
+ };
496
+ const cleanup = () => {
497
+ cleanupResources();
498
+ process.exit(0);
499
+ };
500
+ child.on("exit", (code) => {
501
+ cleanupResources();
502
+ process.exit(code ?? 0);
503
+ });
504
+ process.once("SIGINT", cleanup);
505
+ process.once("SIGTERM", cleanup);
506
+ process.once("SIGHUP", cleanup);
66
507
  }
67
508
 
68
509
  // ── ros2 ───────────────────────────────────────────────────────────────────────
69
- function runRos2(root) {
510
+ async function runRos2(root, portRanges, robotGateway) {
511
+ const manifest = readManifest(root);
512
+ const isRclcpp = manifest.runtime?.rosClient === "rclcpp";
513
+ // Independent of Docker readiness — must still happen if Docker isn't
514
+ // installed/running, since that's exactly when local IDE tooling matters most.
515
+ if (!isRclcpp) syncIdeVenv(root); // rclpy is plain Python — rclcpp is C++, no venv needed
516
+
70
517
  assertDockerReady();
71
518
  const pkg = ros2PackageName(root);
72
519
  if (!pkg) throw new Error("no ROS2 package found under ros2_ws/src.");
73
520
 
74
- ensureImage();
521
+ await ensureImage();
75
522
 
76
- const container = `${ROS2_DEV_IMAGE}-run`;
523
+ const pluginId = process.env.MUSCAT_PK_DEV_PLUGIN_ID || (manifest.id ?? "");
524
+ const container = DEV_CONTAINER;
525
+ const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
526
+ const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
527
+ const net = backendNetwork(portRanges, robotGateway);
528
+ const envFlags = [
529
+ // Same rationale as runPython(): no -t on docker exec below means
530
+ // rclpy's Python process stdout is block-buffered, not line-buffered.
531
+ "-e", "PYTHONUNBUFFERED=1",
532
+ // Same as the robot: ROS discovery stays on this host (allow a PC with ROS_STATIC_PEERS).
533
+ "-e", "ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST",
534
+ ...net.env,
535
+ ...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
536
+ ...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
537
+ ...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
538
+ ...portEnvFlags(portRanges),
539
+ ];
77
540
  const docker = (args, opts = {}) =>
78
541
  spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
79
542
 
80
543
  // start a warm, long-lived container with the workspace mounted
81
544
  docker(["rm", "-f", container], { stdio: "ignore" });
82
- log.step("starting ROS2 dev container…");
83
545
  const up = docker(
84
- ["run", "-d", "--name", container, "-v", `${root}:/plugin`, "-w", "/plugin", ROS2_DEV_IMAGE, "sleep", "infinity"],
85
- { stdio: "inherit" },
546
+ ["run", "-d", "--name", container, ...net.run, ...realtimeFlags(), ...dockerUserFlags(), "-e", "PIP_ROOT_USER_ACTION=ignore", "-v", `${root}:/plugin`, "-w", "/plugin", ROS2_DEV_IMAGE, "sleep", "infinity"],
547
+ { stdio: ["ignore", "ignore", "inherit"] }, // stdout is just the container id
86
548
  );
87
549
  if (up.status !== 0) throw new Error("failed to start dev container.");
550
+ const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [process.env.MUSCAT_ZENOH_PORT] });
88
551
 
89
552
  // one-time deps + symlink build (edits to .py reflect without rebuilding)
90
- log.step("pip install + colcon build --symlink-install (once)…");
91
553
  docker(["exec", container, "bash", "-lc",
92
554
  "pip install -r requirements.txt --break-system-packages --ignore-installed -q || true; cd ros2_ws && colcon build --symlink-install"]);
93
555
 
94
556
  let child = null;
95
557
  const startNode = () => {
96
558
  if (child) child.kill();
97
- log.step(`ros2 run ${pkg} ${pkg}_node`);
559
+ // killing the `docker exec` client doesn't stop the node inside the container
560
+ spawnSync("docker", ["exec", container, "pkill", "-f", `${pkg}_node|${manifest.runtime?.launch || pkg}`], { stdio: "ignore" });
561
+ const peers = readRosPeers(root);
562
+ // Same command the robot runs: rclpy execs the console script, rclcpp uses `ros2 run`;
563
+ // relative topics land under the plugin's namespace (rosNamespace).
564
+ const launch = manifest.runtime?.launch || `${pkg}_node`;
565
+ const nsArgs = PLUGIN_ID_RE.test(pluginId) ? ` --ros-args -r __ns:=${rosNamespace(pluginId)}` : "";
566
+ const run = (isRclcpp ? `ros2 run ${pkg} ${launch}` : `exec ${launch}`) + nsArgs;
98
567
  child = spawn(
99
568
  "docker",
100
- ["exec", container, "bash", "-lc",
101
- `cd ros2_ws && source install/setup.bash && ros2 run ${pkg} ${pkg}_node`],
569
+ ["exec", ...envFlags, ...(peers.length ? ["-e", `ROS_STATIC_PEERS=${peers.join(";")}`] : []), container, "bash", "-lc",
570
+ `cd ros2_ws && source install/setup.bash && ${run}`],
102
571
  { cwd: root, stdio: "inherit" },
103
572
  );
104
573
  };
105
574
  startNode();
575
+ serveRosPeers(root, pluginId, startNode);
106
576
 
107
- // restart node on source change; rebuild only when package metadata changes
577
+ // restart node on source change. rclpy reflects .py edits live (symlink-install);
578
+ // rclcpp (C++) and package-metadata changes require a colcon rebuild.
108
579
  let timer = null;
109
580
  const onChange = (_event, filename) => {
110
581
  clearTimeout(timer);
111
582
  timer = setTimeout(() => {
112
583
  const structural = filename && /(setup\.py|package\.xml|CMakeLists\.txt)$/.test(String(filename));
113
- if (structural) {
114
- log.info("package metadata changed — colcon rebuild…");
584
+ if (structural || isRclcpp) {
585
+ log.info(structural ? "package metadata changed — colcon rebuild…" : "source changed — colcon rebuild…");
115
586
  docker(["exec", container, "bash", "-lc", "cd ros2_ws && colcon build --symlink-install"]);
116
587
  } else {
117
588
  log.info("source changed — restarting node…");
@@ -125,17 +596,157 @@ function runRos2(root) {
125
596
  }
126
597
 
127
598
  const cleanup = () => {
599
+ stopPortMonitor();
128
600
  if (child) child.kill();
129
601
  spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
130
602
  process.exit(0);
131
603
  };
132
604
  process.on("SIGINT", cleanup);
133
605
  process.on("SIGTERM", cleanup);
606
+ process.on("SIGHUP", cleanup);
134
607
  }
135
608
 
136
- export async function runDevBack() {
609
+ // ── cpp ──────────────────────────────────────────────────────────────────────
610
+ async function runCpp(root, portRanges, robotGateway) {
611
+ cppAssertDockerReady();
612
+ const manifest = readManifest(root);
613
+ const entry = manifest.runtime?.entry;
614
+ if (!entry) throw new Error("runtime.entry (the built executable name) is required for cpp.");
615
+
616
+ await cppEnsureImage();
617
+
618
+ const pluginId = process.env.MUSCAT_PK_DEV_PLUGIN_ID || (manifest.id ?? "");
619
+ const container = DEV_CONTAINER;
620
+ const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
621
+ const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
622
+ const net = backendNetwork(portRanges, robotGateway);
623
+ const envFlags = [
624
+ ...net.env,
625
+ ...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
626
+ ...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
627
+ ...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
628
+ ...portEnvFlags(portRanges),
629
+ ];
630
+ const docker = (args, opts = {}) =>
631
+ spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
632
+
633
+ // start a warm, long-lived container with the plugin mounted
634
+ docker(["rm", "-f", container], { stdio: "ignore" });
635
+ const up = docker(
636
+ ["run", "-d", "--name", container, ...net.run, ...realtimeFlags(), ...dockerUserFlags(), "-v", `${root}:/plugin`, "-w", "/plugin", CPP_DEV_IMAGE, "sleep", "infinity"],
637
+ { stdio: ["ignore", "ignore", "inherit"] }, // stdout is just the container id
638
+ );
639
+ if (up.status !== 0) throw new Error("failed to start dev container.");
640
+ const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [process.env.MUSCAT_ZENOH_PORT] });
641
+
642
+ const build = () => {
643
+ return docker(["exec", container, "bash", "-lc",
644
+ "cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=Debug && cmake --build cpp/build -j"]);
645
+ };
646
+ build();
647
+
648
+ let child = null;
649
+ const startBin = () => {
650
+ if (child) child.kill();
651
+ child = spawn(
652
+ "docker",
653
+ ["exec", ...envFlags, container, "bash", "-lc", `./cpp/build/${entry}`],
654
+ { cwd: root, stdio: "inherit" },
655
+ );
656
+ };
657
+ startBin();
658
+
659
+ // rebuild + restart on C++ source changes; ignore the build/ output dir.
660
+ let timer = null;
661
+ const onChange = (_event, filename) => {
662
+ if (filename && String(filename).startsWith("build")) return;
663
+ clearTimeout(timer);
664
+ timer = setTimeout(() => {
665
+ log.info("source changed — rebuilding…");
666
+ const r = build();
667
+ if (r.status === 0) startBin();
668
+ else log.warn("build failed — keeping previous binary");
669
+ }, 300);
670
+ };
671
+ const cppDir = resolve(root, "cpp");
672
+ if (existsSync(cppDir)) watch(cppDir, { recursive: true }, onChange);
673
+
674
+ const cleanup = () => {
675
+ stopPortMonitor();
676
+ if (child) child.kill();
677
+ spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
678
+ process.exit(0);
679
+ };
680
+ process.on("SIGINT", cleanup);
681
+ process.on("SIGTERM", cleanup);
682
+ process.on("SIGHUP", cleanup);
683
+ }
684
+
685
+ // `dev` builds the image and the backend first so the runtime and UI come up with it ready;
686
+ // dev:back's own build is then incremental. A failed build is left to dev:back's watcher.
687
+ export async function prebuildBackend(root) {
688
+ try {
689
+ const type = manifestRuntime(root);
690
+ if (type === "cpp") {
691
+ cppAssertDockerReady();
692
+ await cppEnsureImage();
693
+ cmakeBuild(root);
694
+ } else if (type === "ros2") {
695
+ assertDockerReady();
696
+ await ensureImage();
697
+ colconBuild(root, { symlink: true });
698
+ } else {
699
+ pyAssertDockerReady();
700
+ await pyEnsureImage();
701
+ }
702
+ } catch (e) {
703
+ log.warn(`backend build: ${e.message}`);
704
+ }
705
+ }
706
+
707
+ function warnManifest(root) {
708
+ const manifest = readManifest(root);
709
+ if (!manifest || Object.keys(manifest).length === 0) return;
710
+ const errors = validateManifest(manifest);
711
+ for (const e of errors) log.warn(`manifest.json: ${e}`);
712
+ }
713
+
714
+ export async function runDevBack(args = []) {
137
715
  const root = findPluginRoot();
716
+ await claimDevSession(root, "dev:back");
717
+ assertManifestVariables(root);
718
+ warnManifest(root);
719
+ const portRanges = assertManifestPorts(root);
720
+ ensureDocker();
721
+ const { robotIp, robotToken } = await promptRobotConnection(args);
722
+ if (robotIp) process.env.MUSCAT_ROBOT_HOST = robotIp;
723
+ if (robotToken) process.env.MUSCAT_ROBOT_TOKEN = robotToken;
724
+ // Under `dev` this returns the id `dev` chose (environment); standalone it decides it the same way.
725
+ devPluginId(readManifest(root).id, { robotIp, args });
726
+ // Standalone `dev:back` against a remote robot needs the Zenoh reverse-path
727
+ // advertise host/port too (idempotent — `muscat-pk dev` already set these).
728
+ // Byte-proxy mode (a host-side host_agent set MUSCAT_PROXY_ENDPOINT) makes
729
+ // the container speak the proxy protocol instead, so it needs no reverse-dial.
730
+ if (!process.env.MUSCAT_PROXY_ENDPOINT) await ensureRemoteZenohEnv(robotIp, { log });
731
+ // Standalone against a local Muscat (muscat-dev, …): join its netns like `dev` does,
732
+ // otherwise zenoh reaches nothing and `execute` never arrives.
733
+ if (!process.env.MUSCAT_PK_MUSCAT_CONTAINER && !isRemoteRobot(robotIp)) {
734
+ const muscat = localMuscatContainer();
735
+ if (muscat) {
736
+ process.env.MUSCAT_PK_MUSCAT_CONTAINER = muscat;
737
+ }
738
+ }
739
+ // Stopped while still setting up (image build, pip install, first cmake build): the runtime's own
740
+ // cleanup is not registered yet, so remove the container here. Once it is, it handles the signal.
741
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"])
742
+ process.once(signal, () => {
743
+ if (process.listenerCount(signal)) return;
744
+ stopDevBackContainers();
745
+ process.exit(130);
746
+ });
747
+ const robotGateway = (await robotHasMuscat(robotIp)) ? `http://${robotIp}:${GATEWAY_PORT}` : "";
138
748
  const type = manifestRuntime(root);
139
- if (type === "ros2") return runRos2(root);
140
- return runPython(root);
749
+ if (type === "ros2") return await runRos2(root, portRanges, robotGateway);
750
+ if (type === "cpp") return await runCpp(root, portRanges, robotGateway);
751
+ return await runPython(root, portRanges, robotGateway);
141
752
  }