@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.9 → 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
package/README.md CHANGED
@@ -1,106 +1,1186 @@
1
1
  # @rainbow-robotics/muscat-plugin-kit
2
2
 
3
- Muscat 플러그인 개발 CLI. 스캐폴딩, 빌드/팩, 개발 서버(UI + 백엔드)를 제공합니다.
3
+ CLI for developing Muscat plugins (`muscat-pk`): scaffolding, dev servers (UI + backend), build/pack, and the SDK references a plugin author needs.
4
4
 
5
- ## 설치
5
+ [한국어 문서 (README.ko.md)](./README.ko.md)
6
6
 
7
- ### 방법 1 — npm (Node.js 20+ 필요)
7
+ ## Contents
8
8
 
9
- ```bash
10
- # 최신 stable
11
- npm install -g @rainbow-robotics/muscat-plugin-kit
9
+ 1. [Install](#1-install)
10
+ 2. [Quick start](#2-quick-start)
11
+ 3. [Commands](#3-commands)
12
+ 4. [Project layout](#4-project-layout)
13
+ 5. [manifest.json](#5-manifestjson)
14
+ 6. [Plugin UI SDK (`useRb`)](#6-plugin-ui-sdk-userb)
15
+ 7. [Python plugin SDK](#7-python-plugin-sdk)
16
+ 8. [Other backend runtimes (C++ / ROS 2)](#8-other-backend-runtimes-c--ros-2)
17
+ 9. [Development environment](#9-development-environment)
18
+ 10. [Styles](#10-styles)
19
+ 11. [AI coding agent setup (`muscat-pk mcp`)](#11-ai-coding-agent-setup-muscat-pk-mcp)
20
+ 12. [Environment variables](#12-environment-variables)
21
+ 13. [Troubleshooting](#13-troubleshooting)
22
+
23
+ ---
12
24
 
13
- # dev 채널 (최신 개발 빌드)
14
- npm install -g @rainbow-robotics/muscat-plugin-kit@dev
25
+ ## 1. Install
15
26
 
16
- # 특정 버전
17
- npm install -g @rainbow-robotics/muscat-plugin-kit@1.2.3
27
+ ### npm (Node.js 20+)
28
+
29
+ ```bash
30
+ npm install -g @rainbow-robotics/muscat-plugin-kit # latest stable
31
+ npm install -g @rainbow-robotics/muscat-plugin-kit@dev # dev channel
32
+ npm install -g @rainbow-robotics/muscat-plugin-kit@1.2.3 # specific version
18
33
  ```
19
34
 
20
- ### 방법 2 — 설치 스크립트 (Node.js 없어도 됨)
35
+ ### Install script (no Node.js required)
21
36
 
22
37
  macOS / Linux / WSL:
23
38
 
24
39
  ```bash
25
- # 최신 stable
26
- curl -fsSL https://raw.githubusercontent.com/rainbow-robotics/web-rainbow-ui/main/scripts/install-muscat-pk.sh | sh
27
-
28
- # dev 채널
29
- MUSCAT_PK_TAG=dev curl -fsSL https://raw.githubusercontent.com/rainbow-robotics/web-rainbow-ui/main/scripts/install-muscat-pk.sh | sh
30
-
31
- # 특정 버전
32
- MUSCAT_PK_TAG=1.2.3 curl -fsSL https://raw.githubusercontent.com/rainbow-robotics/web-rainbow-ui/main/scripts/install-muscat-pk.sh | sh
40
+ curl -fsSL https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.sh | sh
41
+ curl -fsSL https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.sh | MUSCAT_PK_TAG=dev sh
42
+ curl -fsSL https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.sh | MUSCAT_PK_TAG=1.2.3 sh
33
43
  ```
34
44
 
35
45
  Windows PowerShell:
36
46
 
37
47
  ```powershell
38
- # 최신 stable
39
- irm https://raw.githubusercontent.com/rainbow-robotics/web-rainbow-ui/main/scripts/install-muscat-pk.ps1 | iex
40
-
41
- # dev 채널
42
- $env:MUSCAT_PK_TAG="dev"; irm https://raw.githubusercontent.com/rainbow-robotics/web-rainbow-ui/main/scripts/install-muscat-pk.ps1 | iex
48
+ irm https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.ps1 | iex
49
+ $env:MUSCAT_PK_TAG="dev"; irm https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.ps1 | iex
43
50
  ```
44
51
 
45
- > 설치 스크립트는 Node.js 20+가 있으면 npm을 우선 사용하고, 없으면 S3에서 self-contained 바이너리를 받습니다.
52
+ The script uses npm when Node.js 20+ is present, otherwise it downloads a self-contained binary.
46
53
 
47
- ### 방법 3 — 바이너리 직접 다운로드
54
+ ### Direct binary download
48
55
 
49
56
  ```
50
- https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/v{버전}/{바이너리}
57
+ https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/v{version}/{binary}
58
+ https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/stable/{binary}
59
+ https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/dev/{binary}
51
60
  ```
52
61
 
53
- | OS | 파일 |
62
+ | OS | File |
54
63
  |---|---|
55
64
  | Linux x64 | `muscat-pk-linux-x64` |
56
65
  | macOS ARM64 | `muscat-pk-darwin-arm64` |
66
+ | macOS x64 | `muscat-pk-darwin-x64` |
57
67
  | Windows x64 | `muscat-pk-win-x64.exe` |
58
68
 
59
- 채널 포인터:
69
+ Channel pointers: `.../muscat-plugin-kit/latest-version.txt` (stable), `.../muscat-plugin-kit/dev-version.txt` (dev).
60
70
 
61
- ```
62
- # 최신 stable 버전 문자열
63
- https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/latest-version.txt
71
+ ### Requirements
72
+
73
+ | Tool | Needed for |
74
+ |---|---|
75
+ | Node.js 20+ (or the binary) | the CLI, UI build |
76
+ | Docker | every backend runtime except `none` (`dev:back`, `dev`, `build`) |
77
+ | `uv` or Python 3.12 (optional) | IDE-only `.venv` for Python linting |
78
+
79
+ ### Uninstall
64
80
 
65
- # 최신 dev 버전 문자열
66
- https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/dev-version.txt
81
+ ```bash
82
+ muscat-pk uninstall # works for npm and binary installs
83
+ npm uninstall -g @rainbow-robotics/muscat-plugin-kit # manual, npm
84
+ sudo rm -f /usr/local/bin/muscat-pk # manual, binary
67
85
  ```
68
86
 
69
87
  ---
70
88
 
71
- ## 빠른 시작
89
+ ## 2. Quick start
72
90
 
73
91
  ```bash
74
- muscat-pk new my-plugin # 플러그인 프로젝트 생성 (python 또는 ros2 선택)
92
+ muscat-pk new my-plugin # scaffold (asks author, email, description, type, package manager)
75
93
  cd my-plugin
94
+ muscat-pk dev # UI preview + backend together
95
+ muscat-pk build # dist/ + distributable ZIP
96
+ ```
97
+
98
+ Install the ZIP from the robot's Muscat plugin page.
99
+
100
+ ---
101
+
102
+ ## 3. Commands
103
+
104
+ | Command | Description |
105
+ |---|---|
106
+ | `muscat-pk new [dir]` | Scaffold a plugin project |
107
+ | `muscat-pk type change` | Switch the backend type |
108
+ | `muscat-pk dev` | Local Muscat runtime + `dev:ui` + `dev:back` |
109
+ | `muscat-pk dev:ui` | UI preview in a mini-Muscat (preview-host) |
110
+ | `muscat-pk dev:back` | Backend only, in Docker, with hot reload |
111
+ | `muscat-pk build` | Build, lock Python deps, vendor wheels, ZIP |
112
+ | `muscat-pk update` | Update the kit, `plugin-ui` and `preview-host` |
113
+ | `muscat-pk sdk <list\|update [version]>` | Manage the `rainbow-rb-sdk` version |
114
+ | `muscat-pk sim list` | List the robot simulators for `dev --sim` |
115
+ | `muscat-pk storybook` | Browse the bundled rb-components Storybook |
116
+ | `muscat-pk mcp` | MCP server for AI coding agents |
117
+ | `muscat-pk uninstall` | Remove the CLI |
118
+ | `muscat-pk version` | Print the running version (`-v`, `--version`) |
119
+ | `muscat-pk completion [shell]` | Print a shell completion script |
120
+
121
+ `muscat-pk <command> --help` prints the options of a command.
122
+
123
+ ### 3.1 `new`
124
+
125
+ ```bash
126
+ muscat-pk new [dir] [--type python|cpp|ros2|ros2-cpp] [--pm npm|yarn|pnpm|bun]
127
+ [--author <name>] [--email <email>] [--description <text>]
128
+ [--yes] [--no-install]
129
+ ```
130
+
131
+ | Option | Meaning |
132
+ |---|---|
133
+ | `dir` | target directory (must be empty); asked when omitted |
134
+ | `--type` | backend type: `python` (default), `cpp`, `ros2` (rclpy), `ros2-cpp` (rclcpp) |
135
+ | `--pm` | front-end package manager; defaults to `yarn` inside a Yarn workspace, else `npm` |
136
+ | `--author` / `--email` / `--description` | manifest fields (email is required by the manifest) |
137
+ | `--yes` | no prompts; unspecified values use defaults |
138
+ | `--no-install` | skip `<pm> install`, the IDE `.venv` and the C++ dev image |
139
+ | `--help` | print the usage |
140
+
141
+ The plugin id is `com.example.<dir>` (lowercased; edit it in `manifest.json`). `new` also writes `.gitignore`, `AGENTS.md`, `CLAUDE.md` and `.mcp.json` (see [11](#11-ai-coding-agent-setup-muscat-pk-mcp)), and runs `git init` with an initial commit (delete `.git` if you don't want it).
142
+
143
+ Unless `--no-install`:
144
+
145
+ - python / ros2 (rclpy): an IDE-only `.venv` (see [9.6](#96-ide-support-python)).
146
+ - cpp / ros2-cpp: builds the C++ dev image so the editor finds the SDK headers (skipped with a warning when Docker is missing).
147
+
148
+ ### 3.2 `type change`
149
+
150
+ ```bash
151
+ muscat-pk type change [--to python|cpp|ros2|ros2-cpp] [--readme]
152
+ ```
153
+
154
+ | Option | Meaning |
155
+ |---|---|
156
+ | `--to` | new type; a numbered prompt when omitted |
157
+ | `--readme` | replace `README.md` with the new type's without asking (old file → `README.md.bak`) |
158
+
159
+ Rewrites `manifest.json` → `runtime` and adds the new backend's files (`cpp/` or `ros2_ws/`) unless that directory already exists. Files of the old type (`cpp/`, `ros2_ws/`, …) are left in place. When switching between a Python and a C++ backend it offers to replace the README (`--readme` answers yes). Switching to a C++ type prepares the C++ dev image like `new`.
160
+
161
+ ### 3.3 `dev`, `dev:ui`, `dev:back`
162
+
163
+ ```bash
164
+ muscat-pk dev [--robot-ip <ip>] [--robot-token <token>] [--keep-id] [--sim <model>]
165
+ [--port <n>] [--no-open]
166
+ muscat-pk dev:ui [--robot-ip <ip>] [--port <n>] [--keep-id] [--no-open]
167
+ muscat-pk dev:back [--robot-ip <ip>] [--robot-token <token>] [--keep-id]
168
+ ```
169
+
170
+ | Command | What runs |
171
+ |---|---|
172
+ | `dev` | builds the dev image and backend first (a failed build only warns), then a local Muscat runtime when no robot is given (or the robot runs no Muscat), then `dev:ui` and `dev:back` |
173
+ | `dev:ui` | the plugin's Vite dev server + preview-host at `http://localhost:4180` (next free port), opened in the browser |
174
+ | `dev:back` | the backend in a Docker container matching the robot. Python restarts on `py/**/*.py` changes (watchfiles) |
175
+
176
+ | Option | Meaning |
177
+ |---|---|
178
+ | `--robot-ip` | robot to develop against; else `MUSCAT_ROBOT_HOST`, else a prompt (blank = no robot) |
179
+ | `--robot-token` | service token for a robot with the login policy on; else `MUSCAT_ROBOT_TOKEN`, else a prompt (blank = none) |
180
+ | `--keep-id` | run under the manifest id instead of a dev id (see [9.3](#93-several-developers-on-one-robot)) |
181
+ | `--sim` | `dev` only, no robot: start a robot simulator (below). Skips the robot IP prompt |
182
+ | `--port` | preview-host port (default `4180`; the next free one when taken) |
183
+ | `--no-open` | don't open the browser (it is not opened in CI or without a TTY either) |
184
+
185
+ - One dev environment per PC: they share the backend container, the local Muscat and its ports. A second `dev` / `dev:ui` / `dev:back` asks to stop the running one (`Stop it and start this one? [y/N]`).
186
+ - Against a remote robot the plugin runs under a dev id (see [9.3](#93-several-developers-on-one-robot)); `--keep-id` uses the manifest id.
187
+ - Before start, `dev` and `dev:back` check `manifest.json` → `variables` and `ports` (errors stop it); `dev:back` also prints other manifest errors as warnings. The full check runs on `build`. Undeclared listening ports stop the backend (see [5.6](#56-ports)).
188
+ - Against a robot running Muscat, the UI is that robot's Muscat UI and the backend uses its gateway. With a local `muscat-dev` container (or `--sim`) it is the local real Muscat UI; otherwise the mock preview.
189
+ - `dev` / `dev:ui` print a hint when a newer kit is published (`MUSCAT_PK_NO_UPDATE_CHECK=1` skips it).
190
+ - Stopping `dev` (Ctrl+C) removes the backend container, and its own local Muscat and simulators.
191
+
192
+ #### Developing without a robot (`--sim`)
193
+
194
+ With no robot IP, `dev --sim <model>` also starts a robot simulator next to the local Muscat. The plugin SDK's robot calls then reach a real coreSW.
195
+
196
+ ```bash
197
+ muscat-pk sim list # "<name>\t<what runs>" per line
198
+ muscat-pk dev --sim 500920
199
+ muscat-pk dev --sim rby1-m_v1.2
200
+ ```
201
+
202
+ | `--sim` | Muscat services | Simulator |
203
+ |---|---|---|
204
+ | `500920` | common, log, manipulate | one `rainbowroboticsofficial/cobot-sim` |
205
+ | `RBY2` | common, log, manipulate | three cobot-sim (C500880, C501880, C502880) |
206
+ | `rby1-a`, `rby1-m`, `rby1-ub` (or a version, e.g. `rby1-m_v1.2`) | common, log, rby1 | `rainbowroboticsofficial/rby1-sim` + Xvfb, built locally |
207
+
208
+ - The simulated model is registered as the local Muscat's robot model (`500920` → C500920, `rby1-m` → m). The UI is then the real Muscat UI (dashboard, components, Move, Log, Settings) instead of the mock preview.
209
+ - A simulator runs only with `--sim`. Any other value fails with `--sim takes one of: 500920, RBY2, rby1-a, rby1-m, rby1-ub (rby1 also takes rby1-m_v1.2)`. A model missing from the manifest's `targetModels` only gets a warning.
210
+ - Simulator images use the latest Docker Hub tag (an rby1 version picks the matching tag). Offline, an already pulled image is used.
211
+ - rby1-sim keeps rendering, so it uses about one CPU core. The cap is `MUSCAT_PK_RBY1_CPUS` (default 1.5).
212
+ - No simulator is attached while another Muscat such as `muscat-dev` is running.
213
+ - There is no amr simulator yet.
214
+ - The local Muscat, the simulators and the backend all run with `--ipc host` like on the robot, so zenoh SHM and `shm_open` reach each other.
215
+
216
+ ### 3.4 `build`
217
+
218
+ ```bash
219
+ muscat-pk build [--arch amd64|arm64|both]
220
+ ```
221
+
222
+ Steps:
223
+
224
+ 1. Asks for a new `manifest.json` version (blank keeps the current one; TTY only).
225
+ 2. Asks for the target arch (unless `--arch`) and deletes an existing ZIP of that version, so a failed build leaves no stale ZIP.
226
+ 3. Validates the manifest (all of section 5) and checks that each `contributes[].ui` has `ui/<entry>.tsx` (or `.ts` / `.jsx` / `.js`).
227
+ 4. `vite build` of every `contributes[].ui` entry → `dist/<entry>.js`. A plugin without `contributes` builds no UI. Without Node.js 20+ (binary CLI) it asks `Build the plugin without its UI? [y/N]`; yes drops `contributes` from `dist/manifest.json`.
228
+ 5. Copies `manifest.json`, `assets/`, `README.md`, `README.<lang>.md`, `CHANGELOG.md`, `py/` and `requirements.txt` into `dist/`.
229
+ 6. Backend:
230
+ - python / ros2 (rclpy): resolves `requirements.txt` into `dist/requirements.lock` (`uv pip compile`, Python 3.12; pre-releases allowed when a pinned line is one) and downloads binary wheels (cp312, `--only-binary=:all:`) into `dist/wheels/` per arch. A package with no matching wheel stops the build.
231
+ - cpp: CMake Release build in Docker → `dist/<arch>/<entry>` plus `dist/<arch>/lib/` (`librb_sdk.so`). The CMake target name must equal `runtime.entry`.
232
+ - ros2: colcon build in Docker → `dist/ros2_ws/install/` (rclpy, built once) or `dist/ros2_ws/install-<arch>/` (rclcpp, per arch).
233
+ 7. Writes `installedSize` (bytes of `dist/` without `wheels/`) into `manifest.json` and `dist/manifest.json`.
234
+ 8. Packs `dist/` into `<project dir>-<version>.zip` in the project root.
235
+
236
+ | Option | Meaning |
237
+ |---|---|
238
+ | `--arch` | `amd64` (`x86_64`), `arm64` (`aarch64`) or `both`; asked in a TTY (default both), `both` otherwise |
239
+
240
+ Every runtime except `none` needs Docker. Building the other arch needs Docker emulation; on native Linux enable it once with `docker run --privileged --rm tonistiigi/binfmt --install <arch>`.
241
+
242
+ ### 3.5 `update`
243
+
244
+ ```bash
245
+ muscat-pk update # this project only (when it has a local install)
246
+ muscat-pk update --global # this project + the global CLI
247
+ muscat-pk update dev # follow the dev channel
248
+ muscat-pk update 1.2.3 # a specific version (also --tag <tag> / --version <v>)
249
+ ```
250
+
251
+ | Option | Meaning |
252
+ |---|---|
253
+ | `[tag]` | npm dist-tag or version (`latest`, `dev`, `1.2.3`). Omitted: `latest`; on a dev build it asks to stay on dev (default) or switch to stable |
254
+ | `--global` | also update the global CLI when run from a project's local install |
255
+
256
+ | Where you run it | What it updates |
257
+ |---|---|
258
+ | Inside a project with a local install | that project's `muscat-plugin-kit` + `plugin-ui` (+ `preview-host` when listed in `package.json`) |
259
+ | Same, with `--global` | the above + the global CLI |
260
+ | Anywhere else | the global CLI (npm `-g` or the S3 binary) and local deps if inside a project |
261
+
262
+ - When the project's local install is older than the CLI you run, that CLI updates the project instead (project only).
263
+ - Packages stay in their current `dependencies` / `devDependencies` section; the package manager is taken from `packageManager` or the lock file.
264
+ - An old project layout is migrated after a `[y/N]` prompt: `src/` → `ui/` (+ `vite.config.ts`, `tsconfig.json`), `main.py` → `py/main.py`, `backend/` → `py/`.
265
+ - Missing `.mcp.json`, `AGENTS.md`, `CLAUDE.md` are added; existing ones are kept.
266
+ - On Windows the new binary replaces the old one on the next run.
267
+
268
+ After updating it lists code that needs changes for the new `plugin-ui` (``This project needs changes for the updated plugin-ui. Fix these before `muscat-pk dev` / `build`:``). Fix them (or run `check_project` through `muscat-pk mcp`).
269
+
270
+ ### 3.6 `sdk`
271
+
272
+ ```bash
273
+ muscat-pk sdk list # 12 newest published versions
274
+ muscat-pk sdk update [version] # latest stable when omitted
275
+ ```
276
+
277
+ | Backend | `list` source | `update` rewrites |
278
+ |---|---|---|
279
+ | python, ros2 (rclpy) | PyPI `rainbow-rb-sdk` | every `rainbow-rb-*==` pin in `requirements.txt` together (they pin each other exactly); adds `rainbow-rb-sdk==<ver>` when missing. An existing `.venv` is refreshed; the dev container installs it on the next `dev` / `dev:back` |
280
+ | C++ (`cpp`, `ros2-cpp`) | apt `rainbow-rb-sdk-dev` (stable, numeric versions only) | `find_package(rb_sdk <ver> EXACT REQUIRED)` in `cpp/CMakeLists.txt` / `ros2_ws/src/*/CMakeLists.txt`; the dev image is rebuilt with it on the next `build` / `dev:back` |
281
+
282
+ A C++ version must be digits and dots (`"<v>" is not a CMake version (digits and dots only)`).
283
+
284
+ ### 3.7 `storybook`, `mcp`, `completion`, `version`
285
+
286
+ ```bash
287
+ muscat-pk storybook [--port 6007] [--rebuild]
288
+ muscat-pk mcp [--install codex]
289
+ muscat-pk completion [bash|zsh|fish|powershell]
290
+ muscat-pk version
291
+ muscat-pk uninstall
292
+ ```
293
+
294
+ | Command / option | Meaning |
295
+ |---|---|
296
+ | `storybook --port` | port to serve on (default `6007`); a taken port fails with `port <n> is in use — pass a free one with --port <n>.` |
297
+ | `storybook --rebuild` | rebuild the Storybook first (only from a repo checkout; the published kit ships it prebuilt) |
298
+ | `mcp` | stdio MCP server (section 11). It relays to the real server (`mcp --serve`) and restarts it when the kit is updated, so a running agent session picks up the new tools |
299
+ | `mcp --install codex` | register the server in `~/.codex/config.toml` (no-op when already there) |
300
+ | `version` | the version of the copy that actually ran (also `-v`, `--version`) |
301
+ | `uninstall` | npm global: `npm uninstall -g`; binary: deletes the executable. A project-local install is removed with the project's package manager instead |
302
+
303
+ `completion` prints a Tab-completion script for your shell — once registered, `muscat-pk d<Tab>` offers `dev`, `dev:ui`, `dev:back`. It only prints the script; append it to your shell's rc file as below. `[shell]` is `bash`, `zsh`, `fish` or `powershell`, defaulting to `$SHELL` (else `powershell` on Windows, `bash` elsewhere). Windows `cmd.exe` has no completion hook — use PowerShell, Git Bash or WSL. Optional convenience; nothing depends on it.
304
+
305
+ | OS / shell | Completion setup |
306
+ |---|---|
307
+ | macOS (zsh) | `muscat-pk completion zsh >> ~/.zshrc && source ~/.zshrc` |
308
+ | Linux (bash) | `muscat-pk completion bash >> ~/.bashrc && source ~/.bashrc` |
309
+ | fish | `muscat-pk completion fish > ~/.config/fish/completions/muscat-pk.fish` |
310
+ | PowerShell | `muscat-pk completion powershell >> $PROFILE; . $PROFILE` |
311
+
312
+ ---
313
+
314
+ ## 4. Project layout
315
+
316
+ ```
317
+ my-plugin/
318
+ ├─ ui/
319
+ │ ├─ content.tsx # UI entry (one file per contributes[].ui), `export default App`
320
+ │ └─ style.css # auto-loaded for every entry
321
+ ├─ py/main.py # python backend entry (runtime.entry)
322
+ ├─ cpp/ # cpp backend (CMake) — cpp type
323
+ ├─ ros2_ws/ # colcon workspace — ros2 / ros2-cpp type
324
+ ├─ assets/icon.svg
325
+ ├─ manifest.json
326
+ ├─ requirements.txt # python deps (locked into dist/requirements.lock on build)
327
+ ├─ package.json # UI deps (not shipped)
328
+ ├─ vite.config.ts # must use the muscat() preset
329
+ ├─ tsconfig.json
330
+ ├─ AGENTS.md, CLAUDE.md # rules for AI coding agents (CLAUDE.md → @AGENTS.md)
331
+ ├─ .mcp.json # registers `muscat-pk mcp` for Claude Code
332
+ ├─ .gitignore
333
+ └─ README.md # copied into dist/ (with README.<lang>.md, CHANGELOG.md)
334
+ ```
335
+
336
+ The scaffolded `requirements.txt` pins `rainbow-rb-sdk`, `rainbow-rb-flat-buffers` and `rainbow-rb-zenoh` to the same version (change it with `muscat-pk sdk update`). Generated, not committed: `node_modules/`, `dist/`, `.muscat-dev/`, `.venv/`, `*.zip`, `cpp/build*`, `ros2_ws/{install,build*,log}`.
337
+
338
+ `vite.config.ts`:
339
+
340
+ ```ts
341
+ import { defineConfig } from "vite";
342
+ import tailwindcss from "@tailwindcss/vite";
343
+ import { muscat } from "@rainbow-robotics/muscat-plugin-kit/vite";
344
+
345
+ export default defineConfig({ plugins: [tailwindcss(), muscat()] });
346
+ ```
347
+
348
+ The ZIP contains only the plugin's own code and assets. `@rainbow-robotics/plugin-ui`, `react` and `react-dom` come from the host's import map.
349
+
350
+ ---
351
+
352
+ ## 5. manifest.json
353
+
354
+ Fully validated by `muscat-pk build` (errors: `manifest.json errors: …`). `dev` / `dev:back` check `variables` and `ports` before start, and `dev:back` prints the rest as warnings.
355
+
356
+ ### 5.1 Full example
357
+
358
+ ```json
359
+ {
360
+ "id": "com.example.my-plugin",
361
+ "name": "My Plugin",
362
+ "version": "1.0.0",
363
+ "minMuscatVersion": "1.0.0",
364
+ "author": "Kim",
365
+ "email": "kim@example.com",
366
+ "company": "Example Inc.",
367
+ "description": { "en": "Gripper helper", "ko": "그리퍼 도우미" },
368
+ "icons": "assets/icon.svg",
369
+ "targetModels": "all",
370
+ "targetCategorys": ["manipulate"],
371
+ "contributes": [
372
+ { "point": "toolbar", "icon": "assets/icon.svg", "label": { "en": "My Plugin" }, "ui": "content", "opens": "popover" }
373
+ ],
374
+ "variables": [
375
+ { "name": "RBP_grip_force", "type": "number", "init": 0.5, "description": { "en": "Gripper force (0–1)" } }
376
+ ],
377
+ "ports": [28600],
378
+ "runtime": { "type": "python", "entry": "main.py" }
379
+ }
380
+ ```
381
+
382
+ ### 5.2 Top-level fields
383
+
384
+ | Field | Required | Rule |
385
+ |---|---|---|
386
+ | `id` | yes | reverse-domain: `.`-separated parts of lowercase letters/digits joined by single `-`, each starting with a letter (`com.example.my-plugin`). No `_` |
387
+ | `name`, `version`, `minMuscatVersion` | yes | non-empty strings |
388
+ | `author`, `email` | yes | non-empty strings |
389
+ | `company` | no | string |
390
+ | `description` | yes | string or i18n object |
391
+ | `icons` | yes | SVG path string, or `{ "16", "48", "128" }` for PNG/JPG (all three) |
392
+ | `targetModels` / `targetCategorys` | at least one | `"all"` or a non-empty array of strings. Categories: `manipulate`, `amr`, `rby1`, `vision` |
393
+ | `contributes` | no | see 5.3. Missing or `[]` = a backend-only plugin (no screens, no UI build) |
394
+ | `variables` | no | see 5.5 |
395
+ | `ports` | no | see 5.6 |
396
+ | `runtime` | yes | see 5.7 |
397
+ | `installedSize` | — | written by `build` (bytes) |
398
+
399
+ i18n object: `en` is required; allowed keys `ko`, `en`, `ja`, `zh`, `vi`, `th`, `es`.
400
+
401
+ ### 5.3 `contributes`
402
+
403
+ | Field | Rule |
404
+ |---|---|
405
+ | `point` | `toolbar`, `lnb`, `move.panel`, `program.panel`, `program.monitor` |
406
+ | `ui` | UI entry name under `ui/` without extension (default `content`); unique across entries |
407
+ | `label` | required, string or i18n object |
408
+ | `icon` | icon path |
409
+ | `opens` | toolbar only: `popover`, `panel`, `page` |
410
+ | `commandInfo` | required for `program.panel` (5.4) |
411
+
412
+ A non-empty `contributes` needs at least one `toolbar` entry (that is where the host lists the plugin). Each `ui` must have a matching `ui/<entry>.tsx` (or `.ts` / `.jsx` / `.js`), checked on `build`.
413
+
414
+ | Point | Where it shows |
415
+ |---|---|
416
+ | `toolbar` | header toolbar button; opens as popover / panel / page |
417
+ | `lnb` | left navigation |
418
+ | `move.panel` | Move page panel |
419
+ | `program.panel` | a custom command in the Program page's step tree; its `ui` is the step's edit form |
420
+ | `program.monitor` | a tab in the Program page's monitor panel (next to General / Digital I/O / Monitor / Log), rendered per task |
421
+
422
+ ### 5.4 `commandInfo` (`program.panel`)
423
+
424
+ ```json
425
+ {
426
+ "point": "program.panel",
427
+ "ui": "program",
428
+ "label": { "en": "My Step" },
429
+ "commandInfo": {
430
+ "name": "My Step",
431
+ "icon": "assets/icon.svg",
432
+ "funcName": "rb_program_sdk.plugin_execute",
433
+ "args": { "message": "hello" }
434
+ }
435
+ }
436
+ ```
437
+
438
+ | Field | Type | Meaning |
439
+ |---|---|---|
440
+ | `name` | string, required | command name in the tree |
441
+ | `icon` | string, required | icon path |
442
+ | `iconClassName` | string | extra icon class |
443
+ | `funcName` | string | function the executor calls (`rb_program_sdk.plugin_execute` for Python backends) |
444
+ | `args` | object | default step args, passed with `plugin_id` to the backend |
445
+ | `disabled`, `syncGroupId`, `disableCopy`, `disableDelete`, `requireChildren`, `requireParent` | boolean | tree editing rules |
446
+ | `allowChildren`, `allowParents`, `notAllowChildren` | string[] | allowed / forbidden neighbour commands |
447
+ | `childrenCommand` | string | command created as a child |
448
+
449
+ ### 5.5 `variables` (PyFM)
450
+
451
+ Variables the plugin exposes to PyFM programs. The Program page's variable picker lists only declared ones.
452
+
453
+ ```json
454
+ { "name": "RBP_target_pose", "type": "array", "init": [0, 0, 0], "description": { "en": "Target pose" } }
455
+ ```
456
+
457
+ | Field | Required | Rule |
458
+ |---|---|---|
459
+ | `name` | yes | `RBP_` + letters/digits/underscore; unique per plugin |
460
+ | `type` | yes | `number`, `string`, `boolean`, `array` |
461
+ | `init` | yes | initial value matching `type` |
462
+ | `description` | no | string or i18n object — write one, the UI shows nothing otherwise |
463
+
464
+ ### 5.6 `ports`
465
+
466
+ A backend that opens a TCP listener must declare every port:
467
+
468
+ ```json
469
+ { "ports": [28600, 28601] }
470
+ ```
471
+
472
+ | Rule | Enforced by |
473
+ |---|---|
474
+ | Only `28600-28999`, `number[]`, non-empty when present, no duplicates | manifest validation |
475
+ | Reserved ports rejected: `502`, `2222`, `4840`, `5170-5179`, `8000-8999`, `10000`, `30004`, `34964`, `44818`, `50000-50005`, `50051` | manifest validation |
476
+ | Every listening port must be declared | `dev` / `dev:back` runtime monitor stops the backend on the first undeclared port (`backend opened undeclared TCP port(s): …`) |
477
+ | No two enabled plugins share a port | the robot, at install (HTTP 409) |
478
+
479
+ - The kit's own dev ports (gateway `10000`, MediaMTX `8554` / `8889` / `8189`, the Zenoh dev peer port) and OS-assigned loopback ports are excluded.
480
+ - The monitor can't tell plugin ports apart when the backend shares a network namespace with other services, so it is off on native Linux (`--network host`) and when the backend joins a local Muscat container (the usual `dev` without a robot). Declare them anyway — the robot checks them at install.
481
+ - Declared ports are published on every host interface, TCP and UDP, so devices on the LAN reach them as on the robot (from the local Muscat under `dev`); a busy host port is skipped with a warning. The backend gets them as `RB_PLUGIN_PORTS`.
482
+
483
+ ### 5.7 `runtime`
484
+
485
+ | runtime | Source | Required fields |
486
+ |---|---|---|
487
+ | `{ "type": "none" }` | — | UI-only plugin |
488
+ | `{ "type": "python", "entry": "main.py" }` | `py/` | `entry` |
489
+ | `{ "type": "cpp", "entry": "<executable>" }` | `cpp/` | `entry` = built executable name |
490
+ | `{ "type": "ros2", "rosClient": "rclpy", "package": "<pkg>", "launch": "<pkg>_node" }` | `ros2_ws/` | `rosClient`, `package`, `launch` |
491
+ | `{ "type": "ros2", "rosClient": "rclcpp", "package": "<pkg>", "launch": "<pkg>_node" }` | `ros2_ws/` | same |
492
+
493
+ ---
494
+
495
+ ## 6. Plugin UI SDK (`useRb`)
496
+
497
+ Package `@rainbow-robotics/plugin-ui`:
498
+
499
+ | Import | Contents |
500
+ |---|---|
501
+ | `@rainbow-robotics/plugin-ui/react` | Muscat components (`RB*`), `useRb`, `useRbFormData`, `useRbSave`, `useRbStorage`, `createRbStore`, `RbSdk` / `RbStore` / `RbSetState` types |
502
+ | `@rainbow-robotics/plugin-ui/sdk` | the same hooks without the components, plus `RbContext` and the `RbNotifyType` type |
503
+ | `@rainbow-robotics/plugin-ui/manifest` | manifest types (`PluginManifest`, `PluginContribution`, `PluginVariable`, `PluginRuntime`), `CONTRIBUTION_POINTS`, `VARIABLE_TYPES`, `validateManifest` |
504
+
505
+ Outside a Muscat host (a unit test, Storybook) `useRb()` returns a stub: calls log `[plugin-ui] rb.<name> called outside of a host context` and resolves to an empty value.
506
+
507
+ ### 6.1 Entry component
508
+
509
+ Each top-level `ui/*.tsx` default-exports a React component. The host mounts it and provides `rb` through context.
510
+
511
+ ```tsx
512
+ import { useRb } from "@rainbow-robotics/plugin-ui/sdk";
513
+ import { RBLabelButton } from "@rainbow-robotics/plugin-ui/react";
514
+
515
+ export default function App() {
516
+ const rb = useRb();
517
+ return <RBLabelButton onClick={() => rb.action("grip", { width: 20 })}>Grip</RBLabelButton>;
518
+ }
519
+ ```
520
+
521
+ Use plugin-ui components instead of native `<button>`, `<input>`, `<select>`, `<textarea>`, `<table>`, `<dialog>`. Common mappings: button → `RBLabelButton` / `RBIconButton`, text input → `RBTextInput` / `RBTextField`, number → `RBSideNumberInput`, select → `RBSelectBox` + `Option`, checkbox → `RBCheckBox`, switch → `RBSwitch`, slider → `RBSlider`, table → `RBTable`, modal → `RBModal`, tabs → `RBTabs`. `muscat-pk storybook` and `muscat-pk mcp` show props and examples.
522
+
523
+ ### 6.2 Messaging with the backend
524
+
525
+ | UI | Backend (Python) | Pattern |
526
+ |---|---|---|
527
+ | `rb.pub(topic, payload)` | `@plugin.subscribe(topic)` | fire-and-forget UI → backend |
528
+ | `rb.sub(topic, handler)` | `plugin.publish(topic, payload)` | backend → UI stream |
529
+ | `rb.action(topic, payload?)` | `@plugin.serve(topic)` | UI request, backend reply |
530
+ | `rb.serve(topic, handler)` | `plugin.call(topic, payload=...)` | backend request, UI reply |
531
+
532
+ ```tsx
533
+ const rb = useRb();
534
+
535
+ await rb.pub("event", { clicked: true });
536
+
537
+ const sub = await rb.sub<{ ready: boolean }>("state", (s) => setReady(s.ready));
538
+ // cleanup: sub.undeclare()
539
+
540
+ const res = await rb.action<{ ok: boolean }, { width: number }>("grip", { width: 20 });
541
+
542
+ await rb.serve("tool/ping", (payload) => ({ pong: payload }));
543
+ ```
544
+
545
+ - Topics are scoped to the plugin id: `"state"` → `{pluginId}/state`. A leading `/` makes it absolute (`"/broadcast/x"`).
546
+ - All four take generics for payload / result types.
547
+ - Unsubscribe in a `useEffect` cleanup:
548
+
549
+ ```tsx
550
+ useEffect(() => {
551
+ let sub: { undeclare(): Promise<void> } | undefined;
552
+ rb.sub("state", setState).then((s) => (sub = s));
553
+ return () => void sub?.undeclare();
554
+ }, [rb]);
555
+ ```
556
+
557
+ ### 6.3 Host dialogs and toast
558
+
559
+ | API | Resolves |
560
+ |---|---|
561
+ | `rb.toast({ title?, message, type?, actionLabel?, handleActionClick?, closeOnActionClick?, duration?, onClose? })` | — (`message`: string or ReactElement; `type`: `info`, `danger`, `warning`, `caution`, `success`) |
562
+ | `rb.alert({ title?, message, buttonText? })` | when closed |
563
+ | `rb.confirm({ title?, message, buttonText?: { yes, no } })` | `true` / `false` |
564
+ | `rb.prompt({ title?, message, value?, placeholder?, buttonText?: { yes, no } })` | entered string, or `undefined` when cancelled |
565
+
566
+ ```tsx
567
+ if (await rb.confirm({ title: "Run", message: "Start the backend action?" })) {
568
+ await rb.action("run");
569
+ rb.toast({ message: "Started", type: "success" });
570
+ }
571
+ ```
572
+
573
+ ### 6.4 Program page (`rb.program`)
574
+
575
+ | API | Meaning |
576
+ |---|---|
577
+ | `rb.program.getSteps(scope?)` | main step tree of the program open on the Program page |
578
+ | `rb.program.setSteps(steps, scope?)` | replace it — an unsaved edit the user then saves |
579
+ | `rb.program.getCatalog(scope?)` | the loaded robot's command catalog (names, default args, nesting rules) |
580
+ | `rb.program.getCommandDefaults?(command, scope?)` | default step of a command whose defaults read the live robot (catalog `rules.hasFactoryDataFn`); optional, host-dependent |
581
+
582
+ ```ts
583
+ type PluginProgramStep = {
584
+ command: string; // "MoveJ", "Point", "Wait", "Repeat", "Condition", ...
585
+ args?: Record<string, unknown>;
586
+ summary?: string;
587
+ fields?: Record<string, unknown>; // step-level fields, e.g. SubTask's subTaskType
588
+ children?: PluginProgramStep[];
589
+ };
590
+ type PluginProgramScope = { taskId?: string; robotModel?: string };
591
+ ```
592
+
593
+ Without `scope` the host uses the first main task. In a `program.monitor` tab `rb.data` is `{ point: "program.monitor", taskId, robotModel }` — pass it as `scope` so a multi-component robot hits the right task.
594
+
595
+ ### 6.5 Step form save/restore (`program.panel`)
596
+
597
+ | Hook | Meaning |
598
+ |---|---|
599
+ | `useRbFormData<T>()` | values saved in this step, `undefined` for a new step |
600
+ | `useRbSave(fn)` | registers the getter the host calls on Save; always sees current state. Return value must be JSON |
601
+
602
+ ```tsx
603
+ import { useState } from "react";
604
+ import { RBSlider, useRbFormData, useRbSave } from "@rainbow-robotics/plugin-ui/react";
605
+
606
+ type Args = { width: number };
607
+
608
+ export default function App() {
609
+ const saved = useRbFormData<Args>();
610
+ const [args, setArgs] = useState<Args>({ width: 20, ...saved });
611
+ useRbSave(() => ({ args, summary: `${args.width}mm` }));
612
+ return (
613
+ <RBSlider
614
+ value={[args.width]}
615
+ min={0}
616
+ max={100}
617
+ onValueChange={(values: number[]) => setArgs({ width: values[0] ?? 0 })}
618
+ />
619
+ );
620
+ }
621
+ ```
622
+
623
+ The saved `args` reach the backend's `on_execute(args)` (see [7.3](#73-pluginprogramcontext--program-command)).
624
+
625
+ ### 6.6 Browser storage (`rb.storage`, `useRbStorage`, `createRbStore`)
626
+
627
+ Keep browser-side plugin data only here. Everything is namespaced `muscat-plugin:<pluginId>`, and the host deletes it when the plugin is uninstalled — data stored any other way (raw `localStorage`, your own IndexedDB) is never cleaned up.
628
+
629
+ | Backend | Use |
630
+ |---|---|
631
+ | `rb.storage` (= `rb.storage.indexedDB`) | default; structured values (objects, arrays, `Date`, `Blob`, …) |
632
+ | `rb.storage.local` | `localStorage`, JSON values, survives reloads |
633
+ | `rb.storage.session` | `sessionStorage`, JSON values, gone when the tab closes |
634
+ | `rb.storage.cookie` | ~4 KB per value and sent with every request to the host — tiny values only |
635
+
636
+ Every backend has the same API:
637
+
638
+ | Method | Meaning |
639
+ |---|---|
640
+ | `get<T>(key)` | value, or `undefined` |
641
+ | `set(key, value)` | save |
642
+ | `delete(key)` | remove |
643
+ | `keys()` | this plugin's keys |
644
+ | `subscribe(listener, key?)` | `({ kind, key, value }) => void` on every change made through this API — this screen, other screens of the plugin (toolbar panel, Program tab, …) and other browser tabs. `value` is `undefined` after a delete. Returns the unsubscribe function |
645
+ | `getItem` / `setItem` / `removeItem` | zustand `StateStorage` (raw strings) |
646
+
647
+ ```tsx
648
+ const rb = useRb();
649
+ await rb.storage.set("lastRun", { at: new Date(), ok: true });
650
+ const last = await rb.storage.get<{ at: Date; ok: boolean }>("lastRun");
651
+ await rb.storage.local.set("theme", "dark");
652
+ ```
653
+
654
+ #### `useRbStorage` — one stored value as React state
655
+
656
+ ```tsx
657
+ import { RBTextInput, useRbStorage } from "@rainbow-robotics/plugin-ui/react";
658
+
659
+ type Draft = { name: string };
660
+
661
+ function Editor() {
662
+ const [draft, setDraft] = useRbStorage<Draft>("draft", { kind: "local", initial: { name: "" } });
663
+ // setDraft(next) saves; setDraft(undefined) deletes
664
+ return <RBTextInput value={draft?.name ?? ""} handleChange={(name: string) => void setDraft({ name })} />;
665
+ }
666
+ ```
667
+
668
+ - `useRbStorage<T>(key, { kind?, initial? })` → `[value, setValue]`. `kind` defaults to `"indexeddb"`.
669
+ - Re-renders when the value changes anywhere (other screens, other tabs).
670
+ - `value` is `initial` until the stored value has loaded.
671
+
672
+ #### `createRbStore` — persisted zustand-style store
673
+
674
+ Define once at module level, use from any screen of the plugin:
675
+
676
+ ```tsx
677
+ import { createRbStore, RBSlider } from "@rainbow-robotics/plugin-ui/react";
678
+
679
+ type Settings = {
680
+ speed: number;
681
+ points: string[];
682
+ setSpeed: (v: number) => void;
683
+ addPoint: (p: string) => void;
684
+ };
685
+
686
+ export const useSettings = createRbStore<Settings>(
687
+ (set, get) => ({
688
+ speed: 50,
689
+ points: [],
690
+ setSpeed: (speed) => set({ speed }),
691
+ addPoint: (p) => set((s) => ({ points: [...s.points, p] })),
692
+ }),
693
+ { name: "settings", storage: "local" },
694
+ );
695
+
696
+ function SpeedPanel() {
697
+ const speed = useSettings((s) => s.speed); // re-renders only when speed changes
698
+ const setSpeed = useSettings((s) => s.setSpeed);
699
+ return <RBSlider value={[speed]} min={0} max={100} onValueChange={(v: number[]) => setSpeed(v[0] ?? 0)} />;
700
+ }
701
+ ```
702
+
703
+ | Member | Meaning |
704
+ |---|---|
705
+ | `createRbStore<T>(init, { name, storage? })` | `init(set, get)` returns initial state + actions. `name` is the storage key; `storage` is a backend kind (default `"indexeddb"`) |
706
+ | `useStore()` / `useStore(selector)` | whole state / a slice (re-renders only when the slice changes) |
707
+ | `useStore.getState()` | current state, outside React |
708
+ | `useStore.setState(partial \| (s) => partial)` | merge a change, outside React |
709
+ | `useStore.subscribe((next, prev) => …)` | listen outside React; returns unsubscribe |
710
+
711
+ - State changes only through `set` / `setState`. Every change is saved and re-renders every screen and tab using the store.
712
+ - Functions (actions) are not saved; only data fields are.
713
+ - The saved data loads when the first component using the store mounts (the plugin id is known only then). Before that `getState()` returns the initial values.
714
+ - Changes are merged shallowly: set a new array/object (`[...s.points, p]`) instead of mutating it.
715
+
716
+ #### Using zustand `persist` directly
717
+
718
+ Every backend is a zustand `StateStorage`:
719
+
720
+ ```ts
721
+ import { persist, createJSONStorage } from "zustand/middleware";
722
+ persist(fn, { name: "settings", storage: createJSONStorage(() => rb.storage.local) });
723
+ ```
724
+
725
+ `zustand` is then a dependency of your plugin; `createRbStore` needs nothing extra.
726
+
727
+ ### 6.7 Other
728
+
729
+ | API | Meaning |
730
+ |---|---|
731
+ | `rb.data` | host-provided mount data (`program.monitor` scope, saved form values); `undefined` for a fresh mount |
732
+ | `rb.onSave(getPayload)` | low-level form of `useRbSave`; returns the unregister function |
733
+ | `rb.rosPeers.get()` | PCs allowed to reach this plugin's ROS 2 nodes |
734
+ | `rb.rosPeers.set(ips)` | save the list and restart the backend; rejects an invalid IP |
735
+
736
+ ---
737
+
738
+ ## 7. Python plugin SDK
739
+
740
+ Python backends (`runtime.type: "python"`, entry `py/main.py`) use `rainbow-rb-sdk`, pinned in `requirements.txt` and changed with `muscat-pk sdk update`. The robot's plugin container provides the SDK at runtime.
741
+
742
+ ```python
743
+ from rainbow.rb_sdk import RBPluginCall, PluginProgramContext, RBManipulateSDK, RBAmrSDK, RBRby1SDK
744
+ from rainbow.rb_sdk.rb_sdk_cpp import ConnectOptions, PluginRouteError, RBPluginGatewayError
745
+ ```
746
+
747
+ `rainbow.rb_sdk` exports `RBPluginCall`, `PluginProgramContext`, `RBManipulateSDK`, `RBAmrSDK`, `RBRby1SDK`, `RBProgramSDK`, `RBBaseSDK` and `rby1` (the vendored rby1-sdk types). Everything else (`ConnectOptions`, `PluginRouter`, `PluginRouteError`, `RBPluginGatewayError`, …) is in `rainbow.rb_sdk.rb_sdk_cpp`.
748
+
749
+ Which API depends on the contribution point:
750
+
751
+ | Contribution point | Backend |
752
+ |---|---|
753
+ | `toolbar` / `lnb` / `move.panel` / `program.monitor` | `RBPluginCall` + robot SDKs (`RBManipulateSDK`, `RBAmrSDK`) called directly. No stop/pause/variable context |
754
+ | `program.panel` | `PluginProgramContext` — runs as a program step, tied to stop/pause and program variables |
755
+
756
+ ### 7.1 `RBPluginCall` — UI ↔ backend
757
+
758
+ Internal only (UI ↔ backend, plugin ↔ plugin); not reachable from outside Muscat. `RBPluginCall(plugin_id=None)`: the id is the argument, else env `RB_PLUGIN_ID`, else `PLUGIN_ID` (slashes trimmed). There is one instance per process and plugin id — calling `RBPluginCall()` again returns the same object.
759
+
760
+ ```python
761
+ plugin = RBPluginCall()
762
+
763
+ @plugin.serve("grip") # UI: await rb.action("grip", { width })
764
+ def grip(payload=None):
765
+ width = (payload or {}).get("width", 20)
766
+ plugin.publish("state", {"last_grip_width": width})
767
+ return {"ok": True, "width": width}
768
+
769
+ @plugin.subscribe("event") # UI: rb.pub("event", ...)
770
+ def on_event(payload=None):
771
+ print(payload)
76
772
 
77
- muscat-pk dev # UI + 백엔드 동시 실행
78
- muscat-pk dev:ui # UI 미리보기만
79
- muscat-pk dev:back # 백엔드만 (Python watchfiles / ROS2 Docker)
773
+ plugin.publish("state", {"ready": True}) # UI: rb.sub("state", ...)
774
+ res = plugin.call("tool/ping", payload={"hi": 1}, timeout=3.0) # UI: rb.serve("tool/ping", ...)
775
+ ```
776
+
777
+ | Member | Meaning |
778
+ |---|---|
779
+ | `serve(keyexpr, handler=None)` | request/response handler; decorator or direct call. Return value = reply. One handler per key — serving it again replaces the previous one |
780
+ | `subscribe(topic, callback=None)` | receive publishes |
781
+ | `publish(topic, payload=None)` | push to subscribers; dropped silently while disconnected |
782
+ | `call(keyexpr, *, payload, timeout=3.0)` | request the other side and wait; raises `RBPluginGatewayError` when disconnected or on timeout |
783
+ | `unserve(keyexpr=None)` / `undeclare_queryable(keyexpr=None)` | remove one / all serve handlers |
784
+ | `unsubscribe(topic=None, callback=None)` / `unsubscribe_topic(topic=None, callback=None)` | remove one callback, a topic's subscriptions, or all |
785
+ | `plugin_id` | the plugin id in use (dev id under `muscat-pk dev`) |
786
+ | `connected` | transport connected |
787
+ | `data_dir` | persistent data directory, created on access: `RB_PLUGIN_DATA_DIR` (set by Muscat), else `/app/data/runtime/plugins/data/{plugin_id}`. Survives updates and restarts; removed with the plugin. The install directory (cwd) is replaced on every update — keep state, caches and settings here |
788
+ | `router` | external endpoints (7.2) |
789
+
790
+ Payloads must be JSON-serialisable. Handlers may be sync or async. Topics are scoped like the UI's: `"state"` → `{plugin_id}/state`, a leading `/` is absolute.
791
+
792
+ ### 7.2 `plugin.router` — external endpoints
793
+
794
+ Exposes `common/plugin/{plugin_id}/{path}` over REST, Socket.IO, TCP, MQTT and WebRTC. The running Muscat (`common`) picks it up without a restart; it disappears when the plugin stops. Requires `rainbow-rb-sdk` ≥ 0.0.9.dev5 on the robot (older: `AttributeError`).
80
795
 
81
- muscat-pk build # dist/ 빌드
82
- muscat-pk pack # 배포용 ZIP 생성
796
+ ```python
797
+ @plugin.router.on("/hello") # every transport
798
+ def hello(payload, meta):
799
+ return {"msg": f"hi {payload.get('name')}", "via": meta["transport"]}
800
+
801
+ @plugin.router.on("/admin", explicit_routes=["api"]) # REST only
802
+ def admin(payload, meta):
803
+ if not (meta.get("principal") or {}).get("is_admin"):
804
+ raise PluginRouteError(403, "admin only")
805
+ return {"ok": True}
806
+
807
+ @plugin.router.subscribe("/cmd", explicit_events=["tcp"])
808
+ def on_cmd(payload):
809
+ ...
810
+
811
+ plugin.router.emit("/state", {"busy": True})
83
812
  ```
84
813
 
85
- ## 명령어 목록
814
+ | API | Meaning | External side |
815
+ |---|---|---|
816
+ | `router.on(path, explicit_routes=None)` | request/response | calls `common/plugin/{id}/{path}` (REST: `POST`) |
817
+ | `router.subscribe(topic, explicit_events=None)` | receive external publishes | publishes to `common/plugin/{id}/{topic}` |
818
+ | `router.emit(topic, payload=None)` | push to external subscribers | subscribes to `common/plugin/{id}/{topic}` |
819
+
820
+ Handler contract:
821
+
822
+ - Signature `(payload, meta)`, `(payload)` or `()` — only declared arguments are injected. Sync or async.
823
+ - `payload`: what the caller sent (REST: JSON body merged with query params).
824
+ - `meta`: `{"transport": "api"|"socket"|"tcp"|"mqtt"|"webrtc", "principal": dict | None}`. With the login policy on, `principal` is the token subject. Role checks are the plugin's job.
825
+ - Return value = response body (JSON). `None` → empty.
826
+ - `raise PluginRouteError(code, message)` sets the status; any other exception → 500 with `str(exc)`.
827
+ - `subscribe` handlers take `(payload)` and return nothing.
828
+
829
+ Rules:
86
830
 
87
- | 명령 | 설명 |
831
+ - Write only the path; the SDK adds `plugin_id`. Multi-segment paths work (`/tool/echo`). Declaring a path twice → `ValueError`.
832
+ - `explicit_routes` / `explicit_events`: `None` = every transport. Names `api | socket | tcp | mqtt | webrtc`; `subscribe` accepts only `tcp | mqtt | webrtc`. Unknown → `ValueError`. A disallowed transport gets 404.
833
+ - `on` and `subscribe` may share a name.
834
+
835
+ Calling from outside:
836
+
837
+ | Transport | How |
88
838
  |---|---|
89
- | `muscat-pk new [dir]` | 플러그인 프로젝트 스캐폴딩 |
90
- | `muscat-pk type change` | 백엔드 타입 전환 (python ↔ ros2) |
91
- | `muscat-pk build` | `src/content.tsx` → `dist/content.js` 빌드 |
92
- | `muscat-pk pack` | 빌드 후 배포용 ZIP 생성 |
93
- | `muscat-pk dev` | UI + 백엔드 동시 개발 서버 |
94
- | `muscat-pk dev:ui` | 미니 Muscat 미리보기 서버 |
95
- | `muscat-pk dev:back` | 백엔드 개발 서버 (hot-reload) |
96
- | `muscat-pk update` | kit + plugin-ui 최신 버전으로 업데이트 |
97
- | `muscat-pk sdk <list\|update>` | Python rainbow-rb-sdk 버전 관리 |
839
+ | REST | `POST /common/plugin/{id}/{path}`, JSON body |
840
+ | Socket.IO | emit `common/plugin/{id}/{path}`; reply in the ack |
841
+ | TCP | topic `common/plugin/{id}/{path}` request / publish |
842
+ | MQTT | request `common/plugin/{id}/{path}/__req__`; publish `common/plugin/{id}/{topic}` |
843
+ | WebRTC | data channel topic `common/plugin/{id}/{path}` |
98
844
 
99
- ## 패키지 역할
845
+ ```bash
846
+ curl -s -X POST http://<robot>/common/plugin/com.example.my-plugin/hello \
847
+ -H 'Content-Type: application/json' -d '{"name":"kim"}'
848
+ # under muscat-pk dev (the dev id is printed at start)
849
+ curl -s -X POST http://localhost:3000/common/plugin/<dev-id>/hello -H 'Content-Type: application/json' -d '{}'
850
+ ```
100
851
 
101
- | 패키지 | 역할 |
852
+ | Situation | Status |
102
853
  |---|---|
103
- | `@rainbow-robotics/plugin-ui` | UI 런타임 계약: rb-* 엘리먼트, React 바인딩, sandbox connect |
104
- | `@rainbow-robotics/muscat-plugin-kit` | CLI, 템플릿, 빌드/팩, 미리보기 호스트 |
854
+ | No such path / plugin down / transport not allowed | 404 |
855
+ | No answer within 10 s | 504 |
856
+ | `common` ↔ gateway not connected | 503 |
857
+ | Handler raised | 500 (`PluginRouteError` → its code) |
858
+
859
+ Auth: with the login policy on a token is required (any role); REST uses the same Bearer token as other Muscat APIs. Off: no token.
860
+
861
+ Known limits:
862
+
863
+ - A path named `install` can't be called over REST (taken by the registry install route).
864
+ - MQTT echoes to the sender: `emit` + `subscribe` on the same topic receives its own push over `mqtt`. Drop `mqtt` from `explicit_events` or use separate topics.
865
+ - After restarting the plugin, external calls may return 504 for up to a minute, then 404, until the gateway drops the old socket.
866
+
867
+ ### 7.3 `PluginProgramContext` — program command
868
+
869
+ Used only for `program.panel`. When the program reaches the step, the executor calls `commandInfo.funcName` with `plugin_id` + the step args, and the SDK calls your `on_execute`.
870
+
871
+ ```python
872
+ import asyncio
873
+ from rainbow.rb_sdk import PluginProgramContext
874
+
875
+ ctx = PluginProgramContext(plugin_id="com.example.my-plugin")
876
+
877
+ @ctx.on_execute
878
+ async def handle(args: dict) -> None:
879
+ count = await ctx.get_variable("counter")
880
+ await ctx.set_variable("counter", (count or 0) + 1)
881
+ await ctx.set_variable("RBP_my_plugin_result", 42) # new variable → RBP_ required
882
+
883
+ for _ in range(100):
884
+ await ctx.check_stop() # stop → CancelledError, pause → waits until resumed
885
+ await asyncio.sleep(0.5)
886
+
887
+ await ctx.done() # always; the step never finishes otherwise
888
+
889
+ @ctx.on_executor_state_change
890
+ async def on_change(robot_model: str, state: str) -> None:
891
+ if state == "paused":
892
+ print(f"{robot_model} paused")
893
+
894
+ async def background() -> None:
895
+ while True:
896
+ await asyncio.sleep(1.0)
897
+ await ctx.set_variable("RBP_my_plugin_heartbeat", 1) # ignored when no program runs
898
+
899
+ async def main() -> None:
900
+ asyncio.create_task(background())
901
+ await ctx.start() # stays resident
902
+
903
+ asyncio.run(main())
904
+ ```
905
+
906
+ | Member | Meaning |
907
+ |---|---|
908
+ | `on_execute(fn)` | decorator; called once per execution of this step with the step args |
909
+ | `done()` | step finished — always call |
910
+ | `check_stop(robot_model=None)` | stop → `CancelledError`, pause → waits. Call every loop iteration |
911
+ | `get_variable(name, robot_model=None, plugin_id=None)` | read; `None` when no program runs |
912
+ | `set_variable(name, value)` | write to every running main task; ignored when no program runs |
913
+ | `executor_state(robot_model=None)` | state string for a model, or `{model: state}` for all |
914
+ | `is_paused(robot_model=None)` / `is_stopped(robot_model=None)` | state checks |
915
+ | `on_executor_state_change(fn)` | decorator; `(robot_model, state)` once per change |
916
+ | `set_execute_handler(fn)` / `set_state_change_handler(fn)` | the same two registrations without a decorator |
917
+ | `begin_execution(robot_model=None)` / `end_execution()` | execute lifecycle hooks the SDK calls around `on_execute`; not needed in normal plugins |
918
+ | `start()` / `stop()` / `pause()` | lifecycle |
919
+ | `in_execute`, `exec_robot_model`, `plugin_id`, `started`, `control_topic` | properties |
920
+
921
+ #### Variables
922
+
923
+ - Existing variable: written by name as-is — exchanging values with the program is the point.
924
+ - New variable: must start with `RBP_` (`RuntimeError` otherwise), inside or outside a step. Use `RBP_{plugin}_{name}` to avoid collisions; `RBP_` alone doesn't prevent two plugins overwriting each other.
925
+ - Declare in `manifest.json` → `variables` what the Program page should offer (5.5).
926
+
927
+ `set_variable` targets every running main task:
928
+
929
+ | When | Own program | Other robots' main tasks |
930
+ |---|---|---|
931
+ | During own step | immediate | at their next step boundary |
932
+ | Outside a step | next step boundary | same |
933
+
934
+ `get_variable` options:
935
+
936
+ ```python
937
+ await ctx.get_variable("counter") # first value across main tasks
938
+ await ctx.get_variable("counter", robot_model="C500920") # one robot
939
+ await ctx.get_variable("RBP_TEST", plugin_id="A.plugin") # what that plugin last wrote
940
+ ```
941
+
942
+ Each variable keeps a per-plugin record (`meta.writers`), so a colliding name is still recoverable: `get_variable(..., plugin_id=...)` from the SDK, `get_var("A.plugin", "RBP_TEST")` in a program expression. A write can rarely be lost when it races the worker's flush — re-check important values.
943
+
944
+ #### Executor state
945
+
946
+ States: `idle | running | paused | waiting | stopped | post_start | completed | error`. A model with no running task is `None` / absent.
947
+
948
+ - Published every 0.8 s on `muscat/executor/state` — up to ~1 s latency.
949
+ - With `IS_DEV=true` there is no publish; always `None`.
950
+ - `on_executor_state_change` fires once per per-model change; handler exceptions are only logged.
951
+
952
+ Scope of `check_stop` / `is_paused` / `is_stopped`:
105
953
 
106
- 플러그인 ZIP에는 플러그인 코드와 에셋만 포함됩니다. `@rainbow-robotics/plugin-ui`, `react`, `react-dom`은 호스트(Muscat/로봇)의 import map에서 제공됩니다.
954
+ 1. `robot_model` given → that model
955
+ 2. omitted inside `on_execute` → the model executing this step
956
+ 3. omitted elsewhere → all models
957
+
958
+ ### 7.4 Robot SDKs from the backend
959
+
960
+ ```python
961
+ manipulate = RBManipulateSDK()
962
+ manipulate.move.call_move_j(
963
+ robot_model="C500920",
964
+ target={"tar_values": [0, 0, 0, 0, 0, 0, 0], "tar_frame": -1, "tar_unit": 0},
965
+ )
966
+ ```
967
+
968
+ Add your own safety checks before moving the robot. Exact signatures: `muscat-pk mcp` (`search_rb_sdk`, `get_rb_sdk_api`).
969
+
970
+ ### 7.5 Remote dev (laptop → robot)
971
+
972
+ ```bash
973
+ export MUSCAT_ROBOT_HOST=192.168.1.100
974
+ export MUSCAT_ROBOT_TOKEN=<service token> # only when the robot's login policy is on
975
+ muscat-pk dev:back
976
+ ```
977
+
978
+ ```python
979
+ async def main():
980
+ host = os.getenv("MUSCAT_ROBOT_HOST")
981
+ if host:
982
+ opts = ConnectOptions()
983
+ opts.host = host
984
+ opts.token = os.getenv("MUSCAT_ROBOT_TOKEN", "")
985
+ manipulate.connect(opts)
986
+ register_handlers() # after connect() — connect() reopens the session and drops earlier handlers
987
+ ...
988
+ if host:
989
+ await manipulate.disconnect()
990
+ ```
991
+
992
+ The scaffolded `py/main.py` already follows this.
993
+
994
+ ---
995
+
996
+ ## 8. Other backend runtimes (C++ / ROS 2)
997
+
998
+ All non-`none` runtimes run in Docker during `dev:back`, `dev` and `build`.
999
+
1000
+ ### 8.1 C++ (`cpp`)
1001
+
1002
+ - Source in `cpp/`, CMake build in Docker against the C++ SDK (apt `rainbow-rb-sdk-dev`).
1003
+ - CMake: `find_package(rb_sdk <ver> EXACT REQUIRED)` + `target_link_libraries(<target> rb_sdk::rb_sdk)` (C++20).
1004
+ - Headers: `<rainbow/rb_sdk/manipulate/rb_manipulate_sdk.hpp>`, `<rainbow/rb_sdk/plugin/rb_plugin.hpp>` (`rb_plugin::RBPluginCall`), `<rainbow/rb_sdk/program/plugin_program_context.hpp>` (`rb_plugin::PluginProgramContext`).
1005
+ - Dynamic link; `build` copies `librb_sdk.so` into `dist/<arch>/lib/`.
1006
+
1007
+ Building outside the container (Ubuntu 24.04):
1008
+
1009
+ ```bash
1010
+ 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
1011
+ 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
1012
+ sudo apt update && sudo apt install rainbow-rb-sdk-dev
1013
+ ```
1014
+
1015
+ ### 8.2 ROS 2 (`ros2` + `rclpy` / `rclcpp`)
1016
+
1017
+ - Source in `ros2_ws/src/<package>/`; `runtime.package` = colcon package, `runtime.launch` = node executable.
1018
+ - Relative topic names live under the plugin's namespace: id with `.` → `/`, `-` → `_` (`com.example.my-plugin` → `/com/example/my_plugin`). Use absolute names (`/cmd_vel`) for shared topics.
1019
+ - `RBPluginCall` works in an rclpy node too.
1020
+ - ROS discovery stays on the robot (`ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST`). Allow external PCs from the UI with `rb.rosPeers.set([...])`; Muscat restarts the backend with `ROS_STATIC_PEERS`. In dev the list is in `.muscat-dev/ros-peers.json`.
1021
+
1022
+ ---
1023
+
1024
+ ## 9. Development environment
1025
+
1026
+ ### 9.1 Local Muscat runtime
1027
+
1028
+ Without a robot IP (or against a robot that runs no Muscat), `dev` / `dev:ui` need a Muscat on `localhost:3000` (API) and `:10000` (plugin gateway):
1029
+
1030
+ - reuses any container already publishing `:10000` (`muscat`, `muscat-dev`, …). A container not started by the kit does not publish the plugin's ports;
1031
+ - uses a Muscat already listening on those ports without a container (robot PC, host networking);
1032
+ - otherwise starts `muscat-pk-muscat` — the S3 runtime image (`muscat-pk-runtime:<version>`, from `version.json`) running `common` (plus the services `--sim` needs) and MediaMTX (`8554`, `8889`, `8189`), cached in `~/.muscat-pk/muscat/`. It is recreated when the published plugin ports or services change, and a new Muscat version replaces the old image. Its data (DB, logs, programs) lives in the container only, so a new container starts empty.
1033
+ - `dev` removes its own `muscat-pk-muscat` on exit; started by `dev:ui` alone it keeps running.
1034
+ - The backend joins the local Muscat's network namespace, so `execute` and the SDK reach it like on the robot.
1035
+
1036
+ ```bash
1037
+ docker rm -f muscat-pk-muscat # stop it
1038
+ MUSCAT_PK_NO_LOCAL_MUSCAT=1 muscat-pk dev # don't start it
1039
+ ```
1040
+
1041
+ Its API docs are at `http://localhost:3000/common/docs`.
1042
+
1043
+ ### 9.2 Developing on the robot PC
1044
+
1045
+ On the robot PC Muscat already serves the gateway on `10000`; the CLI uses it and starts no local Muscat.
1046
+
1047
+ ### 9.3 Several developers on one robot
1048
+
1049
+ Against a remote robot, `dev` / `dev:back` run the plugin as `<manifest id>.dev-<user>-<random>` (random part kept in `~/.muscat-pk/dev-id`), so two machines don't answer each other's UI. UI, backend, ROS namespace and external routes all use the dev id. `--keep-id` uses the manifest id (for program `execute` or external clients) — then only one machine may connect.
1050
+
1051
+ ### 9.4 Local vs global install
1052
+
1053
+ If the project has its own `muscat-plugin-kit` in `node_modules`, any `muscat-pk` hands off to it, so the CLI, `vite` preset and `preview-host` are always the project's pinned version. `muscat-pk version` prints the copy that actually ran.
1054
+
1055
+ ### 9.5 Offline / air-gapped
1056
+
1057
+ Docker images are built only when missing or when the kit's Dockerfile / build args changed (a rebuild contacts the registry even when cached).
1058
+
1059
+ ```bash
1060
+ docker save muscat-pk-python-dev muscat-pk-runtime | gzip > muscat-pk-images.tar.gz # online machine
1061
+ gunzip -c muscat-pk-images.tar.gz | docker load # offline machine
1062
+ ```
1063
+
1064
+ C++ / ROS 2 projects also need `muscat-pk-cpp-dev` / `muscat-pk-ros2-dev` (and the per-arch `-amd64` / `-arm64` images used by `build`). Offline, the local Muscat uses the cached `version.json` and the simulators an already pulled image.
1065
+
1066
+ Force a rebuild with `MUSCAT_PK_REBUILD_IMAGE=1`.
1067
+
1068
+ ### 9.6 IDE support (Python)
1069
+
1070
+ `muscat-pk new` creates `.venv` with `requirements.txt` installed — for editor linting only; the backend runs in Docker. Without `uv` or Python 3 it prints `Could not create .venv (no local Python/uv?) — skipping, Docker dev still works.`; install one and re-run `new` or `type change`.
1071
+
1072
+ ### 9.7 JavaScript dependencies
1073
+
1074
+ `build` runs the project's own Vite build, so packages must be installed. Don't `npm install --omit=dev` before building — `vite`, `typescript`, `tailwindcss`, `sass`, `less` are devDependencies. The ZIP ships no `node_modules`.
1075
+
1076
+ ---
1077
+
1078
+ ## 10. Styles
1079
+
1080
+ - `muscat()` auto-loads `ui/style.{css,scss,sass,less}` / `ui/styles.*` for every entry and `ui/{entry}.*` for a matching entry, and injects the CSS into the JS bundle.
1081
+ - Built selectors are scoped under `[data-muscat-plugin-id="..."]`, so plugin CSS can't touch the host or other plugins.
1082
+ - Use design-token classes (`bg-bg-*`, `text-fg-*`, `typo-*`) rather than hex colors.
1083
+
1084
+ Tailwind (default, no preflight):
1085
+
1086
+ ```css
1087
+ @layer theme, utilities;
1088
+ @import "tailwindcss/theme.css" layer(theme);
1089
+ @import "tailwindcss/utilities.css" layer(utilities);
1090
+ @source "./**/*.{ts,tsx}";
1091
+ ```
1092
+
1093
+ SCSS / Less: `npm i -D sass` (or `less`) and create `ui/style.scss` (`ui/style.less`).
1094
+
1095
+ Emotion `styled` works as is. For the `css` prop:
1096
+
1097
+ ```bash
1098
+ npm i @emotion/react @emotion/styled && npm i -D @emotion/babel-plugin
1099
+ ```
1100
+
1101
+ ```ts
1102
+ muscat({ react: { jsxImportSource: "@emotion/react", babel: { plugins: ["@emotion/babel-plugin"] } } })
1103
+ ```
1104
+
1105
+ and `"jsxImportSource": "@emotion/react"` in `tsconfig.json`. Emotion `Global` is not scoped — avoid it.
1106
+
1107
+ ---
1108
+
1109
+ ## 11. AI coding agent setup (`muscat-pk mcp`)
1110
+
1111
+ An MCP server (stdio) with grounded info on rb-components (props, Storybook examples), design tokens, and the Python `rainbow-rb-sdk` (introspected signatures/docstrings), plus `check_project` for upgrading old projects.
1112
+
1113
+ | Tool | Returns |
1114
+ |---|---|
1115
+ | `list_components`, `get_component_props`, `get_component_example` | plugin-ui components, their prop types, real Storybook usage |
1116
+ | `list_design_tokens`, `search_design_tokens` | design-token Tailwind classes (`bg-bg-*`, `text-fg-*`, `typo-*`) |
1117
+ | `list_rb_sdk`, `search_rb_sdk`, `get_rb_sdk_api` | Python `rainbow-rb-sdk` and C++ SDK classes / signatures / docstrings |
1118
+ | `check_project` | what an older project must change after `muscat-pk update` |
1119
+
1120
+ Claude Code — `.mcp.json` at the plugin root (written by `new`, or by `update` when missing):
1121
+
1122
+ ```json
1123
+ { "mcpServers": { "muscat": { "command": "muscat-pk", "args": ["mcp"] } } }
1124
+ ```
1125
+
1126
+ Codex — `muscat-pk mcp --install codex` (on Windows it writes `muscat-pk.cmd`), or `~/.codex/config.toml`:
1127
+
1128
+ ```toml
1129
+ [mcp_servers.muscat]
1130
+ command = "muscat-pk"
1131
+ args = ["mcp"]
1132
+ ```
1133
+
1134
+ The agent starts the server itself.
1135
+
1136
+ ---
1137
+
1138
+ ## 12. Environment variables
1139
+
1140
+ | Variable | Effect |
1141
+ |---|---|
1142
+ | `MUSCAT_ROBOT_HOST` | robot IP for `dev` / `dev:ui` / `dev:back` (skips the prompt) |
1143
+ | `MUSCAT_ROBOT_TOKEN` | service token when the robot's login policy is on |
1144
+ | `MUSCAT_PK_NO_LOCAL_MUSCAT=1` | don't start the local Muscat runtime |
1145
+ | `MUSCAT_PK_REBUILD_IMAGE=1` | rebuild Docker images |
1146
+ | `MUSCAT_PK_RB_SDK_VERSION` | C++ SDK version baked into the C++ images (with `MUSCAT_PK_REBUILD_IMAGE=1`) |
1147
+ | `MUSCAT_PK_NO_BUILD_CACHE=1` | build Docker images with `--no-cache` (CI) |
1148
+ | `MUSCAT_PK_HOST_NETWORK=0` | on native Linux, use the Docker Desktop (bridge) path instead of `--network host` (CI) |
1149
+ | `MUSCAT_PK_RBY1_CPUS` | CPU cap of the rby1 simulator (default `1.5`) |
1150
+ | `MUSCAT_PK_MUSCAT_S3` | where the local Muscat runtime is downloaded from (default `https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat/main`) |
1151
+ | `MUSCAT_HOST_AGENT_DISABLE=1` | don't start the host Zenoh byte-proxy (macOS / Windows / WSL against a remote robot) |
1152
+ | `MUSCAT_PROXY_PORT` | port of that proxy (default `47100`) |
1153
+ | `MUSCAT_ZENOH_ADVERTISE_HOST` / `MUSCAT_ZENOH_PORT` | LAN IP / port the remote robot dials back to (default: the route to the robot / `53917` or the next free port) |
1154
+ | `MUSCAT_PK_NO_UPDATE_CHECK=1` | skip the update check on `dev` / `dev:ui` |
1155
+ | `RB_REGISTRY` | npm registry for the update check (default `https://registry.npmjs.org`) |
1156
+ | `MUSCAT_PK_TAG` | install script channel / version |
1157
+ | `CI` | set: `dev:ui` doesn't open the browser |
1158
+ | `NO_COLOR=1` / `FORCE_COLOR` | output colors |
1159
+
1160
+ Inside the backend container: `RB_PLUGIN_ID` (the plugin / dev id), `RB_PLUGIN_PORTS` (declared `ports`, comma-separated), `MUSCAT_ROBOT_HOST` / `MUSCAT_ROBOT_TOKEN` (remote dev). On the robot Muscat also sets `RB_PLUGIN_DATA_DIR` (see `data_dir`, 7.1).
1161
+
1162
+ ---
1163
+
1164
+ ## 13. Troubleshooting
1165
+
1166
+ | Symptom | Fix |
1167
+ |---|---|
1168
+ | `manifest.json errors: …` | fix each listed field (section 5) |
1169
+ | `manifest.json variable errors: …` / `manifest.json port errors: …` on `dev` | fix `variables` (5.5) / `ports` (5.6) |
1170
+ | `A Muscat plugin dev environment is already running: …` | answer `y` to stop it, or stop the other `dev` first |
1171
+ | `Docker is installed but the daemon is not running` | start Docker Desktop (or `sudo systemctl start docker`) |
1172
+ | `contributes declares ui "<entry>" but ui/<entry>.tsx … does not exist.` | add the file or fix `contributes[].ui` |
1173
+ | `wheel download failed for <arch>` | a dependency has no cp312 binary wheel for that arch — pick another version or package |
1174
+ | `can't build <arch> on this <host>: Docker has no <arch> emulation.` | `docker run --privileged --rm tonistiigi/binfmt --install <arch>`, or `--arch <host arch>` |
1175
+ | `Permission denied when deleting dist/` | an old build left root-owned files: `sudo rm -rf dist` once |
1176
+ | backend stops with an undeclared port | add the port (28600-28999) to `ports` |
1177
+ | UI preview says "no default export" | entry must `export default App`; `vite.config.ts` must use `muscat()`; run `check_project` via `muscat-pk mcp` |
1178
+ | `vite not found — UI will not load` | run `npm install` in the project |
1179
+ | `AttributeError: router` | robot SDK older than 0.0.9.dev5 |
1180
+ | `RBPluginGatewayError` from `plugin.call` | the gateway is disconnected, or no reply within `timeout` (is a UI `rb.serve` registered?) |
1181
+ | step never finishes | `on_execute` must `await ctx.done()` |
1182
+ | Stop doesn't stop the plugin | call `await ctx.check_stop()` in every loop |
1183
+ | `RuntimeError` on `set_variable` | new variable names need `RBP_` |
1184
+ | `get_variable` returns `None` | no program running (or `IS_DEV=true` for executor state) |
1185
+ | external call 504 after restart | wait up to a minute for the gateway to drop the old socket |
1186
+ | a newer/older local install answers | expected — the project's local kit wins; `muscat-pk update` |