@rainbow-robotics/muscat-plugin-kit 1.3.6 → 1.3.8-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +1 -1
  2. package/cli/README.md +7 -1
  3. package/cli/docker/Dockerfile.cpp +4 -3
  4. package/cli/docker/Dockerfile.dev +2 -2
  5. package/cli/src/bridge.mjs +1 -1
  6. package/cli/src/build.mjs +5 -5
  7. package/cli/src/completion.mjs +1 -1
  8. package/cli/src/cpp.mjs +11 -3
  9. package/cli/src/cppIde.mjs +75 -0
  10. package/cli/src/dev.mjs +9 -2
  11. package/cli/src/devBack.mjs +21 -2
  12. package/cli/src/devUi.mjs +10 -1
  13. package/cli/src/muscatLocal.mjs +160 -0
  14. package/cli/src/new.mjs +15 -0
  15. package/cli/src/ros2.mjs +11 -3
  16. package/cli/src/sdk.mjs +48 -2
  17. package/cli/src/type.mjs +2 -0
  18. package/cli/src/util.mjs +44 -1
  19. package/cli/storybook-dist/assets/{Color-YHDXOIA2-CdTgUqrV.js → Color-YHDXOIA2-DeirXVfl.js} +1 -1
  20. package/cli/storybook-dist/assets/{Docs-CbRKwHpQ.js → Docs-BLxyi8cP.js} +1 -1
  21. package/cli/storybook-dist/assets/{Docs-CsxMprX5.js → Docs-Buyf7Djy.js} +1 -1
  22. package/cli/storybook-dist/assets/{Docs-FyPFrbUS.js → Docs-BxmwdaYa.js} +1 -1
  23. package/cli/storybook-dist/assets/{Docs-C8wx5zsC.js → Docs-CnfIOwf8.js} +1 -1
  24. package/cli/storybook-dist/assets/{Docs-Cf0s04QW.js → Docs-NrBqEYvM.js} +1 -1
  25. package/cli/storybook-dist/assets/{Docs-YtqU-aVu.js → Docs-wzFMK-XA.js} +1 -1
  26. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-VSENNRoY.js → DocsRenderer-CFRXHY34-CWc43rTP.js} +1 -1
  27. package/cli/storybook-dist/assets/{NxAvatar.stories-oCcYtLwq.js → NxAvatar.stories-DfL0kqmN.js} +1 -1
  28. package/cli/storybook-dist/assets/{NxButton.stories-C5jUScIj.js → NxButton.stories-WxSc_gsR.js} +1 -1
  29. package/cli/storybook-dist/assets/{NxNumberInput.stories-gjsEulIN.js → NxNumberInput.stories-C1D3oUiY.js} +1 -1
  30. package/cli/storybook-dist/assets/{NxPagination.stories-BK6uSWIa.js → NxPagination.stories-C1Qa88rn.js} +1 -1
  31. package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BDD5CvSZ.js → NxPointerHoldButton.stories-CmteJX3t.js} +1 -1
  32. package/cli/storybook-dist/assets/{NxRangeInput.stories-C_nW26_x.js → NxRangeInput.stories-B7E-mhyI.js} +1 -1
  33. package/cli/storybook-dist/assets/{NxSelectBox.stories-DqLfKU9H.js → NxSelectBox.stories-De67vMdU.js} +1 -1
  34. package/cli/storybook-dist/assets/{NxSlideRange.stories-CQjHA8yv.js → NxSlideRange.stories-DvCV1MGk.js} +1 -1
  35. package/cli/storybook-dist/assets/{NxSwitch.stories-BMHCiPoL.js → NxSwitch.stories-DuHDZ9-v.js} +1 -1
  36. package/cli/storybook-dist/assets/{NxTextArea.stories-k7r1WVQh.js → NxTextArea.stories-BT1o0ngg.js} +1 -1
  37. package/cli/storybook-dist/assets/{NxTextInput.stories-Cqfe1I32.js → NxTextInput.stories-CSLBT83Q.js} +1 -1
  38. package/cli/storybook-dist/assets/{RBAccordion.stories-CJ3Ttuja.js → RBAccordion.stories-QavkYgV0.js} +1 -1
  39. package/cli/storybook-dist/assets/{RBAccordionGroup.stories-BThv7tM0.js → RBAccordionGroup.stories-C0RPGnO7.js} +1 -1
  40. package/cli/storybook-dist/assets/{RBAlert.stories-CSMP8Vun.js → RBAlert.stories-BUCAT0Sd.js} +1 -1
  41. package/cli/storybook-dist/assets/{RBBadge.stories-cBMA2R6F.js → RBBadge.stories-CnBXTQ4J.js} +1 -1
  42. package/cli/storybook-dist/assets/{RBBattery.stories-BKVOskqs.js → RBBattery.stories-5FbsZcdy.js} +1 -1
  43. package/cli/storybook-dist/assets/{RBCard.stories-BTOfK0HC.js → RBCard.stories-FwTKjtCi.js} +1 -1
  44. package/cli/storybook-dist/assets/{RBCheckBox.stories-DyE912wG.js → RBCheckBox.stories-C0y5psMI.js} +1 -1
  45. package/cli/storybook-dist/assets/{RBCheckIconButton.stories-0sLFU0kM.js → RBCheckIconButton.stories-sJjAZBVh.js} +1 -1
  46. package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-c8VecClE.js → RBCheckLabelButton.stories-COJqEkDr.js} +1 -1
  47. package/cli/storybook-dist/assets/{RBChipTab.stories-CQ9Uav5_.js → RBChipTab.stories-BnXj-Woa.js} +1 -1
  48. package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-Ap4NlhAZ.js → RBChipsBoxTab.stories-DE1yl3rz.js} +1 -1
  49. package/cli/storybook-dist/assets/{RBColorIcons.stories-BPPXjif2.js → RBColorIcons.stories-CYASERLQ.js} +1 -1
  50. package/cli/storybook-dist/assets/{RBCommand.stories-PDpYOKlm.js → RBCommand.stories-BYkWjdMG.js} +1 -1
  51. package/cli/storybook-dist/assets/{RBConfirm.stories-BEWvMAYP.js → RBConfirm.stories-XLLTJZRf.js} +1 -1
  52. package/cli/storybook-dist/assets/{RBDateInput.stories-DLdyNnVF.js → RBDateInput.stories-DpVylrQl.js} +1 -1
  53. package/cli/storybook-dist/assets/{RBDialog.stories-HsawMNOb.js → RBDialog.stories-DcPRAan8.js} +1 -1
  54. package/cli/storybook-dist/assets/{RBDivider.stories-dlWwOxik.js → RBDivider.stories-NOTo-jJG.js} +1 -1
  55. package/cli/storybook-dist/assets/{RBDrawer.stories-C87iqGk0.js → RBDrawer.stories-CUZaQyw8.js} +1 -1
  56. package/cli/storybook-dist/assets/{RBDropdownMenu.stories-B8EQhA0z.js → RBDropdownMenu.stories-5UC0ZA3H.js} +1 -1
  57. package/cli/storybook-dist/assets/{RBEmptyView.stories-SzpIhNDw.js → RBEmptyView.stories-BIRNPjwt.js} +1 -1
  58. package/cli/storybook-dist/assets/{RBIconButton.stories-BQ0nM1gb.js → RBIconButton.stories-CJWeIQTt.js} +1 -1
  59. package/cli/storybook-dist/assets/{RBIcons.stories-C19g5M5z.js → RBIcons.stories-Cu0txCUp.js} +1 -1
  60. package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-DbgI4-1P.js → RBInlineLabelButton.stories-DnGhYrxa.js} +1 -1
  61. package/cli/storybook-dist/assets/{RBJoyStick.stories-smF-vpGL.js → RBJoyStick.stories-BlfvKcGb.js} +1 -1
  62. package/cli/storybook-dist/assets/{RBLabelButton.stories-s53klwDv.js → RBLabelButton.stories-CFwmlhAE.js} +1 -1
  63. package/cli/storybook-dist/assets/{RBLight.stories-BnzkoR51.js → RBLight.stories-wMrFLJ7u.js} +1 -1
  64. package/cli/storybook-dist/assets/{RBMenubar.stories-CW014klu.js → RBMenubar.stories-Bu5wtV-T.js} +1 -1
  65. package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-qa4tH_4Y.js → RBMultiRangeInput.stories-Db1d940z.js} +1 -1
  66. package/cli/storybook-dist/assets/{RBPagination.stories-YoQNUpQH.js → RBPagination.stories-NCpjg-2W.js} +1 -1
  67. package/cli/storybook-dist/assets/{RBPopover.stories-Bhpi0OaH.js → RBPopover.stories-CqsM8byU.js} +1 -1
  68. package/cli/storybook-dist/assets/{RBPrompt.stories-iQM5iAka.js → RBPrompt.stories-TMQEaHk-.js} +1 -1
  69. package/cli/storybook-dist/assets/{RBRadioBox.stories-BGyq4rzx.js → RBRadioBox.stories-pnsBvtzU.js} +1 -1
  70. package/cli/storybook-dist/assets/{RBRadioButton.stories-DF6BcUcJ.js → RBRadioButton.stories-BMDBfq82.js} +1 -1
  71. package/cli/storybook-dist/assets/{RBRangeDateInput.stories-COOQ_HNF.js → RBRangeDateInput.stories-ljAsdz0Q.js} +1 -1
  72. package/cli/storybook-dist/assets/{RBResizable.stories-DSVHvwjp.js → RBResizable.stories-B5YDW687.js} +1 -1
  73. package/cli/storybook-dist/assets/{RBSelectBox.stories-BookV8Sm.js → RBSelectBox.stories-DPtyIwEr.js} +1 -1
  74. package/cli/storybook-dist/assets/{RBSheet.stories-CAK9hb-p.js → RBSheet.stories-DMTeF_hx.js} +1 -1
  75. package/cli/storybook-dist/assets/{RBSideNumberInput.stories-DUymnH-9.js → RBSideNumberInput.stories-DLsS3mzL.js} +1 -1
  76. package/cli/storybook-dist/assets/{RBSlider.stories-BwlDh9--.js → RBSlider.stories-BjTYZ3ri.js} +1 -1
  77. package/cli/storybook-dist/assets/{RBSpinner.stories-VOlkuP6t.js → RBSpinner.stories-BWBvOGUY.js} +1 -1
  78. package/cli/storybook-dist/assets/{RBSwitch.stories-BiW2dQnT.js → RBSwitch.stories-tpthe98k.js} +1 -1
  79. package/cli/storybook-dist/assets/{RBTab.stories-DFKVsy0C.js → RBTab.stories-BijmaLIo.js} +1 -1
  80. package/cli/storybook-dist/assets/{RBTable.stories-BWG0ZSYI.js → RBTable.stories-Dojsz28q.js} +1 -1
  81. package/cli/storybook-dist/assets/{RBTableRow.stories-JVTD83Mh.js → RBTableRow.stories-CQajMiEa.js} +1 -1
  82. package/cli/storybook-dist/assets/{RBTabs.stories-D-aN385u.js → RBTabs.stories-BslRXoCw.js} +1 -1
  83. package/cli/storybook-dist/assets/{RBTextArea.stories-Dx5xkmZS.js → RBTextArea.stories-N44Q45Xp.js} +1 -1
  84. package/cli/storybook-dist/assets/{RBTextField.stories-DmVciExd.js → RBTextField.stories-scpiy0Kx.js} +1 -1
  85. package/cli/storybook-dist/assets/{RBTextInput.stories-CCf0VGEv.js → RBTextInput.stories-D_suHJaA.js} +1 -1
  86. package/cli/storybook-dist/assets/{RBToast.stories-DT0DGn05.js → RBToast.stories-B73JQcKo.js} +1 -1
  87. package/cli/storybook-dist/assets/{RBToggle.stories-Ce_q3EtP.js → RBToggle.stories-CmujM833.js} +1 -1
  88. package/cli/storybook-dist/assets/{RBToggleGroup.stories-CN40YHsR.js → RBToggleGroup.stories-DhCAfiKu.js} +1 -1
  89. package/cli/storybook-dist/assets/{RBTooltip.stories-L7btDEpA.js → RBTooltip.stories-BixEjKZy.js} +1 -1
  90. package/cli/storybook-dist/assets/{SideNumberInput-B5_xLK8z.js → SideNumberInput-B7fbx2jk.js} +83 -83
  91. package/cli/storybook-dist/assets/{entry-preview-raBQdKjS.js → entry-preview-BeR5Td7C.js} +1 -1
  92. package/cli/storybook-dist/assets/{iframe-BmIue__l.js → iframe-D3pKf_6w.js} +2 -2
  93. package/cli/storybook-dist/assets/{index-CSIe-YDr.js → index-gXlU0WyC.js} +3 -3
  94. package/cli/storybook-dist/assets/preview-BYhMD55w.css +1 -0
  95. package/cli/storybook-dist/assets/preview-D-VpQB0_.js +3 -0
  96. package/cli/storybook-dist/assets/{preview-DD51RTKv.js → preview-GwaHbm9r.js} +2 -2
  97. package/cli/storybook-dist/assets/react-resizable-panels-DJMuB2U0.css +1 -0
  98. package/cli/storybook-dist/iframe.html +1 -1
  99. package/cli/storybook-dist/project.json +1 -1
  100. package/cli/templates/common/requirements.txt +3 -3
  101. package/cli/templates/cpp/cpp/CMakeLists.txt +3 -2
  102. package/cli/templates/readme-cpp/README.md +8 -2
  103. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +3 -2
  104. package/package.json +3 -3
  105. package/cli/storybook-dist/assets/preview-CUgthyJT.js +0 -3
  106. package/cli/storybook-dist/assets/preview-zuAvMksL.css +0 -1
  107. package/cli/storybook-dist/assets/react-resizable-panels-BaelaOGy.css +0 -1
  108. /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
 
package/cli/README.md CHANGED
@@ -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
 
@@ -10,14 +10,15 @@ 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).
13
+ # Rainbow Robotics C++ SDK for find_package(rb_sdk). Pin the runtime package too: apt would
14
+ # otherwise pick the newest rainbow-rb-sdk, which -dev (= same version) rejects.
14
15
  # Override with MUSCAT_PK_RB_SDK_VERSION=<ver> (+ MUSCAT_PK_REBUILD_IMAGE=1).
15
- ARG RB_SDK_VERSION=0.1.7
16
+ ARG RB_SDK_VERSION=0.10.2
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 && apt-get install -y --no-install-recommends "rainbow-rb-sdk=${RB_SDK_VERSION}" "rainbow-rb-sdk-dev=${RB_SDK_VERSION}" \
21
22
  && rm -rf /var/lib/apt/lists/*
22
23
 
23
24
  # MediaMTX — same version/ports as the Muscat runtime (rby2 scripts/backend/runtime/mediamtx.yml),
@@ -11,12 +11,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
11
11
 
12
12
  # Rainbow Robotics C++ SDK for find_package(rb_sdk).
13
13
  # Override with MUSCAT_PK_RB_SDK_VERSION=<ver> (+ MUSCAT_PK_REBUILD_IMAGE=1).
14
- ARG RB_SDK_VERSION=0.1.7
14
+ ARG RB_SDK_VERSION=0.10.2
15
15
  RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
16
16
  && install -d /etc/apt/keyrings \
17
17
  && curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key -o /etc/apt/keyrings/rb-sdk.asc \
18
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}" \
19
+ && apt-get update && apt-get install -y --no-install-recommends "rainbow-rb-sdk=${RB_SDK_VERSION}" "rainbow-rb-sdk-dev=${RB_SDK_VERSION}" \
20
20
  && rm -rf /var/lib/apt/lists/*
21
21
 
22
22
  # MediaMTX — same version/ports as the Muscat runtime (rby2 scripts/backend/runtime/mediamtx.yml),
@@ -43,7 +43,7 @@ function assertDockerReady() {
43
43
  }
44
44
 
45
45
  // Is a host TCP port already bound? (Catches another bridge or a stray process.)
46
- function isPortInUse(port) {
46
+ export function isPortInUse(port) {
47
47
  return new Promise((res) => {
48
48
  const srv = createServer();
49
49
  srv.once("error", (err) => res(err.code === "EADDRINUSE"));
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
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
7
7
  import { runDevUi } from "./devUi.mjs";
8
8
  import { runDevBack, stopDevBackContainers } from "./devBack.mjs";
9
9
  import { localMuscatContainer, startBridge, stopBridge, BRIDGE_CONTAINER, BRIDGE_PORT } from "./bridge.mjs";
10
+ import { ensureLocalMuscat } from "./muscatLocal.mjs";
10
11
  import { assertManifestPorts, assertManifestVariables } from "./manifest.mjs";
11
12
  import { formatPortRanges } from "./ports.mjs";
12
13
  import {
@@ -123,6 +124,11 @@ export async function runDev(args = []) {
123
124
  if (robotToken) process.env.MUSCAT_ROBOT_TOKEN = robotToken;
124
125
 
125
126
  const remoteRobot = isRemoteRobot(robotIp);
127
+ // No robot: a local Muscat (common) owns :10000, so the bridge below is skipped and
128
+ // dev:back joins the Muscat netns instead — same as against a `muscat-dev` container.
129
+ const localMuscat = remoteRobot
130
+ ? null
131
+ : await ensureLocalMuscat().catch((e) => { log.warn(`local Muscat: ${e.message}`); return null; });
126
132
  const hostAgent = await maybeStartHostAgent(root, { robotIp, robotToken });
127
133
  const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT || "";
128
134
  // No proxy (Linux, disabled, ...): fall back to the container reverse-dialing
@@ -130,7 +136,8 @@ export async function runDev(args = []) {
130
136
  if (!proxyEndpoint) await ensureRemoteZenohEnv(robotIp, { log });
131
137
 
132
138
  log.step("starting Socket.IO bridge (Docker) + dev:ui + dev:back");
133
- const bridgeStarted = await startBridge({
139
+ // A local Muscat already serves :10000 (gateway) and MediaMTX — no bridge needed.
140
+ const bridgeStarted = localMuscat ? false : await startBridge({
134
141
  zenohPort: remoteRobot && !proxyEndpoint && !usesHostNetwork()
135
142
  ? Number(process.env.MUSCAT_ZENOH_PORT)
136
143
  : null,
@@ -138,7 +145,7 @@ export async function runDev(args = []) {
138
145
  pluginPorts: usesHostNetwork() ? [] : portRanges,
139
146
  });
140
147
 
141
- const muscat = bridgeStarted ? null : localMuscatContainer();
148
+ const muscat = bridgeStarted ? null : (localMuscat ?? localMuscatContainer());
142
149
  if (muscat) {
143
150
  log.step(`local Muscat container '${muscat}' owns :${BRIDGE_PORT} — backend joins its network (gateway, MediaMTX)`);
144
151
  process.env.MUSCAT_PK_MUSCAT_CONTAINER = muscat;
@@ -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 "./bridge.mjs";
16
17
  import { PLUGIN_ID_RE, assertManifestPorts, assertManifestVariables, manifestRuntime, readManifest, rosNamespace, validateManifest } from "./manifest.mjs";
17
18
  import {
18
19
  formatPortRanges,
@@ -56,9 +57,18 @@ export function stopDevBackContainers() {
56
57
  // Where the backend's 127.0.0.1 points. A local Muscat container (muscat-dev): join its
57
58
  // netns so the gateway and MediaMTX resolve like on the robot. Under `dev`: the bridge's
58
59
  // netns. Standalone dev:back: its own 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
73
  const bridge = process.env.MUSCAT_PK_BRIDGE_CONTAINER;
64
74
  if (bridge) return { run: ["--network", `container:${bridge}`], env: [], shared: true };
@@ -656,6 +666,15 @@ export async function runDevBack(args = []) {
656
666
  // Byte-proxy mode (a host-side host_agent set MUSCAT_PROXY_ENDPOINT) makes
657
667
  // the container speak the proxy protocol instead, so it needs no reverse-dial.
658
668
  if (!process.env.MUSCAT_PROXY_ENDPOINT) await ensureRemoteZenohEnv(robotIp, { log });
669
+ // Standalone against a local Muscat (muscat-dev, …): join its netns like `dev` does,
670
+ // otherwise zenoh reaches nothing and `execute` never arrives.
671
+ if (!process.env.MUSCAT_PK_MUSCAT_CONTAINER && !isRemoteRobot(robotIp)) {
672
+ const muscat = localMuscatContainer();
673
+ if (muscat) {
674
+ log.step(`local Muscat container '${muscat}' found — backend joins its network`);
675
+ process.env.MUSCAT_PK_MUSCAT_CONTAINER = muscat;
676
+ }
677
+ }
659
678
  const type = manifestRuntime(root);
660
679
  if (type === "ros2") return await runRos2(root, portRanges);
661
680
  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
 
@@ -0,0 +1,160 @@
1
+ // Local Muscat back end for `dev` / `dev:ui` without a robot.
2
+ // preview-host hits http://localhost:3000/common/* (baked into the nexus bundle), so a
3
+ // Muscat on :3000 makes program preview work with no front-end change. We reuse any
4
+ // Muscat already publishing :10000 (e.g. a `muscat` / `muscat-dev` container) and only
5
+ // otherwise start our own: the S3 runtime image with just the `common` service.
6
+ import { spawnSync } from "node:child_process";
7
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { resolve } from "node:path";
10
+ import { isPortInUse, localMuscatContainer } from "./bridge.mjs";
11
+ import { mediaPortFlags, MEDIA_PORTS } from "./ports.mjs";
12
+ import { canSkipImageBuild, hasBin, log } from "./util.mjs";
13
+
14
+ export const MUSCAT_CONTAINER = "muscat-pk-muscat";
15
+ const IMAGE = "muscat-pk-runtime";
16
+ const S3 = process.env.MUSCAT_PK_MUSCAT_S3 ?? "https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat/main";
17
+ const CACHE = resolve(homedir(), ".muscat-pk", "muscat");
18
+ const API_PORT = 3000;
19
+ const GATEWAY_PORT = 10000;
20
+ // The bin runs inside Docker, so follow the Docker engine's arch, not this Node's
21
+ // (Rosetta/x64 Node on an Apple Silicon host would otherwise pick amd64).
22
+ const dockerArch = () => {
23
+ const r = spawnSync("docker", ["version", "--format", "{{.Server.Arch}}"], { encoding: "utf8" });
24
+ const a = (r.stdout || "").trim();
25
+ return a === "arm64" || a === "amd64" ? a : process.arch === "arm64" ? "arm64" : "amd64";
26
+ };
27
+
28
+ async function fetchBuffer(url) {
29
+ const res = await fetch(url);
30
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
31
+ return Buffer.from(await res.arrayBuffer());
32
+ }
33
+
34
+ // macOS/Windows `tar` reads zip; GNU tar does not, Linux has `unzip`.
35
+ function extractZip(zip, dir, members = []) {
36
+ mkdirSync(dir, { recursive: true });
37
+ const tries = [
38
+ ["unzip", ["-o", "-q", zip, ...members, "-d", dir]],
39
+ ["tar", ["-xf", zip, "-C", dir, ...members]],
40
+ ];
41
+ for (const [bin, args] of tries) {
42
+ if (hasBin(bin) && spawnSync(bin, args, { stdio: "ignore" }).status === 0) return;
43
+ }
44
+ throw new Error(`could not extract ${zip} (needs unzip or tar)`);
45
+ }
46
+
47
+ async function resolveVersions() {
48
+ const file = resolve(CACHE, "version.json");
49
+ try {
50
+ const data = await fetchBuffer(`${S3}/version.json`);
51
+ mkdirSync(CACHE, { recursive: true });
52
+ writeFileSync(file, data);
53
+ return JSON.parse(data.toString());
54
+ } catch (e) {
55
+ if (!existsSync(file)) throw e;
56
+ log.warn(`version.json unreachable (${e.message}) — using cached Muscat version`);
57
+ return JSON.parse(readFileSync(file, "utf8"));
58
+ }
59
+ }
60
+
61
+ async function ensureRuntimeImage(total) {
62
+ const image = `${IMAGE}:${total}`;
63
+ if (canSkipImageBuild(image)) return image;
64
+ const dir = resolve(CACHE, "runtime", total);
65
+ if (!existsSync(resolve(dir, "Dockerfile.runtime"))) {
66
+ log.step(`downloading Muscat runtime scripts (muscat.zip ${total})…`);
67
+ const zip = resolve(CACHE, "muscat.zip");
68
+ writeFileSync(zip, await fetchBuffer(`${S3}/muscat.zip`));
69
+ extractZip(zip, dir);
70
+ rmSync(zip, { force: true });
71
+ }
72
+ log.step(`docker build -t ${image} (first run downloads MongoDB/InfluxDB/ROS base — slow, then cached)…`);
73
+ const r = spawnSync(
74
+ "docker",
75
+ ["build", "-t", image, "-f", resolve(dir, "Dockerfile.runtime"), "--label", "com.rainbow-robotics.muscat-pk=true", dir],
76
+ { stdio: "inherit" },
77
+ );
78
+ if (r.status !== 0) throw new Error("docker build failed (Muscat runtime image).");
79
+ return image;
80
+ }
81
+
82
+ async function ensureCommonBinary(version) {
83
+ const ARCH = dockerArch();
84
+ const dir = resolve(CACHE, "services", "common");
85
+ const stamp = resolve(dir, ".version");
86
+ if (existsSync(stamp) && readFileSync(stamp, "utf8").trim() === version && existsSync(resolve(dir, `common.${ARCH}.bin`))) return dir;
87
+ log.step(`downloading Muscat common service ${version} (${ARCH})…`);
88
+ const zip = resolve(CACHE, `common-${version}.zip`);
89
+ writeFileSync(zip, await fetchBuffer(`${S3}/common/${version}.zip`));
90
+ extractZip(zip, dir, ["config.env", `common.${ARCH}.bin`]);
91
+ rmSync(zip, { force: true });
92
+ chmodSync(resolve(dir, `common.${ARCH}.bin`), 0o755);
93
+ writeFileSync(stamp, `${version}\n`);
94
+ return dir;
95
+ }
96
+
97
+ async function waitForApi(timeoutMs = 180_000) {
98
+ const start = Date.now();
99
+ for (;;) {
100
+ try {
101
+ const res = await fetch(`http://127.0.0.1:${API_PORT}/common/docs`);
102
+ if (res.ok) return;
103
+ } catch { /* not listening yet */ }
104
+ if (Date.now() - start > timeoutMs) throw new Error(`Muscat did not answer on :${API_PORT} in time — see \`docker logs ${MUSCAT_CONTAINER}\``);
105
+ await new Promise((r) => setTimeout(r, 1000));
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Make sure a Muscat answers on localhost:3000 / :10000. Returns the container name,
111
+ * or null when one can't be started (no Docker, foreign process on the ports).
112
+ * The container is left running across runs — stop it with `docker rm -f muscat-pk-muscat`.
113
+ */
114
+ export async function ensureLocalMuscat() {
115
+ if (process.env.MUSCAT_PK_NO_LOCAL_MUSCAT === "1") return null;
116
+ const existing = hasBin("docker") ? localMuscatContainer() : null;
117
+ if (existing) {
118
+ log.step(`local Muscat '${existing}' already on :${GATEWAY_PORT} — reusing it`);
119
+ return existing;
120
+ }
121
+ if (!hasBin("docker")) {
122
+ log.warn("Docker not found — no local Muscat; program preview will have no back end.");
123
+ return null;
124
+ }
125
+ for (const port of [API_PORT, GATEWAY_PORT]) {
126
+ if (await isPortInUse(port)) {
127
+ log.warn(`port ${port} is in use by something that is not a Muscat container — not starting local Muscat.`);
128
+ return null;
129
+ }
130
+ }
131
+
132
+ const versions = await resolveVersions();
133
+ const total = versions.total;
134
+ const commonVersion = versions.services?.common?.version;
135
+ if (!total || !commonVersion) throw new Error("version.json has no total / services.common.version");
136
+
137
+ const image = await ensureRuntimeImage(total);
138
+ const commonDir = await ensureCommonBinary(commonVersion);
139
+
140
+ let media = true;
141
+ for (const port of MEDIA_PORTS) if (await isPortInUse(port)) media = false;
142
+
143
+ spawnSync("docker", ["rm", "-f", MUSCAT_CONTAINER], { stdio: "ignore" });
144
+ log.step(`starting local Muscat (common ${commonVersion}) → http://localhost:${API_PORT}/common/docs`);
145
+ const r = spawnSync("docker", [
146
+ "run", "-d", "--name", MUSCAT_CONTAINER, "--label", "com.rainbow-robotics.muscat-pk=true",
147
+ "-e", "RB_ENABLED_SERVICES=common",
148
+ "-e", "RB_RUNTIME_ENABLE_ZENOH_BRIDGE=0",
149
+ "-v", `${commonDir}:/app/services/common`,
150
+ "-v", `${resolve(CACHE, "data")}:/app/data`,
151
+ "-p", `127.0.0.1:${API_PORT}:${API_PORT}`,
152
+ "-p", `127.0.0.1:${GATEWAY_PORT}:${GATEWAY_PORT}`,
153
+ ...(media ? mediaPortFlags() : []),
154
+ image,
155
+ ], { stdio: ["ignore", "ignore", "inherit"] });
156
+ if (r.status !== 0) throw new Error("failed to start the local Muscat container.");
157
+ await waitForApi();
158
+ log.step(`local Muscat ready — keeps running between runs; stop with: docker rm -f ${MUSCAT_CONTAINER}`);
159
+ return MUSCAT_CONTAINER;
160
+ }
package/cli/src/new.mjs CHANGED
@@ -70,6 +70,20 @@ function printNewSuccess({ targetName, type, pm }) {
70
70
  log.info("");
71
71
  }
72
72
 
73
+ // C++ backends: the editor needs the SDK headers, which only exist in the dev image. Building the image
74
+ // now (dev/build reuse it) lets VS Code resolve rb_sdk right away. Best-effort, like the .venv.
75
+ export async function prepareCppIde(root, choice) {
76
+ const later = "they come with the first `muscat-pk dev` / `build`";
77
+ if (!hasBin("docker")) return log.warn(`Docker not found — SDK headers for the editor: ${later}.`);
78
+ try {
79
+ log.step("preparing the C++ dev image + SDK headers for your editor (first run is slow)…");
80
+ const { ensureImage } = await import(choice.type === "cpp" ? "./cpp.mjs" : "./ros2.mjs");
81
+ await ensureImage(undefined, { root });
82
+ } catch (err) {
83
+ log.warn(`Could not prepare SDK headers for the editor (${err.message}) — ${later}.`);
84
+ }
85
+ }
86
+
73
87
  // Build the manifest runtime object for a given type choice + plugin slug.
74
88
  export function runtimeForChoice(choice, slug) {
75
89
  switch (choice.type) {
@@ -285,6 +299,7 @@ export async function runNew(args) {
285
299
  log.warn("Could not create .venv (no local Python/uv?) — skipping, Docker dev still works.");
286
300
  }
287
301
  }
302
+ if (runtimeChoice.type === "cpp" || runtimeChoice.rosClient === "rclcpp") await prepareCppIde(target, runtimeChoice);
288
303
  }
289
304
 
290
305
  // git init (best-effort). Always runs, even nested inside another repo —
package/cli/src/ros2.mjs CHANGED
@@ -3,7 +3,8 @@
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { existsSync, readdirSync } from "node:fs";
5
5
  import { dirname, resolve } 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 ROS2_DEV_IMAGE = "muscat-pk-ros2-dev";
9
10
 
@@ -29,9 +30,16 @@ export const ARCH_DOCKER_PLATFORM = {
29
30
  };
30
31
 
31
32
  /** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
32
- export async function ensureImage(image = ROS2_DEV_IMAGE, { platform = null } = {}) {
33
+ export async function ensureImage(image = ROS2_DEV_IMAGE, { platform = null, root = pluginRootOrNull() } = {}) {
34
+ await buildImage(image, platform, root);
35
+ // Dev images feed the editor's SDK headers: the native one (dev) or the host-arch one (build).
36
+ const hostArch = process.arch === "arm64" ? "arm64" : "amd64";
37
+ if (root && (image === ROS2_DEV_IMAGE || image === `${ROS2_DEV_IMAGE}-${hostArch}`)) syncCppIde(root, image);
38
+ }
39
+
40
+ async function buildImage(image, platform, root) {
33
41
  const dockerfile = await resolveDockerfile("Dockerfile.dev");
34
- const sdkVer = process.env.MUSCAT_PK_RB_SDK_VERSION;
42
+ const sdkVer = cppSdkVersion(root);
35
43
  const key = imageKey(dockerfile, sdkVer ?? "");
36
44
  if (canSkipImageBuild(image, key)) {
37
45
  log.step(`${image} already built — skipping docker build`);
package/cli/src/sdk.mjs CHANGED
@@ -1,12 +1,17 @@
1
- // muscat-pk sdk <list|update [version]> — manage the python rainbow-rb-sdk.
1
+ // muscat-pk sdk <list|update [version]> — manage the rainbow-rb-sdk version.
2
2
  //
3
+ // C++ backends (cpp, ros2 rclcpp) pin it in CMakeLists.txt (find_package(rb_sdk <ver> EXACT)); the
4
+ // versions come from the rb-sdk apt repository and the dev image installs the pinned one.
5
+ //
6
+ // Python backends:
3
7
  // Pins requirements.txt — the real install happens in the `dev`/`dev:back`
4
8
  // Docker container. If a local .venv exists (see devBack.mjs's syncIdeVenv,
5
9
  // for editor/linter support), also update it in place so pylint/pyright see
6
10
  // the new version immediately; this is best-effort only.
7
11
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
8
12
  import { resolve } from "node:path";
9
- import { ensureIdeVenv, findPluginRoot, log, venvPython } from "./util.mjs";
13
+ import { cppSdkFiles, cppSdkPin, ensureIdeVenv, findPluginRoot, log, setCppSdkPin, venvPython } from "./util.mjs";
14
+ import { readManifest } from "./manifest.mjs";
10
15
 
11
16
  const PKG = "rainbow-rb-sdk";
12
17
 
@@ -24,9 +29,50 @@ async function fetchVersions() {
24
29
  return Object.keys(data?.releases || {}).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
25
30
  }
26
31
 
32
+ // Stable releases of rainbow-rb-sdk-dev, newest first. Pre-releases (0.1.4~dev1) are skipped: CMake
33
+ // versions are numeric only.
34
+ const APT_PACKAGES = "https://rainbowrobotics.github.io/rb-sdk/dists/stable/main/binary-amd64/Packages";
35
+
36
+ export async function fetchAptVersions() {
37
+ try {
38
+ const res = await fetch(APT_PACKAGES);
39
+ if (!res.ok) return [];
40
+ const text = await res.text();
41
+ const versions = [...text.matchAll(/^Package: rainbow-rb-sdk-dev\n(?:.*\n)*?Version: (\S+)/gm)].map((m) => m[1]);
42
+ return [...new Set(versions)].filter((v) => /^[0-9]+(\.[0-9]+)*$/.test(v)).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
43
+ } catch {
44
+ return [];
45
+ }
46
+ }
47
+
48
+ async function runCppSdk(sub, version, root) {
49
+ if (sub === "list") {
50
+ const versions = await fetchAptVersions();
51
+ if (!versions.length) return log.warn("could not fetch versions (offline?).");
52
+ log.info(`rainbow-rb-sdk-dev versions:\n ${versions.slice(0, 12).join("\n ")}`);
53
+ return;
54
+ }
55
+ if (sub === "update") {
56
+ if (!cppSdkFiles(root).length) throw new Error("no find_package(rb_sdk ...) in cpp/CMakeLists.txt or ros2_ws/src/*/CMakeLists.txt");
57
+ if (!version) {
58
+ version = (await fetchAptVersions())[0];
59
+ if (!version) throw new Error("could not fetch the latest version (offline?). Pass one: muscat-pk sdk update <version>");
60
+ }
61
+ if (!/^[0-9]+(\.[0-9]+)*$/.test(version)) throw new Error(`"${version}" is not a CMake version (digits and dots only)`);
62
+ const before = cppSdkPin(root);
63
+ const files = setCppSdkPin(root, version);
64
+ log.ok(`rb_sdk ${before ?? "(unpinned)"} → ${version} in ${files.map((f) => f.slice(root.length + 1)).join(", ")}`);
65
+ log.info("The dev image is rebuilt with this SDK on the next `muscat-pk build` / `dev:back`.");
66
+ return;
67
+ }
68
+ log.info("Usage: muscat-pk sdk <list|update [version]>");
69
+ }
70
+
27
71
  export async function runSdk(args = []) {
28
72
  const sub = args[0];
29
73
  const root = findPluginRoot();
74
+ const runtime = readManifest(root)?.runtime ?? {};
75
+ if (runtime.type === "cpp" || (runtime.type === "ros2" && runtime.rosClient === "rclcpp")) return runCppSdk(sub, args[1], root);
30
76
  const reqs = resolve(root, "requirements.txt");
31
77
 
32
78
  if (sub === "list") {
package/cli/src/type.mjs CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  deriveNames,
14
14
  overlayCpp,
15
15
  overlayRos2,
16
+ prepareCppIde,
16
17
  runtimeForChoice,
17
18
  } from "./new.mjs";
18
19
 
@@ -126,6 +127,7 @@ export async function runType(args) {
126
127
  json.runtime = runtimeForChoice(runtimeChoice, slug);
127
128
  writeFileSync(path, JSON.stringify(json, null, 2) + "\n");
128
129
  log.ok(`${basename(root)} type: ${current} → ${to}`);
130
+ if (isCpp(to)) await prepareCppIde(root, runtimeChoice);
129
131
 
130
132
  if (isCpp(current) !== isCpp(to)) {
131
133
  await offerReadme(root, isCpp(to), args.includes("--readme"), {