@rainbow-robotics/muscat-plugin-kit 1.1.5 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -0
- package/cli/README.md +40 -4
- package/cli/bin/muscat-pk.mjs +33 -6
- package/cli/docker/Dockerfile.cpp +11 -6
- package/cli/docker/Dockerfile.dev +12 -0
- package/cli/src/build.mjs +6 -6
- package/cli/src/cpp.mjs +3 -1
- package/cli/src/dev.mjs +2 -1
- package/cli/src/devBack.mjs +8 -7
- package/cli/src/manifest.mjs +56 -0
- package/cli/src/mcp.mjs +382 -43
- package/cli/src/new.mjs +3 -0
- package/cli/src/ports.mjs +3 -3
- package/cli/src/ros2.mjs +3 -1
- package/cli/src/templates-embedded.mjs +3 -0
- package/cli/src/util.mjs +13 -0
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +13 -1
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-YYWRJKA2.js → Color-YHDXOIA2-DixbmWSF.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-CKJ2w--X.js → Docs-Blx_41TT.js} +1 -1
- package/cli/storybook-dist/assets/Docs-C0E3mHmn.js +7 -0
- package/cli/storybook-dist/assets/Docs-CMEiBjLz.js +20 -0
- package/cli/storybook-dist/assets/{Docs-UmN7NFo1.js → Docs-D6oFYDre.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DIJ7cbFD.js → Docs-DcUa4nGr.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-DT9RD87x.js → Docs-Dv-5qjF4.js} +1 -1
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-CyAy1mnJ.js → DocsRenderer-CFRXHY34-GwWANrWN.js} +1 -1
- package/cli/storybook-dist/assets/NxAvatar.stories-CyZ4AE2b.js +13 -0
- package/cli/storybook-dist/assets/NxButton.stories-Bx8UtGNd.js +20 -0
- package/cli/storybook-dist/assets/{NxIcons.stories-Dg42riT5.js → NxIcons.stories-DeqrVzVs.js} +1 -1
- package/cli/storybook-dist/assets/{NxNumberInput.stories-zlQJuGFP.js → NxNumberInput.stories-skizAdd5.js} +3 -3
- package/cli/storybook-dist/assets/NxPagination.stories-gtIxt2u0.js +11 -0
- package/cli/storybook-dist/assets/NxPointerHoldButton.stories-DSHMk3N5.js +14 -0
- package/cli/storybook-dist/assets/{NxRangeInput.stories-Cu5ZWU0w.js → NxRangeInput.stories-C1YtrFZ1.js} +3 -3
- package/cli/storybook-dist/assets/NxSelectBox.stories-DCySAtBp.js +17 -0
- package/cli/storybook-dist/assets/NxSlideRange.stories-gSaMOWo7.js +14 -0
- package/cli/storybook-dist/assets/NxSwitch.stories-DF0DBiyG.js +11 -0
- package/cli/storybook-dist/assets/NxTextArea.stories-Dill0M6z.js +11 -0
- package/cli/storybook-dist/assets/NxTextInput.stories-Bax14r48.js +11 -0
- package/cli/storybook-dist/assets/RBAccordion.stories-CCwz_pIV.js +17 -0
- package/cli/storybook-dist/assets/RBAccordionGroup.stories-BjDxxdPV.js +33 -0
- package/cli/storybook-dist/assets/RBAlert.stories-5z6GasgP.js +17 -0
- package/cli/storybook-dist/assets/RBBadge.stories-BwcM7i7w.js +1 -0
- package/cli/storybook-dist/assets/RBBattery.stories-DOeQH3rK.js +10 -0
- package/cli/storybook-dist/assets/RBCard.stories-DcmiOXAV.js +45 -0
- package/cli/storybook-dist/assets/RBCheckBox.stories-BYru87fX.js +1 -0
- package/cli/storybook-dist/assets/RBCheckIconButton.stories-CXUjtR1a.js +1 -0
- package/cli/storybook-dist/assets/RBCheckLabelButton.stories-BSOQV7Du.js +24 -0
- package/cli/storybook-dist/assets/RBChipTab.stories-ByBmeukU.js +21 -0
- package/cli/storybook-dist/assets/RBChipsBoxTab.stories-GtJ01RTr.js +1 -0
- package/cli/storybook-dist/assets/RBColorIcons.stories-C9saET1u.js +3 -0
- package/cli/storybook-dist/assets/{RBCommand.stories-C3bwPq2T.js → RBCommand.stories-CcvnWlWZ.js} +3 -3
- package/cli/storybook-dist/assets/RBConfirm.stories-hdBVSdBS.js +20 -0
- package/cli/storybook-dist/assets/RBDateInput.stories-BE9JQl-E.js +15 -0
- package/cli/storybook-dist/assets/RBDialog.stories-BaWuDgl_.js +38 -0
- package/cli/storybook-dist/assets/RBDivider.stories-D3INKm_Z.js +1 -0
- package/cli/storybook-dist/assets/RBDrawer.stories-BZ8V0J6-.js +14 -0
- package/cli/storybook-dist/assets/{RBDropdownMenu.stories-9fBcV2Ct.js → RBDropdownMenu.stories-DxLi8HRw.js} +2 -2
- package/cli/storybook-dist/assets/RBEmptyView.stories-DQcvDt2S.js +12 -0
- package/cli/storybook-dist/assets/RBIconButton.stories-DJfuTpQ0.js +1 -0
- package/cli/storybook-dist/assets/RBIcons.stories-BSsyb0BK.js +3 -0
- package/cli/storybook-dist/assets/RBInlineLabelButton.stories-Di3th--x.js +1 -0
- package/cli/storybook-dist/assets/RBJoyStick.stories-DfGazm4n.js +11 -0
- package/cli/storybook-dist/assets/RBLabelButton.stories-CLbxzW56.js +1 -0
- package/cli/storybook-dist/assets/RBLight.stories-DeiuDm_P.js +1 -0
- package/cli/storybook-dist/assets/RBMenubar.stories-BZQTV9e5.js +3 -0
- package/cli/storybook-dist/assets/RBMultiRangeInput.stories-DDNmdi_n.js +14 -0
- package/cli/storybook-dist/assets/RBPagination.stories-CLBCEyCU.js +17 -0
- package/cli/storybook-dist/assets/RBPopover.stories-DRI5Kfa-.js +11 -0
- package/cli/storybook-dist/assets/RBPrompt.stories-DIAmypoJ.js +20 -0
- package/cli/storybook-dist/assets/RBRadioBox.stories-BS_N4aLf.js +1 -0
- package/cli/storybook-dist/assets/RBRadioButton.stories-BTm8cYvm.js +11 -0
- package/cli/storybook-dist/assets/RBRangeDateInput.stories-D5hBsSyB.js +15 -0
- package/cli/storybook-dist/assets/RBResizable.stories-CxPEkbXQ.js +20 -0
- package/cli/storybook-dist/assets/RBSelectBox.stories-FUQ7AcUi.js +6 -0
- package/cli/storybook-dist/assets/RBSheet.stories-mhVHH_71.js +39 -0
- package/cli/storybook-dist/assets/RBSideNumberInput.stories-5FMkLUim.js +5 -0
- package/cli/storybook-dist/assets/RBSlider.stories-Bnhcp9pz.js +5 -0
- package/cli/storybook-dist/assets/RBSpinner.stories-Bf4416mP.js +14 -0
- package/cli/storybook-dist/assets/{RBSwitch.stories-CBYXvhkd.js → RBSwitch.stories-BNhFjRaV.js} +2 -2
- package/cli/storybook-dist/assets/RBTab.stories-Bsr7jlfb.js +1 -0
- package/cli/storybook-dist/assets/RBTable.stories-BkA6pKzs.js +62 -0
- package/cli/storybook-dist/assets/RBTableRow.stories-D07Z7D5-.js +11 -0
- package/cli/storybook-dist/assets/RBTabs.stories-DHnZiV21.js +21 -0
- package/cli/storybook-dist/assets/RBTextArea.stories-CtLMLuV_.js +5 -0
- package/cli/storybook-dist/assets/RBTextField.stories-Chz0i1-u.js +45 -0
- package/cli/storybook-dist/assets/RBTextInput.stories-Dk5cZfUF.js +5 -0
- package/cli/storybook-dist/assets/RBToast.stories-tZxbjmr8.js +11 -0
- package/cli/storybook-dist/assets/RBToggle.stories-VTe7a4xZ.js +12 -0
- package/cli/storybook-dist/assets/RBToggleGroup.stories-WiFnGGYx.js +17 -0
- package/cli/storybook-dist/assets/RBTooltip.stories-B_L4W7Mn.js +9 -0
- package/cli/storybook-dist/assets/{MapEditor-C6_H9h5t.css → SideNumberInput-SAkhydsn.css} +2 -2
- package/cli/storybook-dist/assets/SideNumberInput-os5wKKf7.js +1109 -0
- package/cli/storybook-dist/assets/browserUtils-CQM9Za7e.js +1 -0
- package/cli/storybook-dist/assets/{entry-preview-N9dpIJQA.js → entry-preview-NpzneD8a.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-BwCxSq5d.js → iframe-DsvRKHwc.js} +2 -2
- package/cli/storybook-dist/assets/{index-CgJh_5EZ.js → index-BzvSJzrT.js} +3 -3
- package/cli/storybook-dist/assets/preview-BXXleNPp.css +1 -0
- package/cli/storybook-dist/assets/preview-BXxjLQ9M.js +3 -0
- package/cli/storybook-dist/assets/{preview-DsYc6ovL.js → preview-yXaYuZ78.js} +2 -2
- package/cli/storybook-dist/assets/react-resizable-panels.browser-CYcsrUgg.js +120 -0
- package/cli/storybook-dist/iframe.html +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/README.md +352 -24
- package/cli/templates/common/manifest.json +1 -0
- package/cli/templates/common/py/main.py +24 -1
- package/cli/templates/common/requirements.txt +3 -3
- package/cli/templates/cpp/cpp/CMakeLists.txt +7 -7
- package/cli/templates/cpp/cpp/main.cpp +13 -6
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +4 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +2 -0
- package/package.json +3 -3
- package/cli/storybook-dist/assets/Docs-DLFwGiks.js +0 -7
- package/cli/storybook-dist/assets/Docs-dOc9cH1E.js +0 -20
- package/cli/storybook-dist/assets/MapEditor-Jj1J2sUz.js +0 -6418
- package/cli/storybook-dist/assets/NxAvatar.stories-B-iQoylp.js +0 -13
- package/cli/storybook-dist/assets/NxButton.stories-CawQZN07.js +0 -20
- package/cli/storybook-dist/assets/NxPagination.stories-VYXLplNl.js +0 -11
- package/cli/storybook-dist/assets/NxPointerHoldButton.stories-BpqIkt0X.js +0 -14
- package/cli/storybook-dist/assets/NxSelectBox.stories-JECi_1PM.js +0 -17
- package/cli/storybook-dist/assets/NxSlideRange.stories-DJe8F2Mv.js +0 -14
- package/cli/storybook-dist/assets/NxSwitch.stories-DX6LxxQL.js +0 -11
- package/cli/storybook-dist/assets/NxTextArea.stories-Cy42nYOI.js +0 -11
- package/cli/storybook-dist/assets/NxTextInput.stories-Bb_q6FuW.js +0 -11
- package/cli/storybook-dist/assets/RBAccordion.stories-ChgPXO0_.js +0 -17
- package/cli/storybook-dist/assets/RBAccordionGroup.stories-nmbqYYSE.js +0 -33
- package/cli/storybook-dist/assets/RBAlert.stories-ChOPuFxI.js +0 -17
- package/cli/storybook-dist/assets/RBBadge.stories-Bas7g4jb.js +0 -1
- package/cli/storybook-dist/assets/RBBattery.stories-C0MywWvB.js +0 -10
- package/cli/storybook-dist/assets/RBCard.stories-Ba7y0u6Q.js +0 -45
- package/cli/storybook-dist/assets/RBCheckBox.stories-CsHFsXCd.js +0 -1
- package/cli/storybook-dist/assets/RBCheckIconButton.stories-GERClL9R.js +0 -1
- package/cli/storybook-dist/assets/RBCheckLabelButton.stories-C-BrmKaW.js +0 -24
- package/cli/storybook-dist/assets/RBChipTab.stories-C8JCa6F9.js +0 -21
- package/cli/storybook-dist/assets/RBChipsBoxTab.stories-DmUaHBJ3.js +0 -1
- package/cli/storybook-dist/assets/RBColorIcons.stories-Bcah6eZe.js +0 -3
- package/cli/storybook-dist/assets/RBConfirm.stories-CT0GuE-J.js +0 -20
- package/cli/storybook-dist/assets/RBDateInput.stories-CMd2xRCF.js +0 -15
- package/cli/storybook-dist/assets/RBDialog.stories-Ui5Ni-P_.js +0 -38
- package/cli/storybook-dist/assets/RBDivider.stories-r8TDiO36.js +0 -1
- package/cli/storybook-dist/assets/RBDrawer.stories-CymmrHjI.js +0 -14
- package/cli/storybook-dist/assets/RBEmptyView.stories-By9vncNC.js +0 -12
- package/cli/storybook-dist/assets/RBIconButton.stories-DC29KMxX.js +0 -1
- package/cli/storybook-dist/assets/RBIcons.stories-Dpd_V9b9.js +0 -3
- package/cli/storybook-dist/assets/RBInlineLabelButton.stories-CfUQdAdt.js +0 -1
- package/cli/storybook-dist/assets/RBJoyStick.stories-Dl-uXk9x.js +0 -11
- package/cli/storybook-dist/assets/RBLabelButton.stories-Dx_Vn6EM.js +0 -1
- package/cli/storybook-dist/assets/RBLight.stories-Cm5UYJ4n.js +0 -1
- package/cli/storybook-dist/assets/RBMenubar.stories-CN5z7F6D.js +0 -3
- package/cli/storybook-dist/assets/RBMultiRangeInput.stories-DrAMI4mQ.js +0 -14
- package/cli/storybook-dist/assets/RBPagination.stories-BVLXuTLw.js +0 -17
- package/cli/storybook-dist/assets/RBPopover.stories-VCws5q80.js +0 -11
- package/cli/storybook-dist/assets/RBPrompt.stories-Cw5-wssh.js +0 -20
- package/cli/storybook-dist/assets/RBRadioBox.stories-ClqGtGA_.js +0 -1
- package/cli/storybook-dist/assets/RBRadioButton.stories-SxcJFioj.js +0 -11
- package/cli/storybook-dist/assets/RBRangeDateInput.stories-L_wCftDv.js +0 -15
- package/cli/storybook-dist/assets/RBResizable.stories-2VytDv6m.js +0 -20
- package/cli/storybook-dist/assets/RBSelectBox.stories-BoOKTSL_.js +0 -6
- package/cli/storybook-dist/assets/RBSheet.stories-CUClMqKK.js +0 -39
- package/cli/storybook-dist/assets/RBSideNumberInput.stories-BTYDDAmk.js +0 -5
- package/cli/storybook-dist/assets/RBSlider.stories-Bu6vD9ym.js +0 -5
- package/cli/storybook-dist/assets/RBSpinner.stories-XmVcaTVS.js +0 -14
- package/cli/storybook-dist/assets/RBTab.stories-DFFYlkCv.js +0 -1
- package/cli/storybook-dist/assets/RBTable.stories-CR2j2EQQ.js +0 -62
- package/cli/storybook-dist/assets/RBTableRow.stories-CpPIP1au.js +0 -11
- package/cli/storybook-dist/assets/RBTabs.stories-Bew5XVDZ.js +0 -21
- package/cli/storybook-dist/assets/RBTextArea.stories-JiRn-tk_.js +0 -5
- package/cli/storybook-dist/assets/RBTextField.stories-8LWyUBm9.js +0 -45
- package/cli/storybook-dist/assets/RBTextInput.stories-PLJ70xm5.js +0 -5
- package/cli/storybook-dist/assets/RBToast.stories-BQf_9x0k.js +0 -11
- package/cli/storybook-dist/assets/RBToggle.stories-C5dZnmX_.js +0 -12
- package/cli/storybook-dist/assets/RBToggleGroup.stories-D7fwXCxA.js +0 -17
- package/cli/storybook-dist/assets/RBTooltip.stories-D-sWRxbA.js +0 -9
- package/cli/storybook-dist/assets/RainbowUiProvider-CsMXuxM1.js +0 -31
- package/cli/storybook-dist/assets/browserUtils-CxJ5gea3.js +0 -1
- package/cli/storybook-dist/assets/keymapTileIndex.worker-BsNYNbqs.js +0 -1
- package/cli/storybook-dist/assets/preview-CzDPTg-k.js +0 -3
- package/cli/storybook-dist/assets/preview-DZWUyVBI.css +0 -1
- package/cli/storybook-dist/assets/react-resizable-panels.browser-CHDWt8fa.js +0 -72
- /package/cli/storybook-dist/assets/{RainbowUiProvider-BaelaOGy.css → react-resizable-panels-BaelaOGy.css} +0 -0
package/README.md
CHANGED
|
@@ -119,6 +119,54 @@ muscat-pk build # build dist/ and produce a distributable ZIP (sk
|
|
|
119
119
|
| `muscat-pk version` | Print the installed CLI version (`-v` / `--version` also work) |
|
|
120
120
|
| `muscat-pk completion [shell]` | Print a shell completion script (`bash` / `zsh` / `fish` / `powershell`; defaults to `$SHELL`) |
|
|
121
121
|
|
|
122
|
+
### Update policy
|
|
123
|
+
|
|
124
|
+
`muscat-pk` can be installed globally (npm `-g` or the S3 binary) and/or per
|
|
125
|
+
plugin project (`node_modules`). Two rules keep those from drifting apart:
|
|
126
|
+
|
|
127
|
+
**The project's local install wins.** When you run `muscat-pk` inside a plugin
|
|
128
|
+
project that has its own install, the CLI hands off to that copy. So the CLI,
|
|
129
|
+
the `vite` preset your `vite.config.ts` imports, and the `preview-host` that
|
|
130
|
+
`dev:ui` launches are always the single version the project pinned — a global
|
|
131
|
+
CLI that is newer or older than the project can't half-apply itself.
|
|
132
|
+
|
|
133
|
+
**`update` only touches what you're in.**
|
|
134
|
+
|
|
135
|
+
| Where you run it | What it updates |
|
|
136
|
+
|---|---|
|
|
137
|
+
| Inside a project with a local install | that project's `muscat-plugin-kit` + `plugin-ui` only |
|
|
138
|
+
| Same, with `--global` | the above, plus the global CLI |
|
|
139
|
+
| Anywhere else | the global CLI (and local deps if you're in a project) |
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
muscat-pk update # this project only
|
|
143
|
+
muscat-pk update --global # this project + the global CLI
|
|
144
|
+
muscat-pk update dev # follow the dev channel instead of latest
|
|
145
|
+
muscat-pk version # which version is actually running here
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`dev:ui` also prints a one-line notice when a newer version is published. It is
|
|
149
|
+
skipped by `MUSCAT_PK_NO_UPDATE_CHECK=1` and fails silently offline.
|
|
150
|
+
|
|
151
|
+
### Offline / air-gapped use
|
|
152
|
+
|
|
153
|
+
Docker images are only built when they are missing. `docker build` contacts the
|
|
154
|
+
registry to resolve the base image tag on every run — even when every layer is
|
|
155
|
+
cached — so rebuilding an image you already have would break `dev` with no
|
|
156
|
+
network. Once an image exists locally, `muscat-pk` skips the build entirely.
|
|
157
|
+
|
|
158
|
+
Move images to a machine with no internet:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# on a machine with network
|
|
162
|
+
docker save muscat-pk-python-dev muscat-pk-bridge | gzip > muscat-pk-images.tar.gz
|
|
163
|
+
|
|
164
|
+
# on the offline machine
|
|
165
|
+
gunzip -c muscat-pk-images.tar.gz | docker load
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Force a rebuild with `MUSCAT_PK_REBUILD_IMAGE=1`.
|
|
169
|
+
|
|
122
170
|
### Shell completion
|
|
123
171
|
|
|
124
172
|
| OS / shell | Command |
|
|
@@ -130,6 +178,57 @@ muscat-pk build # build dist/ and produce a distributable ZIP (sk
|
|
|
130
178
|
|
|
131
179
|
Windows `cmd.exe` has no completion mechanism to hook into — use PowerShell, Git Bash, or WSL.
|
|
132
180
|
|
|
181
|
+
## Backend port policy
|
|
182
|
+
|
|
183
|
+
A plugin backend that opens a TCP listener must declare every such port in
|
|
184
|
+
`manifest.json`:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{ "ports": [28600, 28601] }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Rule | Enforced by |
|
|
191
|
+
|---|---|
|
|
192
|
+
| Ports must be within `28600-28999` | `muscat-pk build` / `dev` (manifest validation) |
|
|
193
|
+
| Reserved robot/industrial ports are rejected | same — `502`, `2222`, `4840`, `5170-5179`, `8000-8999`, `10000`, `30004`, `34964`, `44818`, `50000-50005`, `50051` |
|
|
194
|
+
| **Every listening port must be declared** | `dev` / `dev:back` runtime monitor — stops the backend on the first undeclared port |
|
|
195
|
+
| No two enabled plugins may claim the same port | the robot, at install time (HTTP 409) |
|
|
196
|
+
|
|
197
|
+
The runtime monitor is strict on purpose: an undeclared port outside the plugin
|
|
198
|
+
range is invisible to both the monitor and the robot's conflict check, so it
|
|
199
|
+
would only surface as a collision after deployment.
|
|
200
|
+
|
|
201
|
+
`muscat-pk`'s own dev plumbing (the Socket.IO bridge on `10000`, the Zenoh dev
|
|
202
|
+
peer port) is excluded from the check — it isn't the plugin's traffic.
|
|
203
|
+
|
|
204
|
+
Declared ports are published to `127.0.0.1` on macOS and Windows so you can
|
|
205
|
+
reach the backend from the host; native Linux uses host networking, where they
|
|
206
|
+
already are.
|
|
207
|
+
|
|
208
|
+
## Variables (pyfm)
|
|
209
|
+
|
|
210
|
+
Variables the plugin wants to expose to pyfm programs must be pre-declared in
|
|
211
|
+
`manifest.json` — the Muscat UI lists only declared variables in its variable
|
|
212
|
+
picker:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"variables": [
|
|
217
|
+
{ "name": "RBP_grip_force", "type": "number", "init": 0.5, "description": { "ko": "그리퍼 힘 (0~1)", "en": "Gripper force (0–1)" } },
|
|
218
|
+
{ "name": "RBP_target_pose", "type": "array", "init": [0, 0, 0] }
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
| Field | Required | Rule |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| `name` | yes | must start with `RBP_`, then letters/digits/underscore; unique per plugin |
|
|
226
|
+
| `type` | yes | `"number"` \| `"string"` \| `"array"` |
|
|
227
|
+
| `init` | yes | initial value, must match `type` |
|
|
228
|
+
| `description` | no | string or i18n object (`en` required in object form). Without it the UI shows the variable with no explanation — write one. |
|
|
229
|
+
|
|
230
|
+
Validated by `muscat-pk build` / `dev` alongside the rest of the manifest.
|
|
231
|
+
|
|
133
232
|
## IDE support (Python)
|
|
134
233
|
|
|
135
234
|
`muscat-pk new` creates a `.venv` at the project root with `requirements.txt` installed. The real backend still runs in a Docker container — this `.venv` exists purely for editor linting (pylint/pyright).
|
package/cli/README.md
CHANGED
|
@@ -37,7 +37,17 @@ If a plugin backend opens a TCP server, declare it in `manifest.json`:
|
|
|
37
37
|
`ports` is optional and accepts only `number[]`; ranges or objects are not
|
|
38
38
|
accepted. External plugin backend ports are reserved for `28600-28999`.
|
|
39
39
|
`muscat-pk dev` and `muscat-pk dev:back` validate the manifest before start and
|
|
40
|
-
|
|
40
|
+
then watch the container: **any listening TCP port that is not declared stops
|
|
41
|
+
the backend**, whatever its number. Since a port outside `28600-28999` cannot be
|
|
42
|
+
declared, it cannot be opened either — otherwise it would slip past both this
|
|
43
|
+
check and the robot's install-time conflict check.
|
|
44
|
+
|
|
45
|
+
The kit's own dev ports (Socket.IO bridge `10000`, the Zenoh dev peer port) are
|
|
46
|
+
excluded from that check.
|
|
47
|
+
|
|
48
|
+
Declared ports are published to `127.0.0.1` so the host can reach the backend on
|
|
49
|
+
macOS/Windows; native Linux dev containers use host networking and already have
|
|
50
|
+
them. A declared port that is already busy on the host is skipped with a warning.
|
|
41
51
|
|
|
42
52
|
Known robot/industrial/internal ports such as `502`, `4840`, `5170-5179`,
|
|
43
53
|
`8000-8999`, `10000`, `2222`, `30004`, `34964`, `44818`, `50000-50005`,
|
|
@@ -50,10 +60,10 @@ and `50051` are reserved and must not be used by external plugins.
|
|
|
50
60
|
| runtime | source | notes |
|
|
51
61
|
|---|---|---|
|
|
52
62
|
| `{ "type": "none" }` | — | UI-only plugin; no backend container |
|
|
53
|
-
| `{ "type": "python", "entry": "main.py" }` | `py/` | Uses `rainbow.rb_sdk.RBPluginCall` for UI/backend messaging |
|
|
54
|
-
| `{ "type": "cpp", "entry": "<executable name>" }` | `cpp/` | Built with CMake in Docker
|
|
63
|
+
| `{ "type": "python", "entry": "main.py" }` | `py/` | Uses `rainbow.rb_sdk.RBPluginCall` for UI/backend messaging; `plugin.router` exposes external endpoints (`common/plugin/{id}/{path}`, needs sdk ≥ 0.0.9.dev5 on the robot; the dev bridge only registers routes — external calls need a running `common`) |
|
|
64
|
+
| `{ "type": "cpp", "entry": "<executable name>" }` | `cpp/` | Built with CMake in Docker against the C++ SDK (apt `rainbow-rb-sdk-dev`, `find_package(rb_sdk)`, dynamic link) |
|
|
55
65
|
| `{ "type": "ros2", "rosClient": "rclpy", ... }` | `ros2_ws/` | ROS2 Python node; may use Python SDK alongside ROS2 APIs |
|
|
56
|
-
| `{ "type": "ros2", "rosClient": "rclcpp", ... }` | `ros2_ws/` | ROS2 C++ node built with colcon/CMake |
|
|
66
|
+
| `{ "type": "ros2", "rosClient": "rclcpp", ... }` | `ros2_ws/` | ROS2 C++ node built with colcon/CMake, links the C++ SDK like `cpp` |
|
|
57
67
|
|
|
58
68
|
All non-`none` runtimes run in Docker during `muscat-pk dev:back`, `muscat-pk
|
|
59
69
|
dev`, and build/export flows so development matches the robot environment.
|
|
@@ -229,4 +239,30 @@ offline.
|
|
|
229
239
|
- `RB_REGISTRY` — registry base URL (default `https://registry.npmjs.org`).
|
|
230
240
|
- `MUSCAT_PK_NO_UPDATE_CHECK=1` — skip the check.
|
|
231
241
|
|
|
242
|
+
## Local vs global install
|
|
243
|
+
|
|
244
|
+
If the plugin project has its own `muscat-plugin-kit` in `node_modules`, the CLI
|
|
245
|
+
delegates to it — so the CLI, the `vite` preset, and `preview-host` are always
|
|
246
|
+
the one version the project pinned, no matter which `muscat-pk` you typed.
|
|
247
|
+
|
|
248
|
+
`muscat-pk update` follows the same rule: run from a project's local install it
|
|
249
|
+
updates only that project; add `--global` to also update the machine-wide CLI.
|
|
250
|
+
`muscat-pk version` prints whichever copy actually ran.
|
|
251
|
+
|
|
252
|
+
## Docker images
|
|
253
|
+
|
|
254
|
+
Images are built only when missing, because `docker build` resolves the base
|
|
255
|
+
image tag over the network on every run — even with all layers cached — which
|
|
256
|
+
would break `dev` offline. Pre-seed an air-gapped machine with
|
|
257
|
+
`docker save … | gzip` + `docker load`, or force a rebuild with
|
|
258
|
+
`MUSCAT_PK_REBUILD_IMAGE=1`.
|
|
259
|
+
|
|
260
|
+
The C++ images (`Dockerfile.cpp`, `Dockerfile.dev`) install `rainbow-rb-sdk-dev`
|
|
261
|
+
from `https://rainbowrobotics.github.io/rb-sdk` (suite `dev`), pinned by the
|
|
262
|
+
`RB_SDK_VERSION` build arg. Set `MUSCAT_PK_RB_SDK_VERSION=<version>` (with
|
|
263
|
+
`MUSCAT_PK_REBUILD_IMAGE=1`) to match a robot running a different SDK version.
|
|
264
|
+
|
|
265
|
+
On Linux the dev containers run as the invoking user (`--user`), so build output
|
|
266
|
+
under `dist/` and `cpp/build-*` is not left root-owned.
|
|
267
|
+
|
|
232
268
|
Full spec: [`docs/plugin-system/CREATE-CLI.md`](../../docs/plugin-system/CREATE-CLI.md).
|
package/cli/bin/muscat-pk.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// muscat-pk — Muscat plugin developer CLI.
|
|
3
3
|
// Subcommands: new, build, dev, dev:ui, dev:back, storybook, mcp, update, uninstall, sdk, version.
|
|
4
|
-
import { realpathSync } from "node:fs";
|
|
4
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { spawnSync } from "node:child_process";
|
|
7
8
|
import { color, kitVersion, log, resolveLocalCliBin } from "../src/util.mjs";
|
|
@@ -24,15 +25,39 @@ import { runCompletion } from "../src/completion.mjs";
|
|
|
24
25
|
// 실행 중인 게 글로벌/바이너리인데 cwd 상위에 로컬 설치가 있으면 그쪽으로 넘긴다.
|
|
25
26
|
// vite preset / preview-host / manifest 검증이 항상 프로젝트가 고정한 버전과
|
|
26
27
|
// 맞아야 해서다 (버전이 안 맞으면 "module not found" 류 에러로 터진다).
|
|
28
|
+
function readVersion(binPath) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(readFileSync(resolve(binPath, "../../../package.json"), "utf8")).version;
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isBinaryMode = typeof Bun !== "undefined";
|
|
37
|
+
|
|
27
38
|
(function delegateToLocalInstall() {
|
|
28
39
|
try {
|
|
29
40
|
const localBin = resolveLocalCliBin();
|
|
30
41
|
if (!localBin) return;
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
// 바이너리는 node_modules 안의 mjs일 수 없으니 자기 자신 비교 생략.
|
|
43
|
+
// (import.meta.url이 /$bunfs/ 가상 경로라 realpath가 throw하기도 한다.)
|
|
44
|
+
if (!isBinaryMode && realpathSync(localBin) === realpathSync(fileURLToPath(import.meta.url))) return;
|
|
45
|
+
// Say so — otherwise an outdated local copy silently answers instead, and
|
|
46
|
+
// its missing/renamed subcommands look like the CLI is broken.
|
|
47
|
+
const localVersion = readVersion(localBin);
|
|
48
|
+
console.error(
|
|
49
|
+
color.dim(`[muscat-pk] using this project's local install${localVersion ? ` (v${localVersion})` : ""}: ${localBin}`),
|
|
50
|
+
);
|
|
51
|
+
// Bun 바이너리의 execPath는 스크립트 인자를 실행하지 않으니 PATH의 node로 넘긴다.
|
|
52
|
+
const runtime = isBinaryMode ? "node" : process.execPath;
|
|
53
|
+
const r = spawnSync(runtime, [localBin, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
54
|
+
if (r.error) {
|
|
55
|
+
log.warn(`Failed to run local install via ${runtime} (${r.error.code}); continuing with this binary.`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
33
58
|
process.exit(r.status ?? 0);
|
|
34
59
|
} catch {
|
|
35
|
-
//
|
|
60
|
+
// realpath 실패 등 — 그냥 지금 프로세스로 계속.
|
|
36
61
|
}
|
|
37
62
|
})();
|
|
38
63
|
|
|
@@ -55,7 +80,7 @@ function formatHelp() {
|
|
|
55
80
|
commandRow("dev:ui", `Preview the plugin in a mini-Muscat ${color.dim("(preview-host)")}`),
|
|
56
81
|
commandRow("dev:back", `Run the plugin back end ${color.dim("(python: watchfiles reload / ros2: Docker)")}`),
|
|
57
82
|
commandRow("storybook", `Browse the bundled rb-components Storybook ${color.dim(`(${option("--port")} <n>, ${option("--rebuild")})`)}`),
|
|
58
|
-
commandRow("mcp", `Start an MCP server ${color.dim("(stdio)")} exposing rb-components/rb SDK docs to AI agents`),
|
|
83
|
+
commandRow("mcp", `Start an MCP server ${color.dim("(stdio)")} exposing rb-components/rb SDK docs to AI agents ${color.dim(`(${option("--install")} codex: register in ~/.codex/config.toml)`)}`),
|
|
59
84
|
commandRow("update", `Update ${color.magenta("@rainbow-robotics/muscat-plugin-kit")} and ${color.magenta("@rainbow-robotics/plugin-ui")} ${color.dim(`(local-only if run from a project's local install; ${option("--global")} also updates the global CLI)`)}`),
|
|
60
85
|
commandRow("uninstall", `Remove the installed ${color.cyan("muscat-pk")} CLI ${color.dim("(npm global or S3 binary)")}`),
|
|
61
86
|
commandRow("sdk <list|update [version]>", "Manage the python rainbow-rb-sdk version"),
|
|
@@ -84,7 +109,9 @@ async function main() {
|
|
|
84
109
|
case "storybook":
|
|
85
110
|
return await runStorybook(args);
|
|
86
111
|
case "mcp": {
|
|
87
|
-
const { runMcp } = await import("../src/mcp.mjs");
|
|
112
|
+
const { runMcp, installMcp } = await import("../src/mcp.mjs");
|
|
113
|
+
if (args[0] === "--install" || args[0] === "install")
|
|
114
|
+
return installMcp(args[1]);
|
|
88
115
|
return await runMcp(args);
|
|
89
116
|
}
|
|
90
117
|
case "update":
|
|
@@ -10,12 +10,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
10
10
|
cmake \
|
|
11
11
|
&& rm -rf /var/lib/apt/lists/*
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
# robot's version
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
# Rainbow Robotics C++ SDK (rainbow-rb-sdk-dev): headers + CMake config for
|
|
14
|
+
# find_package(rb_sdk). Pinned to the robot's SDK version — the binary links
|
|
15
|
+
# librb_sdk.so.<ver> by SONAME and the robot provides that exact .so at runtime.
|
|
16
|
+
# Override with MUSCAT_PK_RB_SDK_VERSION=<ver> (+ MUSCAT_PK_REBUILD_IMAGE=1).
|
|
17
|
+
ARG RB_SDK_VERSION=0.1.2~dev1
|
|
18
|
+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
|
|
19
|
+
&& install -d /etc/apt/keyrings \
|
|
20
|
+
&& curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key -o /etc/apt/keyrings/rb-sdk.asc \
|
|
21
|
+
&& echo 'deb [signed-by=/etc/apt/keyrings/rb-sdk.asc] https://rainbowrobotics.github.io/rb-sdk dev main' > /etc/apt/sources.list.d/rb-sdk.list \
|
|
22
|
+
&& apt-get update && apt-get install -y --no-install-recommends "rainbow-rb-sdk-dev=${RB_SDK_VERSION}" \
|
|
23
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
19
24
|
|
|
20
25
|
WORKDIR /plugin
|
|
21
26
|
|
|
@@ -9,6 +9,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
9
9
|
python3-colcon-common-extensions \
|
|
10
10
|
&& rm -rf /var/lib/apt/lists/*
|
|
11
11
|
|
|
12
|
+
# Rainbow Robotics C++ SDK (rainbow-rb-sdk-dev): headers + CMake config for
|
|
13
|
+
# find_package(rb_sdk). Pinned to the robot's SDK version — the binary links
|
|
14
|
+
# librb_sdk.so.<ver> by SONAME and the robot provides that exact .so at runtime.
|
|
15
|
+
# Override with MUSCAT_PK_RB_SDK_VERSION=<ver> (+ MUSCAT_PK_REBUILD_IMAGE=1).
|
|
16
|
+
ARG RB_SDK_VERSION=0.1.2~dev1
|
|
17
|
+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
|
|
18
|
+
&& install -d /etc/apt/keyrings \
|
|
19
|
+
&& curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key -o /etc/apt/keyrings/rb-sdk.asc \
|
|
20
|
+
&& echo 'deb [signed-by=/etc/apt/keyrings/rb-sdk.asc] https://rainbowrobotics.github.io/rb-sdk dev main' > /etc/apt/sources.list.d/rb-sdk.list \
|
|
21
|
+
&& apt-get update && apt-get install -y --no-install-recommends "rainbow-rb-sdk-dev=${RB_SDK_VERSION}" \
|
|
22
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
23
|
+
|
|
12
24
|
WORKDIR /plugin
|
|
13
25
|
|
|
14
26
|
CMD ["bash"]
|
package/cli/src/build.mjs
CHANGED
|
@@ -65,11 +65,11 @@ function runViteBuild(pluginRoot, { mode = "production" } = {}) {
|
|
|
65
65
|
if (r.status !== 0) throw new Error("vite build failed");
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function isMainBranch() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
68
|
+
// function isMainBranch() {
|
|
69
|
+
// const r = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" });
|
|
70
|
+
// const branch = r.stdout?.trim();
|
|
71
|
+
// return branch === "main" || branch === "master";
|
|
72
|
+
// }
|
|
73
73
|
|
|
74
74
|
// For ros2 plugins, compile the colcon workspace inside the Jazzy + Python 3.12 container.
|
|
75
75
|
// rclpy: arch-agnostic (pure Python) → dist/ros2_ws/install/ (built once)
|
|
@@ -169,7 +169,7 @@ function isPrerelease(version) {
|
|
|
169
169
|
// stableOnly=true → skips pre-release (dev/a/b/rc) versions.
|
|
170
170
|
async function latestPypiVersion(pkg, { stableOnly = false } = {}) {
|
|
171
171
|
const res = await fetch(`https://pypi.org/pypi/${pkg}/json`, {
|
|
172
|
-
signal:
|
|
172
|
+
signal: new AbortController().signal.timeout(5000),
|
|
173
173
|
});
|
|
174
174
|
if (!res.ok) throw new Error(`pypi ${pkg}: ${res.status}`);
|
|
175
175
|
const data = await res.json();
|
package/cli/src/cpp.mjs
CHANGED
|
@@ -22,9 +22,11 @@ export async function ensureImage(image = CPP_DEV_IMAGE, { platform = null } = {
|
|
|
22
22
|
const dockerfile = await resolveDockerfile("Dockerfile.cpp");
|
|
23
23
|
log.step(`docker build -t ${image}${platform ? ` (${platform})` : ""} (first run is slow, then cached)…`);
|
|
24
24
|
const platformFlags = platform ? ["--platform", platform] : [];
|
|
25
|
+
const sdkVer = process.env.MUSCAT_PK_RB_SDK_VERSION;
|
|
26
|
+
const buildArgs = sdkVer ? ["--build-arg", `RB_SDK_VERSION=${sdkVer}`] : [];
|
|
25
27
|
const r = spawnSync(
|
|
26
28
|
"docker",
|
|
27
|
-
["build", ...platformFlags, "-t", image, "-f", dockerfile, dirname(dockerfile)],
|
|
29
|
+
["build", ...platformFlags, ...buildArgs, "-t", image, "-f", dockerfile, dirname(dockerfile)],
|
|
28
30
|
{ stdio: "inherit" },
|
|
29
31
|
);
|
|
30
32
|
if (r.status !== 0) throw new Error("docker build failed.");
|
package/cli/src/dev.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { runDevUi } from "./devUi.mjs";
|
|
8
8
|
import { runDevBack, stopDevBackContainers } from "./devBack.mjs";
|
|
9
9
|
import { startBridge, stopBridge, BRIDGE_CONTAINER, BRIDGE_PORT } from "./bridge.mjs";
|
|
10
|
-
import { assertManifestPorts } from "./manifest.mjs";
|
|
10
|
+
import { assertManifestPorts, assertManifestVariables } from "./manifest.mjs";
|
|
11
11
|
import { formatPortRanges } from "./ports.mjs";
|
|
12
12
|
import {
|
|
13
13
|
ensureDocker,
|
|
@@ -111,6 +111,7 @@ async function maybeStartHostAgent(root, { robotIp, robotToken }) {
|
|
|
111
111
|
|
|
112
112
|
export async function runDev(args = []) {
|
|
113
113
|
const root = findPluginRoot();
|
|
114
|
+
assertManifestVariables(root);
|
|
114
115
|
const portRanges = assertManifestPorts(root);
|
|
115
116
|
if (portRanges.length > 0) {
|
|
116
117
|
log.step(`plugin backend ports declared: ${formatPortRanges(portRanges)}`);
|
package/cli/src/devBack.mjs
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
11
11
|
import { existsSync, watch } from "node:fs";
|
|
12
12
|
import { resolve } from "node:path";
|
|
13
|
-
import { ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection } from "./util.mjs";
|
|
13
|
+
import { ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection, realtimeFlags } from "./util.mjs";
|
|
14
14
|
import { ensureRemoteZenohEnv, usesHostNetwork } from "./zenohDev.mjs";
|
|
15
|
-
import { assertManifestPorts, manifestRuntime, readManifest, validateManifest } from "./manifest.mjs";
|
|
15
|
+
import { assertManifestPorts, assertManifestVariables, manifestRuntime, readManifest, validateManifest } from "./manifest.mjs";
|
|
16
16
|
import {
|
|
17
17
|
formatPortRanges,
|
|
18
18
|
portEnvFlags,
|
|
@@ -147,7 +147,7 @@ async function runPython(root, portRanges) {
|
|
|
147
147
|
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
148
148
|
log.step("starting Python dev container…");
|
|
149
149
|
const up = docker(
|
|
150
|
-
["run", "-d", "--name", container, ...netFlags, ...standaloneZenohPortFlags, ...pluginPortFlags,
|
|
150
|
+
["run", "-d", "--name", container, ...netFlags, ...standaloneZenohPortFlags, ...pluginPortFlags, ...realtimeFlags(),
|
|
151
151
|
// host.docker.internal is automatic on Docker Desktop (macOS/Windows);
|
|
152
152
|
// this makes it resolve on Linux too, for the no-bridge gateway fallback above.
|
|
153
153
|
...hostGatewayFlags,
|
|
@@ -445,11 +445,11 @@ async function runRos2(root, portRanges) {
|
|
|
445
445
|
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
446
446
|
log.step("starting ROS2 dev container…");
|
|
447
447
|
const up = docker(
|
|
448
|
-
["run", "-d", "--name", container, ...publishPortFlags(portRanges), "-e", "PIP_ROOT_USER_ACTION=ignore", "-v", `${root}:/plugin`, "-w", "/plugin", ROS2_DEV_IMAGE, "sleep", "infinity"],
|
|
448
|
+
["run", "-d", "--name", container, ...publishPortFlags(portRanges), ...realtimeFlags(), "-e", "PIP_ROOT_USER_ACTION=ignore", "-v", `${root}:/plugin`, "-w", "/plugin", ROS2_DEV_IMAGE, "sleep", "infinity"],
|
|
449
449
|
{ stdio: "inherit" },
|
|
450
450
|
);
|
|
451
451
|
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
452
|
-
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log });
|
|
452
|
+
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [process.env.MUSCAT_ZENOH_PORT] });
|
|
453
453
|
|
|
454
454
|
// one-time deps + symlink build (edits to .py reflect without rebuilding)
|
|
455
455
|
log.step("pip install + colcon build --symlink-install (once)…");
|
|
@@ -526,11 +526,11 @@ async function runCpp(root, portRanges) {
|
|
|
526
526
|
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
527
527
|
log.step("starting C++ dev container…");
|
|
528
528
|
const up = docker(
|
|
529
|
-
["run", "-d", "--name", container, ...publishPortFlags(portRanges), "-v", `${root}:/plugin`, "-w", "/plugin", CPP_DEV_IMAGE, "sleep", "infinity"],
|
|
529
|
+
["run", "-d", "--name", container, ...publishPortFlags(portRanges), ...realtimeFlags(), "-v", `${root}:/plugin`, "-w", "/plugin", CPP_DEV_IMAGE, "sleep", "infinity"],
|
|
530
530
|
{ stdio: "inherit" },
|
|
531
531
|
);
|
|
532
532
|
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
533
|
-
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log });
|
|
533
|
+
const stopPortMonitor = startPortContractMonitor({ container, ranges: portRanges, log, allow: [process.env.MUSCAT_ZENOH_PORT] });
|
|
534
534
|
|
|
535
535
|
const build = () => {
|
|
536
536
|
log.step("cmake build…");
|
|
@@ -585,6 +585,7 @@ function warnManifest(root) {
|
|
|
585
585
|
|
|
586
586
|
export async function runDevBack(args = []) {
|
|
587
587
|
const root = findPluginRoot();
|
|
588
|
+
assertManifestVariables(root);
|
|
588
589
|
warnManifest(root);
|
|
589
590
|
const portRanges = assertManifestPorts(root);
|
|
590
591
|
if (portRanges.length > 0 && process.env.MUSCAT_PK_PORTS_PRECHECKED !== "1")
|
package/cli/src/manifest.mjs
CHANGED
|
@@ -15,6 +15,9 @@ const RUNTIME_TYPES = ["none", "python", "cpp", "ros2"];
|
|
|
15
15
|
const ROS_CLIENTS = ["rclpy", "rclcpp"];
|
|
16
16
|
const I18N_LANGS = ["ko", "en", "ja", "zh", "vi", "th", "es"];
|
|
17
17
|
const TARGET_CATEGORIES = ["manipulate", "amr", "rby1", "vision"];
|
|
18
|
+
const VARIABLE_TYPES = ["number", "string", "boolean", "array"];
|
|
19
|
+
const VARIABLE_NAME_PREFIX = "RBP_";
|
|
20
|
+
const VARIABLE_NAME_RE = /^RBP_[A-Za-z0-9_]+$/;
|
|
18
21
|
|
|
19
22
|
function validateI18nLabel(value, field, errors) {
|
|
20
23
|
if (value === undefined || value === null) {
|
|
@@ -110,6 +113,46 @@ function validateCommandInfo(info, prefix, errors) {
|
|
|
110
113
|
errors.push(`${prefix}.commandInfo.childrenCommand must be a string`);
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
// variables: pyfm에 노출할 변수 선언. 없으면 UI가 사용 가능한 변수를 보여줄 수 없다.
|
|
117
|
+
function validateVariables(variables, errors) {
|
|
118
|
+
if (variables === undefined || variables === null) return;
|
|
119
|
+
if (!Array.isArray(variables)) {
|
|
120
|
+
errors.push(`"variables" must be an array`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const names = variables.map((v) => v?.name).filter(Boolean);
|
|
124
|
+
const duplicates = names.filter((n, i) => names.indexOf(n) !== i);
|
|
125
|
+
if (duplicates.length > 0)
|
|
126
|
+
errors.push(`variables: "name" must be unique (duplicates: ${[...new Set(duplicates)].map((n) => `"${n}"`).join(", ")})`);
|
|
127
|
+
|
|
128
|
+
variables.forEach((v, i) => {
|
|
129
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
130
|
+
errors.push(`variables[${i}] must be an object ({ name, type, init, description? })`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (typeof v.name !== "string" || v.name.length === 0)
|
|
134
|
+
errors.push(`variables[${i}].name is required`);
|
|
135
|
+
else if (!v.name.startsWith(VARIABLE_NAME_PREFIX))
|
|
136
|
+
errors.push(`variables[${i}].name: "${v.name}" must start with "${VARIABLE_NAME_PREFIX}" (e.g. "${VARIABLE_NAME_PREFIX}${v.name}")`);
|
|
137
|
+
else if (!VARIABLE_NAME_RE.test(v.name))
|
|
138
|
+
errors.push(`variables[${i}].name: "${v.name}" is not a valid identifier (after "${VARIABLE_NAME_PREFIX}": letters, digits, underscore only, at least one character)`);
|
|
139
|
+
if (!VARIABLE_TYPES.includes(v.type))
|
|
140
|
+
errors.push(`variables[${i}].type must be one of ${VARIABLE_TYPES.map((t) => `"${t}"`).join(" | ")}`);
|
|
141
|
+
if (v.init === undefined)
|
|
142
|
+
errors.push(`variables[${i}].init is required (initial value)`);
|
|
143
|
+
else if (v.type === "number" && typeof v.init !== "number")
|
|
144
|
+
errors.push(`variables[${i}].init must be a number when type is "number"`);
|
|
145
|
+
else if (v.type === "string" && typeof v.init !== "string")
|
|
146
|
+
errors.push(`variables[${i}].init must be a string when type is "string"`);
|
|
147
|
+
else if (v.type === "boolean" && typeof v.init !== "boolean")
|
|
148
|
+
errors.push(`variables[${i}].init must be a boolean when type is "boolean"`);
|
|
149
|
+
else if (v.type === "array" && !Array.isArray(v.init))
|
|
150
|
+
errors.push(`variables[${i}].init must be an array when type is "array"`);
|
|
151
|
+
if (v.description !== undefined && v.description !== null)
|
|
152
|
+
validateI18nLabel(v.description, `variables[${i}].description`, errors);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
113
156
|
function validateContributes(contributes, errors) {
|
|
114
157
|
if (!Array.isArray(contributes) || contributes.length === 0) {
|
|
115
158
|
errors.push("contributes is required and must be a non-empty array");
|
|
@@ -183,6 +226,7 @@ export function validateManifest(manifest) {
|
|
|
183
226
|
validatePorts(manifest.ports, errors);
|
|
184
227
|
validateRuntime(manifest.runtime, errors);
|
|
185
228
|
validateContributes(manifest.contributes, errors);
|
|
229
|
+
validateVariables(manifest.variables, errors);
|
|
186
230
|
|
|
187
231
|
return errors;
|
|
188
232
|
}
|
|
@@ -202,6 +246,18 @@ export function assertManifest(root) {
|
|
|
202
246
|
return manifest;
|
|
203
247
|
}
|
|
204
248
|
|
|
249
|
+
export function assertManifestVariables(root) {
|
|
250
|
+
const manifest = readManifest(root);
|
|
251
|
+
if (manifest === null)
|
|
252
|
+
throw new Error("manifest.json parse failed — check JSON syntax");
|
|
253
|
+
if (Object.keys(manifest).length === 0)
|
|
254
|
+
return;
|
|
255
|
+
const errors = [];
|
|
256
|
+
validateVariables(manifest.variables, errors);
|
|
257
|
+
if (errors.length > 0)
|
|
258
|
+
throw new Error(`manifest.json variable errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
205
261
|
export function assertManifestPorts(root) {
|
|
206
262
|
const manifest = readManifest(root);
|
|
207
263
|
if (manifest === null)
|