@rainbow-robotics/muscat-plugin-kit 1.10.1-dev.1 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +282 -282
- package/cli/README.md +274 -274
- package/cli/bin/create-muscat-plugin.mjs +8 -8
- package/cli/bin/muscat-pk.mjs +155 -155
- package/cli/bundle-templates.mjs +31 -31
- package/cli/docker/Dockerfile.cpp +28 -28
- package/cli/docker/Dockerfile.dev +30 -30
- package/cli/docker/Dockerfile.python +21 -21
- package/cli/m5-e2e.mjs +69 -69
- package/cli/prepack.mjs +10 -10
- package/cli/src/ai-rules.mjs +70 -70
- package/cli/src/build.mjs +447 -447
- package/cli/src/check-package.mjs +13 -13
- package/cli/src/completion.mjs +166 -166
- package/cli/src/cpp.mjs +78 -78
- package/cli/src/cppIde.mjs +75 -75
- package/cli/src/dev.mjs +165 -165
- package/cli/src/devBack.mjs +734 -727
- package/cli/src/devUi.mjs +247 -243
- package/cli/src/esbuild.mjs +56 -56
- package/cli/src/host_agent.py +223 -223
- package/cli/src/manifest.mjs +287 -287
- package/cli/src/mcp.mjs +1071 -1071
- package/cli/src/muscatLocal.mjs +209 -209
- package/cli/src/new.mjs +343 -343
- package/cli/src/pluginDevServer.mjs +111 -111
- package/cli/src/ports.mjs +193 -193
- package/cli/src/python.mjs +112 -112
- package/cli/src/ros2.mjs +109 -109
- package/cli/src/sdk.mjs +117 -117
- package/cli/src/storybook.mjs +168 -168
- package/cli/src/type.mjs +141 -141
- package/cli/src/uninstall.mjs +81 -81
- package/cli/src/update.mjs +295 -295
- package/cli/src/util.mjs +441 -441
- package/cli/src/zenohDev.mjs +83 -81
- package/cli/storybook-dist/_design-tokens.css +4957 -4957
- package/cli/storybook-dist/_stories-src/NxAvatar.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/NxPagination.stories.tsx +28 -28
- package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordion.stories.tsx +46 -46
- package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordionGroup.stories.tsx +73 -73
- package/cli/storybook-dist/_stories-src/RBBadge/RBBadge.stories.tsx +93 -93
- package/cli/storybook-dist/_stories-src/RBBattery/RBBattery.stories.tsx +74 -74
- package/cli/storybook-dist/_stories-src/RBButton/RBCheckIconButton.stories.tsx +28 -28
- package/cli/storybook-dist/_stories-src/RBButton/RBCheckLabelButton.stories.tsx +84 -84
- package/cli/storybook-dist/_stories-src/RBButton/RBIconButton.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/RBButton/RBInlineLabelButton.stories.tsx +41 -41
- package/cli/storybook-dist/_stories-src/RBButton/RBLabelButton.stories.tsx +60 -60
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBDateInput.stories.tsx +44 -44
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +56 -56
- package/cli/storybook-dist/_stories-src/RBDialog/RBAlert.stories.tsx +51 -51
- package/cli/storybook-dist/_stories-src/RBDialog/RBConfirm.stories.tsx +55 -55
- package/cli/storybook-dist/_stories-src/RBDialog/RBPrompt.stories.tsx +52 -52
- package/cli/storybook-dist/_stories-src/RBDivider/RBDivider.stories.tsx +93 -93
- package/cli/storybook-dist/_stories-src/RBEmptyView/RBEmptyView.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBIcons/RBColorIcons.stories.tsx +108 -108
- package/cli/storybook-dist/_stories-src/RBIcons/RBIcons.stories.tsx +106 -106
- package/cli/storybook-dist/_stories-src/RBInput/RBCheckBox.stories.tsx +89 -89
- package/cli/storybook-dist/_stories-src/RBInput/RBMultiRangeInput.stories.tsx +64 -64
- package/cli/storybook-dist/_stories-src/RBInput/RBRadioBox.stories.tsx +65 -65
- package/cli/storybook-dist/_stories-src/RBInput/RBRadioButton.stories.tsx +68 -68
- package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +216 -216
- package/cli/storybook-dist/_stories-src/RBInput/RBSideNumberInput.stories.tsx +90 -90
- package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextArea.stories.tsx +45 -45
- package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextInput.stories.tsx +50 -50
- package/cli/storybook-dist/_stories-src/RBInput/RBTextField.stories.tsx +102 -102
- package/cli/storybook-dist/_stories-src/RBJoyStick/RBJoyStick.stories.tsx +31 -31
- package/cli/storybook-dist/_stories-src/RBLight/RBLight.stories.tsx +36 -36
- package/cli/storybook-dist/_stories-src/RBPagination/RBPagination.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBShadcn/RBCard/RBCard.stories.tsx +89 -89
- package/cli/storybook-dist/_stories-src/RBShadcn/RBCommand/RBCommand.stories.tsx +116 -116
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDialog/RBDialog.stories.tsx +74 -74
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDrawer/RBDrawer.stories.tsx +99 -99
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDropdownMenu/RBDropdownMenu.stories.tsx +123 -123
- package/cli/storybook-dist/_stories-src/RBShadcn/RBMenubar/RBMenubar.stories.tsx +135 -135
- package/cli/storybook-dist/_stories-src/RBShadcn/RBPopover/RBPopover.stories.tsx +69 -69
- package/cli/storybook-dist/_stories-src/RBShadcn/RBResizable/RBResizable.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSheet/RBSheet.stories.tsx +91 -91
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSlider/RBSlider.stories.tsx +38 -38
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSwitch/RBSwitch.stories.tsx +112 -112
- package/cli/storybook-dist/_stories-src/RBShadcn/RBTabs/RBTabs.stories.tsx +75 -75
- package/cli/storybook-dist/_stories-src/RBShadcn/RBToggle/RBToggle.stories.tsx +51 -51
- package/cli/storybook-dist/_stories-src/RBShadcn/RBToggleGroup/RBToggleGroup.stories.tsx +84 -84
- package/cli/storybook-dist/_stories-src/RBTab/RBChipTab.stories.tsx +102 -102
- package/cli/storybook-dist/_stories-src/RBTab/RBChipsBoxTab.stories.tsx +78 -78
- package/cli/storybook-dist/_stories-src/RBTab/RBTab.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/RBTable/RBTable.stories.tsx +177 -177
- package/cli/storybook-dist/_stories-src/RBTable/RBTableRow.stories.tsx +64 -64
- package/cli/storybook-dist/_stories-src/RBToast/RBToast.stories.tsx +117 -117
- package/cli/storybook-dist/_stories-src/RBTooltip/RBTooltip.stories.tsx +50 -50
- package/cli/storybook-dist/_stories-src/loading/RBSpinner.stories.tsx +30 -30
- package/cli/storybook-dist/_stories-src/nx-button/NxButton.stories.tsx +54 -54
- package/cli/storybook-dist/_stories-src/nx-button/NxPointerHoldButton.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/nx-icons/NxIcons.stories.tsx +48 -48
- package/cli/storybook-dist/_stories-src/nx-inputs/NxNumberInput.stories.tsx +103 -103
- package/cli/storybook-dist/_stories-src/nx-inputs/NxRangeInput.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSelectBox.stories.tsx +67 -67
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSlideRange.stories.tsx +45 -45
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSwitch.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/nx-inputs/NxTextArea.stories.tsx +48 -48
- package/cli/storybook-dist/_stories-src/nx-inputs/NxTextInput.stories.tsx +46 -46
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-D6JqZzDJ.js → Color-YHDXOIA2-B4IER17l.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-ESb7JAHg.js → Docs-C8GCHFtK.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-6gD2kUo1.js → Docs-CgdEMPQP.js} +2 -2
- package/cli/storybook-dist/assets/{Docs-B9wd8Ok7.js → Docs-Ck3WHvk6.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-CgFbWWXM.js → Docs-D9NHsoeZ.js} +2 -2
- package/cli/storybook-dist/assets/{Docs-DBQup8Bu.js → Docs-DNYQC7nl.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-CMjovnSY.js → Docs-DUfeiM4I.js} +4 -4
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-BOUI1AHi.js → DocsRenderer-CFRXHY34-C0WhyDoR.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-DsYFMUh3.js → NxAvatar.stories-CwnK8AUF.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-CHfNcqsc.js → NxButton.stories-ueYdosfQ.js} +8 -8
- package/cli/storybook-dist/assets/{NxNumberInput.stories-BFw_4vFE.js → NxNumberInput.stories-M5ZCLbrK.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-DVfxyw4M.js → NxPagination.stories-bxxUnHEL.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BzyxUWoZ.js → NxPointerHoldButton.stories-BHxB2kr5.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-DmRdzBc1.js → NxRangeInput.stories-BKfr-fp4.js} +6 -6
- package/cli/storybook-dist/assets/{NxSelectBox.stories-B1KkNUOf.js → NxSelectBox.stories-CG8FuTBq.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-CyOdvfQ6.js → NxSlideRange.stories-BRTFtMTL.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-DrDBw0qp.js → NxSwitch.stories-Bwuyzn-R.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-DTB2aqNS.js → NxTextArea.stories-COq2sGqu.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-DU0rucmM.js → NxTextInput.stories-CjsBLz05.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories-DTfOtXtp.js → RBAccordion.stories-BARwuZ0Q.js} +3 -3
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-9y9BjrUe.js → RBAccordionGroup.stories-COrPxETW.js} +10 -10
- package/cli/storybook-dist/assets/{RBAlert.stories-DLTAv9fT.js → RBAlert.stories-BQrkjZPt.js} +3 -3
- package/cli/storybook-dist/assets/{RBBadge.stories-BNMACewA.js → RBBadge.stories-BTsLL-lK.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-B3kXMAt5.js → RBBattery.stories-DAeQdJbc.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-Y-s8R2n7.js → RBCard.stories-HyhOE80R.js} +39 -39
- package/cli/storybook-dist/assets/{RBCheckBox.stories-DBuT9tL4.js → RBCheckBox.stories-KFk0WqX_.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-CWu1TL56.js → RBCheckIconButton.stories-CXbbktF5.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-sCu2MGCX.js → RBCheckLabelButton.stories-DWfYz_9Z.js} +17 -17
- package/cli/storybook-dist/assets/{RBChipTab.stories-BX9qLJkR.js → RBChipTab.stories-JPEgVFAS.js} +5 -5
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-35u1aJ-P.js → RBChipsBoxTab.stories-CTPuWXrQ.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-DAOZ5Ies.js → RBColorIcons.stories-DiFRZ5jk.js} +1 -1
- package/cli/storybook-dist/assets/RBCommand.stories-Pf1HAVIR.js +73 -0
- package/cli/storybook-dist/assets/{RBConfirm.stories-iMXcUh7Z.js → RBConfirm.stories-D1cEPUXk.js} +3 -3
- package/cli/storybook-dist/assets/{RBDateInput.stories-hK5rXECf.js → RBDateInput.stories-RPYpCjig.js} +1 -1
- package/cli/storybook-dist/assets/RBDialog.stories-DJH-0XSR.js +38 -0
- package/cli/storybook-dist/assets/{RBDivider.stories-BAK-iZ1Z.js → RBDivider.stories-uEj1vOgK.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-B2nqf2Nh.js → RBDrawer.stories-BiMzElb2.js} +12 -12
- package/cli/storybook-dist/assets/RBDropdownMenu.stories-Da_TBkla.js +86 -0
- package/cli/storybook-dist/assets/{RBEmptyView.stories-nCenrgBh.js → RBEmptyView.stories-BHeKGfPS.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-DRH7O3sp.js → RBIconButton.stories-NEsRFoYp.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-D3PyJXG8.js → RBIcons.stories-CcZ-TTLS.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-modQbt10.js → RBInlineLabelButton.stories-B0wpdQyR.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-BY14qVgR.js → RBJoyStick.stories-DEgBou3K.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-DzF7vGwm.js → RBLabelButton.stories-DT8drIsF.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-lF4FRmVh.js → RBLight.stories-Dh-aoeyG.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-XhrI7bsV.js → RBMenubar.stories-D-H7UqVh.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-DprllQDl.js → RBMultiRangeInput.stories-hlclv6jI.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-2v2gBJfC.js → RBPagination.stories-DWCeoZ_S.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-C8iLIZpv.js → RBPopover.stories-C514M09_.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-Th_HrWHI.js → RBPrompt.stories-BSItvfiT.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-CoHuLaN3.js → RBRadioBox.stories-BBb4YbFU.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-DULxE_ff.js → RBRadioButton.stories-CXyOGJCv.js} +1 -1
- package/cli/storybook-dist/assets/{RBRangeDateInput.stories-D8A9KBSS.js → RBRangeDateInput.stories-qvhubu0e.js} +1 -1
- package/cli/storybook-dist/assets/{RBResizable.stories-C41QfrQ3.js → RBResizable.stories-CZKeyGjH.js} +14 -14
- package/cli/storybook-dist/assets/{RBSelectBox.stories-C1gNmwzw.js → RBSelectBox.stories-CRKP3ybf.js} +1 -1
- package/cli/storybook-dist/assets/RBSheet.stories-sCM3ZJUa.js +39 -0
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-9LlgQ8nh.js → RBSideNumberInput.stories-Bb7gw8RF.js} +3 -3
- package/cli/storybook-dist/assets/{RBSlider.stories-oUgo5K9d.js → RBSlider.stories-23OAQU5z.js} +3 -3
- package/cli/storybook-dist/assets/{RBSpinner.stories-DDpohbY2.js → RBSpinner.stories-2xcQdcmt.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-gW-GN7QO.js → RBSwitch.stories-BACUaJm_.js} +7 -7
- package/cli/storybook-dist/assets/{RBTab.stories-CN-PV3HH.js → RBTab.stories-CDHE3sVs.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-BAZLpxtM.js → RBTable.stories-C0Cfhfl7.js} +29 -29
- package/cli/storybook-dist/assets/{RBTableRow.stories-CwjVrszB.js → RBTableRow.stories-BF8kIkpp.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-JuEo2sUL.js → RBTabs.stories-BdJWRq1o.js} +7 -7
- package/cli/storybook-dist/assets/{RBTextArea.stories-D0_Pv4qZ.js → RBTextArea.stories-CGy2xeYH.js} +3 -3
- package/cli/storybook-dist/assets/{RBTextField.stories-CCsOvZqV.js → RBTextField.stories-IfUOzxCM.js} +15 -15
- package/cli/storybook-dist/assets/{RBTextInput.stories-Cccn3VNm.js → RBTextInput.stories-VjXNFo4R.js} +3 -3
- package/cli/storybook-dist/assets/{RBToast.stories-I5617t0K.js → RBToast.stories-BzcobblV.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-DsgKjGsJ.js → RBToggle.stories-DuLE5mKv.js} +4 -4
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-BZEzSDuz.js → RBToggleGroup.stories-CuR0N0GJ.js} +5 -5
- package/cli/storybook-dist/assets/{RBTooltip.stories-4oSyAWsA.js → RBTooltip.stories--aAgNC8_.js} +1 -1
- package/cli/storybook-dist/assets/{SideNumberInput-DSKbd0bG.js → SideNumberInput-DyWQxYUj.js} +743 -743
- package/cli/storybook-dist/assets/{entry-preview-B35XREGs.js → entry-preview-D7IHsza_.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-BB797lmL.js → iframe-CbaIp5I-.js} +2 -2
- package/cli/storybook-dist/assets/{index-BU2567EP.js → index-C6CBgMuG.js} +3 -3
- package/cli/storybook-dist/assets/{preview-C7DaSWyc.js → preview-B-iYLDuW.js} +2 -2
- package/cli/storybook-dist/assets/{preview-CbuRHit4.js → preview-CRWqc9DH.js} +1 -1
- package/cli/storybook-dist/assets/{react-resizable-panels.browser-ChIvlwo7.js → react-resizable-panels.browser-8hC8dr-6.js} +8 -8
- package/cli/storybook-dist/iframe.html +2 -2
- package/cli/storybook-dist/index.html +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/.mcp.json +8 -8
- package/cli/templates/common/README.md +1231 -1231
- package/cli/templates/common/assets/icon.svg +45 -45
- package/cli/templates/common/manifest.json +26 -26
- package/cli/templates/common/package.json +22 -22
- package/cli/templates/common/py/main.py +125 -125
- package/cli/templates/common/tsconfig.json +13 -13
- package/cli/templates/common/ui/content.tsx +216 -216
- package/cli/templates/common/ui/style.css +6 -6
- package/cli/templates/common/vite.config.ts +10 -10
- package/cli/templates/cpp/cpp/main.cpp +73 -73
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +25 -25
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/package.xml +10 -10
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/setup.py +18 -18
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -11
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +23 -23
- package/cli/vite.d.mts +25 -25
- package/cli/vite.mjs +292 -292
- package/package.json +3 -3
- package/cli/storybook-dist/assets/RBCommand.stories-BSqXDkKw.js +0 -73
- package/cli/storybook-dist/assets/RBDialog.stories-BAktz-ku.js +0 -38
- package/cli/storybook-dist/assets/RBDropdownMenu.stories-DbW2fWAj.js +0 -86
- package/cli/storybook-dist/assets/RBSheet.stories-_6VH-RUm.js +0 -39
package/cli/src/muscatLocal.mjs
CHANGED
|
@@ -1,209 +1,209 @@
|
|
|
1
|
-
// Local Muscat runtime for `dev` / `dev:ui`, with or without a robot. It is the plugin gateway
|
|
2
|
-
// (:10000, plugin-socketio — same Socket.IO protocol as on the robot), MediaMTX, and `common`:
|
|
3
|
-
// preview-host hits http://localhost:3000/common/* (baked into the nexus bundle), so program
|
|
4
|
-
// preview works with no front-end change. The backend container joins its netns. We reuse any
|
|
5
|
-
// Muscat already publishing :10000 (e.g. a `muscat` / `muscat-dev` container) and only otherwise
|
|
6
|
-
// start our own: the S3 runtime image with just the `common` service.
|
|
7
|
-
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { createServer } from "node:net";
|
|
9
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { homedir } from "node:os";
|
|
11
|
-
import { resolve } from "node:path";
|
|
12
|
-
import { mediaPortFlags, MEDIA_PORTS, publishPortFlags } from "./ports.mjs";
|
|
13
|
-
import { canSkipImageBuild, hasBin, log, buildCacheFlags } from "./util.mjs";
|
|
14
|
-
import { LOCAL_HOSTS } from "./zenohDev.mjs";
|
|
15
|
-
|
|
16
|
-
export const MUSCAT_CONTAINER = "muscat-pk-muscat";
|
|
17
|
-
// Ports published for the backend (it shares this container's netns); a different set recreates it.
|
|
18
|
-
const PORTS_LABEL = "com.rainbow-robotics.muscat-pk.ports";
|
|
19
|
-
const IMAGE = "muscat-pk-runtime";
|
|
20
|
-
const S3 = process.env.MUSCAT_PK_MUSCAT_S3 ?? "https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat/main";
|
|
21
|
-
const CACHE = resolve(homedir(), ".muscat-pk", "muscat");
|
|
22
|
-
const API_PORT = 3000;
|
|
23
|
-
export const GATEWAY_PORT = 10000;
|
|
24
|
-
// The bin runs inside Docker, so follow the Docker engine's arch, not this Node's
|
|
25
|
-
// (Rosetta/x64 Node on an Apple Silicon host would otherwise pick amd64).
|
|
26
|
-
const dockerArch = () => {
|
|
27
|
-
const r = spawnSync("docker", ["version", "--format", "{{.Server.Arch}}"], { encoding: "utf8" });
|
|
28
|
-
const a = (r.stdout || "").trim();
|
|
29
|
-
return a === "arm64" || a === "amd64" ? a : process.arch === "arm64" ? "arm64" : "amd64";
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
async function fetchBuffer(url) {
|
|
33
|
-
const res = await fetch(url);
|
|
34
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
|
|
35
|
-
return Buffer.from(await res.arrayBuffer());
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// macOS/Windows `tar` reads zip; GNU tar does not, Linux has `unzip`.
|
|
39
|
-
function extractZip(zip, dir, members = []) {
|
|
40
|
-
mkdirSync(dir, { recursive: true });
|
|
41
|
-
const tries = [
|
|
42
|
-
["unzip", ["-o", "-q", zip, ...members, "-d", dir]],
|
|
43
|
-
["tar", ["-xf", zip, "-C", dir, ...members]],
|
|
44
|
-
];
|
|
45
|
-
for (const [bin, args] of tries) {
|
|
46
|
-
if (hasBin(bin) && spawnSync(bin, args, { stdio: "ignore" }).status === 0) return;
|
|
47
|
-
}
|
|
48
|
-
throw new Error(`could not extract ${zip} (needs unzip or tar)`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function resolveVersions() {
|
|
52
|
-
const file = resolve(CACHE, "version.json");
|
|
53
|
-
try {
|
|
54
|
-
const data = await fetchBuffer(`${S3}/version.json`);
|
|
55
|
-
mkdirSync(CACHE, { recursive: true });
|
|
56
|
-
writeFileSync(file, data);
|
|
57
|
-
return JSON.parse(data.toString());
|
|
58
|
-
} catch (e) {
|
|
59
|
-
if (!existsSync(file)) throw e;
|
|
60
|
-
log.warn(`version.json unreachable (${e.message}) — using cached Muscat version`);
|
|
61
|
-
return JSON.parse(readFileSync(file, "utf8"));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function ensureRuntimeImage(total) {
|
|
66
|
-
const image = `${IMAGE}:${total}`;
|
|
67
|
-
if (canSkipImageBuild(image)) return image;
|
|
68
|
-
const dir = resolve(CACHE, "runtime", total);
|
|
69
|
-
if (!existsSync(resolve(dir, "Dockerfile.runtime"))) {
|
|
70
|
-
log.step(`downloading Muscat runtime scripts (muscat.zip ${total})…`);
|
|
71
|
-
const zip = resolve(CACHE, "muscat.zip");
|
|
72
|
-
writeFileSync(zip, await fetchBuffer(`${S3}/muscat.zip`));
|
|
73
|
-
extractZip(zip, dir);
|
|
74
|
-
rmSync(zip, { force: true });
|
|
75
|
-
}
|
|
76
|
-
log.step(`docker build -t ${image} (first run downloads MongoDB/InfluxDB/ROS base — slow, then cached)…`);
|
|
77
|
-
const r = spawnSync(
|
|
78
|
-
"docker",
|
|
79
|
-
["build", ...buildCacheFlags(), "-t", image, "-f", resolve(dir, "Dockerfile.runtime"), "--label", "com.rainbow-robotics.muscat-pk=true", dir],
|
|
80
|
-
{ stdio: "inherit" },
|
|
81
|
-
);
|
|
82
|
-
if (r.status !== 0) throw new Error("docker build failed (Muscat runtime image).");
|
|
83
|
-
return image;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function ensureCommonBinary(version) {
|
|
87
|
-
const ARCH = dockerArch();
|
|
88
|
-
const dir = resolve(CACHE, "services", "common");
|
|
89
|
-
const stamp = resolve(dir, ".version");
|
|
90
|
-
if (existsSync(stamp) && readFileSync(stamp, "utf8").trim() === version && existsSync(resolve(dir, `common.${ARCH}.bin`))) return dir;
|
|
91
|
-
log.step(`downloading Muscat common service ${version} (${ARCH})…`);
|
|
92
|
-
const zip = resolve(CACHE, `common-${version}.zip`);
|
|
93
|
-
writeFileSync(zip, await fetchBuffer(`${S3}/common/${version}.zip`));
|
|
94
|
-
extractZip(zip, dir, ["config.env", `common.${ARCH}.bin`]);
|
|
95
|
-
rmSync(zip, { force: true });
|
|
96
|
-
chmodSync(resolve(dir, `common.${ARCH}.bin`), 0o755);
|
|
97
|
-
writeFileSync(stamp, `${version}\n`);
|
|
98
|
-
return dir;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function waitForApi(timeoutMs = 180_000) {
|
|
102
|
-
const start = Date.now();
|
|
103
|
-
for (;;) {
|
|
104
|
-
try {
|
|
105
|
-
const res = await fetch(`http://127.0.0.1:${API_PORT}/common/docs`);
|
|
106
|
-
if (res.ok) return;
|
|
107
|
-
} catch { /* not listening yet */ }
|
|
108
|
-
if (Date.now() - start > timeoutMs) throw new Error(`Muscat did not answer on :${API_PORT} in time — see \`docker logs ${MUSCAT_CONTAINER}\``);
|
|
109
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Does the robot at `ip` run Muscat (common answers /is-muscat)? Then the dev UI and backend use its
|
|
114
|
-
// gateway and no local Muscat is started. Local addresses and unreachable hosts → false.
|
|
115
|
-
export async function robotHasMuscat(ip) {
|
|
116
|
-
if (!ip || LOCAL_HOSTS.has(ip)) return false;
|
|
117
|
-
try {
|
|
118
|
-
const res = await fetch(`http://${ip}:${API_PORT}/common/is-muscat`, { signal: AbortSignal.timeout(3000) });
|
|
119
|
-
return res.ok && (await res.json())?.result === true;
|
|
120
|
-
} catch {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Is a host TCP port already bound?
|
|
126
|
-
export function isPortInUse(port) {
|
|
127
|
-
return new Promise((res) => {
|
|
128
|
-
const srv = createServer();
|
|
129
|
-
srv.once("error", (err) => res(err.code === "EADDRINUSE"));
|
|
130
|
-
srv.once("listening", () => srv.close(() => res(false)));
|
|
131
|
-
srv.listen(port, "0.0.0.0");
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** A running Muscat container (ours or e.g. muscat-dev) that owns host port 10000, or null. */
|
|
136
|
-
export function localMuscatContainer() {
|
|
137
|
-
const r = spawnSync("docker", ["ps", "--filter", `publish=${GATEWAY_PORT}`, "--format", "{{.Names}}"], { encoding: "utf8" });
|
|
138
|
-
return (r.stdout || "").split("\n").map((n) => n.trim()).find(Boolean) ?? null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const containerLabel = (name, label) =>
|
|
142
|
-
spawnSync("docker", ["inspect", "-f", `{{index .Config.Labels "${label}"}}`, name], { encoding: "utf8" }).stdout?.trim() ?? "";
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Make sure a Muscat answers on localhost:3000 / :10000. Returns the container name,
|
|
146
|
-
* or null when one can't be started (no Docker, foreign process on the ports).
|
|
147
|
-
* The container is left running across runs — stop it with `docker rm -f muscat-pk-muscat`.
|
|
148
|
-
*/
|
|
149
|
-
export async function ensureLocalMuscat({ pluginPorts = [], zenohPort = null } = {}) {
|
|
150
|
-
if (process.env.MUSCAT_PK_NO_LOCAL_MUSCAT === "1") return null;
|
|
151
|
-
const portsKey = [...pluginPorts, ...(zenohPort ? [`zenoh:${zenohPort}`] : [])].join(",");
|
|
152
|
-
const existing = hasBin("docker") ? localMuscatContainer() : null;
|
|
153
|
-
if (existing === MUSCAT_CONTAINER && containerLabel(existing, PORTS_LABEL) !== portsKey) {
|
|
154
|
-
log.step(`recreating local Muscat to publish ${portsKey || "no extra"} port(s)`);
|
|
155
|
-
spawnSync("docker", ["rm", "-f", MUSCAT_CONTAINER], { stdio: "ignore" });
|
|
156
|
-
} else if (existing) {
|
|
157
|
-
if (existing !== MUSCAT_CONTAINER && portsKey)
|
|
158
|
-
log.warn(`'${existing}' is not managed by muscat-pk — port(s) ${portsKey} are not published from it.`);
|
|
159
|
-
return existing;
|
|
160
|
-
}
|
|
161
|
-
if (!hasBin("docker")) {
|
|
162
|
-
log.warn("Docker not found — no local Muscat; program preview will have no back end.");
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
// Ports taken but no container publishing them: usually a Muscat on host networking (robot PC,
|
|
166
|
-
// CI runner) — the caller uses it as is.
|
|
167
|
-
for (const port of [API_PORT, GATEWAY_PORT]) {
|
|
168
|
-
if (await isPortInUse(port)) return null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const versions = await resolveVersions();
|
|
172
|
-
const total = versions.total;
|
|
173
|
-
const commonVersion = versions.services?.common?.version;
|
|
174
|
-
if (!total || !commonVersion) throw new Error("version.json has no total / services.common.version");
|
|
175
|
-
|
|
176
|
-
const image = await ensureRuntimeImage(total);
|
|
177
|
-
const commonDir = await ensureCommonBinary(commonVersion);
|
|
178
|
-
|
|
179
|
-
let media = true;
|
|
180
|
-
for (const port of MEDIA_PORTS) if (await isPortInUse(port)) media = false;
|
|
181
|
-
// A busy plugin port would make `docker run` fail with a raw daemon error — skip it and say so.
|
|
182
|
-
const busy = [];
|
|
183
|
-
for (const port of pluginPorts) if (await isPortInUse(port)) busy.push(port);
|
|
184
|
-
if (busy.length) log.warn(`plugin port(s) already in use on the host: ${busy.join(", ")} — not published (the backend still listens inside).`);
|
|
185
|
-
const published = pluginPorts.filter((port) => !busy.includes(port));
|
|
186
|
-
|
|
187
|
-
spawnSync("docker", ["rm", "-f", MUSCAT_CONTAINER], { stdio: "ignore" });
|
|
188
|
-
log.step(`starting local Muscat (common ${commonVersion}) → http://localhost:${API_PORT}/common/docs`);
|
|
189
|
-
const r = spawnSync("docker", [
|
|
190
|
-
"run", "-d", "--name", MUSCAT_CONTAINER, "--label", "com.rainbow-robotics.muscat-pk=true", "--label", `${PORTS_LABEL}=${portsKey}`,
|
|
191
|
-
"-e", "RB_ENABLED_SERVICES=common",
|
|
192
|
-
"-e", "RB_RUNTIME_ENABLE_ZENOH_BRIDGE=0",
|
|
193
|
-
"-v", `${commonDir}:/app/services/common`,
|
|
194
|
-
"-v", `${resolve(CACHE, "data")}:/app/data`,
|
|
195
|
-
"-p", `127.0.0.1:${API_PORT}:${API_PORT}`,
|
|
196
|
-
"-p", `127.0.0.1:${GATEWAY_PORT}:${GATEWAY_PORT}`,
|
|
197
|
-
...(media ? mediaPortFlags() : []),
|
|
198
|
-
...publishPortFlags(published),
|
|
199
|
-
// The robot reverse-dials the backend's Zenoh session from the LAN, so not loopback-only.
|
|
200
|
-
...(zenohPort ? ["-p", `${zenohPort}:${zenohPort}`] : []),
|
|
201
|
-
image,
|
|
202
|
-
], { stdio: ["ignore", "ignore", "inherit"] });
|
|
203
|
-
if (r.status !== 0) throw new Error("failed to start the local Muscat container.");
|
|
204
|
-
await waitForApi();
|
|
205
|
-
log.step(`local Muscat ready — keeps running between runs; stop with: docker rm -f ${MUSCAT_CONTAINER}`);
|
|
206
|
-
if (published.length) log.step(`plugin backend ports published on 127.0.0.1: ${published.join(", ")}`);
|
|
207
|
-
if (zenohPort) log.step(`Zenoh dev peer port published on :${zenohPort}`);
|
|
208
|
-
return MUSCAT_CONTAINER;
|
|
209
|
-
}
|
|
1
|
+
// Local Muscat runtime for `dev` / `dev:ui`, with or without a robot. It is the plugin gateway
|
|
2
|
+
// (:10000, plugin-socketio — same Socket.IO protocol as on the robot), MediaMTX, and `common`:
|
|
3
|
+
// preview-host hits http://localhost:3000/common/* (baked into the nexus bundle), so program
|
|
4
|
+
// preview works with no front-end change. The backend container joins its netns. We reuse any
|
|
5
|
+
// Muscat already publishing :10000 (e.g. a `muscat` / `muscat-dev` container) and only otherwise
|
|
6
|
+
// start our own: the S3 runtime image with just the `common` service.
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { createServer } from "node:net";
|
|
9
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { mediaPortFlags, MEDIA_PORTS, publishPortFlags } from "./ports.mjs";
|
|
13
|
+
import { canSkipImageBuild, hasBin, log, buildCacheFlags } from "./util.mjs";
|
|
14
|
+
import { LOCAL_HOSTS } from "./zenohDev.mjs";
|
|
15
|
+
|
|
16
|
+
export const MUSCAT_CONTAINER = "muscat-pk-muscat";
|
|
17
|
+
// Ports published for the backend (it shares this container's netns); a different set recreates it.
|
|
18
|
+
const PORTS_LABEL = "com.rainbow-robotics.muscat-pk.ports";
|
|
19
|
+
const IMAGE = "muscat-pk-runtime";
|
|
20
|
+
const S3 = process.env.MUSCAT_PK_MUSCAT_S3 ?? "https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat/main";
|
|
21
|
+
const CACHE = resolve(homedir(), ".muscat-pk", "muscat");
|
|
22
|
+
const API_PORT = 3000;
|
|
23
|
+
export const GATEWAY_PORT = 10000;
|
|
24
|
+
// The bin runs inside Docker, so follow the Docker engine's arch, not this Node's
|
|
25
|
+
// (Rosetta/x64 Node on an Apple Silicon host would otherwise pick amd64).
|
|
26
|
+
const dockerArch = () => {
|
|
27
|
+
const r = spawnSync("docker", ["version", "--format", "{{.Server.Arch}}"], { encoding: "utf8" });
|
|
28
|
+
const a = (r.stdout || "").trim();
|
|
29
|
+
return a === "arm64" || a === "amd64" ? a : process.arch === "arm64" ? "arm64" : "amd64";
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
async function fetchBuffer(url) {
|
|
33
|
+
const res = await fetch(url);
|
|
34
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
|
|
35
|
+
return Buffer.from(await res.arrayBuffer());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// macOS/Windows `tar` reads zip; GNU tar does not, Linux has `unzip`.
|
|
39
|
+
function extractZip(zip, dir, members = []) {
|
|
40
|
+
mkdirSync(dir, { recursive: true });
|
|
41
|
+
const tries = [
|
|
42
|
+
["unzip", ["-o", "-q", zip, ...members, "-d", dir]],
|
|
43
|
+
["tar", ["-xf", zip, "-C", dir, ...members]],
|
|
44
|
+
];
|
|
45
|
+
for (const [bin, args] of tries) {
|
|
46
|
+
if (hasBin(bin) && spawnSync(bin, args, { stdio: "ignore" }).status === 0) return;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`could not extract ${zip} (needs unzip or tar)`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function resolveVersions() {
|
|
52
|
+
const file = resolve(CACHE, "version.json");
|
|
53
|
+
try {
|
|
54
|
+
const data = await fetchBuffer(`${S3}/version.json`);
|
|
55
|
+
mkdirSync(CACHE, { recursive: true });
|
|
56
|
+
writeFileSync(file, data);
|
|
57
|
+
return JSON.parse(data.toString());
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (!existsSync(file)) throw e;
|
|
60
|
+
log.warn(`version.json unreachable (${e.message}) — using cached Muscat version`);
|
|
61
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function ensureRuntimeImage(total) {
|
|
66
|
+
const image = `${IMAGE}:${total}`;
|
|
67
|
+
if (canSkipImageBuild(image)) return image;
|
|
68
|
+
const dir = resolve(CACHE, "runtime", total);
|
|
69
|
+
if (!existsSync(resolve(dir, "Dockerfile.runtime"))) {
|
|
70
|
+
log.step(`downloading Muscat runtime scripts (muscat.zip ${total})…`);
|
|
71
|
+
const zip = resolve(CACHE, "muscat.zip");
|
|
72
|
+
writeFileSync(zip, await fetchBuffer(`${S3}/muscat.zip`));
|
|
73
|
+
extractZip(zip, dir);
|
|
74
|
+
rmSync(zip, { force: true });
|
|
75
|
+
}
|
|
76
|
+
log.step(`docker build -t ${image} (first run downloads MongoDB/InfluxDB/ROS base — slow, then cached)…`);
|
|
77
|
+
const r = spawnSync(
|
|
78
|
+
"docker",
|
|
79
|
+
["build", ...buildCacheFlags(), "-t", image, "-f", resolve(dir, "Dockerfile.runtime"), "--label", "com.rainbow-robotics.muscat-pk=true", dir],
|
|
80
|
+
{ stdio: "inherit" },
|
|
81
|
+
);
|
|
82
|
+
if (r.status !== 0) throw new Error("docker build failed (Muscat runtime image).");
|
|
83
|
+
return image;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function ensureCommonBinary(version) {
|
|
87
|
+
const ARCH = dockerArch();
|
|
88
|
+
const dir = resolve(CACHE, "services", "common");
|
|
89
|
+
const stamp = resolve(dir, ".version");
|
|
90
|
+
if (existsSync(stamp) && readFileSync(stamp, "utf8").trim() === version && existsSync(resolve(dir, `common.${ARCH}.bin`))) return dir;
|
|
91
|
+
log.step(`downloading Muscat common service ${version} (${ARCH})…`);
|
|
92
|
+
const zip = resolve(CACHE, `common-${version}.zip`);
|
|
93
|
+
writeFileSync(zip, await fetchBuffer(`${S3}/common/${version}.zip`));
|
|
94
|
+
extractZip(zip, dir, ["config.env", `common.${ARCH}.bin`]);
|
|
95
|
+
rmSync(zip, { force: true });
|
|
96
|
+
chmodSync(resolve(dir, `common.${ARCH}.bin`), 0o755);
|
|
97
|
+
writeFileSync(stamp, `${version}\n`);
|
|
98
|
+
return dir;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function waitForApi(timeoutMs = 180_000) {
|
|
102
|
+
const start = Date.now();
|
|
103
|
+
for (;;) {
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch(`http://127.0.0.1:${API_PORT}/common/docs`);
|
|
106
|
+
if (res.ok) return;
|
|
107
|
+
} catch { /* not listening yet */ }
|
|
108
|
+
if (Date.now() - start > timeoutMs) throw new Error(`Muscat did not answer on :${API_PORT} in time — see \`docker logs ${MUSCAT_CONTAINER}\``);
|
|
109
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Does the robot at `ip` run Muscat (common answers /is-muscat)? Then the dev UI and backend use its
|
|
114
|
+
// gateway and no local Muscat is started. Local addresses and unreachable hosts → false.
|
|
115
|
+
export async function robotHasMuscat(ip) {
|
|
116
|
+
if (!ip || LOCAL_HOSTS.has(ip)) return false;
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetch(`http://${ip}:${API_PORT}/common/is-muscat`, { signal: AbortSignal.timeout(3000) });
|
|
119
|
+
return res.ok && (await res.json())?.result === true;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Is a host TCP port already bound?
|
|
126
|
+
export function isPortInUse(port) {
|
|
127
|
+
return new Promise((res) => {
|
|
128
|
+
const srv = createServer();
|
|
129
|
+
srv.once("error", (err) => res(err.code === "EADDRINUSE"));
|
|
130
|
+
srv.once("listening", () => srv.close(() => res(false)));
|
|
131
|
+
srv.listen(port, "0.0.0.0");
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A running Muscat container (ours or e.g. muscat-dev) that owns host port 10000, or null. */
|
|
136
|
+
export function localMuscatContainer() {
|
|
137
|
+
const r = spawnSync("docker", ["ps", "--filter", `publish=${GATEWAY_PORT}`, "--format", "{{.Names}}"], { encoding: "utf8" });
|
|
138
|
+
return (r.stdout || "").split("\n").map((n) => n.trim()).find(Boolean) ?? null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const containerLabel = (name, label) =>
|
|
142
|
+
spawnSync("docker", ["inspect", "-f", `{{index .Config.Labels "${label}"}}`, name], { encoding: "utf8" }).stdout?.trim() ?? "";
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Make sure a Muscat answers on localhost:3000 / :10000. Returns the container name,
|
|
146
|
+
* or null when one can't be started (no Docker, foreign process on the ports).
|
|
147
|
+
* The container is left running across runs — stop it with `docker rm -f muscat-pk-muscat`.
|
|
148
|
+
*/
|
|
149
|
+
export async function ensureLocalMuscat({ pluginPorts = [], zenohPort = null } = {}) {
|
|
150
|
+
if (process.env.MUSCAT_PK_NO_LOCAL_MUSCAT === "1") return null;
|
|
151
|
+
const portsKey = [...pluginPorts, ...(zenohPort ? [`zenoh:${zenohPort}`] : [])].join(",");
|
|
152
|
+
const existing = hasBin("docker") ? localMuscatContainer() : null;
|
|
153
|
+
if (existing === MUSCAT_CONTAINER && containerLabel(existing, PORTS_LABEL) !== portsKey) {
|
|
154
|
+
log.step(`recreating local Muscat to publish ${portsKey || "no extra"} port(s)`);
|
|
155
|
+
spawnSync("docker", ["rm", "-f", MUSCAT_CONTAINER], { stdio: "ignore" });
|
|
156
|
+
} else if (existing) {
|
|
157
|
+
if (existing !== MUSCAT_CONTAINER && portsKey)
|
|
158
|
+
log.warn(`'${existing}' is not managed by muscat-pk — port(s) ${portsKey} are not published from it.`);
|
|
159
|
+
return existing;
|
|
160
|
+
}
|
|
161
|
+
if (!hasBin("docker")) {
|
|
162
|
+
log.warn("Docker not found — no local Muscat; program preview will have no back end.");
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
// Ports taken but no container publishing them: usually a Muscat on host networking (robot PC,
|
|
166
|
+
// CI runner) — the caller uses it as is.
|
|
167
|
+
for (const port of [API_PORT, GATEWAY_PORT]) {
|
|
168
|
+
if (await isPortInUse(port)) return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const versions = await resolveVersions();
|
|
172
|
+
const total = versions.total;
|
|
173
|
+
const commonVersion = versions.services?.common?.version;
|
|
174
|
+
if (!total || !commonVersion) throw new Error("version.json has no total / services.common.version");
|
|
175
|
+
|
|
176
|
+
const image = await ensureRuntimeImage(total);
|
|
177
|
+
const commonDir = await ensureCommonBinary(commonVersion);
|
|
178
|
+
|
|
179
|
+
let media = true;
|
|
180
|
+
for (const port of MEDIA_PORTS) if (await isPortInUse(port)) media = false;
|
|
181
|
+
// A busy plugin port would make `docker run` fail with a raw daemon error — skip it and say so.
|
|
182
|
+
const busy = [];
|
|
183
|
+
for (const port of pluginPorts) if (await isPortInUse(port)) busy.push(port);
|
|
184
|
+
if (busy.length) log.warn(`plugin port(s) already in use on the host: ${busy.join(", ")} — not published (the backend still listens inside).`);
|
|
185
|
+
const published = pluginPorts.filter((port) => !busy.includes(port));
|
|
186
|
+
|
|
187
|
+
spawnSync("docker", ["rm", "-f", MUSCAT_CONTAINER], { stdio: "ignore" });
|
|
188
|
+
log.step(`starting local Muscat (common ${commonVersion}) → http://localhost:${API_PORT}/common/docs`);
|
|
189
|
+
const r = spawnSync("docker", [
|
|
190
|
+
"run", "-d", "--name", MUSCAT_CONTAINER, "--label", "com.rainbow-robotics.muscat-pk=true", "--label", `${PORTS_LABEL}=${portsKey}`,
|
|
191
|
+
"-e", "RB_ENABLED_SERVICES=common",
|
|
192
|
+
"-e", "RB_RUNTIME_ENABLE_ZENOH_BRIDGE=0",
|
|
193
|
+
"-v", `${commonDir}:/app/services/common`,
|
|
194
|
+
"-v", `${resolve(CACHE, "data")}:/app/data`,
|
|
195
|
+
"-p", `127.0.0.1:${API_PORT}:${API_PORT}`,
|
|
196
|
+
"-p", `127.0.0.1:${GATEWAY_PORT}:${GATEWAY_PORT}`,
|
|
197
|
+
...(media ? mediaPortFlags() : []),
|
|
198
|
+
...publishPortFlags(published),
|
|
199
|
+
// The robot reverse-dials the backend's Zenoh session from the LAN, so not loopback-only.
|
|
200
|
+
...(zenohPort ? ["-p", `${zenohPort}:${zenohPort}`] : []),
|
|
201
|
+
image,
|
|
202
|
+
], { stdio: ["ignore", "ignore", "inherit"] });
|
|
203
|
+
if (r.status !== 0) throw new Error("failed to start the local Muscat container.");
|
|
204
|
+
await waitForApi();
|
|
205
|
+
log.step(`local Muscat ready — keeps running between runs; stop with: docker rm -f ${MUSCAT_CONTAINER}`);
|
|
206
|
+
if (published.length) log.step(`plugin backend ports published on 127.0.0.1: ${published.join(", ")}`);
|
|
207
|
+
if (zenohPort) log.step(`Zenoh dev peer port published on :${zenohPort}`);
|
|
208
|
+
return MUSCAT_CONTAINER;
|
|
209
|
+
}
|