@rainbow-robotics/muscat-plugin-kit 1.3.7 → 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.
- package/README.md +1 -1
- package/cli/README.md +7 -1
- package/cli/docker/Dockerfile.cpp +4 -3
- package/cli/docker/Dockerfile.dev +2 -2
- package/cli/src/bridge.mjs +1 -1
- package/cli/src/build.mjs +5 -5
- package/cli/src/completion.mjs +1 -1
- package/cli/src/cpp.mjs +11 -3
- package/cli/src/cppIde.mjs +75 -0
- package/cli/src/dev.mjs +9 -2
- package/cli/src/devBack.mjs +21 -2
- package/cli/src/devUi.mjs +10 -1
- package/cli/src/muscatLocal.mjs +160 -0
- package/cli/src/new.mjs +15 -0
- package/cli/src/ros2.mjs +11 -3
- package/cli/src/sdk.mjs +48 -2
- package/cli/src/type.mjs +2 -0
- package/cli/src/util.mjs +44 -1
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-CdTgUqrV.js → Color-YHDXOIA2-DeirXVfl.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-CbRKwHpQ.js → Docs-BLxyi8cP.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-CsxMprX5.js → Docs-Buyf7Djy.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-FyPFrbUS.js → Docs-BxmwdaYa.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-C8wx5zsC.js → Docs-CnfIOwf8.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-Cf0s04QW.js → Docs-NrBqEYvM.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-YtqU-aVu.js → Docs-wzFMK-XA.js} +1 -1
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-VSENNRoY.js → DocsRenderer-CFRXHY34-CWc43rTP.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-oCcYtLwq.js → NxAvatar.stories-DfL0kqmN.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-C5jUScIj.js → NxButton.stories-WxSc_gsR.js} +1 -1
- package/cli/storybook-dist/assets/{NxNumberInput.stories-gjsEulIN.js → NxNumberInput.stories-C1D3oUiY.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-BK6uSWIa.js → NxPagination.stories-C1Qa88rn.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BDD5CvSZ.js → NxPointerHoldButton.stories-CmteJX3t.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-C_nW26_x.js → NxRangeInput.stories-B7E-mhyI.js} +1 -1
- package/cli/storybook-dist/assets/{NxSelectBox.stories-DqLfKU9H.js → NxSelectBox.stories-De67vMdU.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-CQjHA8yv.js → NxSlideRange.stories-DvCV1MGk.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-BMHCiPoL.js → NxSwitch.stories-DuHDZ9-v.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-k7r1WVQh.js → NxTextArea.stories-BT1o0ngg.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-Cqfe1I32.js → NxTextInput.stories-CSLBT83Q.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories-CJ3Ttuja.js → RBAccordion.stories-QavkYgV0.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-BThv7tM0.js → RBAccordionGroup.stories-C0RPGnO7.js} +1 -1
- package/cli/storybook-dist/assets/{RBAlert.stories-CSMP8Vun.js → RBAlert.stories-BUCAT0Sd.js} +1 -1
- package/cli/storybook-dist/assets/{RBBadge.stories-cBMA2R6F.js → RBBadge.stories-CnBXTQ4J.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-BKVOskqs.js → RBBattery.stories-5FbsZcdy.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-BTOfK0HC.js → RBCard.stories-FwTKjtCi.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckBox.stories-DyE912wG.js → RBCheckBox.stories-C0y5psMI.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-0sLFU0kM.js → RBCheckIconButton.stories-sJjAZBVh.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-c8VecClE.js → RBCheckLabelButton.stories-COJqEkDr.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipTab.stories-CQ9Uav5_.js → RBChipTab.stories-BnXj-Woa.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-Ap4NlhAZ.js → RBChipsBoxTab.stories-DE1yl3rz.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-BPPXjif2.js → RBColorIcons.stories-CYASERLQ.js} +1 -1
- package/cli/storybook-dist/assets/{RBCommand.stories-PDpYOKlm.js → RBCommand.stories-BYkWjdMG.js} +1 -1
- package/cli/storybook-dist/assets/{RBConfirm.stories-BEWvMAYP.js → RBConfirm.stories-XLLTJZRf.js} +1 -1
- package/cli/storybook-dist/assets/{RBDateInput.stories-DLdyNnVF.js → RBDateInput.stories-DpVylrQl.js} +1 -1
- package/cli/storybook-dist/assets/{RBDialog.stories-HsawMNOb.js → RBDialog.stories-DcPRAan8.js} +1 -1
- package/cli/storybook-dist/assets/{RBDivider.stories-dlWwOxik.js → RBDivider.stories-NOTo-jJG.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-C87iqGk0.js → RBDrawer.stories-CUZaQyw8.js} +1 -1
- package/cli/storybook-dist/assets/{RBDropdownMenu.stories-B8EQhA0z.js → RBDropdownMenu.stories-5UC0ZA3H.js} +1 -1
- package/cli/storybook-dist/assets/{RBEmptyView.stories-SzpIhNDw.js → RBEmptyView.stories-BIRNPjwt.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-BQ0nM1gb.js → RBIconButton.stories-CJWeIQTt.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-C19g5M5z.js → RBIcons.stories-Cu0txCUp.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-DbgI4-1P.js → RBInlineLabelButton.stories-DnGhYrxa.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-smF-vpGL.js → RBJoyStick.stories-BlfvKcGb.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-s53klwDv.js → RBLabelButton.stories-CFwmlhAE.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-BnzkoR51.js → RBLight.stories-wMrFLJ7u.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-CW014klu.js → RBMenubar.stories-Bu5wtV-T.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-qa4tH_4Y.js → RBMultiRangeInput.stories-Db1d940z.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-YoQNUpQH.js → RBPagination.stories-NCpjg-2W.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-Bhpi0OaH.js → RBPopover.stories-CqsM8byU.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-iQM5iAka.js → RBPrompt.stories-TMQEaHk-.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-BGyq4rzx.js → RBRadioBox.stories-pnsBvtzU.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-DF6BcUcJ.js → RBRadioButton.stories-BMDBfq82.js} +1 -1
- package/cli/storybook-dist/assets/{RBRangeDateInput.stories-COOQ_HNF.js → RBRangeDateInput.stories-ljAsdz0Q.js} +1 -1
- package/cli/storybook-dist/assets/{RBResizable.stories-DSVHvwjp.js → RBResizable.stories-B5YDW687.js} +1 -1
- package/cli/storybook-dist/assets/{RBSelectBox.stories-BookV8Sm.js → RBSelectBox.stories-DPtyIwEr.js} +1 -1
- package/cli/storybook-dist/assets/{RBSheet.stories-CAK9hb-p.js → RBSheet.stories-DMTeF_hx.js} +1 -1
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-DUymnH-9.js → RBSideNumberInput.stories-DLsS3mzL.js} +1 -1
- package/cli/storybook-dist/assets/{RBSlider.stories-BwlDh9--.js → RBSlider.stories-BjTYZ3ri.js} +1 -1
- package/cli/storybook-dist/assets/{RBSpinner.stories-VOlkuP6t.js → RBSpinner.stories-BWBvOGUY.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-BiW2dQnT.js → RBSwitch.stories-tpthe98k.js} +1 -1
- package/cli/storybook-dist/assets/{RBTab.stories-DFKVsy0C.js → RBTab.stories-BijmaLIo.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-BWG0ZSYI.js → RBTable.stories-Dojsz28q.js} +1 -1
- package/cli/storybook-dist/assets/{RBTableRow.stories-JVTD83Mh.js → RBTableRow.stories-CQajMiEa.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-D-aN385u.js → RBTabs.stories-BslRXoCw.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextArea.stories-Dx5xkmZS.js → RBTextArea.stories-N44Q45Xp.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextField.stories-DmVciExd.js → RBTextField.stories-scpiy0Kx.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextInput.stories-CCf0VGEv.js → RBTextInput.stories-D_suHJaA.js} +1 -1
- package/cli/storybook-dist/assets/{RBToast.stories-DT0DGn05.js → RBToast.stories-B73JQcKo.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-Ce_q3EtP.js → RBToggle.stories-CmujM833.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-CN40YHsR.js → RBToggleGroup.stories-DhCAfiKu.js} +1 -1
- package/cli/storybook-dist/assets/{RBTooltip.stories-L7btDEpA.js → RBTooltip.stories-BixEjKZy.js} +1 -1
- package/cli/storybook-dist/assets/{SideNumberInput-B5_xLK8z.js → SideNumberInput-B7fbx2jk.js} +83 -83
- package/cli/storybook-dist/assets/{entry-preview-raBQdKjS.js → entry-preview-BeR5Td7C.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-BmIue__l.js → iframe-D3pKf_6w.js} +2 -2
- package/cli/storybook-dist/assets/{index-CSIe-YDr.js → index-gXlU0WyC.js} +3 -3
- package/cli/storybook-dist/assets/preview-BYhMD55w.css +1 -0
- package/cli/storybook-dist/assets/preview-D-VpQB0_.js +3 -0
- package/cli/storybook-dist/assets/{preview-DD51RTKv.js → preview-GwaHbm9r.js} +2 -2
- package/cli/storybook-dist/assets/react-resizable-panels-DJMuB2U0.css +1 -0
- package/cli/storybook-dist/iframe.html +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/requirements.txt +3 -3
- package/cli/templates/cpp/cpp/CMakeLists.txt +3 -2
- package/cli/templates/readme-cpp/README.md +8 -2
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +3 -2
- package/package.json +3 -3
- package/cli/storybook-dist/assets/preview-CUgthyJT.js +0 -3
- package/cli/storybook-dist/assets/preview-zuAvMksL.css +0 -1
- package/cli/storybook-dist/assets/react-resizable-panels-BaelaOGy.css +0 -1
- /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
|
|
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
|
|
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.
|
|
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.
|
|
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),
|
package/cli/src/bridge.mjs
CHANGED
|
@@ -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):
|
|
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
|
-
|
|
369
|
-
|
|
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(
|
package/cli/src/completion.mjs
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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;
|
package/cli/src/devBack.mjs
CHANGED
|
@@ -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:
|
|
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 =
|
|
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
|
|
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"), {
|