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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +6 -6
  2. package/cli/README.md +11 -5
  3. package/cli/bin/muscat-pk.mjs +1 -1
  4. package/cli/bundle-templates.mjs +0 -2
  5. package/cli/docker/Dockerfile.cpp +6 -22
  6. package/cli/docker/Dockerfile.dev +6 -22
  7. package/cli/docker/Dockerfile.python +0 -18
  8. package/cli/src/build.mjs +5 -5
  9. package/cli/src/completion.mjs +1 -1
  10. package/cli/src/cpp.mjs +11 -3
  11. package/cli/src/cppIde.mjs +75 -0
  12. package/cli/src/dev.mjs +19 -26
  13. package/cli/src/devBack.mjs +29 -22
  14. package/cli/src/devUi.mjs +10 -1
  15. package/cli/src/muscatLocal.mjs +198 -0
  16. package/cli/src/new.mjs +15 -0
  17. package/cli/src/ports.mjs +1 -1
  18. package/cli/src/ros2.mjs +11 -3
  19. package/cli/src/sdk.mjs +48 -2
  20. package/cli/src/type.mjs +2 -0
  21. package/cli/src/util.mjs +46 -3
  22. package/cli/storybook-dist/assets/{Color-YHDXOIA2-CdTgUqrV.js → Color-YHDXOIA2-DeirXVfl.js} +1 -1
  23. package/cli/storybook-dist/assets/{Docs-CbRKwHpQ.js → Docs-BLxyi8cP.js} +1 -1
  24. package/cli/storybook-dist/assets/{Docs-CsxMprX5.js → Docs-Buyf7Djy.js} +1 -1
  25. package/cli/storybook-dist/assets/{Docs-FyPFrbUS.js → Docs-BxmwdaYa.js} +1 -1
  26. package/cli/storybook-dist/assets/{Docs-C8wx5zsC.js → Docs-CnfIOwf8.js} +1 -1
  27. package/cli/storybook-dist/assets/{Docs-Cf0s04QW.js → Docs-NrBqEYvM.js} +1 -1
  28. package/cli/storybook-dist/assets/{Docs-YtqU-aVu.js → Docs-wzFMK-XA.js} +1 -1
  29. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-VSENNRoY.js → DocsRenderer-CFRXHY34-CWc43rTP.js} +1 -1
  30. package/cli/storybook-dist/assets/{NxAvatar.stories-oCcYtLwq.js → NxAvatar.stories-DfL0kqmN.js} +1 -1
  31. package/cli/storybook-dist/assets/{NxButton.stories-C5jUScIj.js → NxButton.stories-WxSc_gsR.js} +1 -1
  32. package/cli/storybook-dist/assets/{NxNumberInput.stories-gjsEulIN.js → NxNumberInput.stories-C1D3oUiY.js} +1 -1
  33. package/cli/storybook-dist/assets/{NxPagination.stories-BK6uSWIa.js → NxPagination.stories-C1Qa88rn.js} +1 -1
  34. package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BDD5CvSZ.js → NxPointerHoldButton.stories-CmteJX3t.js} +1 -1
  35. package/cli/storybook-dist/assets/{NxRangeInput.stories-C_nW26_x.js → NxRangeInput.stories-B7E-mhyI.js} +1 -1
  36. package/cli/storybook-dist/assets/{NxSelectBox.stories-DqLfKU9H.js → NxSelectBox.stories-De67vMdU.js} +1 -1
  37. package/cli/storybook-dist/assets/{NxSlideRange.stories-CQjHA8yv.js → NxSlideRange.stories-DvCV1MGk.js} +1 -1
  38. package/cli/storybook-dist/assets/{NxSwitch.stories-BMHCiPoL.js → NxSwitch.stories-DuHDZ9-v.js} +1 -1
  39. package/cli/storybook-dist/assets/{NxTextArea.stories-k7r1WVQh.js → NxTextArea.stories-BT1o0ngg.js} +1 -1
  40. package/cli/storybook-dist/assets/{NxTextInput.stories-Cqfe1I32.js → NxTextInput.stories-CSLBT83Q.js} +1 -1
  41. package/cli/storybook-dist/assets/{RBAccordion.stories-CJ3Ttuja.js → RBAccordion.stories-QavkYgV0.js} +1 -1
  42. package/cli/storybook-dist/assets/{RBAccordionGroup.stories-BThv7tM0.js → RBAccordionGroup.stories-C0RPGnO7.js} +1 -1
  43. package/cli/storybook-dist/assets/{RBAlert.stories-CSMP8Vun.js → RBAlert.stories-BUCAT0Sd.js} +1 -1
  44. package/cli/storybook-dist/assets/{RBBadge.stories-cBMA2R6F.js → RBBadge.stories-CnBXTQ4J.js} +1 -1
  45. package/cli/storybook-dist/assets/{RBBattery.stories-BKVOskqs.js → RBBattery.stories-5FbsZcdy.js} +1 -1
  46. package/cli/storybook-dist/assets/{RBCard.stories-BTOfK0HC.js → RBCard.stories-FwTKjtCi.js} +1 -1
  47. package/cli/storybook-dist/assets/{RBCheckBox.stories-DyE912wG.js → RBCheckBox.stories-C0y5psMI.js} +1 -1
  48. package/cli/storybook-dist/assets/{RBCheckIconButton.stories-0sLFU0kM.js → RBCheckIconButton.stories-sJjAZBVh.js} +1 -1
  49. package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-c8VecClE.js → RBCheckLabelButton.stories-COJqEkDr.js} +1 -1
  50. package/cli/storybook-dist/assets/{RBChipTab.stories-CQ9Uav5_.js → RBChipTab.stories-BnXj-Woa.js} +1 -1
  51. package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-Ap4NlhAZ.js → RBChipsBoxTab.stories-DE1yl3rz.js} +1 -1
  52. package/cli/storybook-dist/assets/{RBColorIcons.stories-BPPXjif2.js → RBColorIcons.stories-CYASERLQ.js} +1 -1
  53. package/cli/storybook-dist/assets/{RBCommand.stories-PDpYOKlm.js → RBCommand.stories-BYkWjdMG.js} +1 -1
  54. package/cli/storybook-dist/assets/{RBConfirm.stories-BEWvMAYP.js → RBConfirm.stories-XLLTJZRf.js} +1 -1
  55. package/cli/storybook-dist/assets/{RBDateInput.stories-DLdyNnVF.js → RBDateInput.stories-DpVylrQl.js} +1 -1
  56. package/cli/storybook-dist/assets/{RBDialog.stories-HsawMNOb.js → RBDialog.stories-DcPRAan8.js} +1 -1
  57. package/cli/storybook-dist/assets/{RBDivider.stories-dlWwOxik.js → RBDivider.stories-NOTo-jJG.js} +1 -1
  58. package/cli/storybook-dist/assets/{RBDrawer.stories-C87iqGk0.js → RBDrawer.stories-CUZaQyw8.js} +1 -1
  59. package/cli/storybook-dist/assets/{RBDropdownMenu.stories-B8EQhA0z.js → RBDropdownMenu.stories-5UC0ZA3H.js} +1 -1
  60. package/cli/storybook-dist/assets/{RBEmptyView.stories-SzpIhNDw.js → RBEmptyView.stories-BIRNPjwt.js} +1 -1
  61. package/cli/storybook-dist/assets/{RBIconButton.stories-BQ0nM1gb.js → RBIconButton.stories-CJWeIQTt.js} +1 -1
  62. package/cli/storybook-dist/assets/{RBIcons.stories-C19g5M5z.js → RBIcons.stories-Cu0txCUp.js} +1 -1
  63. package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-DbgI4-1P.js → RBInlineLabelButton.stories-DnGhYrxa.js} +1 -1
  64. package/cli/storybook-dist/assets/{RBJoyStick.stories-smF-vpGL.js → RBJoyStick.stories-BlfvKcGb.js} +1 -1
  65. package/cli/storybook-dist/assets/{RBLabelButton.stories-s53klwDv.js → RBLabelButton.stories-CFwmlhAE.js} +1 -1
  66. package/cli/storybook-dist/assets/{RBLight.stories-BnzkoR51.js → RBLight.stories-wMrFLJ7u.js} +1 -1
  67. package/cli/storybook-dist/assets/{RBMenubar.stories-CW014klu.js → RBMenubar.stories-Bu5wtV-T.js} +1 -1
  68. package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-qa4tH_4Y.js → RBMultiRangeInput.stories-Db1d940z.js} +1 -1
  69. package/cli/storybook-dist/assets/{RBPagination.stories-YoQNUpQH.js → RBPagination.stories-NCpjg-2W.js} +1 -1
  70. package/cli/storybook-dist/assets/{RBPopover.stories-Bhpi0OaH.js → RBPopover.stories-CqsM8byU.js} +1 -1
  71. package/cli/storybook-dist/assets/{RBPrompt.stories-iQM5iAka.js → RBPrompt.stories-TMQEaHk-.js} +1 -1
  72. package/cli/storybook-dist/assets/{RBRadioBox.stories-BGyq4rzx.js → RBRadioBox.stories-pnsBvtzU.js} +1 -1
  73. package/cli/storybook-dist/assets/{RBRadioButton.stories-DF6BcUcJ.js → RBRadioButton.stories-BMDBfq82.js} +1 -1
  74. package/cli/storybook-dist/assets/{RBRangeDateInput.stories-COOQ_HNF.js → RBRangeDateInput.stories-ljAsdz0Q.js} +1 -1
  75. package/cli/storybook-dist/assets/{RBResizable.stories-DSVHvwjp.js → RBResizable.stories-B5YDW687.js} +1 -1
  76. package/cli/storybook-dist/assets/{RBSelectBox.stories-BookV8Sm.js → RBSelectBox.stories-DPtyIwEr.js} +1 -1
  77. package/cli/storybook-dist/assets/{RBSheet.stories-CAK9hb-p.js → RBSheet.stories-DMTeF_hx.js} +1 -1
  78. package/cli/storybook-dist/assets/{RBSideNumberInput.stories-DUymnH-9.js → RBSideNumberInput.stories-DLsS3mzL.js} +1 -1
  79. package/cli/storybook-dist/assets/{RBSlider.stories-BwlDh9--.js → RBSlider.stories-BjTYZ3ri.js} +1 -1
  80. package/cli/storybook-dist/assets/{RBSpinner.stories-VOlkuP6t.js → RBSpinner.stories-BWBvOGUY.js} +1 -1
  81. package/cli/storybook-dist/assets/{RBSwitch.stories-BiW2dQnT.js → RBSwitch.stories-tpthe98k.js} +1 -1
  82. package/cli/storybook-dist/assets/{RBTab.stories-DFKVsy0C.js → RBTab.stories-BijmaLIo.js} +1 -1
  83. package/cli/storybook-dist/assets/{RBTable.stories-BWG0ZSYI.js → RBTable.stories-Dojsz28q.js} +1 -1
  84. package/cli/storybook-dist/assets/{RBTableRow.stories-JVTD83Mh.js → RBTableRow.stories-CQajMiEa.js} +1 -1
  85. package/cli/storybook-dist/assets/{RBTabs.stories-D-aN385u.js → RBTabs.stories-BslRXoCw.js} +1 -1
  86. package/cli/storybook-dist/assets/{RBTextArea.stories-Dx5xkmZS.js → RBTextArea.stories-N44Q45Xp.js} +1 -1
  87. package/cli/storybook-dist/assets/{RBTextField.stories-DmVciExd.js → RBTextField.stories-scpiy0Kx.js} +1 -1
  88. package/cli/storybook-dist/assets/{RBTextInput.stories-CCf0VGEv.js → RBTextInput.stories-D_suHJaA.js} +1 -1
  89. package/cli/storybook-dist/assets/{RBToast.stories-DT0DGn05.js → RBToast.stories-B73JQcKo.js} +1 -1
  90. package/cli/storybook-dist/assets/{RBToggle.stories-Ce_q3EtP.js → RBToggle.stories-CmujM833.js} +1 -1
  91. package/cli/storybook-dist/assets/{RBToggleGroup.stories-CN40YHsR.js → RBToggleGroup.stories-DhCAfiKu.js} +1 -1
  92. package/cli/storybook-dist/assets/{RBTooltip.stories-L7btDEpA.js → RBTooltip.stories-BixEjKZy.js} +1 -1
  93. package/cli/storybook-dist/assets/{SideNumberInput-B5_xLK8z.js → SideNumberInput-B7fbx2jk.js} +83 -83
  94. package/cli/storybook-dist/assets/{entry-preview-raBQdKjS.js → entry-preview-BeR5Td7C.js} +1 -1
  95. package/cli/storybook-dist/assets/{iframe-BmIue__l.js → iframe-D3pKf_6w.js} +2 -2
  96. package/cli/storybook-dist/assets/{index-CSIe-YDr.js → index-gXlU0WyC.js} +3 -3
  97. package/cli/storybook-dist/assets/preview-BYhMD55w.css +1 -0
  98. package/cli/storybook-dist/assets/preview-D-VpQB0_.js +3 -0
  99. package/cli/storybook-dist/assets/{preview-DD51RTKv.js → preview-GwaHbm9r.js} +2 -2
  100. package/cli/storybook-dist/assets/react-resizable-panels-DJMuB2U0.css +1 -0
  101. package/cli/storybook-dist/iframe.html +1 -1
  102. package/cli/storybook-dist/project.json +1 -1
  103. package/cli/templates/common/README.md +10 -22
  104. package/cli/templates/common/requirements.txt +3 -3
  105. package/cli/templates/cpp/cpp/CMakeLists.txt +3 -2
  106. package/cli/templates/readme-cpp/README.md +18 -23
  107. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +3 -2
  108. package/package.json +3 -4
  109. package/cli/docker/Dockerfile.bridge +0 -17
  110. package/cli/docker/bridge-package.json +0 -8
  111. package/cli/src/bridge.mjs +0 -152
  112. package/cli/src/socketBridge.mjs +0 -133
  113. package/cli/storybook-dist/assets/preview-CUgthyJT.js +0 -3
  114. package/cli/storybook-dist/assets/preview-zuAvMksL.css +0 -1
  115. package/cli/storybook-dist/assets/react-resizable-panels-BaelaOGy.css +0 -1
  116. /package/cli/storybook-dist/assets/{react-resizable-panels.browser-CYcsrUgg.js → react-resizable-panels.browser-D49JKwKo.js} +0 -0
package/README.md CHANGED
@@ -115,7 +115,7 @@ muscat-pk build # build dist/ and produce a distributable ZIP (sk
115
115
  | `muscat-pk storybook` | Browse the bundled rb-components Storybook |
116
116
  | `muscat-pk mcp` | Run an MCP server for AI coding agents (exposes rb-components/design-token/SDK docs) |
117
117
  | `muscat-pk update` | Update the kit + plugin-ui to the latest version |
118
- | `muscat-pk sdk <list\|update>` | Manage the Python `rainbow-rb-sdk` version |
118
+ | `muscat-pk sdk <list\|update>` | Manage the `rainbow-rb-sdk` version — python: `requirements.txt`, C++ (cpp / ros2-cpp): `find_package(rb_sdk <ver> EXACT REQUIRED)` in `CMakeLists.txt` |
119
119
  | `muscat-pk version` | Print the installed CLI version (`-v` / `--version` also work) |
120
120
  | `muscat-pk completion [shell]` | Print a shell completion script (`bash` / `zsh` / `fish` / `powershell`; defaults to `$SHELL`) |
121
121
 
@@ -159,7 +159,7 @@ Move images to a machine with no internet:
159
159
 
160
160
  ```bash
161
161
  # on a machine with network
162
- docker save muscat-pk-python-dev muscat-pk-bridge | gzip > muscat-pk-images.tar.gz
162
+ docker save muscat-pk-python-dev muscat-pk-runtime | gzip > muscat-pk-images.tar.gz
163
163
 
164
164
  # on the offline machine
165
165
  gunzip -c muscat-pk-images.tar.gz | docker load
@@ -198,12 +198,12 @@ The runtime monitor is strict on purpose: an undeclared port outside the plugin
198
198
  range is invisible to both the monitor and the robot's conflict check, so it
199
199
  would only surface as a collision after deployment.
200
200
 
201
- `muscat-pk`'s own dev plumbing (the Socket.IO bridge on `10000`, the Zenoh dev
201
+ `muscat-pk`'s own dev plumbing (the plugin gateway on `10000`, the Zenoh dev
202
202
  peer port) is excluded from the check — it isn't the plugin's traffic.
203
203
 
204
- Declared ports are published to `127.0.0.1` on macOS and Windows so you can
205
- reach the backend from the host; native Linux uses host networking, where they
206
- already are.
204
+ Under `muscat-pk dev` declared ports are published to `127.0.0.1` from the local
205
+ Muscat container (the backend shares its network), so you can reach the backend
206
+ from the host.
207
207
 
208
208
  ## Variables (pyfm)
209
209
 
package/cli/README.md CHANGED
@@ -42,12 +42,12 @@ the backend**, whatever its number. Since a port outside `28600-28999` cannot be
42
42
  declared, it cannot be opened either — otherwise it would slip past both this
43
43
  check and the robot's install-time conflict check.
44
44
 
45
- The kit's own dev ports (Socket.IO bridge `10000`, the Zenoh dev peer port) are
45
+ The kit's own dev ports (plugin gateway `10000`, the Zenoh dev peer port) are
46
46
  excluded from that check.
47
47
 
48
- Declared ports are published to `127.0.0.1` so the host can reach the backend on
49
- macOS/Windows; native Linux dev containers use host networking and already have
50
- them. A declared port that is already busy on the host is skipped with a warning.
48
+ Under `muscat-pk dev` the backend shares the local Muscat container's network, so
49
+ declared ports are published to `127.0.0.1` from that container. A declared port
50
+ that is already busy on the host is skipped with a warning.
51
51
 
52
52
  Known robot/industrial/internal ports such as `502`, `4840`, `5170-5179`,
53
53
  `8000-8999`, `10000`, `2222`, `30004`, `34964`, `44818`, `50000-50005`,
@@ -226,7 +226,13 @@ inside plugins.
226
226
  - **`build`** bundles plugin code into ESM while externalizing
227
227
  `@rainbow-robotics/plugin-ui`, React, and ReactDOM.
228
228
  - **`dev:ui`** builds the plugin then serves it inside the bundled preview host
229
- at `/plugin-dev` — a real mini-Muscat shell with no robot/back end.
229
+ at `/plugin-dev` — a real mini-Muscat shell. Without a robot IP it needs a Muscat
230
+ on `localhost:3000` for program preview: `dev` / `dev:ui` reuse any container that
231
+ already publishes `:10000` (`muscat`, `muscat-dev`, …) and otherwise start
232
+ `muscat-pk-muscat` — the S3 runtime image (`muscat/main/muscat.zip`) running only the
233
+ `common` service from `muscat/main/version.json`, cached under `~/.muscat-pk/muscat/`.
234
+ It keeps running between runs (`docker rm -f muscat-pk-muscat` to stop);
235
+ `MUSCAT_PK_NO_LOCAL_MUSCAT=1` disables it.
230
236
 
231
237
  `create-muscat-plugin <dir>` is the bin alias for `muscat-pk new`.
232
238
 
@@ -81,7 +81,7 @@ function formatHelp() {
81
81
  commandRow("new [dir]", `Scaffold a new plugin project ${color.dim("(prompts: author, email, description, type, front-package-manager)")}`),
82
82
  commandRow("type change", `Switch backend type ${color.dim("(python | cpp | ros2-python | ros2-cpp)")}`),
83
83
  commandRow("build", `Build + lock deps + vendor wheels + zip ${color.dim(`(${option("--no-zip")}: skip zip, ${option("--no-lock")}: skip python lock/vendor, ${option("--arch")} amd64|arm64|both)`)}`),
84
- commandRow("dev", "Run Socket.IO bridge, dev:ui, and dev:back together"),
84
+ commandRow("dev", "Run a local Muscat runtime, dev:ui, and dev:back together"),
85
85
  commandRow("dev:ui", `Preview the plugin in a mini-Muscat ${color.dim("(preview-host)")}`),
86
86
  commandRow("dev:back", `Run the plugin back end ${color.dim("(python: watchfiles reload / ros2: Docker)")}`),
87
87
  commandRow("storybook", `Browse the bundled rb-components Storybook ${color.dim(`(${option("--port")} <n>, ${option("--rebuild")})`)}`),
@@ -19,8 +19,6 @@ function walk(baseDir, prefix) {
19
19
  }
20
20
  walk(resolve(root, "templates") + "/", "");
21
21
  walk(resolve(root, "docker") + "/", "docker/");
22
- // Dockerfile.bridge 의 COPY 대상
23
- files["src/socketBridge.mjs"] = readFileSync(resolve(root, "src/socketBridge.mjs"), "base64");
24
22
 
25
23
  const { version } = JSON.parse(readFileSync(resolve(root, "../package.json"), "utf8"));
26
24
 
@@ -10,34 +10,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
10
10
  cmake \
11
11
  && rm -rf /var/lib/apt/lists/*
12
12
 
13
- # Rainbow Robotics C++ SDK for find_package(rb_sdk).
14
- # Override with MUSCAT_PK_RB_SDK_VERSION=<ver> (+ MUSCAT_PK_REBUILD_IMAGE=1).
15
- ARG RB_SDK_VERSION=0.1.7
13
+ # Rainbow Robotics C++ SDK for find_package(rb_sdk). muscat-pk passes the version pinned in the
14
+ # project's CMakeLists.txt (find_package(rb_sdk <ver> EXACT)); without a pin (ROS 2 rclpy, `muscat-pk mcp`
15
+ # outside a project) the newest SDK is installed. Runtime and -dev are installed together at one version.
16
+ ARG RB_SDK_VERSION=
16
17
  RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
17
18
  && install -d /etc/apt/keyrings \
18
19
  && curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key -o /etc/apt/keyrings/rb-sdk.asc \
19
20
  && printf 'deb [signed-by=/etc/apt/keyrings/rb-sdk.asc] https://rainbowrobotics.github.io/rb-sdk %s main\n' stable dev > /etc/apt/sources.list.d/rb-sdk.list \
20
- && apt-get update && apt-get install -y --no-install-recommends "rainbow-rb-sdk-dev=${RB_SDK_VERSION}" \
21
+ && apt-get update \
22
+ && apt-get install -y --no-install-recommends "rainbow-rb-sdk${RB_SDK_VERSION:+=$RB_SDK_VERSION}" "rainbow-rb-sdk-dev${RB_SDK_VERSION:+=$RB_SDK_VERSION}" \
21
23
  && rm -rf /var/lib/apt/lists/*
22
24
 
23
- # MediaMTX — same version/ports as the Muscat runtime (rby2 scripts/backend/runtime/mediamtx.yml),
24
- # plus webrtcAdditionalHosts so a browser on the dev host reaches the WebRTC media port.
25
- ARG MEDIAMTX_VERSION=1.12.3
26
- RUN set -eux; \
27
- apt-get update; apt-get install -y --no-install-recommends ca-certificates curl; rm -rf /var/lib/apt/lists/*; \
28
- case "$(uname -m)" in x86_64) a=amd64 ;; aarch64|arm64) a=arm64 ;; *) exit 1 ;; esac; \
29
- curl -fsSL "https://github.com/bluenviron/mediamtx/releases/download/v${MEDIAMTX_VERSION}/mediamtx_v${MEDIAMTX_VERSION}_linux_${a}.tar.gz" \
30
- | tar -xz -C /usr/local/bin mediamtx; \
31
- mkdir -p /etc/mediamtx; \
32
- printf '%s\n' \
33
- 'logLevel: warn' \
34
- 'rtsp: yes' 'rtspAddress: :8554' \
35
- 'webrtc: yes' 'webrtcAddress: :8889' \
36
- 'webrtcLocalUDPAddress: :8189' 'webrtcLocalTCPAddress: :8189' \
37
- 'webrtcAdditionalHosts: [127.0.0.1]' 'webrtcAllowOrigin: "*"' \
38
- 'hls: no' 'rtmp: no' 'srt: no' 'api: no' \
39
- 'paths:' ' all_others:' ' source: publisher' \
40
- > /etc/mediamtx/mediamtx.yml
41
25
 
42
26
  WORKDIR /plugin
43
27
 
@@ -9,34 +9,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
9
9
  python3-colcon-common-extensions \
10
10
  && rm -rf /var/lib/apt/lists/*
11
11
 
12
- # Rainbow Robotics C++ SDK for find_package(rb_sdk).
13
- # Override with MUSCAT_PK_RB_SDK_VERSION=<ver> (+ MUSCAT_PK_REBUILD_IMAGE=1).
14
- ARG RB_SDK_VERSION=0.1.7
12
+ # Rainbow Robotics C++ SDK for find_package(rb_sdk). muscat-pk passes the version pinned in the
13
+ # project's CMakeLists.txt (find_package(rb_sdk <ver> EXACT)); without a pin (ROS 2 rclpy, `muscat-pk mcp`
14
+ # outside a project) the newest SDK is installed. Runtime and -dev are installed together at one version.
15
+ ARG RB_SDK_VERSION=
15
16
  RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
16
17
  && install -d /etc/apt/keyrings \
17
18
  && curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key -o /etc/apt/keyrings/rb-sdk.asc \
18
19
  && printf 'deb [signed-by=/etc/apt/keyrings/rb-sdk.asc] https://rainbowrobotics.github.io/rb-sdk %s main\n' stable dev > /etc/apt/sources.list.d/rb-sdk.list \
19
- && apt-get update && apt-get install -y --no-install-recommends "rainbow-rb-sdk-dev=${RB_SDK_VERSION}" \
20
+ && apt-get update \
21
+ && apt-get install -y --no-install-recommends "rainbow-rb-sdk${RB_SDK_VERSION:+=$RB_SDK_VERSION}" "rainbow-rb-sdk-dev${RB_SDK_VERSION:+=$RB_SDK_VERSION}" \
20
22
  && rm -rf /var/lib/apt/lists/*
21
23
 
22
- # MediaMTX — same version/ports as the Muscat runtime (rby2 scripts/backend/runtime/mediamtx.yml),
23
- # plus webrtcAdditionalHosts so a browser on the dev host reaches the WebRTC media port.
24
- ARG MEDIAMTX_VERSION=1.12.3
25
- RUN set -eux; \
26
- apt-get update; apt-get install -y --no-install-recommends ca-certificates curl; rm -rf /var/lib/apt/lists/*; \
27
- case "$(uname -m)" in x86_64) a=amd64 ;; aarch64|arm64) a=arm64 ;; *) exit 1 ;; esac; \
28
- curl -fsSL "https://github.com/bluenviron/mediamtx/releases/download/v${MEDIAMTX_VERSION}/mediamtx_v${MEDIAMTX_VERSION}_linux_${a}.tar.gz" \
29
- | tar -xz -C /usr/local/bin mediamtx; \
30
- mkdir -p /etc/mediamtx; \
31
- printf '%s\n' \
32
- 'logLevel: warn' \
33
- 'rtsp: yes' 'rtspAddress: :8554' \
34
- 'webrtc: yes' 'webrtcAddress: :8889' \
35
- 'webrtcLocalUDPAddress: :8189' 'webrtcLocalTCPAddress: :8189' \
36
- 'webrtcAdditionalHosts: [127.0.0.1]' 'webrtcAllowOrigin: "*"' \
37
- 'hls: no' 'rtmp: no' 'srt: no' 'api: no' \
38
- 'paths:' ' all_others:' ' source: publisher' \
39
- > /etc/mediamtx/mediamtx.yml
40
24
 
41
25
  # `docker exec` skips /ros_entrypoint.sh — source ROS for the login shells dev:back runs.
42
26
  RUN echo ". /opt/ros/${ROS_DISTRO}/setup.sh" > /etc/profile.d/ros.sh
@@ -15,24 +15,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
15
15
  # cpp SDK shared lib must be present for the python module to load. Install the
16
16
  # rainbow cpp SDK (apt, pinned to the robot's version) here once it exists.
17
17
 
18
- # MediaMTX — same version/ports as the Muscat runtime (rby2 scripts/backend/runtime/mediamtx.yml),
19
- # plus webrtcAdditionalHosts so a browser on the dev host reaches the WebRTC media port.
20
- ARG MEDIAMTX_VERSION=1.12.3
21
- RUN set -eux; \
22
- apt-get update; apt-get install -y --no-install-recommends ca-certificates curl; rm -rf /var/lib/apt/lists/*; \
23
- case "$(uname -m)" in x86_64) a=amd64 ;; aarch64|arm64) a=arm64 ;; *) exit 1 ;; esac; \
24
- curl -fsSL "https://github.com/bluenviron/mediamtx/releases/download/v${MEDIAMTX_VERSION}/mediamtx_v${MEDIAMTX_VERSION}_linux_${a}.tar.gz" \
25
- | tar -xz -C /usr/local/bin mediamtx; \
26
- mkdir -p /etc/mediamtx; \
27
- printf '%s\n' \
28
- 'logLevel: warn' \
29
- 'rtsp: yes' 'rtspAddress: :8554' \
30
- 'webrtc: yes' 'webrtcAddress: :8889' \
31
- 'webrtcLocalUDPAddress: :8189' 'webrtcLocalTCPAddress: :8189' \
32
- 'webrtcAdditionalHosts: [127.0.0.1]' 'webrtcAllowOrigin: "*"' \
33
- 'hls: no' 'rtmp: no' 'srt: no' 'api: no' \
34
- 'paths:' ' all_others:' ' source: publisher' \
35
- > /etc/mediamtx/mediamtx.yml
36
18
 
37
19
  WORKDIR /plugin
38
20
 
package/cli/src/build.mjs CHANGED
@@ -353,7 +353,7 @@ function packZip(pluginRoot) {
353
353
 
354
354
  /**
355
355
  * Ask which target arch(es) to build for.
356
- * Non-TTY (CI): auto-selects current machine arch.
356
+ * Non-TTY (CI): both, same as the prompt default.
357
357
  * @returns {Promise<string[]>} e.g. ['amd64'] | ['arm64'] | ['amd64','arm64']
358
358
  */
359
359
  async function promptArch(args = []) {
@@ -364,11 +364,11 @@ async function promptArch(args = []) {
364
364
  if (archFlag === "arm64" || archFlag === "aarch64") return ["arm64"];
365
365
  throw new Error(`unknown --arch value '${archFlag}' (valid: amd64, arm64, both)`);
366
366
  }
367
+ // Same default as the prompt: the target is the robot, not this machine
368
+ // (node's os.arch() is x64 under Rosetta on an arm64 Mac anyway).
367
369
  if (!process.stdin.isTTY) {
368
- const { arch } = await import("node:os");
369
- const machine = arch() === "arm64" ? "arm64" : "amd64";
370
- log.info(`arch auto-selected (non-TTY): ${machine}`);
371
- return [machine];
370
+ log.info("arch auto-selected (non-TTY): both");
371
+ return ["amd64", "arm64"];
372
372
  }
373
373
  const rl = createInterface({ input: process.stdin, output: process.stdout });
374
374
  process.stdout.write(
@@ -19,7 +19,7 @@ const COMMANDS = [
19
19
  ["mcp", "Start an MCP server (stdio)"],
20
20
  ["update", "Update muscat-pk and plugin-ui"],
21
21
  ["uninstall", "Remove the installed muscat-pk CLI"],
22
- ["sdk", "Manage the python rainbow-rb-sdk version"],
22
+ ["sdk", "Manage the rainbow-rb-sdk version (python: requirements.txt, C++: CMakeLists.txt)"],
23
23
  ["version", "Print the installed muscat-pk version"],
24
24
  ["completion", "Print a shell completion script"],
25
25
  ["help", "Show help"],
package/cli/src/cpp.mjs CHANGED
@@ -3,7 +3,8 @@
3
3
  // Used by dev:back (interactive dev loop) and build (export the binary into the ZIP).
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { dirname } from "node:path";
6
- import { canSkipImageBuild, imageKey, imageKeyLabel, dockerUserFlags, hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
6
+ import { canSkipImageBuild, cppSdkVersion, pluginRootOrNull, imageKey, imageKeyLabel, dockerUserFlags, hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
7
+ import { syncCppIde } from "./cppIde.mjs";
7
8
 
8
9
  export const CPP_DEV_IMAGE = "muscat-pk-cpp-dev";
9
10
 
@@ -14,9 +15,16 @@ export function assertDockerReady() {
14
15
  }
15
16
 
16
17
  /** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
17
- export async function ensureImage(image = CPP_DEV_IMAGE, { platform = null } = {}) {
18
+ export async function ensureImage(image = CPP_DEV_IMAGE, { platform = null, root = pluginRootOrNull() } = {}) {
19
+ await buildImage(image, platform, root);
20
+ // Dev images feed the editor's SDK headers: the native one (dev) or the host-arch one (build).
21
+ const hostArch = process.arch === "arm64" ? "arm64" : "amd64";
22
+ if (root && (image === CPP_DEV_IMAGE || image === `${CPP_DEV_IMAGE}-${hostArch}`)) syncCppIde(root, image);
23
+ }
24
+
25
+ async function buildImage(image, platform, root) {
18
26
  const dockerfile = await resolveDockerfile("Dockerfile.cpp");
19
- const sdkVer = process.env.MUSCAT_PK_RB_SDK_VERSION;
27
+ const sdkVer = cppSdkVersion(root);
20
28
  const key = imageKey(dockerfile, sdkVer ?? "");
21
29
  if (canSkipImageBuild(image, key)) {
22
30
  log.step(`${image} already built — skipping docker build`);
@@ -0,0 +1,75 @@
1
+ // Editor (IntelliSense) support for C++ backends. The rb_sdk headers only exist inside the dev image,
2
+ // so a host editor flags every SDK name. Copy them to .muscat-dev/sysroot and point
3
+ // .vscode/c_cpp_properties.json at them, using the include dirs / defines rb_sdk's CMake target exports.
4
+ // The build never reads these copies — it runs in the image.
5
+ import { spawnSync } from "node:child_process";
6
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
+ import { resolve } from "node:path";
8
+ import { cppSdkFiles, log } from "./util.mjs";
9
+
10
+ const SYSROOT = ".muscat-dev/sysroot";
11
+ const CONFIG_NAME = "Muscat";
12
+ const COPY = ["/usr/include/rainbow", "/usr/include/eigen3", "/usr/lib/cmake/rb_sdk/rb_sdkTargets.cmake"];
13
+
14
+ function imageId(image) {
15
+ const r = spawnSync("docker", ["image", "inspect", "-f", "{{.Id}}", image], { encoding: "utf8" });
16
+ return r.status === 0 ? r.stdout.trim() : null;
17
+ }
18
+
19
+ /** Copy the SDK headers out of `image` (once per image build) and write the VS Code C/C++ config. */
20
+ export function syncCppIde(root, image) {
21
+ if (!cppSdkFiles(root).length) return;
22
+ const id = imageId(image);
23
+ if (!id) return;
24
+ const sysroot = resolve(root, SYSROOT);
25
+ const stamp = resolve(sysroot, ".image-id");
26
+ if (!existsSync(stamp) || readFileSync(stamp, "utf8") !== id) {
27
+ rmSync(sysroot, { recursive: true, force: true });
28
+ const create = spawnSync("docker", ["create", image], { encoding: "utf8" });
29
+ if (create.status !== 0) return log.warn("could not copy SDK headers for the editor (docker create failed).");
30
+ const container = create.stdout.trim();
31
+ try {
32
+ for (const src of COPY) {
33
+ const dest = resolve(sysroot, src.slice(1));
34
+ mkdirSync(resolve(dest, ".."), { recursive: true });
35
+ spawnSync("docker", ["cp", `${container}:${src}`, dest], { stdio: "ignore" });
36
+ }
37
+ } finally {
38
+ spawnSync("docker", ["rm", container], { stdio: "ignore" });
39
+ }
40
+ writeFileSync(stamp, id);
41
+ log.step(`SDK headers for the editor → ${SYSROOT}`);
42
+ }
43
+ writeCppProperties(root, readTarget(sysroot));
44
+ }
45
+
46
+ // INTERFACE_INCLUDE_DIRECTORIES / INTERFACE_COMPILE_DEFINITIONS of rb_sdk::rb_sdk, mapped into the sysroot.
47
+ function readTarget(sysroot) {
48
+ const file = resolve(sysroot, "usr/lib/cmake/rb_sdk/rb_sdkTargets.cmake");
49
+ const text = existsSync(file) ? readFileSync(file, "utf8") : "";
50
+ const list = (key) => (text.match(new RegExp(`${key} "([^"]*)"`))?.[1] ?? "").split(";").filter(Boolean);
51
+ const prefix = `\${workspaceFolder}/${SYSROOT}/usr`;
52
+ const includes = list("INTERFACE_INCLUDE_DIRECTORIES").map((d) => d.replace("${_IMPORT_PREFIX}", prefix));
53
+ return {
54
+ includePath: [...(includes.length ? includes : [`${prefix}/include`]), `${prefix}/include/eigen3`],
55
+ defines: list("INTERFACE_COMPILE_DEFINITIONS"),
56
+ };
57
+ }
58
+
59
+ // Keeps other configurations; ours is replaced in place or put first so VS Code picks it by default.
60
+ function writeCppProperties(root, { includePath, defines }) {
61
+ const file = resolve(root, ".vscode", "c_cpp_properties.json");
62
+ let props = { configurations: [], version: 4 };
63
+ if (existsSync(file)) {
64
+ try {
65
+ props = JSON.parse(readFileSync(file, "utf8"));
66
+ } catch {
67
+ return log.warn(".vscode/c_cpp_properties.json is not plain JSON — add the SDK include paths yourself.");
68
+ }
69
+ }
70
+ const config = { name: CONFIG_NAME, includePath: ["${workspaceFolder}/**", ...includePath], defines, cppStandard: "c++20" };
71
+ const configs = (props.configurations ?? []).filter((c) => c.name !== CONFIG_NAME);
72
+ props.configurations = [config, ...configs];
73
+ mkdirSync(resolve(root, ".vscode"), { recursive: true });
74
+ writeFileSync(file, JSON.stringify(props, null, 2) + "\n");
75
+ }
package/cli/src/dev.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // muscat-pk dev — run the Socket.IO bridge (Docker), dev:ui, and dev:back together.
1
+ // muscat-pk dev — run the local Muscat runtime (gateway, MediaMTX, common), dev:ui, and dev:back together.
2
2
  import { spawn } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
4
  import { connect as netConnect } from "node:net";
@@ -6,7 +6,7 @@ import { resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { runDevUi } from "./devUi.mjs";
8
8
  import { runDevBack, stopDevBackContainers } from "./devBack.mjs";
9
- import { localMuscatContainer, startBridge, stopBridge, BRIDGE_CONTAINER, BRIDGE_PORT } from "./bridge.mjs";
9
+ import { ensureLocalMuscat, GATEWAY_PORT, isPortInUse } from "./muscatLocal.mjs";
10
10
  import { assertManifestPorts, assertManifestVariables } from "./manifest.mjs";
11
11
  import { formatPortRanges } from "./ports.mjs";
12
12
  import {
@@ -18,7 +18,7 @@ import {
18
18
  promptRobotConnection,
19
19
  venvPython,
20
20
  } from "./util.mjs";
21
- import { ensureRemoteZenohEnv, isRemoteRobot, isWsl, usesHostNetwork } from "./zenohDev.mjs";
21
+ import { ensureRemoteZenohEnv, isRemoteRobot, isWsl } from "./zenohDev.mjs";
22
22
 
23
23
  const HOST_AGENT_PATH = fileURLToPath(new URL("./host_agent.py", import.meta.url));
24
24
  const canAutoStartHostAgent = () => process.platform === "darwin" || isWsl();
@@ -99,7 +99,7 @@ async function maybeStartHostAgent(root, { robotIp, robotToken }) {
99
99
  }
100
100
 
101
101
  // advertiseHost (not host.docker.internal): a Docker Desktop container run
102
- // under `dev` may share the bridge container's netns (--network container:),
102
+ // under `dev` shares the local Muscat container's netns (--network container:),
103
103
  // which has no host-gateway alias configured — the host's real LAN IP
104
104
  // always resolves regardless of netns mode.
105
105
  const proxyEndpoint = `${advertiseHost}:${proxyPort}`;
@@ -126,32 +126,26 @@ export async function runDev(args = []) {
126
126
  const hostAgent = await maybeStartHostAgent(root, { robotIp, robotToken });
127
127
  const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT || "";
128
128
  // No proxy (Linux, disabled, ...): fall back to the container reverse-dialing
129
- // the robot itself — startBridge() then publishes the zenoh port for it.
129
+ // the robot itself — the local Muscat then publishes the zenoh port for it.
130
130
  if (!proxyEndpoint) await ensureRemoteZenohEnv(robotIp, { log });
131
131
 
132
- log.step("starting Socket.IO bridge (Docker) + dev:ui + dev:back");
133
- const bridgeStarted = await startBridge({
134
- zenohPort: remoteRobot && !proxyEndpoint && !usesHostNetwork()
135
- ? Number(process.env.MUSCAT_ZENOH_PORT)
136
- : null,
137
- // Linux uses --network host, so the ports are already on the host.
138
- pluginPorts: usesHostNetwork() ? [] : portRanges,
139
- });
140
-
141
- const muscat = bridgeStarted ? null : localMuscatContainer();
132
+ // Robot or not, the local Muscat runtime is the gateway (:10000), MediaMTX and common; the
133
+ // backend joins its netns, so the plugin's declared ports are published from it too.
134
+ log.step("starting local Muscat (Docker) + dev:ui + dev:back");
135
+ const muscat = await ensureLocalMuscat({
136
+ pluginPorts: portRanges,
137
+ zenohPort: remoteRobot && !proxyEndpoint ? Number(process.env.MUSCAT_ZENOH_PORT) || null : null,
138
+ }).catch((e) => { log.warn(`local Muscat: ${e.message}`); return null; });
142
139
  if (muscat) {
143
- log.step(`local Muscat container '${muscat}' owns :${BRIDGE_PORT} — backend joins its network (gateway, MediaMTX)`);
140
+ log.step(`local Muscat container '${muscat}' owns :${GATEWAY_PORT} — backend joins its network (gateway, MediaMTX)`);
144
141
  process.env.MUSCAT_PK_MUSCAT_CONTAINER = muscat;
145
- } else if (!bridgeStarted) {
146
- log.warn(`port ${BRIDGE_PORT} is already in use — can't start Socket.IO bridge.`);
147
- const answer = await prompt(" Proceed without Socket.IO bridge? [y/N]: ");
148
- if (answer.toLowerCase() !== "y") return;
149
- log.info("proceeding without bridge — backend will skip network sharing.");
142
+ } else if (await isPortInUse(GATEWAY_PORT)) {
143
+ // e.g. developing on the robot PC: its Muscat already serves the gateway; the backend reaches it via the host.
144
+ log.step(`:${GATEWAY_PORT} is already served (Muscat on this machine) — using it`);
150
145
  } else {
151
- // Tell dev:back which bridge container to share a netns with, so the backend
152
- // reaches the bridge at localhost:10000 (no host.docker.internal).
153
- process.env.MUSCAT_PK_BRIDGE_CONTAINER = BRIDGE_CONTAINER;
154
- if (bridgeStarted.media) process.env.MUSCAT_PK_MEDIA = "1";
146
+ log.warn(`no local Muscat on :${GATEWAY_PORT} — the plugin UI cannot reach the backend.`);
147
+ const answer = await prompt(" Proceed anyway? [y/N]: ");
148
+ if (answer.toLowerCase() !== "y") return;
155
149
  }
156
150
 
157
151
  process.env.MUSCAT_PK_MANAGED_DEV = "1";
@@ -159,7 +153,6 @@ export async function runDev(args = []) {
159
153
  const cleanup = () => {
160
154
  if (hostAgent && hostAgent.exitCode === null) hostAgent.kill();
161
155
  stopDevBackContainers();
162
- if (bridgeStarted) stopBridge();
163
156
  };
164
157
  process.once("SIGINT", cleanup);
165
158
  process.once("SIGTERM", cleanup);
@@ -2,7 +2,7 @@
2
2
  // Every backend runs in a warm Docker container that matches the robot
3
3
  // (Ubuntu 24.04), so dev behaves like production.
4
4
  // python : Python 3.12 container, pip install, run main.py with watchfiles
5
- // hot-reload. Shares the bridge container's netns (localhost:10000).
5
+ // hot-reload. Shares the local Muscat container's netns (localhost:10000).
6
6
  // cpp : Ubuntu + cmake container, build cpp/, run the binary, rebuild +
7
7
  // restart on source changes.
8
8
  // ros2 : Jazzy + Python 3.12 container, colcon build, run the node, restart
@@ -12,7 +12,8 @@ import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from "node:
12
12
  import { isIP } from "node:net";
13
13
  import { dirname, resolve } from "node:path";
14
14
  import { dockerUserFlags, ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection, realtimeFlags } from "./util.mjs";
15
- import { ensureRemoteZenohEnv, usesHostNetwork } from "./zenohDev.mjs";
15
+ import { ensureRemoteZenohEnv, isRemoteRobot, usesHostNetwork } from "./zenohDev.mjs";
16
+ import { localMuscatContainer } from "./muscatLocal.mjs";
16
17
  import { PLUGIN_ID_RE, assertManifestPorts, assertManifestVariables, manifestRuntime, readManifest, rosNamespace, validateManifest } from "./manifest.mjs";
17
18
  import {
18
19
  formatPortRanges,
@@ -53,15 +54,22 @@ export function stopDevBackContainers() {
53
54
  }
54
55
  }
55
56
 
56
- // Where the backend's 127.0.0.1 points. A local Muscat container (muscat-dev): join its
57
- // netns so the gateway and MediaMTX resolve like on the robot. Under `dev`: the bridge's
58
- // netns. Standalone dev:back: its own netns, gateway via the host.
57
+ // Where the backend's 127.0.0.1 points. A local Muscat container (muscat-pk-muscat, muscat-dev):
58
+ // join its netns so the gateway and MediaMTX resolve like on the robot. Otherwise its own
59
+ // netns, gateway via the host.
60
+ // IS_DEV Muscat (muscat-dev): common is a zenoh-router client and opens no 7447 listener,
61
+ // so the plugin must join the router too or `execute` queries never reach it.
62
+ function muscatZenohEnv(container) {
63
+ const r = spawnSync("docker", ["inspect", "-f", "{{range .Config.Env}}{{println .}}{{end}}", container], { encoding: "utf8" });
64
+ return /^IS_DEV=(true|1)$/im.test(r.stdout || "")
65
+ ? ["-e", "ZENOH_PEER_CONNECT_ENDPOINTS=tcp/zenoh-router:7447"]
66
+ : [];
67
+ }
68
+
59
69
  function backendNetwork(portRanges) {
60
70
  const muscat = process.env.MUSCAT_PK_MUSCAT_CONTAINER;
61
- if (muscat) return { run: ["--network", `container:${muscat}`], env: [], shared: true };
71
+ if (muscat) return { run: ["--network", `container:${muscat}`], env: muscatZenohEnv(muscat), shared: true };
62
72
  if (usesHostNetwork()) return { run: ["--network", "host"], env: [], shared: true };
63
- const bridge = process.env.MUSCAT_PK_BRIDGE_CONTAINER;
64
- if (bridge) return { run: ["--network", `container:${bridge}`], env: [], shared: true };
65
73
  return {
66
74
  // host.docker.internal is automatic on Docker Desktop; this makes it resolve on Linux too.
67
75
  run: [...publishPortFlags(portRanges), "--add-host", "host.docker.internal:host-gateway"],
@@ -107,13 +115,6 @@ async function serveRosPeers(root, pluginId, restart) {
107
115
  });
108
116
  }
109
117
 
110
- // Our own MediaMTX, only when `dev` found its ports free (a local Muscat brings its own).
111
- function startMediaServer(container) {
112
- if (process.env.MUSCAT_PK_MEDIA !== "1") return;
113
- spawnSync("docker", ["exec", "-d", container, "mediamtx", "/etc/mediamtx/mediamtx.yml"], { stdio: "ignore" });
114
- log.step("MediaMTX: publish rtsp://127.0.0.1:8554/<path> · play http://localhost:8889/<path>");
115
- }
116
-
117
118
  // ── IDE tooling venv (best-effort, non-blocking) ────────────────────────────
118
119
  // `muscat-pk new` already creates this up front (see new.mjs); this just
119
120
  // refreshes it on every `dev`/`dev:back` in case requirements.txt changed.
@@ -134,8 +135,8 @@ function syncIdeVenv(root) {
134
135
 
135
136
  // ── python ────────────────────────────────────────────────────────────────────
136
137
  // Runs in a warm Ubuntu 24.04 + Python 3.12 container (matches the robot).
137
- // Under `muscat-pk dev`, it shares the bridge container's netns, so the SDK
138
- // reaches the Socket.IO bridge at the default localhost:10000.
138
+ // Under `muscat-pk dev`, it shares the local Muscat container's netns, so the SDK
139
+ // reaches the plugin gateway at the default localhost:10000.
139
140
  async function runPython(root, portRanges) {
140
141
  // Independent of Docker readiness — must still happen if Docker isn't
141
142
  // installed/running, since that's exactly when local IDE tooling matters most.
@@ -154,7 +155,7 @@ async function runPython(root, portRanges) {
154
155
  // Zenoh mesh, so it speaks a byte protocol to the host-side proxy instead.
155
156
  const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT ?? "";
156
157
  // Native Linux (not WSL, not Docker Desktop) has real host networking: the
157
- // container shares the host's network stack, so it reaches the bridge at
158
+ // container shares the host's network stack, so it reaches the gateway at
158
159
  // localhost:10000 and the robot's Zenoh router / reverse-dial path directly
159
160
  // — no netns sharing, port publishing, host.docker.internal, or byte-proxy.
160
161
  const net = backendNetwork(portRanges);
@@ -195,7 +196,6 @@ async function runPython(root, portRanges) {
195
196
  { stdio: "inherit" },
196
197
  );
197
198
  if (up.status !== 0) throw new Error("failed to start dev container.");
198
- startMediaServer(container);
199
199
  const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [zenohPort] });
200
200
 
201
201
  log.step("pip install requirements + watchfiles (in container)…");
@@ -413,7 +413,7 @@ runpy.run_path("/plugin/py/main.py", run_name="__main__")
413
413
  PY`]);
414
414
 
415
415
  if (pluginId) log.step(`RB_PLUGIN_ID=${pluginId}`);
416
- if (usesHostNetwork()) log.step("network: host (native Linux — direct robot/bridge access)");
416
+ if (usesHostNetwork()) log.step("network: host (native Linux — direct robot/gateway access)");
417
417
  if (proxyEndpoint) log.step(`Zenoh via host byte-proxy → ${proxyEndpoint}`);
418
418
  else if (zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
419
419
  log.step("watchfiles → py/main.py (reload on any .py change in py/)");
@@ -493,7 +493,6 @@ async function runRos2(root, portRanges) {
493
493
  { stdio: "inherit" },
494
494
  );
495
495
  if (up.status !== 0) throw new Error("failed to start dev container.");
496
- startMediaServer(container);
497
496
  const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [process.env.MUSCAT_ZENOH_PORT] });
498
497
 
499
498
  // one-time deps + symlink build (edits to .py reflect without rebuilding)
@@ -586,7 +585,6 @@ async function runCpp(root, portRanges) {
586
585
  { stdio: "inherit" },
587
586
  );
588
587
  if (up.status !== 0) throw new Error("failed to start dev container.");
589
- startMediaServer(container);
590
588
  const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [process.env.MUSCAT_ZENOH_PORT] });
591
589
 
592
590
  const build = () => {
@@ -656,6 +654,15 @@ export async function runDevBack(args = []) {
656
654
  // Byte-proxy mode (a host-side host_agent set MUSCAT_PROXY_ENDPOINT) makes
657
655
  // the container speak the proxy protocol instead, so it needs no reverse-dial.
658
656
  if (!process.env.MUSCAT_PROXY_ENDPOINT) await ensureRemoteZenohEnv(robotIp, { log });
657
+ // Standalone against a local Muscat (muscat-dev, …): join its netns like `dev` does,
658
+ // otherwise zenoh reaches nothing and `execute` never arrives.
659
+ if (!process.env.MUSCAT_PK_MUSCAT_CONTAINER && !isRemoteRobot(robotIp)) {
660
+ const muscat = localMuscatContainer();
661
+ if (muscat) {
662
+ log.step(`local Muscat container '${muscat}' found — backend joins its network`);
663
+ process.env.MUSCAT_PK_MUSCAT_CONTAINER = muscat;
664
+ }
665
+ }
659
666
  const type = manifestRuntime(root);
660
667
  if (type === "ros2") return await runRos2(root, portRanges);
661
668
  if (type === "cpp") return await runCpp(root, portRanges);
package/cli/src/devUi.mjs CHANGED
@@ -7,6 +7,8 @@ import { homedir } from "node:os";
7
7
  import { dirname, resolve } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { checkKitUpdate, findPluginRoot, hasBin, kitRoot, log, parseArgs, prompt } from "./util.mjs";
10
+ import { ensureLocalMuscat } from "./muscatLocal.mjs";
11
+ import { isRemoteRobot } from "./zenohDev.mjs";
10
12
 
11
13
 
12
14
  const DEV_PLUGIN_PORT = 4181;
@@ -44,7 +46,9 @@ function findPreviewHostRoot() {
44
46
  try {
45
47
  const req = createRequire(import.meta.url);
46
48
  return dirname(req.resolve(`${PREVIEW_PKG}/package.json`));
47
- } catch {}
49
+ } catch {
50
+ console.log("Try the sibling and cached package locations below.")
51
+ }
48
52
 
49
53
  const sibling = resolve(kitRoot, "../preview-host");
50
54
  if (existsSync(resolve(sibling, "package.json"))) return sibling;
@@ -137,6 +141,7 @@ function waitForPluginDevPort(readyFile, proc, timeoutMs = 15_000) {
137
141
  const { port } = JSON.parse(readFileSync(readyFile, "utf8"));
138
142
  if (Number.isInteger(port)) return res(port);
139
143
  } catch {
144
+ console.log("Keep polling until the ready file contains valid JSON.")
140
145
  }
141
146
  }
142
147
  if (Date.now() - start > timeoutMs)
@@ -160,6 +165,10 @@ export async function runDevUi(args = []) {
160
165
  log.warn(`preview-host port ${requestedPort} is in use — using ${port} instead.`);
161
166
 
162
167
  if (robotIp) log.info(`MUSCAT_API_ORIGIN → http://${robotIp}:*`);
168
+ // No robot: the bundle talks to localhost:3000, so put a Muscat there (`dev` already did).
169
+ if (!isRemoteRobot(robotIp) && process.env.MUSCAT_PK_MANAGED_DEV !== "1") {
170
+ await ensureLocalMuscat().catch((e) => log.warn(`local Muscat: ${e.message}`));
171
+ }
163
172
 
164
173
  watchAssets(pluginRoot, outDir);
165
174