@rainbow-robotics/muscat-plugin-kit 1.3.6-dev.8 → 1.3.7

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 (125) hide show
  1. package/cli/README.md +8 -3
  2. package/cli/bin/muscat-pk.mjs +5 -0
  3. package/cli/bundle-templates.mjs +6 -1
  4. package/cli/docker/Dockerfile.cpp +28 -6
  5. package/cli/docker/Dockerfile.dev +32 -0
  6. package/cli/docker/Dockerfile.python +19 -0
  7. package/cli/src/bridge.mjs +45 -12
  8. package/cli/src/build.mjs +48 -79
  9. package/cli/src/cpp.mjs +11 -5
  10. package/cli/src/dev.mjs +7 -2
  11. package/cli/src/devBack.mjs +97 -40
  12. package/cli/src/host_agent.py +143 -83
  13. package/cli/src/manifest.mjs +11 -0
  14. package/cli/src/mcp.mjs +398 -54
  15. package/cli/src/new.mjs +13 -3
  16. package/cli/src/ports.mjs +21 -7
  17. package/cli/src/python.mjs +16 -20
  18. package/cli/src/ros2.mjs +16 -8
  19. package/cli/src/sdk.mjs +23 -21
  20. package/cli/src/type.mjs +44 -10
  21. package/cli/src/uninstall.mjs +3 -20
  22. package/cli/src/update.mjs +14 -8
  23. package/cli/src/util.mjs +70 -9
  24. package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +13 -1
  25. package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +98 -1
  26. package/cli/storybook-dist/assets/{Color-YHDXOIA2-Du8dJ-nM.js → Color-YHDXOIA2-CdTgUqrV.js} +1 -1
  27. package/cli/storybook-dist/assets/{Docs-Sy5RDbtH.js → Docs-C8wx5zsC.js} +1 -1
  28. package/cli/storybook-dist/assets/{Docs-guGflRzZ.js → Docs-CbRKwHpQ.js} +1 -1
  29. package/cli/storybook-dist/assets/{Docs-Cb72AJnj.js → Docs-Cf0s04QW.js} +1 -1
  30. package/cli/storybook-dist/assets/{Docs-pwGqGTV_.js → Docs-CsxMprX5.js} +1 -1
  31. package/cli/storybook-dist/assets/{Docs-LdjYwuG4.js → Docs-FyPFrbUS.js} +1 -1
  32. package/cli/storybook-dist/assets/{Docs--Fm8069p.js → Docs-YtqU-aVu.js} +1 -1
  33. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-YGBLQ4BD.js → DocsRenderer-CFRXHY34-VSENNRoY.js} +1 -1
  34. package/cli/storybook-dist/assets/{NxAvatar.stories-C1d4n8cq.js → NxAvatar.stories-oCcYtLwq.js} +1 -1
  35. package/cli/storybook-dist/assets/{NxButton.stories-gCBoZdMq.js → NxButton.stories-C5jUScIj.js} +1 -1
  36. package/cli/storybook-dist/assets/{NxIcons.stories-Dg42riT5.js → NxIcons.stories-DeqrVzVs.js} +1 -1
  37. package/cli/storybook-dist/assets/{NxNumberInput.stories-DB-vLM2J.js → NxNumberInput.stories-gjsEulIN.js} +1 -1
  38. package/cli/storybook-dist/assets/{NxPagination.stories-CS8iMIak.js → NxPagination.stories-BK6uSWIa.js} +1 -1
  39. package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-D66BopeS.js → NxPointerHoldButton.stories-BDD5CvSZ.js} +1 -1
  40. package/cli/storybook-dist/assets/{NxRangeInput.stories-CZxddsJ2.js → NxRangeInput.stories-C_nW26_x.js} +3 -3
  41. package/cli/storybook-dist/assets/{NxSelectBox.stories-CmezGGrm.js → NxSelectBox.stories-DqLfKU9H.js} +1 -1
  42. package/cli/storybook-dist/assets/{NxSlideRange.stories-CGAFMITi.js → NxSlideRange.stories-CQjHA8yv.js} +1 -1
  43. package/cli/storybook-dist/assets/{NxSwitch.stories-B3QInkXX.js → NxSwitch.stories-BMHCiPoL.js} +1 -1
  44. package/cli/storybook-dist/assets/{NxTextArea.stories-BgJcgZNe.js → NxTextArea.stories-k7r1WVQh.js} +1 -1
  45. package/cli/storybook-dist/assets/{NxTextInput.stories-CwMQwI9L.js → NxTextInput.stories-Cqfe1I32.js} +1 -1
  46. package/cli/storybook-dist/assets/{RBAccordion.stories-BuBVNzJT.js → RBAccordion.stories-CJ3Ttuja.js} +1 -1
  47. package/cli/storybook-dist/assets/{RBAccordionGroup.stories-Crdb6kFF.js → RBAccordionGroup.stories-BThv7tM0.js} +1 -1
  48. package/cli/storybook-dist/assets/{RBAlert.stories-CqXgUvA2.js → RBAlert.stories-CSMP8Vun.js} +1 -1
  49. package/cli/storybook-dist/assets/{RBBadge.stories-BCw_ksML.js → RBBadge.stories-cBMA2R6F.js} +1 -1
  50. package/cli/storybook-dist/assets/{RBBattery.stories-0id4XYoc.js → RBBattery.stories-BKVOskqs.js} +1 -1
  51. package/cli/storybook-dist/assets/{RBCard.stories-CgpG-7Lk.js → RBCard.stories-BTOfK0HC.js} +1 -1
  52. package/cli/storybook-dist/assets/{RBCheckBox.stories-BG02BoGX.js → RBCheckBox.stories-DyE912wG.js} +1 -1
  53. package/cli/storybook-dist/assets/{RBCheckIconButton.stories-vrjD4GQ4.js → RBCheckIconButton.stories-0sLFU0kM.js} +1 -1
  54. package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-POba8lU-.js → RBCheckLabelButton.stories-c8VecClE.js} +1 -1
  55. package/cli/storybook-dist/assets/{RBChipTab.stories-XWJbFrHF.js → RBChipTab.stories-CQ9Uav5_.js} +1 -1
  56. package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-BdU0Dzdp.js → RBChipsBoxTab.stories-Ap4NlhAZ.js} +1 -1
  57. package/cli/storybook-dist/assets/{RBColorIcons.stories-C_0dCNXG.js → RBColorIcons.stories-BPPXjif2.js} +1 -1
  58. package/cli/storybook-dist/assets/{RBCommand.stories-Dvrko5No.js → RBCommand.stories-PDpYOKlm.js} +1 -1
  59. package/cli/storybook-dist/assets/{RBConfirm.stories-CkTn0sHa.js → RBConfirm.stories-BEWvMAYP.js} +1 -1
  60. package/cli/storybook-dist/assets/{RBDateInput.stories-BH3D08cs.js → RBDateInput.stories-DLdyNnVF.js} +1 -1
  61. package/cli/storybook-dist/assets/{RBDialog.stories-C2nNX9fl.js → RBDialog.stories-HsawMNOb.js} +1 -1
  62. package/cli/storybook-dist/assets/{RBDivider.stories-D-GrJ0QZ.js → RBDivider.stories-dlWwOxik.js} +1 -1
  63. package/cli/storybook-dist/assets/{RBDrawer.stories-XLOnLOnh.js → RBDrawer.stories-C87iqGk0.js} +1 -1
  64. package/cli/storybook-dist/assets/{RBDropdownMenu.stories-BjBSeoV2.js → RBDropdownMenu.stories-B8EQhA0z.js} +1 -1
  65. package/cli/storybook-dist/assets/{RBEmptyView.stories-C6U2zKNE.js → RBEmptyView.stories-SzpIhNDw.js} +1 -1
  66. package/cli/storybook-dist/assets/{RBIconButton.stories-MCJq5Hta.js → RBIconButton.stories-BQ0nM1gb.js} +1 -1
  67. package/cli/storybook-dist/assets/{RBIcons.stories-DxOCiCe0.js → RBIcons.stories-C19g5M5z.js} +1 -1
  68. package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-BVZi1aEs.js → RBInlineLabelButton.stories-DbgI4-1P.js} +1 -1
  69. package/cli/storybook-dist/assets/{RBJoyStick.stories-BDThbEek.js → RBJoyStick.stories-smF-vpGL.js} +1 -1
  70. package/cli/storybook-dist/assets/{RBLabelButton.stories-CaPEKUS6.js → RBLabelButton.stories-s53klwDv.js} +1 -1
  71. package/cli/storybook-dist/assets/{RBLight.stories-CihQzyEL.js → RBLight.stories-BnzkoR51.js} +1 -1
  72. package/cli/storybook-dist/assets/{RBMenubar.stories-J852XVFh.js → RBMenubar.stories-CW014klu.js} +1 -1
  73. package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-DXyX0cOZ.js → RBMultiRangeInput.stories-qa4tH_4Y.js} +1 -1
  74. package/cli/storybook-dist/assets/{RBPagination.stories-C9_49ymO.js → RBPagination.stories-YoQNUpQH.js} +1 -1
  75. package/cli/storybook-dist/assets/{RBPopover.stories-DxLE912-.js → RBPopover.stories-Bhpi0OaH.js} +1 -1
  76. package/cli/storybook-dist/assets/{RBPrompt.stories-DlsfGqWb.js → RBPrompt.stories-iQM5iAka.js} +1 -1
  77. package/cli/storybook-dist/assets/{RBRadioBox.stories-hb5cNWW1.js → RBRadioBox.stories-BGyq4rzx.js} +1 -1
  78. package/cli/storybook-dist/assets/{RBRadioButton.stories-CSuOgeN-.js → RBRadioButton.stories-DF6BcUcJ.js} +1 -1
  79. package/cli/storybook-dist/assets/RBRangeDateInput.stories-COOQ_HNF.js +15 -0
  80. package/cli/storybook-dist/assets/{RBResizable.stories-BetjF9vG.js → RBResizable.stories-DSVHvwjp.js} +1 -1
  81. package/cli/storybook-dist/assets/RBSelectBox.stories-BookV8Sm.js +74 -0
  82. package/cli/storybook-dist/assets/{RBSheet.stories-D3QbcMKq.js → RBSheet.stories-CAK9hb-p.js} +1 -1
  83. package/cli/storybook-dist/assets/{RBSideNumberInput.stories-DcTh0IjM.js → RBSideNumberInput.stories-DUymnH-9.js} +1 -1
  84. package/cli/storybook-dist/assets/{RBSlider.stories-B59We7ei.js → RBSlider.stories-BwlDh9--.js} +1 -1
  85. package/cli/storybook-dist/assets/{RBSpinner.stories-9u3_ETbi.js → RBSpinner.stories-VOlkuP6t.js} +1 -1
  86. package/cli/storybook-dist/assets/{RBSwitch.stories-DrVm1WGX.js → RBSwitch.stories-BiW2dQnT.js} +1 -1
  87. package/cli/storybook-dist/assets/{RBTab.stories-D8Dm7cRW.js → RBTab.stories-DFKVsy0C.js} +1 -1
  88. package/cli/storybook-dist/assets/{RBTable.stories-dRdZ8g1j.js → RBTable.stories-BWG0ZSYI.js} +1 -1
  89. package/cli/storybook-dist/assets/{RBTableRow.stories-rZeoGwHZ.js → RBTableRow.stories-JVTD83Mh.js} +1 -1
  90. package/cli/storybook-dist/assets/{RBTabs.stories-STRV2lgO.js → RBTabs.stories-D-aN385u.js} +1 -1
  91. package/cli/storybook-dist/assets/{RBTextArea.stories-CfgUbrbJ.js → RBTextArea.stories-Dx5xkmZS.js} +1 -1
  92. package/cli/storybook-dist/assets/{RBTextField.stories-B5t8-Y97.js → RBTextField.stories-DmVciExd.js} +1 -1
  93. package/cli/storybook-dist/assets/{RBTextInput.stories-Bf1edN6l.js → RBTextInput.stories-CCf0VGEv.js} +1 -1
  94. package/cli/storybook-dist/assets/{RBToast.stories-Cnm8JwQ6.js → RBToast.stories-DT0DGn05.js} +1 -1
  95. package/cli/storybook-dist/assets/{RBToggle.stories-B0OCo6zd.js → RBToggle.stories-Ce_q3EtP.js} +1 -1
  96. package/cli/storybook-dist/assets/{RBToggleGroup.stories-GdYO2Fxg.js → RBToggleGroup.stories-CN40YHsR.js} +1 -1
  97. package/cli/storybook-dist/assets/{RBTooltip.stories-DXJTCScO.js → RBTooltip.stories-L7btDEpA.js} +1 -1
  98. package/cli/storybook-dist/assets/SideNumberInput-B5_xLK8z.js +1109 -0
  99. package/cli/storybook-dist/assets/browserUtils-CQM9Za7e.js +1 -0
  100. package/cli/storybook-dist/assets/{entry-preview-DSRwIaVJ.js → entry-preview-raBQdKjS.js} +1 -1
  101. package/cli/storybook-dist/assets/{iframe-Cj53Wn3d.js → iframe-BmIue__l.js} +2 -2
  102. package/cli/storybook-dist/assets/{index-CkHtLEe8.js → index-CSIe-YDr.js} +3 -3
  103. package/cli/storybook-dist/assets/{index-C1OzuK6-.js → index-ClAc6FBW.js} +1 -1
  104. package/cli/storybook-dist/assets/{preview-CILRoSnB.js → preview-BVJwlGUF.js} +1 -1
  105. package/cli/storybook-dist/assets/{preview-D_2um8Sd.js → preview-CUgthyJT.js} +1 -1
  106. package/cli/storybook-dist/assets/{preview-CgOtgtCD.js → preview-DD51RTKv.js} +2 -2
  107. package/cli/storybook-dist/assets/preview-zuAvMksL.css +1 -0
  108. package/cli/storybook-dist/assets/react-resizable-panels.browser-CYcsrUgg.js +120 -0
  109. package/cli/storybook-dist/iframe.html +1 -1
  110. package/cli/storybook-dist/index.json +1 -1
  111. package/cli/storybook-dist/project.json +1 -1
  112. package/cli/templates/common/README.md +211 -7
  113. package/cli/templates/common/py/main.py +29 -2
  114. package/cli/templates/cpp/cpp/CMakeLists.txt +6 -7
  115. package/cli/templates/cpp/cpp/main.cpp +57 -10
  116. package/cli/templates/readme-cpp/README.md +993 -0
  117. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +4 -0
  118. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +5 -4
  119. package/package.json +3 -3
  120. package/cli/storybook-dist/assets/RBRangeDateInput.stories-KRg2glX8.js +0 -15
  121. package/cli/storybook-dist/assets/RBSelectBox.stories-ILLQQ8M8.js +0 -6
  122. package/cli/storybook-dist/assets/SideNumberInput-CrF-a1Nr.js +0 -1114
  123. package/cli/storybook-dist/assets/browserUtils-CxJ5gea3.js +0 -1
  124. package/cli/storybook-dist/assets/preview-CEfMaO1t.css +0 -1
  125. package/cli/storybook-dist/assets/react-resizable-panels.browser-CtORL3tG.js +0 -102
package/cli/README.md CHANGED
@@ -60,10 +60,10 @@ and `50051` are reserved and must not be used by external plugins.
60
60
  | runtime | source | notes |
61
61
  |---|---|---|
62
62
  | `{ "type": "none" }` | — | UI-only plugin; no backend container |
63
- | `{ "type": "python", "entry": "main.py" }` | `py/` | Uses `rainbow.rb_sdk.RBPluginCall` for UI/backend messaging |
64
- | `{ "type": "cpp", "entry": "<executable name>" }` | `cpp/` | Built with CMake in Docker; current template is a run skeleton |
63
+ | `{ "type": "python", "entry": "main.py" }` | `py/` | Uses `rainbow.rb_sdk.RBPluginCall` for UI/backend messaging; `plugin.router` exposes external endpoints (`common/plugin/{id}/{path}`, needs sdk ≥ 0.0.9.dev5 on the robot; the dev bridge only registers routes — external calls need a running `common`) |
64
+ | `{ "type": "cpp", "entry": "<executable name>" }` | `cpp/` | Built with CMake in Docker against the C++ SDK (apt `rainbow-rb-sdk-dev`, `find_package(rb_sdk)`, dynamic link) |
65
65
  | `{ "type": "ros2", "rosClient": "rclpy", ... }` | `ros2_ws/` | ROS2 Python node; may use Python SDK alongside ROS2 APIs |
66
- | `{ "type": "ros2", "rosClient": "rclcpp", ... }` | `ros2_ws/` | ROS2 C++ node built with colcon/CMake |
66
+ | `{ "type": "ros2", "rosClient": "rclcpp", ... }` | `ros2_ws/` | ROS2 C++ node built with colcon/CMake, links the C++ SDK like `cpp` |
67
67
 
68
68
  All non-`none` runtimes run in Docker during `muscat-pk dev:back`, `muscat-pk
69
69
  dev`, and build/export flows so development matches the robot environment.
@@ -257,6 +257,11 @@ would break `dev` offline. Pre-seed an air-gapped machine with
257
257
  `docker save … | gzip` + `docker load`, or force a rebuild with
258
258
  `MUSCAT_PK_REBUILD_IMAGE=1`.
259
259
 
260
+ The C++ images (`Dockerfile.cpp`, `Dockerfile.dev`) install `rainbow-rb-sdk-dev`
261
+ from `https://rainbowrobotics.github.io/rb-sdk` (suite `dev`), pinned by the
262
+ `RB_SDK_VERSION` build arg. Set `MUSCAT_PK_RB_SDK_VERSION=<version>` (with
263
+ `MUSCAT_PK_REBUILD_IMAGE=1`) to match a robot running a different SDK version.
264
+
260
265
  On Linux the dev containers run as the invoking user (`--user`), so build output
261
266
  under `dist/` and `cpp/build-*` is not left root-owned.
262
267
 
@@ -35,6 +35,11 @@ function readVersion(binPath) {
35
35
 
36
36
  const isBinaryMode = typeof Bun !== "undefined";
37
37
 
38
+ // 바이너리에는 package.json 이 없다 — 컴파일 시 임베드한 버전을 쓴다.
39
+ if (isBinaryMode && !process.env.MUSCAT_PK_VERSION) {
40
+ process.env.MUSCAT_PK_VERSION = (await import("../src/templates-embedded.mjs")).KIT_VERSION;
41
+ }
42
+
38
43
  (function delegateToLocalInstall() {
39
44
  try {
40
45
  const localBin = resolveLocalCliBin();
@@ -2,7 +2,7 @@
2
2
  // Pre-build step: serialise cli/templates/** into cli/src/templates-embedded.mjs
3
3
  // so that `bun build --compile` can embed them without needing the filesystem.
4
4
  import { readdirSync, readFileSync, writeFileSync, statSync } from "node:fs";
5
- import { resolve, relative } from "node:path";
5
+ import { resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
8
  const root = resolve(fileURLToPath(import.meta.url), "..");
@@ -19,10 +19,15 @@ 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
+
25
+ const { version } = JSON.parse(readFileSync(resolve(root, "../package.json"), "utf8"));
22
26
 
23
27
  writeFileSync(out,
24
28
  `// AUTO-GENERATED by cli/bundle-templates.mjs — do not edit.\n` +
25
29
  `// ponytail: generated at publish time; not tracked in git.\n` +
30
+ `export const KIT_VERSION = ${JSON.stringify(version)};\n` +
26
31
  `export const TEMPLATES = ${JSON.stringify(files)};\n`
27
32
  );
28
33
  console.log(`bundled ${Object.keys(files).length} template files → src/templates-embedded.mjs`);
@@ -10,12 +10,34 @@ 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
- # TODO(cpp-sdk): install the rainbow cpp SDK (dev package) here, pinned to the
14
- # robot's version, so plugins build/link against the same SDK as production.
15
- # Plan: apt package + dynamic link (the robot provides the .so at runtime;
16
- # we do NOT bundle it into the plugin binary). e.g.:
17
- # RUN apt-get update && apt-get install -y rainbow-cpp-sdk-dev=<robot-version> \
18
- # && rm -rf /var/lib/apt/lists/*
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
16
+ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
17
+ && install -d /etc/apt/keyrings \
18
+ && curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key -o /etc/apt/keyrings/rb-sdk.asc \
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 \
20
+ && apt-get update && apt-get install -y --no-install-recommends "rainbow-rb-sdk-dev=${RB_SDK_VERSION}" \
21
+ && rm -rf /var/lib/apt/lists/*
22
+
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
19
41
 
20
42
  WORKDIR /plugin
21
43
 
@@ -9,6 +9,38 @@ 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
15
+ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
16
+ && install -d /etc/apt/keyrings \
17
+ && curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key -o /etc/apt/keyrings/rb-sdk.asc \
18
+ && 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
+ && rm -rf /var/lib/apt/lists/*
21
+
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
+
41
+ # `docker exec` skips /ros_entrypoint.sh — source ROS for the login shells dev:back runs.
42
+ RUN echo ". /opt/ros/${ROS_DISTRO}/setup.sh" > /etc/profile.d/ros.sh
43
+
12
44
  WORKDIR /plugin
13
45
 
14
46
  CMD ["bash"]
@@ -15,6 +15,25 @@ 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
  WORKDIR /plugin
19
38
 
20
39
  CMD ["bash"]
@@ -5,24 +5,41 @@
5
5
  // it at localhost:10000 too — no host.docker.internal needed (robust cross-OS).
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { createServer, connect } from "node:net";
8
- import { existsSync } from "node:fs";
9
- import { resolve } from "node:path";
8
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { dirname, resolve } from "node:path";
10
11
  import { canSkipImageBuild, hasBin, kitRoot, log, pruneMuskatImages } from "./util.mjs";
12
+ import { MEDIA_PORTS, mediaPortFlags } from "./ports.mjs";
13
+ import { usesHostNetwork } from "./zenohDev.mjs";
11
14
 
12
15
  export const BRIDGE_IMAGE = "muscat-pk-bridge";
13
16
  export const BRIDGE_CONTAINER = "muscat-pk-bridge-run";
14
17
  export const BRIDGE_PORT = 10000;
15
18
 
16
- const DOCKERFILE = resolve(kitRoot, "cli/docker/Dockerfile.bridge");
17
- const BUILD_CONTEXT = resolve(kitRoot, "cli");
19
+ // 컴파일된 바이너리에는 cli/ 트리가 없어 임베드 스냅샷에서 build context 를 tmp 에 복원한다.
20
+ const BRIDGE_CONTEXT_FILES = ["docker/Dockerfile.bridge", "docker/bridge-package.json", "src/socketBridge.mjs"];
21
+
22
+ async function resolveBridgeContext() {
23
+ const context = resolve(kitRoot, "cli");
24
+ const dockerfile = resolve(context, "docker/Dockerfile.bridge");
25
+ if (existsSync(dockerfile)) return { dockerfile, context };
26
+
27
+ const { TEMPLATES } = await import("./templates-embedded.mjs");
28
+ const dir = resolve(tmpdir(), "muscat-pk-bridge-context");
29
+ for (const key of BRIDGE_CONTEXT_FILES) {
30
+ const b64 = TEMPLATES[key];
31
+ if (!b64) throw new Error(`Embedded file missing: ${key}. Reinstall muscat-plugin-kit.`);
32
+ const file = resolve(dir, key);
33
+ mkdirSync(dirname(file), { recursive: true });
34
+ writeFileSync(file, Buffer.from(b64, "base64"));
35
+ }
36
+ return { dockerfile: resolve(dir, "docker/Dockerfile.bridge"), context: dir };
37
+ }
18
38
 
19
39
  function assertDockerReady() {
20
40
  if (!hasBin("docker")) {
21
41
  throw new Error("Docker not found. `muscat-pk dev` runs the Socket.IO bridge in a container.");
22
42
  }
23
- if (!existsSync(DOCKERFILE)) {
24
- throw new Error(`kit Dockerfile missing at ${DOCKERFILE} (reinstall muscat-plugin-kit).`);
25
- }
26
43
  }
27
44
 
28
45
  // Is a host TCP port already bound? (Catches another bridge or a stray process.)
@@ -52,20 +69,28 @@ function waitForPort(port, timeoutMs = 15_000) {
52
69
  });
53
70
  }
54
71
 
55
- function ensureImage() {
72
+ async function ensureImage() {
56
73
  if (canSkipImageBuild(BRIDGE_IMAGE)) {
57
74
  log.step(`${BRIDGE_IMAGE} already built — skipping docker build`);
58
75
  return;
59
76
  }
77
+ const { dockerfile, context } = await resolveBridgeContext();
60
78
  log.step(`docker build -t ${BRIDGE_IMAGE} (first run is slow, then cached)…`);
61
- const r = spawnSync("docker", ["build", "-t", BRIDGE_IMAGE, "-f", DOCKERFILE, BUILD_CONTEXT], { stdio: "inherit" });
79
+ const r = spawnSync("docker", ["build", "-t", BRIDGE_IMAGE, "-f", dockerfile, context], { stdio: "inherit" });
62
80
  if (r.status !== 0) throw new Error("docker build failed (bridge image).");
63
81
  pruneMuskatImages();
64
82
  }
65
83
 
84
+ /** A running Muscat container (e.g. muscat-dev) that owns host port 10000, or null. */
85
+ export function localMuscatContainer() {
86
+ const r = spawnSync("docker", ["ps", "--filter", `publish=${BRIDGE_PORT}`, "--format", "{{.Names}}"], { encoding: "utf8" });
87
+ return (r.stdout || "").split("\n").map((n) => n.trim()).find((n) => n && n !== BRIDGE_CONTAINER) ?? null;
88
+ }
89
+
66
90
  /**
67
91
  * Build the image, preflight the port, and start the bridge container.
68
- * Returns true if bridge started, false if port was already in use (caller decides).
92
+ * Returns false if port 10000 was already in use (caller decides), else { media }:
93
+ * whether the MediaMTX ports are free for the backend's own MediaMTX.
69
94
  */
70
95
  export async function startBridge({ zenohPort = null, pluginPorts = [] } = {}) {
71
96
  assertDockerReady();
@@ -88,7 +113,14 @@ export async function startBridge({ zenohPort = null, pluginPorts = [] } = {}) {
88
113
  }
89
114
  const publishable = pluginPorts.filter((port) => !busy.includes(port));
90
115
 
91
- ensureImage();
116
+ const mediaBusy = [];
117
+ for (const port of MEDIA_PORTS) {
118
+ if (await isPortInUse(port)) mediaBusy.push(port);
119
+ }
120
+ const media = mediaBusy.length === 0;
121
+ if (!media) log.warn(`MediaMTX port(s) already in use: ${mediaBusy.join(", ")} — video streaming disabled in dev.`);
122
+
123
+ await ensureImage();
92
124
 
93
125
  log.step("starting Socket.IO bridge container…");
94
126
  // The backend joins THIS container's netns (--network container:), and a
@@ -98,6 +130,7 @@ export async function startBridge({ zenohPort = null, pluginPorts = [] } = {}) {
98
130
  "-p", `127.0.0.1:${BRIDGE_PORT}:${BRIDGE_PORT}`,
99
131
  ...(zenohPort ? ["-p", `${zenohPort}:${zenohPort}`] : []),
100
132
  ...publishable.flatMap((port) => ["-p", `127.0.0.1:${port}:${port}`]),
133
+ ...(media && !usesHostNetwork() ? mediaPortFlags() : []),
101
134
  ];
102
135
  const up = spawnSync(
103
136
  "docker",
@@ -111,7 +144,7 @@ export async function startBridge({ zenohPort = null, pluginPorts = [] } = {}) {
111
144
  if (zenohPort) log.step(`Zenoh dev peer port published on :${zenohPort}`);
112
145
  if (publishable.length > 0)
113
146
  log.step(`plugin backend ports published on 127.0.0.1: ${publishable.join(", ")}`);
114
- return true;
147
+ return { media };
115
148
  }
116
149
 
117
150
  export function stopBridge() {
package/cli/src/build.mjs CHANGED
@@ -74,34 +74,36 @@ function runViteBuild(pluginRoot, { mode = "production" } = {}) {
74
74
  // For ros2 plugins, compile the colcon workspace inside the Jazzy + Python 3.12 container.
75
75
  // rclpy: arch-agnostic (pure Python) → dist/ros2_ws/install/ (built once)
76
76
  // rclcpp: arch-specific C++ binaries → dist/ros2_ws/install-{arch}/ per arch
77
+ // Cross-arch C++ builds run inside an emulated container. Docker Desktop ships the
78
+ // emulation; native Linux needs binfmt/QEMU — check up front so the error says so.
79
+ function assertCanBuildArch(arch, dockerPlatform) {
80
+ const info = spawnSync("docker", ["info", "--format", "{{.Architecture}}"], { encoding: "utf8" });
81
+ const host = { x86_64: "amd64", aarch64: "arm64", arm64: "arm64" }[(info.stdout || "").trim()] ?? "";
82
+ if (arch === host) return;
83
+ const r = spawnSync("docker", ["run", "--rm", "--platform", dockerPlatform, "ubuntu:24.04", "true"], { stdio: "ignore" });
84
+ if (r.status === 0) return;
85
+ throw new Error(
86
+ `can't build ${arch} on this ${host || "machine"}: Docker has no ${arch} emulation.\n` +
87
+ ` enable it once: docker run --privileged --rm tonistiigi/binfmt --install ${arch}\n` +
88
+ ` or build only this machine's arch: muscat-pk build --arch ${host || "<arch>"}`,
89
+ );
90
+ }
91
+
77
92
  export async function exportRos2Build(root, archs = ["amd64", "arm64"]) {
78
- if (manifestRuntime(root) !== "ros2") return [];
93
+ if (manifestRuntime(root) !== "ros2") return;
79
94
  const { assertDockerReady, ensureImage, colconBuild, ROS2_DEV_IMAGE, ARCH_DOCKER_PLATFORM } = await import("./ros2.mjs");
80
- try {
81
- assertDockerReady();
82
- } catch (e) {
83
- log.warn(`ros2 build skipped: ${e.message}`);
84
- return [];
85
- }
95
+ assertDockerReady();
86
96
 
87
97
  const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8"));
88
- const rosClient = manifest.runtime?.rosClient;
89
-
90
- const skipped = [];
91
- if (rosClient === "rclcpp") {
98
+ if (manifest.runtime?.rosClient === "rclcpp") {
92
99
  // C++ ROS2: compile per arch so each robot picks the right install-{arch}/
93
100
  for (const arch of archs) {
94
101
  const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
95
- if (!dockerPlatform) { log.warn(`unknown arch '${arch}' — skipped`); skipped.push(arch); continue; }
96
- const archImage = `${ROS2_DEV_IMAGE}-${arch}`;
97
- try {
98
- await ensureImage(archImage, { platform: dockerPlatform });
99
- colconBuild(root, { symlink: false, arch });
100
- log.ok(`dist/ros2_ws/install-${arch}/ ready`);
101
- } catch {
102
- log.warn(`${arch} ros2 build skipped — QEMU not available`);
103
- skipped.push(arch);
104
- }
102
+ if (!dockerPlatform) throw new Error(`unknown arch '${arch}'`);
103
+ assertCanBuildArch(arch, dockerPlatform);
104
+ await ensureImage(`${ROS2_DEV_IMAGE}-${arch}`, { platform: dockerPlatform });
105
+ colconBuild(root, { symlink: false, arch });
106
+ log.ok(`dist/ros2_ws/install-${arch}/ ready`);
105
107
  }
106
108
  } else {
107
109
  // rclpy / pure Python: arch-agnostic, build once
@@ -109,54 +111,38 @@ export async function exportRos2Build(root, archs = ["amd64", "arm64"]) {
109
111
  colconBuild(root, { symlink: false });
110
112
  log.ok("dist/ros2_ws/install/ ready");
111
113
  }
112
- return skipped;
113
114
  }
114
115
 
115
116
  // For cpp plugins, compile cpp/ inside the Ubuntu container (Release) per arch
116
117
  // and copy each binary into dist/{arch}/{entry} so the ZIP carries relocatable
117
- // robot-compatible executables. rb-runtime prefers dist/{arch}/ and falls back
118
- // to dist/{entry} (legacy). Skipped (with a warning) if Docker is unavailable.
118
+ // robot-compatible executables.
119
119
  export async function exportCppBuild(root, distDir, archs = ["amd64", "arm64"]) {
120
- if (manifestRuntime(root) !== "cpp") return [];
120
+ if (manifestRuntime(root) !== "cpp") return;
121
121
  const { assertDockerReady, ensureImage, cmakeBuild, CPP_DEV_IMAGE, ARCH_DOCKER_PLATFORM } = await import("./cpp.mjs");
122
- try {
123
- assertDockerReady();
124
- } catch (e) {
125
- log.warn(`cpp build skipped: ${e.message}`);
126
- return [];
127
- }
122
+ assertDockerReady();
128
123
 
129
124
  const entry = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime?.entry;
130
- if (!entry) {
131
- log.warn("manifest.json runtime.entry not set — cpp build skipped");
132
- return;
133
- }
125
+ if (!entry) throw new Error("manifest.json runtime.entry is required for cpp (the built executable name)");
134
126
 
135
- const skipped = [];
136
127
  for (const arch of archs) {
137
128
  const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
138
- if (!dockerPlatform) { log.warn(`unknown arch '${arch}' — skipped`); skipped.push(arch); continue; }
139
- const archImage = `${CPP_DEV_IMAGE}-${arch}`;
140
- try {
141
- await ensureImage(archImage, { platform: dockerPlatform });
142
- cmakeBuild(root, { buildType: "Release", arch });
143
- const built = resolve(root, `cpp/build-${arch}`, entry);
144
- if (existsSync(built)) {
145
- const archDir = resolve(distDir, arch);
146
- mkdirSync(archDir, { recursive: true });
147
- const dest = resolve(archDir, entry);
148
- copyFileSync(built, dest);
149
- chmodSync(dest, 0o755);
150
- log.ok(`cpp binary → dist/${arch}/${entry}`);
151
- } else {
152
- log.warn(`cpp binary not found at cpp/build-${arch}/${entry}`);
153
- }
154
- } catch {
155
- log.warn(`${arch} cpp build skipped — QEMU not available`);
156
- skipped.push(arch);
129
+ if (!dockerPlatform) throw new Error(`unknown arch '${arch}'`);
130
+ assertCanBuildArch(arch, dockerPlatform);
131
+ await ensureImage(`${CPP_DEV_IMAGE}-${arch}`, { platform: dockerPlatform });
132
+ cmakeBuild(root, { buildType: "Release", arch });
133
+ const built = resolve(root, `cpp/build-${arch}`, entry);
134
+ if (!existsSync(built)) {
135
+ throw new Error(`cpp/build-${arch}/${entry} not found — the CMake target name must equal runtime.entry`);
157
136
  }
137
+ const archDir = resolve(distDir, arch);
138
+ mkdirSync(archDir, { recursive: true });
139
+ const dest = resolve(archDir, entry);
140
+ copyFileSync(built, dest);
141
+ chmodSync(dest, 0o755);
142
+ const libDir = resolve(root, `cpp/build-${arch}`, "lib");
143
+ if (existsSync(libDir)) cpSync(libDir, resolve(archDir, "lib"), { recursive: true, verbatimSymlinks: true });
144
+ log.ok(`cpp binary → dist/${arch}/${entry}${existsSync(libDir) ? " (+ lib/)" : ""}`);
158
145
  }
159
- return skipped;
160
146
  }
161
147
 
162
148
  const PRERELEASE_RE = /\.(dev|a|b|rc|alpha|beta)\d*/i;
@@ -426,11 +412,12 @@ export async function runBuild(args) {
426
412
 
427
413
  await promptVersion(root);
428
414
  const archs = await promptArch(args);
415
+ // Drop a previous ZIP of this version so a failed build can't leave a stale one to upload.
416
+ const version = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).version || "0.0.0";
417
+ rmSync(resolve(root, `${basename(root)}-${version}.zip`), { force: true });
429
418
 
430
419
  const { pluginRoot, distDir } = await buildPlugin({ root, minify: true });
431
420
 
432
- const skippedArches = { wheels: [], ros2: [], cpp: [] };
433
-
434
421
  // python / rclpy: lock + wheel vendor in Ubuntu 24.04 container
435
422
  if (!noLock) {
436
423
  const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime ?? {};
@@ -439,30 +426,12 @@ export async function runBuild(args) {
439
426
  (manifest.type === "ros2" && manifest.rosClient === "rclpy");
440
427
  if (needsLock) {
441
428
  const { lockAndVendor } = await import("./python.mjs");
442
- try {
443
- skippedArches.wheels = await lockAndVendor(pluginRoot, archs);
444
- } catch (e) {
445
- log.warn(`python lock/vendor skipped: ${e.message}`);
446
- skippedArches.wheels = archs;
447
- }
429
+ await lockAndVendor(pluginRoot, archs);
448
430
  }
449
431
  }
450
432
 
451
- skippedArches.ros2 = await exportRos2Build(root, archs) ?? [];
452
- skippedArches.cpp = await exportCppBuild(root, distDir, archs) ?? [];
433
+ await exportRos2Build(root, archs);
434
+ await exportCppBuild(root, distDir, archs);
453
435
  calcInstalledSize(pluginRoot);
454
436
  if (!noZip) packZip(pluginRoot);
455
-
456
- // Build summary
457
- const allSkipped = [...new Set([...skippedArches.wheels, ...skippedArches.ros2, ...skippedArches.cpp])];
458
- if (allSkipped.length > 0) {
459
- const line = "─".repeat(58);
460
- console.log(`\n${line}`);
461
- log.warn(`Cross-arch builds skipped (QEMU not available): ${allSkipped.join(", ")}`);
462
- if (skippedArches.wheels.length) log.warn(` wheels: ${skippedArches.wheels.join(", ")}`);
463
- if (skippedArches.ros2.length) log.warn(` ros2: ${skippedArches.ros2.join(", ")}`);
464
- if (skippedArches.cpp.length) log.warn(` cpp: ${skippedArches.cpp.join(", ")}`);
465
- log.warn(` Enable: docker run --privileged --rm tonistiigi/binfmt --install all`);
466
- console.log(line);
467
- }
468
437
  }
package/cli/src/cpp.mjs CHANGED
@@ -3,7 +3,7 @@
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, dockerUserFlags, hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
6
+ import { canSkipImageBuild, imageKey, imageKeyLabel, dockerUserFlags, hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
7
7
 
8
8
  export const CPP_DEV_IMAGE = "muscat-pk-cpp-dev";
9
9
 
@@ -15,16 +15,19 @@ export function assertDockerReady() {
15
15
 
16
16
  /** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
17
17
  export async function ensureImage(image = CPP_DEV_IMAGE, { platform = null } = {}) {
18
- if (canSkipImageBuild(image)) {
18
+ const dockerfile = await resolveDockerfile("Dockerfile.cpp");
19
+ const sdkVer = process.env.MUSCAT_PK_RB_SDK_VERSION;
20
+ const key = imageKey(dockerfile, sdkVer ?? "");
21
+ if (canSkipImageBuild(image, key)) {
19
22
  log.step(`${image} already built — skipping docker build`);
20
23
  return;
21
24
  }
22
- const dockerfile = await resolveDockerfile("Dockerfile.cpp");
23
25
  log.step(`docker build -t ${image}${platform ? ` (${platform})` : ""} (first run is slow, then cached)…`);
24
26
  const platformFlags = platform ? ["--platform", platform] : [];
27
+ const buildArgs = [...(sdkVer ? ["--build-arg", `RB_SDK_VERSION=${sdkVer}`] : []), ...imageKeyLabel(key)];
25
28
  const r = spawnSync(
26
29
  "docker",
27
- ["build", ...platformFlags, "-t", image, "-f", dockerfile, dirname(dockerfile)],
30
+ ["build", ...platformFlags, ...buildArgs, "-t", image, "-f", dockerfile, dirname(dockerfile)],
28
31
  { stdio: "inherit" },
29
32
  );
30
33
  if (r.status !== 0) throw new Error("docker build failed.");
@@ -50,9 +53,12 @@ export function cmakeBuild(root, { image = CPP_DEV_IMAGE, buildType = "Debug", a
50
53
  const platformFlags = dockerPlatform ? ["--platform", dockerPlatform] : [];
51
54
  // arch 빌드 시 arch별 이미지 사용 (로컬 이미지와 platform 불일치 방지)
52
55
  const actualImage = arch ? `${CPP_DEV_IMAGE}-${arch}` : image;
56
+ // Export builds ship lib/ next to the binary: librb_sdk.so (robot has no SDK) + any .so the
57
+ // plugin copied there itself. Found via RPATH $ORIGIN/lib.
53
58
  const script = [
54
- `cmake -S cpp -B ${buildDir} -DCMAKE_BUILD_TYPE=${buildType}`,
59
+ `cmake -S cpp -B ${buildDir} -DCMAKE_BUILD_TYPE=${buildType}` + (arch ? ` -DCMAKE_BUILD_RPATH='$ORIGIN/lib'` : ""),
55
60
  `cmake --build ${buildDir} -j`,
61
+ ...(arch ? [`rm -f ${buildDir}/lib/librb_sdk.so.* && if readelf -d $(find ${buildDir} -maxdepth 1 -type f -executable) 2>/dev/null | grep -q librb_sdk; then mkdir -p ${buildDir}/lib && cp -a $(dpkg -L rainbow-rb-sdk | grep -E '/librb_sdk[.]so[.][0-9]') ${buildDir}/lib/; fi`] : []),
56
62
  ].join(" && ");
57
63
  const label = arch ? `cmake build (${buildType}, ${arch}, in container)…` : `cmake build (${buildType}, in container)…`;
58
64
  log.step(label);
package/cli/src/dev.mjs CHANGED
@@ -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 { startBridge, stopBridge, BRIDGE_CONTAINER, BRIDGE_PORT } from "./bridge.mjs";
9
+ import { localMuscatContainer, startBridge, stopBridge, BRIDGE_CONTAINER, BRIDGE_PORT } from "./bridge.mjs";
10
10
  import { assertManifestPorts, assertManifestVariables } from "./manifest.mjs";
11
11
  import { formatPortRanges } from "./ports.mjs";
12
12
  import {
@@ -138,7 +138,11 @@ export async function runDev(args = []) {
138
138
  pluginPorts: usesHostNetwork() ? [] : portRanges,
139
139
  });
140
140
 
141
- if (!bridgeStarted) {
141
+ const muscat = bridgeStarted ? null : localMuscatContainer();
142
+ if (muscat) {
143
+ log.step(`local Muscat container '${muscat}' owns :${BRIDGE_PORT} — backend joins its network (gateway, MediaMTX)`);
144
+ process.env.MUSCAT_PK_MUSCAT_CONTAINER = muscat;
145
+ } else if (!bridgeStarted) {
142
146
  log.warn(`port ${BRIDGE_PORT} is already in use — can't start Socket.IO bridge.`);
143
147
  const answer = await prompt(" Proceed without Socket.IO bridge? [y/N]: ");
144
148
  if (answer.toLowerCase() !== "y") return;
@@ -147,6 +151,7 @@ export async function runDev(args = []) {
147
151
  // Tell dev:back which bridge container to share a netns with, so the backend
148
152
  // reaches the bridge at localhost:10000 (no host.docker.internal).
149
153
  process.env.MUSCAT_PK_BRIDGE_CONTAINER = BRIDGE_CONTAINER;
154
+ if (bridgeStarted.media) process.env.MUSCAT_PK_MEDIA = "1";
150
155
  }
151
156
 
152
157
  process.env.MUSCAT_PK_MANAGED_DEV = "1";