@rainbow-robotics/muscat-plugin-kit 1.10.1-dev.1 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +282 -282
  2. package/cli/README.md +274 -274
  3. package/cli/bin/create-muscat-plugin.mjs +8 -8
  4. package/cli/bin/muscat-pk.mjs +155 -155
  5. package/cli/bundle-templates.mjs +31 -31
  6. package/cli/docker/Dockerfile.cpp +28 -28
  7. package/cli/docker/Dockerfile.dev +30 -30
  8. package/cli/docker/Dockerfile.python +21 -21
  9. package/cli/m5-e2e.mjs +69 -69
  10. package/cli/prepack.mjs +10 -10
  11. package/cli/src/ai-rules.mjs +70 -70
  12. package/cli/src/build.mjs +447 -447
  13. package/cli/src/check-package.mjs +13 -13
  14. package/cli/src/completion.mjs +166 -166
  15. package/cli/src/cpp.mjs +78 -78
  16. package/cli/src/cppIde.mjs +75 -75
  17. package/cli/src/dev.mjs +165 -165
  18. package/cli/src/devBack.mjs +734 -727
  19. package/cli/src/devUi.mjs +247 -243
  20. package/cli/src/esbuild.mjs +56 -56
  21. package/cli/src/host_agent.py +223 -223
  22. package/cli/src/manifest.mjs +287 -287
  23. package/cli/src/mcp.mjs +1071 -1071
  24. package/cli/src/muscatLocal.mjs +209 -209
  25. package/cli/src/new.mjs +343 -343
  26. package/cli/src/pluginDevServer.mjs +111 -111
  27. package/cli/src/ports.mjs +193 -193
  28. package/cli/src/python.mjs +112 -112
  29. package/cli/src/ros2.mjs +109 -109
  30. package/cli/src/sdk.mjs +117 -117
  31. package/cli/src/storybook.mjs +168 -168
  32. package/cli/src/type.mjs +141 -141
  33. package/cli/src/uninstall.mjs +81 -81
  34. package/cli/src/update.mjs +295 -295
  35. package/cli/src/util.mjs +441 -441
  36. package/cli/src/zenohDev.mjs +83 -81
  37. package/cli/storybook-dist/_design-tokens.css +4957 -4957
  38. package/cli/storybook-dist/_stories-src/NxAvatar.stories.tsx +27 -27
  39. package/cli/storybook-dist/_stories-src/NxPagination.stories.tsx +28 -28
  40. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordion.stories.tsx +46 -46
  41. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordionGroup.stories.tsx +73 -73
  42. package/cli/storybook-dist/_stories-src/RBBadge/RBBadge.stories.tsx +93 -93
  43. package/cli/storybook-dist/_stories-src/RBBattery/RBBattery.stories.tsx +74 -74
  44. package/cli/storybook-dist/_stories-src/RBButton/RBCheckIconButton.stories.tsx +28 -28
  45. package/cli/storybook-dist/_stories-src/RBButton/RBCheckLabelButton.stories.tsx +84 -84
  46. package/cli/storybook-dist/_stories-src/RBButton/RBIconButton.stories.tsx +57 -57
  47. package/cli/storybook-dist/_stories-src/RBButton/RBInlineLabelButton.stories.tsx +41 -41
  48. package/cli/storybook-dist/_stories-src/RBButton/RBLabelButton.stories.tsx +60 -60
  49. package/cli/storybook-dist/_stories-src/RBDatePicker/RBDateInput.stories.tsx +44 -44
  50. package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +56 -56
  51. package/cli/storybook-dist/_stories-src/RBDialog/RBAlert.stories.tsx +51 -51
  52. package/cli/storybook-dist/_stories-src/RBDialog/RBConfirm.stories.tsx +55 -55
  53. package/cli/storybook-dist/_stories-src/RBDialog/RBPrompt.stories.tsx +52 -52
  54. package/cli/storybook-dist/_stories-src/RBDivider/RBDivider.stories.tsx +93 -93
  55. package/cli/storybook-dist/_stories-src/RBEmptyView/RBEmptyView.stories.tsx +49 -49
  56. package/cli/storybook-dist/_stories-src/RBIcons/RBColorIcons.stories.tsx +108 -108
  57. package/cli/storybook-dist/_stories-src/RBIcons/RBIcons.stories.tsx +106 -106
  58. package/cli/storybook-dist/_stories-src/RBInput/RBCheckBox.stories.tsx +89 -89
  59. package/cli/storybook-dist/_stories-src/RBInput/RBMultiRangeInput.stories.tsx +64 -64
  60. package/cli/storybook-dist/_stories-src/RBInput/RBRadioBox.stories.tsx +65 -65
  61. package/cli/storybook-dist/_stories-src/RBInput/RBRadioButton.stories.tsx +68 -68
  62. package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +216 -216
  63. package/cli/storybook-dist/_stories-src/RBInput/RBSideNumberInput.stories.tsx +90 -90
  64. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextArea.stories.tsx +45 -45
  65. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextInput.stories.tsx +50 -50
  66. package/cli/storybook-dist/_stories-src/RBInput/RBTextField.stories.tsx +102 -102
  67. package/cli/storybook-dist/_stories-src/RBJoyStick/RBJoyStick.stories.tsx +31 -31
  68. package/cli/storybook-dist/_stories-src/RBLight/RBLight.stories.tsx +36 -36
  69. package/cli/storybook-dist/_stories-src/RBPagination/RBPagination.stories.tsx +49 -49
  70. package/cli/storybook-dist/_stories-src/RBShadcn/RBCard/RBCard.stories.tsx +89 -89
  71. package/cli/storybook-dist/_stories-src/RBShadcn/RBCommand/RBCommand.stories.tsx +116 -116
  72. package/cli/storybook-dist/_stories-src/RBShadcn/RBDialog/RBDialog.stories.tsx +74 -74
  73. package/cli/storybook-dist/_stories-src/RBShadcn/RBDrawer/RBDrawer.stories.tsx +99 -99
  74. package/cli/storybook-dist/_stories-src/RBShadcn/RBDropdownMenu/RBDropdownMenu.stories.tsx +123 -123
  75. package/cli/storybook-dist/_stories-src/RBShadcn/RBMenubar/RBMenubar.stories.tsx +135 -135
  76. package/cli/storybook-dist/_stories-src/RBShadcn/RBPopover/RBPopover.stories.tsx +69 -69
  77. package/cli/storybook-dist/_stories-src/RBShadcn/RBResizable/RBResizable.stories.tsx +49 -49
  78. package/cli/storybook-dist/_stories-src/RBShadcn/RBSheet/RBSheet.stories.tsx +91 -91
  79. package/cli/storybook-dist/_stories-src/RBShadcn/RBSlider/RBSlider.stories.tsx +38 -38
  80. package/cli/storybook-dist/_stories-src/RBShadcn/RBSwitch/RBSwitch.stories.tsx +112 -112
  81. package/cli/storybook-dist/_stories-src/RBShadcn/RBTabs/RBTabs.stories.tsx +75 -75
  82. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggle/RBToggle.stories.tsx +51 -51
  83. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggleGroup/RBToggleGroup.stories.tsx +84 -84
  84. package/cli/storybook-dist/_stories-src/RBTab/RBChipTab.stories.tsx +102 -102
  85. package/cli/storybook-dist/_stories-src/RBTab/RBChipsBoxTab.stories.tsx +78 -78
  86. package/cli/storybook-dist/_stories-src/RBTab/RBTab.stories.tsx +57 -57
  87. package/cli/storybook-dist/_stories-src/RBTable/RBTable.stories.tsx +177 -177
  88. package/cli/storybook-dist/_stories-src/RBTable/RBTableRow.stories.tsx +64 -64
  89. package/cli/storybook-dist/_stories-src/RBToast/RBToast.stories.tsx +117 -117
  90. package/cli/storybook-dist/_stories-src/RBTooltip/RBTooltip.stories.tsx +50 -50
  91. package/cli/storybook-dist/_stories-src/loading/RBSpinner.stories.tsx +30 -30
  92. package/cli/storybook-dist/_stories-src/nx-button/NxButton.stories.tsx +54 -54
  93. package/cli/storybook-dist/_stories-src/nx-button/NxPointerHoldButton.stories.tsx +57 -57
  94. package/cli/storybook-dist/_stories-src/nx-icons/NxIcons.stories.tsx +48 -48
  95. package/cli/storybook-dist/_stories-src/nx-inputs/NxNumberInput.stories.tsx +103 -103
  96. package/cli/storybook-dist/_stories-src/nx-inputs/NxRangeInput.stories.tsx +27 -27
  97. package/cli/storybook-dist/_stories-src/nx-inputs/NxSelectBox.stories.tsx +67 -67
  98. package/cli/storybook-dist/_stories-src/nx-inputs/NxSlideRange.stories.tsx +45 -45
  99. package/cli/storybook-dist/_stories-src/nx-inputs/NxSwitch.stories.tsx +27 -27
  100. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextArea.stories.tsx +48 -48
  101. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextInput.stories.tsx +46 -46
  102. package/cli/storybook-dist/assets/{Color-YHDXOIA2-D6JqZzDJ.js → Color-YHDXOIA2-B4IER17l.js} +1 -1
  103. package/cli/storybook-dist/assets/{Docs-ESb7JAHg.js → Docs-C8GCHFtK.js} +3 -3
  104. package/cli/storybook-dist/assets/{Docs-6gD2kUo1.js → Docs-CgdEMPQP.js} +2 -2
  105. package/cli/storybook-dist/assets/{Docs-B9wd8Ok7.js → Docs-Ck3WHvk6.js} +3 -3
  106. package/cli/storybook-dist/assets/{Docs-CgFbWWXM.js → Docs-D9NHsoeZ.js} +2 -2
  107. package/cli/storybook-dist/assets/{Docs-DBQup8Bu.js → Docs-DNYQC7nl.js} +3 -3
  108. package/cli/storybook-dist/assets/{Docs-CMjovnSY.js → Docs-DUfeiM4I.js} +4 -4
  109. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-BOUI1AHi.js → DocsRenderer-CFRXHY34-C0WhyDoR.js} +1 -1
  110. package/cli/storybook-dist/assets/{NxAvatar.stories-DsYFMUh3.js → NxAvatar.stories-CwnK8AUF.js} +1 -1
  111. package/cli/storybook-dist/assets/{NxButton.stories-CHfNcqsc.js → NxButton.stories-ueYdosfQ.js} +8 -8
  112. package/cli/storybook-dist/assets/{NxNumberInput.stories-BFw_4vFE.js → NxNumberInput.stories-M5ZCLbrK.js} +1 -1
  113. package/cli/storybook-dist/assets/{NxPagination.stories-DVfxyw4M.js → NxPagination.stories-bxxUnHEL.js} +1 -1
  114. package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BzyxUWoZ.js → NxPointerHoldButton.stories-BHxB2kr5.js} +1 -1
  115. package/cli/storybook-dist/assets/{NxRangeInput.stories-DmRdzBc1.js → NxRangeInput.stories-BKfr-fp4.js} +6 -6
  116. package/cli/storybook-dist/assets/{NxSelectBox.stories-B1KkNUOf.js → NxSelectBox.stories-CG8FuTBq.js} +1 -1
  117. package/cli/storybook-dist/assets/{NxSlideRange.stories-CyOdvfQ6.js → NxSlideRange.stories-BRTFtMTL.js} +1 -1
  118. package/cli/storybook-dist/assets/{NxSwitch.stories-DrDBw0qp.js → NxSwitch.stories-Bwuyzn-R.js} +1 -1
  119. package/cli/storybook-dist/assets/{NxTextArea.stories-DTB2aqNS.js → NxTextArea.stories-COq2sGqu.js} +1 -1
  120. package/cli/storybook-dist/assets/{NxTextInput.stories-DU0rucmM.js → NxTextInput.stories-CjsBLz05.js} +1 -1
  121. package/cli/storybook-dist/assets/{RBAccordion.stories-DTfOtXtp.js → RBAccordion.stories-BARwuZ0Q.js} +3 -3
  122. package/cli/storybook-dist/assets/{RBAccordionGroup.stories-9y9BjrUe.js → RBAccordionGroup.stories-COrPxETW.js} +10 -10
  123. package/cli/storybook-dist/assets/{RBAlert.stories-DLTAv9fT.js → RBAlert.stories-BQrkjZPt.js} +3 -3
  124. package/cli/storybook-dist/assets/{RBBadge.stories-BNMACewA.js → RBBadge.stories-BTsLL-lK.js} +1 -1
  125. package/cli/storybook-dist/assets/{RBBattery.stories-B3kXMAt5.js → RBBattery.stories-DAeQdJbc.js} +1 -1
  126. package/cli/storybook-dist/assets/{RBCard.stories-Y-s8R2n7.js → RBCard.stories-HyhOE80R.js} +39 -39
  127. package/cli/storybook-dist/assets/{RBCheckBox.stories-DBuT9tL4.js → RBCheckBox.stories-KFk0WqX_.js} +1 -1
  128. package/cli/storybook-dist/assets/{RBCheckIconButton.stories-CWu1TL56.js → RBCheckIconButton.stories-CXbbktF5.js} +1 -1
  129. package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-sCu2MGCX.js → RBCheckLabelButton.stories-DWfYz_9Z.js} +17 -17
  130. package/cli/storybook-dist/assets/{RBChipTab.stories-BX9qLJkR.js → RBChipTab.stories-JPEgVFAS.js} +5 -5
  131. package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-35u1aJ-P.js → RBChipsBoxTab.stories-CTPuWXrQ.js} +1 -1
  132. package/cli/storybook-dist/assets/{RBColorIcons.stories-DAOZ5Ies.js → RBColorIcons.stories-DiFRZ5jk.js} +1 -1
  133. package/cli/storybook-dist/assets/RBCommand.stories-Pf1HAVIR.js +73 -0
  134. package/cli/storybook-dist/assets/{RBConfirm.stories-iMXcUh7Z.js → RBConfirm.stories-D1cEPUXk.js} +3 -3
  135. package/cli/storybook-dist/assets/{RBDateInput.stories-hK5rXECf.js → RBDateInput.stories-RPYpCjig.js} +1 -1
  136. package/cli/storybook-dist/assets/RBDialog.stories-DJH-0XSR.js +38 -0
  137. package/cli/storybook-dist/assets/{RBDivider.stories-BAK-iZ1Z.js → RBDivider.stories-uEj1vOgK.js} +1 -1
  138. package/cli/storybook-dist/assets/{RBDrawer.stories-B2nqf2Nh.js → RBDrawer.stories-BiMzElb2.js} +12 -12
  139. package/cli/storybook-dist/assets/RBDropdownMenu.stories-Da_TBkla.js +86 -0
  140. package/cli/storybook-dist/assets/{RBEmptyView.stories-nCenrgBh.js → RBEmptyView.stories-BHeKGfPS.js} +1 -1
  141. package/cli/storybook-dist/assets/{RBIconButton.stories-DRH7O3sp.js → RBIconButton.stories-NEsRFoYp.js} +1 -1
  142. package/cli/storybook-dist/assets/{RBIcons.stories-D3PyJXG8.js → RBIcons.stories-CcZ-TTLS.js} +1 -1
  143. package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-modQbt10.js → RBInlineLabelButton.stories-B0wpdQyR.js} +1 -1
  144. package/cli/storybook-dist/assets/{RBJoyStick.stories-BY14qVgR.js → RBJoyStick.stories-DEgBou3K.js} +1 -1
  145. package/cli/storybook-dist/assets/{RBLabelButton.stories-DzF7vGwm.js → RBLabelButton.stories-DT8drIsF.js} +1 -1
  146. package/cli/storybook-dist/assets/{RBLight.stories-lF4FRmVh.js → RBLight.stories-Dh-aoeyG.js} +1 -1
  147. package/cli/storybook-dist/assets/{RBMenubar.stories-XhrI7bsV.js → RBMenubar.stories-D-H7UqVh.js} +1 -1
  148. package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-DprllQDl.js → RBMultiRangeInput.stories-hlclv6jI.js} +1 -1
  149. package/cli/storybook-dist/assets/{RBPagination.stories-2v2gBJfC.js → RBPagination.stories-DWCeoZ_S.js} +1 -1
  150. package/cli/storybook-dist/assets/{RBPopover.stories-C8iLIZpv.js → RBPopover.stories-C514M09_.js} +1 -1
  151. package/cli/storybook-dist/assets/{RBPrompt.stories-Th_HrWHI.js → RBPrompt.stories-BSItvfiT.js} +1 -1
  152. package/cli/storybook-dist/assets/{RBRadioBox.stories-CoHuLaN3.js → RBRadioBox.stories-BBb4YbFU.js} +1 -1
  153. package/cli/storybook-dist/assets/{RBRadioButton.stories-DULxE_ff.js → RBRadioButton.stories-CXyOGJCv.js} +1 -1
  154. package/cli/storybook-dist/assets/{RBRangeDateInput.stories-D8A9KBSS.js → RBRangeDateInput.stories-qvhubu0e.js} +1 -1
  155. package/cli/storybook-dist/assets/{RBResizable.stories-C41QfrQ3.js → RBResizable.stories-CZKeyGjH.js} +14 -14
  156. package/cli/storybook-dist/assets/{RBSelectBox.stories-C1gNmwzw.js → RBSelectBox.stories-CRKP3ybf.js} +1 -1
  157. package/cli/storybook-dist/assets/RBSheet.stories-sCM3ZJUa.js +39 -0
  158. package/cli/storybook-dist/assets/{RBSideNumberInput.stories-9LlgQ8nh.js → RBSideNumberInput.stories-Bb7gw8RF.js} +3 -3
  159. package/cli/storybook-dist/assets/{RBSlider.stories-oUgo5K9d.js → RBSlider.stories-23OAQU5z.js} +3 -3
  160. package/cli/storybook-dist/assets/{RBSpinner.stories-DDpohbY2.js → RBSpinner.stories-2xcQdcmt.js} +1 -1
  161. package/cli/storybook-dist/assets/{RBSwitch.stories-gW-GN7QO.js → RBSwitch.stories-BACUaJm_.js} +7 -7
  162. package/cli/storybook-dist/assets/{RBTab.stories-CN-PV3HH.js → RBTab.stories-CDHE3sVs.js} +1 -1
  163. package/cli/storybook-dist/assets/{RBTable.stories-BAZLpxtM.js → RBTable.stories-C0Cfhfl7.js} +29 -29
  164. package/cli/storybook-dist/assets/{RBTableRow.stories-CwjVrszB.js → RBTableRow.stories-BF8kIkpp.js} +1 -1
  165. package/cli/storybook-dist/assets/{RBTabs.stories-JuEo2sUL.js → RBTabs.stories-BdJWRq1o.js} +7 -7
  166. package/cli/storybook-dist/assets/{RBTextArea.stories-D0_Pv4qZ.js → RBTextArea.stories-CGy2xeYH.js} +3 -3
  167. package/cli/storybook-dist/assets/{RBTextField.stories-CCsOvZqV.js → RBTextField.stories-IfUOzxCM.js} +15 -15
  168. package/cli/storybook-dist/assets/{RBTextInput.stories-Cccn3VNm.js → RBTextInput.stories-VjXNFo4R.js} +3 -3
  169. package/cli/storybook-dist/assets/{RBToast.stories-I5617t0K.js → RBToast.stories-BzcobblV.js} +1 -1
  170. package/cli/storybook-dist/assets/{RBToggle.stories-DsgKjGsJ.js → RBToggle.stories-DuLE5mKv.js} +4 -4
  171. package/cli/storybook-dist/assets/{RBToggleGroup.stories-BZEzSDuz.js → RBToggleGroup.stories-CuR0N0GJ.js} +5 -5
  172. package/cli/storybook-dist/assets/{RBTooltip.stories-4oSyAWsA.js → RBTooltip.stories--aAgNC8_.js} +1 -1
  173. package/cli/storybook-dist/assets/{SideNumberInput-DSKbd0bG.js → SideNumberInput-DyWQxYUj.js} +743 -743
  174. package/cli/storybook-dist/assets/{entry-preview-B35XREGs.js → entry-preview-D7IHsza_.js} +1 -1
  175. package/cli/storybook-dist/assets/{iframe-BB797lmL.js → iframe-CbaIp5I-.js} +2 -2
  176. package/cli/storybook-dist/assets/{index-BU2567EP.js → index-C6CBgMuG.js} +3 -3
  177. package/cli/storybook-dist/assets/{preview-C7DaSWyc.js → preview-B-iYLDuW.js} +2 -2
  178. package/cli/storybook-dist/assets/{preview-CbuRHit4.js → preview-CRWqc9DH.js} +1 -1
  179. package/cli/storybook-dist/assets/{react-resizable-panels.browser-ChIvlwo7.js → react-resizable-panels.browser-8hC8dr-6.js} +8 -8
  180. package/cli/storybook-dist/iframe.html +2 -2
  181. package/cli/storybook-dist/index.html +1 -1
  182. package/cli/storybook-dist/project.json +1 -1
  183. package/cli/templates/common/.mcp.json +8 -8
  184. package/cli/templates/common/README.md +1231 -1231
  185. package/cli/templates/common/assets/icon.svg +45 -45
  186. package/cli/templates/common/manifest.json +26 -26
  187. package/cli/templates/common/package.json +22 -22
  188. package/cli/templates/common/py/main.py +125 -125
  189. package/cli/templates/common/tsconfig.json +13 -13
  190. package/cli/templates/common/ui/content.tsx +216 -216
  191. package/cli/templates/common/ui/style.css +6 -6
  192. package/cli/templates/common/vite.config.ts +10 -10
  193. package/cli/templates/cpp/cpp/main.cpp +73 -73
  194. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +25 -25
  195. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/package.xml +10 -10
  196. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/setup.py +18 -18
  197. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -11
  198. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +23 -23
  199. package/cli/vite.d.mts +25 -25
  200. package/cli/vite.mjs +292 -292
  201. package/package.json +3 -3
  202. package/cli/storybook-dist/assets/RBCommand.stories-BSqXDkKw.js +0 -73
  203. package/cli/storybook-dist/assets/RBDialog.stories-BAktz-ku.js +0 -38
  204. package/cli/storybook-dist/assets/RBDropdownMenu.stories-DbW2fWAj.js +0 -86
  205. package/cli/storybook-dist/assets/RBSheet.stories-_6VH-RUm.js +0 -39
package/cli/src/new.mjs CHANGED
@@ -1,343 +1,343 @@
1
- // muscat-pk new — scaffold a new plugin project from templates.
2
- import { execSync } from "node:child_process";
3
- import {
4
- cpSync,
5
- existsSync,
6
- mkdirSync,
7
- readFileSync,
8
- readdirSync,
9
- renameSync,
10
- rmSync,
11
- statSync,
12
- writeFileSync,
13
- } from "node:fs";
14
- import { basename, dirname, resolve } from "node:path";
15
- import { color, ensureIdeVenv, hasBin, kitVersion, parseArgs, prompt, templatesDir, log } from "./util.mjs";
16
- import { agentsMd, CLAUDE_MD } from "./ai-rules.mjs";
17
-
18
- // Like cpSync(src, dst, {recursive:true}) but reads from the embedded snapshot
19
- // when the templates dir doesn't exist on disk (compiled binary mode).
20
- // ponytail: import() is module-cached by the runtime — no manual cache needed.
21
- export async function copyTemplate(srcDir, dst) {
22
- if (existsSync(templatesDir)) { cpSync(srcDir, dst, { recursive: true }); return; }
23
- const { TEMPLATES } = await import("./templates-embedded.mjs");
24
- const prefix = srcDir.slice(templatesDir.length + 1).replace(/\\/g, "/") + "/";
25
- for (const [key, b64] of Object.entries(TEMPLATES)) {
26
- if (!key.startsWith(prefix)) continue;
27
- const dest = resolve(dst, key.slice(prefix.length));
28
- mkdirSync(dirname(dest), { recursive: true });
29
- writeFileSync(dest, Buffer.from(b64, "base64"));
30
- }
31
- }
32
-
33
- // `--type` values map to a runtime descriptor. ros2 has a python (rclpy) and a
34
- // C++ (rclcpp) flavor; we expose them as distinct type choices for clarity.
35
- export const TYPE_CHOICES = {
36
- python: { type: "python" },
37
- cpp: { type: "cpp" },
38
- ros2: { type: "ros2", rosClient: "rclpy" },
39
- "ros2-cpp":{ type: "ros2", rosClient: "rclcpp" },
40
- };
41
- const TYPES = Object.keys(TYPE_CHOICES);
42
- const PMS = ["npm", "yarn", "pnpm", "bun"];
43
-
44
- function printNewSuccess({ targetName, type, pm }) {
45
- const mark = [
46
- " /\\",
47
- " / \\_",
48
- " ____||__",
49
- " / \\__/ \\",
50
- " __/\\__/ \\__/\\",
51
- " / \\__/\\__/\\__/\\",
52
- " \\__/ \\__/ \\__/",
53
- " \\__/ \\__/",
54
- " \\__/",
55
- ].map((line) => color.green(line)).join("\n");
56
-
57
- log.info("");
58
- log.info(mark);
59
- log.info(color.bold(color.green("Welcome to Muscat Plugin Kit!")));
60
- log.info(color.dim("A fresh plugin workspace is ready."));
61
- log.info("");
62
- log.ok(`created ${color.bold(targetName)} ${color.dim(`(type: ${type}, pm: ${pm})`)}`);
63
- log.info("");
64
- log.info(color.bold("Next steps"));
65
- log.info(` ${color.cyan("cd")} ${targetName}`);
66
- log.info(` ${color.cyan("muscat-pk dev")} ${color.dim("# preview in mini-Muscat + back end")}`);
67
- log.info(` ${color.cyan("muscat-pk build")} ${color.dim("# build + distributable ZIP")}`);
68
- log.info("");
69
- log.info(color.dim("AI assistant? Claude Code reads the scaffolded .mcp.json and CLAUDE.md (→ AGENTS.md);"));
70
- log.info(color.dim(`Codex users run ${color.cyan("muscat-pk mcp --install codex")} once.`));
71
- log.info("");
72
- }
73
-
74
- // C++ backends: the editor needs the SDK headers, which only exist in the dev image. Building the image
75
- // now (dev/build reuse it) lets VS Code resolve rb_sdk right away. Best-effort, like the .venv.
76
- export async function prepareCppIde(root, choice) {
77
- const later = "they come with the first `muscat-pk dev` / `build`";
78
- if (!hasBin("docker")) return log.warn(`Docker not found — SDK headers for the editor: ${later}.`);
79
- try {
80
- log.step("preparing the C++ dev image + SDK headers for your editor (first run is slow)…");
81
- const { ensureImage } = await import(choice.type === "cpp" ? "./cpp.mjs" : "./ros2.mjs");
82
- await ensureImage(undefined, { root });
83
- } catch (err) {
84
- log.warn(`Could not prepare SDK headers for the editor (${err.message}) — ${later}.`);
85
- }
86
- }
87
-
88
- // Build the manifest runtime object for a given type choice + plugin slug.
89
- export function runtimeForChoice(choice, slug) {
90
- switch (choice.type) {
91
- case "cpp":
92
- // entry = built executable name (matches add_executable in cpp/CMakeLists.txt)
93
- return { type: "cpp", entry: slug };
94
- case "ros2":
95
- // package = colcon package name; launch = node executable run on the robot
96
- return { type: "ros2", rosClient: choice.rosClient, package: slug, launch: `${slug}_node` };
97
- case "python":
98
- default:
99
- return { type: "python", entry: "main.py" };
100
- }
101
- }
102
-
103
- export function applyTokens(dir, tokens) {
104
- for (const entry of readdirSync(dir)) {
105
- if (entry === "node_modules" || entry === ".git" || entry === ".venv") continue;
106
- const full = resolve(dir, entry);
107
- if (statSync(full).isDirectory()) {
108
- applyTokens(full, tokens);
109
- continue;
110
- }
111
- if (/\.(png|jpg|jpeg|gif|woff2?|glb|mp3)$/.test(entry)) continue;
112
- let content = readFileSync(full, "utf8");
113
- let changed = false;
114
- for (const [k, v] of Object.entries(tokens)) {
115
- if (content.includes(k)) {
116
- content = content.split(k).join(v);
117
- changed = true;
118
- }
119
- }
120
- if (changed) writeFileSync(full, content);
121
- }
122
- }
123
-
124
- // Derive naming tokens from a plugin directory name.
125
- // One id part (see PLUGIN_ID_RE): lowercase, single "-", starts with a letter.
126
- function idPart(name) {
127
- const part = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
128
- return /^[a-z]/.test(part) ? part : part ? `plugin-${part}` : "plugin";
129
- }
130
-
131
- export function deriveNames(target) {
132
- const base = basename(target);
133
- const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "_");
134
- const pluginName = base.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
135
- return {
136
- dir: base,
137
- slug,
138
- pluginName,
139
- pluginClass: pluginName.replace(/\s+/g, ""),
140
- pluginId: `com.example.${idPart(base)}`,
141
- };
142
- }
143
-
144
- // Recursively rename any file/dir whose name contains __PLUGIN_SLUG__.
145
- function renameSlugPaths(dir, slug) {
146
- for (const entry of readdirSync(dir)) {
147
- let full = resolve(dir, entry);
148
- if (entry.includes("__PLUGIN_SLUG__")) {
149
- full = resolve(dir, entry.split("__PLUGIN_SLUG__").join(slug));
150
- renameSync(resolve(dir, entry), full);
151
- }
152
- if (statSync(full).isDirectory()) renameSlugPaths(full, slug);
153
- }
154
- }
155
-
156
- // Copy the ros2_ws/ overlay into a plugin and substitute the package slug into
157
- // every path segment. The dev Dockerfile is NOT scaffolded here — it lives in
158
- // the kit (cli/docker/) so authors can't edit it. Caller runs applyTokens after.
159
- // rclpy → templates/ros2 (ament_python)
160
- // rclcpp → templates/ros2-cpp (ament_cmake)
161
- export async function overlayRos2(target, slug, rosClient = "rclpy") {
162
- const tmpl = rosClient === "rclcpp" ? "ros2-cpp" : "ros2";
163
- await copyTemplate(resolve(templatesDir, tmpl), target);
164
- renameSlugPaths(resolve(target, "ros2_ws"), slug);
165
- }
166
-
167
- // Copy the cpp/ overlay (CMake project) into a plugin. Caller runs applyTokens after.
168
- export async function overlayCpp(target) {
169
- await copyTemplate(resolve(templatesDir, "cpp"), target);
170
- }
171
-
172
- export async function runNew(args) {
173
- const opts = parseArgs(args, { booleans: ["yes", "no-install", "help"] });
174
- if (opts.help) {
175
- console.log(
176
- "muscat-pk new [dir] [--type python|cpp|ros2|ros2-cpp] [--pm npm|yarn|pnpm|bun]\n" +
177
- " [--author <name>] [--email <email>] [--description <text>]\n" +
178
- " [--yes] [--no-install]",
179
- );
180
- return;
181
- }
182
-
183
- const dirArg = opts._[0];
184
- const targetName =
185
- dirArg || (await prompt(color.cyan("Plugin directory name: "), "my-plugin"));
186
- const target = resolve(process.cwd(), targetName);
187
- if (existsSync(target) && readdirSync(target).length > 0) {
188
- throw new Error(`directory not empty: ${target}`);
189
- }
190
-
191
- let authorArg = opts.author;
192
- let emailArg = opts.email;
193
- let descriptionArg = opts.description;
194
-
195
- if (!opts.yes) {
196
- if (!authorArg) authorArg = await prompt(color.cyan("Author name: "), "");
197
- if (!emailArg) emailArg = await prompt(`${color.cyan("Email")} ${color.yellow("(required)")}: `, "");
198
- if (!descriptionArg) descriptionArg = await prompt(color.cyan("Description (English): "), "");
199
- }
200
-
201
- let type = opts.type;
202
- if (!type && !opts.yes) {
203
- console.log(`\n${color.bold(color.cyan("Select the plugin type:"))}`);
204
- console.log(` ${color.green("1)") } ${color.bold("Python")}`);
205
- console.log(` ${color.green("2)") } ${color.bold("C++")}`);
206
- console.log(` ${color.green("3)") } ${color.bold("ROS2")} ${color.dim("(rclpy / Python)")}`);
207
- console.log(` ${color.green("4)") } ${color.bold("ROS2")} ${color.dim("(rclcpp / C++)")}`);
208
- const choice = await prompt(`\n${color.cyan("선택")} ${color.dim("[1/2/3/4, default: 1]")}: `, "1");
209
- type = { "2": "cpp", "3": "ros2", "4": "ros2-cpp" }[choice] ?? "python";
210
- }
211
- type = TYPES.includes(type) ? type : "python";
212
- const runtimeChoice = TYPE_CHOICES[type];
213
-
214
- // Auto-detect yarn workspace: if .yarnrc.yml exists anywhere up the tree, default to yarn.
215
- function isInsideYarnWorkspace(dir) {
216
- let cur = dir;
217
- while (true) {
218
- if (existsSync(resolve(cur, ".yarnrc.yml"))) return true;
219
- const parent = resolve(cur, "..");
220
- if (parent === cur) return false;
221
- cur = parent;
222
- }
223
- }
224
-
225
- let pm = opts.pm;
226
- if (!pm && !opts.yes) {
227
- const defaultPm = isInsideYarnWorkspace(process.cwd()) ? "yarn" : "npm";
228
- pm = await prompt(
229
- `${color.cyan("Front-end package manager")} ${color.dim(`(${PMS.join("/")})`)} ${color.dim(`[${defaultPm}]`)}: `,
230
- defaultPm,
231
- );
232
- }
233
- pm = PMS.includes(pm) ? pm : "npm";
234
-
235
- const { dir, slug, pluginName, pluginClass, pluginId } = deriveNames(target);
236
-
237
- mkdirSync(target, { recursive: true });
238
-
239
- // common(UI + manifest/config)은 항상 포함. 백엔드 오버레이는 타입별로 얹는다.
240
- // python → common의 py/ 사용
241
- // cpp → cpp/ 오버레이, py/·requirements.txt 제거
242
- // ros2(*) → ros2_ws/ 오버레이 (rclpy=ament_python, rclcpp=ament_cmake)
243
- log.step(`scaffolding ${type} plugin → ${target}`);
244
- await copyTemplate(resolve(templatesDir, "common"), target);
245
- if (runtimeChoice.type === "ros2") await overlayRos2(target, slug, runtimeChoice.rosClient);
246
- if (runtimeChoice.type === "cpp") {
247
- await overlayCpp(target);
248
- rmSync(resolve(target, "py"), { recursive: true, force: true });
249
- rmSync(resolve(target, "requirements.txt"), { force: true });
250
- }
251
- if (runtimeChoice.type === "cpp" || runtimeChoice.rosClient === "rclcpp") {
252
- await copyTemplate(resolve(templatesDir, "readme-cpp"), target);
253
- }
254
-
255
- applyTokens(target, {
256
- __PLUGIN_ID__: pluginId,
257
- __PLUGIN_NAME__: pluginName,
258
- __PLUGIN_DIR__: dir,
259
- __PLUGIN_SLUG__: slug,
260
- __PLUGIN_CLASS__: pluginClass,
261
- __PLUGIN_AUTHOR__: authorArg || "",
262
- __PLUGIN_EMAIL__: emailArg || "",
263
- __PM__: pm,
264
- __RUNTIME_TYPE__: runtimeChoice.type,
265
- __RB_VERSION__: kitVersion(),
266
- });
267
-
268
- // Write the correct runtime block into manifest.json (per-type fields).
269
- const manifestPath = resolve(target, "manifest.json");
270
- const manifestJson = JSON.parse(readFileSync(manifestPath, "utf8"));
271
- manifestJson.runtime = runtimeForChoice(runtimeChoice, slug);
272
- if (descriptionArg) manifestJson.description = { en: descriptionArg };
273
- writeFileSync(manifestPath, JSON.stringify(manifestJson, null, 2) + "\n");
274
-
275
- // gitignore (templates can't ship a literal .gitignore via npm reliably)
276
- writeFileSync(
277
- resolve(target, ".gitignore"),
278
- ["node_modules", "dist", ".muscat-dev", ".venv", "*.zip", "ros2_ws/install", "ros2_ws/build*", "ros2_ws/log", "cpp/build*", ""].join("\n"),
279
- );
280
-
281
- // Rules for AI agents, generated from the same source as the MCP server's instructions.
282
- writeFileSync(resolve(target, "AGENTS.md"), agentsMd({ pluginName }));
283
- writeFileSync(resolve(target, "CLAUDE.md"), CLAUDE_MD);
284
-
285
- if (!opts["no-install"]) {
286
- try {
287
- log.step(`installing JS deps with ${pm}…`);
288
- execSync(`${pm} install`, { cwd: target, stdio: "inherit" });
289
- } catch {
290
- log.warn(`'${pm} install' failed; run it manually in ${targetName}`);
291
- }
292
-
293
- // Local .venv is for your editor/linter (pylint, pyright, ...) only — the
294
- // backend actually RUNS in Docker (see README), this never affects that.
295
- // rclcpp is C++, so no venv applies there.
296
- const needsIdeVenv =
297
- runtimeChoice.type === "python" ||
298
- (runtimeChoice.type === "ros2" && runtimeChoice.rosClient === "rclpy");
299
- if (needsIdeVenv) {
300
- log.step("creating .venv for your editor (pylint/pyright)…");
301
- if (ensureIdeVenv(target)) {
302
- log.ok(".venv ready — point your editor's Python interpreter there.");
303
- } else {
304
- log.warn("Could not create .venv (no local Python/uv?) — skipping, Docker dev still works.");
305
- }
306
- }
307
- if (runtimeChoice.type === "cpp" || runtimeChoice.rosClient === "rclcpp") await prepareCppIde(target, runtimeChoice);
308
- }
309
-
310
- // git init (best-effort). Always runs, even nested inside another repo —
311
- // delete target/.git if you don't want it.
312
- if (hasBin("git")) {
313
- try {
314
- execSync("git init", { cwd: target, stdio: "ignore" });
315
- execSync("git add -A", { cwd: target, stdio: "ignore" });
316
- execSync('git commit -m "Initial commit from muscat-pk new" --no-verify', { cwd: target, stdio: "ignore" });
317
- } catch {
318
- log.warn("git init failed — run it manually if you want version control here.");
319
- }
320
- }
321
-
322
- // The back end (python / cpp / ros2) runs in a Docker container that matches
323
- // the robot — no local Python/toolchain setup needed. Only `none` skips it.
324
- const needsDocker = runtimeChoice.type !== "none";
325
- const noDocker = needsDocker && !hasBin("docker");
326
-
327
- printNewSuccess({ targetName, type, pm });
328
-
329
- // ── post-scaffold environment warnings ───────────────────────────────────
330
- const noNode = !hasBin("node");
331
- if (noNode || noDocker) {
332
- log.info("");
333
- if (noNode) {
334
- log.warn("Node.js not found. Install Node.js 20+ to run JS deps and dev:ui.");
335
- log.warn(" https://nodejs.org");
336
- }
337
- if (noDocker) {
338
- log.warn("Docker not found. The plugin back end runs in a container (matches the robot env).");
339
- log.warn(" https://docs.docker.com/get-docker/");
340
- }
341
- log.info("");
342
- }
343
- }
1
+ // muscat-pk new — scaffold a new plugin project from templates.
2
+ import { execSync } from "node:child_process";
3
+ import {
4
+ cpSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ renameSync,
10
+ rmSync,
11
+ statSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { basename, dirname, resolve } from "node:path";
15
+ import { color, ensureIdeVenv, hasBin, kitVersion, parseArgs, prompt, templatesDir, log } from "./util.mjs";
16
+ import { agentsMd, CLAUDE_MD } from "./ai-rules.mjs";
17
+
18
+ // Like cpSync(src, dst, {recursive:true}) but reads from the embedded snapshot
19
+ // when the templates dir doesn't exist on disk (compiled binary mode).
20
+ // ponytail: import() is module-cached by the runtime — no manual cache needed.
21
+ export async function copyTemplate(srcDir, dst) {
22
+ if (existsSync(templatesDir)) { cpSync(srcDir, dst, { recursive: true }); return; }
23
+ const { TEMPLATES } = await import("./templates-embedded.mjs");
24
+ const prefix = srcDir.slice(templatesDir.length + 1).replace(/\\/g, "/") + "/";
25
+ for (const [key, b64] of Object.entries(TEMPLATES)) {
26
+ if (!key.startsWith(prefix)) continue;
27
+ const dest = resolve(dst, key.slice(prefix.length));
28
+ mkdirSync(dirname(dest), { recursive: true });
29
+ writeFileSync(dest, Buffer.from(b64, "base64"));
30
+ }
31
+ }
32
+
33
+ // `--type` values map to a runtime descriptor. ros2 has a python (rclpy) and a
34
+ // C++ (rclcpp) flavor; we expose them as distinct type choices for clarity.
35
+ export const TYPE_CHOICES = {
36
+ python: { type: "python" },
37
+ cpp: { type: "cpp" },
38
+ ros2: { type: "ros2", rosClient: "rclpy" },
39
+ "ros2-cpp":{ type: "ros2", rosClient: "rclcpp" },
40
+ };
41
+ const TYPES = Object.keys(TYPE_CHOICES);
42
+ const PMS = ["npm", "yarn", "pnpm", "bun"];
43
+
44
+ function printNewSuccess({ targetName, type, pm }) {
45
+ const mark = [
46
+ " /\\",
47
+ " / \\_",
48
+ " ____||__",
49
+ " / \\__/ \\",
50
+ " __/\\__/ \\__/\\",
51
+ " / \\__/\\__/\\__/\\",
52
+ " \\__/ \\__/ \\__/",
53
+ " \\__/ \\__/",
54
+ " \\__/",
55
+ ].map((line) => color.green(line)).join("\n");
56
+
57
+ log.info("");
58
+ log.info(mark);
59
+ log.info(color.bold(color.green("Welcome to Muscat Plugin Kit!")));
60
+ log.info(color.dim("A fresh plugin workspace is ready."));
61
+ log.info("");
62
+ log.ok(`created ${color.bold(targetName)} ${color.dim(`(type: ${type}, pm: ${pm})`)}`);
63
+ log.info("");
64
+ log.info(color.bold("Next steps"));
65
+ log.info(` ${color.cyan("cd")} ${targetName}`);
66
+ log.info(` ${color.cyan("muscat-pk dev")} ${color.dim("# preview in mini-Muscat + back end")}`);
67
+ log.info(` ${color.cyan("muscat-pk build")} ${color.dim("# build + distributable ZIP")}`);
68
+ log.info("");
69
+ log.info(color.dim("AI assistant? Claude Code reads the scaffolded .mcp.json and CLAUDE.md (→ AGENTS.md);"));
70
+ log.info(color.dim(`Codex users run ${color.cyan("muscat-pk mcp --install codex")} once.`));
71
+ log.info("");
72
+ }
73
+
74
+ // C++ backends: the editor needs the SDK headers, which only exist in the dev image. Building the image
75
+ // now (dev/build reuse it) lets VS Code resolve rb_sdk right away. Best-effort, like the .venv.
76
+ export async function prepareCppIde(root, choice) {
77
+ const later = "they come with the first `muscat-pk dev` / `build`";
78
+ if (!hasBin("docker")) return log.warn(`Docker not found — SDK headers for the editor: ${later}.`);
79
+ try {
80
+ log.step("preparing the C++ dev image + SDK headers for your editor (first run is slow)…");
81
+ const { ensureImage } = await import(choice.type === "cpp" ? "./cpp.mjs" : "./ros2.mjs");
82
+ await ensureImage(undefined, { root });
83
+ } catch (err) {
84
+ log.warn(`Could not prepare SDK headers for the editor (${err.message}) — ${later}.`);
85
+ }
86
+ }
87
+
88
+ // Build the manifest runtime object for a given type choice + plugin slug.
89
+ export function runtimeForChoice(choice, slug) {
90
+ switch (choice.type) {
91
+ case "cpp":
92
+ // entry = built executable name (matches add_executable in cpp/CMakeLists.txt)
93
+ return { type: "cpp", entry: slug };
94
+ case "ros2":
95
+ // package = colcon package name; launch = node executable run on the robot
96
+ return { type: "ros2", rosClient: choice.rosClient, package: slug, launch: `${slug}_node` };
97
+ case "python":
98
+ default:
99
+ return { type: "python", entry: "main.py" };
100
+ }
101
+ }
102
+
103
+ export function applyTokens(dir, tokens) {
104
+ for (const entry of readdirSync(dir)) {
105
+ if (entry === "node_modules" || entry === ".git" || entry === ".venv") continue;
106
+ const full = resolve(dir, entry);
107
+ if (statSync(full).isDirectory()) {
108
+ applyTokens(full, tokens);
109
+ continue;
110
+ }
111
+ if (/\.(png|jpg|jpeg|gif|woff2?|glb|mp3)$/.test(entry)) continue;
112
+ let content = readFileSync(full, "utf8");
113
+ let changed = false;
114
+ for (const [k, v] of Object.entries(tokens)) {
115
+ if (content.includes(k)) {
116
+ content = content.split(k).join(v);
117
+ changed = true;
118
+ }
119
+ }
120
+ if (changed) writeFileSync(full, content);
121
+ }
122
+ }
123
+
124
+ // Derive naming tokens from a plugin directory name.
125
+ // One id part (see PLUGIN_ID_RE): lowercase, single "-", starts with a letter.
126
+ function idPart(name) {
127
+ const part = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
128
+ return /^[a-z]/.test(part) ? part : part ? `plugin-${part}` : "plugin";
129
+ }
130
+
131
+ export function deriveNames(target) {
132
+ const base = basename(target);
133
+ const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "_");
134
+ const pluginName = base.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
135
+ return {
136
+ dir: base,
137
+ slug,
138
+ pluginName,
139
+ pluginClass: pluginName.replace(/\s+/g, ""),
140
+ pluginId: `com.example.${idPart(base)}`,
141
+ };
142
+ }
143
+
144
+ // Recursively rename any file/dir whose name contains __PLUGIN_SLUG__.
145
+ function renameSlugPaths(dir, slug) {
146
+ for (const entry of readdirSync(dir)) {
147
+ let full = resolve(dir, entry);
148
+ if (entry.includes("__PLUGIN_SLUG__")) {
149
+ full = resolve(dir, entry.split("__PLUGIN_SLUG__").join(slug));
150
+ renameSync(resolve(dir, entry), full);
151
+ }
152
+ if (statSync(full).isDirectory()) renameSlugPaths(full, slug);
153
+ }
154
+ }
155
+
156
+ // Copy the ros2_ws/ overlay into a plugin and substitute the package slug into
157
+ // every path segment. The dev Dockerfile is NOT scaffolded here — it lives in
158
+ // the kit (cli/docker/) so authors can't edit it. Caller runs applyTokens after.
159
+ // rclpy → templates/ros2 (ament_python)
160
+ // rclcpp → templates/ros2-cpp (ament_cmake)
161
+ export async function overlayRos2(target, slug, rosClient = "rclpy") {
162
+ const tmpl = rosClient === "rclcpp" ? "ros2-cpp" : "ros2";
163
+ await copyTemplate(resolve(templatesDir, tmpl), target);
164
+ renameSlugPaths(resolve(target, "ros2_ws"), slug);
165
+ }
166
+
167
+ // Copy the cpp/ overlay (CMake project) into a plugin. Caller runs applyTokens after.
168
+ export async function overlayCpp(target) {
169
+ await copyTemplate(resolve(templatesDir, "cpp"), target);
170
+ }
171
+
172
+ export async function runNew(args) {
173
+ const opts = parseArgs(args, { booleans: ["yes", "no-install", "help"] });
174
+ if (opts.help) {
175
+ console.log(
176
+ "muscat-pk new [dir] [--type python|cpp|ros2|ros2-cpp] [--pm npm|yarn|pnpm|bun]\n" +
177
+ " [--author <name>] [--email <email>] [--description <text>]\n" +
178
+ " [--yes] [--no-install]",
179
+ );
180
+ return;
181
+ }
182
+
183
+ const dirArg = opts._[0];
184
+ const targetName =
185
+ dirArg || (await prompt(color.cyan("Plugin directory name: "), "my-plugin"));
186
+ const target = resolve(process.cwd(), targetName);
187
+ if (existsSync(target) && readdirSync(target).length > 0) {
188
+ throw new Error(`directory not empty: ${target}`);
189
+ }
190
+
191
+ let authorArg = opts.author;
192
+ let emailArg = opts.email;
193
+ let descriptionArg = opts.description;
194
+
195
+ if (!opts.yes) {
196
+ if (!authorArg) authorArg = await prompt(color.cyan("Author name: "), "");
197
+ if (!emailArg) emailArg = await prompt(`${color.cyan("Email")} ${color.yellow("(required)")}: `, "");
198
+ if (!descriptionArg) descriptionArg = await prompt(color.cyan("Description (English): "), "");
199
+ }
200
+
201
+ let type = opts.type;
202
+ if (!type && !opts.yes) {
203
+ console.log(`\n${color.bold(color.cyan("Select the plugin type:"))}`);
204
+ console.log(` ${color.green("1)") } ${color.bold("Python")}`);
205
+ console.log(` ${color.green("2)") } ${color.bold("C++")}`);
206
+ console.log(` ${color.green("3)") } ${color.bold("ROS2")} ${color.dim("(rclpy / Python)")}`);
207
+ console.log(` ${color.green("4)") } ${color.bold("ROS2")} ${color.dim("(rclcpp / C++)")}`);
208
+ const choice = await prompt(`\n${color.cyan("선택")} ${color.dim("[1/2/3/4, default: 1]")}: `, "1");
209
+ type = { "2": "cpp", "3": "ros2", "4": "ros2-cpp" }[choice] ?? "python";
210
+ }
211
+ type = TYPES.includes(type) ? type : "python";
212
+ const runtimeChoice = TYPE_CHOICES[type];
213
+
214
+ // Auto-detect yarn workspace: if .yarnrc.yml exists anywhere up the tree, default to yarn.
215
+ function isInsideYarnWorkspace(dir) {
216
+ let cur = dir;
217
+ while (true) {
218
+ if (existsSync(resolve(cur, ".yarnrc.yml"))) return true;
219
+ const parent = resolve(cur, "..");
220
+ if (parent === cur) return false;
221
+ cur = parent;
222
+ }
223
+ }
224
+
225
+ let pm = opts.pm;
226
+ if (!pm && !opts.yes) {
227
+ const defaultPm = isInsideYarnWorkspace(process.cwd()) ? "yarn" : "npm";
228
+ pm = await prompt(
229
+ `${color.cyan("Front-end package manager")} ${color.dim(`(${PMS.join("/")})`)} ${color.dim(`[${defaultPm}]`)}: `,
230
+ defaultPm,
231
+ );
232
+ }
233
+ pm = PMS.includes(pm) ? pm : "npm";
234
+
235
+ const { dir, slug, pluginName, pluginClass, pluginId } = deriveNames(target);
236
+
237
+ mkdirSync(target, { recursive: true });
238
+
239
+ // common(UI + manifest/config)은 항상 포함. 백엔드 오버레이는 타입별로 얹는다.
240
+ // python → common의 py/ 사용
241
+ // cpp → cpp/ 오버레이, py/·requirements.txt 제거
242
+ // ros2(*) → ros2_ws/ 오버레이 (rclpy=ament_python, rclcpp=ament_cmake)
243
+ log.step(`scaffolding ${type} plugin → ${target}`);
244
+ await copyTemplate(resolve(templatesDir, "common"), target);
245
+ if (runtimeChoice.type === "ros2") await overlayRos2(target, slug, runtimeChoice.rosClient);
246
+ if (runtimeChoice.type === "cpp") {
247
+ await overlayCpp(target);
248
+ rmSync(resolve(target, "py"), { recursive: true, force: true });
249
+ rmSync(resolve(target, "requirements.txt"), { force: true });
250
+ }
251
+ if (runtimeChoice.type === "cpp" || runtimeChoice.rosClient === "rclcpp") {
252
+ await copyTemplate(resolve(templatesDir, "readme-cpp"), target);
253
+ }
254
+
255
+ applyTokens(target, {
256
+ __PLUGIN_ID__: pluginId,
257
+ __PLUGIN_NAME__: pluginName,
258
+ __PLUGIN_DIR__: dir,
259
+ __PLUGIN_SLUG__: slug,
260
+ __PLUGIN_CLASS__: pluginClass,
261
+ __PLUGIN_AUTHOR__: authorArg || "",
262
+ __PLUGIN_EMAIL__: emailArg || "",
263
+ __PM__: pm,
264
+ __RUNTIME_TYPE__: runtimeChoice.type,
265
+ __RB_VERSION__: kitVersion(),
266
+ });
267
+
268
+ // Write the correct runtime block into manifest.json (per-type fields).
269
+ const manifestPath = resolve(target, "manifest.json");
270
+ const manifestJson = JSON.parse(readFileSync(manifestPath, "utf8"));
271
+ manifestJson.runtime = runtimeForChoice(runtimeChoice, slug);
272
+ if (descriptionArg) manifestJson.description = { en: descriptionArg };
273
+ writeFileSync(manifestPath, JSON.stringify(manifestJson, null, 2) + "\n");
274
+
275
+ // gitignore (templates can't ship a literal .gitignore via npm reliably)
276
+ writeFileSync(
277
+ resolve(target, ".gitignore"),
278
+ ["node_modules", "dist", ".muscat-dev", ".venv", "*.zip", "ros2_ws/install", "ros2_ws/build*", "ros2_ws/log", "cpp/build*", ""].join("\n"),
279
+ );
280
+
281
+ // Rules for AI agents, generated from the same source as the MCP server's instructions.
282
+ writeFileSync(resolve(target, "AGENTS.md"), agentsMd({ pluginName }));
283
+ writeFileSync(resolve(target, "CLAUDE.md"), CLAUDE_MD);
284
+
285
+ if (!opts["no-install"]) {
286
+ try {
287
+ log.step(`installing JS deps with ${pm}…`);
288
+ execSync(`${pm} install`, { cwd: target, stdio: "inherit" });
289
+ } catch {
290
+ log.warn(`'${pm} install' failed; run it manually in ${targetName}`);
291
+ }
292
+
293
+ // Local .venv is for your editor/linter (pylint, pyright, ...) only — the
294
+ // backend actually RUNS in Docker (see README), this never affects that.
295
+ // rclcpp is C++, so no venv applies there.
296
+ const needsIdeVenv =
297
+ runtimeChoice.type === "python" ||
298
+ (runtimeChoice.type === "ros2" && runtimeChoice.rosClient === "rclpy");
299
+ if (needsIdeVenv) {
300
+ log.step("creating .venv for your editor (pylint/pyright)…");
301
+ if (ensureIdeVenv(target)) {
302
+ log.ok(".venv ready — point your editor's Python interpreter there.");
303
+ } else {
304
+ log.warn("Could not create .venv (no local Python/uv?) — skipping, Docker dev still works.");
305
+ }
306
+ }
307
+ if (runtimeChoice.type === "cpp" || runtimeChoice.rosClient === "rclcpp") await prepareCppIde(target, runtimeChoice);
308
+ }
309
+
310
+ // git init (best-effort). Always runs, even nested inside another repo —
311
+ // delete target/.git if you don't want it.
312
+ if (hasBin("git")) {
313
+ try {
314
+ execSync("git init", { cwd: target, stdio: "ignore" });
315
+ execSync("git add -A", { cwd: target, stdio: "ignore" });
316
+ execSync('git commit -m "Initial commit from muscat-pk new" --no-verify', { cwd: target, stdio: "ignore" });
317
+ } catch {
318
+ log.warn("git init failed — run it manually if you want version control here.");
319
+ }
320
+ }
321
+
322
+ // The back end (python / cpp / ros2) runs in a Docker container that matches
323
+ // the robot — no local Python/toolchain setup needed. Only `none` skips it.
324
+ const needsDocker = runtimeChoice.type !== "none";
325
+ const noDocker = needsDocker && !hasBin("docker");
326
+
327
+ printNewSuccess({ targetName, type, pm });
328
+
329
+ // ── post-scaffold environment warnings ───────────────────────────────────
330
+ const noNode = !hasBin("node");
331
+ if (noNode || noDocker) {
332
+ log.info("");
333
+ if (noNode) {
334
+ log.warn("Node.js not found. Install Node.js 20+ to run JS deps and dev:ui.");
335
+ log.warn(" https://nodejs.org");
336
+ }
337
+ if (noDocker) {
338
+ log.warn("Docker not found. The plugin back end runs in a container (matches the robot env).");
339
+ log.warn(" https://docs.docker.com/get-docker/");
340
+ }
341
+ log.info("");
342
+ }
343
+ }