@rainbow-robotics/muscat-plugin-kit 1.10.1-dev.1 → 1.10.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 (205) hide show
  1. package/README.md +282 -282
  2. package/cli/README.md +274 -274
  3. package/cli/bin/create-muscat-plugin.mjs +8 -8
  4. package/cli/bin/muscat-pk.mjs +155 -155
  5. package/cli/bundle-templates.mjs +31 -31
  6. package/cli/docker/Dockerfile.cpp +28 -28
  7. package/cli/docker/Dockerfile.dev +30 -30
  8. package/cli/docker/Dockerfile.python +21 -21
  9. package/cli/m5-e2e.mjs +69 -69
  10. package/cli/prepack.mjs +10 -10
  11. package/cli/src/ai-rules.mjs +70 -70
  12. package/cli/src/build.mjs +447 -447
  13. package/cli/src/check-package.mjs +13 -13
  14. package/cli/src/completion.mjs +166 -166
  15. package/cli/src/cpp.mjs +78 -78
  16. package/cli/src/cppIde.mjs +75 -75
  17. package/cli/src/dev.mjs +165 -165
  18. package/cli/src/devBack.mjs +734 -727
  19. package/cli/src/devUi.mjs +247 -243
  20. package/cli/src/esbuild.mjs +56 -56
  21. package/cli/src/host_agent.py +223 -223
  22. package/cli/src/manifest.mjs +287 -287
  23. package/cli/src/mcp.mjs +1071 -1071
  24. package/cli/src/muscatLocal.mjs +209 -209
  25. package/cli/src/new.mjs +343 -343
  26. package/cli/src/pluginDevServer.mjs +111 -111
  27. package/cli/src/ports.mjs +193 -193
  28. package/cli/src/python.mjs +112 -112
  29. package/cli/src/ros2.mjs +109 -109
  30. package/cli/src/sdk.mjs +117 -117
  31. package/cli/src/storybook.mjs +168 -168
  32. package/cli/src/type.mjs +141 -141
  33. package/cli/src/uninstall.mjs +81 -81
  34. package/cli/src/update.mjs +295 -295
  35. package/cli/src/util.mjs +441 -441
  36. package/cli/src/zenohDev.mjs +83 -81
  37. package/cli/storybook-dist/_design-tokens.css +4957 -4957
  38. package/cli/storybook-dist/_stories-src/NxAvatar.stories.tsx +27 -27
  39. package/cli/storybook-dist/_stories-src/NxPagination.stories.tsx +28 -28
  40. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordion.stories.tsx +46 -46
  41. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordionGroup.stories.tsx +73 -73
  42. package/cli/storybook-dist/_stories-src/RBBadge/RBBadge.stories.tsx +93 -93
  43. package/cli/storybook-dist/_stories-src/RBBattery/RBBattery.stories.tsx +74 -74
  44. package/cli/storybook-dist/_stories-src/RBButton/RBCheckIconButton.stories.tsx +28 -28
  45. package/cli/storybook-dist/_stories-src/RBButton/RBCheckLabelButton.stories.tsx +84 -84
  46. package/cli/storybook-dist/_stories-src/RBButton/RBIconButton.stories.tsx +57 -57
  47. package/cli/storybook-dist/_stories-src/RBButton/RBInlineLabelButton.stories.tsx +41 -41
  48. package/cli/storybook-dist/_stories-src/RBButton/RBLabelButton.stories.tsx +60 -60
  49. package/cli/storybook-dist/_stories-src/RBDatePicker/RBDateInput.stories.tsx +44 -44
  50. package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +56 -56
  51. package/cli/storybook-dist/_stories-src/RBDialog/RBAlert.stories.tsx +51 -51
  52. package/cli/storybook-dist/_stories-src/RBDialog/RBConfirm.stories.tsx +55 -55
  53. package/cli/storybook-dist/_stories-src/RBDialog/RBPrompt.stories.tsx +52 -52
  54. package/cli/storybook-dist/_stories-src/RBDivider/RBDivider.stories.tsx +93 -93
  55. package/cli/storybook-dist/_stories-src/RBEmptyView/RBEmptyView.stories.tsx +49 -49
  56. package/cli/storybook-dist/_stories-src/RBIcons/RBColorIcons.stories.tsx +108 -108
  57. package/cli/storybook-dist/_stories-src/RBIcons/RBIcons.stories.tsx +106 -106
  58. package/cli/storybook-dist/_stories-src/RBInput/RBCheckBox.stories.tsx +89 -89
  59. package/cli/storybook-dist/_stories-src/RBInput/RBMultiRangeInput.stories.tsx +64 -64
  60. package/cli/storybook-dist/_stories-src/RBInput/RBRadioBox.stories.tsx +65 -65
  61. package/cli/storybook-dist/_stories-src/RBInput/RBRadioButton.stories.tsx +68 -68
  62. package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +216 -216
  63. package/cli/storybook-dist/_stories-src/RBInput/RBSideNumberInput.stories.tsx +90 -90
  64. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextArea.stories.tsx +45 -45
  65. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextInput.stories.tsx +50 -50
  66. package/cli/storybook-dist/_stories-src/RBInput/RBTextField.stories.tsx +102 -102
  67. package/cli/storybook-dist/_stories-src/RBJoyStick/RBJoyStick.stories.tsx +31 -31
  68. package/cli/storybook-dist/_stories-src/RBLight/RBLight.stories.tsx +36 -36
  69. package/cli/storybook-dist/_stories-src/RBPagination/RBPagination.stories.tsx +49 -49
  70. package/cli/storybook-dist/_stories-src/RBShadcn/RBCard/RBCard.stories.tsx +89 -89
  71. package/cli/storybook-dist/_stories-src/RBShadcn/RBCommand/RBCommand.stories.tsx +116 -116
  72. package/cli/storybook-dist/_stories-src/RBShadcn/RBDialog/RBDialog.stories.tsx +74 -74
  73. package/cli/storybook-dist/_stories-src/RBShadcn/RBDrawer/RBDrawer.stories.tsx +99 -99
  74. package/cli/storybook-dist/_stories-src/RBShadcn/RBDropdownMenu/RBDropdownMenu.stories.tsx +123 -123
  75. package/cli/storybook-dist/_stories-src/RBShadcn/RBMenubar/RBMenubar.stories.tsx +135 -135
  76. package/cli/storybook-dist/_stories-src/RBShadcn/RBPopover/RBPopover.stories.tsx +69 -69
  77. package/cli/storybook-dist/_stories-src/RBShadcn/RBResizable/RBResizable.stories.tsx +49 -49
  78. package/cli/storybook-dist/_stories-src/RBShadcn/RBSheet/RBSheet.stories.tsx +91 -91
  79. package/cli/storybook-dist/_stories-src/RBShadcn/RBSlider/RBSlider.stories.tsx +38 -38
  80. package/cli/storybook-dist/_stories-src/RBShadcn/RBSwitch/RBSwitch.stories.tsx +112 -112
  81. package/cli/storybook-dist/_stories-src/RBShadcn/RBTabs/RBTabs.stories.tsx +75 -75
  82. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggle/RBToggle.stories.tsx +51 -51
  83. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggleGroup/RBToggleGroup.stories.tsx +84 -84
  84. package/cli/storybook-dist/_stories-src/RBTab/RBChipTab.stories.tsx +102 -102
  85. package/cli/storybook-dist/_stories-src/RBTab/RBChipsBoxTab.stories.tsx +78 -78
  86. package/cli/storybook-dist/_stories-src/RBTab/RBTab.stories.tsx +57 -57
  87. package/cli/storybook-dist/_stories-src/RBTable/RBTable.stories.tsx +177 -177
  88. package/cli/storybook-dist/_stories-src/RBTable/RBTableRow.stories.tsx +64 -64
  89. package/cli/storybook-dist/_stories-src/RBToast/RBToast.stories.tsx +117 -117
  90. package/cli/storybook-dist/_stories-src/RBTooltip/RBTooltip.stories.tsx +50 -50
  91. package/cli/storybook-dist/_stories-src/loading/RBSpinner.stories.tsx +30 -30
  92. package/cli/storybook-dist/_stories-src/nx-button/NxButton.stories.tsx +54 -54
  93. package/cli/storybook-dist/_stories-src/nx-button/NxPointerHoldButton.stories.tsx +57 -57
  94. package/cli/storybook-dist/_stories-src/nx-icons/NxIcons.stories.tsx +48 -48
  95. package/cli/storybook-dist/_stories-src/nx-inputs/NxNumberInput.stories.tsx +103 -103
  96. package/cli/storybook-dist/_stories-src/nx-inputs/NxRangeInput.stories.tsx +27 -27
  97. package/cli/storybook-dist/_stories-src/nx-inputs/NxSelectBox.stories.tsx +67 -67
  98. package/cli/storybook-dist/_stories-src/nx-inputs/NxSlideRange.stories.tsx +45 -45
  99. package/cli/storybook-dist/_stories-src/nx-inputs/NxSwitch.stories.tsx +27 -27
  100. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextArea.stories.tsx +48 -48
  101. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextInput.stories.tsx +46 -46
  102. package/cli/storybook-dist/assets/{Color-YHDXOIA2-D6JqZzDJ.js → Color-YHDXOIA2-B4IER17l.js} +1 -1
  103. package/cli/storybook-dist/assets/{Docs-ESb7JAHg.js → Docs-C8GCHFtK.js} +3 -3
  104. package/cli/storybook-dist/assets/{Docs-6gD2kUo1.js → Docs-CgdEMPQP.js} +2 -2
  105. package/cli/storybook-dist/assets/{Docs-B9wd8Ok7.js → Docs-Ck3WHvk6.js} +3 -3
  106. package/cli/storybook-dist/assets/{Docs-CgFbWWXM.js → Docs-D9NHsoeZ.js} +2 -2
  107. package/cli/storybook-dist/assets/{Docs-DBQup8Bu.js → Docs-DNYQC7nl.js} +3 -3
  108. package/cli/storybook-dist/assets/{Docs-CMjovnSY.js → Docs-DUfeiM4I.js} +4 -4
  109. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-BOUI1AHi.js → DocsRenderer-CFRXHY34-C0WhyDoR.js} +1 -1
  110. package/cli/storybook-dist/assets/{NxAvatar.stories-DsYFMUh3.js → NxAvatar.stories-CwnK8AUF.js} +1 -1
  111. package/cli/storybook-dist/assets/{NxButton.stories-CHfNcqsc.js → NxButton.stories-ueYdosfQ.js} +8 -8
  112. package/cli/storybook-dist/assets/{NxNumberInput.stories-BFw_4vFE.js → NxNumberInput.stories-M5ZCLbrK.js} +1 -1
  113. package/cli/storybook-dist/assets/{NxPagination.stories-DVfxyw4M.js → NxPagination.stories-bxxUnHEL.js} +1 -1
  114. package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BzyxUWoZ.js → NxPointerHoldButton.stories-BHxB2kr5.js} +1 -1
  115. package/cli/storybook-dist/assets/{NxRangeInput.stories-DmRdzBc1.js → NxRangeInput.stories-BKfr-fp4.js} +6 -6
  116. package/cli/storybook-dist/assets/{NxSelectBox.stories-B1KkNUOf.js → NxSelectBox.stories-CG8FuTBq.js} +1 -1
  117. package/cli/storybook-dist/assets/{NxSlideRange.stories-CyOdvfQ6.js → NxSlideRange.stories-BRTFtMTL.js} +1 -1
  118. package/cli/storybook-dist/assets/{NxSwitch.stories-DrDBw0qp.js → NxSwitch.stories-Bwuyzn-R.js} +1 -1
  119. package/cli/storybook-dist/assets/{NxTextArea.stories-DTB2aqNS.js → NxTextArea.stories-COq2sGqu.js} +1 -1
  120. package/cli/storybook-dist/assets/{NxTextInput.stories-DU0rucmM.js → NxTextInput.stories-CjsBLz05.js} +1 -1
  121. package/cli/storybook-dist/assets/{RBAccordion.stories-DTfOtXtp.js → RBAccordion.stories-BARwuZ0Q.js} +3 -3
  122. package/cli/storybook-dist/assets/{RBAccordionGroup.stories-9y9BjrUe.js → RBAccordionGroup.stories-COrPxETW.js} +10 -10
  123. package/cli/storybook-dist/assets/{RBAlert.stories-DLTAv9fT.js → RBAlert.stories-BQrkjZPt.js} +3 -3
  124. package/cli/storybook-dist/assets/{RBBadge.stories-BNMACewA.js → RBBadge.stories-BTsLL-lK.js} +1 -1
  125. package/cli/storybook-dist/assets/{RBBattery.stories-B3kXMAt5.js → RBBattery.stories-DAeQdJbc.js} +1 -1
  126. package/cli/storybook-dist/assets/{RBCard.stories-Y-s8R2n7.js → RBCard.stories-HyhOE80R.js} +39 -39
  127. package/cli/storybook-dist/assets/{RBCheckBox.stories-DBuT9tL4.js → RBCheckBox.stories-KFk0WqX_.js} +1 -1
  128. package/cli/storybook-dist/assets/{RBCheckIconButton.stories-CWu1TL56.js → RBCheckIconButton.stories-CXbbktF5.js} +1 -1
  129. package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-sCu2MGCX.js → RBCheckLabelButton.stories-DWfYz_9Z.js} +17 -17
  130. package/cli/storybook-dist/assets/{RBChipTab.stories-BX9qLJkR.js → RBChipTab.stories-JPEgVFAS.js} +5 -5
  131. package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-35u1aJ-P.js → RBChipsBoxTab.stories-CTPuWXrQ.js} +1 -1
  132. package/cli/storybook-dist/assets/{RBColorIcons.stories-DAOZ5Ies.js → RBColorIcons.stories-DiFRZ5jk.js} +1 -1
  133. package/cli/storybook-dist/assets/RBCommand.stories-Pf1HAVIR.js +73 -0
  134. package/cli/storybook-dist/assets/{RBConfirm.stories-iMXcUh7Z.js → RBConfirm.stories-D1cEPUXk.js} +3 -3
  135. package/cli/storybook-dist/assets/{RBDateInput.stories-hK5rXECf.js → RBDateInput.stories-RPYpCjig.js} +1 -1
  136. package/cli/storybook-dist/assets/RBDialog.stories-DJH-0XSR.js +38 -0
  137. package/cli/storybook-dist/assets/{RBDivider.stories-BAK-iZ1Z.js → RBDivider.stories-uEj1vOgK.js} +1 -1
  138. package/cli/storybook-dist/assets/{RBDrawer.stories-B2nqf2Nh.js → RBDrawer.stories-BiMzElb2.js} +12 -12
  139. package/cli/storybook-dist/assets/RBDropdownMenu.stories-Da_TBkla.js +86 -0
  140. package/cli/storybook-dist/assets/{RBEmptyView.stories-nCenrgBh.js → RBEmptyView.stories-BHeKGfPS.js} +1 -1
  141. package/cli/storybook-dist/assets/{RBIconButton.stories-DRH7O3sp.js → RBIconButton.stories-NEsRFoYp.js} +1 -1
  142. package/cli/storybook-dist/assets/{RBIcons.stories-D3PyJXG8.js → RBIcons.stories-CcZ-TTLS.js} +1 -1
  143. package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-modQbt10.js → RBInlineLabelButton.stories-B0wpdQyR.js} +1 -1
  144. package/cli/storybook-dist/assets/{RBJoyStick.stories-BY14qVgR.js → RBJoyStick.stories-DEgBou3K.js} +1 -1
  145. package/cli/storybook-dist/assets/{RBLabelButton.stories-DzF7vGwm.js → RBLabelButton.stories-DT8drIsF.js} +1 -1
  146. package/cli/storybook-dist/assets/{RBLight.stories-lF4FRmVh.js → RBLight.stories-Dh-aoeyG.js} +1 -1
  147. package/cli/storybook-dist/assets/{RBMenubar.stories-XhrI7bsV.js → RBMenubar.stories-D-H7UqVh.js} +1 -1
  148. package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-DprllQDl.js → RBMultiRangeInput.stories-hlclv6jI.js} +1 -1
  149. package/cli/storybook-dist/assets/{RBPagination.stories-2v2gBJfC.js → RBPagination.stories-DWCeoZ_S.js} +1 -1
  150. package/cli/storybook-dist/assets/{RBPopover.stories-C8iLIZpv.js → RBPopover.stories-C514M09_.js} +1 -1
  151. package/cli/storybook-dist/assets/{RBPrompt.stories-Th_HrWHI.js → RBPrompt.stories-BSItvfiT.js} +1 -1
  152. package/cli/storybook-dist/assets/{RBRadioBox.stories-CoHuLaN3.js → RBRadioBox.stories-BBb4YbFU.js} +1 -1
  153. package/cli/storybook-dist/assets/{RBRadioButton.stories-DULxE_ff.js → RBRadioButton.stories-CXyOGJCv.js} +1 -1
  154. package/cli/storybook-dist/assets/{RBRangeDateInput.stories-D8A9KBSS.js → RBRangeDateInput.stories-qvhubu0e.js} +1 -1
  155. package/cli/storybook-dist/assets/{RBResizable.stories-C41QfrQ3.js → RBResizable.stories-CZKeyGjH.js} +14 -14
  156. package/cli/storybook-dist/assets/{RBSelectBox.stories-C1gNmwzw.js → RBSelectBox.stories-CRKP3ybf.js} +1 -1
  157. package/cli/storybook-dist/assets/RBSheet.stories-sCM3ZJUa.js +39 -0
  158. package/cli/storybook-dist/assets/{RBSideNumberInput.stories-9LlgQ8nh.js → RBSideNumberInput.stories-Bb7gw8RF.js} +3 -3
  159. package/cli/storybook-dist/assets/{RBSlider.stories-oUgo5K9d.js → RBSlider.stories-23OAQU5z.js} +3 -3
  160. package/cli/storybook-dist/assets/{RBSpinner.stories-DDpohbY2.js → RBSpinner.stories-2xcQdcmt.js} +1 -1
  161. package/cli/storybook-dist/assets/{RBSwitch.stories-gW-GN7QO.js → RBSwitch.stories-BACUaJm_.js} +7 -7
  162. package/cli/storybook-dist/assets/{RBTab.stories-CN-PV3HH.js → RBTab.stories-CDHE3sVs.js} +1 -1
  163. package/cli/storybook-dist/assets/{RBTable.stories-BAZLpxtM.js → RBTable.stories-C0Cfhfl7.js} +29 -29
  164. package/cli/storybook-dist/assets/{RBTableRow.stories-CwjVrszB.js → RBTableRow.stories-BF8kIkpp.js} +1 -1
  165. package/cli/storybook-dist/assets/{RBTabs.stories-JuEo2sUL.js → RBTabs.stories-BdJWRq1o.js} +7 -7
  166. package/cli/storybook-dist/assets/{RBTextArea.stories-D0_Pv4qZ.js → RBTextArea.stories-CGy2xeYH.js} +3 -3
  167. package/cli/storybook-dist/assets/{RBTextField.stories-CCsOvZqV.js → RBTextField.stories-IfUOzxCM.js} +15 -15
  168. package/cli/storybook-dist/assets/{RBTextInput.stories-Cccn3VNm.js → RBTextInput.stories-VjXNFo4R.js} +3 -3
  169. package/cli/storybook-dist/assets/{RBToast.stories-I5617t0K.js → RBToast.stories-BzcobblV.js} +1 -1
  170. package/cli/storybook-dist/assets/{RBToggle.stories-DsgKjGsJ.js → RBToggle.stories-DuLE5mKv.js} +4 -4
  171. package/cli/storybook-dist/assets/{RBToggleGroup.stories-BZEzSDuz.js → RBToggleGroup.stories-CuR0N0GJ.js} +5 -5
  172. package/cli/storybook-dist/assets/{RBTooltip.stories-4oSyAWsA.js → RBTooltip.stories--aAgNC8_.js} +1 -1
  173. package/cli/storybook-dist/assets/{SideNumberInput-DSKbd0bG.js → SideNumberInput-DyWQxYUj.js} +743 -743
  174. package/cli/storybook-dist/assets/{entry-preview-B35XREGs.js → entry-preview-D7IHsza_.js} +1 -1
  175. package/cli/storybook-dist/assets/{iframe-BB797lmL.js → iframe-CbaIp5I-.js} +2 -2
  176. package/cli/storybook-dist/assets/{index-BU2567EP.js → index-C6CBgMuG.js} +3 -3
  177. package/cli/storybook-dist/assets/{preview-C7DaSWyc.js → preview-B-iYLDuW.js} +2 -2
  178. package/cli/storybook-dist/assets/{preview-CbuRHit4.js → preview-CRWqc9DH.js} +1 -1
  179. package/cli/storybook-dist/assets/{react-resizable-panels.browser-ChIvlwo7.js → react-resizable-panels.browser-8hC8dr-6.js} +8 -8
  180. package/cli/storybook-dist/iframe.html +2 -2
  181. package/cli/storybook-dist/index.html +1 -1
  182. package/cli/storybook-dist/project.json +1 -1
  183. package/cli/templates/common/.mcp.json +8 -8
  184. package/cli/templates/common/README.md +1231 -1231
  185. package/cli/templates/common/assets/icon.svg +45 -45
  186. package/cli/templates/common/manifest.json +26 -26
  187. package/cli/templates/common/package.json +22 -22
  188. package/cli/templates/common/py/main.py +125 -125
  189. package/cli/templates/common/tsconfig.json +13 -13
  190. package/cli/templates/common/ui/content.tsx +216 -216
  191. package/cli/templates/common/ui/style.css +6 -6
  192. package/cli/templates/common/vite.config.ts +10 -10
  193. package/cli/templates/cpp/cpp/main.cpp +73 -73
  194. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +25 -25
  195. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/package.xml +10 -10
  196. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/setup.py +18 -18
  197. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -11
  198. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +23 -23
  199. package/cli/vite.d.mts +25 -25
  200. package/cli/vite.mjs +292 -292
  201. package/package.json +3 -3
  202. package/cli/storybook-dist/assets/RBCommand.stories-BSqXDkKw.js +0 -73
  203. package/cli/storybook-dist/assets/RBDialog.stories-BAktz-ku.js +0 -38
  204. package/cli/storybook-dist/assets/RBDropdownMenu.stories-DbW2fWAj.js +0 -86
  205. package/cli/storybook-dist/assets/RBSheet.stories-_6VH-RUm.js +0 -39
@@ -1,75 +1,75 @@
1
- // Editor (IntelliSense) support for C++ backends. The rb_sdk headers only exist inside the dev image,
2
- // so a host editor flags every SDK name. Copy them to .muscat-dev/sysroot and point
3
- // .vscode/c_cpp_properties.json at them, using the include dirs / defines rb_sdk's CMake target exports.
4
- // The build never reads these copies — it runs in the image.
5
- import { spawnSync } from "node:child_process";
6
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
- import { resolve } from "node:path";
8
- import { cppSdkFiles, log } from "./util.mjs";
9
-
10
- const SYSROOT = ".muscat-dev/sysroot";
11
- const CONFIG_NAME = "Muscat";
12
- const COPY = ["/usr/include/rainbow", "/usr/include/eigen3", "/usr/lib/cmake/rb_sdk/rb_sdkTargets.cmake"];
13
-
14
- function imageId(image) {
15
- const r = spawnSync("docker", ["image", "inspect", "-f", "{{.Id}}", image], { encoding: "utf8" });
16
- return r.status === 0 ? r.stdout.trim() : null;
17
- }
18
-
19
- /** Copy the SDK headers out of `image` (once per image build) and write the VS Code C/C++ config. */
20
- export function syncCppIde(root, image) {
21
- if (!cppSdkFiles(root).length) return;
22
- const id = imageId(image);
23
- if (!id) return;
24
- const sysroot = resolve(root, SYSROOT);
25
- const stamp = resolve(sysroot, ".image-id");
26
- if (!existsSync(stamp) || readFileSync(stamp, "utf8") !== id) {
27
- rmSync(sysroot, { recursive: true, force: true });
28
- const create = spawnSync("docker", ["create", image], { encoding: "utf8" });
29
- if (create.status !== 0) return log.warn("could not copy SDK headers for the editor (docker create failed).");
30
- const container = create.stdout.trim();
31
- try {
32
- for (const src of COPY) {
33
- const dest = resolve(sysroot, src.slice(1));
34
- mkdirSync(resolve(dest, ".."), { recursive: true });
35
- spawnSync("docker", ["cp", `${container}:${src}`, dest], { stdio: "ignore" });
36
- }
37
- } finally {
38
- spawnSync("docker", ["rm", container], { stdio: "ignore" });
39
- }
40
- writeFileSync(stamp, id);
41
- log.step(`SDK headers for the editor → ${SYSROOT}`);
42
- }
43
- writeCppProperties(root, readTarget(sysroot));
44
- }
45
-
46
- // INTERFACE_INCLUDE_DIRECTORIES / INTERFACE_COMPILE_DEFINITIONS of rb_sdk::rb_sdk, mapped into the sysroot.
47
- function readTarget(sysroot) {
48
- const file = resolve(sysroot, "usr/lib/cmake/rb_sdk/rb_sdkTargets.cmake");
49
- const text = existsSync(file) ? readFileSync(file, "utf8") : "";
50
- const list = (key) => (text.match(new RegExp(`${key} "([^"]*)"`))?.[1] ?? "").split(";").filter(Boolean);
51
- const prefix = `\${workspaceFolder}/${SYSROOT}/usr`;
52
- const includes = list("INTERFACE_INCLUDE_DIRECTORIES").map((d) => d.replace("${_IMPORT_PREFIX}", prefix));
53
- return {
54
- includePath: [...(includes.length ? includes : [`${prefix}/include`]), `${prefix}/include/eigen3`],
55
- defines: list("INTERFACE_COMPILE_DEFINITIONS"),
56
- };
57
- }
58
-
59
- // Keeps other configurations; ours is replaced in place or put first so VS Code picks it by default.
60
- function writeCppProperties(root, { includePath, defines }) {
61
- const file = resolve(root, ".vscode", "c_cpp_properties.json");
62
- let props = { configurations: [], version: 4 };
63
- if (existsSync(file)) {
64
- try {
65
- props = JSON.parse(readFileSync(file, "utf8"));
66
- } catch {
67
- return log.warn(".vscode/c_cpp_properties.json is not plain JSON — add the SDK include paths yourself.");
68
- }
69
- }
70
- const config = { name: CONFIG_NAME, includePath: ["${workspaceFolder}/**", ...includePath], defines, cppStandard: "c++20" };
71
- const configs = (props.configurations ?? []).filter((c) => c.name !== CONFIG_NAME);
72
- props.configurations = [config, ...configs];
73
- mkdirSync(resolve(root, ".vscode"), { recursive: true });
74
- writeFileSync(file, JSON.stringify(props, null, 2) + "\n");
75
- }
1
+ // Editor (IntelliSense) support for C++ backends. The rb_sdk headers only exist inside the dev image,
2
+ // so a host editor flags every SDK name. Copy them to .muscat-dev/sysroot and point
3
+ // .vscode/c_cpp_properties.json at them, using the include dirs / defines rb_sdk's CMake target exports.
4
+ // The build never reads these copies — it runs in the image.
5
+ import { spawnSync } from "node:child_process";
6
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
+ import { resolve } from "node:path";
8
+ import { cppSdkFiles, log } from "./util.mjs";
9
+
10
+ const SYSROOT = ".muscat-dev/sysroot";
11
+ const CONFIG_NAME = "Muscat";
12
+ const COPY = ["/usr/include/rainbow", "/usr/include/eigen3", "/usr/lib/cmake/rb_sdk/rb_sdkTargets.cmake"];
13
+
14
+ function imageId(image) {
15
+ const r = spawnSync("docker", ["image", "inspect", "-f", "{{.Id}}", image], { encoding: "utf8" });
16
+ return r.status === 0 ? r.stdout.trim() : null;
17
+ }
18
+
19
+ /** Copy the SDK headers out of `image` (once per image build) and write the VS Code C/C++ config. */
20
+ export function syncCppIde(root, image) {
21
+ if (!cppSdkFiles(root).length) return;
22
+ const id = imageId(image);
23
+ if (!id) return;
24
+ const sysroot = resolve(root, SYSROOT);
25
+ const stamp = resolve(sysroot, ".image-id");
26
+ if (!existsSync(stamp) || readFileSync(stamp, "utf8") !== id) {
27
+ rmSync(sysroot, { recursive: true, force: true });
28
+ const create = spawnSync("docker", ["create", image], { encoding: "utf8" });
29
+ if (create.status !== 0) return log.warn("could not copy SDK headers for the editor (docker create failed).");
30
+ const container = create.stdout.trim();
31
+ try {
32
+ for (const src of COPY) {
33
+ const dest = resolve(sysroot, src.slice(1));
34
+ mkdirSync(resolve(dest, ".."), { recursive: true });
35
+ spawnSync("docker", ["cp", `${container}:${src}`, dest], { stdio: "ignore" });
36
+ }
37
+ } finally {
38
+ spawnSync("docker", ["rm", container], { stdio: "ignore" });
39
+ }
40
+ writeFileSync(stamp, id);
41
+ log.step(`SDK headers for the editor → ${SYSROOT}`);
42
+ }
43
+ writeCppProperties(root, readTarget(sysroot));
44
+ }
45
+
46
+ // INTERFACE_INCLUDE_DIRECTORIES / INTERFACE_COMPILE_DEFINITIONS of rb_sdk::rb_sdk, mapped into the sysroot.
47
+ function readTarget(sysroot) {
48
+ const file = resolve(sysroot, "usr/lib/cmake/rb_sdk/rb_sdkTargets.cmake");
49
+ const text = existsSync(file) ? readFileSync(file, "utf8") : "";
50
+ const list = (key) => (text.match(new RegExp(`${key} "([^"]*)"`))?.[1] ?? "").split(";").filter(Boolean);
51
+ const prefix = `\${workspaceFolder}/${SYSROOT}/usr`;
52
+ const includes = list("INTERFACE_INCLUDE_DIRECTORIES").map((d) => d.replace("${_IMPORT_PREFIX}", prefix));
53
+ return {
54
+ includePath: [...(includes.length ? includes : [`${prefix}/include`]), `${prefix}/include/eigen3`],
55
+ defines: list("INTERFACE_COMPILE_DEFINITIONS"),
56
+ };
57
+ }
58
+
59
+ // Keeps other configurations; ours is replaced in place or put first so VS Code picks it by default.
60
+ function writeCppProperties(root, { includePath, defines }) {
61
+ const file = resolve(root, ".vscode", "c_cpp_properties.json");
62
+ let props = { configurations: [], version: 4 };
63
+ if (existsSync(file)) {
64
+ try {
65
+ props = JSON.parse(readFileSync(file, "utf8"));
66
+ } catch {
67
+ return log.warn(".vscode/c_cpp_properties.json is not plain JSON — add the SDK include paths yourself.");
68
+ }
69
+ }
70
+ const config = { name: CONFIG_NAME, includePath: ["${workspaceFolder}/**", ...includePath], defines, cppStandard: "c++20" };
71
+ const configs = (props.configurations ?? []).filter((c) => c.name !== CONFIG_NAME);
72
+ props.configurations = [config, ...configs];
73
+ mkdirSync(resolve(root, ".vscode"), { recursive: true });
74
+ writeFileSync(file, JSON.stringify(props, null, 2) + "\n");
75
+ }
package/cli/src/dev.mjs CHANGED
@@ -1,165 +1,165 @@
1
- // muscat-pk dev — run the local Muscat runtime (gateway, MediaMTX, common), dev:ui, and dev:back together.
2
- import { spawn } from "node:child_process";
3
- import { existsSync } from "node:fs";
4
- import { connect as netConnect } from "node:net";
5
- import { resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
- import { runDevUi } from "./devUi.mjs";
8
- import { stopDevBackContainers } from "./devBack.mjs";
9
- import { ensureLocalMuscat, GATEWAY_PORT, isPortInUse, robotHasMuscat } from "./muscatLocal.mjs";
10
- import { assertManifestPorts, assertManifestVariables } from "./manifest.mjs";
11
- import {
12
- ensureDocker,
13
- ensureIdeVenv,
14
- findPluginRoot,
15
- log,
16
- prompt,
17
- promptRobotConnection,
18
- venvPython,
19
- } from "./util.mjs";
20
- import { ensureRemoteZenohEnv, isRemoteRobot, isWsl } from "./zenohDev.mjs";
21
-
22
- const HOST_AGENT_PATH = fileURLToPath(new URL("./host_agent.py", import.meta.url));
23
- const canAutoStartHostAgent = () => process.platform === "darwin" || process.platform === "win32" || isWsl();
24
-
25
- function waitForHostAgent(port, { timeoutMs = 30_000 } = {}) {
26
- const deadline = Date.now() + timeoutMs;
27
- return new Promise((resolve, reject) => {
28
- const probe = () => {
29
- const sock = netConnect(port, "127.0.0.1");
30
- sock.once("connect", () => { sock.destroy(); resolve(true); });
31
- sock.once("error", () => {
32
- sock.destroy();
33
- if (Date.now() >= deadline) {
34
- reject(new Error(`host SDK agent did not open port ${port} in time`));
35
- return;
36
- }
37
- setTimeout(probe, 300);
38
- });
39
- };
40
- probe();
41
- });
42
- }
43
-
44
- const DEFAULT_PROXY_PORT = 47100;
45
-
46
- // Start the host-side Zenoh byte-proxy (host_agent.py) for a remote robot —
47
- // see host_agent.py for why a Docker Desktop container can't join the
48
- // robot's Zenoh mesh itself. Sets MUSCAT_PROXY_ENDPOINT for dev:back.
49
- // Returns the child process, or null if not applicable / disabled.
50
- async function maybeStartHostAgent(root, { robotIp, robotToken }) {
51
- if (process.env.MUSCAT_PROXY_ENDPOINT) {
52
- log.step(`Zenoh via host byte-proxy → ${process.env.MUSCAT_PROXY_ENDPOINT}`);
53
- return null;
54
- }
55
- if (process.env.MUSCAT_HOST_AGENT_DISABLE === "1") return null;
56
- if (!canAutoStartHostAgent() || !isRemoteRobot(robotIp)) return null;
57
-
58
- const agentPath = process.env.MUSCAT_HOST_AGENT_SCRIPT || HOST_AGENT_PATH;
59
- if (!existsSync(agentPath)) return null;
60
-
61
- if (!ensureIdeVenv(root)) {
62
- log.warn("host Zenoh byte-proxy skipped: could not prepare .venv");
63
- return null;
64
- }
65
-
66
- // advertiseHost/zenohPort = the host_agent's OWN robot-facing Zenoh session
67
- // (host LAN IP + port the robot reverse-dials into). The container never
68
- // sees or needs these — it only talks to the proxyPort below.
69
- const zenoh = await ensureRemoteZenohEnv(robotIp, { log });
70
- const advertiseHost = zenoh?.advertiseHost || process.env.MUSCAT_ZENOH_ADVERTISE_HOST;
71
- const zenohPort = zenoh?.zenohPort || Number(process.env.MUSCAT_ZENOH_PORT);
72
- if (!advertiseHost || !zenohPort) {
73
- log.warn("host Zenoh byte-proxy skipped: could not determine host LAN IP / port");
74
- return null;
75
- }
76
- const proxyPort = Number(process.env.MUSCAT_PROXY_PORT) || DEFAULT_PROXY_PORT;
77
-
78
- log.step("starting host Zenoh byte-proxy (Docker Desktop query-routing workaround)...");
79
- const py = venvPython(resolve(root, ".venv"));
80
- const child = spawn(py, [agentPath], {
81
- cwd: root,
82
- stdio: ["ignore", "inherit", "inherit"],
83
- env: {
84
- ...process.env,
85
- MUSCAT_ROBOT_HOST: robotIp,
86
- MUSCAT_ROBOT_TOKEN: robotToken,
87
- MUSCAT_ZENOH_ADVERTISE_HOST: advertiseHost,
88
- MUSCAT_ZENOH_PORT: String(zenohPort),
89
- MUSCAT_PROXY_PORT: String(proxyPort),
90
- },
91
- });
92
-
93
- try {
94
- await waitForHostAgent(proxyPort);
95
- } catch (e) {
96
- child.kill();
97
- throw e;
98
- }
99
-
100
- // advertiseHost (not host.docker.internal): a Docker Desktop container run
101
- // under `dev` shares the local Muscat container's netns (--network container:),
102
- // which has no host-gateway alias configured — the host's real LAN IP
103
- // always resolves regardless of netns mode.
104
- const proxyEndpoint = `${advertiseHost}:${proxyPort}`;
105
- process.env.MUSCAT_PROXY_ENDPOINT = proxyEndpoint;
106
- process.env.MUSCAT_PROXY_PORT = String(proxyPort);
107
- log.step(`Zenoh via host byte-proxy → ${proxyEndpoint}`);
108
- return child;
109
- }
110
-
111
- export async function runDev(args = []) {
112
- const root = findPluginRoot();
113
- assertManifestVariables(root);
114
- const portRanges = assertManifestPorts(root);
115
- ensureDocker();
116
- const { robotIp, robotToken } = await promptRobotConnection(args);
117
- process.env.MUSCAT_ROBOT_HOST = robotIp;
118
- if (robotToken) process.env.MUSCAT_ROBOT_TOKEN = robotToken;
119
-
120
- const remoteRobot = isRemoteRobot(robotIp);
121
- const hostAgent = await maybeStartHostAgent(root, { robotIp, robotToken });
122
- const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT || "";
123
- // No proxy (Linux, disabled, ...): fall back to the container reverse-dialing
124
- // the robot itself — the local Muscat then publishes the zenoh port for it.
125
- if (!proxyEndpoint) await ensureRemoteZenohEnv(robotIp, { log });
126
-
127
- // A robot running Muscat is the gateway (:10000) and common for both the UI and the backend.
128
- // Otherwise the local Muscat runtime is; the backend joins its netns, so the plugin's declared
129
- // ports are published from it too.
130
- const muscat = (await robotHasMuscat(robotIp)) ? null : await ensureLocalMuscat({
131
- pluginPorts: portRanges,
132
- zenohPort: remoteRobot && !proxyEndpoint ? Number(process.env.MUSCAT_ZENOH_PORT) || null : null,
133
- }).catch((e) => { log.warn(`local Muscat: ${e.message}`); return null; });
134
- if (muscat) {
135
- process.env.MUSCAT_PK_MUSCAT_CONTAINER = muscat;
136
- } else if (remoteRobot || (await isPortInUse(GATEWAY_PORT))) {
137
- // e.g. developing on the robot PC: its Muscat already serves the gateway; the backend reaches it via the host.
138
- } else {
139
- log.warn(`no local Muscat on :${GATEWAY_PORT} — the plugin UI cannot reach the backend.`);
140
- const answer = await prompt(" Proceed anyway? [y/N]: ");
141
- if (answer.toLowerCase() !== "y") return;
142
- }
143
-
144
- // dev:back in its own process: its docker build / pip install / cmake build are synchronous and would
145
- // freeze dev:ui's server on this event loop until they finish (or Ctrl+C killed them).
146
- // A Bun binary re-runs itself with just the command; under Node argv[1] is the script.
147
- const backend = spawn(process.execPath, [...(typeof Bun === "undefined" ? [process.argv[1]] : []), "dev:back"], {
148
- stdio: ["ignore", "inherit", "inherit"],
149
- });
150
- backend.on("exit", (code) => code && log.warn(`dev:back exited (${code})`));
151
-
152
- process.env.MUSCAT_PK_MANAGED_DEV = "1";
153
-
154
- const cleanup = () => {
155
- if (hostAgent && hostAgent.exitCode === null) hostAgent.kill();
156
- if (backend.exitCode === null) backend.kill();
157
- stopDevBackContainers();
158
- };
159
- process.once("SIGINT", cleanup);
160
- process.once("SIGTERM", cleanup);
161
- process.once("SIGHUP", cleanup); // terminal closed (VS Code re-running Dev disposes it)
162
- process.once("exit", cleanup);
163
-
164
- await runDevUi(args);
165
- }
1
+ // muscat-pk dev — run the local Muscat runtime (gateway, MediaMTX, common), dev:ui, and dev:back together.
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { connect as netConnect } from "node:net";
5
+ import { resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { runDevUi } from "./devUi.mjs";
8
+ import { stopDevBackContainers } from "./devBack.mjs";
9
+ import { ensureLocalMuscat, GATEWAY_PORT, isPortInUse, robotHasMuscat } from "./muscatLocal.mjs";
10
+ import { assertManifestPorts, assertManifestVariables } from "./manifest.mjs";
11
+ import {
12
+ ensureDocker,
13
+ ensureIdeVenv,
14
+ findPluginRoot,
15
+ log,
16
+ prompt,
17
+ promptRobotConnection,
18
+ venvPython,
19
+ } from "./util.mjs";
20
+ import { ensureRemoteZenohEnv, isRemoteRobot, isWsl } from "./zenohDev.mjs";
21
+
22
+ const HOST_AGENT_PATH = fileURLToPath(new URL("./host_agent.py", import.meta.url));
23
+ const canAutoStartHostAgent = () => process.platform === "darwin" || process.platform === "win32" || isWsl();
24
+
25
+ function waitForHostAgent(port, { timeoutMs = 30_000 } = {}) {
26
+ const deadline = Date.now() + timeoutMs;
27
+ return new Promise((resolve, reject) => {
28
+ const probe = () => {
29
+ const sock = netConnect(port, "127.0.0.1");
30
+ sock.once("connect", () => { sock.destroy(); resolve(true); });
31
+ sock.once("error", () => {
32
+ sock.destroy();
33
+ if (Date.now() >= deadline) {
34
+ reject(new Error(`host SDK agent did not open port ${port} in time`));
35
+ return;
36
+ }
37
+ setTimeout(probe, 300);
38
+ });
39
+ };
40
+ probe();
41
+ });
42
+ }
43
+
44
+ const DEFAULT_PROXY_PORT = 47100;
45
+
46
+ // Start the host-side Zenoh byte-proxy (host_agent.py) for a remote robot —
47
+ // see host_agent.py for why a Docker Desktop container can't join the
48
+ // robot's Zenoh mesh itself. Sets MUSCAT_PROXY_ENDPOINT for dev:back.
49
+ // Returns the child process, or null if not applicable / disabled.
50
+ async function maybeStartHostAgent(root, { robotIp, robotToken }) {
51
+ if (process.env.MUSCAT_PROXY_ENDPOINT) {
52
+ log.step(`Zenoh via host byte-proxy → ${process.env.MUSCAT_PROXY_ENDPOINT}`);
53
+ return null;
54
+ }
55
+ if (process.env.MUSCAT_HOST_AGENT_DISABLE === "1") return null;
56
+ if (!canAutoStartHostAgent() || !isRemoteRobot(robotIp)) return null;
57
+
58
+ const agentPath = process.env.MUSCAT_HOST_AGENT_SCRIPT || HOST_AGENT_PATH;
59
+ if (!existsSync(agentPath)) return null;
60
+
61
+ if (!ensureIdeVenv(root)) {
62
+ log.warn("host Zenoh byte-proxy skipped: could not prepare .venv");
63
+ return null;
64
+ }
65
+
66
+ // advertiseHost/zenohPort = the host_agent's OWN robot-facing Zenoh session
67
+ // (host LAN IP + port the robot reverse-dials into). The container never
68
+ // sees or needs these — it only talks to the proxyPort below.
69
+ const zenoh = await ensureRemoteZenohEnv(robotIp, { log });
70
+ const advertiseHost = zenoh?.advertiseHost || process.env.MUSCAT_ZENOH_ADVERTISE_HOST;
71
+ const zenohPort = zenoh?.zenohPort || Number(process.env.MUSCAT_ZENOH_PORT);
72
+ if (!advertiseHost || !zenohPort) {
73
+ log.warn("host Zenoh byte-proxy skipped: could not determine host LAN IP / port");
74
+ return null;
75
+ }
76
+ const proxyPort = Number(process.env.MUSCAT_PROXY_PORT) || DEFAULT_PROXY_PORT;
77
+
78
+ log.step("starting host Zenoh byte-proxy (Docker Desktop query-routing workaround)...");
79
+ const py = venvPython(resolve(root, ".venv"));
80
+ const child = spawn(py, [agentPath], {
81
+ cwd: root,
82
+ stdio: ["ignore", "inherit", "inherit"],
83
+ env: {
84
+ ...process.env,
85
+ MUSCAT_ROBOT_HOST: robotIp,
86
+ MUSCAT_ROBOT_TOKEN: robotToken,
87
+ MUSCAT_ZENOH_ADVERTISE_HOST: advertiseHost,
88
+ MUSCAT_ZENOH_PORT: String(zenohPort),
89
+ MUSCAT_PROXY_PORT: String(proxyPort),
90
+ },
91
+ });
92
+
93
+ try {
94
+ await waitForHostAgent(proxyPort);
95
+ } catch (e) {
96
+ child.kill();
97
+ throw e;
98
+ }
99
+
100
+ // advertiseHost (not host.docker.internal): a Docker Desktop container run
101
+ // under `dev` shares the local Muscat container's netns (--network container:),
102
+ // which has no host-gateway alias configured — the host's real LAN IP
103
+ // always resolves regardless of netns mode.
104
+ const proxyEndpoint = `${advertiseHost}:${proxyPort}`;
105
+ process.env.MUSCAT_PROXY_ENDPOINT = proxyEndpoint;
106
+ process.env.MUSCAT_PROXY_PORT = String(proxyPort);
107
+ log.step(`Zenoh via host byte-proxy → ${proxyEndpoint}`);
108
+ return child;
109
+ }
110
+
111
+ export async function runDev(args = []) {
112
+ const root = findPluginRoot();
113
+ assertManifestVariables(root);
114
+ const portRanges = assertManifestPorts(root);
115
+ ensureDocker();
116
+ const { robotIp, robotToken } = await promptRobotConnection(args);
117
+ process.env.MUSCAT_ROBOT_HOST = robotIp;
118
+ if (robotToken) process.env.MUSCAT_ROBOT_TOKEN = robotToken;
119
+
120
+ const remoteRobot = isRemoteRobot(robotIp);
121
+ const hostAgent = await maybeStartHostAgent(root, { robotIp, robotToken });
122
+ const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT || "";
123
+ // No proxy (Linux, disabled, ...): fall back to the container reverse-dialing
124
+ // the robot itself — the local Muscat then publishes the zenoh port for it.
125
+ if (!proxyEndpoint) await ensureRemoteZenohEnv(robotIp, { log });
126
+
127
+ // A robot running Muscat is the gateway (:10000) and common for both the UI and the backend.
128
+ // Otherwise the local Muscat runtime is; the backend joins its netns, so the plugin's declared
129
+ // ports are published from it too.
130
+ const muscat = (await robotHasMuscat(robotIp)) ? null : await ensureLocalMuscat({
131
+ pluginPorts: portRanges,
132
+ zenohPort: remoteRobot && !proxyEndpoint ? Number(process.env.MUSCAT_ZENOH_PORT) || null : null,
133
+ }).catch((e) => { log.warn(`local Muscat: ${e.message}`); return null; });
134
+ if (muscat) {
135
+ process.env.MUSCAT_PK_MUSCAT_CONTAINER = muscat;
136
+ } else if (remoteRobot || (await isPortInUse(GATEWAY_PORT))) {
137
+ // e.g. developing on the robot PC: its Muscat already serves the gateway; the backend reaches it via the host.
138
+ } else {
139
+ log.warn(`no local Muscat on :${GATEWAY_PORT} — the plugin UI cannot reach the backend.`);
140
+ const answer = await prompt(" Proceed anyway? [y/N]: ");
141
+ if (answer.toLowerCase() !== "y") return;
142
+ }
143
+
144
+ // dev:back in its own process: its docker build / pip install / cmake build are synchronous and would
145
+ // freeze dev:ui's server on this event loop until they finish (or Ctrl+C killed them).
146
+ // A Bun binary re-runs itself with just the command; under Node argv[1] is the script.
147
+ const backend = spawn(process.execPath, [...(typeof Bun === "undefined" ? [process.argv[1]] : []), "dev:back"], {
148
+ stdio: ["ignore", "inherit", "inherit"],
149
+ });
150
+ backend.on("exit", (code) => code && log.warn(`dev:back exited (${code})`));
151
+
152
+ process.env.MUSCAT_PK_MANAGED_DEV = "1";
153
+
154
+ const cleanup = () => {
155
+ if (hostAgent && hostAgent.exitCode === null) hostAgent.kill();
156
+ if (backend.exitCode === null) backend.kill();
157
+ stopDevBackContainers();
158
+ };
159
+ process.once("SIGINT", cleanup);
160
+ process.once("SIGTERM", cleanup);
161
+ process.once("SIGHUP", cleanup); // terminal closed (VS Code re-running Dev disposes it)
162
+ process.once("exit", cleanup);
163
+
164
+ await runDevUi(args);
165
+ }