@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.8 → 0.10.1-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +1186 -0
- package/README.md +1137 -57
- package/cli/README.md +239 -7
- package/cli/bin/create-muscat-plugin.mjs +1 -1
- package/cli/bin/muscat-pk.mjs +123 -27
- package/cli/bundle-templates.mjs +31 -0
- package/cli/docker/Dockerfile.cpp +28 -0
- package/cli/docker/Dockerfile.dev +17 -0
- package/cli/docker/Dockerfile.python +21 -0
- package/cli/prepack.mjs +10 -0
- package/cli/src/__pycache__/host_agent.cpython-37.pyc +0 -0
- package/cli/src/ai-rules.mjs +107 -0
- package/cli/src/build.mjs +333 -93
- package/cli/src/check-package.mjs +8 -23
- package/cli/src/completion.mjs +168 -0
- package/cli/src/cpp.mjs +78 -0
- package/cli/src/cppIde.mjs +76 -0
- package/cli/src/dev.mjs +195 -6
- package/cli/src/devBack.mjs +671 -60
- package/cli/src/devId.mjs +54 -0
- package/cli/src/devSession.mjs +67 -0
- package/cli/src/devUi.mjs +214 -25
- package/cli/src/esbuild.mjs +27 -10
- package/cli/src/host_agent.py +223 -0
- package/cli/src/manifest.mjs +288 -0
- package/cli/src/mcp.mjs +1153 -0
- package/cli/src/mcpProxy.mjs +110 -0
- package/cli/src/muscatLocal.mjs +292 -0
- package/cli/src/new.mjs +205 -68
- package/cli/src/pluginDevServer.mjs +111 -0
- package/cli/src/ports.mjs +193 -0
- package/cli/src/projectCheck.mjs +102 -0
- package/cli/src/python.mjs +112 -0
- package/cli/src/ros2.mjs +62 -18
- package/cli/src/sdk.mjs +74 -24
- package/cli/src/sim.mjs +177 -0
- package/cli/src/storybook.mjs +150 -0
- package/cli/src/templates-embedded.mjs +4 -0
- package/cli/src/type.mjs +93 -34
- package/cli/src/uninstall.mjs +81 -0
- package/cli/src/update.mjs +364 -35
- package/cli/src/util.mjs +341 -11
- package/cli/src/zenohDev.mjs +83 -0
- package/cli/src/zip.mjs +121 -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 +56 -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/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 +216 -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-pCKrjUal.js +1 -0
- package/cli/storybook-dist/assets/Docs-B-Y7I1GA.js +7 -0
- package/cli/storybook-dist/assets/Docs-BFJcE_VS.js +8 -0
- package/cli/storybook-dist/assets/Docs-B_QaTtz8.js +8 -0
- package/cli/storybook-dist/assets/Docs-CryDGmfh.js +20 -0
- package/cli/storybook-dist/assets/Docs-Cw2AVNKx.js +7 -0
- package/cli/storybook-dist/assets/Docs-Q6r1fBeL.js +12 -0
- package/cli/storybook-dist/assets/DocsRenderer-CFRXHY34-_OmZBCn8.js +2 -0
- package/cli/storybook-dist/assets/NxAvatar.stories-Ctf5G2FQ.js +13 -0
- package/cli/storybook-dist/assets/NxButton.stories-DeLObVSj.js +20 -0
- package/cli/storybook-dist/assets/NxIcons.stories-DeqrVzVs.js +3 -0
- package/cli/storybook-dist/assets/NxNumberInput.stories-LqoY0ed1.js +26 -0
- package/cli/storybook-dist/assets/NxPagination.stories-DI6bXuJM.js +11 -0
- package/cli/storybook-dist/assets/NxPointerHoldButton.stories-3HSV1XmN.js +14 -0
- package/cli/storybook-dist/assets/NxRangeInput.stories-1pjgQVaG.js +17 -0
- package/cli/storybook-dist/assets/NxSelectBox.stories-bcQPZEuF.js +17 -0
- package/cli/storybook-dist/assets/NxSlideRange.stories-BosX4axk.js +14 -0
- package/cli/storybook-dist/assets/NxSwitch.stories-DxV6dS33.js +11 -0
- package/cli/storybook-dist/assets/NxTextArea.stories-CO545-LP.js +11 -0
- package/cli/storybook-dist/assets/NxTextInput.stories-BP0npai1.js +11 -0
- package/cli/storybook-dist/assets/PretendardVariable-CJuje-Rk.woff2 +0 -0
- package/cli/storybook-dist/assets/RBAccordion.stories-fVR4Va2a.js +17 -0
- package/cli/storybook-dist/assets/RBAccordionGroup.stories-Du5bsckD.js +33 -0
- package/cli/storybook-dist/assets/RBAlert.stories-DNkMXUu6.js +17 -0
- package/cli/storybook-dist/assets/RBBadge.stories-C44pXt8D.js +1 -0
- package/cli/storybook-dist/assets/RBBattery.stories-DE2JHvfL.js +10 -0
- package/cli/storybook-dist/assets/RBCard.stories-CuIda7NI.js +45 -0
- package/cli/storybook-dist/assets/RBCheckBox.stories-D1qM1ayr.js +1 -0
- package/cli/storybook-dist/assets/RBCheckIconButton.stories-e4gkliBO.js +1 -0
- package/cli/storybook-dist/assets/RBCheckLabelButton.stories-CDzCtsMI.js +24 -0
- package/cli/storybook-dist/assets/RBChipTab.stories-DBrc_3Mp.js +21 -0
- package/cli/storybook-dist/assets/RBChipsBoxTab.stories-hlrsHjKY.js +1 -0
- package/cli/storybook-dist/assets/RBColorIcons.stories-BHUvpkdu.js +3 -0
- package/cli/storybook-dist/assets/RBCommand.stories-azAF41NP.js +73 -0
- package/cli/storybook-dist/assets/RBConfirm.stories-BMNjwbmT.js +20 -0
- package/cli/storybook-dist/assets/RBDateInput.stories-D0VezDzv.js +15 -0
- package/cli/storybook-dist/assets/RBDialog.stories-AOJ975Fy.js +38 -0
- package/cli/storybook-dist/assets/RBDivider.stories-NSvnj43Z.js +1 -0
- package/cli/storybook-dist/assets/RBDrawer.stories-Du0oTvD1.js +14 -0
- package/cli/storybook-dist/assets/RBDropdownMenu.stories-Hsxve4PW.js +86 -0
- package/cli/storybook-dist/assets/RBEmptyView.stories-BlwHx3zp.js +12 -0
- package/cli/storybook-dist/assets/RBIconButton.stories-DAPDY63x.js +1 -0
- package/cli/storybook-dist/assets/RBIcons.stories-W5ZUV2L1.js +3 -0
- package/cli/storybook-dist/assets/RBInlineLabelButton.stories-C99j0Y3g.js +1 -0
- package/cli/storybook-dist/assets/RBJoyStick.stories-Du0H1iAS.js +11 -0
- package/cli/storybook-dist/assets/RBLabelButton.stories-BlvHaU_Z.js +1 -0
- package/cli/storybook-dist/assets/RBLight.stories-BY6DKrre.js +1 -0
- package/cli/storybook-dist/assets/RBMenubar.stories-D2PEGpM9.js +3 -0
- package/cli/storybook-dist/assets/RBMultiRangeInput.stories-Bm2qSmjq.js +14 -0
- package/cli/storybook-dist/assets/RBPagination.stories-DDleJa5N.js +17 -0
- package/cli/storybook-dist/assets/RBPopover.stories-27YxPSFG.js +11 -0
- package/cli/storybook-dist/assets/RBPrompt.stories-BFaIOctr.js +20 -0
- package/cli/storybook-dist/assets/RBRadioBox.stories-BroDar0t.js +1 -0
- package/cli/storybook-dist/assets/RBRadioButton.stories-M3O-wX7V.js +11 -0
- package/cli/storybook-dist/assets/RBRangeDateInput.stories-CWlM_D5X.js +15 -0
- package/cli/storybook-dist/assets/RBResizable.stories-ui8X1qnc.js +20 -0
- package/cli/storybook-dist/assets/RBSelectBox.stories-C8T6cfA0.js +74 -0
- package/cli/storybook-dist/assets/RBSheet.stories-BCOo_Zcc.js +39 -0
- package/cli/storybook-dist/assets/RBSideNumberInput.stories-CU2_MVlJ.js +5 -0
- package/cli/storybook-dist/assets/RBSlider.stories-DgOp1TFc.js +5 -0
- package/cli/storybook-dist/assets/RBSpinner.stories-qLScY3zZ.js +14 -0
- package/cli/storybook-dist/assets/RBSwitch.stories-SXSY1QMb.js +65 -0
- package/cli/storybook-dist/assets/RBTab.stories-BzXXTsXP.js +1 -0
- package/cli/storybook-dist/assets/RBTable.stories-DHTeRNVk.js +62 -0
- package/cli/storybook-dist/assets/RBTableRow.stories-DRNRsd15.js +11 -0
- package/cli/storybook-dist/assets/RBTabs.stories-DP0noBLp.js +21 -0
- package/cli/storybook-dist/assets/RBTextArea.stories-TkaejfxJ.js +5 -0
- package/cli/storybook-dist/assets/RBTextField.stories-B6VpCGyt.js +45 -0
- package/cli/storybook-dist/assets/RBTextInput.stories-D3hZ08e-.js +5 -0
- package/cli/storybook-dist/assets/RBToast.stories-BQ2wS2U0.js +11 -0
- package/cli/storybook-dist/assets/RBToggle.stories-GVOjGqo3.js +12 -0
- package/cli/storybook-dist/assets/RBToggleGroup.stories-pzx0btOD.js +17 -0
- package/cli/storybook-dist/assets/RBTooltip.stories-jufB6fa1.js +9 -0
- package/cli/storybook-dist/assets/SideNumberInput-BYlmNHEw.js +1109 -0
- package/cli/storybook-dist/assets/SideNumberInput-SAkhydsn.css +21 -0
- package/cli/storybook-dist/assets/browserUtils-CQM9Za7e.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/entry-preview-CAkU-e0D.js +2 -0
- package/cli/storybook-dist/assets/entry-preview-docs-b8bpVBXy.js +46 -0
- package/cli/storybook-dist/assets/iframe-B-BmTsxV.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-CT3U1fyk.js +1 -0
- package/cli/storybook-dist/assets/index-ChsGqxH_.js +9 -0
- package/cli/storybook-dist/assets/index-CkB0GfaI.js +575 -0
- package/cli/storybook-dist/assets/index-ClAc6FBW.js +240 -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/preview-B8lJiyuQ.js +34 -0
- package/cli/storybook-dist/assets/preview-BBWR9nbA.js +1 -0
- package/cli/storybook-dist/assets/preview-BVJwlGUF.js +1 -0
- package/cli/storybook-dist/assets/preview-BWzBA1C2.js +396 -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-CznQwwbo.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/preview-M77JRg1A.js +2 -0
- package/cli/storybook-dist/assets/preview-kTZPgP6D.css +1 -0
- package/cli/storybook-dist/assets/react-18-hB3smqWJ.js +1 -0
- package/cli/storybook-dist/assets/react-resizable-panels-HsFbxPbn.css +1 -0
- package/cli/storybook-dist/assets/react-resizable-panels.browser-DuxpZLkF.js +120 -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/README.md +1070 -83
- package/cli/templates/common/assets/icon.svg +44 -2
- package/cli/templates/common/manifest.json +14 -13
- package/cli/templates/common/package.json +11 -4
- package/cli/templates/common/py/main.py +125 -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 +73 -0
- package/cli/templates/readme-cpp/README.md +1012 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +12 -1
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +22 -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 +41 -0
- package/cli/vite.d.mts +25 -0
- package/cli/vite.mjs +292 -0
- package/package.json +34 -7
- 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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// The plugin id a dev session runs under.
|
|
2
|
+
//
|
|
3
|
+
// Machines developing the same plugin against one robot register the same topics on its gateway, and
|
|
4
|
+
// requests (rb.action, external routes) then reach whichever backend registered last — with no error,
|
|
5
|
+
// so one developer's button runs the other's code. Against a remote robot each machine therefore runs
|
|
6
|
+
// as <manifest id>.dev-<user>-<random>. The random part is kept in ~/.muscat-pk/dev-id: the id stays the
|
|
7
|
+
// same across runs (stable external URLs) and differs between machines even when host or user names match.
|
|
8
|
+
// --keep-id runs under the manifest id (program execute, external clients that use the real id); then only
|
|
9
|
+
// one machine may be connected to that robot.
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { homedir, userInfo } from "node:os";
|
|
13
|
+
import { dirname, resolve } from "node:path";
|
|
14
|
+
import { PLUGIN_ID_RE } from "./manifest.mjs";
|
|
15
|
+
import { log } from "./util.mjs";
|
|
16
|
+
import { isRemoteRobot } from "./zenohDev.mjs";
|
|
17
|
+
|
|
18
|
+
const DEV_ID_FILE = resolve(homedir(), ".muscat-pk", "dev-id");
|
|
19
|
+
const ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
20
|
+
// `dev` decides once; its dev:back child gets no CLI args and reads the id from here.
|
|
21
|
+
const ENV = "MUSCAT_PK_DEV_PLUGIN_ID";
|
|
22
|
+
|
|
23
|
+
function machineToken() {
|
|
24
|
+
const saved = existsSync(DEV_ID_FILE) ? readFileSync(DEV_ID_FILE, "utf8").trim() : "";
|
|
25
|
+
if (/^[a-z0-9]{6}$/.test(saved)) return saved;
|
|
26
|
+
const token = [...randomBytes(6)].map((b) => ALPHABET[b % ALPHABET.length]).join("");
|
|
27
|
+
mkdirSync(dirname(DEV_ID_FILE), { recursive: true });
|
|
28
|
+
writeFileSync(DEV_ID_FILE, `${token}\n`);
|
|
29
|
+
return token;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function userName() {
|
|
33
|
+
try {
|
|
34
|
+
return userInfo().username.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 12);
|
|
35
|
+
} catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The id for this dev session (UI, backend, ROS namespace and external routes all use it). */
|
|
41
|
+
export function devPluginId(manifestId, { robotIp, args = [] }) {
|
|
42
|
+
if (process.env[ENV]) return process.env[ENV];
|
|
43
|
+
let id = manifestId;
|
|
44
|
+
if (manifestId && isRemoteRobot(robotIp) && !args.includes("--keep-id")) {
|
|
45
|
+
const candidate = `${manifestId}.${["dev", userName(), machineToken()].filter(Boolean).join("-")}`;
|
|
46
|
+
if (PLUGIN_ID_RE.test(candidate)) id = candidate;
|
|
47
|
+
}
|
|
48
|
+
if (id !== manifestId) {
|
|
49
|
+
log.step(`dev plugin id ${id} — this machine's own on the shared robot (--keep-id runs as ${manifestId})`);
|
|
50
|
+
log.step(`external routes: http://${robotIp}:3000/common/plugin/${id}/<path>`);
|
|
51
|
+
}
|
|
52
|
+
if (id) process.env[ENV] = id;
|
|
53
|
+
return id;
|
|
54
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// One Muscat plugin dev environment per PC: the backend container, the local Muscat runtime (and
|
|
2
|
+
// the plugin ports it publishes) and the host_agent relay port are shared. A new dev / dev:ui /
|
|
3
|
+
// dev:back offers to stop the running one and take over.
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, resolve } from "node:path";
|
|
8
|
+
import { log, prompt } from "./util.mjs";
|
|
9
|
+
|
|
10
|
+
export const DEV_CONTAINER = "muscat-pk-dev-run";
|
|
11
|
+
// Per-runtime names from older kits.
|
|
12
|
+
const OLD_CONTAINERS = ["muscat-pk-python-dev-run", "muscat-pk-ros2-dev-run", "muscat-pk-cpp-dev-run"];
|
|
13
|
+
const LOCK = resolve(homedir(), ".muscat-pk", "dev-session.json");
|
|
14
|
+
|
|
15
|
+
export function stopDevBackContainers() {
|
|
16
|
+
for (const c of [DEV_CONTAINER, ...OLD_CONTAINERS]) spawnSync("docker", ["rm", "-f", c], { stdio: "ignore" });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const alive = (pid) => {
|
|
20
|
+
try {
|
|
21
|
+
process.kill(pid, 0);
|
|
22
|
+
return true;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return e.code === "EPERM";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function runningSession() {
|
|
29
|
+
try {
|
|
30
|
+
const s = JSON.parse(readFileSync(LOCK, "utf8"));
|
|
31
|
+
return s.pid !== process.pid && alive(s.pid) ? s : null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function stopSession(pid) {
|
|
38
|
+
// SIGINT runs its own cleanup (UI, host_agent, containers). Windows has no signals — kill the tree.
|
|
39
|
+
if (process.platform === "win32") spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
40
|
+
else process.kill(pid, "SIGINT");
|
|
41
|
+
for (let i = 0; i < 60 && alive(pid); i++) await new Promise((r) => setTimeout(r, 500));
|
|
42
|
+
if (alive(pid)) process.kill(pid, "SIGKILL");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function claimDevSession(root, command) {
|
|
46
|
+
if (process.env.MUSCAT_PK_MANAGED_DEV === "1") return; // `dev` claimed it for its dev:ui / dev:back
|
|
47
|
+
const other = runningSession();
|
|
48
|
+
if (other?.pid === process.ppid) return; // `dev`'s own dev:back child
|
|
49
|
+
if (other) {
|
|
50
|
+
log.warn(`A Muscat plugin dev environment is already running: muscat-pk ${other.command} in ${other.root} (pid ${other.pid}).`);
|
|
51
|
+
log.warn("Only one can run per PC — they share the backend container, the local Muscat and its ports.");
|
|
52
|
+
const answer = (await prompt(" Stop it and start this one? [y/N]: ")).toLowerCase();
|
|
53
|
+
if (answer !== "y" && answer !== "yes") throw new Error("another plugin dev environment is running — stop it first.");
|
|
54
|
+
await stopSession(other.pid);
|
|
55
|
+
stopDevBackContainers(); // whatever it could not clean up
|
|
56
|
+
log.ok("stopped the other plugin dev environment");
|
|
57
|
+
}
|
|
58
|
+
mkdirSync(dirname(LOCK), { recursive: true });
|
|
59
|
+
writeFileSync(LOCK, JSON.stringify({ pid: process.pid, root, command }));
|
|
60
|
+
process.on("exit", () => {
|
|
61
|
+
try {
|
|
62
|
+
if (JSON.parse(readFileSync(LOCK, "utf8")).pid === process.pid) rmSync(LOCK, { force: true });
|
|
63
|
+
} catch {
|
|
64
|
+
/* already gone */
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
package/cli/src/devUi.mjs
CHANGED
|
@@ -1,38 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { createServer as createNetServer } from "node:net";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
5
6
|
import { homedir } from "node:os";
|
|
6
7
|
import { dirname, resolve } from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
import { checkKitUpdate, findPluginRoot, hasBin, kitRoot, log } from "./util.mjs";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { checkKitUpdate, ensureDevDir, findPluginRoot, hasBin, kitRoot, log, openBrowser, parseArgs, prompt } from "./util.mjs";
|
|
10
|
+
import { devPluginId } from "./devId.mjs";
|
|
11
|
+
import { readManifest } from "./manifest.mjs";
|
|
12
|
+
import { ensureLocalMuscat, GATEWAY_PORT, localFullMuscat, robotHasMuscat } from "./muscatLocal.mjs";
|
|
13
|
+
import { isRemoteRobot } from "./zenohDev.mjs";
|
|
14
|
+
import { claimDevSession } from "./devSession.mjs";
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const DEV_PLUGIN_PORT = 4181;
|
|
18
|
+
|
|
19
|
+
function getFreePort(preferred) {
|
|
20
|
+
return new Promise((res) => {
|
|
21
|
+
const srv = createNetServer();
|
|
22
|
+
srv.once("error", () => {
|
|
23
|
+
const s2 = createNetServer();
|
|
24
|
+
s2.listen(0, () => {
|
|
25
|
+
const { port } = s2.address();
|
|
26
|
+
s2.close(() => res(port));
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
srv.listen(preferred, () => srv.close(() => res(preferred)));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
9
32
|
|
|
10
33
|
const PREVIEW_PKG = "@rainbow-robotics/preview-host";
|
|
11
34
|
const CACHE_DIR = resolve(homedir(), ".muscat-pk");
|
|
12
35
|
const CACHE_ROOT = resolve(CACHE_DIR, "node_modules", "@rainbow-robotics", "preview-host");
|
|
13
36
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
function findNodeBin() {
|
|
40
|
+
if (typeof Bun === "undefined") return process.execPath;
|
|
41
|
+
for (const c of ["node", "node20", "node18"]) {
|
|
42
|
+
if (hasBin(c)) return c;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
21
47
|
function findPreviewHostRoot() {
|
|
22
|
-
// 1. npm/yarn 설치 경로
|
|
23
48
|
try {
|
|
24
49
|
const req = createRequire(import.meta.url);
|
|
25
50
|
return dirname(req.resolve(`${PREVIEW_PKG}/package.json`));
|
|
26
|
-
} catch {
|
|
51
|
+
} catch {
|
|
52
|
+
console.log("Try the sibling and cached package locations below.")
|
|
53
|
+
}
|
|
27
54
|
|
|
28
|
-
// 2. 모노레포 sibling
|
|
29
55
|
const sibling = resolve(kitRoot, "../preview-host");
|
|
30
56
|
if (existsSync(resolve(sibling, "package.json"))) return sibling;
|
|
31
57
|
|
|
32
|
-
// 3. 캐시
|
|
33
58
|
if (existsSync(resolve(CACHE_ROOT, "package.json"))) return CACHE_ROOT;
|
|
34
59
|
|
|
35
|
-
// 4. npm 자동 설치 → 캐시
|
|
36
60
|
if (!hasBin("npm")) {
|
|
37
61
|
throw new Error(
|
|
38
62
|
`preview-host를 찾을 수 없습니다.\n` +
|
|
@@ -53,32 +77,197 @@ function findPreviewHostRoot() {
|
|
|
53
77
|
return CACHE_ROOT;
|
|
54
78
|
}
|
|
55
79
|
|
|
80
|
+
function syncAssets(pluginRoot, outDir) {
|
|
81
|
+
const assetsSrc = resolve(pluginRoot, "assets");
|
|
82
|
+
if (existsSync(assetsSrc)) cpSync(assetsSrc, resolve(outDir, "assets"), { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
// "(dev)" after every name the plugin shows, so a dev session is never mistaken for an installed copy.
|
|
87
|
+
const devLabel = (v) =>
|
|
88
|
+
typeof v === "string"
|
|
89
|
+
? `${v} (dev)`
|
|
90
|
+
: v && typeof v === "object"
|
|
91
|
+
? Object.fromEntries(Object.entries(v).map(([lang, s]) => [lang, `${s} (dev)`]))
|
|
92
|
+
: v;
|
|
93
|
+
|
|
94
|
+
// preview-host takes the plugin id from this copy, so the UI talks under the session's dev id.
|
|
95
|
+
function stageDevAssets(pluginRoot, pluginId) {
|
|
96
|
+
const outDir = ensureDevDir(pluginRoot);
|
|
97
|
+
const manifestSrc = resolve(pluginRoot, "manifest.json");
|
|
98
|
+
if (existsSync(manifestSrc)) {
|
|
99
|
+
const manifest = JSON.parse(readFileSync(manifestSrc, "utf8"));
|
|
100
|
+
if (pluginId) manifest.id = pluginId;
|
|
101
|
+
manifest.name = devLabel(manifest.name);
|
|
102
|
+
for (const c of manifest.contributes ?? []) {
|
|
103
|
+
c.label = devLabel(c.label);
|
|
104
|
+
if (c.commandInfo?.name) c.commandInfo.name = devLabel(c.commandInfo.name);
|
|
105
|
+
}
|
|
106
|
+
writeFileSync(resolve(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
107
|
+
}
|
|
108
|
+
syncAssets(pluginRoot, outDir);
|
|
109
|
+
return outDir;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function watchAssets(pluginRoot, outDir) {
|
|
113
|
+
const assetsSrc = resolve(pluginRoot, "assets");
|
|
114
|
+
if (!existsSync(assetsSrc)) return;
|
|
115
|
+
watch(assetsSrc, { recursive: true }, () => syncAssets(pluginRoot, outDir));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
function findViteBin(pluginRoot) {
|
|
120
|
+
try {
|
|
121
|
+
const req = createRequire(resolve(pluginRoot, "package.json"));
|
|
122
|
+
const vitePkg = req.resolve("vite/package.json");
|
|
123
|
+
return resolve(dirname(vitePkg), "bin", "vite.js");
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
function spawnPluginDevServer(pluginRoot, node, port, readyFile, sharedRuntimeBase) {
|
|
131
|
+
if (!node || !findViteBin(pluginRoot)) return null;
|
|
132
|
+
const runner = fileURLToPath(new URL("./pluginDevServer.mjs", import.meta.url));
|
|
133
|
+
|
|
134
|
+
rmSync(resolve(pluginRoot, "node_modules", ".vite"), { recursive: true, force: true });
|
|
135
|
+
|
|
136
|
+
return spawn(node, [runner, pluginRoot, String(port), readyFile], {
|
|
137
|
+
cwd: pluginRoot,
|
|
138
|
+
stdio: "inherit",
|
|
139
|
+
env: {
|
|
140
|
+
...process.env,
|
|
141
|
+
VITE_PLUGIN_PREVIEW: "true",
|
|
142
|
+
MUSCAT_PLUGIN_SHARED_RUNTIME_BASE: sharedRuntimeBase,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The ready file is the signal and an exit the failure; the timeout only guards a hang. A cold vite start
|
|
148
|
+
// takes ~11s on Windows and more while `dev`'s first docker build loads the machine, so a tight limit
|
|
149
|
+
// stopped a healthy `dev`.
|
|
150
|
+
function waitForPluginDevPort(readyFile, proc, timeoutMs = 120_000) {
|
|
151
|
+
const start = Date.now();
|
|
152
|
+
const slow = setTimeout(() => log.info("plugin dev server (vite) is still starting…"), 15_000);
|
|
153
|
+
return new Promise((res, rej) => {
|
|
154
|
+
let exited = false;
|
|
155
|
+
proc.once("exit", (code) => {
|
|
156
|
+
exited = true;
|
|
157
|
+
rej(new Error(`plugin dev server exited before ready (code ${code ?? 0})`));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const tick = () => {
|
|
161
|
+
if (existsSync(readyFile)) {
|
|
162
|
+
try {
|
|
163
|
+
const { port } = JSON.parse(readFileSync(readyFile, "utf8"));
|
|
164
|
+
if (Number.isInteger(port)) return res(port);
|
|
165
|
+
} catch {
|
|
166
|
+
console.log("Keep polling until the ready file contains valid JSON.")
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (Date.now() - start > timeoutMs)
|
|
170
|
+
return rej(new Error("plugin dev server did not become ready in time"));
|
|
171
|
+
if (!exited) setTimeout(tick, 100);
|
|
172
|
+
};
|
|
173
|
+
tick();
|
|
174
|
+
}).finally(() => clearTimeout(slow));
|
|
175
|
+
}
|
|
176
|
+
|
|
56
177
|
export async function runDevUi(args = []) {
|
|
57
|
-
const
|
|
58
|
-
const
|
|
178
|
+
const parsed = parseArgs(args);
|
|
179
|
+
const requestedPort = Number(parsed.port ?? "4180");
|
|
180
|
+
await claimDevSession(findPluginRoot(), "dev:ui");
|
|
181
|
+
const robotIp = parsed["robot-ip"] ?? process.env.MUSCAT_ROBOT_HOST ?? await prompt(" Robot IP (skip if no robot): ");
|
|
59
182
|
|
|
60
183
|
await checkKitUpdate();
|
|
61
|
-
const
|
|
184
|
+
const pluginRoot = findPluginRoot();
|
|
185
|
+
const outDir = stageDevAssets(pluginRoot, devPluginId(readManifest(pluginRoot).id, { robotIp, args }));
|
|
186
|
+
const port = await getFreePort(requestedPort);
|
|
187
|
+
|
|
188
|
+
// No robot: the bundle talks to localhost:3000, so put a Muscat there (`dev` already did).
|
|
189
|
+
if (!isRemoteRobot(robotIp) && process.env.MUSCAT_PK_MANAGED_DEV !== "1") {
|
|
190
|
+
await ensureLocalMuscat().catch((e) => log.warn(`local Muscat: ${e.message}`));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
watchAssets(pluginRoot, outDir);
|
|
194
|
+
|
|
195
|
+
const node = findNodeBin();
|
|
196
|
+
const readyFile = resolve(outDir, "plugin-dev-port.json");
|
|
197
|
+
rmSync(readyFile, { force: true });
|
|
198
|
+
const sharedRuntimeBase = `http://localhost:${port}/plugin-shared`;
|
|
199
|
+
const viteProc = spawnPluginDevServer(
|
|
200
|
+
pluginRoot,
|
|
201
|
+
node,
|
|
202
|
+
DEV_PLUGIN_PORT,
|
|
203
|
+
readyFile,
|
|
204
|
+
sharedRuntimeBase,
|
|
205
|
+
);
|
|
206
|
+
let devPort = DEV_PLUGIN_PORT;
|
|
207
|
+
|
|
208
|
+
if (!viteProc) {
|
|
209
|
+
log.warn("vite not found — UI will not load. Run `npm install` in the plugin project.");
|
|
210
|
+
} else {
|
|
211
|
+
devPort = await waitForPluginDevPort(readyFile, viteProc);
|
|
212
|
+
viteProc.on("exit", (code) => {
|
|
213
|
+
if (code) log.warn(`plugin dev server exited (code ${code}) — UI won't load (is :${devPort} free?).`);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// muscat-dev, or our own Muscat with a simulator (`dev --sim`): a full local Muscat to show.
|
|
218
|
+
const localDevMuscat = !!process.env.MUSCAT_PK_SIM || (await localFullMuscat(robotIp));
|
|
219
|
+
const uiHost = isRemoteRobot(robotIp) ? robotIp : localDevMuscat ? "localhost" : "";
|
|
62
220
|
|
|
63
221
|
const previewRoot = findPreviewHostRoot();
|
|
64
222
|
const serveEntry = resolve(previewRoot, "serve.mjs");
|
|
65
223
|
if (!existsSync(resolve(previewRoot, "dist", "index.html"))) {
|
|
66
224
|
log.step("building preview-host bundle (first run)…");
|
|
67
225
|
await new Promise((res, rej) => {
|
|
68
|
-
const p = spawn(process.execPath, [resolve(previewRoot, "build.mjs")], {
|
|
226
|
+
const p = spawn(node ?? process.execPath, [resolve(previewRoot, "build.mjs")], {
|
|
69
227
|
stdio: "inherit",
|
|
228
|
+
env: { ...process.env, VITE_PLUGIN_PREVIEW: "true" },
|
|
70
229
|
});
|
|
71
230
|
p.on("exit", (c) => (c === 0 ? res() : rej(new Error("preview-host build failed"))));
|
|
72
231
|
});
|
|
73
232
|
}
|
|
74
233
|
|
|
75
|
-
log.step(`starting preview-host on :${port}`);
|
|
76
234
|
const child = spawn(
|
|
77
|
-
process.execPath,
|
|
78
|
-
[
|
|
79
|
-
|
|
235
|
+
node ?? process.execPath,
|
|
236
|
+
[
|
|
237
|
+
serveEntry,
|
|
238
|
+
"--plugin", outDir,
|
|
239
|
+
"--port", String(port),
|
|
240
|
+
"--dev-server-url", `http://localhost:${devPort}`,
|
|
241
|
+
// A remote robot (or a local muscat-dev) turns the bundle into that Muscat's UI; otherwise
|
|
242
|
+
// it stays the mock preview.
|
|
243
|
+
...(uiHost ? ["--robot-host", uiHost] : []),
|
|
244
|
+
// The dev plugin's socket goes to that Muscat's gateway; otherwise to the local runtime.
|
|
245
|
+
...((await robotHasMuscat(robotIp)) || localDevMuscat ? ["--socket-url", `ws://${uiHost}:${GATEWAY_PORT}`] : []),
|
|
246
|
+
],
|
|
247
|
+
// preview-host only prints its address and paths; the "UI →" line below says what matters.
|
|
248
|
+
{ stdio: ["inherit", "ignore", "inherit"] },
|
|
80
249
|
);
|
|
81
|
-
|
|
250
|
+
|
|
251
|
+
for (let i = 0; i < 50 && !(await fetch(`http://localhost:${port}/`).then(() => true, () => false)); i++)
|
|
252
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
253
|
+
log.ok(`UI → http://localhost:${port}`);
|
|
254
|
+
// Only for a person at a terminal — not CI, scripts or `--no-open`.
|
|
255
|
+
if (process.stdout.isTTY && !process.env.CI && !parsed["no-open"]) openBrowser(`http://localhost:${port}`);
|
|
256
|
+
|
|
257
|
+
let cleaned = false;
|
|
258
|
+
const cleanup = () => {
|
|
259
|
+
if (cleaned) return;
|
|
260
|
+
cleaned = true;
|
|
261
|
+
if (viteProc && !viteProc.killed && viteProc.exitCode === null) viteProc.kill();
|
|
262
|
+
if (!child.killed && child.exitCode === null) child.kill();
|
|
263
|
+
};
|
|
264
|
+
process.once("SIGINT", cleanup);
|
|
265
|
+
process.once("SIGTERM", cleanup);
|
|
266
|
+
process.once("SIGHUP", cleanup);
|
|
267
|
+
child.on("exit", (code) => {
|
|
268
|
+
cleanup();
|
|
269
|
+
process.exit(code ?? 0);
|
|
270
|
+
});
|
|
82
271
|
}
|
|
83
272
|
|
|
84
273
|
export { findPluginRoot, findPreviewHostRoot };
|
package/cli/src/esbuild.mjs
CHANGED
|
@@ -20,20 +20,37 @@ async function buildWithBun(options) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
async function buildWithWasm(options) {
|
|
24
|
+
const wasm = await import("esbuild-wasm");
|
|
25
|
+
return wasm.build(options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isNativeMissing(err) {
|
|
29
|
+
const msg = String(err?.message || err?.code || err || "");
|
|
30
|
+
return (
|
|
31
|
+
msg.includes("another platform") ||
|
|
32
|
+
msg.includes("Cannot find module") ||
|
|
33
|
+
msg.includes("Cannot find package") ||
|
|
34
|
+
msg.includes("not found") ||
|
|
35
|
+
msg.includes("No such file") ||
|
|
36
|
+
msg.includes("ENOENT") ||
|
|
37
|
+
msg.includes("MODULE_NOT_FOUND")
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
export async function buildWithFallback(options) {
|
|
24
42
|
if (typeof Bun !== "undefined") return buildWithBun(options);
|
|
25
43
|
|
|
26
|
-
|
|
44
|
+
// Try native esbuild (faster, no extra setup). Falls back to wasm when:
|
|
45
|
+
// - esbuild was not installed (removed from hard dependencies to avoid
|
|
46
|
+
// postinstall script blocking npm i -g on Windows)
|
|
47
|
+
// - binary was built for a different platform
|
|
27
48
|
try {
|
|
49
|
+
const native = await import("esbuild");
|
|
28
50
|
return await native.build(options);
|
|
29
|
-
} catch (
|
|
30
|
-
|
|
31
|
-
if (!message.includes("another platform")) {
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const wasm = await import("esbuild-wasm");
|
|
36
|
-
console.warn("[muscat-pk] native esbuild binary mismatch; falling back to esbuild-wasm");
|
|
37
|
-
return wasm.build(options);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (!isNativeMissing(err)) throw err;
|
|
38
53
|
}
|
|
54
|
+
|
|
55
|
+
return buildWithWasm(options);
|
|
39
56
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Host-side Zenoh byte-proxy for Docker Desktop containers (Mac/WSL remote dev).
|
|
2
|
+
|
|
3
|
+
Joins the robot's Zenoh mesh with eclipse-zenoh (no rainbow SDK) and relays
|
|
4
|
+
{query, put, subscribe} bytes over TCP to the dev container.
|
|
5
|
+
|
|
6
|
+
Env: MUSCAT_ROBOT_HOST, MUSCAT_ROBOT_TOKEN, MUSCAT_ZENOH_ADVERTISE_HOST,
|
|
7
|
+
MUSCAT_ZENOH_PORT (robot-facing), MUSCAT_PROXY_PORT (container-facing).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import signal
|
|
16
|
+
import socket
|
|
17
|
+
import struct
|
|
18
|
+
import sys
|
|
19
|
+
import threading
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.request
|
|
22
|
+
|
|
23
|
+
import zenoh
|
|
24
|
+
|
|
25
|
+
PEER_TTL = 30
|
|
26
|
+
QUERY_TARGETS = {0: zenoh.QueryTarget.BEST_MATCHING, 1: zenoh.QueryTarget.ALL, 2: zenoh.QueryTarget.ALL_COMPLETE}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _send_frame(conn: socket.socket, lock: threading.Lock, obj: dict) -> None:
|
|
30
|
+
body = json.dumps(obj).encode()
|
|
31
|
+
with lock:
|
|
32
|
+
conn.sendall(struct.pack(">I", len(body)) + body)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _recv_frame(conn: socket.socket) -> dict | None:
|
|
36
|
+
hdr = b""
|
|
37
|
+
while len(hdr) < 4:
|
|
38
|
+
chunk = conn.recv(4 - len(hdr))
|
|
39
|
+
if not chunk:
|
|
40
|
+
return None
|
|
41
|
+
hdr += chunk
|
|
42
|
+
n = struct.unpack(">I", hdr)[0]
|
|
43
|
+
buf = b""
|
|
44
|
+
while len(buf) < n:
|
|
45
|
+
chunk = conn.recv(n - len(buf))
|
|
46
|
+
if not chunk:
|
|
47
|
+
return None
|
|
48
|
+
buf += chunk
|
|
49
|
+
return json.loads(buf.decode())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _att(sample) -> str:
|
|
53
|
+
return sample.attachment.to_string() if sample.attachment is not None else ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _query(session: zenoh.Session, req: dict) -> list[dict]:
|
|
57
|
+
payload = bytes.fromhex(req["payload_hex"]) if req.get("payload_hex") else None
|
|
58
|
+
replies = session.get(
|
|
59
|
+
req["keyexpr"],
|
|
60
|
+
payload=payload,
|
|
61
|
+
timeout=req.get("timeout_ms", 2000) / 1000,
|
|
62
|
+
target=QUERY_TARGETS.get(req.get("target", 0), zenoh.QueryTarget.BEST_MATCHING),
|
|
63
|
+
)
|
|
64
|
+
out = []
|
|
65
|
+
for reply in replies:
|
|
66
|
+
if reply.ok is not None:
|
|
67
|
+
s = reply.ok
|
|
68
|
+
out.append({"key": str(s.key_expr), "payload_hex": s.payload.to_bytes().hex(),
|
|
69
|
+
"att": _att(s), "encoding": str(s.encoding), "is_error": False})
|
|
70
|
+
else:
|
|
71
|
+
e = reply.err
|
|
72
|
+
out.append({"key": req["keyexpr"], "payload_hex": e.payload.to_bytes().hex(),
|
|
73
|
+
"att": "", "encoding": str(e.encoding), "is_error": True})
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _handle_conn(conn: socket.socket, session: zenoh.Session) -> None:
|
|
78
|
+
send_lock = threading.Lock()
|
|
79
|
+
subs: dict[str, zenoh.Subscriber] = {}
|
|
80
|
+
try:
|
|
81
|
+
while True:
|
|
82
|
+
req = _recv_frame(conn)
|
|
83
|
+
if req is None:
|
|
84
|
+
return
|
|
85
|
+
op = req.get("type")
|
|
86
|
+
rid = req.get("id")
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
if op == "query":
|
|
90
|
+
out = {"type": "query_resp", "id": rid, "ok": True, "replies": _query(session, req)}
|
|
91
|
+
|
|
92
|
+
elif op == "put":
|
|
93
|
+
payload = bytes.fromhex(req["payload_hex"]) if req.get("payload_hex") else b""
|
|
94
|
+
session.put(req["keyexpr"], payload)
|
|
95
|
+
out = {"type": "put_resp", "id": rid, "ok": True}
|
|
96
|
+
|
|
97
|
+
elif op == "subscribe":
|
|
98
|
+
keyexpr = req["keyexpr"]
|
|
99
|
+
|
|
100
|
+
def on_sample(s, _ke: str = keyexpr) -> None:
|
|
101
|
+
_send_frame(conn, send_lock, {
|
|
102
|
+
"type": "sample", "keyexpr": _ke, "key": str(s.key_expr),
|
|
103
|
+
"payload_hex": s.payload.to_bytes().hex(), "att": _att(s),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
old = subs.pop(keyexpr, None)
|
|
107
|
+
if old is not None:
|
|
108
|
+
old.undeclare()
|
|
109
|
+
subs[keyexpr] = session.declare_subscriber(keyexpr, on_sample)
|
|
110
|
+
out = {"type": "subscribe_resp", "id": rid, "ok": True}
|
|
111
|
+
|
|
112
|
+
elif op == "unsubscribe":
|
|
113
|
+
sub = subs.pop(req["keyexpr"], None)
|
|
114
|
+
if sub is not None:
|
|
115
|
+
with contextlib.suppress(Exception):
|
|
116
|
+
sub.undeclare()
|
|
117
|
+
out = {"type": "unsubscribe_resp", "id": rid, "ok": True}
|
|
118
|
+
|
|
119
|
+
else:
|
|
120
|
+
out = {"type": "error", "error": f"unknown op {op!r}"}
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
out = {"type": f"{op}_resp", "id": rid, "ok": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
123
|
+
_send_frame(conn, send_lock, out)
|
|
124
|
+
except (ConnectionResetError, BrokenPipeError, OSError):
|
|
125
|
+
pass
|
|
126
|
+
finally:
|
|
127
|
+
for sub in subs.values():
|
|
128
|
+
with contextlib.suppress(Exception):
|
|
129
|
+
sub.undeclare()
|
|
130
|
+
conn.close()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Peer:
|
|
134
|
+
"""Registers this host with the robot's common: POST /zenoh/peer, heartbeat, DELETE."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, host: str, token: str, ip: str, port: int) -> None:
|
|
137
|
+
self.base = f"http://{host}:8000"
|
|
138
|
+
self.headers = {"Content-Type": "application/json"}
|
|
139
|
+
if token:
|
|
140
|
+
self.headers["Authorization"] = f"Bearer {token}"
|
|
141
|
+
self.body = {"ip": ip, "port": port, "ttl": PEER_TTL}
|
|
142
|
+
self.peer_id: str | None = None
|
|
143
|
+
self.stop = threading.Event()
|
|
144
|
+
|
|
145
|
+
def _http(self, method: str, path: str, body: dict | None = None) -> dict:
|
|
146
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
147
|
+
req = urllib.request.Request(self.base + path, data=data, headers=self.headers, method=method)
|
|
148
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
149
|
+
raw = resp.read()
|
|
150
|
+
return json.loads(raw) if raw else {}
|
|
151
|
+
|
|
152
|
+
def register(self) -> None:
|
|
153
|
+
self.peer_id = self._http("POST", "/zenoh/peer", self.body)["peer_id"]
|
|
154
|
+
print(f"[host-agent] registered with {self.base} as zenoh peer {self.peer_id}", flush=True)
|
|
155
|
+
|
|
156
|
+
def maintain(self) -> None:
|
|
157
|
+
while True:
|
|
158
|
+
wait = PEER_TTL / 2 if self.peer_id else 3
|
|
159
|
+
if self.stop.wait(wait):
|
|
160
|
+
return
|
|
161
|
+
try:
|
|
162
|
+
if self.peer_id is None:
|
|
163
|
+
self.register()
|
|
164
|
+
elif not self._http("POST", f"/zenoh/peer/{self.peer_id}/heartbeat", {"ttl": PEER_TTL}).get("ok"):
|
|
165
|
+
print(f"[host-agent] heartbeat rejected for {self.peer_id}, re-registering", flush=True)
|
|
166
|
+
self.peer_id = None
|
|
167
|
+
except Exception as exc:
|
|
168
|
+
print(f"[host-agent] peer maintenance failed ({exc}); retrying", flush=True)
|
|
169
|
+
if self.peer_id is not None and isinstance(exc, urllib.error.HTTPError) and exc.code == 404:
|
|
170
|
+
self.peer_id = None
|
|
171
|
+
|
|
172
|
+
def close(self) -> None:
|
|
173
|
+
self.stop.set()
|
|
174
|
+
if self.peer_id:
|
|
175
|
+
with contextlib.suppress(Exception):
|
|
176
|
+
self._http("DELETE", f"/zenoh/peer/{self.peer_id}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def main() -> None:
|
|
180
|
+
# dev stops us with SIGTERM; exit via `finally` to deregister.
|
|
181
|
+
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
|
182
|
+
host = os.environ["MUSCAT_ROBOT_HOST"]
|
|
183
|
+
token = os.environ.get("MUSCAT_ROBOT_TOKEN", "")
|
|
184
|
+
advertise = os.environ["MUSCAT_ZENOH_ADVERTISE_HOST"]
|
|
185
|
+
zenoh_port = int(os.environ["MUSCAT_ZENOH_PORT"])
|
|
186
|
+
proxy_port = int(os.environ["MUSCAT_PROXY_PORT"])
|
|
187
|
+
|
|
188
|
+
# Listen only; the robot dials in after registration. Router mode so a C++ SDK dev container
|
|
189
|
+
# (native Zenoh session, no byte shim) can connect here and be routed to the robot.
|
|
190
|
+
conf = zenoh.Config()
|
|
191
|
+
conf.insert_json5("mode", '"peer"')
|
|
192
|
+
conf.insert_json5("listen/endpoints", json.dumps([f"tcp/0.0.0.0:{zenoh_port}"]))
|
|
193
|
+
conf.insert_json5("scouting/multicast/enabled", "false")
|
|
194
|
+
session = zenoh.open(conf)
|
|
195
|
+
|
|
196
|
+
peer = Peer(host, token, advertise, zenoh_port)
|
|
197
|
+
try:
|
|
198
|
+
peer.register()
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
print(f"[host-agent] registration with {peer.base} failed, retrying in background: {exc}", flush=True)
|
|
201
|
+
threading.Thread(target=peer.maintain, daemon=True, name="zenoh-peer").start()
|
|
202
|
+
print(f"[host-agent] zenoh peer listening :{zenoh_port} (advertised as {advertise})", flush=True)
|
|
203
|
+
|
|
204
|
+
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
205
|
+
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
206
|
+
srv.bind(("0.0.0.0", proxy_port))
|
|
207
|
+
srv.listen(32)
|
|
208
|
+
print(f"[host-agent] byte-proxy listening :{proxy_port} (container connects via host.docker.internal)", flush=True)
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
while True:
|
|
212
|
+
conn, _addr = srv.accept()
|
|
213
|
+
threading.Thread(target=_handle_conn, args=(conn, session), daemon=True).start()
|
|
214
|
+
except KeyboardInterrupt:
|
|
215
|
+
pass
|
|
216
|
+
finally:
|
|
217
|
+
peer.close()
|
|
218
|
+
with contextlib.suppress(Exception):
|
|
219
|
+
session.close()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == "__main__":
|
|
223
|
+
main()
|