@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.9 → 0.10.1-dev.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 (276) hide show
  1. package/README.ko.md +1188 -0
  2. package/README.md +1139 -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 +12 -0
  11. package/cli/program-commands.json +2079 -0
  12. package/cli/src/__pycache__/host_agent.cpython-37.pyc +0 -0
  13. package/cli/src/ai-rules.mjs +108 -0
  14. package/cli/src/build.mjs +333 -93
  15. package/cli/src/check-package.mjs +8 -23
  16. package/cli/src/completion.mjs +168 -0
  17. package/cli/src/cpp.mjs +78 -0
  18. package/cli/src/cppIde.mjs +76 -0
  19. package/cli/src/dev.mjs +195 -6
  20. package/cli/src/devBack.mjs +671 -60
  21. package/cli/src/devId.mjs +54 -0
  22. package/cli/src/devSession.mjs +67 -0
  23. package/cli/src/devUi.mjs +218 -25
  24. package/cli/src/esbuild.mjs +27 -10
  25. package/cli/src/host_agent.py +223 -0
  26. package/cli/src/manifest.mjs +288 -0
  27. package/cli/src/mcp.mjs +1232 -0
  28. package/cli/src/mcpProxy.mjs +110 -0
  29. package/cli/src/muscatLocal.mjs +292 -0
  30. package/cli/src/new.mjs +205 -68
  31. package/cli/src/pluginDevServer.mjs +111 -0
  32. package/cli/src/ports.mjs +193 -0
  33. package/cli/src/programCommands.mjs +327 -0
  34. package/cli/src/projectCheck.mjs +102 -0
  35. package/cli/src/python.mjs +112 -0
  36. package/cli/src/ros2.mjs +62 -18
  37. package/cli/src/sdk.mjs +74 -24
  38. package/cli/src/sim.mjs +177 -0
  39. package/cli/src/storybook.mjs +150 -0
  40. package/cli/src/templates-embedded.mjs +4 -0
  41. package/cli/src/type.mjs +93 -34
  42. package/cli/src/uninstall.mjs +81 -0
  43. package/cli/src/update.mjs +364 -35
  44. package/cli/src/util.mjs +341 -11
  45. package/cli/src/zenohDev.mjs +83 -0
  46. package/cli/src/zip.mjs +121 -0
  47. package/cli/storybook-dist/_design-tokens.css +4957 -0
  48. package/cli/storybook-dist/_stories-src/NxAvatar.stories.tsx +27 -0
  49. package/cli/storybook-dist/_stories-src/NxPagination.stories.tsx +28 -0
  50. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordion.stories.tsx +46 -0
  51. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordionGroup.stories.tsx +73 -0
  52. package/cli/storybook-dist/_stories-src/RBBadge/RBBadge.stories.tsx +93 -0
  53. package/cli/storybook-dist/_stories-src/RBBattery/RBBattery.stories.tsx +74 -0
  54. package/cli/storybook-dist/_stories-src/RBButton/RBCheckIconButton.stories.tsx +28 -0
  55. package/cli/storybook-dist/_stories-src/RBButton/RBCheckLabelButton.stories.tsx +84 -0
  56. package/cli/storybook-dist/_stories-src/RBButton/RBIconButton.stories.tsx +57 -0
  57. package/cli/storybook-dist/_stories-src/RBButton/RBInlineLabelButton.stories.tsx +41 -0
  58. package/cli/storybook-dist/_stories-src/RBButton/RBLabelButton.stories.tsx +60 -0
  59. package/cli/storybook-dist/_stories-src/RBDatePicker/RBDateInput.stories.tsx +44 -0
  60. package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +56 -0
  61. package/cli/storybook-dist/_stories-src/RBDialog/RBAlert.stories.tsx +51 -0
  62. package/cli/storybook-dist/_stories-src/RBDialog/RBConfirm.stories.tsx +55 -0
  63. package/cli/storybook-dist/_stories-src/RBDialog/RBPrompt.stories.tsx +52 -0
  64. package/cli/storybook-dist/_stories-src/RBDivider/RBDivider.stories.tsx +93 -0
  65. package/cli/storybook-dist/_stories-src/RBEmptyView/RBEmptyView.stories.tsx +49 -0
  66. package/cli/storybook-dist/_stories-src/RBIcons/RBColorIcons.stories.tsx +108 -0
  67. package/cli/storybook-dist/_stories-src/RBIcons/RBIcons.stories.tsx +106 -0
  68. package/cli/storybook-dist/_stories-src/RBInput/RBCheckBox.stories.tsx +89 -0
  69. package/cli/storybook-dist/_stories-src/RBInput/RBMultiRangeInput.stories.tsx +64 -0
  70. package/cli/storybook-dist/_stories-src/RBInput/RBRadioBox.stories.tsx +65 -0
  71. package/cli/storybook-dist/_stories-src/RBInput/RBRadioButton.stories.tsx +68 -0
  72. package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +216 -0
  73. package/cli/storybook-dist/_stories-src/RBInput/RBSideNumberInput.stories.tsx +90 -0
  74. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextArea.stories.tsx +45 -0
  75. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextInput.stories.tsx +50 -0
  76. package/cli/storybook-dist/_stories-src/RBInput/RBTextField.stories.tsx +102 -0
  77. package/cli/storybook-dist/_stories-src/RBJoyStick/RBJoyStick.stories.tsx +31 -0
  78. package/cli/storybook-dist/_stories-src/RBLight/RBLight.stories.tsx +36 -0
  79. package/cli/storybook-dist/_stories-src/RBPagination/RBPagination.stories.tsx +49 -0
  80. package/cli/storybook-dist/_stories-src/RBShadcn/RBCard/RBCard.stories.tsx +89 -0
  81. package/cli/storybook-dist/_stories-src/RBShadcn/RBCommand/RBCommand.stories.tsx +116 -0
  82. package/cli/storybook-dist/_stories-src/RBShadcn/RBDialog/RBDialog.stories.tsx +74 -0
  83. package/cli/storybook-dist/_stories-src/RBShadcn/RBDrawer/RBDrawer.stories.tsx +99 -0
  84. package/cli/storybook-dist/_stories-src/RBShadcn/RBDropdownMenu/RBDropdownMenu.stories.tsx +123 -0
  85. package/cli/storybook-dist/_stories-src/RBShadcn/RBMenubar/RBMenubar.stories.tsx +135 -0
  86. package/cli/storybook-dist/_stories-src/RBShadcn/RBPopover/RBPopover.stories.tsx +69 -0
  87. package/cli/storybook-dist/_stories-src/RBShadcn/RBResizable/RBResizable.stories.tsx +49 -0
  88. package/cli/storybook-dist/_stories-src/RBShadcn/RBSheet/RBSheet.stories.tsx +91 -0
  89. package/cli/storybook-dist/_stories-src/RBShadcn/RBSlider/RBSlider.stories.tsx +38 -0
  90. package/cli/storybook-dist/_stories-src/RBShadcn/RBSwitch/RBSwitch.stories.tsx +112 -0
  91. package/cli/storybook-dist/_stories-src/RBShadcn/RBTabs/RBTabs.stories.tsx +75 -0
  92. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggle/RBToggle.stories.tsx +51 -0
  93. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggleGroup/RBToggleGroup.stories.tsx +84 -0
  94. package/cli/storybook-dist/_stories-src/RBTab/RBChipTab.stories.tsx +102 -0
  95. package/cli/storybook-dist/_stories-src/RBTab/RBChipsBoxTab.stories.tsx +78 -0
  96. package/cli/storybook-dist/_stories-src/RBTab/RBTab.stories.tsx +57 -0
  97. package/cli/storybook-dist/_stories-src/RBTable/RBTable.stories.tsx +177 -0
  98. package/cli/storybook-dist/_stories-src/RBTable/RBTableRow.stories.tsx +64 -0
  99. package/cli/storybook-dist/_stories-src/RBToast/RBToast.stories.tsx +117 -0
  100. package/cli/storybook-dist/_stories-src/RBTooltip/RBTooltip.stories.tsx +50 -0
  101. package/cli/storybook-dist/_stories-src/loading/RBSpinner.stories.tsx +30 -0
  102. package/cli/storybook-dist/_stories-src/nx-button/NxButton.stories.tsx +54 -0
  103. package/cli/storybook-dist/_stories-src/nx-button/NxPointerHoldButton.stories.tsx +57 -0
  104. package/cli/storybook-dist/_stories-src/nx-icons/NxIcons.stories.tsx +48 -0
  105. package/cli/storybook-dist/_stories-src/nx-inputs/NxNumberInput.stories.tsx +103 -0
  106. package/cli/storybook-dist/_stories-src/nx-inputs/NxRangeInput.stories.tsx +27 -0
  107. package/cli/storybook-dist/_stories-src/nx-inputs/NxSelectBox.stories.tsx +67 -0
  108. package/cli/storybook-dist/_stories-src/nx-inputs/NxSlideRange.stories.tsx +45 -0
  109. package/cli/storybook-dist/_stories-src/nx-inputs/NxSwitch.stories.tsx +27 -0
  110. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextArea.stories.tsx +48 -0
  111. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextInput.stories.tsx +46 -0
  112. package/cli/storybook-dist/assets/Color-YHDXOIA2-pCKrjUal.js +1 -0
  113. package/cli/storybook-dist/assets/Docs-B-Y7I1GA.js +7 -0
  114. package/cli/storybook-dist/assets/Docs-BFJcE_VS.js +8 -0
  115. package/cli/storybook-dist/assets/Docs-B_QaTtz8.js +8 -0
  116. package/cli/storybook-dist/assets/Docs-CryDGmfh.js +20 -0
  117. package/cli/storybook-dist/assets/Docs-Cw2AVNKx.js +7 -0
  118. package/cli/storybook-dist/assets/Docs-Q6r1fBeL.js +12 -0
  119. package/cli/storybook-dist/assets/DocsRenderer-CFRXHY34-_OmZBCn8.js +2 -0
  120. package/cli/storybook-dist/assets/NxAvatar.stories-Ctf5G2FQ.js +13 -0
  121. package/cli/storybook-dist/assets/NxButton.stories-DeLObVSj.js +20 -0
  122. package/cli/storybook-dist/assets/NxIcons.stories-DeqrVzVs.js +3 -0
  123. package/cli/storybook-dist/assets/NxNumberInput.stories-LqoY0ed1.js +26 -0
  124. package/cli/storybook-dist/assets/NxPagination.stories-DI6bXuJM.js +11 -0
  125. package/cli/storybook-dist/assets/NxPointerHoldButton.stories-3HSV1XmN.js +14 -0
  126. package/cli/storybook-dist/assets/NxRangeInput.stories-1pjgQVaG.js +17 -0
  127. package/cli/storybook-dist/assets/NxSelectBox.stories-bcQPZEuF.js +17 -0
  128. package/cli/storybook-dist/assets/NxSlideRange.stories-BosX4axk.js +14 -0
  129. package/cli/storybook-dist/assets/NxSwitch.stories-DxV6dS33.js +11 -0
  130. package/cli/storybook-dist/assets/NxTextArea.stories-CO545-LP.js +11 -0
  131. package/cli/storybook-dist/assets/NxTextInput.stories-BP0npai1.js +11 -0
  132. package/cli/storybook-dist/assets/PretendardVariable-CJuje-Rk.woff2 +0 -0
  133. package/cli/storybook-dist/assets/RBAccordion.stories-fVR4Va2a.js +17 -0
  134. package/cli/storybook-dist/assets/RBAccordionGroup.stories-Du5bsckD.js +33 -0
  135. package/cli/storybook-dist/assets/RBAlert.stories-DNkMXUu6.js +17 -0
  136. package/cli/storybook-dist/assets/RBBadge.stories-C44pXt8D.js +1 -0
  137. package/cli/storybook-dist/assets/RBBattery.stories-DE2JHvfL.js +10 -0
  138. package/cli/storybook-dist/assets/RBCard.stories-CuIda7NI.js +45 -0
  139. package/cli/storybook-dist/assets/RBCheckBox.stories-D1qM1ayr.js +1 -0
  140. package/cli/storybook-dist/assets/RBCheckIconButton.stories-e4gkliBO.js +1 -0
  141. package/cli/storybook-dist/assets/RBCheckLabelButton.stories-CDzCtsMI.js +24 -0
  142. package/cli/storybook-dist/assets/RBChipTab.stories-DBrc_3Mp.js +21 -0
  143. package/cli/storybook-dist/assets/RBChipsBoxTab.stories-hlrsHjKY.js +1 -0
  144. package/cli/storybook-dist/assets/RBColorIcons.stories-BHUvpkdu.js +3 -0
  145. package/cli/storybook-dist/assets/RBCommand.stories-azAF41NP.js +73 -0
  146. package/cli/storybook-dist/assets/RBConfirm.stories-BMNjwbmT.js +20 -0
  147. package/cli/storybook-dist/assets/RBDateInput.stories-D0VezDzv.js +15 -0
  148. package/cli/storybook-dist/assets/RBDialog.stories-AOJ975Fy.js +38 -0
  149. package/cli/storybook-dist/assets/RBDivider.stories-NSvnj43Z.js +1 -0
  150. package/cli/storybook-dist/assets/RBDrawer.stories-Du0oTvD1.js +14 -0
  151. package/cli/storybook-dist/assets/RBDropdownMenu.stories-Hsxve4PW.js +86 -0
  152. package/cli/storybook-dist/assets/RBEmptyView.stories-BlwHx3zp.js +12 -0
  153. package/cli/storybook-dist/assets/RBIconButton.stories-DAPDY63x.js +1 -0
  154. package/cli/storybook-dist/assets/RBIcons.stories-W5ZUV2L1.js +3 -0
  155. package/cli/storybook-dist/assets/RBInlineLabelButton.stories-C99j0Y3g.js +1 -0
  156. package/cli/storybook-dist/assets/RBJoyStick.stories-Du0H1iAS.js +11 -0
  157. package/cli/storybook-dist/assets/RBLabelButton.stories-BlvHaU_Z.js +1 -0
  158. package/cli/storybook-dist/assets/RBLight.stories-BY6DKrre.js +1 -0
  159. package/cli/storybook-dist/assets/RBMenubar.stories-D2PEGpM9.js +3 -0
  160. package/cli/storybook-dist/assets/RBMultiRangeInput.stories-Bm2qSmjq.js +14 -0
  161. package/cli/storybook-dist/assets/RBPagination.stories-DDleJa5N.js +17 -0
  162. package/cli/storybook-dist/assets/RBPopover.stories-27YxPSFG.js +11 -0
  163. package/cli/storybook-dist/assets/RBPrompt.stories-BFaIOctr.js +20 -0
  164. package/cli/storybook-dist/assets/RBRadioBox.stories-BroDar0t.js +1 -0
  165. package/cli/storybook-dist/assets/RBRadioButton.stories-M3O-wX7V.js +11 -0
  166. package/cli/storybook-dist/assets/RBRangeDateInput.stories-CWlM_D5X.js +15 -0
  167. package/cli/storybook-dist/assets/RBResizable.stories-ui8X1qnc.js +20 -0
  168. package/cli/storybook-dist/assets/RBSelectBox.stories-C8T6cfA0.js +74 -0
  169. package/cli/storybook-dist/assets/RBSheet.stories-BCOo_Zcc.js +39 -0
  170. package/cli/storybook-dist/assets/RBSideNumberInput.stories-CU2_MVlJ.js +5 -0
  171. package/cli/storybook-dist/assets/RBSlider.stories-DgOp1TFc.js +5 -0
  172. package/cli/storybook-dist/assets/RBSpinner.stories-qLScY3zZ.js +14 -0
  173. package/cli/storybook-dist/assets/RBSwitch.stories-SXSY1QMb.js +65 -0
  174. package/cli/storybook-dist/assets/RBTab.stories-BzXXTsXP.js +1 -0
  175. package/cli/storybook-dist/assets/RBTable.stories-DHTeRNVk.js +62 -0
  176. package/cli/storybook-dist/assets/RBTableRow.stories-DRNRsd15.js +11 -0
  177. package/cli/storybook-dist/assets/RBTabs.stories-DP0noBLp.js +21 -0
  178. package/cli/storybook-dist/assets/RBTextArea.stories-TkaejfxJ.js +5 -0
  179. package/cli/storybook-dist/assets/RBTextField.stories-B6VpCGyt.js +45 -0
  180. package/cli/storybook-dist/assets/RBTextInput.stories-D3hZ08e-.js +5 -0
  181. package/cli/storybook-dist/assets/RBToast.stories-BQ2wS2U0.js +11 -0
  182. package/cli/storybook-dist/assets/RBToggle.stories-GVOjGqo3.js +12 -0
  183. package/cli/storybook-dist/assets/RBToggleGroup.stories-pzx0btOD.js +17 -0
  184. package/cli/storybook-dist/assets/RBTooltip.stories-jufB6fa1.js +9 -0
  185. package/cli/storybook-dist/assets/SideNumberInput-BYlmNHEw.js +1109 -0
  186. package/cli/storybook-dist/assets/SideNumberInput-SAkhydsn.css +21 -0
  187. package/cli/storybook-dist/assets/browserUtils-CQM9Za7e.js +1 -0
  188. package/cli/storybook-dist/assets/chunk-XP5HYGXS-BHXC9YeQ.js +1 -0
  189. package/cli/storybook-dist/assets/client-DTf7SId2.js +1 -0
  190. package/cli/storybook-dist/assets/entry-preview-CAkU-e0D.js +2 -0
  191. package/cli/storybook-dist/assets/entry-preview-docs-b8bpVBXy.js +46 -0
  192. package/cli/storybook-dist/assets/iframe-B-BmTsxV.js +211 -0
  193. package/cli/storybook-dist/assets/index-B4FGfHer.js +1 -0
  194. package/cli/storybook-dist/assets/index-BRq5TIsn.js +24 -0
  195. package/cli/storybook-dist/assets/index-CT3U1fyk.js +1 -0
  196. package/cli/storybook-dist/assets/index-ChsGqxH_.js +9 -0
  197. package/cli/storybook-dist/assets/index-CkB0GfaI.js +575 -0
  198. package/cli/storybook-dist/assets/index-ClAc6FBW.js +240 -0
  199. package/cli/storybook-dist/assets/index-DKFBREX_.js +1 -0
  200. package/cli/storybook-dist/assets/index-DgH-xKnr.js +11 -0
  201. package/cli/storybook-dist/assets/index-Dmy2JGbm.js +1 -0
  202. package/cli/storybook-dist/assets/index-DrFu-skq.js +6 -0
  203. package/cli/storybook-dist/assets/jsx-runtime-BTJTZTIL.js +9 -0
  204. package/cli/storybook-dist/assets/preview-B8lJiyuQ.js +34 -0
  205. package/cli/storybook-dist/assets/preview-BBWR9nbA.js +1 -0
  206. package/cli/storybook-dist/assets/preview-BVJwlGUF.js +1 -0
  207. package/cli/storybook-dist/assets/preview-BWzBA1C2.js +396 -0
  208. package/cli/storybook-dist/assets/preview-CVgpLj3b.js +1 -0
  209. package/cli/storybook-dist/assets/preview-CvbIS5ZJ.js +1 -0
  210. package/cli/storybook-dist/assets/preview-CznQwwbo.js +3 -0
  211. package/cli/storybook-dist/assets/preview-DD_OYowb.js +1 -0
  212. package/cli/storybook-dist/assets/preview-DGUiP6tS.js +7 -0
  213. package/cli/storybook-dist/assets/preview-DHQbi4pV.js +1 -0
  214. package/cli/storybook-dist/assets/preview-M77JRg1A.js +2 -0
  215. package/cli/storybook-dist/assets/preview-kTZPgP6D.css +1 -0
  216. package/cli/storybook-dist/assets/react-18-hB3smqWJ.js +1 -0
  217. package/cli/storybook-dist/assets/react-resizable-panels-HsFbxPbn.css +1 -0
  218. package/cli/storybook-dist/assets/react-resizable-panels.browser-DuxpZLkF.js +120 -0
  219. package/cli/storybook-dist/assets/test-utils-BQudk2xt.js +9 -0
  220. package/cli/storybook-dist/favicon.svg +1 -0
  221. package/cli/storybook-dist/iframe.html +666 -0
  222. package/cli/storybook-dist/index.html +190 -0
  223. package/cli/storybook-dist/index.json +1 -0
  224. package/cli/storybook-dist/nunito-sans-bold-italic.woff2 +0 -0
  225. package/cli/storybook-dist/nunito-sans-bold.woff2 +0 -0
  226. package/cli/storybook-dist/nunito-sans-italic.woff2 +0 -0
  227. package/cli/storybook-dist/nunito-sans-regular.woff2 +0 -0
  228. package/cli/storybook-dist/project.json +1 -0
  229. package/cli/storybook-dist/sb-addons/chromatic-com-storybook-10/manager-bundle.js +331 -0
  230. package/cli/storybook-dist/sb-addons/chromatic-com-storybook-10/manager-bundle.js.LEGAL.txt +51 -0
  231. package/cli/storybook-dist/sb-addons/essentials-actions-3/manager-bundle.js +3 -0
  232. package/cli/storybook-dist/sb-addons/essentials-backgrounds-5/manager-bundle.js +12 -0
  233. package/cli/storybook-dist/sb-addons/essentials-controls-2/manager-bundle.js +405 -0
  234. package/cli/storybook-dist/sb-addons/essentials-docs-4/manager-bundle.js +245 -0
  235. package/cli/storybook-dist/sb-addons/essentials-measure-8/manager-bundle.js +3 -0
  236. package/cli/storybook-dist/sb-addons/essentials-outline-9/manager-bundle.js +3 -0
  237. package/cli/storybook-dist/sb-addons/essentials-toolbars-7/manager-bundle.js +3 -0
  238. package/cli/storybook-dist/sb-addons/essentials-viewport-6/manager-bundle.js +3 -0
  239. package/cli/storybook-dist/sb-addons/links-1/manager-bundle.js +3 -0
  240. package/cli/storybook-dist/sb-addons/storybook-13/manager-bundle.js +3 -0
  241. package/cli/storybook-dist/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js +3 -0
  242. package/cli/storybook-dist/sb-addons/storybook-experimental-addon-test-12/manager-bundle.js +223 -0
  243. package/cli/storybook-dist/sb-addons/themes-11/manager-bundle.js +3 -0
  244. package/cli/storybook-dist/sb-common-assets/favicon.svg +1 -0
  245. package/cli/storybook-dist/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
  246. package/cli/storybook-dist/sb-common-assets/nunito-sans-bold.woff2 +0 -0
  247. package/cli/storybook-dist/sb-common-assets/nunito-sans-italic.woff2 +0 -0
  248. package/cli/storybook-dist/sb-common-assets/nunito-sans-regular.woff2 +0 -0
  249. package/cli/storybook-dist/sb-manager/globals-module-info.js +1052 -0
  250. package/cli/storybook-dist/sb-manager/globals-runtime.js +41775 -0
  251. package/cli/storybook-dist/sb-manager/globals.js +48 -0
  252. package/cli/storybook-dist/sb-manager/runtime.js +12048 -0
  253. package/cli/templates/common/README.md +1070 -83
  254. package/cli/templates/common/assets/icon.svg +44 -2
  255. package/cli/templates/common/manifest.json +14 -13
  256. package/cli/templates/common/package.json +11 -4
  257. package/cli/templates/common/py/main.py +125 -0
  258. package/cli/templates/common/requirements.txt +4 -3
  259. package/cli/templates/common/tsconfig.json +1 -1
  260. package/cli/templates/common/ui/content.tsx +216 -0
  261. package/cli/templates/common/ui/style.css +6 -0
  262. package/cli/templates/common/vite.config.ts +6 -25
  263. package/cli/templates/cpp/cpp/CMakeLists.txt +14 -0
  264. package/cli/templates/cpp/cpp/main.cpp +73 -0
  265. package/cli/templates/readme-cpp/README.md +1012 -0
  266. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +12 -1
  267. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +22 -0
  268. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -0
  269. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +41 -0
  270. package/cli/vite.d.mts +25 -0
  271. package/cli/vite.mjs +292 -0
  272. package/package.json +34 -7
  273. package/cli/src/pack.mjs +0 -61
  274. package/cli/src/preview.mjs +0 -91
  275. package/cli/templates/common/main.py +0 -114
  276. package/cli/templates/common/src/content.tsx +0 -33
@@ -0,0 +1,54 @@
1
+ // The plugin id a dev session runs under.
2
+ //
3
+ // Machines developing the same plugin against one robot register the same topics on its gateway, and
4
+ // requests (rb.action, external routes) then reach whichever backend registered last — with no error,
5
+ // so one developer's button runs the other's code. Against a remote robot each machine therefore runs
6
+ // as <manifest id>.dev-<user>-<random>. The random part is kept in ~/.muscat-pk/dev-id: the id stays the
7
+ // same across runs (stable external URLs) and differs between machines even when host or user names match.
8
+ // --keep-id runs under the manifest id (program execute, external clients that use the real id); then only
9
+ // one machine may be connected to that robot.
10
+ import { randomBytes } from "node:crypto";
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { homedir, userInfo } from "node:os";
13
+ import { dirname, resolve } from "node:path";
14
+ import { PLUGIN_ID_RE } from "./manifest.mjs";
15
+ import { log } from "./util.mjs";
16
+ import { isRemoteRobot } from "./zenohDev.mjs";
17
+
18
+ const DEV_ID_FILE = resolve(homedir(), ".muscat-pk", "dev-id");
19
+ const ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
20
+ // `dev` decides once; its dev:back child gets no CLI args and reads the id from here.
21
+ const ENV = "MUSCAT_PK_DEV_PLUGIN_ID";
22
+
23
+ function machineToken() {
24
+ const saved = existsSync(DEV_ID_FILE) ? readFileSync(DEV_ID_FILE, "utf8").trim() : "";
25
+ if (/^[a-z0-9]{6}$/.test(saved)) return saved;
26
+ const token = [...randomBytes(6)].map((b) => ALPHABET[b % ALPHABET.length]).join("");
27
+ mkdirSync(dirname(DEV_ID_FILE), { recursive: true });
28
+ writeFileSync(DEV_ID_FILE, `${token}\n`);
29
+ return token;
30
+ }
31
+
32
+ function userName() {
33
+ try {
34
+ return userInfo().username.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 12);
35
+ } catch {
36
+ return "";
37
+ }
38
+ }
39
+
40
+ /** The id for this dev session (UI, backend, ROS namespace and external routes all use it). */
41
+ export function devPluginId(manifestId, { robotIp, args = [] }) {
42
+ if (process.env[ENV]) return process.env[ENV];
43
+ let id = manifestId;
44
+ if (manifestId && isRemoteRobot(robotIp) && !args.includes("--keep-id")) {
45
+ const candidate = `${manifestId}.${["dev", userName(), machineToken()].filter(Boolean).join("-")}`;
46
+ if (PLUGIN_ID_RE.test(candidate)) id = candidate;
47
+ }
48
+ if (id !== manifestId) {
49
+ log.step(`dev plugin id ${id} — this machine's own on the shared robot (--keep-id runs as ${manifestId})`);
50
+ log.step(`external routes: http://${robotIp}:3000/common/plugin/${id}/<path>`);
51
+ }
52
+ if (id) process.env[ENV] = id;
53
+ return id;
54
+ }
@@ -0,0 +1,67 @@
1
+ // One Muscat plugin dev environment per PC: the backend container, the local Muscat runtime (and
2
+ // the plugin ports it publishes) and the host_agent relay port are shared. A new dev / dev:ui /
3
+ // dev:back offers to stop the running one and take over.
4
+ import { spawnSync } from "node:child_process";
5
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { dirname, resolve } from "node:path";
8
+ import { log, prompt } from "./util.mjs";
9
+
10
+ export const DEV_CONTAINER = "muscat-pk-dev-run";
11
+ // Per-runtime names from older kits.
12
+ const OLD_CONTAINERS = ["muscat-pk-python-dev-run", "muscat-pk-ros2-dev-run", "muscat-pk-cpp-dev-run"];
13
+ const LOCK = resolve(homedir(), ".muscat-pk", "dev-session.json");
14
+
15
+ export function stopDevBackContainers() {
16
+ for (const c of [DEV_CONTAINER, ...OLD_CONTAINERS]) spawnSync("docker", ["rm", "-f", c], { stdio: "ignore" });
17
+ }
18
+
19
+ const alive = (pid) => {
20
+ try {
21
+ process.kill(pid, 0);
22
+ return true;
23
+ } catch (e) {
24
+ return e.code === "EPERM";
25
+ }
26
+ };
27
+
28
+ function runningSession() {
29
+ try {
30
+ const s = JSON.parse(readFileSync(LOCK, "utf8"));
31
+ return s.pid !== process.pid && alive(s.pid) ? s : null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ async function stopSession(pid) {
38
+ // SIGINT runs its own cleanup (UI, host_agent, containers). Windows has no signals — kill the tree.
39
+ if (process.platform === "win32") spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
40
+ else process.kill(pid, "SIGINT");
41
+ for (let i = 0; i < 60 && alive(pid); i++) await new Promise((r) => setTimeout(r, 500));
42
+ if (alive(pid)) process.kill(pid, "SIGKILL");
43
+ }
44
+
45
+ export async function claimDevSession(root, command) {
46
+ if (process.env.MUSCAT_PK_MANAGED_DEV === "1") return; // `dev` claimed it for its dev:ui / dev:back
47
+ const other = runningSession();
48
+ if (other?.pid === process.ppid) return; // `dev`'s own dev:back child
49
+ if (other) {
50
+ log.warn(`A Muscat plugin dev environment is already running: muscat-pk ${other.command} in ${other.root} (pid ${other.pid}).`);
51
+ log.warn("Only one can run per PC — they share the backend container, the local Muscat and its ports.");
52
+ const answer = (await prompt(" Stop it and start this one? [y/N]: ")).toLowerCase();
53
+ if (answer !== "y" && answer !== "yes") throw new Error("another plugin dev environment is running — stop it first.");
54
+ await stopSession(other.pid);
55
+ stopDevBackContainers(); // whatever it could not clean up
56
+ log.ok("stopped the other plugin dev environment");
57
+ }
58
+ mkdirSync(dirname(LOCK), { recursive: true });
59
+ writeFileSync(LOCK, JSON.stringify({ pid: process.pid, root, command }));
60
+ process.on("exit", () => {
61
+ try {
62
+ if (JSON.parse(readFileSync(LOCK, "utf8")).pid === process.pid) rmSync(LOCK, { force: true });
63
+ } catch {
64
+ /* already gone */
65
+ }
66
+ });
67
+ }
package/cli/src/devUi.mjs CHANGED
@@ -1,38 +1,62 @@
1
- // muscat-pk dev:ui — preview the plugin inside a mini-Muscat (preview-host).
1
+
2
2
  import { spawn, spawnSync } from "node:child_process";
3
+ import { createServer as createNetServer } from "node:net";
3
4
  import { createRequire } from "node:module";
4
- import { existsSync, mkdirSync } from "node:fs";
5
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, watch, writeFileSync } from "node:fs";
5
6
  import { homedir } from "node:os";
6
7
  import { dirname, resolve } from "node:path";
7
- import { buildPlugin } from "./build.mjs";
8
- import { checkKitUpdate, findPluginRoot, hasBin, kitRoot, log } from "./util.mjs";
8
+ import { fileURLToPath } from "node:url";
9
+ import { checkKitUpdate, ensureDevDir, findPluginRoot, hasBin, kitRoot, log, openBrowser, parseArgs, prompt } from "./util.mjs";
10
+ import { devPluginId } from "./devId.mjs";
11
+ import { readManifest } from "./manifest.mjs";
12
+ import { ensureLocalMuscat, GATEWAY_PORT, localFullMuscat, robotHasMuscat } from "./muscatLocal.mjs";
13
+ import { isRemoteRobot } from "./zenohDev.mjs";
14
+ import { claimDevSession } from "./devSession.mjs";
15
+
16
+
17
+ const DEV_PLUGIN_PORT = 4181;
18
+
19
+ function getFreePort(preferred) {
20
+ return new Promise((res) => {
21
+ const srv = createNetServer();
22
+ srv.once("error", () => {
23
+ const s2 = createNetServer();
24
+ s2.listen(0, () => {
25
+ const { port } = s2.address();
26
+ s2.close(() => res(port));
27
+ });
28
+ });
29
+ srv.listen(preferred, () => srv.close(() => res(preferred)));
30
+ });
31
+ }
9
32
 
10
33
  const PREVIEW_PKG = "@rainbow-robotics/preview-host";
11
34
  const CACHE_DIR = resolve(homedir(), ".muscat-pk");
12
35
  const CACHE_ROOT = resolve(CACHE_DIR, "node_modules", "@rainbow-robotics", "preview-host");
13
36
 
14
- /**
15
- * Resolve preview-host root in order:
16
- * 1. Standard require (npm/yarn global install)
17
- * 2. Monorepo sibling (dev workspace)
18
- * 3. ~/.muscat-pk cache (previous auto-install)
19
- * 4. Auto-install to ~/.muscat-pk via npm (binary users who have Node.js)
20
- */
37
+
38
+
39
+ function findNodeBin() {
40
+ if (typeof Bun === "undefined") return process.execPath;
41
+ for (const c of ["node", "node20", "node18"]) {
42
+ if (hasBin(c)) return c;
43
+ }
44
+ return null;
45
+ }
46
+
21
47
  function findPreviewHostRoot() {
22
- // 1. npm/yarn 설치 경로
23
48
  try {
24
49
  const req = createRequire(import.meta.url);
25
50
  return dirname(req.resolve(`${PREVIEW_PKG}/package.json`));
26
- } catch {}
51
+ } catch {
52
+ console.log("Try the sibling and cached package locations below.")
53
+ }
27
54
 
28
- // 2. 모노레포 sibling
29
55
  const sibling = resolve(kitRoot, "../preview-host");
30
56
  if (existsSync(resolve(sibling, "package.json"))) return sibling;
31
57
 
32
- // 3. 캐시
33
58
  if (existsSync(resolve(CACHE_ROOT, "package.json"))) return CACHE_ROOT;
34
59
 
35
- // 4. npm 자동 설치 → 캐시
36
60
  if (!hasBin("npm")) {
37
61
  throw new Error(
38
62
  `preview-host를 찾을 수 없습니다.\n` +
@@ -53,32 +77,201 @@ function findPreviewHostRoot() {
53
77
  return CACHE_ROOT;
54
78
  }
55
79
 
80
+ function syncAssets(pluginRoot, outDir) {
81
+ const assetsSrc = resolve(pluginRoot, "assets");
82
+ if (existsSync(assetsSrc)) cpSync(assetsSrc, resolve(outDir, "assets"), { recursive: true });
83
+ }
84
+
85
+
86
+ // "(dev)" after every name the plugin shows, so a dev session is never mistaken for an installed copy.
87
+ const devLabel = (v) =>
88
+ typeof v === "string"
89
+ ? `${v} (dev)`
90
+ : v && typeof v === "object"
91
+ ? Object.fromEntries(Object.entries(v).map(([lang, s]) => [lang, `${s} (dev)`]))
92
+ : v;
93
+
94
+ // preview-host takes the plugin id from this copy, so the UI talks under the session's dev id.
95
+ function stageDevAssets(pluginRoot, pluginId) {
96
+ const outDir = ensureDevDir(pluginRoot);
97
+ const manifestSrc = resolve(pluginRoot, "manifest.json");
98
+ if (existsSync(manifestSrc)) {
99
+ const manifest = JSON.parse(readFileSync(manifestSrc, "utf8"));
100
+ if (pluginId) manifest.id = pluginId;
101
+ manifest.name = devLabel(manifest.name);
102
+ for (const c of manifest.contributes ?? []) {
103
+ c.label = devLabel(c.label);
104
+ if (c.commandInfo?.name) c.commandInfo.name = devLabel(c.commandInfo.name);
105
+ }
106
+ writeFileSync(resolve(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
107
+ }
108
+ syncAssets(pluginRoot, outDir);
109
+ return outDir;
110
+ }
111
+
112
+ function watchAssets(pluginRoot, outDir) {
113
+ const assetsSrc = resolve(pluginRoot, "assets");
114
+ if (!existsSync(assetsSrc)) return;
115
+ watch(assetsSrc, { recursive: true }, () => syncAssets(pluginRoot, outDir));
116
+ }
117
+
118
+
119
+ function findViteBin(pluginRoot) {
120
+ try {
121
+ const req = createRequire(resolve(pluginRoot, "package.json"));
122
+ const vitePkg = req.resolve("vite/package.json");
123
+ return resolve(dirname(vitePkg), "bin", "vite.js");
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+
130
+ function spawnPluginDevServer(pluginRoot, node, port, readyFile, sharedRuntimeBase) {
131
+ if (!node || !findViteBin(pluginRoot)) return null;
132
+ const runner = fileURLToPath(new URL("./pluginDevServer.mjs", import.meta.url));
133
+
134
+ rmSync(resolve(pluginRoot, "node_modules", ".vite"), { recursive: true, force: true });
135
+
136
+ return spawn(node, [runner, pluginRoot, String(port), readyFile], {
137
+ cwd: pluginRoot,
138
+ stdio: "inherit",
139
+ env: {
140
+ ...process.env,
141
+ VITE_PLUGIN_PREVIEW: "true",
142
+ MUSCAT_PLUGIN_SHARED_RUNTIME_BASE: sharedRuntimeBase,
143
+ },
144
+ });
145
+ }
146
+
147
+ // The ready file is the signal and an exit the failure; the timeout only guards a hang. A cold vite start
148
+ // takes ~11s on Windows and more while `dev`'s first docker build loads the machine, so a tight limit
149
+ // stopped a healthy `dev`.
150
+ function waitForPluginDevPort(readyFile, proc, timeoutMs = 120_000) {
151
+ const start = Date.now();
152
+ const slow = setTimeout(() => log.info("plugin dev server (vite) is still starting…"), 15_000);
153
+ return new Promise((res, rej) => {
154
+ let exited = false;
155
+ proc.once("exit", (code) => {
156
+ exited = true;
157
+ rej(new Error(`plugin dev server exited before ready (code ${code ?? 0})`));
158
+ });
159
+
160
+ const tick = () => {
161
+ if (existsSync(readyFile)) {
162
+ try {
163
+ const { port } = JSON.parse(readFileSync(readyFile, "utf8"));
164
+ if (Number.isInteger(port)) return res(port);
165
+ } catch {
166
+ console.log("Keep polling until the ready file contains valid JSON.")
167
+ }
168
+ }
169
+ if (Date.now() - start > timeoutMs)
170
+ return rej(new Error("plugin dev server did not become ready in time"));
171
+ if (!exited) setTimeout(tick, 100);
172
+ };
173
+ tick();
174
+ }).finally(() => clearTimeout(slow));
175
+ }
176
+
56
177
  export async function runDevUi(args = []) {
57
- const portIdx = args.indexOf("--port");
58
- const port = portIdx !== -1 ? args[portIdx + 1] : "4180";
178
+ const parsed = parseArgs(args);
179
+ const requestedPort = Number(parsed.port ?? "4180");
180
+ await claimDevSession(findPluginRoot(), "dev:ui");
181
+ const robotIp = parsed["robot-ip"] ?? process.env.MUSCAT_ROBOT_HOST ?? await prompt(" Robot IP (skip if no robot): ");
59
182
 
60
183
  await checkKitUpdate();
61
- const { distDir } = await buildPlugin();
184
+ const pluginRoot = findPluginRoot();
185
+ const outDir = stageDevAssets(pluginRoot, devPluginId(readManifest(pluginRoot).id, { robotIp, args }));
186
+ const port = await getFreePort(requestedPort);
187
+
188
+ // No robot: the bundle talks to localhost:3000, so put a Muscat there (`dev` already did).
189
+ if (!isRemoteRobot(robotIp) && process.env.MUSCAT_PK_MANAGED_DEV !== "1") {
190
+ await ensureLocalMuscat().catch((e) => log.warn(`local Muscat: ${e.message}`));
191
+ }
192
+
193
+ watchAssets(pluginRoot, outDir);
194
+
195
+ const node = findNodeBin();
196
+ const readyFile = resolve(outDir, "plugin-dev-port.json");
197
+ rmSync(readyFile, { force: true });
198
+ const sharedRuntimeBase = `http://localhost:${port}/plugin-shared`;
199
+ // Registered before the first spawn: a Ctrl+C that arrived during `dev`'s synchronous backend build
200
+ // is handled only now, and a process started after it never got the signal itself.
201
+ const procs = [];
202
+ const stopProcs = () => {
203
+ for (const p of procs) if (p.exitCode === null) p.kill();
204
+ };
205
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"])
206
+ process.once(signal, () => {
207
+ stopProcs();
208
+ process.exit(130);
209
+ });
210
+ const viteProc = spawnPluginDevServer(
211
+ pluginRoot,
212
+ node,
213
+ DEV_PLUGIN_PORT,
214
+ readyFile,
215
+ sharedRuntimeBase,
216
+ );
217
+ if (viteProc) procs.push(viteProc);
218
+ let devPort = DEV_PLUGIN_PORT;
219
+
220
+ if (!viteProc) {
221
+ log.warn("vite not found — UI will not load. Run `npm install` in the plugin project.");
222
+ } else {
223
+ devPort = await waitForPluginDevPort(readyFile, viteProc);
224
+ viteProc.on("exit", (code) => {
225
+ if (code) log.warn(`plugin dev server exited (code ${code}) — UI won't load (is :${devPort} free?).`);
226
+ });
227
+ }
228
+
229
+ // muscat-dev, or our own Muscat with a simulator (`dev --sim`): a full local Muscat to show.
230
+ const localDevMuscat = !!process.env.MUSCAT_PK_SIM || (await localFullMuscat(robotIp));
231
+ const uiHost = isRemoteRobot(robotIp) ? robotIp : localDevMuscat ? "localhost" : "";
62
232
 
63
233
  const previewRoot = findPreviewHostRoot();
64
234
  const serveEntry = resolve(previewRoot, "serve.mjs");
65
235
  if (!existsSync(resolve(previewRoot, "dist", "index.html"))) {
66
236
  log.step("building preview-host bundle (first run)…");
67
237
  await new Promise((res, rej) => {
68
- const p = spawn(process.execPath, [resolve(previewRoot, "build.mjs")], {
238
+ const p = spawn(node ?? process.execPath, [resolve(previewRoot, "build.mjs")], {
69
239
  stdio: "inherit",
240
+ env: { ...process.env, VITE_PLUGIN_PREVIEW: "true" },
70
241
  });
242
+ procs.push(p);
71
243
  p.on("exit", (c) => (c === 0 ? res() : rej(new Error("preview-host build failed"))));
72
244
  });
73
245
  }
74
246
 
75
- log.step(`starting preview-host on :${port}`);
76
247
  const child = spawn(
77
- process.execPath,
78
- [serveEntry, "--plugin", distDir, "--port", String(port)],
79
- { stdio: "inherit" },
248
+ node ?? process.execPath,
249
+ [
250
+ serveEntry,
251
+ "--plugin", outDir,
252
+ "--port", String(port),
253
+ "--dev-server-url", `http://localhost:${devPort}`,
254
+ // A remote robot (or a local muscat-dev) turns the bundle into that Muscat's UI; otherwise
255
+ // it stays the mock preview.
256
+ ...(uiHost ? ["--robot-host", uiHost] : []),
257
+ // The dev plugin's socket goes to that Muscat's gateway; otherwise to the local runtime.
258
+ ...((await robotHasMuscat(robotIp)) || localDevMuscat ? ["--socket-url", `ws://${uiHost}:${GATEWAY_PORT}`] : []),
259
+ ],
260
+ // preview-host only prints its address and paths; the "UI →" line below says what matters.
261
+ { stdio: ["inherit", "ignore", "inherit"] },
80
262
  );
81
- child.on("exit", (code) => process.exit(code ?? 0));
263
+ procs.push(child);
264
+
265
+ for (let i = 0; i < 50 && !(await fetch(`http://localhost:${port}/`).then(() => true, () => false)); i++)
266
+ await new Promise((r) => setTimeout(r, 200));
267
+ log.ok(`UI → http://localhost:${port}`);
268
+ // Only for a person at a terminal — not CI, scripts or `--no-open`.
269
+ if (process.stdout.isTTY && !process.env.CI && !parsed["no-open"]) openBrowser(`http://localhost:${port}`);
270
+
271
+ child.on("exit", (code) => {
272
+ stopProcs();
273
+ process.exit(code ?? 0);
274
+ });
82
275
  }
83
276
 
84
277
  export { findPluginRoot, findPreviewHostRoot };
@@ -20,20 +20,37 @@ async function buildWithBun(options) {
20
20
  }
21
21
  }
22
22
 
23
+ async function buildWithWasm(options) {
24
+ const wasm = await import("esbuild-wasm");
25
+ return wasm.build(options);
26
+ }
27
+
28
+ function isNativeMissing(err) {
29
+ const msg = String(err?.message || err?.code || err || "");
30
+ return (
31
+ msg.includes("another platform") ||
32
+ msg.includes("Cannot find module") ||
33
+ msg.includes("Cannot find package") ||
34
+ msg.includes("not found") ||
35
+ msg.includes("No such file") ||
36
+ msg.includes("ENOENT") ||
37
+ msg.includes("MODULE_NOT_FOUND")
38
+ );
39
+ }
40
+
23
41
  export async function buildWithFallback(options) {
24
42
  if (typeof Bun !== "undefined") return buildWithBun(options);
25
43
 
26
- const native = await import("esbuild");
44
+ // Try native esbuild (faster, no extra setup). Falls back to wasm when:
45
+ // - esbuild was not installed (removed from hard dependencies to avoid
46
+ // postinstall script blocking npm i -g on Windows)
47
+ // - binary was built for a different platform
27
48
  try {
49
+ const native = await import("esbuild");
28
50
  return await native.build(options);
29
- } catch (error) {
30
- const message = String(error?.message || error);
31
- if (!message.includes("another platform")) {
32
- throw error;
33
- }
34
-
35
- const wasm = await import("esbuild-wasm");
36
- console.warn("[muscat-pk] native esbuild binary mismatch; falling back to esbuild-wasm");
37
- return wasm.build(options);
51
+ } catch (err) {
52
+ if (!isNativeMissing(err)) throw err;
38
53
  }
54
+
55
+ return buildWithWasm(options);
39
56
  }
@@ -0,0 +1,223 @@
1
+ """Host-side Zenoh byte-proxy for Docker Desktop containers (Mac/WSL remote dev).
2
+
3
+ Joins the robot's Zenoh mesh with eclipse-zenoh (no rainbow SDK) and relays
4
+ {query, put, subscribe} bytes over TCP to the dev container.
5
+
6
+ Env: MUSCAT_ROBOT_HOST, MUSCAT_ROBOT_TOKEN, MUSCAT_ZENOH_ADVERTISE_HOST,
7
+ MUSCAT_ZENOH_PORT (robot-facing), MUSCAT_PROXY_PORT (container-facing).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import contextlib
13
+ import json
14
+ import os
15
+ import signal
16
+ import socket
17
+ import struct
18
+ import sys
19
+ import threading
20
+ import urllib.error
21
+ import urllib.request
22
+
23
+ import zenoh
24
+
25
+ PEER_TTL = 30
26
+ QUERY_TARGETS = {0: zenoh.QueryTarget.BEST_MATCHING, 1: zenoh.QueryTarget.ALL, 2: zenoh.QueryTarget.ALL_COMPLETE}
27
+
28
+
29
+ def _send_frame(conn: socket.socket, lock: threading.Lock, obj: dict) -> None:
30
+ body = json.dumps(obj).encode()
31
+ with lock:
32
+ conn.sendall(struct.pack(">I", len(body)) + body)
33
+
34
+
35
+ def _recv_frame(conn: socket.socket) -> dict | None:
36
+ hdr = b""
37
+ while len(hdr) < 4:
38
+ chunk = conn.recv(4 - len(hdr))
39
+ if not chunk:
40
+ return None
41
+ hdr += chunk
42
+ n = struct.unpack(">I", hdr)[0]
43
+ buf = b""
44
+ while len(buf) < n:
45
+ chunk = conn.recv(n - len(buf))
46
+ if not chunk:
47
+ return None
48
+ buf += chunk
49
+ return json.loads(buf.decode())
50
+
51
+
52
+ def _att(sample) -> str:
53
+ return sample.attachment.to_string() if sample.attachment is not None else ""
54
+
55
+
56
+ def _query(session: zenoh.Session, req: dict) -> list[dict]:
57
+ payload = bytes.fromhex(req["payload_hex"]) if req.get("payload_hex") else None
58
+ replies = session.get(
59
+ req["keyexpr"],
60
+ payload=payload,
61
+ timeout=req.get("timeout_ms", 2000) / 1000,
62
+ target=QUERY_TARGETS.get(req.get("target", 0), zenoh.QueryTarget.BEST_MATCHING),
63
+ )
64
+ out = []
65
+ for reply in replies:
66
+ if reply.ok is not None:
67
+ s = reply.ok
68
+ out.append({"key": str(s.key_expr), "payload_hex": s.payload.to_bytes().hex(),
69
+ "att": _att(s), "encoding": str(s.encoding), "is_error": False})
70
+ else:
71
+ e = reply.err
72
+ out.append({"key": req["keyexpr"], "payload_hex": e.payload.to_bytes().hex(),
73
+ "att": "", "encoding": str(e.encoding), "is_error": True})
74
+ return out
75
+
76
+
77
+ def _handle_conn(conn: socket.socket, session: zenoh.Session) -> None:
78
+ send_lock = threading.Lock()
79
+ subs: dict[str, zenoh.Subscriber] = {}
80
+ try:
81
+ while True:
82
+ req = _recv_frame(conn)
83
+ if req is None:
84
+ return
85
+ op = req.get("type")
86
+ rid = req.get("id")
87
+
88
+ try:
89
+ if op == "query":
90
+ out = {"type": "query_resp", "id": rid, "ok": True, "replies": _query(session, req)}
91
+
92
+ elif op == "put":
93
+ payload = bytes.fromhex(req["payload_hex"]) if req.get("payload_hex") else b""
94
+ session.put(req["keyexpr"], payload)
95
+ out = {"type": "put_resp", "id": rid, "ok": True}
96
+
97
+ elif op == "subscribe":
98
+ keyexpr = req["keyexpr"]
99
+
100
+ def on_sample(s, _ke: str = keyexpr) -> None:
101
+ _send_frame(conn, send_lock, {
102
+ "type": "sample", "keyexpr": _ke, "key": str(s.key_expr),
103
+ "payload_hex": s.payload.to_bytes().hex(), "att": _att(s),
104
+ })
105
+
106
+ old = subs.pop(keyexpr, None)
107
+ if old is not None:
108
+ old.undeclare()
109
+ subs[keyexpr] = session.declare_subscriber(keyexpr, on_sample)
110
+ out = {"type": "subscribe_resp", "id": rid, "ok": True}
111
+
112
+ elif op == "unsubscribe":
113
+ sub = subs.pop(req["keyexpr"], None)
114
+ if sub is not None:
115
+ with contextlib.suppress(Exception):
116
+ sub.undeclare()
117
+ out = {"type": "unsubscribe_resp", "id": rid, "ok": True}
118
+
119
+ else:
120
+ out = {"type": "error", "error": f"unknown op {op!r}"}
121
+ except Exception as exc:
122
+ out = {"type": f"{op}_resp", "id": rid, "ok": False, "error": f"{type(exc).__name__}: {exc}"}
123
+ _send_frame(conn, send_lock, out)
124
+ except (ConnectionResetError, BrokenPipeError, OSError):
125
+ pass
126
+ finally:
127
+ for sub in subs.values():
128
+ with contextlib.suppress(Exception):
129
+ sub.undeclare()
130
+ conn.close()
131
+
132
+
133
+ class Peer:
134
+ """Registers this host with the robot's common: POST /zenoh/peer, heartbeat, DELETE."""
135
+
136
+ def __init__(self, host: str, token: str, ip: str, port: int) -> None:
137
+ self.base = f"http://{host}:8000"
138
+ self.headers = {"Content-Type": "application/json"}
139
+ if token:
140
+ self.headers["Authorization"] = f"Bearer {token}"
141
+ self.body = {"ip": ip, "port": port, "ttl": PEER_TTL}
142
+ self.peer_id: str | None = None
143
+ self.stop = threading.Event()
144
+
145
+ def _http(self, method: str, path: str, body: dict | None = None) -> dict:
146
+ data = json.dumps(body).encode() if body is not None else None
147
+ req = urllib.request.Request(self.base + path, data=data, headers=self.headers, method=method)
148
+ with urllib.request.urlopen(req, timeout=10) as resp:
149
+ raw = resp.read()
150
+ return json.loads(raw) if raw else {}
151
+
152
+ def register(self) -> None:
153
+ self.peer_id = self._http("POST", "/zenoh/peer", self.body)["peer_id"]
154
+ print(f"[host-agent] registered with {self.base} as zenoh peer {self.peer_id}", flush=True)
155
+
156
+ def maintain(self) -> None:
157
+ while True:
158
+ wait = PEER_TTL / 2 if self.peer_id else 3
159
+ if self.stop.wait(wait):
160
+ return
161
+ try:
162
+ if self.peer_id is None:
163
+ self.register()
164
+ elif not self._http("POST", f"/zenoh/peer/{self.peer_id}/heartbeat", {"ttl": PEER_TTL}).get("ok"):
165
+ print(f"[host-agent] heartbeat rejected for {self.peer_id}, re-registering", flush=True)
166
+ self.peer_id = None
167
+ except Exception as exc:
168
+ print(f"[host-agent] peer maintenance failed ({exc}); retrying", flush=True)
169
+ if self.peer_id is not None and isinstance(exc, urllib.error.HTTPError) and exc.code == 404:
170
+ self.peer_id = None
171
+
172
+ def close(self) -> None:
173
+ self.stop.set()
174
+ if self.peer_id:
175
+ with contextlib.suppress(Exception):
176
+ self._http("DELETE", f"/zenoh/peer/{self.peer_id}")
177
+
178
+
179
+ def main() -> None:
180
+ # dev stops us with SIGTERM; exit via `finally` to deregister.
181
+ signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
182
+ host = os.environ["MUSCAT_ROBOT_HOST"]
183
+ token = os.environ.get("MUSCAT_ROBOT_TOKEN", "")
184
+ advertise = os.environ["MUSCAT_ZENOH_ADVERTISE_HOST"]
185
+ zenoh_port = int(os.environ["MUSCAT_ZENOH_PORT"])
186
+ proxy_port = int(os.environ["MUSCAT_PROXY_PORT"])
187
+
188
+ # Listen only; the robot dials in after registration. Router mode so a C++ SDK dev container
189
+ # (native Zenoh session, no byte shim) can connect here and be routed to the robot.
190
+ conf = zenoh.Config()
191
+ conf.insert_json5("mode", '"peer"')
192
+ conf.insert_json5("listen/endpoints", json.dumps([f"tcp/0.0.0.0:{zenoh_port}"]))
193
+ conf.insert_json5("scouting/multicast/enabled", "false")
194
+ session = zenoh.open(conf)
195
+
196
+ peer = Peer(host, token, advertise, zenoh_port)
197
+ try:
198
+ peer.register()
199
+ except Exception as exc:
200
+ print(f"[host-agent] registration with {peer.base} failed, retrying in background: {exc}", flush=True)
201
+ threading.Thread(target=peer.maintain, daemon=True, name="zenoh-peer").start()
202
+ print(f"[host-agent] zenoh peer listening :{zenoh_port} (advertised as {advertise})", flush=True)
203
+
204
+ srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
205
+ srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
206
+ srv.bind(("0.0.0.0", proxy_port))
207
+ srv.listen(32)
208
+ print(f"[host-agent] byte-proxy listening :{proxy_port} (container connects via host.docker.internal)", flush=True)
209
+
210
+ try:
211
+ while True:
212
+ conn, _addr = srv.accept()
213
+ threading.Thread(target=_handle_conn, args=(conn, session), daemon=True).start()
214
+ except KeyboardInterrupt:
215
+ pass
216
+ finally:
217
+ peer.close()
218
+ with contextlib.suppress(Exception):
219
+ session.close()
220
+
221
+
222
+ if __name__ == "__main__":
223
+ main()