@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
@@ -8,7 +8,7 @@ A Muscat plugin project. Build, develop, and ship with `muscat-pk` CLI.
8
8
  muscat-pk dev # UI preview + backend together
9
9
  muscat-pk dev:ui # UI preview only
10
10
  muscat-pk dev:back # backend only (Python watchfiles / ROS2 Docker)
11
- muscat-pk pack # build a distributable ZIP
11
+ muscat-pk build # build + distributable ZIP
12
12
  ```
13
13
 
14
14
  ---
@@ -17,20 +17,47 @@ muscat-pk pack # build a distributable ZIP
17
17
 
18
18
  ```
19
19
  __PLUGIN_DIR__/
20
- ├─ src/
21
- │ └─ content.tsx # plugin UI entry point
20
+ ├─ ui/
21
+ │ ├─ content.tsx # plugin UI entry point
22
+ │ └─ style.css # plugin styles auto-loaded by muscat()
23
+ ├─ py/
24
+ │ └─ main.py # Python backend entry point
22
25
  ├─ assets/
23
26
  │ └─ icon.svg # icon referenced from manifest
24
27
  ├─ manifest.json # metadata, contribution points, runtime config
25
- ├─ main.py # backend (Python or ROS2)
26
- └─ requirements.txt # Python deps (auto-pinned to latest on pack)
28
+ ├─ README.md # this file (copied to dist/ on build)
29
+ └─ requirements.txt # Python deps (auto-pinned to latest on build)
27
30
  ```
28
31
 
29
32
  > ROS2 plugins additionally include a `ros2_ws/` directory.
30
33
 
31
34
  ---
32
35
 
33
- ## Frontend (`src/content.tsx`)
36
+ ## Frontend (`ui/content.tsx`)
37
+
38
+ ### JavaScript dependencies
39
+
40
+ `muscat-pk build` runs this plugin project's own Vite build. Vite resolves
41
+ imports from installed `node_modules`; it does not care whether a package is
42
+ listed under `dependencies` or `devDependencies`, but the package must be
43
+ installed.
44
+
45
+ Do not run a production-only install such as `npm install --omit=dev` before
46
+ building the plugin UI, because build tools like `vite`, `typescript`,
47
+ `tailwindcss`, `@tailwindcss/vite`, `sass`, or `less` are usually
48
+ `devDependencies`.
49
+
50
+ The scaffold keeps this split:
51
+
52
+ | package kind | examples | why |
53
+ |---|---|---|
54
+ | `dependencies` | `@rainbow-robotics/plugin-ui`, UI libraries imported by plugin source | Author-facing APIs used by `ui/*.tsx` |
55
+ | `devDependencies` | `vite`, `typescript`, `tailwindcss`, `@tailwindcss/vite`, `sass`, `less` | Build/dev tools needed before `dist/*.js` is produced |
56
+
57
+ The built plugin ZIP does not include `package.json` or `node_modules`. For
58
+ production, Muscat provides `@rainbow-robotics/plugin-ui`, React, and ReactDOM
59
+ through the host import map; plugin UI build output imports those as host
60
+ externals.
34
61
 
35
62
  ### useRb() hook
36
63
 
@@ -46,13 +73,19 @@ function App() {
46
73
 
47
74
  ### rb API
48
75
 
49
- | Method | Description |
50
- |---|---|
51
- | `rb.pub(topic, payload)` | Publish a message to a topic |
52
- | `rb.sub(topic, handler)` | Subscribe to a topic — returns `{ undeclare() }` |
53
- | `rb.action(topic, payload?)` | Send a request and await a reply |
54
- | `rb.serve(topic, handler)` | Handle incoming requests from the backend |
55
- | `rb.toast(message, type?)` | Host toast notification (`"info" · "success" · "warning" · "error"`) |
76
+ | Method | Description |
77
+ | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
78
+ | `rb.pub(topic, payload)` | Publish a message to a topic |
79
+ | `rb.sub(topic, handler)` | Subscribe to a topic — returns `{ undeclare() }` |
80
+ | `rb.action(topic, payload?)` | Send a request and await a reply |
81
+ | `rb.serve(topic, handler)` | Handle incoming requests from the backend |
82
+ | `rb.toast({ title?, message, type?, actionLabel?, handleActionClick?, closeOnActionClick?, duration?, onClose? })` | Show a host toast notification. `message` accepts a string or `ReactElement`. `type` can be `"info"`, `"danger"`, `"warning"`, `"caution"`, or `"success"` |
83
+ | `rb.alert({ title?, message, buttonText? })` | Show a host alert dialog. Resolves when the dialog is closed |
84
+ | `rb.confirm({ title?, message, buttonText? })` | Show a host confirmation dialog. `buttonText` accepts `{ yes, no }`. Resolves to `true` or `false` |
85
+ | `rb.prompt({ title?, message, value?, placeholder?, buttonText? })` | Show a host prompt dialog. `buttonText` accepts `{ yes, no }`. Resolves to the entered string or `undefined` |
86
+
87
+ Topic names passed to `pub`, `sub`, `action`, and `serve` are isolated per plugin.
88
+
56
89
 
57
90
  All methods support Generic Types:
58
91
 
@@ -66,91 +99,675 @@ const sub = await rb.sub<{ value: number }>("sensor/data", (data) => {
66
99
  console.log(data.value);
67
100
  });
68
101
  sub.undeclare(); // cleanup
102
+
103
+ const ok = await rb.confirm({
104
+ title: "Run",
105
+ message: "Start the backend action?",
106
+ });
107
+ if (ok) {
108
+ await rb.action("run");
109
+ }
69
110
  ```
70
111
 
71
- ### Topic namespace
112
+ ### Saving and restoring your program command step (`useRbFormData` / `useRbSave`)
72
113
 
73
- Topics are automatically namespaced by plugin ID to prevent collisions across plugins.
114
+ Use these only in your `program.panel` UI, where the user edits your program
115
+ command step: `useRbFormData` gives back the values saved in the step, and
116
+ `useRbSave` hands the current values to Muscat when the step is saved.
74
117
 
118
+ ```tsx
119
+ import { useState } from "react";
120
+ import { useRbFormData, useRbSave } from "@rainbow-robotics/plugin-ui/react";
121
+
122
+ type GripArgs = { targetName: string; width: number };
123
+ const DEFAULTS: GripArgs = { targetName: "", width: 20 };
124
+
125
+ export default function App() {
126
+ // Previously-saved values, or undefined for a brand-new instance.
127
+ const saved = useRbFormData<GripArgs>();
128
+ const [args, setArgs] = useState<GripArgs>({ ...DEFAULTS, ...saved });
129
+
130
+ // Registers a getter the host calls when its Save button is clicked.
131
+ // Always returns the CURRENT values — re-registering on every render keeps
132
+ // the closure fresh, which useRbSave already handles for you.
133
+ useRbSave(() => ({
134
+ args,
135
+ summary: `${args.targetName || "target"} · ${args.width}mm`,
136
+ }));
137
+
138
+ return (/* ...form UI reading/writing `args`... */);
139
+ }
75
140
  ```
76
- "sensor/data" → plugin/{pluginId}/sensor/data (auto-namespaced)
77
- "/broadcast/x" → broadcast/x (leading / = absolute)
78
- ```
141
+
142
+ The value returned from `useRbSave` must be JSON-serialisable — the host
143
+ persists it as-is. Not every host embeds plugins in a save/restore form; if
144
+ yours doesn't, you can skip these hooks entirely.
79
145
 
80
146
  ### UI components
81
147
 
82
- Import Muscat components from `@rainbow-robotics/plugin-ui/react`. The plugin emits
83
- `rb-*` tags; the host renders the actual components.
148
+ Import Muscat components from `@rainbow-robotics/plugin-ui/react` and export a
149
+ React component as the default export. The host mounts that component directly
150
+ and provides `useRb()` through context.
84
151
 
85
152
  ```tsx
86
- import { connect } from "@rainbow-robotics/plugin-ui/connect";
87
- import { Button, Slider, Text, Switch } from "@rainbow-robotics/plugin-ui/react";
153
+ import { useState } from "react";
154
+ import { RBLabelButton, RBSlider, RBSwitch } from "@rainbow-robotics/plugin-ui/react";
88
155
  import { useRb } from "@rainbow-robotics/plugin-ui/sdk";
89
156
 
90
- function App() {
157
+ export default function App() {
91
158
  const rb = useRb();
92
- return <Button label="Run" onPress={() => rb.action("run")} />;
159
+ const [width, setWidth] = useState(20);
160
+ const [enabled, setEnabled] = useState(true);
161
+
162
+ return (
163
+ <div className="flex flex-col gap-4">
164
+ <RBSlider
165
+ value={[width]}
166
+ min={0}
167
+ max={100}
168
+ onValueChange={(values: number[]) => setWidth(values[0] ?? 0)}
169
+ />
170
+ <RBSwitch checked={enabled} onCheckedChange={setEnabled} />
171
+ <RBLabelButton
172
+ disabled={!enabled}
173
+ onClick={() => rb.action("grip", { width })}
174
+ >
175
+ Grip
176
+ </RBLabelButton>
177
+ </div>
178
+ );
93
179
  }
94
-
95
- connect(App);
96
180
  ```
97
181
 
98
182
  ---
99
183
 
100
- ## Backend (`main.py`)
184
+ ## Developing directly on the robot PC
185
+
186
+ If you're running `muscat-pk dev` **on the robot PC itself**, Muscat already serves the
187
+ plugin gateway on port 10000. The CLI sees that, does not start its own local Muscat, and
188
+ your plugin backend connects to the robot's gateway — exactly what you want when
189
+ developing on the robot.
190
+
191
+ ---
192
+
193
+ ## Several developers on one robot
194
+
195
+ Against a remote robot, `muscat-pk dev` / `dev:back` run your plugin under a dev id of its own,
196
+ `<manifest id>.dev-<user>-<random>` (printed at start; the random part is kept in `~/.muscat-pk/dev-id`).
197
+ Two machines developing the same plugin on one robot would otherwise register the same topics on its
198
+ gateway, and a button in one UI would silently run the other machine's backend. The UI, the backend,
199
+ the ROS namespace and external routes (`common/plugin/<dev id>/<path>`) all use the dev id.
200
+
201
+ Pass `--keep-id` to run under the manifest id instead, e.g. to test program `execute` or an external
202
+ client that uses the real id. Then only one machine may be connected to that robot.
203
+
204
+ ---
205
+
206
+ ## Backend runtimes
207
+
208
+ `manifest.json` chooses one backend runtime. `muscat-pk dev` runs backend code in
209
+ a Docker container matching the robot environment. Backend code should contain
210
+ plugin logic only; it should not start its own copy of the plugin gateway.
211
+
212
+ ### Which backend pattern? (depends on the contribution point)
213
+
214
+ **How you control the robot depends on what your plugin contributes** (see
215
+ `manifest.json` → `contributes[].point`). Don't mix these up:
216
+
217
+ | Contribution point | What it is | Backend pattern |
218
+ | --- | --- | --- |
219
+ | `program.panel` (with `commandInfo`) | Defines a **custom command** the plugin adds to the Program page's rule-based sequence builder. The operator places it into the program tree like any built-in command (Move J, Point, Wait, …); it runs when the program reaches that step. | **`PluginProgramContext`** — integrated with the program's stop/pause and variables. See _Program command backend_ below. |
220
+ | `toolbar` / `lnb` / `move.panel` / `program.monitor` | A live UI panel/popover the operator interacts with directly (`program.monitor` is a tab in the Program page's monitor panel, next to General / Digital I/O / Monitor / Log; it is rendered per selected task, and `rb.data` is `{ point: "program.monitor", taskId, robotModel }` — pass it as the `scope` argument of `rb.program.*` so a multi-component robot's calls hit that component's task) | Call the robot **SDK directly** (`RBManipulateSDK`, `RBAmrSDK`) from `py/main.py`, or `rb.action(...)` → your backend → SDK. **Not** a program step — there is no stop/pause/variable context. |
221
+
222
+ Rule of thumb: use `PluginProgramContext` **only** for a `program.panel` command
223
+ that runs as part of a program. To move the robot live from a toolbar/move panel,
224
+ use the SDK — a program-step context does not apply.
225
+
226
+ ### Frontend-only (`none`)
227
+
228
+ Use this when the plugin only has UI and calls host services:
101
229
 
102
- The backend communicates with the frontend using `rainbow_rb_zenoh`.
103
- Set the `prefix` to `plugin/{manifest.id}` to match the frontend's auto-namespace.
230
+ ```json
231
+ { "runtime": { "type": "none" } }
232
+ ```
233
+
234
+ ### Python
104
235
 
105
- ### Python plugin
236
+ Python plugins use `py/main.py` and can communicate with the UI through
237
+ `rainbow.rb_sdk.RBPluginCall`.
106
238
 
107
239
  ```python
108
- import asyncio
109
- from rainbow_rb_zenoh import ZenohRouter, ZenohClient
240
+ import time
241
+ from rainbow.rb_sdk import RBAmrSDK, RBManipulateSDK, RBPluginCall
242
+
243
+ plugin = RBPluginCall()
244
+
110
245
 
111
- router = ZenohRouter(prefix="plugin/__PLUGIN_ID__")
112
- client = ZenohClient()
246
+ plugin.publish("state", {"ready": True})
113
247
 
114
248
 
115
- @router.subscribe("sensor/data")
116
- async def on_sensor(*, dict_payload, **_):
117
- print("received:", dict_payload)
249
+ @plugin.subscribe("event")
250
+ def on_ui_event(payload=None):
251
+ print(payload)
118
252
 
119
253
 
120
- @router.queryable("get-status")
121
- async def get_status(req=None, **_):
122
- return {"ok": True}
254
+ @plugin.serve("tool/echo")
255
+ def echo(payload=None):
256
+ return {"echo": payload}
123
257
 
124
258
 
125
- async def main():
126
- await router.startup()
127
- await asyncio.Event().wait() # keep alive
259
+ def call_frontend():
260
+ res = plugin.call("tool/ping", payload={"hello": "plugin"})
261
+ print(res)
262
+
263
+
264
+ def move_j_example():
265
+ amr = RBAmrSDK()
266
+ manipulate = RBManipulateSDK()
267
+ return manipulate.move.call_move_j(
268
+ robot_model="C500920",
269
+ target={"tar_values": [0, 0, 0, 0, 0, 0, 0], "tar_frame": -1, "tar_unit": 0},
270
+ )
128
271
 
129
272
 
130
273
  if __name__ == "__main__":
131
- asyncio.run(main())
274
+ while True:
275
+ time.sleep(1)
276
+ ```
277
+
278
+ #### External endpoints (`plugin.router`)
279
+
280
+ `plugin.serve` / `plugin.subscribe` / `plugin.publish` are **internal only** (plugin
281
+ UI ↔ backend, plugin ↔ plugin). To expose an endpoint to the outside world —
282
+ REST, Socket.IO, TCP, MQTT, WebRTC — use `plugin.router`. Declaring one is a
283
+ single decorator; the running Muscat (`common`) picks it up without a restart,
284
+ and it disappears when the plugin stops or is removed.
285
+
286
+ > Requires a robot running `rainbow-rb-sdk` **0.0.9.dev5 or newer** (the SDK is
287
+ > provided by the plugin container at runtime). On older robots `plugin.router`
288
+ > does not exist and raises `AttributeError`.
289
+
290
+ ```python
291
+ from rainbow.rb_sdk import RBPluginCall
292
+ from rainbow.rb_sdk.plugin import PluginRouteError
293
+
294
+ plugin = RBPluginCall() # plugin_id comes from env RB_PLUGIN_ID
295
+
296
+
297
+ @plugin.router.on("/hello") # all 5 transports
298
+ def hello(payload: dict, meta: dict):
299
+ return {"msg": f"hi {payload.get('name')}"} # return value = response body
300
+
301
+
302
+ @plugin.router.on("/admin", explicit_routes=["api"]) # REST only
303
+ def admin(payload, meta):
304
+ if not (meta.get("principal") or {}).get("is_admin"):
305
+ raise PluginRouteError(403, "admin only") # becomes the external status
306
+ ...
307
+
308
+
309
+ @plugin.router.subscribe("/cmd", explicit_events=["tcp"]) # receive external TCP publish
310
+ def on_cmd(payload: dict):
311
+ ...
312
+
313
+
314
+ plugin.router.emit("/state", {"busy": True}) # push to external subscribers
315
+ ```
316
+
317
+ | API | Meaning | External address |
318
+ | --- | --- | --- |
319
+ | `router.on(path, explicit_routes=None)` | request/response handler | `common/plugin/{plugin_id}/{path}` (REST is always `POST`) |
320
+ | `router.subscribe(topic, explicit_events=None)` | receive external publishes | external side publishes to `common/plugin/{plugin_id}/{topic}` |
321
+ | `router.emit(topic, payload)` | plugin → external push | external side subscribes to `common/plugin/{plugin_id}/{topic}` |
322
+
323
+ - The `plugin_id` prefix is added by the SDK — write only the path in plugin code.
324
+ - Paths may have several segments (`/tool/echo`). Declaring the same path twice raises `ValueError`.
325
+ - Transport names: `api | socket | tcp | mqtt | webrtc`. `subscribe` accepts only `tcp | mqtt | webrtc`. Unknown names raise `ValueError`.
326
+ - `explicit_routes` / `explicit_events` of `None` means every transport. A request on a transport not listed gets 404.
327
+
328
+ Handler contract:
329
+
330
+ - Signature `(payload, meta)`, `(payload)` or `()` — only the arguments you declare are injected. Sync and async both work.
331
+ - `payload`: what the caller sent (for REST: JSON body merged with query params). The shape is up to the plugin.
332
+ - `meta`: `{"transport": "api"|"socket"|"tcp"|"mqtt"|"webrtc", "principal": dict | None}`. When the robot's login policy is on, `principal` holds the token subject. **Role checks are the plugin's job** — inspect `meta["principal"]` yourself.
333
+ - The return value is the response body (must be JSON-serialisable). `None` → empty response.
334
+ - Status codes: `raise PluginRouteError(code, message)`. Any other exception → 500 with `str(exc)` as the message.
335
+ - `subscribe` handlers take `(payload)` only and return nothing.
336
+
337
+ #### Calling plugin endpoints from outside
338
+
339
+ | Transport | How |
340
+ | --- | --- |
341
+ | REST | `POST /common/plugin/{plugin_id}/{path}` (through the api-gateway), JSON body |
342
+ | Socket.IO | emit event `common/plugin/{plugin_id}/{path}`, response arrives in the ack |
343
+ | TCP | topic `common/plugin/{plugin_id}/{path}` request / publish |
344
+ | MQTT | request: `common/plugin/{plugin_id}/{path}/__req__`, publish: `common/plugin/{plugin_id}/{topic}` |
345
+ | WebRTC | data channel topic `common/plugin/{plugin_id}/{path}` |
346
+
347
+ Auth: when the robot's login policy is on, a token is **required** (any role — GUEST passes). When it is off, calls go straight through. REST uses the same Bearer token as the other Muscat APIs.
348
+
349
+ ```bash
350
+ curl -s -X POST http://<robot>/common/plugin/my-plugin/hello \
351
+ -H 'Content-Type: application/json' -d '{"name":"kim"}'
352
+ ```
353
+
354
+ | Situation | Status |
355
+ | --- | --- |
356
+ | No plugin declares that path / plugin is down / transport not allowed | 404 |
357
+ | Plugin did not answer within 10 s | 504 |
358
+ | `common` ↔ gateway not connected | 503 |
359
+ | Handler raised | 500 (`PluginRouteError` → its `code`) |
360
+
361
+ Known limits:
362
+
363
+ - A plugin path named `install` cannot be called over REST (`POST /common/plugin/{id}/install` is taken by the registry install route first). Socket/TCP are unaffected.
364
+ - Swagger shows one catch-all route, not per-plugin paths.
365
+ - The MQTT broker echoes to the sender: a plugin that `emit`s and `subscribe`s the same topic receives its own push over `mqtt`. Drop `mqtt` from `explicit_events` or use separate topics. (TCP is filtered by `sender_id`.)
366
+ - `on` and `subscribe` may share a name — they live in separate tables.
367
+
368
+ During `muscat-pk dev` the local Muscat runtime takes the `router.*` registrations
369
+ on its gateway (port `10000`), and its `common` (port `3000`) routes
370
+ `common/plugin/...` to them like on the robot — try
371
+ `curl http://localhost:3000/common/plugin/<plugin-id>/<path>`. After restarting the plugin process,
372
+ external calls may return 504 for up to about a minute (until the gateway notices
373
+ the old socket is gone), then 404 — that is expected.
374
+
375
+ #### Program command backend (`program.panel`)
376
+
377
+ A `program.panel` contribution defines a **custom command** your plugin adds to
378
+ the Muscat **Program page**, where the operator builds a robot program as a
379
+ rule-based sequence (a program tree). Your command appears alongside the
380
+ built-ins (Move J, Point, Wait, …); the operator places it into the tree, and
381
+ each time the program executes that step the host calls your backend through
382
+ `PluginProgramContext` — receiving the step args, checking for stop/pause,
383
+ reading/writing program variables, and signalling completion. This pattern is
384
+ **only** for `program.panel`; a toolbar/move-panel plugin uses the SDK directly
385
+ instead (see the table above).
386
+
387
+ ```python
388
+ """
389
+ Plugin backend main.py example (program.panel command).
390
+
391
+ manifest.json:
392
+ "runtime": {"type": "python", "entry": "main.py"},
393
+ "contributes": [{
394
+ "point": "program.panel",
395
+ "ui": "program",
396
+ "label": {"en": "My Plugin Step"},
397
+ "commandInfo": {
398
+ "name": "My Plugin Step",
399
+ "icon": "assets/icon.svg",
400
+ "funcName": "rb_program_sdk.plugin_execute",
401
+ "args": {"message": "hello"} # plugin-author-defined args
402
+ }
403
+ }]
404
+
405
+ The host invokes commandInfo.funcName (rb_program_sdk.plugin_execute) with
406
+ args.plugin_id + your args when the program reaches this step.
407
+ """
408
+ import asyncio
409
+ from rainbow.rb_sdk import PluginProgramContext
410
+
411
+ ctx = PluginProgramContext(plugin_id="my-plugin")
412
+
413
+
414
+ @ctx.on_execute
415
+ async def handle(args: dict) -> None:
416
+ """Called once when my Step executes. Variable writes apply to the
417
+ running program immediately + broadcast to other robots' main tasks."""
418
+ count = await ctx.get_variable("counter") # read an existing program variable
419
+ await ctx.set_variable("counter", count + 1) # update an existing variable — allowed
420
+ await ctx.set_variable("RBP_TEST", 1) # NEW variables must start with RBP_
421
+
422
+ for i in range(100):
423
+ await ctx.check_stop() # auto-scoped to this Step's robot model
424
+ await asyncio.sleep(0.5)
425
+
426
+ await ctx.done()
427
+
428
+
429
+ async def background_monitor() -> None:
430
+ """Resident task outside any Step. If no program is running,
431
+ get returns None and set is silently ignored — always safe to call."""
432
+ while True:
433
+ await asyncio.sleep(1.0)
434
+
435
+ # targets every running main task (no robot_model param — always broadcast)
436
+ await ctx.set_variable("RBP_TEST", 3)
437
+
438
+ # get options
439
+ v1 = await ctx.get_variable("counter") # first value found in the whole tree
440
+ v2 = await ctx.get_variable("counter", robot_model="C500920") # a specific robot
441
+ v3 = await ctx.get_variable("RBP_TEST", plugin_id="my-plugin") # the value my-plugin last wrote
442
+
443
+ if ctx.executor_state("C500920") == "paused":
444
+ print("C500920 program is paused")
445
+
446
+
447
+ async def main() -> None:
448
+ asyncio.create_task(background_monitor())
449
+ await ctx.start() # stays resident
450
+
451
+
452
+ asyncio.run(main())
453
+ ```
454
+
455
+ #### Program variables & executor state (`PluginProgramContext`)
456
+
457
+ `rainbow.rb_sdk.PluginProgramContext` lets the plugin process interact with the
458
+ running program (PyFM): read/write program variables and observe execution
459
+ state — inside a Step handler or from a resident task, at any time.
460
+
461
+ - The `on_execute` handler is called **only when PyFM executes this plugin's
462
+ Step**. One Step execution = one handler call, and the Step does not finish
463
+ until you call `ctx.done()` — always call it.
464
+ - The plugin process itself stays resident after `ctx.start()`; executor state
465
+ queries and variable access below keep working whether or not a Step is
466
+ executing.
467
+
468
+ #### Variable naming rules (important)
469
+
470
+ PyFM variables are a **program-wide shared namespace**. One rule:
471
+
472
+ - **Modifying an existing variable** — the name passes as-is. Exchanging
473
+ variables between the program and the plugin is the intended usage.
474
+ - **Creating a variable that doesn't exist** — the `RBP_` prefix is
475
+ **required** (enforced by the SDK/server; a violation raises
476
+ `RuntimeError`). Applies equally inside and outside Step execution.
477
+
478
+ For new variable names, a convention that includes the plugin id is strongly
479
+ recommended:
480
+
481
+ ```
482
+ RBP_{plugin_id}_{name}
483
+
484
+ e.g. RBP_vision_detect_count, RBP_gripper_last_error
485
+ ```
486
+
487
+ Why:
488
+
489
+ - `RBP_` — instantly identifies plugin-created variables in the program
490
+ editor/UI.
491
+ - `{plugin_id}` — **prevents name collisions with other plugins.** If two
492
+ plugins use the same name they overwrite the same variable (`RBP_` alone is
493
+ not something the SDK guards against).
494
+
495
+ #### Reading / writing variables
496
+
497
+ ```python
498
+ value = await ctx.get_variable("counter") # read (no name restriction)
499
+ await ctx.set_variable("counter", value + 1) # modify existing variable — allowed
500
+ await ctx.set_variable("RBP_my-plugin_result", 42) # create new variable — RBP_ required
501
+ ```
502
+
503
+ `set_variable` always applies to **every running main task** (a plugin belongs
504
+ to muscat, not to a specific robot — there is no `robot_model` parameter):
505
+
506
+ | When | Own program | Other robots' main tasks |
507
+ |---|---|---|
508
+ | During own Step execution | via the control queryable — **immediate** | broadcast — each at its next Step boundary |
509
+ | Outside a Step (resident task, …) | resident endpoint — next Step boundary | same |
510
+
511
+ `get_variable` has options:
512
+
513
+ ```python
514
+ await ctx.get_variable("counter") # search all main tasks (first value found)
515
+ await ctx.get_variable("counter", robot_model="C500920") # a specific robot's value
516
+ await ctx.get_variable("RBP_TEST", plugin_id="A.plugin") # the value that plugin last wrote
517
+ ```
518
+
519
+ Caveats:
520
+
521
+ - If no program is running, `set_variable` is **silently ignored** and
522
+ `get_variable` returns `None`. (`RuntimeError` is raised only for real
523
+ errors, like the `RBP_` violation on a new variable — the message is tagged
524
+ `[plugin:{id}]` with the offending plugin.)
525
+ - Rarely, a write can be lost if it races the worker's variable flush
526
+ (lock-free read-modify-write). For important values, re-check and retry on
527
+ the next cycle.
528
+
529
+ #### Per-plugin value storage (meta)
530
+
531
+ Besides the shared value, each variable entry records which plugin last wrote
532
+ what:
533
+
534
+ ```python
535
+ # state_dict["variables"] — after A wrote 1 and B wrote 2
536
+ "RBP_TEST": {
537
+ "value": 2, # shared slot = last written value
538
+ "type": "local",
539
+ "meta": {"writers": {"A.plugin": 1, "B.plugin": 2}}, # per-plugin record
540
+ }
541
+ ```
542
+
543
+ So the exact value is recoverable even when names collide:
544
+
545
+ - Plugin SDK: `ctx.get_variable("RBP_TEST", plugin_id="A.plugin")` → `1`
546
+ - Program step expression: `COUNT = get_var("A.plugin", "RBP_TEST") + 1` → `2`
547
+ - A plain name (`RBP_TEST += 1`) uses the shared slot (last written value) → `3`
548
+
549
+ #### Executor state
550
+
551
+ Subscribes to the `muscat/executor/state` publish (0.8 s interval) and exposes
552
+ per-robot-model state:
553
+
554
+ ```python
555
+ ctx.executor_state("C500920") # "running" | "paused" | "waiting" | ... | None
556
+ ctx.executor_state() # {"C500920": "paused", "amr": "running"}
557
+ ctx.is_paused("C500920") # whether that model is paused
558
+ ctx.is_stopped("C500920") # stop/error transition of a model seen active this run
559
+ ```
560
+
561
+ - States: `idle | running | paused | waiting | stopped | post_start | completed | error`
562
+ - A model with no running task is `None` / absent from the dict.
563
+ - The publish interval is 0.8 s, so expect up to ~1 s of latency.
564
+ - With `IS_DEV=true` there is no publish at all — always `None`.
565
+
566
+ To get a callback at the moment the state changes, without polling:
567
+
568
+ ```python
569
+ @ctx.on_executor_state_change
570
+ async def on_change(robot_model: str, state: str) -> None:
571
+ if state == "paused":
572
+ print(f"{robot_model} paused")
573
+ ```
574
+
575
+ - Not called for repeated publishes of the same state — fires exactly once
576
+ per per-model state change.
577
+ - An exception in the handler is only logged; state tracking keeps running.
578
+
579
+ #### Responding to pause / stop
580
+
581
+ ```python
582
+ @ctx.on_execute
583
+ async def handle(args: dict) -> None:
584
+ for i in range(100):
585
+ # stop → CancelledError (propagated to the executor as a stop),
586
+ # pause → waits here until resumed.
587
+ # Inside on_execute this automatically watches only the robot model
588
+ # executing this Step.
589
+ await ctx.check_stop()
590
+
591
+ ... # actual work
592
+ await asyncio.sleep(0.5)
593
+
594
+ await ctx.done()
132
595
  ```
133
596
 
134
- ### ROS2 plugin
597
+ Scope priority for `check_stop` / `is_paused` / `is_stopped`:
135
598
 
136
- The node is launched with `--ros-args -r __ns:=/{slug}` automatically by `dev:back`.
137
- ROS2 topics are bridged to the plugin communication layer — no extra code needed.
599
+ 1. `robot_model` given — that model only
600
+ 2. Omitted inside `on_execute` — **only the model executing this Step**
601
+ (passed in by the executor)
602
+ 3. Omitted elsewhere (resident task, …) — all models
603
+
604
+ A handler doing long-running work must call `check_stop()` every loop
605
+ iteration so it reacts to pause/stop immediately. Without it, the plugin keeps
606
+ running even after the operator presses Stop.
607
+
608
+ Variables your plugin wants the Program page variable picker to offer must be
609
+ pre-declared in `manifest.json` under `"variables"` (see the manifest section
610
+ below).
611
+
612
+ ### C++
613
+
614
+ C++ plugins use `cpp/` and are built with CMake inside Docker against the
615
+ Rainbow Robotics C++ SDK (see _C++ SDK_ below).
616
+
617
+ ```cpp
618
+ #include <chrono>
619
+ #include <cstdlib>
620
+ #include <iostream>
621
+ #include <string>
622
+ #include <thread>
623
+
624
+ int main() {
625
+ const char* plugin_id = std::getenv("RB_PLUGIN_ID");
626
+ std::cout << "[__PLUGIN_NAME__] backend ready"
627
+ << (plugin_id ? std::string(" (id=") + plugin_id + ")" : "")
628
+ << std::endl;
629
+
630
+ while (true) {
631
+ std::this_thread::sleep_for(std::chrono::seconds(1));
632
+ }
633
+ return 0;
634
+ }
635
+ ```
636
+
637
+ #### C++ SDK (`rainbow-rb-sdk-dev`)
638
+
639
+ Both C++ runtimes (`cpp`, `ros2` + `rclcpp`) link the Rainbow Robotics C++ SDK
640
+ from the apt package `rainbow-rb-sdk-dev`. The `muscat-pk` dev containers already
641
+ have it, so `muscat-pk dev` / `build` need nothing extra. To build outside the
642
+ container (your own Ubuntu 24.04 machine, or the robot PC):
643
+
644
+ ```bash
645
+ curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/rb-sdk-apt.gpg
646
+ echo 'deb [signed-by=/usr/share/keyrings/rb-sdk-apt.gpg] https://rainbowrobotics.github.io/rb-sdk stable main' | sudo tee /etc/apt/sources.list.d/rb-sdk.list
647
+ sudo apt update && sudo apt install rainbow-rb-sdk-dev
648
+ ```
649
+
650
+ - CMake: `find_package(rb_sdk REQUIRED)` + `target_link_libraries(<target> rb_sdk::rb_sdk)` (C++20). pkg-config: `rb_sdk`.
651
+ - Headers: `<rainbow/rb_sdk/manipulate/rb_manipulate_sdk.hpp>` (`RBManipulateSDK`),
652
+ `<rainbow/rb_sdk/program/plugin_program_context.hpp>` (`rb_plugin::PluginProgramContext`, for `program.panel` commands — the header's top comment has a full `main()` example).
653
+ - The binary is linked **dynamically**; `muscat-pk build` copies the matching `librb_sdk.so` into `dist/<arch>/lib/` (the robot has no SDK installed), so each plugin keeps the SDK version it was built with. The version is the one pinned in `CMakeLists.txt` (`find_package(rb_sdk <version> EXACT REQUIRED)`); change it with `muscat-pk sdk update <version>`.
654
+ - UI ↔ backend messaging works from C++ too: `rb_plugin::RBPluginCall` in `<rainbow/rb_sdk/plugin/rb_plugin.hpp>` (see the scaffolded `cpp/main.cpp`).
655
+
656
+ ### ROS2 (rclpy)
657
+
658
+ ROS2 Python plugins use `ros2_ws/` with `runtime.rosClient: "rclpy"`. relative topic names are placed under the plugin's ROS namespace — the plugin id
659
+ with "." → "/" and "-" → "_" (`com.example.my-plugin` → `/com/example/my_plugin`), in dev
660
+ and on the robot. Use an absolute name (`/cmd_vel`) for topics shared with other nodes.
661
+ `RBPluginCall` works in the node module too (UI messaging and `plugin.router`).
138
662
 
139
663
  ```python
140
664
  # ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py
141
665
  import rclpy
142
666
  from rclpy.node import Node
143
- from std_msgs.msg import Float32
667
+ from rainbow.rb_sdk import RBManipulateSDK
668
+
669
+ sdk = RBManipulateSDK()
670
+
144
671
 
145
672
  class __PLUGIN_CLASS__Node(Node):
146
673
  def __init__(self):
147
674
  super().__init__("__PLUGIN_SLUG___node")
148
- # relative topic name — __ns prefix applied automatically
149
- self.pub = self.create_publisher(Float32, "sensor_data", 10)
150
- self.create_timer(1.0, self.timer_cb)
675
+ self.get_logger().info("__PLUGIN_NAME__ node started")
676
+ # use sdk alongside ROS2 publishers/subscribers/services as needed
677
+ ```
678
+
679
+
680
+ #### Talking to an external PC over ROS 2
681
+
682
+ ROS 2 discovery stays on the robot (`ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST`; `muscat-pk dev`
683
+ runs the same), so robots on one LAN don't mix topics. Allow specific PCs from the plugin UI —
684
+ Muscat keeps the list per plugin, starts your backend with `ROS_STATIC_PEERS` set to it, and
685
+ restarts the plugin when it changes. Your ROS code needs no changes; the PC needs no extra settings.
686
+
687
+ ```tsx
688
+ const rb = useRb();
689
+ const peers = await rb.rosPeers.get(); // ["192.168.0.50"]
690
+ await rb.rosPeers.set(["192.168.0.50", "192.168.0.51"]); // saves + restarts the plugin; rejects on a bad IP
691
+ ```
692
+
693
+ - Per plugin: plugin B keeps its allowed PCs when plugin A (allowing the same PC) is stopped or removed.
694
+ - It opens communication; it is not access control — a PC that knows the robot's IP can still
695
+ add the robot to its own `ROS_STATIC_PEERS`.
696
+ - In `muscat-pk dev` the list is kept in `.muscat-dev/ros-peers.json` (ROS 2 backends).
697
+
698
+ ### ROS2 (rclcpp)
699
+
700
+ ROS2 C++ plugins use `ros2_ws/` with `runtime.rosClient: "rclcpp"`. The node links
701
+ the Rainbow C++ SDK the same way as the `cpp` runtime (see _C++ SDK_ above).
702
+
703
+ ```cpp
704
+ #include "rclcpp/rclcpp.hpp"
705
+
706
+ class __PLUGIN_CLASS__Node : public rclcpp::Node {
707
+ public:
708
+ __PLUGIN_CLASS__Node() : Node("__PLUGIN_SLUG___node") {
709
+ RCLCPP_INFO(this->get_logger(), "__PLUGIN_NAME__ node started");
710
+ }
711
+ };
712
+ ```
713
+
714
+ ---
715
+
716
+ ## Video streaming (MediaMTX)
717
+
718
+ Muscat runs a MediaMTX media server. The backend publishes a stream over RTSP and
719
+ the UI plays it over WebRTC. `muscat-pk dev` runs the same server (or uses a local
720
+ Muscat's), so the addresses are identical in dev and on the robot.
721
+
722
+ | | Address |
723
+ | --- | --- |
724
+ | Publish (backend) | `rtsp://127.0.0.1:8554/<plugin_id>/<name>` |
725
+ | Play (UI) | `http://<host>:8889/<plugin_id>/<name>/` |
726
+
727
+ - Prefix the path with your plugin id — paths are shared with robot cameras and other plugins.
728
+ - The robot runtime has no ffmpeg, GStreamer or OpenCV; bring your own encoder (below).
151
729
 
152
- def timer_cb(self):
153
- self.pub.publish(Float32(data=42.0))
730
+ **Backend (Python)** — use PyAV: add `av` and `numpy` to `requirements.txt`. Its wheels
731
+ include FFmpeg, so they ship in the ZIP like any other dependency.
732
+
733
+ ```python
734
+ import fractions, time
735
+ import av, numpy as np
736
+
737
+ out = av.open("rtsp://127.0.0.1:8554/__PLUGIN_ID__/cam", mode="w", format="rtsp",
738
+ options={"rtsp_transport": "tcp"})
739
+ stream = out.add_stream("libx264", rate=25, options={"preset": "ultrafast", "tune": "zerolatency"})
740
+ stream.width, stream.height, stream.pix_fmt = 640, 360, "yuv420p"
741
+
742
+ i = 0
743
+ while True:
744
+ img = np.zeros((360, 640, 3), dtype=np.uint8) # your RGB frame here
745
+ frame = av.VideoFrame.from_ndarray(img, format="rgb24")
746
+ frame.pts, frame.time_base = i, fractions.Fraction(1, 25)
747
+ for packet in stream.encode(frame):
748
+ out.mux(packet)
749
+ i += 1
750
+ time.sleep(1 / 25)
751
+ ```
752
+
753
+ Run the encoder loop in a thread (or `asyncio.to_thread`) so it doesn't block the
754
+ `RBPluginCall` handlers.
755
+
756
+ **UI** — embed MediaMTX's WebRTC player. `location.hostname` is `localhost` in the
757
+ preview and the robot's address on the robot:
758
+
759
+ ```tsx
760
+ const STREAM = "__PLUGIN_ID__/cam"; // the path the backend publishes
761
+
762
+ export default function Camera() {
763
+ return (
764
+ <iframe
765
+ src={`${location.protocol}//${location.hostname}:8889/${STREAM}/`}
766
+ allow="autoplay"
767
+ className="aspect-video w-full border-0"
768
+ />
769
+ );
770
+ }
154
771
  ```
155
772
 
156
773
  ---
@@ -159,30 +776,186 @@ class __PLUGIN_CLASS__Node(Node):
159
776
 
160
777
  ```jsonc
161
778
  {
162
- "id": "com.example.my-plugin", // unique plugin ID (reverse-domain recommended)
779
+ // unique plugin ID — reverse-domain format recommended
780
+ "id": "com.example.my-plugin",
163
781
  "name": "My Plugin",
164
782
  "version": "1.0.0",
165
- "minAppVersion": "2.0.0",
166
- "icon": "assets/icon.svg",
783
+
784
+ // minimum Muscat version required to load this plugin
785
+ "minMuscatVersion": "1.0.0",
786
+
787
+ // plugin author (empty string is fine)
788
+ "author": "Your Name",
789
+
790
+ // displayed in the Muscat plugin store / info panel
791
+ // string or { en (required), ko, ja, zh, vi, th, es }
792
+ "description": { "en": "Plugin description", "ko": "플러그인 설명" },
793
+
794
+ // SVG string (single file) or PNG/JPG size map (all three sizes required)
795
+ "icons": "assets/icon.svg",
796
+ // "icons": { "16": "assets/icon-16.png", "48": "assets/icon-48.png", "128": "assets/icon-128.png" },
797
+
798
+ // targetModels and/or targetCategorys — at least one is required
799
+ // "all" = every target, or list specific values
800
+ "targetModels": "all",
801
+ // "targetModels": ["C500920", "C500930"],
802
+ "targetCategorys": "all",
803
+ // "targetCategorys": ["manipulate", "amr", "rby1", "vision"],
167
804
 
168
805
  "contributes": [
169
- { "point": "program.panel", "label": "My Plugin", "icon": "assets/icon.svg" },
170
- { "point": "lnb", "label": "My Plugin", "icon": "assets/icon.svg" },
171
- { "point": "header.toolbar","label": "My Plugin", "icon": "assets/icon.svg", "opens": "popover" }
806
+ // program.panel — defines a custom command for the Program page sequence
807
+ // builder; runs via PluginProgramContext when the program reaches it
808
+ {
809
+ "point": "program.panel",
810
+ "icon": "assets/icon.svg",
811
+ "label": { "ko": "내 플러그인", "en": "My Plugin" },
812
+ "ui": "program", // must be unique across all contributions
813
+ "commandInfo": {
814
+ "name": "My Plugin",
815
+ "icon": "assets/icon.svg",
816
+ "funcName": "rb_program_sdk.plugin_execute",
817
+ "args": {}
818
+ }
819
+ },
820
+ // toolbar — adds a button to the top toolbar (required)
821
+ {
822
+ "point": "toolbar",
823
+ "icon": "assets/icon.svg",
824
+ "label": { "ko": "내 플러그인", "en": "My Plugin" },
825
+ "ui": "content", // must be unique across all contributions
826
+ "opens": "popover" // popover | panel | page
827
+ },
828
+ // move.panel — adds a panel to the Move screen
829
+ {
830
+ "point": "move.panel",
831
+ "icon": "assets/icon.svg",
832
+ "label": { "ko": "내 플러그인", "en": "My Plugin" },
833
+ "ui": "move" // must be unique across all contributions
834
+ }
835
+ ],
836
+
837
+ // optional: variables this plugin exposes to pyfm programs — the Program
838
+ // page variable picker lists ONLY declared variables. name must start with
839
+ // RBP_; description is optional but without it the UI can't explain the
840
+ // variable, so write one.
841
+ "variables": [
842
+ { "name": "RBP_grip_force", "type": "number", "init": 0.5, "description": { "ko": "그리퍼 힘", "en": "Gripper force" } },
843
+ { "name": "RBP_target_pose", "type": "array", "init": [0, 0, 0] }
172
844
  ],
173
845
 
174
- // "python" | "ros2"
175
- "runtime": { "type": "python", "entry": "main.py" }
846
+ // runtime — one of:
847
+ // { "type": "none" }
848
+ // { "type": "python", "entry": "main.py" }
849
+ // { "type": "cpp", "entry": "<executable name>" }
850
+ // { "type": "ros2", "rosClient": "rclpy" | "rclcpp", "package": "...", "launch": "..._node" }
851
+ "runtime": { "type": "python", "entry": "main.py" },
852
+
853
+ // optional: TCP ports opened by this plugin backend
854
+ // only number[] is allowed; ranges/objects are not accepted
855
+ // external plugin ports must be within 28600-28999
856
+ "ports": [28600]
176
857
  }
177
858
  ```
178
859
 
860
+ **Backend types:**
861
+
862
+ | type | source dir | build / run | entry |
863
+ |---|---|---|---|
864
+ | `python` | `py/` | pip + run in Docker | `main.py` |
865
+ | `cpp` | `cpp/` | CMake in Docker | built executable name |
866
+ | `ros2` (rclpy) | `ros2_ws/` | colcon (ament_python) in Docker | — |
867
+ | `ros2` (rclcpp) | `ros2_ws/` | colcon (ament_cmake) in Docker | — |
868
+
869
+ > Every back end runs in a Docker container matching the robot (Ubuntu 24.04 + Python 3.12), so dev behaves like production and compiled binaries are ABI-compatible. **Docker is required** for all backend types (only a frontend-only `none` plugin skips it). No local Python/toolchain setup is needed.
870
+
179
871
  **Contribution points:**
180
872
 
181
- | point | Where it appears |
182
- |---|---|
183
- | `program.panel` | Program panel tab |
184
- | `lnb` | Left navigation bar |
185
- | `header.toolbar` | Header toolbar button |
873
+ | point | Where it appears | Backend | `commandInfo` |
874
+ |---|---|---|:---:|
875
+ | `toolbar` | Top toolbar button (must have at least one) | SDK / `rb.action` | — |
876
+ | `lnb` | Left navigation entry | SDK / `rb.action` | — |
877
+ | `move.panel` | Move screen panel | SDK / `rb.action` | — |
878
+ | `program.panel` | Custom command in the Program page sequence builder | `PluginProgramContext` | ✓ |
879
+
880
+ **`commandInfo` fields** (for `program.panel`):
881
+
882
+ | field | type | description |
883
+ |---|---|---|
884
+ | `name` | `string` | Display name of the command in the program tree |
885
+ | `icon` | `string` | Icon path |
886
+ | `iconClassName` | `string?` | Optional CSS class for the icon |
887
+ | `funcName` | `string?` | Backend function name to call |
888
+ | `args` | `Record<string, any>?` | Default arguments |
889
+ | `disabled` | `boolean?` | Whether the node is disabled |
890
+ | `allowChildren` | `string[]?` | Allowed child commands |
891
+ | `allowParents` | `string[]?` | Allowed parent commands |
892
+ | `notAllowChildren` | `string[]?` | Disallowed child commands |
893
+ | `syncGroupId` | `boolean?` | Sync group ID across children |
894
+ | `disableCopy` | `boolean?` | Prevent copy |
895
+ | `disableDelete` | `boolean?` | Prevent delete |
896
+ | `requireChildren` | `boolean?` | Whether a child command is required |
897
+ | `requireParent` | `boolean?` | Whether a parent command is required |
898
+ | `childrenCommand` | `string?` | Default command type for children |
899
+
900
+ **Validation rules:**
901
+ - `id`, `name`, `version`, `minMuscatVersion` — required non-empty strings
902
+ - `author` — required non-empty string
903
+ - `description` — required (`string` or `{ en (required), ko?, ja?, zh?, vi?, th?, es? }`)
904
+ - `icons` — SVG: single string path ending in `.svg`; PNG/JPG: object with `"16"`, `"48"`, `"128"` all present
905
+ - `targetModels` — `"all"` or a non-empty array of robot model ID strings (at least one of `targetModels` / `targetCategorys` required)
906
+ - `targetCategorys` — `"all"` or a non-empty array of `"manipulate" | "amr" | "rby1" | "vision"` (at least one of `targetModels` / `targetCategorys` required)
907
+ - `contributes` — the plugin's screens; when present it must contain at least one `toolbar` entry. Leave it out (or `[]`) for a backend-only plugin: no UI is built, and Node.js is not needed to build it
908
+ - `ui` — each contribution must have a unique value
909
+ - `commandInfo` — required when `point` is `"program.panel"`
910
+ - `ports` — optional `number[]`; every port must be within `28600-28999`
911
+ and must not be a reserved robot/industrial/internal port
912
+ - `variables` — optional array of `{ name, type, init, description? }`:
913
+ - `name` — must start with `RBP_`, then letters/digits/underscore; unique per plugin
914
+ - `type` — `"number" | "string" | "array"`
915
+ - `init` — required initial value, must match `type`
916
+ - `description` — optional `string` or i18n object (`en` required in object form)
917
+
918
+ `muscat-pk build` throws on any validation error. `muscat-pk dev` prints
919
+ warnings — except `variables` errors (e.g. a name without the `RBP_` prefix),
920
+ which are hard errors in `dev` / `dev:back` too.
921
+
922
+ **Backend port rules:**
923
+ - Declare a port only if your backend opens a TCP server/listener.
924
+ - **Every listening TCP port must be declared.** `muscat-pk dev` and
925
+ `muscat-pk dev:back` watch the backend and stop it the moment it opens a port
926
+ that is not in `ports`. A port outside `28600-28999` can't be declared, so it
927
+ must not be opened either — picking e.g. `5500` is not a way around the rule.
928
+ It would escape this check *and* the robot's install-time conflict check, and
929
+ only collide once it's running on a robot.
930
+ - Reserved ports include `502`, `2222`, `4840`, `5170-5179`, `8000-8999`,
931
+ `10000`, `30004`, `34964`, `44818`, `50000-50005`, and `50051`.
932
+ - The robot rejects installation (HTTP 409) if a declared port is reserved,
933
+ out of range, or already claimed by another enabled plugin.
934
+
935
+ **Reaching your backend's ports during development:**
936
+ - Declared ports are published to `127.0.0.1` on your machine, so
937
+ `curl http://127.0.0.1:28600` works from the host on macOS, Windows, and Linux.
938
+ - On native Linux the dev container uses host networking, so the ports are on
939
+ the host already.
940
+ - If a declared port is already taken on your machine, `muscat-pk dev` warns and
941
+ skips publishing that one — the backend still listens inside the container.
942
+
943
+ ---
944
+
945
+ ## Localized documentation
946
+
947
+ You can ship per-language README files alongside the main `README.md`. Place them in the project root:
948
+
949
+ ```
950
+ README.md # default / English fallback
951
+ README.ko.md # Korean
952
+ README.en.md # explicit English (optional)
953
+ README.ja.md # Japanese
954
+ ```
955
+
956
+ All `README*.md` files found at the project root are automatically copied to `dist/` on
957
+ `muscat-pk build`. Muscat displays the file matching the user's locale,
958
+ falling back to `README.md` if none matches.
186
959
 
187
960
  ---
188
961
 
@@ -197,37 +970,119 @@ numpy==1.26.4
197
970
  opencv-python
198
971
  ```
199
972
 
200
- Then just run `muscat-pk dev` or `muscat-pk dev:back` — dependencies are installed automatically on every startup:
973
+ Run `muscat-pk dev` or `muscat-pk dev:back` — dependencies are installed automatically
974
+ inside the dev container (`pip install -r requirements.txt`) on every startup. No
975
+ local Python is required for this.
201
976
 
202
- - **Python plugin**: installed into `.venv` via `pip install -r requirements.txt`
203
- - **ROS2 plugin**: installed inside the Docker container via `pip install -r requirements.txt`
977
+ `muscat-pk dev:back` also best-effort creates/refreshes a local `.venv` in the
978
+ background (via `uv` if available, else your system Python) with the same deps —
979
+ purely so your editor (pylint, pyright, mypy, ...) can resolve imports and lint
980
+ correctly. It's not used to run the backend; Docker is the actual execution
981
+ environment, so this step is silently skipped if no local Python toolchain is
982
+ available. Point your editor's Python interpreter at `.venv`.
204
983
 
205
- No extra steps needed.
984
+ `muscat-pk sdk update` also refreshes `.venv` in place if one already exists, so
985
+ your editor picks up the new `rainbow-rb-sdk` version immediately.
206
986
 
207
- > `muscat-pk build` / `muscat-pk pack` only **pins** versions in `requirements.txt` — it does not run `pip install`.
987
+ > `muscat-pk build` only **pins** versions in `requirements.txt` — it does not run `pip install` (the robot installs them at load time).
208
988
 
209
989
  ---
210
990
 
211
991
  ## Switching backend type
212
992
 
213
993
  ```bash
214
- muscat-pk type change # interactive
215
- muscat-pk type change --to ros2 # python → ros2
216
- muscat-pk type change --to python # ros2 → python
994
+ muscat-pk type change # interactive
995
+ muscat-pk type change --to cpp # → C++
996
+ muscat-pk type change --to ros2 # → ROS2 (rclpy / Python)
997
+ muscat-pk type change --to ros2-cpp # → ROS2 (rclcpp / C++)
998
+ muscat-pk type change --to python # → Python
999
+ ```
1000
+
1001
+ > Existing backend dirs (`ros2_ws/`, `cpp/`) are not deleted when switching away. Remove them manually if no longer needed.
1002
+
1003
+ > Switching between a Python and a C++ backend asks whether to replace this README with the matching one (the old file is kept as `README.md.bak`). Add `--readme` to answer yes without the prompt.
1004
+
1005
+ ---
1006
+
1007
+ ## AI coding assistants (MCP)
1008
+
1009
+ If you build your plugin with an AI assistant (Claude Code, Codex, …), give it
1010
+ grounded knowledge of the Muscat UI components, design tokens, and SDK instead of
1011
+ letting it guess. `muscat-pk mcp` is an MCP server exposing:
1012
+
1013
+ - `list_components` / `get_component_props` / `get_component_example` — every
1014
+ `@rainbow-robotics/plugin-ui/react` component with its real props and a working
1015
+ Storybook example.
1016
+ - `list_design_tokens` / `search_design_tokens` — the Tailwind classes to use
1017
+ (`bg-bg-*`, `text-fg-*`, `typo-*`, …) so your UI matches the host theme.
1018
+ - `search_rb_sdk` — how to use `useRb()`, `rb.*`, `useRbFormData`/`useRbSave`, and
1019
+ the backend runtimes.
1020
+ - `list_rb_sdk` / `get_rb_sdk_api` — the installed `rainbow-rb-sdk` (Python
1021
+ backend) classes and signatures; with `lang: "cpp"`, the C++ SDK headers
1022
+ (`rainbow-rb-sdk-dev`, read out of the `muscat-pk` cpp dev image — build it
1023
+ once with `muscat-pk dev:back`).
1024
+ - `check_project` — what this project must change for the plugin-ui it has
1025
+ installed (old APIs such as `connect(App)` or `Slider`, a `vite.config` without
1026
+ the `muscat()` preset, `tsc` errors), the same list `muscat-pk update` prints.
1027
+ Ask the assistant to fix the project after an update; it re-checks until clean.
1028
+
1029
+ **Browse the components live** with `muscat-pk storybook`.
1030
+
1031
+ `muscat-pk new` also writes `AGENTS.md` (read by Codex, Cursor, Gemini CLI, Copilot)
1032
+ and a `CLAUDE.md` that imports it: the UI rules (plugin-ui components instead of
1033
+ native `<select>`/`<input>`/`<button>`, design tokens for colors) for assistants that
1034
+ do not call the MCP tools on their own. Add your own project notes there.
1035
+
1036
+ ### Register the server
1037
+
1038
+ **Claude Code** — a `.mcp.json` is already scaffolded at your plugin root, so
1039
+ it works out of the box:
1040
+
1041
+ ```json
1042
+ {
1043
+ "mcpServers": {
1044
+ "muscat": {
1045
+ "command": "muscat-pk",
1046
+ "args": ["mcp"]
1047
+ }
1048
+ }
1049
+ }
1050
+ ```
1051
+
1052
+ **Codex** — register once with:
1053
+
1054
+ ```bash
1055
+ muscat-pk mcp --install codex
1056
+ ```
1057
+
1058
+ This appends the server to `~/.codex/config.toml` (idempotent — safe to run
1059
+ again). Equivalent manual config:
1060
+
1061
+ ```toml
1062
+ [mcp_servers.muscat]
1063
+ command = "muscat-pk"
1064
+ args = ["mcp"]
217
1065
  ```
218
1066
 
219
- > `ros2_ws/` is not deleted when switching back to python. Remove it manually if no longer needed.
1067
+ Any other MCP client works too — it's a standard stdio MCP server started with
1068
+ `muscat-pk mcp`. If `muscat-pk` isn't on the client's PATH, use its absolute path
1069
+ (`which muscat-pk`) as `command`.
220
1070
 
221
1071
  ---
222
1072
 
223
1073
  ## Shipping
224
1074
 
225
1075
  ```bash
226
- muscat-pk pack
1076
+ muscat-pk build # build + ZIP
227
1077
  ```
228
1078
 
229
- - Builds `src/content.tsx` → `dist/content.js`
1079
+ > Building the other arch (e.g. arm64 on an amd64 PC) runs in an emulated container for
1080
+ > C++ backends. Docker Desktop has it built in; on Linux enable it once with
1081
+ > `docker run --privileged --rm tonistiigi/binfmt --install all`, or pass `--arch` for one arch.
1082
+
1083
+ - Builds `ui/content.tsx` → `dist/content.js` (minified)
230
1084
  - Pins `requirements.txt` to latest versions (stable-only on `main` branch)
1085
+ - Copies `assets/`, `manifest.json`, `README*.md`, `py/`, and `requirements.txt` into `dist/`
231
1086
  - ROS2: runs `colcon build` inside the Jazzy container; includes `ros2_ws/install/`
232
1087
  - Output: `__PLUGIN_DIR__-{version}.zip`
233
1088
 
@@ -235,10 +1090,15 @@ ZIP layout:
235
1090
 
236
1091
  ```
237
1092
  __PLUGIN_DIR__-1.0.0.zip
238
- ├─ dist/ # built UI
239
- ├─ main.py
240
- ├─ requirements.txt # pinned
241
- └─ ros2_ws/install/ # ROS2 only
1093
+ ├─ dist/
1094
+ │ ├─ content.js # built UI
1095
+ │ ├─ manifest.json
1096
+ │ ├─ assets/
1097
+ │ ├─ README.md
1098
+ │ ├─ README.ko.md # if present
1099
+ │ ├─ main.py # from py/
1100
+ │ └─ requirements.txt # pinned
1101
+ └─ ros2_ws/install/ # ROS2 only
242
1102
  ```
243
1103
 
244
1104
  ---
@@ -249,8 +1109,9 @@ __PLUGIN_DIR__-1.0.0.zip
249
1109
  muscat-pk dev # UI + backend
250
1110
  muscat-pk dev:ui # UI preview only (preview-host)
251
1111
  muscat-pk dev:back # backend only (Python: watchfiles / ROS2: Docker)
252
- muscat-pk build # UI build only → dist/
253
- muscat-pk pack # build + ZIP
1112
+ muscat-pk build # build → dist/ + distributable ZIP
1113
+ muscat-pk storybook # browse the rb-components UI library in a browser
1114
+ muscat-pk mcp # MCP server for AI assistants (components/tokens/SDK docs)
254
1115
  ```
255
1116
 
256
1117
  ```bash
@@ -258,4 +1119,130 @@ muscat-pk type change # switch backend type
258
1119
  muscat-pk sdk list # show current Python SDK version
259
1120
  muscat-pk sdk update # update Python SDK to latest
260
1121
  muscat-pk update # update muscat-plugin-kit itself
1122
+ muscat-pk uninstall # remove muscat-pk itself
1123
+ ```
1124
+
1125
+ ---
1126
+
1127
+ ## Styles
1128
+
1129
+ `muscat()` automatically loads `ui/style.{css,scss,sass,less}` (or
1130
+ `ui/styles.{css,scss,sass,less}`) for every UI entry, plus
1131
+ `ui/{entry}.{css,scss,sass,less}` for a matching entry-specific stylesheet. You
1132
+ do not need to import CSS from every `.tsx` file.
1133
+
1134
+ The scaffold uses Tailwind's theme/utilities layers only, so plugin utilities
1135
+ are generated from files under `ui/` without applying Tailwind preflight resets
1136
+ to the host page.
1137
+
1138
+ Built CSS selectors are scoped under the current plugin id
1139
+ (`[data-muscat-plugin-id="..."]`). CSS-in-JS libraries such as Emotion work
1140
+ normally, but global styles should be scoped by the author.
1141
+
1142
+ ### Tailwind
1143
+
1144
+ This template enables Tailwind in `vite.config.ts`:
1145
+
1146
+ ```ts
1147
+ import { defineConfig } from "vite";
1148
+ import tailwindcss from "@tailwindcss/vite";
1149
+ import { muscat } from "@rainbow-robotics/muscat-plugin-kit/vite";
1150
+
1151
+ export default defineConfig({
1152
+ plugins: [tailwindcss(), muscat()],
1153
+ });
1154
+ ```
1155
+
1156
+ The default `ui/style.css` uses Tailwind theme/utilities only, not preflight:
1157
+
1158
+ ```css
1159
+ @layer theme, utilities;
1160
+
1161
+ @import "tailwindcss/theme.css" layer(theme);
1162
+ @import "tailwindcss/utilities.css" layer(utilities);
1163
+
1164
+ @source "./**/*.{ts,tsx}";
1165
+ ```
1166
+
1167
+ ### SCSS, Sass, and Less
1168
+
1169
+ Install the preprocessor package, then create one of the auto-loaded style files.
1170
+
1171
+ ```bash
1172
+ npm i -D sass
261
1173
  ```
1174
+
1175
+ ```scss
1176
+ /* ui/style.scss */
1177
+ $accent: #3b82f6;
1178
+
1179
+ .panel {
1180
+ border: 1px solid color-mix(in srgb, $accent 40%, transparent);
1181
+
1182
+ &__title {
1183
+ color: $accent;
1184
+ }
1185
+ }
1186
+ ```
1187
+
1188
+ For Less, install `less` and use `ui/style.less`.
1189
+
1190
+ ### Emotion
1191
+
1192
+ Emotion dependencies may be in `dependencies` or `devDependencies`; Vite can
1193
+ bundle them when they are installed and imported by the plugin.
1194
+
1195
+ For `styled`, no Vite config change is needed:
1196
+
1197
+ ```bash
1198
+ npm i @emotion/react @emotion/styled
1199
+ ```
1200
+
1201
+ ```tsx
1202
+ import styled from "@emotion/styled";
1203
+
1204
+ const Panel = styled.div({
1205
+ display: "grid",
1206
+ gap: 12,
1207
+ });
1208
+ ```
1209
+
1210
+ For Emotion's `css` prop, pass React plugin options through `muscat()` and add
1211
+ `jsxImportSource` to `tsconfig.json`:
1212
+
1213
+ ```bash
1214
+ npm i @emotion/react @emotion/styled
1215
+ npm i -D @emotion/babel-plugin
1216
+ ```
1217
+
1218
+ ```ts
1219
+ // vite.config.ts
1220
+ import { defineConfig } from "vite";
1221
+ import tailwindcss from "@tailwindcss/vite";
1222
+ import { muscat } from "@rainbow-robotics/muscat-plugin-kit/vite";
1223
+
1224
+ export default defineConfig({
1225
+ plugins: [
1226
+ tailwindcss(),
1227
+ muscat({
1228
+ react: {
1229
+ jsxImportSource: "@emotion/react",
1230
+ babel: {
1231
+ plugins: ["@emotion/babel-plugin"],
1232
+ },
1233
+ },
1234
+ }),
1235
+ ],
1236
+ });
1237
+ ```
1238
+
1239
+ ```json
1240
+ {
1241
+ "compilerOptions": {
1242
+ "jsxImportSource": "@emotion/react"
1243
+ }
1244
+ }
1245
+ ```
1246
+
1247
+ Emotion `Global` writes global CSS at runtime, so scope it manually or avoid it
1248
+ inside plugins.