@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -49
- package/cli/README.md +191 -5
- package/cli/bin/create-muscat-plugin.mjs +1 -1
- package/cli/bin/muscat-pk.mjs +25 -14
- 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 -0
- package/cli/docker/Dockerfile.python +20 -0
- package/cli/docker/bridge-package.json +8 -0
- package/cli/prepack.mjs +10 -0
- package/cli/src/__pycache__/host_agent.cpython-37.pyc +0 -0
- package/cli/src/bridge.mjs +95 -0
- package/cli/src/build.mjs +345 -85
- package/cli/src/check-package.mjs +8 -23
- package/cli/src/cpp.mjs +61 -0
- package/cli/src/dev.mjs +156 -4
- package/cli/src/devBack.mjs +505 -57
- package/cli/src/devUi.mjs +167 -22
- package/cli/src/esbuild.mjs +27 -10
- package/cli/src/host_agent.py +162 -0
- package/cli/src/manifest.mjs +208 -0
- package/cli/src/mcp.mjs +617 -0
- package/cli/src/new.mjs +170 -67
- 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 +133 -0
- package/cli/src/storybook.mjs +168 -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 +123 -9
- package/cli/src/zenohDev.mjs +75 -0
- 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-ttZJDaKn.js +1 -0
- package/cli/storybook-dist/assets/Docs-B1jnlXHd.js +12 -0
- package/cli/storybook-dist/assets/Docs-B7dRsHh7.js +8 -0
- package/cli/storybook-dist/assets/Docs-C68t3mnn.js +20 -0
- package/cli/storybook-dist/assets/Docs-D7rPMMhK.js +7 -0
- package/cli/storybook-dist/assets/Docs-DM98U-ZG.js +7 -0
- package/cli/storybook-dist/assets/Docs-guWsANnn.js +8 -0
- package/cli/storybook-dist/assets/DocsRenderer-CFRXHY34-t12O9QfY.js +2 -0
- package/cli/storybook-dist/assets/NxAvatar.stories-ikdLTtys.js +13 -0
- package/cli/storybook-dist/assets/NxButton.stories-DL3S2KEV.js +20 -0
- package/cli/storybook-dist/assets/NxIcons.stories-Ca7qpAaO.js +3 -0
- package/cli/storybook-dist/assets/NxNumberInput.stories-D6NVMTos.js +26 -0
- package/cli/storybook-dist/assets/NxPagination.stories-BDKSlHiQ.js +11 -0
- package/cli/storybook-dist/assets/NxPointerHoldButton.stories-BI544zqB.js +14 -0
- package/cli/storybook-dist/assets/NxRangeInput.stories-BigBBkE7.js +17 -0
- package/cli/storybook-dist/assets/NxSelectBox.stories-Ci0IyUWd.js +17 -0
- package/cli/storybook-dist/assets/NxSlideRange.stories-sjnPAjFy.js +14 -0
- package/cli/storybook-dist/assets/NxSwitch.stories-CRxPDlJm.js +11 -0
- package/cli/storybook-dist/assets/NxTextArea.stories-Jh09n24O.js +11 -0
- package/cli/storybook-dist/assets/NxTextInput.stories-BeGCjxES.js +11 -0
- package/cli/storybook-dist/assets/PretendardVariable-CJuje-Rk.woff2 +0 -0
- package/cli/storybook-dist/assets/RBAccordion.stories-CnC95OwM.js +17 -0
- package/cli/storybook-dist/assets/RBAccordionGroup.stories-og-6ePox.js +33 -0
- package/cli/storybook-dist/assets/RBAlert.stories-BcE9-UOT.js +17 -0
- package/cli/storybook-dist/assets/RBBadge.stories-DYZMXb-e.js +1 -0
- package/cli/storybook-dist/assets/RBBattery.stories-B7YAUvlM.js +10 -0
- package/cli/storybook-dist/assets/RBCard.stories-DTL_7n6I.js +45 -0
- package/cli/storybook-dist/assets/RBCheckBox.stories-D9KC6hTg.js +1 -0
- package/cli/storybook-dist/assets/RBCheckIconButton.stories-BR8aIkOC.js +1 -0
- package/cli/storybook-dist/assets/RBCheckLabelButton.stories-C1ABYhEr.js +24 -0
- package/cli/storybook-dist/assets/RBChipTab.stories-Ceo_vnNi.js +21 -0
- package/cli/storybook-dist/assets/RBChipsBoxTab.stories-CB_bfK8M.js +1 -0
- package/cli/storybook-dist/assets/RBColorIcons.stories-DHZKezld.js +3 -0
- package/cli/storybook-dist/assets/RBCommand.stories-DjuCDn5h.js +73 -0
- package/cli/storybook-dist/assets/RBConfirm.stories-HMmUPkB1.js +20 -0
- package/cli/storybook-dist/assets/RBDateInput.stories-BRfpYtJ4.js +15 -0
- package/cli/storybook-dist/assets/RBDialog.stories-Ln23Gyhk.js +38 -0
- package/cli/storybook-dist/assets/RBDivider.stories-Bhc_D5Cm.js +1 -0
- package/cli/storybook-dist/assets/RBDrawer.stories-CEEhamr_.js +14 -0
- package/cli/storybook-dist/assets/RBDropdownMenu.stories-DBvwDCnf.js +86 -0
- package/cli/storybook-dist/assets/RBEmptyView.stories-BY5QtXVc.js +12 -0
- package/cli/storybook-dist/assets/RBIconButton.stories-DyMFWpIV.js +1 -0
- package/cli/storybook-dist/assets/RBIcons.stories-DisU8G0d.js +3 -0
- package/cli/storybook-dist/assets/RBInlineLabelButton.stories-Cx0KJqxD.js +1 -0
- package/cli/storybook-dist/assets/RBJoyStick.stories-D1THM1Du.js +11 -0
- package/cli/storybook-dist/assets/RBLabelButton.stories-BDktlyzc.js +1 -0
- package/cli/storybook-dist/assets/RBLight.stories-C5qpnM6J.js +1 -0
- package/cli/storybook-dist/assets/RBMenuTreeIcons.stories-DUCeQPKJ.js +3 -0
- package/cli/storybook-dist/assets/RBMenubar.stories-ClyM9v37.js +3 -0
- package/cli/storybook-dist/assets/RBMultiRangeInput.stories--xSn64ww.js +14 -0
- package/cli/storybook-dist/assets/RBPagination.stories-C_uTj83z.js +17 -0
- package/cli/storybook-dist/assets/RBPopover.stories-DWmb9Por.js +11 -0
- package/cli/storybook-dist/assets/RBProgramIcons.stories-B8ZjEtvw.js +3 -0
- package/cli/storybook-dist/assets/RBPrompt.stories-BFKfyYtl.js +20 -0
- package/cli/storybook-dist/assets/RBRadioBox.stories-DUz8a27t.js +1 -0
- package/cli/storybook-dist/assets/RBRadioButton.stories-DKF61xvC.js +11 -0
- package/cli/storybook-dist/assets/RBRangeDateInput.stories-GhMVDyMr.js +15 -0
- package/cli/storybook-dist/assets/RBResizable.stories-V0iisR-x.js +20 -0
- package/cli/storybook-dist/assets/RBSelectBox.stories-DhvooAcg.js +6 -0
- package/cli/storybook-dist/assets/RBSheet.stories-C93Hpmvn.js +39 -0
- package/cli/storybook-dist/assets/RBSideNumberInput.stories-Y547mUsB.js +5 -0
- package/cli/storybook-dist/assets/RBSlider.stories-BJZyHiLO.js +5 -0
- package/cli/storybook-dist/assets/RBSpinner.stories-6wkbu-bU.js +14 -0
- package/cli/storybook-dist/assets/RBSwitch.stories-T9InolEP.js +65 -0
- package/cli/storybook-dist/assets/RBTab.stories-CjGzZODx.js +1 -0
- package/cli/storybook-dist/assets/RBTable.stories-CaolKsVG.js +62 -0
- package/cli/storybook-dist/assets/RBTableRow.stories-D13urGfs.js +11 -0
- package/cli/storybook-dist/assets/RBTabs.stories-Cv2iIxvP.js +21 -0
- package/cli/storybook-dist/assets/RBTextArea.stories-CmrPeit2.js +5 -0
- package/cli/storybook-dist/assets/RBTextField.stories-ByW-EKCD.js +45 -0
- package/cli/storybook-dist/assets/RBTextInput.stories-DSPlr3uh.js +5 -0
- package/cli/storybook-dist/assets/RBToast.stories-DJNZ9tku.js +11 -0
- package/cli/storybook-dist/assets/RBToggle.stories-BDQW2WO1.js +12 -0
- package/cli/storybook-dist/assets/RBToggleGroup.stories-CjwH6YbI.js +17 -0
- package/cli/storybook-dist/assets/RBTooltip.stories-Cp1oL68L.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-769P8deX.js +6257 -0
- package/cli/storybook-dist/assets/createConveyorBeltLayer-Ds5AOper.css +21 -0
- package/cli/storybook-dist/assets/entry-preview-CNdFC92L.js +2 -0
- package/cli/storybook-dist/assets/entry-preview-docs-b8bpVBXy.js +46 -0
- package/cli/storybook-dist/assets/iframe-CLUgOMp4.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-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/index-mT46TdHs.js +575 -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-Com4scch.css +1 -0
- package/cli/storybook-dist/assets/preview-CvbIS5ZJ.js +1 -0
- package/cli/storybook-dist/assets/preview-CvycU0Br.js +2 -0
- package/cli/storybook-dist/assets/preview-CzhIqee1.js +3 -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/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 +644 -81
- package/cli/templates/common/assets/icon.svg +44 -2
- package/cli/templates/common/manifest.json +13 -13
- package/cli/templates/common/package.json +11 -4
- 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 +1 -1
- 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 +292 -0
- package/package.json +32 -5
- package/cli/src/pack.mjs +0 -61
- package/cli/src/preview.mjs +0 -91
- package/cli/templates/common/main.py +0 -114
- package/cli/templates/common/src/content.tsx +0 -33
package/cli/src/devBack.mjs
CHANGED
|
@@ -1,79 +1,427 @@
|
|
|
1
1
|
// muscat-pk dev:back — run the plugin back end during development.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// Every backend runs in a warm Docker container that matches the robot
|
|
3
|
+
// (Ubuntu 24.04), so dev behaves like production.
|
|
4
|
+
// python : Python 3.12 container, pip install, run main.py with watchfiles
|
|
5
|
+
// hot-reload. Shares the bridge container's netns (localhost:10000).
|
|
6
|
+
// cpp : Ubuntu + cmake container, build cpp/, run the binary, rebuild +
|
|
7
|
+
// restart on source changes.
|
|
8
|
+
// ros2 : Jazzy + Python 3.12 container, colcon build, run the node, restart
|
|
9
|
+
// on source changes (rclcpp rebuilds on change).
|
|
7
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
8
|
-
import { existsSync,
|
|
11
|
+
import { existsSync, watch } from "node:fs";
|
|
9
12
|
import { resolve } from "node:path";
|
|
10
|
-
import {
|
|
13
|
+
import { ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection } from "./util.mjs";
|
|
14
|
+
import { ensureRemoteZenohEnv } from "./zenohDev.mjs";
|
|
15
|
+
import { assertManifestPorts, manifestRuntime, readManifest, validateManifest } from "./manifest.mjs";
|
|
16
|
+
import {
|
|
17
|
+
formatPortRanges,
|
|
18
|
+
portEnvFlags,
|
|
19
|
+
startPortContractMonitor,
|
|
20
|
+
} from "./ports.mjs";
|
|
21
|
+
import {
|
|
22
|
+
PYTHON_DEV_IMAGE,
|
|
23
|
+
assertDockerReady as pyAssertDockerReady,
|
|
24
|
+
ensureImage as pyEnsureImage,
|
|
25
|
+
} from "./python.mjs";
|
|
11
26
|
import {
|
|
12
27
|
ROS2_DEV_IMAGE,
|
|
13
28
|
assertDockerReady,
|
|
14
29
|
ensureImage,
|
|
15
30
|
ros2PackageName,
|
|
16
31
|
} from "./ros2.mjs";
|
|
32
|
+
import {
|
|
33
|
+
CPP_DEV_IMAGE,
|
|
34
|
+
assertDockerReady as cppAssertDockerReady,
|
|
35
|
+
ensureImage as cppEnsureImage,
|
|
36
|
+
} from "./cpp.mjs";
|
|
37
|
+
|
|
38
|
+
const MANAGED_DEV_ENV = "MUSCAT_PK_MANAGED_DEV";
|
|
17
39
|
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
function exitAfterCleanup(code = 0) {
|
|
41
|
+
if (process.env[MANAGED_DEV_ENV] !== "1") process.exit(code ?? 0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function stopDevBackContainers() {
|
|
45
|
+
for (const container of [
|
|
46
|
+
`${PYTHON_DEV_IMAGE}-run`,
|
|
47
|
+
`${ROS2_DEV_IMAGE}-run`,
|
|
48
|
+
`${CPP_DEV_IMAGE}-run`,
|
|
49
|
+
]) {
|
|
50
|
+
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
24
51
|
}
|
|
25
52
|
}
|
|
26
53
|
|
|
27
|
-
// ──
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
spawnSync(cmd[0], [...cmd.slice(1), "-m", "venv", venv], { cwd: root, stdio: "inherit" });
|
|
42
|
-
} else {
|
|
43
|
-
throw new Error("Python not found (uv / conda / python3 none available). Install Python 3.12 to run the backend.");
|
|
54
|
+
// ── IDE tooling venv (best-effort, non-blocking) ────────────────────────────
|
|
55
|
+
// `muscat-pk new` already creates this up front (see new.mjs); this just
|
|
56
|
+
// refreshes it on every `dev`/`dev:back` in case requirements.txt changed.
|
|
57
|
+
// Docker remains the actual execution environment — this never affects it.
|
|
58
|
+
// Runs in the background so it never delays `dev:back` starting, and any
|
|
59
|
+
// failure here (no local Python toolchain, offline, ...) is silent.
|
|
60
|
+
function syncIdeVenv(root) {
|
|
61
|
+
setImmediate(() => {
|
|
62
|
+
try {
|
|
63
|
+
if (ensureIdeVenv(root)) {
|
|
64
|
+
log.info("IDE venv ready → .venv (point your editor's Python interpreter there for pylint/pyright)");
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
/* best-effort only */
|
|
44
68
|
}
|
|
45
|
-
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
46
71
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
72
|
+
// ── python ────────────────────────────────────────────────────────────────────
|
|
73
|
+
// Runs in a warm Ubuntu 24.04 + Python 3.12 container (matches the robot).
|
|
74
|
+
// Under `muscat-pk dev`, it shares the bridge container's netns, so the SDK
|
|
75
|
+
// reaches the Socket.IO bridge at the default localhost:10000.
|
|
76
|
+
async function runPython(root, portRanges) {
|
|
77
|
+
// Independent of Docker readiness — must still happen if Docker isn't
|
|
78
|
+
// installed/running, since that's exactly when local IDE tooling matters most.
|
|
79
|
+
syncIdeVenv(root);
|
|
80
|
+
pyAssertDockerReady();
|
|
81
|
+
await pyEnsureImage();
|
|
82
|
+
|
|
83
|
+
const manifest = readManifest(root);
|
|
84
|
+
const pluginId = manifest.id ?? "";
|
|
85
|
+
const container = `${PYTHON_DEV_IMAGE}-run`;
|
|
86
|
+
const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
|
|
87
|
+
const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
|
|
88
|
+
const zenohAdvertiseHost = process.env.MUSCAT_ZENOH_ADVERTISE_HOST ?? "";
|
|
89
|
+
const zenohPort = process.env.MUSCAT_ZENOH_PORT ?? "";
|
|
90
|
+
// See host_agent.py: a Docker Desktop container can't join the robot's
|
|
91
|
+
// Zenoh mesh, so it speaks a byte protocol to the host-side proxy instead.
|
|
92
|
+
const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT ?? "";
|
|
93
|
+
// Share the bridge container's network namespace so localhost:10000 is the
|
|
94
|
+
// bridge (set by `muscat-pk dev`). Standalone dev:back has no bridge to join.
|
|
95
|
+
const bridgeContainer = process.env.MUSCAT_PK_BRIDGE_CONTAINER;
|
|
96
|
+
const netFlags = bridgeContainer ? ["--network", `container:${bridgeContainer}`] : [];
|
|
97
|
+
// Only the container's OWN reverse-dial path needs to publish a listen port.
|
|
98
|
+
// With the byte-proxy, the container just connects out to host.docker.internal
|
|
99
|
+
// — nothing to publish.
|
|
100
|
+
const standaloneZenohPortFlags =
|
|
101
|
+
!bridgeContainer && !proxyEndpoint && zenohPort ? ["-p", `${zenohPort}:${zenohPort}`] : [];
|
|
102
|
+
const hostGatewayFlags = bridgeContainer
|
|
103
|
+
? []
|
|
104
|
+
: ["--add-host", "host.docker.internal:host-gateway"];
|
|
105
|
+
// Without the bridge's netns, 127.0.0.1 (the SDK's gateway default) is the
|
|
106
|
+
// container itself, not the host — host.docker.internal reaches the host.
|
|
107
|
+
const gatewayFlags = bridgeContainer
|
|
108
|
+
? []
|
|
109
|
+
: ["-e", "RB_PLUGIN_GATEWAY_URL=http://host.docker.internal:10000"];
|
|
110
|
+
const envFlags = [
|
|
111
|
+
// docker exec below has no -t (no pseudo-TTY), so Python defaults to
|
|
112
|
+
// block-buffered stdout — print() output wouldn't show up until the
|
|
113
|
+
// buffer fills or the process exits. Force unbuffered.
|
|
114
|
+
"-e", "PYTHONUNBUFFERED=1",
|
|
115
|
+
...gatewayFlags,
|
|
116
|
+
...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
|
|
117
|
+
...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
|
|
118
|
+
...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
|
|
119
|
+
// Byte-proxy mode: the container has no Zenoh session of its own — the
|
|
120
|
+
// entry script's shim speaks the raw query/put/subscribe protocol to the
|
|
121
|
+
// host_agent process at this address instead.
|
|
122
|
+
...(proxyEndpoint ? ["-e", `MUSCAT_PROXY_ENDPOINT=${proxyEndpoint}`] : []),
|
|
123
|
+
// Fallback (no proxy): the container reverse-dials the robot itself.
|
|
124
|
+
...(!proxyEndpoint && zenohAdvertiseHost ? ["-e", `MUSCAT_ZENOH_ADVERTISE_HOST=${zenohAdvertiseHost}`] : []),
|
|
125
|
+
...(!proxyEndpoint && zenohPort ? ["-e", `MUSCAT_ZENOH_PORT=${zenohPort}`] : []),
|
|
126
|
+
...portEnvFlags(portRanges),
|
|
127
|
+
];
|
|
128
|
+
const docker = (args, opts = {}) =>
|
|
129
|
+
spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
|
|
130
|
+
|
|
131
|
+
// start a warm, long-lived container with the plugin mounted
|
|
132
|
+
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
133
|
+
log.step("starting Python dev container…");
|
|
134
|
+
const up = docker(
|
|
135
|
+
["run", "-d", "--name", container, ...netFlags, ...standaloneZenohPortFlags,
|
|
136
|
+
// host.docker.internal is automatic on Docker Desktop (macOS/Windows);
|
|
137
|
+
// this makes it resolve on Linux too, for the no-bridge gateway fallback above.
|
|
138
|
+
...hostGatewayFlags,
|
|
139
|
+
"-e", "PIP_ROOT_USER_ACTION=ignore",
|
|
140
|
+
"-v", `${root}:/plugin`, "-w", "/plugin", PYTHON_DEV_IMAGE, "sleep", "infinity"],
|
|
141
|
+
{ stdio: "inherit" },
|
|
142
|
+
);
|
|
143
|
+
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
144
|
+
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log });
|
|
145
|
+
|
|
146
|
+
log.step("pip install requirements + watchfiles (in container)…");
|
|
147
|
+
docker(["exec", container, "bash", "-lc",
|
|
148
|
+
"pip install --break-system-packages --ignore-installed -q watchfiles" +
|
|
149
|
+
" && { [ -f requirements.txt ] && pip install --break-system-packages --ignore-installed -q -r requirements.txt || true; }"]);
|
|
150
|
+
|
|
151
|
+
docker(["exec", container, "bash", "-lc", `cat > /tmp/muscat-pk-python-dev-entry.py <<'PY'
|
|
152
|
+
import os
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
proxy_endpoint = os.getenv("MUSCAT_PROXY_ENDPOINT")
|
|
156
|
+
if proxy_endpoint:
|
|
157
|
+
# Byte-proxy mode: this container has no Zenoh session of its own.
|
|
158
|
+
# ZenohClient's C++ core is replaced with a shim that speaks a raw
|
|
159
|
+
# query/put/subscribe byte protocol to host_agent.py instead.
|
|
160
|
+
import json
|
|
161
|
+
import socket
|
|
162
|
+
import struct
|
|
163
|
+
import threading
|
|
164
|
+
|
|
165
|
+
_proxy_host, _proxy_port_s = proxy_endpoint.rsplit(":", 1)
|
|
166
|
+
_proxy_port = int(_proxy_port_s)
|
|
167
|
+
|
|
168
|
+
def _send_frame(conn, lock, obj):
|
|
169
|
+
body = json.dumps(obj).encode()
|
|
170
|
+
with lock:
|
|
171
|
+
conn.sendall(struct.pack(">I", len(body)) + body)
|
|
172
|
+
|
|
173
|
+
def _recv_frame(conn):
|
|
174
|
+
hdr = b""
|
|
175
|
+
while len(hdr) < 4:
|
|
176
|
+
chunk = conn.recv(4 - len(hdr))
|
|
177
|
+
if not chunk:
|
|
178
|
+
return None
|
|
179
|
+
hdr += chunk
|
|
180
|
+
n = struct.unpack(">I", hdr)[0]
|
|
181
|
+
buf = b""
|
|
182
|
+
while len(buf) < n:
|
|
183
|
+
chunk = conn.recv(n - len(buf))
|
|
184
|
+
if not chunk:
|
|
185
|
+
return None
|
|
186
|
+
buf += chunk
|
|
187
|
+
return json.loads(buf.decode())
|
|
188
|
+
|
|
189
|
+
class _ProxyRawClient:
|
|
190
|
+
"""Drop-in replacement for ZenohClient's C++ core, backed by the
|
|
191
|
+
host_agent byte-proxy instead of a local Zenoh session."""
|
|
192
|
+
|
|
193
|
+
sender = "RRS"
|
|
194
|
+
sender_id = ""
|
|
195
|
+
|
|
196
|
+
def __init__(self, host, port):
|
|
197
|
+
self._conn = socket.create_connection((host, port), timeout=10)
|
|
198
|
+
# create_connection() leaves the connect timeout applied to
|
|
199
|
+
# the socket permanently; the background read loop below does
|
|
200
|
+
# a long-lived blocking recv() for async "sample" pushes, so
|
|
201
|
+
# it must not time out on an idle connection.
|
|
202
|
+
self._conn.settimeout(None)
|
|
203
|
+
self._send_lock = threading.Lock()
|
|
204
|
+
self._req_id = 0
|
|
205
|
+
self._req_lock = threading.Lock()
|
|
206
|
+
self._pending = {}
|
|
207
|
+
self._subs = {} # sub_id -> callback
|
|
208
|
+
self._sub_by_keyexpr = {}
|
|
209
|
+
threading.Thread(target=self._read_loop, daemon=True).start()
|
|
210
|
+
|
|
211
|
+
def _next_id(self):
|
|
212
|
+
with self._req_lock:
|
|
213
|
+
self._req_id += 1
|
|
214
|
+
return self._req_id
|
|
215
|
+
|
|
216
|
+
def _read_loop(self):
|
|
217
|
+
while True:
|
|
218
|
+
frame = _recv_frame(self._conn)
|
|
219
|
+
if frame is None:
|
|
220
|
+
return
|
|
221
|
+
ftype = frame.get("type")
|
|
222
|
+
if ftype == "sample":
|
|
223
|
+
cb = self._sub_by_keyexpr.get(frame["keyexpr"])
|
|
224
|
+
if cb is not None:
|
|
225
|
+
try:
|
|
226
|
+
cb(frame["key"], bytes.fromhex(frame["payload_hex"]), frame.get("att", ""))
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
print(f"[muscat-pk] proxy subscriber callback error: {exc}", flush=True)
|
|
229
|
+
elif "id" in frame:
|
|
230
|
+
ev = self._pending.pop(frame["id"], None)
|
|
231
|
+
if ev is not None:
|
|
232
|
+
ev[1] = frame
|
|
233
|
+
ev[0].set()
|
|
234
|
+
|
|
235
|
+
def _request(self, obj, timeout=30):
|
|
236
|
+
req_id = self._next_id()
|
|
237
|
+
obj["id"] = req_id
|
|
238
|
+
ev = threading.Event()
|
|
239
|
+
slot = [ev, None]
|
|
240
|
+
self._pending[req_id] = slot
|
|
241
|
+
_send_frame(self._conn, self._send_lock, obj)
|
|
242
|
+
if not ev.wait(timeout):
|
|
243
|
+
self._pending.pop(req_id, None)
|
|
244
|
+
raise TimeoutError(f"proxy request timed out: {obj.get('type')} {obj.get('keyexpr')}")
|
|
245
|
+
return slot[1]
|
|
246
|
+
|
|
247
|
+
def query(self, keyexpr, payload, timeout_ms, target):
|
|
248
|
+
resp = self._request({
|
|
249
|
+
"type": "query", "keyexpr": keyexpr,
|
|
250
|
+
"payload_hex": payload.hex() if payload else None,
|
|
251
|
+
"timeout_ms": timeout_ms, "target": target,
|
|
252
|
+
}, timeout=(timeout_ms / 1000) + 5)
|
|
253
|
+
if not resp.get("ok"):
|
|
254
|
+
raise RuntimeError(resp.get("error", "proxy query failed"))
|
|
255
|
+
return [
|
|
256
|
+
(r["key"], bytes.fromhex(r["payload_hex"]), r["att"], r["encoding"], r["is_error"])
|
|
257
|
+
for r in resp["replies"]
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
def query_all(self, keyexpr, payload, timeout_ms, target):
|
|
261
|
+
return self.query(keyexpr, payload, timeout_ms, target)
|
|
262
|
+
|
|
263
|
+
def publish(self, topic, payload, att, congestion_control, priority, express):
|
|
264
|
+
resp = self._request({
|
|
265
|
+
"type": "put", "keyexpr": topic,
|
|
266
|
+
"payload_hex": bytes(payload).hex() if payload else None,
|
|
267
|
+
})
|
|
268
|
+
if not resp.get("ok"):
|
|
269
|
+
raise RuntimeError(resp.get("error", "proxy put failed"))
|
|
270
|
+
|
|
271
|
+
def has_subscriber(self, topic):
|
|
272
|
+
return topic in self._sub_by_keyexpr
|
|
273
|
+
|
|
274
|
+
def declare_subscriber(self, topic, callback, opts=None):
|
|
275
|
+
resp = self._request({"type": "subscribe", "keyexpr": topic})
|
|
276
|
+
if not resp.get("ok"):
|
|
277
|
+
raise RuntimeError(resp.get("error", "proxy subscribe failed"))
|
|
278
|
+
sub_id = self._next_id()
|
|
279
|
+
self._subs[sub_id] = topic
|
|
280
|
+
self._sub_by_keyexpr[topic] = callback
|
|
281
|
+
return sub_id
|
|
282
|
+
|
|
283
|
+
def undeclare_subscriber(self, sub_id):
|
|
284
|
+
topic = self._subs.pop(sub_id, None)
|
|
285
|
+
if topic is None:
|
|
286
|
+
return
|
|
287
|
+
self._sub_by_keyexpr.pop(topic, None)
|
|
288
|
+
self._request({"type": "unsubscribe", "keyexpr": topic})
|
|
289
|
+
|
|
290
|
+
def unsubscribe_topic(self, topic):
|
|
291
|
+
for sub_id, t in list(self._subs.items()):
|
|
292
|
+
if t == topic:
|
|
293
|
+
self.undeclare_subscriber(sub_id)
|
|
294
|
+
|
|
295
|
+
def unsubscribe_all(self):
|
|
296
|
+
for sub_id in list(self._subs):
|
|
297
|
+
self.undeclare_subscriber(sub_id)
|
|
298
|
+
|
|
299
|
+
def get_sub_stats(self, sub_id):
|
|
300
|
+
return {}
|
|
301
|
+
|
|
302
|
+
def declare_queryable(self, keyexpr, handler):
|
|
303
|
+
raise NotImplementedError("byte-proxy mode does not support declare_queryable")
|
|
304
|
+
|
|
305
|
+
def undeclare_queryable_by_topic(self, keyexpr):
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
# ZenohClient is a process-wide singleton whose __init__ eagerly
|
|
309
|
+
# calls connect() on first construction. rainbow.rb_sdk.base
|
|
310
|
+
# transitively imports rainbow.rb_log.log, which constructs one at
|
|
311
|
+
# module level — so this patch must land before RBBaseSDK is
|
|
312
|
+
# imported, or that eager construction wins with a real session.
|
|
313
|
+
from rainbow.rb_zenoh.client import ZenohClient
|
|
314
|
+
|
|
315
|
+
def _proxy_connect(self, *, config_pairs=None, sender=None, max_attempts=8, initial_delay_s=0.5, max_delay_s=3.0):
|
|
316
|
+
if self._client is not None:
|
|
317
|
+
return
|
|
318
|
+
if sender is not None:
|
|
319
|
+
self._sender = sender
|
|
320
|
+
print(f"[muscat-pk] zenoh via host byte-proxy → {proxy_endpoint} (no local Zenoh session)", flush=True)
|
|
321
|
+
self._client = _ProxyRawClient(_proxy_host, _proxy_port)
|
|
322
|
+
|
|
323
|
+
ZenohClient.connect = _proxy_connect
|
|
324
|
+
|
|
325
|
+
from rainbow.rb_sdk.base import RBBaseSDK
|
|
52
326
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
327
|
+
async def _sdk_connect_via_proxy(self, *args, **kwargs):
|
|
328
|
+
# No HTTP registration with common — host_agent already handles
|
|
329
|
+
# that. Just make sure the (proxy-backed) shared ZenohClient exists.
|
|
330
|
+
self._ensure_zenoh_client_alive()
|
|
331
|
+
return None
|
|
58
332
|
|
|
59
|
-
|
|
333
|
+
async def _sdk_disconnect_via_proxy(self, *args, **kwargs):
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
RBBaseSDK.connect = _sdk_connect_via_proxy
|
|
337
|
+
RBBaseSDK.disconnect = _sdk_disconnect_via_proxy
|
|
338
|
+
else:
|
|
339
|
+
# Fallback: no host byte-proxy — the container reverse-dials the robot itself.
|
|
340
|
+
advertise_host = os.getenv("MUSCAT_ZENOH_ADVERTISE_HOST")
|
|
341
|
+
zenoh_port = os.getenv("MUSCAT_ZENOH_PORT")
|
|
342
|
+
from rainbow.rb_sdk.base import RBBaseSDK
|
|
343
|
+
if advertise_host:
|
|
344
|
+
RBBaseSDK._detect_local_ip = staticmethod(lambda host: advertise_host)
|
|
345
|
+
if zenoh_port:
|
|
346
|
+
_orig_connect = RBBaseSDK.connect
|
|
347
|
+
|
|
348
|
+
async def _connect_with_dev_port(self, *args, **kwargs):
|
|
349
|
+
kwargs.setdefault("zenoh_port", int(zenoh_port))
|
|
350
|
+
return await _orig_connect(self, *args, **kwargs)
|
|
351
|
+
|
|
352
|
+
RBBaseSDK.connect = _connect_with_dev_port
|
|
353
|
+
except Exception as exc:
|
|
354
|
+
print(f"[muscat-pk] SDK dev network patch skipped: {exc}", flush=True)
|
|
355
|
+
|
|
356
|
+
import runpy
|
|
357
|
+
runpy.run_path("/plugin/py/main.py", run_name="__main__")
|
|
358
|
+
PY`]);
|
|
359
|
+
|
|
360
|
+
if (pluginId) log.step(`RB_PLUGIN_ID=${pluginId}`);
|
|
361
|
+
if (proxyEndpoint) log.step(`Zenoh via host byte-proxy → ${proxyEndpoint}`);
|
|
362
|
+
else if (zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
|
|
363
|
+
log.step("watchfiles → py/main.py (reload on any .py change in py/)");
|
|
60
364
|
const child = spawn(
|
|
61
|
-
|
|
62
|
-
["
|
|
63
|
-
|
|
365
|
+
"docker",
|
|
366
|
+
["exec", ...envFlags, container, "bash", "-lc",
|
|
367
|
+
"python3 -m watchfiles --filter python --target-type command 'python3 /tmp/muscat-pk-python-dev-entry.py' py"],
|
|
368
|
+
{ cwd: root, stdio: ["inherit", "inherit", "pipe"] },
|
|
64
369
|
);
|
|
65
|
-
|
|
370
|
+
// watchfiles prints a KeyboardInterrupt traceback on SIGTERM — suppress it
|
|
371
|
+
child.stderr.on("data", (buf) => {
|
|
372
|
+
const text = buf.toString();
|
|
373
|
+
if (!text.includes("KeyboardInterrupt") && !text.includes("raise_keyboard_interrupt")) {
|
|
374
|
+
process.stderr.write(buf);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
let cleaned = false;
|
|
379
|
+
const cleanupResources = () => {
|
|
380
|
+
if (cleaned) return;
|
|
381
|
+
cleaned = true;
|
|
382
|
+
stopPortMonitor();
|
|
383
|
+
if (!child.killed && child.exitCode === null) child.kill();
|
|
384
|
+
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
385
|
+
};
|
|
386
|
+
const cleanup = () => {
|
|
387
|
+
cleanupResources();
|
|
388
|
+
exitAfterCleanup(0);
|
|
389
|
+
};
|
|
390
|
+
child.on("exit", (code) => {
|
|
391
|
+
cleanupResources();
|
|
392
|
+
exitAfterCleanup(code ?? 0);
|
|
393
|
+
});
|
|
394
|
+
process.once("SIGINT", cleanup);
|
|
395
|
+
process.once("SIGTERM", cleanup);
|
|
66
396
|
}
|
|
67
397
|
|
|
68
398
|
// ── ros2 ───────────────────────────────────────────────────────────────────────
|
|
69
|
-
function runRos2(root) {
|
|
399
|
+
async function runRos2(root, portRanges) {
|
|
400
|
+
const manifest = readManifest(root);
|
|
401
|
+
const isRclcpp = manifest.runtime?.rosClient === "rclcpp";
|
|
402
|
+
// Independent of Docker readiness — must still happen if Docker isn't
|
|
403
|
+
// installed/running, since that's exactly when local IDE tooling matters most.
|
|
404
|
+
if (!isRclcpp) syncIdeVenv(root); // rclpy is plain Python — rclcpp is C++, no venv needed
|
|
405
|
+
|
|
70
406
|
assertDockerReady();
|
|
71
407
|
const pkg = ros2PackageName(root);
|
|
72
408
|
if (!pkg) throw new Error("no ROS2 package found under ros2_ws/src.");
|
|
73
409
|
|
|
74
|
-
ensureImage();
|
|
410
|
+
await ensureImage();
|
|
75
411
|
|
|
412
|
+
const pluginId = manifest.id ?? "";
|
|
76
413
|
const container = `${ROS2_DEV_IMAGE}-run`;
|
|
414
|
+
const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
|
|
415
|
+
const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
|
|
416
|
+
const envFlags = [
|
|
417
|
+
// Same rationale as runPython(): no -t on docker exec below means
|
|
418
|
+
// rclpy's Python process stdout is block-buffered, not line-buffered.
|
|
419
|
+
"-e", "PYTHONUNBUFFERED=1",
|
|
420
|
+
...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
|
|
421
|
+
...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
|
|
422
|
+
...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
|
|
423
|
+
...portEnvFlags(portRanges),
|
|
424
|
+
];
|
|
77
425
|
const docker = (args, opts = {}) =>
|
|
78
426
|
spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
|
|
79
427
|
|
|
@@ -81,10 +429,11 @@ function runRos2(root) {
|
|
|
81
429
|
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
82
430
|
log.step("starting ROS2 dev container…");
|
|
83
431
|
const up = docker(
|
|
84
|
-
["run", "-d", "--name", container, "-v", `${root}:/plugin`, "-w", "/plugin", ROS2_DEV_IMAGE, "sleep", "infinity"],
|
|
432
|
+
["run", "-d", "--name", container, "-e", "PIP_ROOT_USER_ACTION=ignore", "-v", `${root}:/plugin`, "-w", "/plugin", ROS2_DEV_IMAGE, "sleep", "infinity"],
|
|
85
433
|
{ stdio: "inherit" },
|
|
86
434
|
);
|
|
87
435
|
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
436
|
+
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log });
|
|
88
437
|
|
|
89
438
|
// one-time deps + symlink build (edits to .py reflect without rebuilding)
|
|
90
439
|
log.step("pip install + colcon build --symlink-install (once)…");
|
|
@@ -97,21 +446,22 @@ function runRos2(root) {
|
|
|
97
446
|
log.step(`ros2 run ${pkg} ${pkg}_node`);
|
|
98
447
|
child = spawn(
|
|
99
448
|
"docker",
|
|
100
|
-
["exec", container, "bash", "-lc",
|
|
449
|
+
["exec", ...envFlags, container, "bash", "-lc",
|
|
101
450
|
`cd ros2_ws && source install/setup.bash && ros2 run ${pkg} ${pkg}_node`],
|
|
102
451
|
{ cwd: root, stdio: "inherit" },
|
|
103
452
|
);
|
|
104
453
|
};
|
|
105
454
|
startNode();
|
|
106
455
|
|
|
107
|
-
// restart node on source change
|
|
456
|
+
// restart node on source change. rclpy reflects .py edits live (symlink-install);
|
|
457
|
+
// rclcpp (C++) and package-metadata changes require a colcon rebuild.
|
|
108
458
|
let timer = null;
|
|
109
459
|
const onChange = (_event, filename) => {
|
|
110
460
|
clearTimeout(timer);
|
|
111
461
|
timer = setTimeout(() => {
|
|
112
462
|
const structural = filename && /(setup\.py|package\.xml|CMakeLists\.txt)$/.test(String(filename));
|
|
113
|
-
if (structural) {
|
|
114
|
-
log.info("package metadata changed — colcon rebuild…");
|
|
463
|
+
if (structural || isRclcpp) {
|
|
464
|
+
log.info(structural ? "package metadata changed — colcon rebuild…" : "source changed — colcon rebuild…");
|
|
115
465
|
docker(["exec", container, "bash", "-lc", "cd ros2_ws && colcon build --symlink-install"]);
|
|
116
466
|
} else {
|
|
117
467
|
log.info("source changed — restarting node…");
|
|
@@ -125,17 +475,115 @@ function runRos2(root) {
|
|
|
125
475
|
}
|
|
126
476
|
|
|
127
477
|
const cleanup = () => {
|
|
478
|
+
stopPortMonitor();
|
|
479
|
+
if (child) child.kill();
|
|
480
|
+
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
481
|
+
exitAfterCleanup(0);
|
|
482
|
+
};
|
|
483
|
+
process.on("SIGINT", cleanup);
|
|
484
|
+
process.on("SIGTERM", cleanup);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ── cpp ──────────────────────────────────────────────────────────────────────
|
|
488
|
+
async function runCpp(root, portRanges) {
|
|
489
|
+
cppAssertDockerReady();
|
|
490
|
+
const manifest = readManifest(root);
|
|
491
|
+
const entry = manifest.runtime?.entry;
|
|
492
|
+
if (!entry) throw new Error("runtime.entry (the built executable name) is required for cpp.");
|
|
493
|
+
|
|
494
|
+
await cppEnsureImage();
|
|
495
|
+
|
|
496
|
+
const pluginId = manifest.id ?? "";
|
|
497
|
+
const container = `${CPP_DEV_IMAGE}-run`;
|
|
498
|
+
const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
|
|
499
|
+
const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
|
|
500
|
+
const envFlags = [
|
|
501
|
+
...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
|
|
502
|
+
...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
|
|
503
|
+
...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
|
|
504
|
+
...portEnvFlags(portRanges),
|
|
505
|
+
];
|
|
506
|
+
const docker = (args, opts = {}) =>
|
|
507
|
+
spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
|
|
508
|
+
|
|
509
|
+
// start a warm, long-lived container with the plugin mounted
|
|
510
|
+
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
511
|
+
log.step("starting C++ dev container…");
|
|
512
|
+
const up = docker(
|
|
513
|
+
["run", "-d", "--name", container, "-v", `${root}:/plugin`, "-w", "/plugin", CPP_DEV_IMAGE, "sleep", "infinity"],
|
|
514
|
+
{ stdio: "inherit" },
|
|
515
|
+
);
|
|
516
|
+
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
517
|
+
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log });
|
|
518
|
+
|
|
519
|
+
const build = () => {
|
|
520
|
+
log.step("cmake build…");
|
|
521
|
+
return docker(["exec", container, "bash", "-lc",
|
|
522
|
+
"cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=Debug && cmake --build cpp/build -j"]);
|
|
523
|
+
};
|
|
524
|
+
build();
|
|
525
|
+
|
|
526
|
+
let child = null;
|
|
527
|
+
const startBin = () => {
|
|
528
|
+
if (child) child.kill();
|
|
529
|
+
log.step(`run cpp/build/${entry}`);
|
|
530
|
+
child = spawn(
|
|
531
|
+
"docker",
|
|
532
|
+
["exec", ...envFlags, container, "bash", "-lc", `./cpp/build/${entry}`],
|
|
533
|
+
{ cwd: root, stdio: "inherit" },
|
|
534
|
+
);
|
|
535
|
+
};
|
|
536
|
+
startBin();
|
|
537
|
+
|
|
538
|
+
// rebuild + restart on C++ source changes; ignore the build/ output dir.
|
|
539
|
+
let timer = null;
|
|
540
|
+
const onChange = (_event, filename) => {
|
|
541
|
+
if (filename && String(filename).startsWith("build")) return;
|
|
542
|
+
clearTimeout(timer);
|
|
543
|
+
timer = setTimeout(() => {
|
|
544
|
+
log.info("source changed — rebuilding…");
|
|
545
|
+
const r = build();
|
|
546
|
+
if (r.status === 0) startBin();
|
|
547
|
+
else log.warn("build failed — keeping previous binary");
|
|
548
|
+
}, 300);
|
|
549
|
+
};
|
|
550
|
+
const cppDir = resolve(root, "cpp");
|
|
551
|
+
if (existsSync(cppDir)) watch(cppDir, { recursive: true }, onChange);
|
|
552
|
+
|
|
553
|
+
const cleanup = () => {
|
|
554
|
+
stopPortMonitor();
|
|
128
555
|
if (child) child.kill();
|
|
129
556
|
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
130
|
-
|
|
557
|
+
exitAfterCleanup(0);
|
|
131
558
|
};
|
|
132
559
|
process.on("SIGINT", cleanup);
|
|
133
560
|
process.on("SIGTERM", cleanup);
|
|
134
561
|
}
|
|
135
562
|
|
|
136
|
-
|
|
563
|
+
function warnManifest(root) {
|
|
564
|
+
const manifest = readManifest(root);
|
|
565
|
+
if (!manifest || Object.keys(manifest).length === 0) return;
|
|
566
|
+
const errors = validateManifest(manifest);
|
|
567
|
+
for (const e of errors) log.warn(`manifest.json: ${e}`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export async function runDevBack(args = []) {
|
|
137
571
|
const root = findPluginRoot();
|
|
572
|
+
warnManifest(root);
|
|
573
|
+
const portRanges = assertManifestPorts(root);
|
|
574
|
+
if (portRanges.length > 0 && process.env.MUSCAT_PK_PORTS_PRECHECKED !== "1")
|
|
575
|
+
log.step(`plugin backend ports declared: ${formatPortRanges(portRanges)}`);
|
|
576
|
+
ensureDocker();
|
|
577
|
+
const { robotIp, robotToken } = await promptRobotConnection(args);
|
|
578
|
+
if (robotIp) process.env.MUSCAT_ROBOT_HOST = robotIp;
|
|
579
|
+
if (robotToken) process.env.MUSCAT_ROBOT_TOKEN = robotToken;
|
|
580
|
+
// Standalone `dev:back` against a remote robot needs the Zenoh reverse-path
|
|
581
|
+
// advertise host/port too (idempotent — `muscat-pk dev` already set these).
|
|
582
|
+
// Byte-proxy mode (a host-side host_agent set MUSCAT_PROXY_ENDPOINT) makes
|
|
583
|
+
// the container speak the proxy protocol instead, so it needs no reverse-dial.
|
|
584
|
+
if (!process.env.MUSCAT_PROXY_ENDPOINT) await ensureRemoteZenohEnv(robotIp, { log });
|
|
138
585
|
const type = manifestRuntime(root);
|
|
139
|
-
if (type === "ros2") return runRos2(root);
|
|
140
|
-
return
|
|
586
|
+
if (type === "ros2") return await runRos2(root, portRanges);
|
|
587
|
+
if (type === "cpp") return await runCpp(root, portRanges);
|
|
588
|
+
return await runPython(root, portRanges);
|
|
141
589
|
}
|