@rainbow-robotics/muscat-plugin-kit 1.10.3-dev.3 → 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 (94) hide show
  1. package/README.ko.md +186 -60
  2. package/README.md +186 -60
  3. package/cli/README.md +3 -1
  4. package/cli/bin/muscat-pk.mjs +5 -1
  5. package/cli/src/completion.mjs +2 -0
  6. package/cli/src/dev.mjs +19 -2
  7. package/cli/src/devBack.mjs +1 -1
  8. package/cli/src/devUi.mjs +2 -1
  9. package/cli/src/muscatLocal.mjs +55 -19
  10. package/cli/src/sim.mjs +163 -0
  11. package/cli/src/util.mjs +2 -0
  12. package/cli/storybook-dist/assets/{Color-YHDXOIA2-Cp8LHsF-.js → Color-YHDXOIA2-DSB_yvex.js} +1 -1
  13. package/cli/storybook-dist/assets/{Docs-Kg1Im4XT.js → Docs-C_90bDRy.js} +1 -1
  14. package/cli/storybook-dist/assets/{Docs-UJwiVwO2.js → Docs-CbW9HZd4.js} +1 -1
  15. package/cli/storybook-dist/assets/{Docs-LeW_BIsw.js → Docs-CoskoRMT.js} +1 -1
  16. package/cli/storybook-dist/assets/{Docs-B2ox3o_G.js → Docs-Cy9XFeLJ.js} +1 -1
  17. package/cli/storybook-dist/assets/{Docs-By11aG_M.js → Docs-De1oc64k.js} +1 -1
  18. package/cli/storybook-dist/assets/{Docs-ik2AhpV0.js → Docs-klD3F_Ai.js} +1 -1
  19. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-DPFneFUi.js → DocsRenderer-CFRXHY34-Bkm4uV_D.js} +1 -1
  20. package/cli/storybook-dist/assets/{NxAvatar.stories-CwnK8AUF.js → NxAvatar.stories-Ctf5G2FQ.js} +1 -1
  21. package/cli/storybook-dist/assets/{NxButton.stories-ueYdosfQ.js → NxButton.stories-DeLObVSj.js} +1 -1
  22. package/cli/storybook-dist/assets/{NxNumberInput.stories-M5ZCLbrK.js → NxNumberInput.stories-LqoY0ed1.js} +1 -1
  23. package/cli/storybook-dist/assets/{NxPagination.stories-bxxUnHEL.js → NxPagination.stories-DI6bXuJM.js} +1 -1
  24. package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BHxB2kr5.js → NxPointerHoldButton.stories-3HSV1XmN.js} +1 -1
  25. package/cli/storybook-dist/assets/{NxRangeInput.stories-BKfr-fp4.js → NxRangeInput.stories-1pjgQVaG.js} +1 -1
  26. package/cli/storybook-dist/assets/{NxSelectBox.stories-CG8FuTBq.js → NxSelectBox.stories-bcQPZEuF.js} +1 -1
  27. package/cli/storybook-dist/assets/{NxSlideRange.stories-BRTFtMTL.js → NxSlideRange.stories-BosX4axk.js} +1 -1
  28. package/cli/storybook-dist/assets/{NxSwitch.stories-Bwuyzn-R.js → NxSwitch.stories-DxV6dS33.js} +1 -1
  29. package/cli/storybook-dist/assets/{NxTextArea.stories-COq2sGqu.js → NxTextArea.stories-CO545-LP.js} +1 -1
  30. package/cli/storybook-dist/assets/{NxTextInput.stories-CjsBLz05.js → NxTextInput.stories-BP0npai1.js} +1 -1
  31. package/cli/storybook-dist/assets/{RBAccordion.stories-BARwuZ0Q.js → RBAccordion.stories-fVR4Va2a.js} +1 -1
  32. package/cli/storybook-dist/assets/{RBAccordionGroup.stories-COrPxETW.js → RBAccordionGroup.stories-Du5bsckD.js} +1 -1
  33. package/cli/storybook-dist/assets/{RBAlert.stories-BQrkjZPt.js → RBAlert.stories-DNkMXUu6.js} +1 -1
  34. package/cli/storybook-dist/assets/{RBBadge.stories-BTsLL-lK.js → RBBadge.stories-C44pXt8D.js} +1 -1
  35. package/cli/storybook-dist/assets/{RBBattery.stories-DAeQdJbc.js → RBBattery.stories-DE2JHvfL.js} +1 -1
  36. package/cli/storybook-dist/assets/{RBCard.stories-HyhOE80R.js → RBCard.stories-CuIda7NI.js} +1 -1
  37. package/cli/storybook-dist/assets/{RBCheckBox.stories-KFk0WqX_.js → RBCheckBox.stories-D1qM1ayr.js} +1 -1
  38. package/cli/storybook-dist/assets/{RBCheckIconButton.stories-CXbbktF5.js → RBCheckIconButton.stories-e4gkliBO.js} +1 -1
  39. package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-DWfYz_9Z.js → RBCheckLabelButton.stories-CDzCtsMI.js} +1 -1
  40. package/cli/storybook-dist/assets/{RBChipTab.stories-JPEgVFAS.js → RBChipTab.stories-DBrc_3Mp.js} +1 -1
  41. package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-CTPuWXrQ.js → RBChipsBoxTab.stories-hlrsHjKY.js} +1 -1
  42. package/cli/storybook-dist/assets/{RBColorIcons.stories-DiFRZ5jk.js → RBColorIcons.stories-BHUvpkdu.js} +1 -1
  43. package/cli/storybook-dist/assets/{RBCommand.stories-Pf1HAVIR.js → RBCommand.stories-azAF41NP.js} +1 -1
  44. package/cli/storybook-dist/assets/{RBConfirm.stories-D1cEPUXk.js → RBConfirm.stories-BMNjwbmT.js} +1 -1
  45. package/cli/storybook-dist/assets/{RBDateInput.stories-RPYpCjig.js → RBDateInput.stories-D0VezDzv.js} +1 -1
  46. package/cli/storybook-dist/assets/{RBDialog.stories-DJH-0XSR.js → RBDialog.stories-AOJ975Fy.js} +1 -1
  47. package/cli/storybook-dist/assets/{RBDivider.stories-uEj1vOgK.js → RBDivider.stories-NSvnj43Z.js} +1 -1
  48. package/cli/storybook-dist/assets/{RBDrawer.stories-BiMzElb2.js → RBDrawer.stories-Du0oTvD1.js} +1 -1
  49. package/cli/storybook-dist/assets/{RBDropdownMenu.stories-Da_TBkla.js → RBDropdownMenu.stories-Hsxve4PW.js} +1 -1
  50. package/cli/storybook-dist/assets/{RBEmptyView.stories-BHeKGfPS.js → RBEmptyView.stories-BlwHx3zp.js} +1 -1
  51. package/cli/storybook-dist/assets/{RBIconButton.stories-NEsRFoYp.js → RBIconButton.stories-DAPDY63x.js} +1 -1
  52. package/cli/storybook-dist/assets/{RBIcons.stories-CcZ-TTLS.js → RBIcons.stories-W5ZUV2L1.js} +1 -1
  53. package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-B0wpdQyR.js → RBInlineLabelButton.stories-C99j0Y3g.js} +1 -1
  54. package/cli/storybook-dist/assets/{RBJoyStick.stories-DEgBou3K.js → RBJoyStick.stories-Du0H1iAS.js} +1 -1
  55. package/cli/storybook-dist/assets/{RBLabelButton.stories-DT8drIsF.js → RBLabelButton.stories-BlvHaU_Z.js} +1 -1
  56. package/cli/storybook-dist/assets/{RBLight.stories-Dh-aoeyG.js → RBLight.stories-BY6DKrre.js} +1 -1
  57. package/cli/storybook-dist/assets/{RBMenubar.stories-D-H7UqVh.js → RBMenubar.stories-D2PEGpM9.js} +1 -1
  58. package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-hlclv6jI.js → RBMultiRangeInput.stories-Bm2qSmjq.js} +1 -1
  59. package/cli/storybook-dist/assets/{RBPagination.stories-DWCeoZ_S.js → RBPagination.stories-DDleJa5N.js} +1 -1
  60. package/cli/storybook-dist/assets/{RBPopover.stories-C514M09_.js → RBPopover.stories-27YxPSFG.js} +1 -1
  61. package/cli/storybook-dist/assets/{RBPrompt.stories-BSItvfiT.js → RBPrompt.stories-BFaIOctr.js} +1 -1
  62. package/cli/storybook-dist/assets/{RBRadioBox.stories-BBb4YbFU.js → RBRadioBox.stories-BroDar0t.js} +1 -1
  63. package/cli/storybook-dist/assets/{RBRadioButton.stories-CXyOGJCv.js → RBRadioButton.stories-M3O-wX7V.js} +1 -1
  64. package/cli/storybook-dist/assets/{RBRangeDateInput.stories-qvhubu0e.js → RBRangeDateInput.stories-CWlM_D5X.js} +1 -1
  65. package/cli/storybook-dist/assets/{RBResizable.stories-CZKeyGjH.js → RBResizable.stories-ui8X1qnc.js} +1 -1
  66. package/cli/storybook-dist/assets/{RBSelectBox.stories-CRKP3ybf.js → RBSelectBox.stories-C8T6cfA0.js} +1 -1
  67. package/cli/storybook-dist/assets/{RBSheet.stories-sCM3ZJUa.js → RBSheet.stories-BCOo_Zcc.js} +1 -1
  68. package/cli/storybook-dist/assets/{RBSideNumberInput.stories-Bb7gw8RF.js → RBSideNumberInput.stories-CU2_MVlJ.js} +1 -1
  69. package/cli/storybook-dist/assets/{RBSlider.stories-23OAQU5z.js → RBSlider.stories-DgOp1TFc.js} +1 -1
  70. package/cli/storybook-dist/assets/{RBSpinner.stories-2xcQdcmt.js → RBSpinner.stories-qLScY3zZ.js} +1 -1
  71. package/cli/storybook-dist/assets/{RBSwitch.stories-BACUaJm_.js → RBSwitch.stories-SXSY1QMb.js} +1 -1
  72. package/cli/storybook-dist/assets/{RBTab.stories-CDHE3sVs.js → RBTab.stories-BzXXTsXP.js} +1 -1
  73. package/cli/storybook-dist/assets/{RBTable.stories-C0Cfhfl7.js → RBTable.stories-DHTeRNVk.js} +1 -1
  74. package/cli/storybook-dist/assets/{RBTableRow.stories-BF8kIkpp.js → RBTableRow.stories-DRNRsd15.js} +1 -1
  75. package/cli/storybook-dist/assets/{RBTabs.stories-BdJWRq1o.js → RBTabs.stories-DP0noBLp.js} +1 -1
  76. package/cli/storybook-dist/assets/{RBTextArea.stories-CGy2xeYH.js → RBTextArea.stories-TkaejfxJ.js} +1 -1
  77. package/cli/storybook-dist/assets/{RBTextField.stories-IfUOzxCM.js → RBTextField.stories-B6VpCGyt.js} +1 -1
  78. package/cli/storybook-dist/assets/{RBTextInput.stories-VjXNFo4R.js → RBTextInput.stories-D3hZ08e-.js} +1 -1
  79. package/cli/storybook-dist/assets/{RBToast.stories-BzcobblV.js → RBToast.stories-BQ2wS2U0.js} +1 -1
  80. package/cli/storybook-dist/assets/{RBToggle.stories-DuLE5mKv.js → RBToggle.stories-GVOjGqo3.js} +1 -1
  81. package/cli/storybook-dist/assets/{RBToggleGroup.stories-CuR0N0GJ.js → RBToggleGroup.stories-pzx0btOD.js} +1 -1
  82. package/cli/storybook-dist/assets/{RBTooltip.stories--aAgNC8_.js → RBTooltip.stories-jufB6fa1.js} +1 -1
  83. package/cli/storybook-dist/assets/{SideNumberInput-DyWQxYUj.js → SideNumberInput-BYlmNHEw.js} +1 -1
  84. package/cli/storybook-dist/assets/{entry-preview-BTw3Jh8E.js → entry-preview-DY0ZrYNE.js} +1 -1
  85. package/cli/storybook-dist/assets/{iframe-BqPQIkPQ.js → iframe-LwKb1w63.js} +2 -2
  86. package/cli/storybook-dist/assets/{index-k_VkBflK.js → index-BsAhjDKO.js} +3 -3
  87. package/cli/storybook-dist/assets/{preview-Cog7BnbD.js → preview-BJ0FvQZh.js} +2 -2
  88. package/cli/storybook-dist/assets/{preview-C3ctYQQj.js → preview-DiY2_30w.js} +2 -2
  89. package/cli/storybook-dist/assets/{preview-CigNHo-I.css → preview-kTZPgP6D.css} +1 -1
  90. package/cli/storybook-dist/assets/{react-resizable-panels-Dp9zi-o3.css → react-resizable-panels-HsFbxPbn.css} +1 -1
  91. package/cli/storybook-dist/iframe.html +1 -1
  92. package/cli/storybook-dist/project.json +1 -1
  93. package/package.json +3 -3
  94. /package/cli/storybook-dist/assets/{react-resizable-panels.browser-8hC8dr-6.js → react-resizable-panels.browser-DuxpZLkF.js} +0 -0
package/README.md CHANGED
@@ -111,6 +111,7 @@ Install the ZIP from the robot's Muscat plugin page.
111
111
  | `muscat-pk build` | Build, lock Python deps, vendor wheels, ZIP |
112
112
  | `muscat-pk update` | Update the kit, `plugin-ui` and `preview-host` |
113
113
  | `muscat-pk sdk <list\|update [version]>` | Manage the `rainbow-rb-sdk` version |
114
+ | `muscat-pk sim list` | List the robot simulators for `dev --sim` |
114
115
  | `muscat-pk storybook` | Browse the bundled rb-components Storybook |
115
116
  | `muscat-pk mcp` | MCP server for AI coding agents |
116
117
  | `muscat-pk uninstall` | Remove the CLI |
@@ -134,9 +135,15 @@ muscat-pk new [dir] [--type python|cpp|ros2|ros2-cpp] [--pm npm|yarn|pnpm|bun]
134
135
  | `--pm` | front-end package manager; defaults to `yarn` inside a Yarn workspace, else `npm` |
135
136
  | `--author` / `--email` / `--description` | manifest fields (email is required by the manifest) |
136
137
  | `--yes` | no prompts; unspecified values use defaults |
137
- | `--no-install` | skip `npm install` |
138
+ | `--no-install` | skip `<pm> install`, the IDE `.venv` and the C++ dev image |
139
+ | `--help` | print the usage |
138
140
 
139
- Python projects also get an IDE-only `.venv` (see [9.6](#96-ide-support-python)).
141
+ The plugin id is `com.example.<dir>` (lowercased; edit it in `manifest.json`). `new` also writes `.gitignore`, `AGENTS.md`, `CLAUDE.md` and `.mcp.json` (see [11](#11-ai-coding-agent-setup-muscat-pk-mcp)), and runs `git init` with an initial commit (delete `.git` if you don't want it).
142
+
143
+ Unless `--no-install`:
144
+
145
+ - python / ros2 (rclpy): an IDE-only `.venv` (see [9.6](#96-ide-support-python)).
146
+ - cpp / ros2-cpp: builds the C++ dev image so the editor finds the SDK headers (skipped with a warning when Docker is missing).
140
147
 
141
148
  ### 3.2 `type change`
142
149
 
@@ -144,25 +151,67 @@ Python projects also get an IDE-only `.venv` (see [9.6](#96-ide-support-python))
144
151
  muscat-pk type change [--to python|cpp|ros2|ros2-cpp] [--readme]
145
152
  ```
146
153
 
147
- Rewrites `manifest.json` → `runtime` and adds the new backend's files. Files of the old type (`cpp/`, `ros2_ws/`, …) are left in place. `--readme` also replaces the README with the new type's.
154
+ | Option | Meaning |
155
+ |---|---|
156
+ | `--to` | new type; a numbered prompt when omitted |
157
+ | `--readme` | replace `README.md` with the new type's without asking (old file → `README.md.bak`) |
158
+
159
+ Rewrites `manifest.json` → `runtime` and adds the new backend's files (`cpp/` or `ros2_ws/`) unless that directory already exists. Files of the old type (`cpp/`, `ros2_ws/`, …) are left in place. When switching between a Python and a C++ backend it offers to replace the README (`--readme` answers yes). Switching to a C++ type prepares the C++ dev image like `new`.
148
160
 
149
161
  ### 3.3 `dev`, `dev:ui`, `dev:back`
150
162
 
151
163
  ```bash
152
- muscat-pk dev [--robot-ip <ip>] [--keep-id]
153
- muscat-pk dev:ui [--robot-ip <ip>] [--port <n>] [--keep-id]
154
- muscat-pk dev:back [--keep-id]
164
+ muscat-pk dev [--robot-ip <ip>] [--robot-token <token>] [--keep-id] [--sim <model>]
165
+ [--port <n>] [--no-open]
166
+ muscat-pk dev:ui [--robot-ip <ip>] [--port <n>] [--keep-id] [--no-open]
167
+ muscat-pk dev:back [--robot-ip <ip>] [--robot-token <token>] [--keep-id]
155
168
  ```
156
169
 
157
170
  | Command | What runs |
158
171
  |---|---|
159
- | `dev` | a local Muscat runtime when no robot is given, then `dev:ui` and `dev:back` |
160
- | `dev:ui` | the plugin's Vite dev server + preview-host at `http://localhost:4180/plugin-dev` (next free port) |
172
+ | `dev` | a local Muscat runtime when no robot is given (or the robot runs no Muscat), then `dev:ui` and `dev:back` |
173
+ | `dev:ui` | the plugin's Vite dev server + preview-host at `http://localhost:4180` (next free port), opened in the browser |
161
174
  | `dev:back` | the backend in a Docker container matching the robot. Python restarts on `py/**/*.py` changes (watchfiles) |
162
175
 
163
- - Robot IP comes from `--robot-ip`, `MUSCAT_ROBOT_HOST`, or a prompt (leave blank for local).
176
+ | Option | Meaning |
177
+ |---|---|
178
+ | `--robot-ip` | robot to develop against; else `MUSCAT_ROBOT_HOST`, else a prompt (blank = no robot) |
179
+ | `--robot-token` | service token for a robot with the login policy on; else `MUSCAT_ROBOT_TOKEN`, else a prompt (blank = none) |
180
+ | `--keep-id` | run under the manifest id instead of a dev id (see [9.3](#93-several-developers-on-one-robot)) |
181
+ | `--sim` | `dev` only, no robot: start a robot simulator (below). Skips the robot IP prompt |
182
+ | `--port` | preview-host port (default `4180`; the next free one when taken) |
183
+ | `--no-open` | don't open the browser (it is not opened in CI or without a TTY either) |
184
+
185
+ - One dev environment per PC: they share the backend container, the local Muscat and its ports. A second `dev` / `dev:ui` / `dev:back` asks to stop the running one (`Stop it and start this one? [y/N]`).
164
186
  - Against a remote robot the plugin runs under a dev id (see [9.3](#93-several-developers-on-one-robot)); `--keep-id` uses the manifest id.
165
- - The manifest is validated before start; undeclared listening ports stop the backend (see [5.6](#56-ports)).
187
+ - Before start, `dev` and `dev:back` check `manifest.json` → `variables` and `ports` (errors stop it); `dev:back` also prints other manifest errors as warnings. The full check runs on `build`. Undeclared listening ports stop the backend (see [5.6](#56-ports)).
188
+ - Against a robot running Muscat, the UI is that robot's Muscat UI and the backend uses its gateway. With a local `muscat-dev` container (or `--sim`) it is the local real Muscat UI; otherwise the mock preview.
189
+ - `dev` / `dev:ui` print a hint when a newer kit is published (`MUSCAT_PK_NO_UPDATE_CHECK=1` skips it).
190
+ - Stopping `dev` (Ctrl+C) removes the backend container, and its own local Muscat and simulators.
191
+
192
+ #### Developing without a robot (`--sim`)
193
+
194
+ With no robot IP, `dev --sim <model>` also starts a robot simulator next to the local Muscat. The plugin SDK's robot calls then reach a real coreSW.
195
+
196
+ ```bash
197
+ muscat-pk sim list # "<name>\t<what runs>" per line
198
+ muscat-pk dev --sim 500920
199
+ muscat-pk dev --sim rby1-m_v1.2
200
+ ```
201
+
202
+ | `--sim` | Muscat services | Simulator |
203
+ |---|---|---|
204
+ | `500880`, `500920` | common, log, manipulate | one `rainbowroboticsofficial/cobot-sim` |
205
+ | `RBY2` | common, log, manipulate | three cobot-sim (C500880, C501880, C502880) |
206
+ | `rby1-a`, `rby1-m`, `rby1-ub` (or a version, e.g. `rby1-m_v1.2`) | common, log, rby1 | `rainbowroboticsofficial/rby1-sim` + Xvfb, built locally |
207
+
208
+ - The simulated model is registered as the local Muscat's robot model (`500880` → C500880, `500920` → C500920, `rby1-m` → m). The UI is then the real Muscat UI (dashboard, components, Move, Log, Settings) instead of the mock preview.
209
+ - A simulator runs only with `--sim`. Any other value fails with `--sim takes one of: 500880, 500920, RBY2, rby1-a, rby1-m, rby1-ub (rby1 also takes rby1-m_v1.2)`. A model missing from the manifest's `targetModels` only gets a warning.
210
+ - Simulator images use the latest Docker Hub tag (an rby1 version picks the matching tag). Offline, an already pulled image is used.
211
+ - rby1-sim keeps rendering, so it uses about one CPU core. The cap is `MUSCAT_PK_RBY1_CPUS` (default 1.5).
212
+ - No simulator is attached while another Muscat such as `muscat-dev` is running.
213
+ - There is no amr simulator yet.
214
+ - The local Muscat, the simulators and the backend all run with `--ipc host` like on the robot, so zenoh SHM and `shm_open` reach each other.
166
215
 
167
216
  ### 3.4 `build`
168
217
 
@@ -172,18 +221,23 @@ muscat-pk build [--arch amd64|arm64|both]
172
221
 
173
222
  Steps:
174
223
 
175
- 1. Asks for a new `manifest.json` version (blank keeps the current one).
176
- 2. Validates the manifest.
177
- 3. `vite build` of every `contributes[].ui` entry → `dist/<entry>.js`.
178
- 4. Backend:
179
- - python: pins unpinned lines of `requirements.txt` to the latest PyPI version, writes `dist/requirements.lock` (`uv pip compile`), and vendors wheels into `dist/wheels/` per arch.
180
- - cpp: CMake build in Docker → `dist/<arch>/<entry>` plus `dist/<arch>/lib/librb_sdk.so`.
181
- - ros2: colcon build in Docker → `dist/ros2_ws/install-<arch>/`.
182
- 5. Packs `dist/` into `<project dir>-<version>.zip` in the project root.
224
+ 1. Asks for a new `manifest.json` version (blank keeps the current one; TTY only).
225
+ 2. Asks for the target arch (unless `--arch`) and deletes an existing ZIP of that version, so a failed build leaves no stale ZIP.
226
+ 3. Validates the manifest (all of section 5) and checks that each `contributes[].ui` has `ui/<entry>.tsx` (or `.ts` / `.jsx` / `.js`).
227
+ 4. `vite build` of every `contributes[].ui` entry → `dist/<entry>.js`. A plugin without `contributes` builds no UI. Without Node.js 20+ (binary CLI) it asks `Build the plugin without its UI? [y/N]`; yes drops `contributes` from `dist/manifest.json`.
228
+ 5. Copies `manifest.json`, `assets/`, `README.md`, `README.<lang>.md`, `CHANGELOG.md`, `py/` and `requirements.txt` into `dist/`.
229
+ 6. Backend:
230
+ - python / ros2 (rclpy): resolves `requirements.txt` into `dist/requirements.lock` (`uv pip compile`, Python 3.12; pre-releases allowed when a pinned line is one) and downloads binary wheels (cp312, `--only-binary=:all:`) into `dist/wheels/` per arch. A package with no matching wheel stops the build.
231
+ - cpp: CMake Release build in Docker → `dist/<arch>/<entry>` plus `dist/<arch>/lib/` (`librb_sdk.so`). The CMake target name must equal `runtime.entry`.
232
+ - ros2: colcon build in Docker → `dist/ros2_ws/install/` (rclpy, built once) or `dist/ros2_ws/install-<arch>/` (rclcpp, per arch).
233
+ 7. Writes `installedSize` (bytes of `dist/` without `wheels/`) into `manifest.json` and `dist/manifest.json`.
234
+ 8. Packs `dist/` into `<project dir>-<version>.zip` in the project root.
183
235
 
184
236
  | Option | Meaning |
185
237
  |---|---|
186
- | `--arch` | target arch; asked in a TTY, `both` otherwise |
238
+ | `--arch` | `amd64` (`x86_64`), `arm64` (`aarch64`) or `both`; asked in a TTY (default both), `both` otherwise |
239
+
240
+ Every runtime except `none` needs Docker. Building the other arch needs Docker emulation; on native Linux enable it once with `docker run --privileged --rm tonistiigi/binfmt --install <arch>`.
187
241
 
188
242
  ### 3.5 `update`
189
243
 
@@ -191,25 +245,41 @@ Steps:
191
245
  muscat-pk update # this project only (when it has a local install)
192
246
  muscat-pk update --global # this project + the global CLI
193
247
  muscat-pk update dev # follow the dev channel
248
+ muscat-pk update 1.2.3 # a specific version (also --tag <tag> / --version <v>)
194
249
  ```
195
250
 
251
+ | Option | Meaning |
252
+ |---|---|
253
+ | `[tag]` | npm dist-tag or version (`latest`, `dev`, `1.2.3`). Omitted: `latest`; on a dev build it asks to stay on dev (default) or switch to stable |
254
+ | `--global` | also update the global CLI when run from a project's local install |
255
+
196
256
  | Where you run it | What it updates |
197
257
  |---|---|
198
- | Inside a project with a local install | that project's `muscat-plugin-kit` + `plugin-ui` |
258
+ | Inside a project with a local install | that project's `muscat-plugin-kit` + `plugin-ui` (+ `preview-host` when listed in `package.json`) |
199
259
  | Same, with `--global` | the above + the global CLI |
200
- | Anywhere else | the global CLI (and local deps if inside a project) |
260
+ | Anywhere else | the global CLI (npm `-g` or the S3 binary) and local deps if inside a project |
261
+
262
+ - When the project's local install is older than the CLI you run, that CLI updates the project instead (project only).
263
+ - Packages stay in their current `dependencies` / `devDependencies` section; the package manager is taken from `packageManager` or the lock file.
264
+ - An old project layout is migrated after a `[y/N]` prompt: `src/` → `ui/` (+ `vite.config.ts`, `tsconfig.json`), `main.py` → `py/main.py`, `backend/` → `py/`.
265
+ - Missing `.mcp.json`, `AGENTS.md`, `CLAUDE.md` are added; existing ones are kept.
266
+ - On Windows the new binary replaces the old one on the next run.
201
267
 
202
- After updating it lists UI code that still uses removed APIs. Fix them (or run `check_project` through `muscat-pk mcp`).
268
+ After updating it lists code that needs changes for the new `plugin-ui` (``This project needs changes for the updated plugin-ui. Fix these before `muscat-pk dev` / `build`:``). Fix them (or run `check_project` through `muscat-pk mcp`).
203
269
 
204
270
  ### 3.6 `sdk`
205
271
 
206
272
  ```bash
207
- muscat-pk sdk list # published rainbow-rb-sdk versions
208
- muscat-pk sdk update [version] # latest when omitted
273
+ muscat-pk sdk list # 12 newest published versions
274
+ muscat-pk sdk update [version] # latest stable when omitted
209
275
  ```
210
276
 
211
- - python: rewrites every `rainbow-rb-*==` pin in `requirements.txt` together (they pin each other exactly).
212
- - C++ (`cpp`, `ros2-cpp`): rewrites `find_package(rb_sdk <ver> EXACT REQUIRED)` in `CMakeLists.txt`.
277
+ | Backend | `list` source | `update` rewrites |
278
+ |---|---|---|
279
+ | python, ros2 (rclpy) | PyPI `rainbow-rb-sdk` | every `rainbow-rb-*==` pin in `requirements.txt` together (they pin each other exactly); adds `rainbow-rb-sdk==<ver>` when missing. An existing `.venv` is refreshed; the dev container installs it on the next `dev` / `dev:back` |
280
+ | C++ (`cpp`, `ros2-cpp`) | apt `rainbow-rb-sdk-dev` (stable, numeric versions only) | `find_package(rb_sdk <ver> EXACT REQUIRED)` in `cpp/CMakeLists.txt` / `ros2_ws/src/*/CMakeLists.txt`; the dev image is rebuilt with it on the next `build` / `dev:back` |
281
+
282
+ A C++ version must be digits and dots (`"<v>" is not a CMake version (digits and dots only)`).
213
283
 
214
284
  ### 3.7 `storybook`, `mcp`, `completion`, `version`
215
285
 
@@ -218,9 +288,19 @@ muscat-pk storybook [--port 6007] [--rebuild]
218
288
  muscat-pk mcp [--install codex]
219
289
  muscat-pk completion [bash|zsh|fish|powershell]
220
290
  muscat-pk version
291
+ muscat-pk uninstall
221
292
  ```
222
293
 
223
- `completion` prints a Tab-completion script for your shell — once registered, `muscat-pk d<Tab>` offers `dev`, `dev:ui`, `dev:back`. It only prints the script; append it to your shell's rc file as below. `[shell]` is `bash`, `zsh`, `fish` or `powershell`, defaulting to `$SHELL`. Windows `cmd.exe` has no completion hook — use PowerShell, Git Bash or WSL. Optional convenience; nothing depends on it.
294
+ | Command / option | Meaning |
295
+ |---|---|
296
+ | `storybook --port` | port to serve on (default `6007`); a taken port fails with `port <n> is in use — pass a free one with --port <n>.` |
297
+ | `storybook --rebuild` | rebuild the Storybook first (only from a repo checkout; the published kit ships it prebuilt) |
298
+ | `mcp` | stdio MCP server (section 11). It relays to the real server (`mcp --serve`) and restarts it when the kit is updated, so a running agent session picks up the new tools |
299
+ | `mcp --install codex` | register the server in `~/.codex/config.toml` (no-op when already there) |
300
+ | `version` | the version of the copy that actually ran (also `-v`, `--version`) |
301
+ | `uninstall` | npm global: `npm uninstall -g`; binary: deletes the executable. A project-local install is removed with the project's package manager instead |
302
+
303
+ `completion` prints a Tab-completion script for your shell — once registered, `muscat-pk d<Tab>` offers `dev`, `dev:ui`, `dev:back`. It only prints the script; append it to your shell's rc file as below. `[shell]` is `bash`, `zsh`, `fish` or `powershell`, defaulting to `$SHELL` (else `powershell` on Windows, `bash` elsewhere). Windows `cmd.exe` has no completion hook — use PowerShell, Git Bash or WSL. Optional convenience; nothing depends on it.
224
304
 
225
305
  | OS / shell | Completion setup |
226
306
  |---|---|
@@ -243,12 +323,18 @@ my-plugin/
243
323
  ├─ ros2_ws/ # colcon workspace — ros2 / ros2-cpp type
244
324
  ├─ assets/icon.svg
245
325
  ├─ manifest.json
246
- ├─ requirements.txt # python deps (pinned on build)
326
+ ├─ requirements.txt # python deps (locked into dist/requirements.lock on build)
247
327
  ├─ package.json # UI deps (not shipped)
248
328
  ├─ vite.config.ts # must use the muscat() preset
249
- └─ README.md # copied into dist/
329
+ ├─ tsconfig.json
330
+ ├─ AGENTS.md, CLAUDE.md # rules for AI coding agents (CLAUDE.md → @AGENTS.md)
331
+ ├─ .mcp.json # registers `muscat-pk mcp` for Claude Code
332
+ ├─ .gitignore
333
+ └─ README.md # copied into dist/ (with README.<lang>.md, CHANGELOG.md)
250
334
  ```
251
335
 
336
+ The scaffolded `requirements.txt` pins `rainbow-rb-sdk`, `rainbow-rb-flat-buffers` and `rainbow-rb-zenoh` to the same version (change it with `muscat-pk sdk update`). Generated, not committed: `node_modules/`, `dist/`, `.muscat-dev/`, `.venv/`, `*.zip`, `cpp/build*`, `ros2_ws/{install,build*,log}`.
337
+
252
338
  `vite.config.ts`:
253
339
 
254
340
  ```ts
@@ -265,7 +351,7 @@ The ZIP contains only the plugin's own code and assets. `@rainbow-robotics/plugi
265
351
 
266
352
  ## 5. manifest.json
267
353
 
268
- Validated by `muscat-pk dev`, `dev:back` and `build`.
354
+ Fully validated by `muscat-pk build` (errors: `manifest.json errors: …`). `dev` / `dev:back` check `variables` and `ports` before start, and `dev:back` prints the rest as warnings.
269
355
 
270
356
  ### 5.1 Full example
271
357
 
@@ -303,11 +389,12 @@ Validated by `muscat-pk dev`, `dev:back` and `build`.
303
389
  | `company` | no | string |
304
390
  | `description` | yes | string or i18n object |
305
391
  | `icons` | yes | SVG path string, or `{ "16", "48", "128" }` for PNG/JPG (all three) |
306
- | `targetModels` / `targetCategorys` | at least one | `"all"` or a non-empty array. Categories: `manipulate`, `amr`, `rby1`, `vision` |
307
- | `contributes` | yes | see 5.3 |
392
+ | `targetModels` / `targetCategorys` | at least one | `"all"` or a non-empty array of strings. Categories: `manipulate`, `amr`, `rby1`, `vision` |
393
+ | `contributes` | no | see 5.3. Missing or `[]` = a backend-only plugin (no screens, no UI build) |
308
394
  | `variables` | no | see 5.5 |
309
395
  | `ports` | no | see 5.6 |
310
396
  | `runtime` | yes | see 5.7 |
397
+ | `installedSize` | — | written by `build` (bytes) |
311
398
 
312
399
  i18n object: `en` is required; allowed keys `ko`, `en`, `ja`, `zh`, `vi`, `th`, `es`.
313
400
 
@@ -322,7 +409,7 @@ i18n object: `en` is required; allowed keys `ko`, `en`, `ja`, `zh`, `vi`, `th`,
322
409
  | `opens` | toolbar only: `popover`, `panel`, `page` |
323
410
  | `commandInfo` | required for `program.panel` (5.4) |
324
411
 
325
- At least one `toolbar` entry is required.
412
+ A non-empty `contributes` needs at least one `toolbar` entry (that is where the host lists the plugin). Each `ui` must have a matching `ui/<entry>.tsx` (or `.ts` / `.jsx` / `.js`), checked on `build`.
326
413
 
327
414
  | Point | Where it shows |
328
415
  |---|---|
@@ -384,12 +471,14 @@ A backend that opens a TCP listener must declare every port:
384
471
 
385
472
  | Rule | Enforced by |
386
473
  |---|---|
387
- | Only `28600-28999`, `number[]` | manifest validation |
474
+ | Only `28600-28999`, `number[]`, non-empty when present, no duplicates | manifest validation |
388
475
  | Reserved ports rejected: `502`, `2222`, `4840`, `5170-5179`, `8000-8999`, `10000`, `30004`, `34964`, `44818`, `50000-50005`, `50051` | manifest validation |
389
- | Every listening port must be declared | `dev` / `dev:back` runtime monitor stops the backend on the first undeclared port |
476
+ | Every listening port must be declared | `dev` / `dev:back` runtime monitor stops the backend on the first undeclared port (`backend opened undeclared TCP port(s): …`) |
390
477
  | No two enabled plugins share a port | the robot, at install (HTTP 409) |
391
478
 
392
- The kit's own dev ports (gateway `10000`, the Zenoh dev peer port) are excluded. Under `muscat-pk dev` declared ports are published on `127.0.0.1`; a busy host port is skipped with a warning.
479
+ - The kit's own dev ports (gateway `10000`, MediaMTX `8554` / `8889` / `8189`, the Zenoh dev peer port) and OS-assigned loopback ports are excluded.
480
+ - The monitor can't tell plugin ports apart when the backend shares a network namespace with other services, so it is off on native Linux (`--network host`) and when the backend joins a local Muscat container (the usual `dev` without a robot). Declare them anyway — the robot checks them at install.
481
+ - Declared ports are published on `127.0.0.1` (from the local Muscat under `dev`); a busy host port is skipped with a warning. The backend gets them as `RB_PLUGIN_PORTS`.
393
482
 
394
483
  ### 5.7 `runtime`
395
484
 
@@ -409,8 +498,11 @@ Package `@rainbow-robotics/plugin-ui`:
409
498
 
410
499
  | Import | Contents |
411
500
  |---|---|
412
- | `@rainbow-robotics/plugin-ui/react` | Muscat components (`RB*`), `useRb`, `useRbFormData`, `useRbSave`, `useRbStorage`, `createRbStore` |
413
- | `@rainbow-robotics/plugin-ui/sdk` | the same hooks without the components, `RbSdk` / `RbStore` / `RbSetState` types |
501
+ | `@rainbow-robotics/plugin-ui/react` | Muscat components (`RB*`), `useRb`, `useRbFormData`, `useRbSave`, `useRbStorage`, `createRbStore`, `RbSdk` / `RbStore` / `RbSetState` types |
502
+ | `@rainbow-robotics/plugin-ui/sdk` | the same hooks without the components, plus `RbContext` and the `RbNotifyType` type |
503
+ | `@rainbow-robotics/plugin-ui/manifest` | manifest types (`PluginManifest`, `PluginContribution`, `PluginVariable`, `PluginRuntime`), `CONTRIBUTION_POINTS`, `VARIABLE_TYPES`, `validateManifest` |
504
+
505
+ Outside a Muscat host (a unit test, Storybook) `useRb()` returns a stub: calls log `[plugin-ui] rb.<name> called outside of a host context` and resolves to an empty value.
414
506
 
415
507
  ### 6.1 Entry component
416
508
 
@@ -561,7 +653,7 @@ await rb.storage.local.set("theme", "dark");
561
653
  #### `useRbStorage` — one stored value as React state
562
654
 
563
655
  ```tsx
564
- import { useRbStorage } from "@rainbow-robotics/plugin-ui/react";
656
+ import { RBTextInput, useRbStorage } from "@rainbow-robotics/plugin-ui/react";
565
657
 
566
658
  type Draft = { name: string };
567
659
 
@@ -581,7 +673,7 @@ function Editor() {
581
673
  Define once at module level, use from any screen of the plugin:
582
674
 
583
675
  ```tsx
584
- import { createRbStore } from "@rainbow-robotics/plugin-ui/react";
676
+ import { createRbStore, RBSlider } from "@rainbow-robotics/plugin-ui/react";
585
677
 
586
678
  type Settings = {
587
679
  speed: number;
@@ -635,7 +727,8 @@ persist(fn, { name: "settings", storage: createJSONStorage(() => rb.storage.loca
635
727
 
636
728
  | API | Meaning |
637
729
  |---|---|
638
- | `rb.data` | host-provided mount data (`program.monitor` scope, saved form values) |
730
+ | `rb.data` | host-provided mount data (`program.monitor` scope, saved form values); `undefined` for a fresh mount |
731
+ | `rb.onSave(getPayload)` | low-level form of `useRbSave`; returns the unregister function |
639
732
  | `rb.rosPeers.get()` | PCs allowed to reach this plugin's ROS 2 nodes |
640
733
  | `rb.rosPeers.set(ips)` | save the list and restart the backend; rejects an invalid IP |
641
734
 
@@ -646,11 +739,12 @@ persist(fn, { name: "settings", storage: createJSONStorage(() => rb.storage.loca
646
739
  Python backends (`runtime.type: "python"`, entry `py/main.py`) use `rainbow-rb-sdk`, pinned in `requirements.txt` and changed with `muscat-pk sdk update`. The robot's plugin container provides the SDK at runtime.
647
740
 
648
741
  ```python
649
- from rainbow.rb_sdk import RBPluginCall, PluginProgramContext, RBManipulateSDK, RBAmrSDK
650
- from rainbow.rb_sdk.plugin import PluginRouteError
651
- from rainbow.rb_sdk.rb_sdk_cpp import ConnectOptions
742
+ from rainbow.rb_sdk import RBPluginCall, PluginProgramContext, RBManipulateSDK, RBAmrSDK, RBRby1SDK
743
+ from rainbow.rb_sdk.rb_sdk_cpp import ConnectOptions, PluginRouteError, RBPluginGatewayError
652
744
  ```
653
745
 
746
+ `rainbow.rb_sdk` exports `RBPluginCall`, `PluginProgramContext`, `RBManipulateSDK`, `RBAmrSDK`, `RBRby1SDK`, `RBProgramSDK`, `RBBaseSDK` and `rby1` (the vendored rby1-sdk types). Everything else (`ConnectOptions`, `PluginRouter`, `PluginRouteError`, `RBPluginGatewayError`, …) is in `rainbow.rb_sdk.rb_sdk_cpp`.
747
+
654
748
  Which API depends on the contribution point:
655
749
 
656
750
  | Contribution point | Backend |
@@ -660,7 +754,7 @@ Which API depends on the contribution point:
660
754
 
661
755
  ### 7.1 `RBPluginCall` — UI ↔ backend
662
756
 
663
- Internal only (UI ↔ backend, plugin ↔ plugin); not reachable from outside Muscat. The plugin id comes from env `RB_PLUGIN_ID`.
757
+ Internal only (UI ↔ backend, plugin ↔ plugin); not reachable from outside Muscat. `RBPluginCall(plugin_id=None)`: the id is the argument, else env `RB_PLUGIN_ID`, else `PLUGIN_ID` (slashes trimmed). There is one instance per process and plugin id — calling `RBPluginCall()` again returns the same object.
664
758
 
665
759
  ```python
666
760
  plugin = RBPluginCall()
@@ -681,18 +775,18 @@ res = plugin.call("tool/ping", payload={"hi": 1}, timeout=3.0) # UI: rb.serve(
681
775
 
682
776
  | Member | Meaning |
683
777
  |---|---|
684
- | `serve(keyexpr, handler=None)` | request/response handler; decorator or direct call. Return value = reply |
778
+ | `serve(keyexpr, handler=None)` | request/response handler; decorator or direct call. Return value = reply. One handler per key — serving it again replaces the previous one |
685
779
  | `subscribe(topic, callback=None)` | receive publishes |
686
- | `publish(topic, payload=None)` | push to subscribers |
687
- | `call(keyexpr, *, payload, timeout=3.0)` | request the other side and wait |
688
- | `unserve(keyexpr=None)` | remove one / all serve handlers |
689
- | `unsubscribe(topic=None, callback=None)` | remove subscriptions |
780
+ | `publish(topic, payload=None)` | push to subscribers; dropped silently while disconnected |
781
+ | `call(keyexpr, *, payload, timeout=3.0)` | request the other side and wait; raises `RBPluginGatewayError` when disconnected or on timeout |
782
+ | `unserve(keyexpr=None)` / `undeclare_queryable(keyexpr=None)` | remove one / all serve handlers |
783
+ | `unsubscribe(topic=None, callback=None)` / `unsubscribe_topic(topic=None, callback=None)` | remove one callback, a topic's subscriptions, or all |
690
784
  | `plugin_id` | the plugin id in use (dev id under `muscat-pk dev`) |
691
785
  | `connected` | transport connected |
692
- | `data_dir` | persistent data directory, created on access. Survives updates and restarts; removed with the plugin. The install directory (cwd) is replaced on every update — keep state, caches and settings here |
786
+ | `data_dir` | persistent data directory, created on access: `RB_PLUGIN_DATA_DIR` (set by Muscat), else `/app/data/runtime/plugins/data/{plugin_id}`. Survives updates and restarts; removed with the plugin. The install directory (cwd) is replaced on every update — keep state, caches and settings here |
693
787
  | `router` | external endpoints (7.2) |
694
788
 
695
- Payloads must be JSON-serialisable. Handlers may be sync or async.
789
+ Payloads must be JSON-serialisable. Handlers may be sync or async. Topics are scoped like the UI's: `"state"` → `{plugin_id}/state`, a leading `/` is absolute.
696
790
 
697
791
  ### 7.2 `plugin.router` — external endpoints
698
792
 
@@ -818,6 +912,8 @@ asyncio.run(main())
818
912
  | `executor_state(robot_model=None)` | state string for a model, or `{model: state}` for all |
819
913
  | `is_paused(robot_model=None)` / `is_stopped(robot_model=None)` | state checks |
820
914
  | `on_executor_state_change(fn)` | decorator; `(robot_model, state)` once per change |
915
+ | `set_execute_handler(fn)` / `set_state_change_handler(fn)` | the same two registrations without a decorator |
916
+ | `begin_execution(robot_model=None)` / `end_execution()` | execute lifecycle hooks the SDK calls around `on_execute`; not needed in normal plugins |
821
917
  | `start()` / `stop()` / `pause()` | lifecycle |
822
918
  | `in_execute`, `exec_robot_model`, `plugin_id`, `started`, `control_topic` | properties |
823
919
 
@@ -928,16 +1024,21 @@ sudo apt update && sudo apt install rainbow-rb-sdk-dev
928
1024
 
929
1025
  ### 9.1 Local Muscat runtime
930
1026
 
931
- Without a robot IP, `dev` / `dev:ui` need a Muscat on `localhost:3000`:
1027
+ Without a robot IP (or against a robot that runs no Muscat), `dev` / `dev:ui` need a Muscat on `localhost:3000` (API) and `:10000` (plugin gateway):
932
1028
 
933
- - reuses any container already publishing `:10000` (`muscat`, `muscat-dev`, …);
934
- - otherwise starts `muscat-pk-muscat` — the S3 runtime image running only `common`, cached in `~/.muscat-pk/muscat/`. It keeps running between runs.
1029
+ - reuses any container already publishing `:10000` (`muscat`, `muscat-dev`, …). A container not started by the kit does not publish the plugin's ports;
1030
+ - uses a Muscat already listening on those ports without a container (robot PC, host networking);
1031
+ - otherwise starts `muscat-pk-muscat` — the S3 runtime image (`muscat-pk-runtime:<version>`, from `version.json`) running `common` (plus the services `--sim` needs) and MediaMTX (`8554`, `8889`, `8189`), cached in `~/.muscat-pk/muscat/`. It is recreated when the published plugin ports or services change, and a new Muscat version replaces the old image.
1032
+ - `dev` removes its own `muscat-pk-muscat` on exit; started by `dev:ui` alone it keeps running.
1033
+ - The backend joins the local Muscat's network namespace, so `execute` and the SDK reach it like on the robot.
935
1034
 
936
1035
  ```bash
937
1036
  docker rm -f muscat-pk-muscat # stop it
938
1037
  MUSCAT_PK_NO_LOCAL_MUSCAT=1 muscat-pk dev # don't start it
939
1038
  ```
940
1039
 
1040
+ Its API docs are at `http://localhost:3000/common/docs`.
1041
+
941
1042
  ### 9.2 Developing on the robot PC
942
1043
 
943
1044
  On the robot PC Muscat already serves the gateway on `10000`; the CLI uses it and starts no local Muscat.
@@ -952,13 +1053,15 @@ If the project has its own `muscat-plugin-kit` in `node_modules`, any `muscat-pk
952
1053
 
953
1054
  ### 9.5 Offline / air-gapped
954
1055
 
955
- Docker images are built only when missing (a rebuild contacts the registry even when cached).
1056
+ Docker images are built only when missing or when the kit's Dockerfile / build args changed (a rebuild contacts the registry even when cached).
956
1057
 
957
1058
  ```bash
958
1059
  docker save muscat-pk-python-dev muscat-pk-runtime | gzip > muscat-pk-images.tar.gz # online machine
959
1060
  gunzip -c muscat-pk-images.tar.gz | docker load # offline machine
960
1061
  ```
961
1062
 
1063
+ C++ / ROS 2 projects also need `muscat-pk-cpp-dev` / `muscat-pk-ros2-dev` (and the per-arch `-amd64` / `-arm64` images used by `build`). Offline, the local Muscat uses the cached `version.json` and the simulators an already pulled image.
1064
+
962
1065
  Force a rebuild with `MUSCAT_PK_REBUILD_IMAGE=1`.
963
1066
 
964
1067
  ### 9.6 IDE support (Python)
@@ -1006,13 +1109,20 @@ and `"jsxImportSource": "@emotion/react"` in `tsconfig.json`. Emotion `Global` i
1006
1109
 
1007
1110
  An MCP server (stdio) with grounded info on rb-components (props, Storybook examples), design tokens, and the Python `rainbow-rb-sdk` (introspected signatures/docstrings), plus `check_project` for upgrading old projects.
1008
1111
 
1009
- Claude Code — `.mcp.json` at the plugin root:
1112
+ | Tool | Returns |
1113
+ |---|---|
1114
+ | `list_components`, `get_component_props`, `get_component_example` | plugin-ui components, their prop types, real Storybook usage |
1115
+ | `list_design_tokens`, `search_design_tokens` | design-token Tailwind classes (`bg-bg-*`, `text-fg-*`, `typo-*`) |
1116
+ | `list_rb_sdk`, `search_rb_sdk`, `get_rb_sdk_api` | Python `rainbow-rb-sdk` and C++ SDK classes / signatures / docstrings |
1117
+ | `check_project` | what an older project must change after `muscat-pk update` |
1118
+
1119
+ Claude Code — `.mcp.json` at the plugin root (written by `new`, or by `update` when missing):
1010
1120
 
1011
1121
  ```json
1012
1122
  { "mcpServers": { "muscat": { "command": "muscat-pk", "args": ["mcp"] } } }
1013
1123
  ```
1014
1124
 
1015
- Codex — `muscat-pk mcp --install codex`, or `~/.codex/config.toml`:
1125
+ Codex — `muscat-pk mcp --install codex` (on Windows it writes `muscat-pk.cmd`), or `~/.codex/config.toml`:
1016
1126
 
1017
1127
  ```toml
1018
1128
  [mcp_servers.muscat]
@@ -1033,12 +1143,20 @@ The agent starts the server itself.
1033
1143
  | `MUSCAT_PK_NO_LOCAL_MUSCAT=1` | don't start the local Muscat runtime |
1034
1144
  | `MUSCAT_PK_REBUILD_IMAGE=1` | rebuild Docker images |
1035
1145
  | `MUSCAT_PK_RB_SDK_VERSION` | C++ SDK version baked into the C++ images (with `MUSCAT_PK_REBUILD_IMAGE=1`) |
1146
+ | `MUSCAT_PK_NO_BUILD_CACHE=1` | build Docker images with `--no-cache` (CI) |
1147
+ | `MUSCAT_PK_HOST_NETWORK=0` | on native Linux, use the Docker Desktop (bridge) path instead of `--network host` (CI) |
1148
+ | `MUSCAT_PK_RBY1_CPUS` | CPU cap of the rby1 simulator (default `1.5`) |
1149
+ | `MUSCAT_PK_MUSCAT_S3` | where the local Muscat runtime is downloaded from (default `https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat/main`) |
1150
+ | `MUSCAT_HOST_AGENT_DISABLE=1` | don't start the host Zenoh byte-proxy (macOS / Windows / WSL against a remote robot) |
1151
+ | `MUSCAT_PROXY_PORT` | port of that proxy (default `47100`) |
1152
+ | `MUSCAT_ZENOH_ADVERTISE_HOST` / `MUSCAT_ZENOH_PORT` | LAN IP / port the remote robot dials back to (default: the route to the robot / `53917` or the next free port) |
1036
1153
  | `MUSCAT_PK_NO_UPDATE_CHECK=1` | skip the update check on `dev` / `dev:ui` |
1037
1154
  | `RB_REGISTRY` | npm registry for the update check (default `https://registry.npmjs.org`) |
1038
1155
  | `MUSCAT_PK_TAG` | install script channel / version |
1039
- | `NO_COLOR` / `FORCE_COLOR` | output colors |
1156
+ | `CI` | set: `dev:ui` doesn't open the browser |
1157
+ | `NO_COLOR=1` / `FORCE_COLOR` | output colors |
1040
1158
 
1041
- Inside the backend container: `RB_PLUGIN_ID` (the plugin / dev id).
1159
+ Inside the backend container: `RB_PLUGIN_ID` (the plugin / dev id), `RB_PLUGIN_PORTS` (declared `ports`, comma-separated), `MUSCAT_ROBOT_HOST` / `MUSCAT_ROBOT_TOKEN` (remote dev). On the robot Muscat also sets `RB_PLUGIN_DATA_DIR` (see `data_dir`, 7.1).
1042
1160
 
1043
1161
  ---
1044
1162
 
@@ -1047,10 +1165,18 @@ Inside the backend container: `RB_PLUGIN_ID` (the plugin / dev id).
1047
1165
  | Symptom | Fix |
1048
1166
  |---|---|
1049
1167
  | `manifest.json errors: …` | fix each listed field (section 5) |
1168
+ | `manifest.json variable errors: …` / `manifest.json port errors: …` on `dev` | fix `variables` (5.5) / `ports` (5.6) |
1169
+ | `A Muscat plugin dev environment is already running: …` | answer `y` to stop it, or stop the other `dev` first |
1170
+ | `Docker is installed but the daemon is not running` | start Docker Desktop (or `sudo systemctl start docker`) |
1171
+ | `contributes declares ui "<entry>" but ui/<entry>.tsx … does not exist.` | add the file or fix `contributes[].ui` |
1172
+ | `wheel download failed for <arch>` | a dependency has no cp312 binary wheel for that arch — pick another version or package |
1173
+ | `can't build <arch> on this <host>: Docker has no <arch> emulation.` | `docker run --privileged --rm tonistiigi/binfmt --install <arch>`, or `--arch <host arch>` |
1174
+ | `Permission denied when deleting dist/` | an old build left root-owned files: `sudo rm -rf dist` once |
1050
1175
  | backend stops with an undeclared port | add the port (28600-28999) to `ports` |
1051
1176
  | UI preview says "no default export" | entry must `export default App`; `vite.config.ts` must use `muscat()`; run `check_project` via `muscat-pk mcp` |
1052
1177
  | `vite not found — UI will not load` | run `npm install` in the project |
1053
1178
  | `AttributeError: router` | robot SDK older than 0.0.9.dev5 |
1179
+ | `RBPluginGatewayError` from `plugin.call` | the gateway is disconnected, or no reply within `timeout` (is a UI `rb.serve` registered?) |
1054
1180
  | step never finishes | `on_execute` must `await ctx.done()` |
1055
1181
  | Stop doesn't stop the plugin | call `await ctx.check_stop()` in every loop |
1056
1182
  | `RuntimeError` on `set_variable` | new variable names need `RBP_` |
package/cli/README.md CHANGED
@@ -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
 
@@ -88,7 +88,7 @@ function formatHelp() {
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
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"),
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":
@@ -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,4 +1,5 @@
1
- // muscat-pk dev — run the local Muscat runtime (gateway, MediaMTX, common), dev:ui, and dev:back together.
1
+ // muscat-pk dev — run the local Muscat runtime (gateway, MediaMTX, common), robot simulators (--sim),
2
+ // dev:ui, and dev:back together.
2
3
  import { spawn, spawnSync } from "node:child_process";
3
4
  import { existsSync } from "node:fs";
4
5
  import { connect as netConnect } from "node:net";
@@ -7,6 +8,7 @@ import { fileURLToPath } from "node:url";
7
8
  import { runDevUi } from "./devUi.mjs";
8
9
  import { claimDevSession, stopDevBackContainers } from "./devSession.mjs";
9
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))) {
@@ -161,7 +175,10 @@ export async function runDev(args = []) {
161
175
  if (backend.exitCode === null) backend.kill();
162
176
  stopDevBackContainers();
163
177
  // Only our own runtime — a muscat-dev the user started stays up.
164
- if (muscat === MUSCAT_CONTAINER) spawnSync("docker", ["rm", "-f", MUSCAT_CONTAINER], { stdio: "ignore" });
178
+ if (muscat === MUSCAT_CONTAINER) {
179
+ stopSims();
180
+ spawnSync("docker", ["rm", "-f", MUSCAT_CONTAINER], { stdio: "ignore" });
181
+ }
165
182
  };
166
183
  process.once("SIGINT", cleanup);
167
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();