@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.5 → 0.1.2-dev.50
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.
- package/README.md +82 -49
- package/cli/README.md +191 -5
- package/cli/bin/create-muscat-plugin.mjs +1 -1
- package/cli/bin/muscat-pk.mjs +25 -10
- package/cli/bundle-templates.mjs +28 -0
- package/cli/docker/Dockerfile.bridge +17 -0
- package/cli/docker/Dockerfile.cpp +22 -0
- package/cli/docker/Dockerfile.dev +1 -2
- package/cli/docker/Dockerfile.python +20 -0
- package/cli/docker/bridge-package.json +8 -0
- package/cli/prepack.mjs +10 -0
- package/cli/src/bridge.mjs +90 -0
- package/cli/src/build.mjs +377 -85
- package/cli/src/check-package.mjs +8 -23
- package/cli/src/cpp.mjs +61 -0
- package/cli/src/dev.mjs +40 -4
- package/cli/src/devBack.mjs +274 -49
- package/cli/src/devUi.mjs +198 -19
- package/cli/src/esbuild.mjs +27 -10
- package/cli/src/manifest.mjs +208 -0
- package/cli/src/mcp.mjs +592 -0
- package/cli/src/new.mjs +143 -43
- package/cli/src/pluginDevServer.mjs +112 -0
- package/cli/src/ports.mjs +160 -0
- package/cli/src/python.mjs +109 -0
- package/cli/src/ros2.mjs +43 -18
- package/cli/src/sdk.mjs +11 -9
- package/cli/src/socketBridge.mjs +129 -0
- package/cli/src/storybook.mjs +168 -0
- package/cli/src/templates-embedded.mjs +3 -0
- package/cli/src/type.mjs +52 -29
- package/cli/src/uninstall.mjs +98 -0
- package/cli/src/update.mjs +259 -30
- package/cli/src/util.mjs +114 -7
- package/cli/storybook-dist/_design-tokens.css +4957 -0
- package/cli/storybook-dist/_stories-src/NxAvatar.stories.tsx +27 -0
- package/cli/storybook-dist/_stories-src/NxPagination.stories.tsx +28 -0
- package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordion.stories.tsx +46 -0
- package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordionGroup.stories.tsx +73 -0
- package/cli/storybook-dist/_stories-src/RBBadge/RBBadge.stories.tsx +93 -0
- package/cli/storybook-dist/_stories-src/RBBattery/RBBattery.stories.tsx +74 -0
- package/cli/storybook-dist/_stories-src/RBButton/RBCheckIconButton.stories.tsx +28 -0
- package/cli/storybook-dist/_stories-src/RBButton/RBCheckLabelButton.stories.tsx +84 -0
- package/cli/storybook-dist/_stories-src/RBButton/RBIconButton.stories.tsx +57 -0
- package/cli/storybook-dist/_stories-src/RBButton/RBInlineLabelButton.stories.tsx +41 -0
- package/cli/storybook-dist/_stories-src/RBButton/RBLabelButton.stories.tsx +60 -0
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBDateInput.stories.tsx +44 -0
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +44 -0
- package/cli/storybook-dist/_stories-src/RBDialog/RBAlert.stories.tsx +51 -0
- package/cli/storybook-dist/_stories-src/RBDialog/RBConfirm.stories.tsx +55 -0
- package/cli/storybook-dist/_stories-src/RBDialog/RBPrompt.stories.tsx +52 -0
- package/cli/storybook-dist/_stories-src/RBDivider/RBDivider.stories.tsx +93 -0
- package/cli/storybook-dist/_stories-src/RBEmptyView/RBEmptyView.stories.tsx +49 -0
- package/cli/storybook-dist/_stories-src/RBIcons/RBColorIcons.stories.tsx +108 -0
- package/cli/storybook-dist/_stories-src/RBIcons/RBIcons.stories.tsx +106 -0
- package/cli/storybook-dist/_stories-src/RBIcons/RBMenuTreeIcons.stories.tsx +108 -0
- package/cli/storybook-dist/_stories-src/RBIcons/RBProgramIcons.stories.tsx +116 -0
- package/cli/storybook-dist/_stories-src/RBInput/RBCheckBox.stories.tsx +89 -0
- package/cli/storybook-dist/_stories-src/RBInput/RBMultiRangeInput.stories.tsx +64 -0
- package/cli/storybook-dist/_stories-src/RBInput/RBRadioBox.stories.tsx +65 -0
- package/cli/storybook-dist/_stories-src/RBInput/RBRadioButton.stories.tsx +68 -0
- package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +119 -0
- package/cli/storybook-dist/_stories-src/RBInput/RBSideNumberInput.stories.tsx +90 -0
- package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextArea.stories.tsx +45 -0
- package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextInput.stories.tsx +50 -0
- package/cli/storybook-dist/_stories-src/RBInput/RBTextField.stories.tsx +102 -0
- package/cli/storybook-dist/_stories-src/RBJoyStick/RBJoyStick.stories.tsx +31 -0
- package/cli/storybook-dist/_stories-src/RBLight/RBLight.stories.tsx +36 -0
- package/cli/storybook-dist/_stories-src/RBPagination/RBPagination.stories.tsx +49 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBCard/RBCard.stories.tsx +89 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBCommand/RBCommand.stories.tsx +116 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDialog/RBDialog.stories.tsx +74 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDrawer/RBDrawer.stories.tsx +99 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDropdownMenu/RBDropdownMenu.stories.tsx +123 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBMenubar/RBMenubar.stories.tsx +135 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBPopover/RBPopover.stories.tsx +69 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBResizable/RBResizable.stories.tsx +49 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSheet/RBSheet.stories.tsx +91 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSlider/RBSlider.stories.tsx +38 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSwitch/RBSwitch.stories.tsx +112 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBTabs/RBTabs.stories.tsx +75 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBToggle/RBToggle.stories.tsx +51 -0
- package/cli/storybook-dist/_stories-src/RBShadcn/RBToggleGroup/RBToggleGroup.stories.tsx +84 -0
- package/cli/storybook-dist/_stories-src/RBTab/RBChipTab.stories.tsx +102 -0
- package/cli/storybook-dist/_stories-src/RBTab/RBChipsBoxTab.stories.tsx +78 -0
- package/cli/storybook-dist/_stories-src/RBTab/RBTab.stories.tsx +57 -0
- package/cli/storybook-dist/_stories-src/RBTable/RBTable.stories.tsx +177 -0
- package/cli/storybook-dist/_stories-src/RBTable/RBTableRow.stories.tsx +64 -0
- package/cli/storybook-dist/_stories-src/RBToast/RBToast.stories.tsx +117 -0
- package/cli/storybook-dist/_stories-src/RBTooltip/RBTooltip.stories.tsx +50 -0
- package/cli/storybook-dist/_stories-src/loading/RBSpinner.stories.tsx +30 -0
- package/cli/storybook-dist/_stories-src/nx-button/NxButton.stories.tsx +54 -0
- package/cli/storybook-dist/_stories-src/nx-button/NxPointerHoldButton.stories.tsx +57 -0
- package/cli/storybook-dist/_stories-src/nx-icons/NxIcons.stories.tsx +48 -0
- package/cli/storybook-dist/_stories-src/nx-inputs/NxNumberInput.stories.tsx +103 -0
- package/cli/storybook-dist/_stories-src/nx-inputs/NxRangeInput.stories.tsx +27 -0
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSelectBox.stories.tsx +67 -0
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSlideRange.stories.tsx +45 -0
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSwitch.stories.tsx +27 -0
- package/cli/storybook-dist/_stories-src/nx-inputs/NxTextArea.stories.tsx +48 -0
- package/cli/storybook-dist/_stories-src/nx-inputs/NxTextInput.stories.tsx +46 -0
- package/cli/storybook-dist/assets/Color-YHDXOIA2-BjuzF3Je.js +1 -0
- package/cli/storybook-dist/assets/Docs-BAFZYTMR.js +12 -0
- package/cli/storybook-dist/assets/Docs-BQesYW-Q.js +8 -0
- package/cli/storybook-dist/assets/Docs-BsMEe093.js +7 -0
- package/cli/storybook-dist/assets/Docs-By6k6vcM.js +20 -0
- package/cli/storybook-dist/assets/Docs-D1J63cJc.js +7 -0
- package/cli/storybook-dist/assets/Docs-GoNbntAU.js +8 -0
- package/cli/storybook-dist/assets/DocsRenderer-CFRXHY34-BwY_WiM7.js +2 -0
- package/cli/storybook-dist/assets/NxAvatar.stories-CvzBHfYa.js +13 -0
- package/cli/storybook-dist/assets/NxButton.stories-B83PesgN.js +20 -0
- package/cli/storybook-dist/assets/NxIcons.stories-Ca7qpAaO.js +3 -0
- package/cli/storybook-dist/assets/NxNumberInput.stories-DpNcCG6B.js +26 -0
- package/cli/storybook-dist/assets/NxPagination.stories-Xnvd3xhH.js +11 -0
- package/cli/storybook-dist/assets/NxPointerHoldButton.stories-C3YSIpM0.js +14 -0
- package/cli/storybook-dist/assets/NxRangeInput.stories-DIhaNZig.js +17 -0
- package/cli/storybook-dist/assets/NxSelectBox.stories-BGwO2mYJ.js +17 -0
- package/cli/storybook-dist/assets/NxSlideRange.stories-BXlt5Bgt.js +14 -0
- package/cli/storybook-dist/assets/NxSwitch.stories-C5ZqIRtw.js +11 -0
- package/cli/storybook-dist/assets/NxTextArea.stories-hm_ycALo.js +11 -0
- package/cli/storybook-dist/assets/NxTextInput.stories-DkZAySr_.js +11 -0
- package/cli/storybook-dist/assets/PretendardVariable-CJuje-Rk.woff2 +0 -0
- package/cli/storybook-dist/assets/RBAccordion.stories--bY36GZY.js +17 -0
- package/cli/storybook-dist/assets/RBAccordionGroup.stories-BVr1ccI4.js +33 -0
- package/cli/storybook-dist/assets/RBAlert.stories-CkjFBEkv.js +17 -0
- package/cli/storybook-dist/assets/RBBadge.stories-CNXILDPh.js +1 -0
- package/cli/storybook-dist/assets/RBBattery.stories-AokMv8ZY.js +10 -0
- package/cli/storybook-dist/assets/RBCard.stories-CX1sLKB2.js +45 -0
- package/cli/storybook-dist/assets/RBCheckBox.stories-By3QHBFm.js +1 -0
- package/cli/storybook-dist/assets/RBCheckIconButton.stories-Dud1uK3X.js +1 -0
- package/cli/storybook-dist/assets/RBCheckLabelButton.stories-C8iBkZxY.js +24 -0
- package/cli/storybook-dist/assets/RBChipTab.stories-DsgbiS2U.js +21 -0
- package/cli/storybook-dist/assets/RBChipsBoxTab.stories-C3DZGmq0.js +1 -0
- package/cli/storybook-dist/assets/RBColorIcons.stories-CusR20qk.js +3 -0
- package/cli/storybook-dist/assets/RBCommand.stories-DhJNgaLZ.js +73 -0
- package/cli/storybook-dist/assets/RBConfirm.stories-B6LKpCDa.js +20 -0
- package/cli/storybook-dist/assets/RBDateInput.stories-PEEhQ1wJ.js +15 -0
- package/cli/storybook-dist/assets/RBDialog.stories-CAtOdRen.js +38 -0
- package/cli/storybook-dist/assets/RBDivider.stories-0zpIUlXT.js +1 -0
- package/cli/storybook-dist/assets/RBDrawer.stories-CWMG4glY.js +14 -0
- package/cli/storybook-dist/assets/RBDropdownMenu.stories-6fbEYZfk.js +86 -0
- package/cli/storybook-dist/assets/RBEmptyView.stories-BRDZHjn4.js +12 -0
- package/cli/storybook-dist/assets/RBIconButton.stories-sYrRLYob.js +1 -0
- package/cli/storybook-dist/assets/RBIcons.stories-DFe8O1_g.js +3 -0
- package/cli/storybook-dist/assets/RBInlineLabelButton.stories-DX4VdUfk.js +1 -0
- package/cli/storybook-dist/assets/RBJoyStick.stories-SA2Foelf.js +11 -0
- package/cli/storybook-dist/assets/RBLabelButton.stories-CicBns_Y.js +1 -0
- package/cli/storybook-dist/assets/RBLight.stories-WlP3oEmY.js +1 -0
- package/cli/storybook-dist/assets/RBMenuTreeIcons.stories-CrWjPfxj.js +3 -0
- package/cli/storybook-dist/assets/RBMenubar.stories-Di4phWw7.js +3 -0
- package/cli/storybook-dist/assets/RBMultiRangeInput.stories-DS3wrR3l.js +14 -0
- package/cli/storybook-dist/assets/RBPagination.stories-BDVPAUVD.js +17 -0
- package/cli/storybook-dist/assets/RBPopover.stories-DL9b_iX9.js +11 -0
- package/cli/storybook-dist/assets/RBProgramIcons.stories-ByRVx-on.js +3 -0
- package/cli/storybook-dist/assets/RBPrompt.stories-vTmKWyA5.js +20 -0
- package/cli/storybook-dist/assets/RBRadioBox.stories-C9WHt4jb.js +1 -0
- package/cli/storybook-dist/assets/RBRadioButton.stories-D8KYq63j.js +11 -0
- package/cli/storybook-dist/assets/RBRangeDateInput.stories-Bvcf4Su7.js +15 -0
- package/cli/storybook-dist/assets/RBResizable.stories-3O2OMuH_.js +20 -0
- package/cli/storybook-dist/assets/RBSelectBox.stories-CKZLmPzX.js +6 -0
- package/cli/storybook-dist/assets/RBSheet.stories-Dko0It1W.js +39 -0
- package/cli/storybook-dist/assets/RBSideNumberInput.stories-CUvqVtId.js +5 -0
- package/cli/storybook-dist/assets/RBSlider.stories-CcbaKRYo.js +5 -0
- package/cli/storybook-dist/assets/RBSpinner.stories-D15e_JlE.js +14 -0
- package/cli/storybook-dist/assets/RBSwitch.stories-C4JRsYJ6.js +65 -0
- package/cli/storybook-dist/assets/RBTab.stories-BPvzQSM-.js +1 -0
- package/cli/storybook-dist/assets/RBTable.stories-nr8OS1mV.js +62 -0
- package/cli/storybook-dist/assets/RBTableRow.stories-rI0rK8Ci.js +11 -0
- package/cli/storybook-dist/assets/RBTabs.stories-CxMtHGWA.js +21 -0
- package/cli/storybook-dist/assets/RBTextArea.stories-Bru4AhJ3.js +5 -0
- package/cli/storybook-dist/assets/RBTextField.stories-Ah3D9lto.js +45 -0
- package/cli/storybook-dist/assets/RBTextInput.stories-C3qAVngJ.js +5 -0
- package/cli/storybook-dist/assets/RBToast.stories-C1occsvf.js +11 -0
- package/cli/storybook-dist/assets/RBToggle.stories-BNrNERc7.js +12 -0
- package/cli/storybook-dist/assets/RBToggleGroup.stories-D_RRBwxY.js +17 -0
- package/cli/storybook-dist/assets/RBTooltip.stories-DuyOQqrB.js +9 -0
- package/cli/storybook-dist/assets/RainbowUiProvider-BYlbsa69.css +1 -0
- package/cli/storybook-dist/assets/RainbowUiProvider-B_piZBLb.js +31 -0
- package/cli/storybook-dist/assets/browserUtils-DeopUpDy.js +1 -0
- package/cli/storybook-dist/assets/chunk-XP5HYGXS-BHXC9YeQ.js +1 -0
- package/cli/storybook-dist/assets/client-DTf7SId2.js +1 -0
- package/cli/storybook-dist/assets/createConveyorBeltLayer-Cn41pFRH.js +6210 -0
- package/cli/storybook-dist/assets/createConveyorBeltLayer-Ds5AOper.css +21 -0
- package/cli/storybook-dist/assets/entry-preview-D-mLV0cS.js +2 -0
- package/cli/storybook-dist/assets/entry-preview-docs-b8bpVBXy.js +46 -0
- package/cli/storybook-dist/assets/iframe-DNru2eN7.js +211 -0
- package/cli/storybook-dist/assets/index-B4FGfHer.js +1 -0
- package/cli/storybook-dist/assets/index-BRq5TIsn.js +24 -0
- package/cli/storybook-dist/assets/index-C1OzuK6-.js +240 -0
- package/cli/storybook-dist/assets/index-CT3U1fyk.js +1 -0
- package/cli/storybook-dist/assets/index-ChsGqxH_.js +9 -0
- package/cli/storybook-dist/assets/index-Cwr2bQOB.js +575 -0
- package/cli/storybook-dist/assets/index-DKFBREX_.js +1 -0
- package/cli/storybook-dist/assets/index-DgH-xKnr.js +11 -0
- package/cli/storybook-dist/assets/index-Dmy2JGbm.js +1 -0
- package/cli/storybook-dist/assets/index-DrFu-skq.js +6 -0
- package/cli/storybook-dist/assets/jsx-runtime-BTJTZTIL.js +9 -0
- package/cli/storybook-dist/assets/keymapTileIndex.worker-BsNYNbqs.js +1 -0
- package/cli/storybook-dist/assets/preview-B8lJiyuQ.js +34 -0
- package/cli/storybook-dist/assets/preview-BBWR9nbA.js +1 -0
- package/cli/storybook-dist/assets/preview-BWzBA1C2.js +396 -0
- package/cli/storybook-dist/assets/preview-CILRoSnB.js +1 -0
- package/cli/storybook-dist/assets/preview-CVgpLj3b.js +1 -0
- package/cli/storybook-dist/assets/preview-CvbIS5ZJ.js +1 -0
- package/cli/storybook-dist/assets/preview-DD_OYowb.js +1 -0
- package/cli/storybook-dist/assets/preview-DGUiP6tS.js +7 -0
- package/cli/storybook-dist/assets/preview-DHQbi4pV.js +1 -0
- package/cli/storybook-dist/assets/preview-DUTcCPBe.css +1 -0
- package/cli/storybook-dist/assets/preview-QAsoD6Sa.js +2 -0
- package/cli/storybook-dist/assets/preview-rcRMbKPr.js +3 -0
- package/cli/storybook-dist/assets/react-18-hB3smqWJ.js +1 -0
- package/cli/storybook-dist/assets/react-resizable-panels.browser-CvAJUcrm.js +72 -0
- package/cli/storybook-dist/assets/test-utils-BQudk2xt.js +9 -0
- package/cli/storybook-dist/favicon.svg +1 -0
- package/cli/storybook-dist/iframe.html +666 -0
- package/cli/storybook-dist/index.html +190 -0
- package/cli/storybook-dist/index.json +1 -0
- package/cli/storybook-dist/nunito-sans-bold-italic.woff2 +0 -0
- package/cli/storybook-dist/nunito-sans-bold.woff2 +0 -0
- package/cli/storybook-dist/nunito-sans-italic.woff2 +0 -0
- package/cli/storybook-dist/nunito-sans-regular.woff2 +0 -0
- package/cli/storybook-dist/project.json +1 -0
- package/cli/storybook-dist/sb-addons/chromatic-com-storybook-10/manager-bundle.js +331 -0
- package/cli/storybook-dist/sb-addons/chromatic-com-storybook-10/manager-bundle.js.LEGAL.txt +51 -0
- package/cli/storybook-dist/sb-addons/essentials-actions-3/manager-bundle.js +3 -0
- package/cli/storybook-dist/sb-addons/essentials-backgrounds-5/manager-bundle.js +12 -0
- package/cli/storybook-dist/sb-addons/essentials-controls-2/manager-bundle.js +405 -0
- package/cli/storybook-dist/sb-addons/essentials-docs-4/manager-bundle.js +245 -0
- package/cli/storybook-dist/sb-addons/essentials-measure-8/manager-bundle.js +3 -0
- package/cli/storybook-dist/sb-addons/essentials-outline-9/manager-bundle.js +3 -0
- package/cli/storybook-dist/sb-addons/essentials-toolbars-7/manager-bundle.js +3 -0
- package/cli/storybook-dist/sb-addons/essentials-viewport-6/manager-bundle.js +3 -0
- package/cli/storybook-dist/sb-addons/links-1/manager-bundle.js +3 -0
- package/cli/storybook-dist/sb-addons/storybook-13/manager-bundle.js +3 -0
- package/cli/storybook-dist/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js +3 -0
- package/cli/storybook-dist/sb-addons/storybook-experimental-addon-test-12/manager-bundle.js +223 -0
- package/cli/storybook-dist/sb-addons/themes-11/manager-bundle.js +3 -0
- package/cli/storybook-dist/sb-common-assets/favicon.svg +1 -0
- package/cli/storybook-dist/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
- package/cli/storybook-dist/sb-common-assets/nunito-sans-bold.woff2 +0 -0
- package/cli/storybook-dist/sb-common-assets/nunito-sans-italic.woff2 +0 -0
- package/cli/storybook-dist/sb-common-assets/nunito-sans-regular.woff2 +0 -0
- package/cli/storybook-dist/sb-manager/globals-module-info.js +1052 -0
- package/cli/storybook-dist/sb-manager/globals-runtime.js +41775 -0
- package/cli/storybook-dist/sb-manager/globals.js +48 -0
- package/cli/storybook-dist/sb-manager/runtime.js +12048 -0
- package/cli/templates/common/.mcp.json +8 -0
- package/cli/templates/common/README.md +817 -9
- package/cli/templates/common/assets/icon.svg +44 -2
- package/cli/templates/common/manifest.json +13 -13
- package/cli/templates/common/package.json +9 -11
- package/cli/templates/common/py/main.py +98 -0
- package/cli/templates/common/requirements.txt +4 -3
- package/cli/templates/common/tsconfig.json +1 -1
- package/cli/templates/common/ui/content.tsx +216 -0
- package/cli/templates/common/ui/style.css +6 -0
- package/cli/templates/common/vite.config.ts +6 -25
- package/cli/templates/cpp/cpp/CMakeLists.txt +14 -0
- package/cli/templates/cpp/cpp/main.cpp +26 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +5 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +17 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +22 -0
- package/cli/vite.d.mts +25 -0
- package/cli/vite.mjs +281 -0
- package/package.json +32 -5
- package/cli/src/pack.mjs +0 -61
- package/cli/templates/common/main.py +0 -21
- package/cli/templates/common/src/content.tsx +0 -32
package/cli/src/new.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// muscat-pk new — scaffold a new plugin project from templates.
|
|
2
|
-
import {
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
3
|
import {
|
|
4
4
|
cpSync,
|
|
5
5
|
existsSync,
|
|
@@ -7,22 +7,52 @@ import {
|
|
|
7
7
|
readFileSync,
|
|
8
8
|
readdirSync,
|
|
9
9
|
renameSync,
|
|
10
|
+
rmSync,
|
|
10
11
|
statSync,
|
|
11
12
|
writeFileSync,
|
|
12
13
|
} from "node:fs";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { detectPythonCmd, parseArgs, templatesDir, log, venvPython } from "./util.mjs";
|
|
14
|
+
import { basename, dirname, resolve } from "node:path";
|
|
15
|
+
import { ensureIdeVenv, hasBin, kitVersion, parseArgs, prompt, templatesDir, log } from "./util.mjs";
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// Like cpSync(src, dst, {recursive:true}) but reads from the embedded snapshot
|
|
18
|
+
// when the templates dir doesn't exist on disk (compiled binary mode).
|
|
19
|
+
// ponytail: import() is module-cached by the runtime — no manual cache needed.
|
|
20
|
+
async function copyTemplate(srcDir, dst) {
|
|
21
|
+
if (existsSync(templatesDir)) { cpSync(srcDir, dst, { recursive: true }); return; }
|
|
22
|
+
const { TEMPLATES } = await import("./templates-embedded.mjs");
|
|
23
|
+
const prefix = srcDir.slice(templatesDir.length + 1).replace(/\\/g, "/") + "/";
|
|
24
|
+
for (const [key, b64] of Object.entries(TEMPLATES)) {
|
|
25
|
+
if (!key.startsWith(prefix)) continue;
|
|
26
|
+
const dest = resolve(dst, key.slice(prefix.length));
|
|
27
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
28
|
+
writeFileSync(dest, Buffer.from(b64, "base64"));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// `--type` values map to a runtime descriptor. ros2 has a python (rclpy) and a
|
|
33
|
+
// C++ (rclcpp) flavor; we expose them as distinct type choices for clarity.
|
|
34
|
+
export const TYPE_CHOICES = {
|
|
35
|
+
python: { type: "python" },
|
|
36
|
+
cpp: { type: "cpp" },
|
|
37
|
+
ros2: { type: "ros2", rosClient: "rclpy" },
|
|
38
|
+
"ros2-cpp":{ type: "ros2", rosClient: "rclcpp" },
|
|
39
|
+
};
|
|
40
|
+
const TYPES = Object.keys(TYPE_CHOICES);
|
|
18
41
|
const PMS = ["npm", "yarn", "pnpm", "bun"];
|
|
19
42
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
43
|
+
// Build the manifest runtime object for a given type choice + plugin slug.
|
|
44
|
+
export function runtimeForChoice(choice, slug) {
|
|
45
|
+
switch (choice.type) {
|
|
46
|
+
case "cpp":
|
|
47
|
+
// entry = built executable name (matches add_executable in cpp/CMakeLists.txt)
|
|
48
|
+
return { type: "cpp", entry: slug };
|
|
49
|
+
case "ros2":
|
|
50
|
+
// package = colcon package name; launch = node executable run on the robot
|
|
51
|
+
return { type: "ros2", rosClient: choice.rosClient, package: slug, launch: `${slug}_node` };
|
|
52
|
+
case "python":
|
|
53
|
+
default:
|
|
54
|
+
return { type: "python", entry: "main.py" };
|
|
55
|
+
}
|
|
26
56
|
}
|
|
27
57
|
|
|
28
58
|
export function applyTokens(dir, tokens) {
|
|
@@ -51,6 +81,7 @@ export function deriveNames(target) {
|
|
|
51
81
|
const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
52
82
|
const pluginName = base.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
53
83
|
return {
|
|
84
|
+
dir: base,
|
|
54
85
|
slug,
|
|
55
86
|
pluginName,
|
|
56
87
|
pluginClass: pluginName.replace(/\s+/g, ""),
|
|
@@ -73,16 +104,25 @@ function renameSlugPaths(dir, slug) {
|
|
|
73
104
|
// Copy the ros2_ws/ overlay into a plugin and substitute the package slug into
|
|
74
105
|
// every path segment. The dev Dockerfile is NOT scaffolded here — it lives in
|
|
75
106
|
// the kit (cli/docker/) so authors can't edit it. Caller runs applyTokens after.
|
|
76
|
-
|
|
77
|
-
|
|
107
|
+
// rclpy → templates/ros2 (ament_python)
|
|
108
|
+
// rclcpp → templates/ros2-cpp (ament_cmake)
|
|
109
|
+
export async function overlayRos2(target, slug, rosClient = "rclpy") {
|
|
110
|
+
const tmpl = rosClient === "rclcpp" ? "ros2-cpp" : "ros2";
|
|
111
|
+
await copyTemplate(resolve(templatesDir, tmpl), target);
|
|
78
112
|
renameSlugPaths(resolve(target, "ros2_ws"), slug);
|
|
79
113
|
}
|
|
80
114
|
|
|
115
|
+
// Copy the cpp/ overlay (CMake project) into a plugin. Caller runs applyTokens after.
|
|
116
|
+
export async function overlayCpp(target) {
|
|
117
|
+
await copyTemplate(resolve(templatesDir, "cpp"), target);
|
|
118
|
+
}
|
|
119
|
+
|
|
81
120
|
export async function runNew(args) {
|
|
82
121
|
const opts = parseArgs(args, { booleans: ["yes", "no-install", "help"] });
|
|
83
122
|
if (opts.help) {
|
|
84
123
|
console.log(
|
|
85
|
-
"muscat-pk new [dir] [--type python|ros2] [--pm npm|yarn|pnpm|bun]\n" +
|
|
124
|
+
"muscat-pk new [dir] [--type python|cpp|ros2|ros2-cpp] [--pm npm|yarn|pnpm|bun]\n" +
|
|
125
|
+
" [--author <name>] [--email <email>] [--description <text>]\n" +
|
|
86
126
|
" [--yes] [--no-install]",
|
|
87
127
|
);
|
|
88
128
|
return;
|
|
@@ -96,45 +136,88 @@ export async function runNew(args) {
|
|
|
96
136
|
throw new Error(`directory not empty: ${target}`);
|
|
97
137
|
}
|
|
98
138
|
|
|
139
|
+
let authorArg = opts.author;
|
|
140
|
+
let emailArg = opts.email;
|
|
141
|
+
let descriptionArg = opts.description;
|
|
142
|
+
|
|
143
|
+
if (!opts.yes) {
|
|
144
|
+
if (!authorArg) authorArg = await prompt("Author name: ", "");
|
|
145
|
+
if (!emailArg) emailArg = await prompt("Email (required): ", "");
|
|
146
|
+
if (!descriptionArg) descriptionArg = await prompt("Description (English): ", "");
|
|
147
|
+
}
|
|
148
|
+
|
|
99
149
|
let type = opts.type;
|
|
100
150
|
if (!type && !opts.yes) {
|
|
101
|
-
console.log("\
|
|
151
|
+
console.log("\nSelect the plugin type:");
|
|
102
152
|
console.log(" 1) Python");
|
|
103
|
-
console.log(" 2)
|
|
104
|
-
|
|
105
|
-
|
|
153
|
+
console.log(" 2) C++");
|
|
154
|
+
console.log(" 3) ROS2 (rclpy / Python)");
|
|
155
|
+
console.log(" 4) ROS2 (rclcpp / C++)");
|
|
156
|
+
const choice = await prompt("\n선택 [1/2/3/4, default: 1]: ", "1");
|
|
157
|
+
type = { "2": "cpp", "3": "ros2", "4": "ros2-cpp" }[choice] ?? "python";
|
|
106
158
|
}
|
|
107
159
|
type = TYPES.includes(type) ? type : "python";
|
|
160
|
+
const runtimeChoice = TYPE_CHOICES[type];
|
|
161
|
+
|
|
162
|
+
// Auto-detect yarn workspace: if .yarnrc.yml exists anywhere up the tree, default to yarn.
|
|
163
|
+
function isInsideYarnWorkspace(dir) {
|
|
164
|
+
let cur = dir;
|
|
165
|
+
while (true) {
|
|
166
|
+
if (existsSync(resolve(cur, ".yarnrc.yml"))) return true;
|
|
167
|
+
const parent = resolve(cur, "..");
|
|
168
|
+
if (parent === cur) return false;
|
|
169
|
+
cur = parent;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
108
172
|
|
|
109
173
|
let pm = opts.pm;
|
|
110
174
|
if (!pm && !opts.yes) {
|
|
111
|
-
|
|
175
|
+
const defaultPm = isInsideYarnWorkspace(process.cwd()) ? "yarn" : "npm";
|
|
176
|
+
pm = await prompt(`Front-end package manager (${PMS.join("/")}) [${defaultPm}]: `, defaultPm);
|
|
112
177
|
}
|
|
113
178
|
pm = PMS.includes(pm) ? pm : "npm";
|
|
114
179
|
|
|
115
|
-
const { slug, pluginName, pluginClass, pluginId } = deriveNames(target);
|
|
180
|
+
const { dir, slug, pluginName, pluginClass, pluginId } = deriveNames(target);
|
|
116
181
|
|
|
117
182
|
mkdirSync(target, { recursive: true });
|
|
118
183
|
|
|
119
|
-
// common(UI +
|
|
120
|
-
//
|
|
184
|
+
// common(UI + manifest/config)은 항상 포함. 백엔드 오버레이는 타입별로 얹는다.
|
|
185
|
+
// python → common의 py/ 사용
|
|
186
|
+
// cpp → cpp/ 오버레이, py/·requirements.txt 제거
|
|
187
|
+
// ros2(*) → ros2_ws/ 오버레이 (rclpy=ament_python, rclcpp=ament_cmake)
|
|
121
188
|
log.step(`scaffolding ${type} plugin → ${target}`);
|
|
122
|
-
|
|
123
|
-
if (type === "ros2") overlayRos2(target, slug);
|
|
189
|
+
await copyTemplate(resolve(templatesDir, "common"), target);
|
|
190
|
+
if (runtimeChoice.type === "ros2") await overlayRos2(target, slug, runtimeChoice.rosClient);
|
|
191
|
+
if (runtimeChoice.type === "cpp") {
|
|
192
|
+
await overlayCpp(target);
|
|
193
|
+
rmSync(resolve(target, "py"), { recursive: true, force: true });
|
|
194
|
+
rmSync(resolve(target, "requirements.txt"), { force: true });
|
|
195
|
+
}
|
|
124
196
|
|
|
125
197
|
applyTokens(target, {
|
|
126
198
|
__PLUGIN_ID__: pluginId,
|
|
127
199
|
__PLUGIN_NAME__: pluginName,
|
|
200
|
+
__PLUGIN_DIR__: dir,
|
|
128
201
|
__PLUGIN_SLUG__: slug,
|
|
129
202
|
__PLUGIN_CLASS__: pluginClass,
|
|
203
|
+
__PLUGIN_AUTHOR__: authorArg || "",
|
|
204
|
+
__PLUGIN_EMAIL__: emailArg || "",
|
|
130
205
|
__PM__: pm,
|
|
131
|
-
__RUNTIME_TYPE__: type,
|
|
206
|
+
__RUNTIME_TYPE__: runtimeChoice.type,
|
|
207
|
+
__RB_VERSION__: kitVersion(),
|
|
132
208
|
});
|
|
133
209
|
|
|
210
|
+
// Write the correct runtime block into manifest.json (per-type fields).
|
|
211
|
+
const manifestPath = resolve(target, "manifest.json");
|
|
212
|
+
const manifestJson = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
213
|
+
manifestJson.runtime = runtimeForChoice(runtimeChoice, slug);
|
|
214
|
+
if (descriptionArg) manifestJson.description = { en: descriptionArg };
|
|
215
|
+
writeFileSync(manifestPath, JSON.stringify(manifestJson, null, 2) + "\n");
|
|
216
|
+
|
|
134
217
|
// gitignore (templates can't ship a literal .gitignore via npm reliably)
|
|
135
218
|
writeFileSync(
|
|
136
219
|
resolve(target, ".gitignore"),
|
|
137
|
-
["node_modules", "dist", ".venv", "*.zip", "ros2_ws/install", ""].join("\n"),
|
|
220
|
+
["node_modules", "dist", ".muscat-dev", ".venv", "*.zip", "ros2_ws/install", "cpp/build", ""].join("\n"),
|
|
138
221
|
);
|
|
139
222
|
|
|
140
223
|
if (!opts["no-install"]) {
|
|
@@ -145,27 +228,44 @@ export async function runNew(args) {
|
|
|
145
228
|
log.warn(`'${pm} install' failed; run it manually in ${targetName}`);
|
|
146
229
|
}
|
|
147
230
|
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
});
|
|
231
|
+
// Local .venv is for your editor/linter (pylint, pyright, ...) only — the
|
|
232
|
+
// backend actually RUNS in Docker (see README), this never affects that.
|
|
233
|
+
// rclcpp is C++, so no venv applies there.
|
|
234
|
+
const needsIdeVenv =
|
|
235
|
+
runtimeChoice.type === "python" ||
|
|
236
|
+
(runtimeChoice.type === "ros2" && runtimeChoice.rosClient === "rclpy");
|
|
237
|
+
if (needsIdeVenv) {
|
|
238
|
+
log.step("creating .venv for your editor (pylint/pyright)…");
|
|
239
|
+
if (ensureIdeVenv(target)) {
|
|
240
|
+
log.ok(".venv ready — point your editor's Python interpreter there.");
|
|
241
|
+
} else {
|
|
242
|
+
log.warn("Could not create .venv (no local Python/uv?) — skipping, Docker dev still works.");
|
|
161
243
|
}
|
|
162
|
-
} catch {
|
|
163
|
-
log.warn("venv setup failed; create it manually: python3.12 -m venv .venv");
|
|
164
244
|
}
|
|
165
245
|
}
|
|
166
246
|
|
|
247
|
+
// The back end (python / cpp / ros2) runs in a Docker container that matches
|
|
248
|
+
// the robot — no local Python/toolchain setup needed. Only `none` skips it.
|
|
249
|
+
const needsDocker = runtimeChoice.type !== "none";
|
|
250
|
+
const noDocker = needsDocker && !hasBin("docker");
|
|
251
|
+
|
|
167
252
|
log.ok(`created ${targetName} (type: ${type}, pm: ${pm})`);
|
|
168
253
|
log.info(`\n cd ${targetName}`);
|
|
169
|
-
log.info(`
|
|
170
|
-
log.info(`
|
|
254
|
+
log.info(` muscat-pk dev # preview in mini-Muscat + back end`);
|
|
255
|
+
log.info(` muscat-pk build # build + distributable ZIP\n`);
|
|
256
|
+
|
|
257
|
+
// ── post-scaffold environment warnings ───────────────────────────────────
|
|
258
|
+
const noNode = !hasBin("node");
|
|
259
|
+
if (noNode || noDocker) {
|
|
260
|
+
log.info("");
|
|
261
|
+
if (noNode) {
|
|
262
|
+
log.warn("Node.js not found. Install Node.js 20+ to run JS deps and dev:ui.");
|
|
263
|
+
log.warn(" https://nodejs.org");
|
|
264
|
+
}
|
|
265
|
+
if (noDocker) {
|
|
266
|
+
log.warn("Docker not found. The plugin back end runs in a container (matches the robot env).");
|
|
267
|
+
log.warn(" https://docs.docker.com/get-docker/");
|
|
268
|
+
}
|
|
269
|
+
log.info("");
|
|
270
|
+
}
|
|
171
271
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { writeFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const pluginRoot = resolve(process.argv[2]);
|
|
8
|
+
const port = Number(process.argv[3] || "4181");
|
|
9
|
+
const readyFile = process.argv[4] ? resolve(process.argv[4]) : "";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
process.chdir(pluginRoot);
|
|
13
|
+
|
|
14
|
+
const require = createRequire(resolve(pluginRoot, "package.json"));
|
|
15
|
+
|
|
16
|
+
const viteMod = await import(pathToFileURL(require.resolve("vite")));
|
|
17
|
+
const createServer = viteMod.createServer ?? viteMod.default?.createServer;
|
|
18
|
+
|
|
19
|
+
const BOOTSTRAP_PREFIX = "\0rb-bootstrap:";
|
|
20
|
+
const PREAMBLE_ID = "\0rb-preamble";
|
|
21
|
+
const bootstrapPlugin = {
|
|
22
|
+
name: "rb-plugin-bootstrap",
|
|
23
|
+
resolveId(id) {
|
|
24
|
+
if (id.startsWith("rb-bootstrap:")) return "\0" + id;
|
|
25
|
+
if (id === "rb-preamble") return PREAMBLE_ID;
|
|
26
|
+
},
|
|
27
|
+
load(id) {
|
|
28
|
+
if (id === PREAMBLE_ID) {
|
|
29
|
+
return `
|
|
30
|
+
import RefreshRuntime from "/@react-refresh";
|
|
31
|
+
RefreshRuntime.injectIntoGlobalHook(window);
|
|
32
|
+
window.$RefreshReg$ = () => {};
|
|
33
|
+
window.$RefreshSig$ = () => (type) => type;
|
|
34
|
+
window.__vite_plugin_react_preamble_installed__ = true;
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
if (!id.startsWith(BOOTSTRAP_PREFIX)) return;
|
|
38
|
+
const ui = id.slice(BOOTSTRAP_PREFIX.length) || "content";
|
|
39
|
+
const entry = JSON.stringify(`muscat-ui-entry:${ui}`);
|
|
40
|
+
return `
|
|
41
|
+
import "rb-preamble";
|
|
42
|
+
import { createElement } from "react";
|
|
43
|
+
import { createRoot } from "react-dom/client";
|
|
44
|
+
import { RbContext } from "@rainbow-robotics/plugin-ui/sdk";
|
|
45
|
+
import { setHostServices } from "@rainbow-robotics/plugin-ui/host-ui";
|
|
46
|
+
import { RbWs, createLazyRbSdk } from "@rainbow-robotics/plugin-ui/ws";
|
|
47
|
+
import App from ${entry};
|
|
48
|
+
|
|
49
|
+
// HMR passes the new module to the accept callback but doesn't rebind the
|
|
50
|
+
// static \`App\` import, so we track the latest at module scope. Without this,
|
|
51
|
+
// navigating away and back (a fresh mount() on this cached module) renders the
|
|
52
|
+
// stale App from first import. \`activeRender\` points at the mounted root so
|
|
53
|
+
// live edits re-render in place.
|
|
54
|
+
let latestApp = App;
|
|
55
|
+
let activeRender = null;
|
|
56
|
+
if (import.meta.hot) {
|
|
57
|
+
import.meta.hot.accept(${entry}, (m) => {
|
|
58
|
+
if (!m || !m.default) return;
|
|
59
|
+
latestApp = m.default;
|
|
60
|
+
activeRender?.(latestApp);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function mount(container, { socketUrl, pluginId, hostServices, data, onSave }) {
|
|
65
|
+
container.setAttribute("data-muscat-plugin-root", "");
|
|
66
|
+
container.setAttribute("data-muscat-plugin-id", pluginId);
|
|
67
|
+
setHostServices(hostServices);
|
|
68
|
+
// Lazy connect: don't block the first render on the WebSocket handshake
|
|
69
|
+
// (there may be no backend at all in dev, e.g. no robot reachable) — each rb
|
|
70
|
+
// method awaits this promise internally instead.
|
|
71
|
+
const wsPromise = RbWs.connect(socketUrl, pluginId);
|
|
72
|
+
const rb = createLazyRbSdk(wsPromise, hostServices, { data, onSave });
|
|
73
|
+
const root = createRoot(container);
|
|
74
|
+
let mounted = true;
|
|
75
|
+
const render = (Component) => {
|
|
76
|
+
if (!mounted) return;
|
|
77
|
+
root.render(createElement(RbContext.Provider, { value: rb }, createElement(Component)));
|
|
78
|
+
};
|
|
79
|
+
const boundRender = (Component) => queueMicrotask(() => render(Component));
|
|
80
|
+
activeRender = boundRender;
|
|
81
|
+
render(latestApp);
|
|
82
|
+
return () => {
|
|
83
|
+
mounted = false;
|
|
84
|
+
// Don't clobber a newer mount's render (host defers unmount a microtask).
|
|
85
|
+
if (activeRender === boundRender) activeRender = null;
|
|
86
|
+
root.unmount();
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const server = await createServer({
|
|
94
|
+
root: pluginRoot,
|
|
95
|
+
configFile: resolve(pluginRoot, "vite.config.ts"),
|
|
96
|
+
mode: "development",
|
|
97
|
+
plugins: [bootstrapPlugin],
|
|
98
|
+
server: {
|
|
99
|
+
port,
|
|
100
|
+
strictPort: false,
|
|
101
|
+
cors: true,
|
|
102
|
+
hmr: { host: "localhost", protocol: "ws" },
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await server.listen();
|
|
107
|
+
const address = server.httpServer?.address();
|
|
108
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
109
|
+
if (readyFile) {
|
|
110
|
+
writeFileSync(readyFile, JSON.stringify({ port: actualPort }), "utf8");
|
|
111
|
+
}
|
|
112
|
+
console.log(`[plugin-dev] vite dev server → http://localhost:${actualPort}`);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Port contract helpers for plugin back ends.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
export const PLUGIN_PORT_MIN = 28600;
|
|
5
|
+
export const PLUGIN_PORT_MAX = 28999;
|
|
6
|
+
export const INTERNAL_DEV_PORTS = new Set([10000]);
|
|
7
|
+
export const RESERVED_BACKEND_PORTS = new Map([
|
|
8
|
+
[502, "Modbus TCP"],
|
|
9
|
+
[4840, "OPC-UA"],
|
|
10
|
+
[10000, "muscat-pk Socket.IO bridge"],
|
|
11
|
+
[2222, "EtherNet/IP Implicit"],
|
|
12
|
+
[30004, "RTDE"],
|
|
13
|
+
[34964, "ProfiNET"],
|
|
14
|
+
[44818, "EtherNet/IP Explicit"],
|
|
15
|
+
[50051, "gRPC"],
|
|
16
|
+
]);
|
|
17
|
+
export const RESERVED_BACKEND_PORT_RANGES = [
|
|
18
|
+
{ from: 5170, to: 5179, name: "UI 517x servers" },
|
|
19
|
+
{ from: 8000, to: 8999, name: "Muscat/internal 8000 range" },
|
|
20
|
+
{ from: 50000, to: 50005, name: "robot TCP/UDP ASCII and REST interfaces" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function reservedPortReason(port) {
|
|
24
|
+
if (RESERVED_BACKEND_PORTS.has(port)) return RESERVED_BACKEND_PORTS.get(port);
|
|
25
|
+
const range = RESERVED_BACKEND_PORT_RANGES.find((item) => port >= item.from && port <= item.to);
|
|
26
|
+
if (!range) return null;
|
|
27
|
+
return `${range.name} (${range.from}-${range.to})`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isPluginManagedPort(port) {
|
|
31
|
+
return reservedPortReason(port) || (port >= PLUGIN_PORT_MIN && port <= PLUGIN_PORT_MAX);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isInt(value) {
|
|
35
|
+
return Number.isInteger(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validatePort(port, field, errors) {
|
|
39
|
+
if (!isInt(port)) {
|
|
40
|
+
errors.push(`${field}: must be a port number`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (port < 1 || port > 65535) {
|
|
44
|
+
errors.push(`${field}: port must be between 1 and 65535`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const reserved = reservedPortReason(port);
|
|
48
|
+
if (reserved) {
|
|
49
|
+
errors.push(`${field}: ${port} is reserved for ${reserved}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (port < PLUGIN_PORT_MIN || port > PLUGIN_PORT_MAX) {
|
|
53
|
+
errors.push(
|
|
54
|
+
`${field}: external plugin backend ports must be within ${PLUGIN_PORT_MIN}-${PLUGIN_PORT_MAX}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function normalizePorts(ports, errors = []) {
|
|
60
|
+
if (ports === undefined || ports === null) return [];
|
|
61
|
+
if (!Array.isArray(ports)) {
|
|
62
|
+
errors.push(`"ports" must be an array`);
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
if (ports.length === 0) {
|
|
66
|
+
errors.push(`"ports" must not be empty when provided`);
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const normalized = [];
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
ports.forEach((port, i) => {
|
|
73
|
+
validatePort(port, `ports[${i}]`, errors);
|
|
74
|
+
if (!isInt(port)) return;
|
|
75
|
+
if (seen.has(port)) {
|
|
76
|
+
errors.push(`"ports" entries must be unique (duplicate: ${port})`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
seen.add(port);
|
|
80
|
+
normalized.push(port);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return normalized;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function validatePorts(ports, errors) {
|
|
87
|
+
normalizePorts(ports, errors);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function manifestPortRanges(manifest) {
|
|
91
|
+
return normalizePorts(manifest?.ports, []);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function formatPortRange(range) {
|
|
95
|
+
return String(range);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function formatPortRanges(ranges) {
|
|
99
|
+
return ranges.map(formatPortRange).join(", ");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function portInRanges(port, ranges) {
|
|
103
|
+
return ranges.includes(port);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function portEnvFlags(ranges) {
|
|
107
|
+
if (ranges.length === 0) return [];
|
|
108
|
+
return ["-e", `RB_PLUGIN_PORTS=${ranges.map(formatPortRange).join(",")}`];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function listeningTcpPorts(container) {
|
|
112
|
+
const script = `
|
|
113
|
+
for f in /proc/net/tcp /proc/net/tcp6; do
|
|
114
|
+
[ -r "$f" ] || continue
|
|
115
|
+
tail -n +2 "$f" | while read -r sl local_address rem_address st rest; do
|
|
116
|
+
[ "$st" = "0A" ] || continue
|
|
117
|
+
hex_port="\${local_address##*:}"
|
|
118
|
+
printf "%d\\n" "0x$hex_port"
|
|
119
|
+
done
|
|
120
|
+
done | sort -n -u
|
|
121
|
+
`;
|
|
122
|
+
const r = spawnSync("docker", ["exec", container, "bash", "-lc", script], {
|
|
123
|
+
encoding: "utf8",
|
|
124
|
+
});
|
|
125
|
+
if (r.status !== 0) return [];
|
|
126
|
+
return String(r.stdout || "")
|
|
127
|
+
.split(/\s+/)
|
|
128
|
+
.filter(Boolean)
|
|
129
|
+
.map((v) => Number(v))
|
|
130
|
+
.filter((v) => Number.isInteger(v));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function startPortContractMonitor({ container, ranges, log, intervalMs = 1000 }) {
|
|
134
|
+
const declared = ranges ?? [];
|
|
135
|
+
let reported = false;
|
|
136
|
+
const timer = setInterval(() => {
|
|
137
|
+
const ports = listeningTcpPorts(container)
|
|
138
|
+
.filter((port) => !INTERNAL_DEV_PORTS.has(port))
|
|
139
|
+
.filter((port) => isPluginManagedPort(port));
|
|
140
|
+
const unexpected = ports.filter((port) => reservedPortReason(port) || !portInRanges(port, declared));
|
|
141
|
+
if (unexpected.length === 0) return;
|
|
142
|
+
|
|
143
|
+
clearInterval(timer);
|
|
144
|
+
reported = true;
|
|
145
|
+
const declaredText = declared.length > 0 ? formatPortRanges(declared) : "(none)";
|
|
146
|
+
log.error(
|
|
147
|
+
`backend opened reserved or undeclared plugin TCP port(s): ${unexpected.join(", ")}. ` +
|
|
148
|
+
`Add them to manifest.json "ports" within ${PLUGIN_PORT_MIN}-${PLUGIN_PORT_MAX}. ` +
|
|
149
|
+
`Declared: ${declaredText}`,
|
|
150
|
+
);
|
|
151
|
+
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}, intervalMs);
|
|
154
|
+
timer.unref?.();
|
|
155
|
+
|
|
156
|
+
return () => {
|
|
157
|
+
clearInterval(timer);
|
|
158
|
+
return reported;
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Shared Python Docker helpers (Ubuntu 24.04 + Python 3.12 — matches the robot).
|
|
2
|
+
// Used by dev:back (hot-reload) and build (lock + wheel vendor).
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
|
|
7
|
+
|
|
8
|
+
export const PYTHON_DEV_IMAGE = "muscat-pk-python-dev";
|
|
9
|
+
|
|
10
|
+
export function assertDockerReady() {
|
|
11
|
+
if (!hasBin("docker")) {
|
|
12
|
+
throw new Error("Docker not found. The Python backend runs in a container (Ubuntu 24.04 + Python 3.12).");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
|
|
17
|
+
export async function ensureImage(image = PYTHON_DEV_IMAGE, { platform = null } = {}) {
|
|
18
|
+
const dockerfile = await resolveDockerfile("Dockerfile.python");
|
|
19
|
+
log.step(`docker build -t ${image}${platform ? ` (${platform})` : ""} (first run is slow, then cached)…`);
|
|
20
|
+
const platformFlags = platform ? ["--platform", platform] : [];
|
|
21
|
+
const r = spawnSync(
|
|
22
|
+
"docker",
|
|
23
|
+
["build", ...platformFlags, "-t", image, "-f", dockerfile, dirname(dockerfile)],
|
|
24
|
+
{ stdio: "inherit" },
|
|
25
|
+
);
|
|
26
|
+
if (r.status !== 0) throw new Error("docker build failed.");
|
|
27
|
+
pruneMuskatImages();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// pip --platform does NOT auto-expand manylinux tags (unlike sys_tags()) — exact match only.
|
|
31
|
+
// Specifying all common glibc milestones ensures packages built for any Ubuntu era are found.
|
|
32
|
+
const PIP_PLATFORM = {
|
|
33
|
+
amd64: [
|
|
34
|
+
"manylinux_2_5_x86_64",
|
|
35
|
+
"manylinux_2_17_x86_64",
|
|
36
|
+
"manylinux_2_27_x86_64",
|
|
37
|
+
"manylinux_2_28_x86_64",
|
|
38
|
+
"manylinux_2_34_x86_64",
|
|
39
|
+
"manylinux_2_39_x86_64",
|
|
40
|
+
"linux_x86_64",
|
|
41
|
+
],
|
|
42
|
+
arm64: [
|
|
43
|
+
"manylinux_2_17_aarch64",
|
|
44
|
+
"manylinux_2_27_aarch64",
|
|
45
|
+
"manylinux_2_28_aarch64",
|
|
46
|
+
"manylinux_2_34_aarch64",
|
|
47
|
+
"manylinux_2_39_aarch64",
|
|
48
|
+
"linux_aarch64",
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export async function lockAndVendor(pluginRoot, archs = ["amd64", "arm64"]) {
|
|
53
|
+
// ponytail: pip --platform used intentionally (not QEMU) — downloads wheels without running target arch code.
|
|
54
|
+
const reqTxt = resolve(pluginRoot, "requirements.txt");
|
|
55
|
+
if (!existsSync(reqTxt)) {
|
|
56
|
+
log.warn("requirements.txt not found — lock/vendor skipped");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await ensureImage();
|
|
61
|
+
|
|
62
|
+
log.step("generating lock (uv pip compile)…");
|
|
63
|
+
const lockResult = spawnSync(
|
|
64
|
+
"docker",
|
|
65
|
+
[
|
|
66
|
+
"run", "--rm",
|
|
67
|
+
"-v", `${pluginRoot}:/plugin`,
|
|
68
|
+
PYTHON_DEV_IMAGE,
|
|
69
|
+
"bash", "-c",
|
|
70
|
+
"set -e && cd /plugin && mkdir -p dist && uv pip compile requirements.txt --python-version 3.12 -o dist/requirements.lock",
|
|
71
|
+
],
|
|
72
|
+
{ stdio: "inherit" },
|
|
73
|
+
);
|
|
74
|
+
if (lockResult.status !== 0) throw new Error("lock generation failed (uv pip compile)");
|
|
75
|
+
log.ok("dist/requirements.lock ready");
|
|
76
|
+
|
|
77
|
+
const failed = [];
|
|
78
|
+
for (const arch of archs) {
|
|
79
|
+
const pipPlatforms = PIP_PLATFORM[arch];
|
|
80
|
+
if (!pipPlatforms) { log.warn(`unknown arch '${arch}' — skipped`); continue; }
|
|
81
|
+
log.step(`vendoring wheels for ${arch}…`);
|
|
82
|
+
const r = spawnSync(
|
|
83
|
+
"docker",
|
|
84
|
+
[
|
|
85
|
+
"run", "--rm",
|
|
86
|
+
"-e", "PIP_ROOT_USER_ACTION=ignore",
|
|
87
|
+
"-v", `${pluginRoot}:/plugin`,
|
|
88
|
+
"-w", "/plugin",
|
|
89
|
+
PYTHON_DEV_IMAGE,
|
|
90
|
+
"bash", "-c",
|
|
91
|
+
"mkdir -p dist/wheels && pip3 download -r dist/requirements.lock --no-deps --only-binary=:all: -d dist/wheels -q " +
|
|
92
|
+
"--python-version 312 --implementation cp --abi cp312 " +
|
|
93
|
+
pipPlatforms.map((p) => `--platform ${p}`).join(" "),
|
|
94
|
+
],
|
|
95
|
+
{ stdio: "inherit" },
|
|
96
|
+
);
|
|
97
|
+
if (r.status !== 0) {
|
|
98
|
+
log.warn(`wheel vendoring failed for ${arch}`);
|
|
99
|
+
failed.push(arch);
|
|
100
|
+
} else {
|
|
101
|
+
log.ok(`dist/wheels/ ready (${arch})`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (failed.length === archs.length) {
|
|
106
|
+
throw new Error(`wheel vendoring failed for all archs: ${failed.join(", ")}`);
|
|
107
|
+
}
|
|
108
|
+
return failed;
|
|
109
|
+
}
|