@rainbow-robotics/muscat-plugin-kit 1.10.3-dev.2 → 1.10.3-dev.4

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 (103) hide show
  1. package/README.ko.md +1185 -0
  2. package/README.md +1051 -148
  3. package/cli/README.md +4 -2
  4. package/cli/bin/muscat-pk.mjs +6 -2
  5. package/cli/src/build.mjs +9 -13
  6. package/cli/src/completion.mjs +2 -0
  7. package/cli/src/dev.mjs +22 -3
  8. package/cli/src/devBack.mjs +1 -1
  9. package/cli/src/devUi.mjs +2 -1
  10. package/cli/src/mcp.mjs +17 -3
  11. package/cli/src/muscatLocal.mjs +57 -21
  12. package/cli/src/pluginDevServer.mjs +1 -1
  13. package/cli/src/sim.mjs +163 -0
  14. package/cli/src/util.mjs +2 -0
  15. package/cli/storybook-dist/assets/{Color-YHDXOIA2-JZmvrBu2.js → Color-YHDXOIA2-DSB_yvex.js} +1 -1
  16. package/cli/storybook-dist/assets/{Docs-DjjxchlZ.js → Docs-C_90bDRy.js} +1 -1
  17. package/cli/storybook-dist/assets/{Docs-DrMHaIyt.js → Docs-CbW9HZd4.js} +1 -1
  18. package/cli/storybook-dist/assets/{Docs-GWKBJz_w.js → Docs-CoskoRMT.js} +1 -1
  19. package/cli/storybook-dist/assets/{Docs-CHh5amiz.js → Docs-Cy9XFeLJ.js} +1 -1
  20. package/cli/storybook-dist/assets/{Docs-DdU-wsGN.js → Docs-De1oc64k.js} +1 -1
  21. package/cli/storybook-dist/assets/{Docs-DXJ25Kqq.js → Docs-klD3F_Ai.js} +1 -1
  22. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-DNcXzd9o.js → DocsRenderer-CFRXHY34-Bkm4uV_D.js} +1 -1
  23. package/cli/storybook-dist/assets/{NxAvatar.stories-CwnK8AUF.js → NxAvatar.stories-Ctf5G2FQ.js} +1 -1
  24. package/cli/storybook-dist/assets/{NxButton.stories-ueYdosfQ.js → NxButton.stories-DeLObVSj.js} +1 -1
  25. package/cli/storybook-dist/assets/{NxNumberInput.stories-M5ZCLbrK.js → NxNumberInput.stories-LqoY0ed1.js} +1 -1
  26. package/cli/storybook-dist/assets/{NxPagination.stories-bxxUnHEL.js → NxPagination.stories-DI6bXuJM.js} +1 -1
  27. package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BHxB2kr5.js → NxPointerHoldButton.stories-3HSV1XmN.js} +1 -1
  28. package/cli/storybook-dist/assets/{NxRangeInput.stories-BKfr-fp4.js → NxRangeInput.stories-1pjgQVaG.js} +1 -1
  29. package/cli/storybook-dist/assets/{NxSelectBox.stories-CG8FuTBq.js → NxSelectBox.stories-bcQPZEuF.js} +1 -1
  30. package/cli/storybook-dist/assets/{NxSlideRange.stories-BRTFtMTL.js → NxSlideRange.stories-BosX4axk.js} +1 -1
  31. package/cli/storybook-dist/assets/{NxSwitch.stories-Bwuyzn-R.js → NxSwitch.stories-DxV6dS33.js} +1 -1
  32. package/cli/storybook-dist/assets/{NxTextArea.stories-COq2sGqu.js → NxTextArea.stories-CO545-LP.js} +1 -1
  33. package/cli/storybook-dist/assets/{NxTextInput.stories-CjsBLz05.js → NxTextInput.stories-BP0npai1.js} +1 -1
  34. package/cli/storybook-dist/assets/{RBAccordion.stories-BARwuZ0Q.js → RBAccordion.stories-fVR4Va2a.js} +1 -1
  35. package/cli/storybook-dist/assets/{RBAccordionGroup.stories-COrPxETW.js → RBAccordionGroup.stories-Du5bsckD.js} +1 -1
  36. package/cli/storybook-dist/assets/{RBAlert.stories-BQrkjZPt.js → RBAlert.stories-DNkMXUu6.js} +1 -1
  37. package/cli/storybook-dist/assets/{RBBadge.stories-BTsLL-lK.js → RBBadge.stories-C44pXt8D.js} +1 -1
  38. package/cli/storybook-dist/assets/{RBBattery.stories-DAeQdJbc.js → RBBattery.stories-DE2JHvfL.js} +1 -1
  39. package/cli/storybook-dist/assets/{RBCard.stories-HyhOE80R.js → RBCard.stories-CuIda7NI.js} +1 -1
  40. package/cli/storybook-dist/assets/{RBCheckBox.stories-KFk0WqX_.js → RBCheckBox.stories-D1qM1ayr.js} +1 -1
  41. package/cli/storybook-dist/assets/{RBCheckIconButton.stories-CXbbktF5.js → RBCheckIconButton.stories-e4gkliBO.js} +1 -1
  42. package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-DWfYz_9Z.js → RBCheckLabelButton.stories-CDzCtsMI.js} +1 -1
  43. package/cli/storybook-dist/assets/{RBChipTab.stories-JPEgVFAS.js → RBChipTab.stories-DBrc_3Mp.js} +1 -1
  44. package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-CTPuWXrQ.js → RBChipsBoxTab.stories-hlrsHjKY.js} +1 -1
  45. package/cli/storybook-dist/assets/{RBColorIcons.stories-DiFRZ5jk.js → RBColorIcons.stories-BHUvpkdu.js} +1 -1
  46. package/cli/storybook-dist/assets/{RBCommand.stories-Pf1HAVIR.js → RBCommand.stories-azAF41NP.js} +1 -1
  47. package/cli/storybook-dist/assets/{RBConfirm.stories-D1cEPUXk.js → RBConfirm.stories-BMNjwbmT.js} +1 -1
  48. package/cli/storybook-dist/assets/{RBDateInput.stories-RPYpCjig.js → RBDateInput.stories-D0VezDzv.js} +1 -1
  49. package/cli/storybook-dist/assets/{RBDialog.stories-DJH-0XSR.js → RBDialog.stories-AOJ975Fy.js} +1 -1
  50. package/cli/storybook-dist/assets/{RBDivider.stories-uEj1vOgK.js → RBDivider.stories-NSvnj43Z.js} +1 -1
  51. package/cli/storybook-dist/assets/{RBDrawer.stories-BiMzElb2.js → RBDrawer.stories-Du0oTvD1.js} +1 -1
  52. package/cli/storybook-dist/assets/{RBDropdownMenu.stories-Da_TBkla.js → RBDropdownMenu.stories-Hsxve4PW.js} +1 -1
  53. package/cli/storybook-dist/assets/{RBEmptyView.stories-BHeKGfPS.js → RBEmptyView.stories-BlwHx3zp.js} +1 -1
  54. package/cli/storybook-dist/assets/{RBIconButton.stories-NEsRFoYp.js → RBIconButton.stories-DAPDY63x.js} +1 -1
  55. package/cli/storybook-dist/assets/{RBIcons.stories-CcZ-TTLS.js → RBIcons.stories-W5ZUV2L1.js} +1 -1
  56. package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-B0wpdQyR.js → RBInlineLabelButton.stories-C99j0Y3g.js} +1 -1
  57. package/cli/storybook-dist/assets/{RBJoyStick.stories-DEgBou3K.js → RBJoyStick.stories-Du0H1iAS.js} +1 -1
  58. package/cli/storybook-dist/assets/{RBLabelButton.stories-DT8drIsF.js → RBLabelButton.stories-BlvHaU_Z.js} +1 -1
  59. package/cli/storybook-dist/assets/{RBLight.stories-Dh-aoeyG.js → RBLight.stories-BY6DKrre.js} +1 -1
  60. package/cli/storybook-dist/assets/{RBMenubar.stories-D-H7UqVh.js → RBMenubar.stories-D2PEGpM9.js} +1 -1
  61. package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-hlclv6jI.js → RBMultiRangeInput.stories-Bm2qSmjq.js} +1 -1
  62. package/cli/storybook-dist/assets/{RBPagination.stories-DWCeoZ_S.js → RBPagination.stories-DDleJa5N.js} +1 -1
  63. package/cli/storybook-dist/assets/{RBPopover.stories-C514M09_.js → RBPopover.stories-27YxPSFG.js} +1 -1
  64. package/cli/storybook-dist/assets/{RBPrompt.stories-BSItvfiT.js → RBPrompt.stories-BFaIOctr.js} +1 -1
  65. package/cli/storybook-dist/assets/{RBRadioBox.stories-BBb4YbFU.js → RBRadioBox.stories-BroDar0t.js} +1 -1
  66. package/cli/storybook-dist/assets/{RBRadioButton.stories-CXyOGJCv.js → RBRadioButton.stories-M3O-wX7V.js} +1 -1
  67. package/cli/storybook-dist/assets/{RBRangeDateInput.stories-qvhubu0e.js → RBRangeDateInput.stories-CWlM_D5X.js} +1 -1
  68. package/cli/storybook-dist/assets/{RBResizable.stories-CZKeyGjH.js → RBResizable.stories-ui8X1qnc.js} +1 -1
  69. package/cli/storybook-dist/assets/{RBSelectBox.stories-CRKP3ybf.js → RBSelectBox.stories-C8T6cfA0.js} +1 -1
  70. package/cli/storybook-dist/assets/{RBSheet.stories-sCM3ZJUa.js → RBSheet.stories-BCOo_Zcc.js} +1 -1
  71. package/cli/storybook-dist/assets/{RBSideNumberInput.stories-Bb7gw8RF.js → RBSideNumberInput.stories-CU2_MVlJ.js} +1 -1
  72. package/cli/storybook-dist/assets/{RBSlider.stories-23OAQU5z.js → RBSlider.stories-DgOp1TFc.js} +1 -1
  73. package/cli/storybook-dist/assets/{RBSpinner.stories-2xcQdcmt.js → RBSpinner.stories-qLScY3zZ.js} +1 -1
  74. package/cli/storybook-dist/assets/{RBSwitch.stories-BACUaJm_.js → RBSwitch.stories-SXSY1QMb.js} +1 -1
  75. package/cli/storybook-dist/assets/{RBTab.stories-CDHE3sVs.js → RBTab.stories-BzXXTsXP.js} +1 -1
  76. package/cli/storybook-dist/assets/{RBTable.stories-C0Cfhfl7.js → RBTable.stories-DHTeRNVk.js} +1 -1
  77. package/cli/storybook-dist/assets/{RBTableRow.stories-BF8kIkpp.js → RBTableRow.stories-DRNRsd15.js} +1 -1
  78. package/cli/storybook-dist/assets/{RBTabs.stories-BdJWRq1o.js → RBTabs.stories-DP0noBLp.js} +1 -1
  79. package/cli/storybook-dist/assets/{RBTextArea.stories-CGy2xeYH.js → RBTextArea.stories-TkaejfxJ.js} +1 -1
  80. package/cli/storybook-dist/assets/{RBTextField.stories-IfUOzxCM.js → RBTextField.stories-B6VpCGyt.js} +1 -1
  81. package/cli/storybook-dist/assets/{RBTextInput.stories-VjXNFo4R.js → RBTextInput.stories-D3hZ08e-.js} +1 -1
  82. package/cli/storybook-dist/assets/{RBToast.stories-BzcobblV.js → RBToast.stories-BQ2wS2U0.js} +1 -1
  83. package/cli/storybook-dist/assets/{RBToggle.stories-DuLE5mKv.js → RBToggle.stories-GVOjGqo3.js} +1 -1
  84. package/cli/storybook-dist/assets/{RBToggleGroup.stories-CuR0N0GJ.js → RBToggleGroup.stories-pzx0btOD.js} +1 -1
  85. package/cli/storybook-dist/assets/{RBTooltip.stories--aAgNC8_.js → RBTooltip.stories-jufB6fa1.js} +1 -1
  86. package/cli/storybook-dist/assets/{SideNumberInput-DyWQxYUj.js → SideNumberInput-BYlmNHEw.js} +1 -1
  87. package/cli/storybook-dist/assets/{entry-preview-Du01HPfn.js → entry-preview-DY0ZrYNE.js} +1 -1
  88. package/cli/storybook-dist/assets/{iframe-DFBATQRS.js → iframe-LwKb1w63.js} +2 -2
  89. package/cli/storybook-dist/assets/{index-DG8XddNr.js → index-BsAhjDKO.js} +3 -3
  90. package/cli/storybook-dist/assets/preview-BJ0FvQZh.js +3 -0
  91. package/cli/storybook-dist/assets/{preview-donYBfDJ.js → preview-DiY2_30w.js} +2 -2
  92. package/cli/storybook-dist/assets/{preview-CigNHo-I.css → preview-kTZPgP6D.css} +1 -1
  93. package/cli/storybook-dist/assets/{react-resizable-panels-Dp9zi-o3.css → react-resizable-panels-HsFbxPbn.css} +1 -1
  94. package/cli/storybook-dist/iframe.html +1 -1
  95. package/cli/storybook-dist/project.json +1 -1
  96. package/cli/templates/common/README.md +2 -3
  97. package/cli/templates/common/requirements.txt +3 -3
  98. package/cli/templates/cpp/cpp/CMakeLists.txt +1 -1
  99. package/cli/templates/readme-cpp/README.md +3 -4
  100. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +1 -1
  101. package/package.json +5 -4
  102. package/cli/storybook-dist/assets/preview-e0NAgyy_.js +0 -3
  103. /package/cli/storybook-dist/assets/{react-resizable-panels.browser-8hC8dr-6.js → react-resizable-panels.browser-DuxpZLkF.js} +0 -0
package/cli/README.md CHANGED
@@ -11,7 +11,7 @@ muscat-pk new my-plugin
11
11
 
12
12
  ```bash
13
13
  muscat-pk new my-plugin [--type python|cpp|ros2|ros2-cpp] [--pm npm|yarn|pnpm|bun] [--yes]
14
- muscat-pk build # ui/content.tsx -> dist/ (minified) + distributable ZIP (--no-zip to skip)
14
+ muscat-pk build # ui/content.tsx -> dist/ (minified) + distributable ZIP
15
15
  muscat-pk dev:ui # preview the plugin in a mini-Muscat (preview-host)
16
16
  muscat-pk dev:back # run the back end in a Docker container (python / cpp / ros2)
17
17
  muscat-pk dev # dev:ui + dev:back
@@ -229,8 +229,10 @@ inside plugins.
229
229
  at `/plugin-dev` — a real mini-Muscat shell. Without a robot IP it needs a Muscat
230
230
  on `localhost:3000` for program preview: `dev` / `dev:ui` reuse any container that
231
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
232
+ `muscat-pk-muscat` — the S3 runtime image (`muscat/main/muscat.zip`) running the
233
233
  `common` service from `muscat/main/version.json`, cached under `~/.muscat-pk/muscat/`.
234
+ `dev --sim <model>` adds the services the model needs (log + manipulate / rby1), opens
235
+ common's Zenoh on the `muscat-pk` network, and starts simulators there (`cli/src/sim.mjs`).
234
236
  It keeps running between runs (`docker rm -f muscat-pk-muscat` to stop);
235
237
  `MUSCAT_PK_NO_LOCAL_MUSCAT=1` disables it.
236
238
 
@@ -87,8 +87,8 @@ function formatHelp() {
87
87
  color.bold("Commands:"),
88
88
  commandRow("new [dir]", `Scaffold a new plugin project ${color.dim("(prompts: author, email, description, type, front-package-manager)")}`),
89
89
  commandRow("type change", `Switch backend type ${color.dim("(python | cpp | ros2-python | ros2-cpp)")}`),
90
- commandRow("build", `Build + lock deps + vendor wheels + zip ${color.dim(`(${option("--no-zip")}: skip zip, ${option("--no-lock")}: skip python lock/vendor, ${option("--arch")} amd64|arm64|both)`)}`),
91
- commandRow("dev", "Run a local Muscat runtime, dev:ui, and dev:back together"),
90
+ commandRow("build", `Build + lock deps + vendor wheels + zip ${color.dim(`(${option("--arch")} amd64|arm64|both)`)}`),
91
+ commandRow("dev", `Run a local Muscat runtime, dev:ui, and dev:back together ${color.dim(`(${option("--sim")} 500880|500920|RBY2|rby1-a|rby1-m|rby1-ub)`)}`),
92
92
  commandRow("dev:ui", `Preview the plugin in a mini-Muscat ${color.dim("(preview-host)")}`),
93
93
  commandRow("dev:back", `Run the plugin back end ${color.dim("(python: watchfiles reload / ros2: Docker)")}`),
94
94
  commandRow("storybook", `Browse the bundled rb-components Storybook ${color.dim(`(${option("--port")} <n>, ${option("--rebuild")})`)}`),
@@ -96,6 +96,7 @@ function formatHelp() {
96
96
  commandRow("update", `Update ${color.magenta("@rainbow-robotics/muscat-plugin-kit")}, ${color.magenta("@rainbow-robotics/plugin-ui")} and ${color.magenta("@rainbow-robotics/preview-host")} ${color.dim(`(local-only if run from a project's local install; ${option("--global")} also updates the global CLI)`)}`),
97
97
  commandRow("uninstall", `Remove the installed ${color.cyan("muscat-pk")} CLI ${color.dim("(npm global or S3 binary)")}`),
98
98
  commandRow("sdk <list|update [version]>", "Manage the python rainbow-rb-sdk version"),
99
+ commandRow("sim list", `List the robot simulators for ${option("dev --sim")}`),
99
100
  commandRow("version", `Print the installed ${color.cyan("muscat-pk")} version`),
100
101
  commandRow("completion [shell]", `Print a shell completion script ${color.dim("(bash | zsh | fish | powershell)")}`),
101
102
  "",
@@ -137,6 +138,9 @@ async function main() {
137
138
  return await runUninstall(args);
138
139
  case "sdk":
139
140
  return await runSdk(args);
141
+ case "sim":
142
+ if (args[0] !== "list") throw new Error("usage: muscat-pk sim list");
143
+ return (await import("../src/sim.mjs")).runSimList();
140
144
  case "completion":
141
145
  return await runCompletion(args);
142
146
  case "version":
package/cli/src/build.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // muscat-pk build — bundle ui/content.tsx into dist/content.js, stage
2
2
  // manifest.json and assets/ for distribution, and zip dist/ into a
3
- // distributable ZIP (skip with --no-zip).
3
+ // distributable ZIP.
4
4
  //
5
5
  // sandbox.html is generated by the CLI — plugin authors never manage it.
6
6
  // The import map always reflects the current plugin-ui entry set, so users
@@ -407,8 +407,6 @@ async function promptVersion(root) {
407
407
  }
408
408
 
409
409
  export async function runBuild(args) {
410
- const noZip = args.includes("--no-zip");
411
- const noLock = args.includes("--no-lock");
412
410
  const root = findPluginRoot();
413
411
  const rt = manifestRuntime(root);
414
412
 
@@ -424,19 +422,17 @@ export async function runBuild(args) {
424
422
  const { pluginRoot, distDir } = await buildPlugin({ root, minify: true });
425
423
 
426
424
  // python / rclpy: lock + wheel vendor in Ubuntu 24.04 container
427
- if (!noLock) {
428
- const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime ?? {};
429
- const needsLock =
430
- manifest.type === "python" ||
431
- (manifest.type === "ros2" && manifest.rosClient === "rclpy");
432
- if (needsLock) {
433
- const { lockAndVendor } = await import("./python.mjs");
434
- await lockAndVendor(pluginRoot, archs);
435
- }
425
+ const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime ?? {};
426
+ const needsLock =
427
+ manifest.type === "python" ||
428
+ (manifest.type === "ros2" && manifest.rosClient === "rclpy");
429
+ if (needsLock) {
430
+ const { lockAndVendor } = await import("./python.mjs");
431
+ await lockAndVendor(pluginRoot, archs);
436
432
  }
437
433
 
438
434
  await exportRos2Build(root, archs);
439
435
  await exportCppBuild(root, distDir, archs);
440
436
  calcInstalledSize(pluginRoot);
441
- if (!noZip) packZip(pluginRoot);
437
+ packZip(pluginRoot);
442
438
  }
@@ -20,6 +20,7 @@ const COMMANDS = [
20
20
  ["update", "Update muscat-pk and plugin-ui"],
21
21
  ["uninstall", "Remove the installed muscat-pk CLI"],
22
22
  ["sdk", "Manage the rainbow-rb-sdk version (python: requirements.txt, C++: CMakeLists.txt)"],
23
+ ["sim", "List the robot simulators for dev --sim"],
23
24
  ["version", "Print the installed muscat-pk version"],
24
25
  ["completion", "Print a shell completion script"],
25
26
  ["help", "Show help"],
@@ -28,6 +29,7 @@ const COMMANDS = [
28
29
  const SUBCOMMANDS = {
29
30
  type: ["change"],
30
31
  sdk: ["list", "update"],
32
+ sim: ["list"],
31
33
  completion: ["bash", "zsh", "fish", "powershell"],
32
34
  };
33
35
 
package/cli/src/dev.mjs CHANGED
@@ -1,12 +1,14 @@
1
- // muscat-pk dev — run the local Muscat runtime (gateway, MediaMTX, common), dev:ui, and dev:back together.
2
- import { spawn } from "node:child_process";
1
+ // muscat-pk dev — run the local Muscat runtime (gateway, MediaMTX, common), robot simulators (--sim),
2
+ // dev:ui, and dev:back together.
3
+ import { spawn, spawnSync } from "node:child_process";
3
4
  import { existsSync } from "node:fs";
4
5
  import { connect as netConnect } from "node:net";
5
6
  import { resolve } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { runDevUi } from "./devUi.mjs";
8
9
  import { claimDevSession, stopDevBackContainers } from "./devSession.mjs";
9
- import { ensureLocalMuscat, GATEWAY_PORT, isPortInUse, robotHasMuscat } from "./muscatLocal.mjs";
10
+ import { ensureLocalMuscat, GATEWAY_PORT, isPortInUse, MUSCAT_CONTAINER, robotHasMuscat } from "./muscatLocal.mjs";
11
+ import { resolveSim, startSim, stopSims } from "./sim.mjs";
10
12
  import { assertManifestPorts, assertManifestVariables, readManifest } from "./manifest.mjs";
11
13
  import { devPluginId } from "./devId.mjs";
12
14
  import {
@@ -14,6 +16,7 @@ import {
14
16
  ensureIdeVenv,
15
17
  findPluginRoot,
16
18
  log,
19
+ parseArgs,
17
20
  prompt,
18
21
  promptRobotConnection,
19
22
  venvPython,
@@ -129,13 +132,24 @@ export async function runDev(args = []) {
129
132
  // the robot itself — the local Muscat then publishes the zenoh port for it.
130
133
  if (!proxyEndpoint) await ensureRemoteZenohEnv(robotIp, { log });
131
134
 
135
+ // No robot: a simulator stands in for it (--sim, else the manifest's target).
136
+ const sim = remoteRobot ? null : resolveSim(parseArgs(args).sim, readManifest(root));
137
+
132
138
  // A robot running Muscat is the gateway (:10000) and common for both the UI and the backend.
133
139
  // Otherwise the local Muscat runtime is; the backend joins its netns, so the plugin's declared
134
140
  // ports are published from it too.
135
141
  const muscat = (await robotHasMuscat(robotIp)) ? null : await ensureLocalMuscat({
136
142
  pluginPorts: portRanges,
137
143
  zenohPort: remoteRobot && !proxyEndpoint ? Number(process.env.MUSCAT_ZENOH_PORT) || null : null,
144
+ services: sim?.services,
145
+ env: sim?.env,
138
146
  }).catch((e) => { log.warn(`local Muscat: ${e.message}`); return null; });
147
+ if (sim && muscat !== MUSCAT_CONTAINER) {
148
+ log.warn(`--sim needs muscat-pk's own Muscat${muscat ? ` ('${muscat}' is running — stop it to use the simulator)` : ""}; no simulator.`);
149
+ } else if (sim) {
150
+ // dev:ui then serves the real Muscat UI against the local Muscat, like with muscat-dev.
151
+ if (await startSim(sim).catch((e) => log.warn(`simulator: ${e.message}`))) process.env.MUSCAT_PK_SIM = sim.name;
152
+ }
139
153
  if (muscat) {
140
154
  process.env.MUSCAT_PK_MUSCAT_CONTAINER = muscat;
141
155
  } else if (remoteRobot || (await isPortInUse(GATEWAY_PORT))) {
@@ -160,6 +174,11 @@ export async function runDev(args = []) {
160
174
  if (hostAgent && hostAgent.exitCode === null) hostAgent.kill();
161
175
  if (backend.exitCode === null) backend.kill();
162
176
  stopDevBackContainers();
177
+ // Only our own runtime — a muscat-dev the user started stays up.
178
+ if (muscat === MUSCAT_CONTAINER) {
179
+ stopSims();
180
+ spawnSync("docker", ["rm", "-f", MUSCAT_CONTAINER], { stdio: "ignore" });
181
+ }
163
182
  };
164
183
  process.once("SIGINT", cleanup);
165
184
  process.once("SIGTERM", cleanup);
@@ -61,7 +61,7 @@ function muscatZenohEnv(container) {
61
61
 
62
62
  function backendNetwork(portRanges, robotGateway) {
63
63
  const muscat = process.env.MUSCAT_PK_MUSCAT_CONTAINER;
64
- if (muscat) return { run: ["--network", `container:${muscat}`], env: muscatZenohEnv(muscat), shared: true };
64
+ if (muscat) return { run: ["--network", `container:${muscat}`, "--ipc", "host"], env: muscatZenohEnv(muscat), shared: true };
65
65
  // The robot runs Muscat: its gateway, own netns (host network on Linux), declared ports published.
66
66
  if (robotGateway) {
67
67
  const run = usesHostNetwork() ? ["--network", "host"] : publishPortFlags(portRanges);
package/cli/src/devUi.mjs CHANGED
@@ -214,7 +214,8 @@ export async function runDevUi(args = []) {
214
214
  });
215
215
  }
216
216
 
217
- const localDevMuscat = await localFullMuscat(robotIp);
217
+ // muscat-dev, or our own Muscat with a simulator (`dev --sim`): a full local Muscat to show.
218
+ const localDevMuscat = !!process.env.MUSCAT_PK_SIM || (await localFullMuscat(robotIp));
218
219
  const uiHost = isRemoteRobot(robotIp) ? robotIp : localDevMuscat ? "localhost" : "";
219
220
 
220
221
  const previewRoot = findPreviewHostRoot();
package/cli/src/mcp.mjs CHANGED
@@ -39,7 +39,7 @@ import { spawnSync } from "node:child_process";
39
39
  import { homedir } from "node:os";
40
40
  import { join, resolve } from "node:path";
41
41
  import { fileURLToPath } from "node:url";
42
- import { kitRoot, kitVersion, findPluginRoot, venvPython } from "./util.mjs";
42
+ import { dockerUserFlags, kitRoot, kitVersion, findPluginRoot, venvPython } from "./util.mjs";
43
43
  import { PYTHON_DEV_IMAGE } from "./python.mjs";
44
44
  import { SHIPPED_STORIES_SRC, SHIPPED_DESIGN_TOKENS } from "./storybook.mjs";
45
45
  import { CPP_DEV_IMAGE } from "./cpp.mjs";
@@ -483,14 +483,14 @@ function introspectInDocker(probe) {
483
483
  const cache = join(cacheRoot, ver || "latest");
484
484
  // Only the SDK version in use is kept (~165MB each) — other versions don't pile up.
485
485
  for (const old of existsSync(cacheRoot) ? readdirSync(cacheRoot) : []) {
486
- if (join(cacheRoot, old) !== cache) rmSync(join(cacheRoot, old), { recursive: true, force: true });
486
+ if (join(cacheRoot, old) !== cache) removeCache(join(cacheRoot, old));
487
487
  }
488
488
  mkdirSync(cache, { recursive: true });
489
489
  const pkg = ver ? `rainbow-rb-sdk==${ver}` : "rainbow-rb-sdk";
490
490
  const script =
491
491
  `[ -f /c/.ok ] || { pip3 install -q --break-system-packages --target /c '${pkg}' >&2 && touch /c/.ok; } && ` +
492
492
  `PYTHONPATH=/c python3 -c "$PROBE"`;
493
- const r = spawnSync("docker", ["run", "--rm", "-v", `${cache}:/c`, "-e", "PROBE", PYTHON_DEV_IMAGE, "bash", "-c", script], {
493
+ const r = spawnSync("docker", ["run", "--rm", ...dockerUserFlags(), "-v", `${cache}:/c`, "-e", "PROBE", PYTHON_DEV_IMAGE, "bash", "-c", script], {
494
494
  encoding: "utf8",
495
495
  maxBuffer: 8 << 20,
496
496
  env: { ...process.env, PROBE: probe },
@@ -499,6 +499,20 @@ function introspectInDocker(probe) {
499
499
  return parseProbe(r);
500
500
  }
501
501
 
502
+ // Caches written by older kits as root (Linux) can't be unlinked by the user — clear them as root in Docker.
503
+ function removeCache(dir) {
504
+ try {
505
+ rmSync(dir, { recursive: true, force: true });
506
+ } catch {
507
+ spawnSync("docker", ["run", "--rm", "-v", `${dir}:/c`, PYTHON_DEV_IMAGE, "find", "/c", "-mindepth", "1", "-delete"], { stdio: "ignore" });
508
+ try {
509
+ rmSync(dir, { recursive: true, force: true });
510
+ } catch {
511
+ /* left for a later run — must not block reading the SDK */
512
+ }
513
+ }
514
+ }
515
+
502
516
  function parseProbe(r) {
503
517
  // Importing rainbow.rb_sdk has side effects that print to stdout (e.g. the
504
518
  // Zenoh client logs its connection endpoints), ahead of our own final
@@ -3,12 +3,12 @@
3
3
  // preview-host hits http://localhost:3000/common/* (baked into the nexus bundle), so program
4
4
  // preview works with no front-end change. The backend container joins its netns. We reuse any
5
5
  // Muscat already publishing :10000 (e.g. a `muscat` / `muscat-dev` container) and only otherwise
6
- // start our own: the S3 runtime image with just the `common` service.
6
+ // start our own: the S3 runtime image with `common` (plus the services `--sim` needs).
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { createServer } from "node:net";
9
9
  import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
- import { resolve } from "node:path";
11
+ import { dirname, resolve } from "node:path";
12
12
  import { mediaPortFlags, MEDIA_PORTS, publishPortFlags } from "./ports.mjs";
13
13
  import { canSkipImageBuild, hasBin, log, buildCacheFlags } from "./util.mjs";
14
14
  import { LOCAL_HOSTS } from "./zenohDev.mjs";
@@ -113,21 +113,40 @@ async function ensureRuntimeImage(total) {
113
113
  return image;
114
114
  }
115
115
 
116
- async function ensureCommonBinary(version) {
116
+ async function ensureServiceBinary(svc, version) {
117
117
  const ARCH = dockerArch();
118
- const dir = resolve(CACHE, "services", "common");
118
+ const dir = resolve(CACHE, "services", svc);
119
119
  const stamp = resolve(dir, ".version");
120
- if (existsSync(stamp) && readFileSync(stamp, "utf8").trim() === version && existsSync(resolve(dir, `common.${ARCH}.bin`))) return dir;
121
- log.step(`downloading Muscat common service ${version} (${ARCH})…`);
122
- const zip = resolve(CACHE, `common-${version}.zip`);
123
- writeFileSync(zip, await fetchBuffer(`${S3}/common/${version}.zip`));
124
- extractZip(zip, dir, ["config.env", `common.${ARCH}.bin`]);
120
+ if (existsSync(stamp) && readFileSync(stamp, "utf8").trim() === version && existsSync(resolve(dir, `${svc}.${ARCH}.bin`))) return dir;
121
+ log.step(`downloading Muscat ${svc} service ${version} (${ARCH})…`);
122
+ const zip = resolve(CACHE, `${svc}-${version}.zip`);
123
+ writeFileSync(zip, await fetchBuffer(`${S3}/${svc}/${version}.zip`));
124
+ extractZip(zip, dir, ["config.env", `${svc}.${ARCH}.bin`]);
125
125
  rmSync(zip, { force: true });
126
- chmodSync(resolve(dir, `common.${ARCH}.bin`), 0o755);
126
+ chmodSync(resolve(dir, `${svc}.${ARCH}.bin`), 0o755);
127
127
  writeFileSync(stamp, `${version}\n`);
128
128
  return dir;
129
129
  }
130
130
 
131
+ // Sim containers live on this network and connect to common's Zenoh by container name.
132
+ export const MUSCAT_NETWORK = "muscat-pk";
133
+
134
+ function ensureNetwork() {
135
+ if (spawnSync("docker", ["network", "inspect", MUSCAT_NETWORK], { stdio: "ignore" }).status === 0) return;
136
+ spawnSync("docker", ["network", "create", "--label", "com.rainbow-robotics.muscat-pk=true", MUSCAT_NETWORK], { stdio: "ignore" });
137
+ }
138
+
139
+ // common (127.0.0.1:7447) and the services (127.0.0.1:0) listen on loopback by default; rb-ctl's
140
+ // listen-endpoints file opens both to the network. Sims reach common as muscat-pk-muscat:7447, and
141
+ // gossip has them dial each service back — on loopback only the sim that happened to connect first
142
+ // answers manipulate's receive_one.
143
+ function writeZenohListen(open) {
144
+ const file = resolve(CACHE, "data", "zenoh_listen_endpoints.json5");
145
+ if (!open) return rmSync(file, { force: true });
146
+ mkdirSync(dirname(file), { recursive: true });
147
+ writeFileSync(file, `{\n common: ["tcp/0.0.0.0:7447"],\n service: ["tcp/0.0.0.0:0"],\n}\n`);
148
+ }
149
+
131
150
  async function waitForApi(timeoutMs = 180_000) {
132
151
  const start = Date.now();
133
152
  for (;;) {
@@ -187,14 +206,23 @@ const containerLabel = (name, label) =>
187
206
  /**
188
207
  * Make sure a Muscat answers on localhost:3000 / :10000. Returns the container name,
189
208
  * or null when one can't be started (no Docker, foreign process on the ports).
190
- * The container is left running across runs — stop it with `docker rm -f muscat-pk-muscat`.
209
+ * `dev` removes it on exit; `dev:ui` alone leaves it running (`docker rm -f muscat-pk-muscat`).
191
210
  */
192
- export async function ensureLocalMuscat({ pluginPorts = [], zenohPort = null } = {}) {
211
+ export async function ensureLocalMuscat({ pluginPorts = [], zenohPort = null, services = [], env = {} } = {}) {
193
212
  if (process.env.MUSCAT_PK_NO_LOCAL_MUSCAT === "1") return null;
194
- const portsKey = [...pluginPorts, ...(zenohPort ? [`zenoh:${zenohPort}`] : [])].join(",");
213
+ // Extra services (log, manipulate, rby1, ...) and env come with --sim; a different set recreates it too.
214
+ const svcs = ["common", ...services.filter((s) => s !== "common")];
215
+ const envFlags = Object.entries(env).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
216
+ const portsKey = [
217
+ ...pluginPorts,
218
+ ...(zenohPort ? [`zenoh:${zenohPort}`] : []),
219
+ "ipc:host",
220
+ ...(services.length ? [`svc:${svcs.join("+")}`] : []),
221
+ ...Object.entries(env).map(([k, v]) => `${k}=${v}`),
222
+ ].join(",");
195
223
  const existing = hasBin("docker") ? localMuscatContainer() : null;
196
224
  if (existing === MUSCAT_CONTAINER && containerLabel(existing, PORTS_LABEL) !== portsKey) {
197
- log.step(`recreating local Muscat to publish ${portsKey || "no extra"} port(s)`);
225
+ log.step(`recreating local Muscat for ${portsKey || "no extra ports / services"}`);
198
226
  spawnSync("docker", ["rm", "-f", MUSCAT_CONTAINER], { stdio: "ignore" });
199
227
  } else if (existing) {
200
228
  if (existing !== MUSCAT_CONTAINER && portsKey)
@@ -213,11 +241,15 @@ export async function ensureLocalMuscat({ pluginPorts = [], zenohPort = null } =
213
241
 
214
242
  const versions = await resolveVersions();
215
243
  const total = versions.total;
216
- const commonVersion = versions.services?.common?.version;
217
- if (!total || !commonVersion) throw new Error("version.json has no total / services.common.version");
244
+ if (!total) throw new Error("version.json has no total");
245
+ const missing = svcs.filter((s) => !versions.services?.[s]?.version);
246
+ if (missing.length) throw new Error(`version.json has no services.${missing.join(" / ")}.version`);
218
247
 
219
248
  const image = await ensureRuntimeImage(total);
220
- const commonDir = await ensureCommonBinary(commonVersion);
249
+ const svcMounts = [];
250
+ for (const s of svcs) svcMounts.push("-v", `${await ensureServiceBinary(s, versions.services[s].version)}:/app/services/${s}`);
251
+ ensureNetwork();
252
+ writeZenohListen(services.length > 0);
221
253
 
222
254
  let media = true;
223
255
  for (const port of MEDIA_PORTS) if (await isPortInUse(port)) media = false;
@@ -228,12 +260,16 @@ export async function ensureLocalMuscat({ pluginPorts = [], zenohPort = null } =
228
260
  const published = pluginPorts.filter((port) => !busy.includes(port));
229
261
 
230
262
  spawnSync("docker", ["rm", "-f", MUSCAT_CONTAINER], { stdio: "ignore" });
231
- log.step(`starting local Muscat (common ${commonVersion}) → http://localhost:${API_PORT}/common/docs`);
263
+ log.step(`starting local Muscat (${svcs.map((s) => `${s} ${versions.services[s].version}`).join(", ")}) → http://localhost:${API_PORT}/common/docs`);
232
264
  const r = spawnSync("docker", [
233
265
  "run", "-d", "--name", MUSCAT_CONTAINER, "--label", "com.rainbow-robotics.muscat-pk=true", "--label", `${PORTS_LABEL}=${portsKey}`,
234
- "-e", "RB_ENABLED_SERVICES=common",
266
+ "--network", MUSCAT_NETWORK,
267
+ // Like the robot, where plugins run with --ipc host: zenoh SHM and plugin shm_open reach the services.
268
+ "--ipc", "host",
269
+ "-e", `RB_ENABLED_SERVICES=${svcs.join(",")}`,
235
270
  "-e", "RB_RUNTIME_ENABLE_ZENOH_BRIDGE=0",
236
- "-v", `${commonDir}:/app/services/common`,
271
+ ...envFlags,
272
+ ...svcMounts,
237
273
  "-v", `${resolve(CACHE, "data")}:/app/data`,
238
274
  "-p", `127.0.0.1:${API_PORT}:${API_PORT}`,
239
275
  "-p", `127.0.0.1:${GATEWAY_PORT}:${GATEWAY_PORT}`,
@@ -245,7 +281,7 @@ export async function ensureLocalMuscat({ pluginPorts = [], zenohPort = null } =
245
281
  ], { stdio: ["ignore", "ignore", "inherit"] });
246
282
  if (r.status !== 0) throw new Error("failed to start the local Muscat container.");
247
283
  await waitForApi();
248
- log.step(`local Muscat ready — keeps running between runs; stop with: docker rm -f ${MUSCAT_CONTAINER}`);
284
+ log.step("local Muscat ready");
249
285
  if (published.length) log.step(`plugin backend ports published on 127.0.0.1: ${published.join(", ")}`);
250
286
  if (zenohPort) log.step(`Zenoh dev peer port published on :${zenohPort}`);
251
287
  return MUSCAT_CONTAINER;
@@ -69,7 +69,7 @@ export function mount(container, { socketUrl, pluginId, hostServices, data, onSa
69
69
  // (there may be no backend at all in dev, e.g. no robot reachable) — each rb
70
70
  // method awaits this promise internally instead.
71
71
  const wsPromise = RbWs.connect(socketUrl, pluginId);
72
- const rb = createLazyRbSdk(wsPromise, hostServices, { data, onSave });
72
+ const rb = createLazyRbSdk(wsPromise, hostServices, { data, onSave, pluginId });
73
73
  const root = createRoot(container);
74
74
  let mounted = true;
75
75
  const render = (Component) => {
@@ -0,0 +1,163 @@
1
+ // `dev --sim <model>`: robot simulators next to the local Muscat, for developing without a robot.
2
+ // Cobot: coreSW (rainbowroboticsofficial/cobot-sim) per component, joined to common's Zenoh.
3
+ // RB-Y1: rby1-sim + Xvfb built locally (it needs a GLFW window; there is no headless mode yet).
4
+ import { spawnSync } from "node:child_process";
5
+ import { canSkipImageBuild, log } from "./util.mjs";
6
+ import { MUSCAT_NETWORK } from "./muscatLocal.mjs";
7
+
8
+ const COBOT_IMAGE = "rainbowroboticsofficial/cobot-sim";
9
+ const RBY1_IMAGE = "rainbowroboticsofficial/rby1-sim";
10
+ const RBY1_LOCAL = "muscat-pk-rby1-sim";
11
+ const RBY1_CONTAINER = "muscat-pk-sim-rby1";
12
+ const SIM_LABEL = "com.rainbow-robotics.muscat-pk.sim";
13
+
14
+ // ponytail: model table in code; move to S3 version.json when sims are versioned with Muscat (amr: no sim yet)
15
+ // sim name → coreSW RB_MODEL per component, and the Muscat robot model it registers as.
16
+ const COBOT_MODELS = {
17
+ "500880": { robotModel: "C500880", models: ["500880"] },
18
+ "500920": { robotModel: "C500920", models: ["500920"] },
19
+ RBY2: { robotModel: "RBY2", models: ["C500880", "C501880", "C502880"] },
20
+ };
21
+ const RBY1_TYPES = ["a", "m", "ub"];
22
+
23
+ // rby1-sim renders MuJoCo with software GL every frame; one GL thread and a tiny screen cut it from
24
+ // ~3.5 cores to ~1. The cap keeps a busy Mac usable; the image already skips RT-loop timeouts.
25
+ const RBY1_DOCKERFILE = `ARG BASE=0.10.6-m_v1.3
26
+ FROM ${RBY1_IMAGE}:\${BASE}
27
+ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xvfb && rm -rf /var/lib/apt/lists/*
28
+ ENV LP_NUM_THREADS=0 DISPLAY=:99
29
+ CMD ["sh", "-c", "Xvfb :99 -screen 0 320x240x16 -nolisten tcp & sleep 1; exec ./app_main"]
30
+ `;
31
+
32
+ export const SIM_NAMES = [...Object.keys(COBOT_MODELS), ...RBY1_TYPES.map((t) => `rby1-${t}`)];
33
+
34
+ /** `muscat-pk sim list`: "<name>\t<what runs>" per line, for scripts and the VS Code Dev picker. */
35
+ export function runSimList() {
36
+ for (const [name, { robotModel, models }] of Object.entries(COBOT_MODELS))
37
+ console.log(`${name}\tcobot · ${robotModel}${models.length > 1 ? ` (${models.join(", ")})` : ""}`);
38
+ for (const t of RBY1_TYPES) console.log(`rby1-${t}\tRB-Y1 ${t.toUpperCase()}`);
39
+ }
40
+
41
+ /** "500880" | "RBY2" | "rby1-m" | "rby1-m_v1.2" → { services, env, ... } or null. */
42
+ export function parseSim(name) {
43
+ if (COBOT_MODELS[name]) return { name, kind: "cobot", services: ["log", "manipulate"], env: {}, ...COBOT_MODELS[name] };
44
+ const m = /^rby1-(a|m|ub)(?:_(v[\d.]+))?$/.exec(name ?? "");
45
+ if (m) return { name, kind: "rby1", services: ["log", "rby1"], env: { RBY1_HOST: RBY1_CONTAINER }, robotModel: m[1], type: m[1], version: m[2] };
46
+ return null;
47
+ }
48
+
49
+ /** Only an explicit --sim <name> runs a simulator. */
50
+ export function resolveSim(simArg, manifest) {
51
+ if (simArg === undefined) return null;
52
+ const sim = typeof simArg === "string" ? parseSim(simArg) : null;
53
+ if (!sim) throw new Error(`--sim takes one of: ${SIM_NAMES.join(", ")} (rby1 also takes rby1-m_v1.2)`);
54
+ const models = Array.isArray(manifest.targetModels) ? manifest.targetModels : null;
55
+ if (models && !models.includes(simArg)) log.warn(`--sim ${simArg} is not in manifest targetModels (${models.join(", ")})`);
56
+ return sim;
57
+ }
58
+
59
+ async function hubTags(repo) {
60
+ const res = await fetch(`https://hub.docker.com/v2/repositories/${repo}/tags?page_size=100&ordering=last_updated`, {
61
+ signal: AbortSignal.timeout(10_000),
62
+ });
63
+ if (!res.ok) throw new Error(`Docker Hub ${repo}: HTTP ${res.status}`);
64
+ return (await res.json()).results.map((t) => t.name);
65
+ }
66
+
67
+ // "0.10.6-m_v1.3" → [0, 10, 6, 1, 3]
68
+ const semver = (v) => v.split(/[._v-]/).map(Number).filter((n) => !Number.isNaN(n));
69
+ const newer = (a, b) => {
70
+ const [x, y] = [semver(a), semver(b)];
71
+ for (let i = 0; i < Math.max(x.length, y.length); i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) - (y[i] ?? 0);
72
+ return 0;
73
+ };
74
+
75
+ // Latest published tag; offline falls back to whatever was pulled before.
76
+ async function latestTag(repo, pattern) {
77
+ try {
78
+ const tags = (await hubTags(repo)).filter((t) => pattern.test(t));
79
+ if (tags.length) return tags.sort(newer).at(-1);
80
+ } catch (e) {
81
+ log.warn(`${e.message} — using a local image`);
82
+ }
83
+ const local = spawnSync("docker", ["images", repo, "--format", "{{.Tag}}"], { encoding: "utf8" }).stdout ?? "";
84
+ const tags = local.split("\n").map((t) => t.trim()).filter((t) => pattern.test(t));
85
+ if (!tags.length) throw new Error(`no ${repo} image matching ${pattern} (offline and nothing pulled yet)`);
86
+ return tags.sort(newer).at(-1);
87
+ }
88
+
89
+ function pull(image) {
90
+ if (spawnSync("docker", ["image", "inspect", image], { stdio: "ignore" }).status === 0) return;
91
+ log.step(`docker pull ${image}…`);
92
+ if (spawnSync("docker", ["pull", image], { stdio: "inherit" }).status !== 0) throw new Error(`docker pull ${image} failed`);
93
+ }
94
+
95
+ function run(name, args) {
96
+ spawnSync("docker", ["rm", "-f", name], { stdio: "ignore" });
97
+ const r = spawnSync("docker", [
98
+ // --ipc host like the robot: the cobot coreSWs share one shm segment (RB_MESSAGE_SHM, a slot per model).
99
+ "run", "-d", "--name", name, "--network", MUSCAT_NETWORK, "--ipc", "host",
100
+ "--label", "com.rainbow-robotics.muscat-pk=true", "--label", `${SIM_LABEL}=true`,
101
+ ...args,
102
+ ], { stdio: ["ignore", "ignore", "inherit"] });
103
+ if (r.status !== 0) throw new Error(`failed to start ${name}`);
104
+ }
105
+
106
+ // The Muscat UI (robot info, components, connection state) follows the registered robot model.
107
+ async function registerRobot(robotModel) {
108
+ const res = await fetch("http://127.0.0.1:3000/common/upsert-robot-info", {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: JSON.stringify({ robotModel }),
112
+ });
113
+ if (!res.ok) throw new Error(`upsert-robot-info ${robotModel}: HTTP ${res.status}`);
114
+ log.step(`robot model ${robotModel} registered in the local Muscat`);
115
+ }
116
+
117
+ /** Start the simulator containers and register the robot model; returns the container names. */
118
+ export async function startSim(sim) {
119
+ const names = await startContainers(sim);
120
+ await registerRobot(sim.robotModel);
121
+ return names;
122
+ }
123
+
124
+ // Needs the local Muscat on MUSCAT_NETWORK.
125
+ async function startContainers(sim) {
126
+ stopSims();
127
+ if (sim.kind === "cobot") {
128
+ const image = `${COBOT_IMAGE}:${await latestTag(COBOT_IMAGE, /^\d+\.\d+\.\d+$/)}`;
129
+ pull(image);
130
+ const names = sim.models.map((model) => `muscat-pk-sim-${model}`);
131
+ sim.models.forEach((model, i) => run(names[i], [
132
+ "--cap-add", "SYS_NICE", "--ulimit", "rtprio=99",
133
+ "-e", `RB_MODEL=${model}`,
134
+ "-e", "RB_ZENOH_CONNECT=tcp/muscat-pk-muscat:7447",
135
+ image,
136
+ ]));
137
+ log.step(`cobot sim ${sim.name} (${image}) → ${sim.models.join(", ")}`);
138
+ return names;
139
+ }
140
+
141
+ const pattern = sim.version ? new RegExp(`^\\d+\\.\\d+\\.\\d+-${sim.type}_${sim.version.replace(/\./g, "\\.")}$`) : new RegExp(`^\\d+\\.\\d+\\.\\d+-${sim.type}_v[\\d.]+$`);
142
+ const base = await latestTag(RBY1_IMAGE, pattern);
143
+ const image = `${RBY1_LOCAL}:${base}`;
144
+ if (!canSkipImageBuild(image)) {
145
+ log.step(`building ${image} (rby1-sim + Xvfb, first run pulls ~150MB)…`);
146
+ const r = spawnSync("docker", ["build", "-t", image, "--build-arg", `BASE=${base}`, "--label", "com.rainbow-robotics.muscat-pk=true", "-"], {
147
+ input: RBY1_DOCKERFILE,
148
+ stdio: ["pipe", "inherit", "inherit"],
149
+ });
150
+ if (r.status !== 0) throw new Error(`docker build ${image} failed`);
151
+ }
152
+ run(RBY1_CONTAINER, ["--cpus", process.env.MUSCAT_PK_RBY1_CPUS || "1.5", image]);
153
+ log.step(`rby1 sim ${base} → ${RBY1_CONTAINER}:50051`);
154
+ return [RBY1_CONTAINER];
155
+ }
156
+
157
+ export function stopSims() {
158
+ const ids = (spawnSync("docker", ["ps", "-aq", "--filter", `label=${SIM_LABEL}=true`], { encoding: "utf8" }).stdout ?? "").split("\n").filter(Boolean);
159
+ if (!ids.length) return;
160
+ // The cobot coreSWs share these under --ipc host; drop them so the next run starts from a fresh segment.
161
+ spawnSync("docker", ["exec", ids[0], "sh", "-c", "rm -f /dev/shm/RB_COBOT_SHM /dev/shm/RB_MESSAGE_SHM"], { stdio: "ignore" });
162
+ spawnSync("docker", ["rm", "-f", ...ids], { stdio: "ignore" });
163
+ }
package/cli/src/util.mjs CHANGED
@@ -144,6 +144,8 @@ const LOCAL_HOSTS = new Set(["", "localhost", "127.0.0.1", "::1"]);
144
144
  */
145
145
  export async function promptRobotConnection(args = []) {
146
146
  const parsed = parseArgs(args);
147
+ // A simulator stands in for the robot.
148
+ if (parsed.sim !== undefined) return { robotIp: "", robotToken: "" };
147
149
  const robotIp =
148
150
  parsed["robot-ip"] ??
149
151
  process.env.MUSCAT_ROBOT_HOST ??