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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.ko.md +1185 -0
  2. package/README.md +1051 -148
  3. package/cli/README.md +4 -2
  4. package/cli/bin/muscat-pk.mjs +6 -2
  5. package/cli/src/build.mjs +9 -13
  6. package/cli/src/completion.mjs +2 -0
  7. package/cli/src/dev.mjs +22 -3
  8. package/cli/src/devBack.mjs +1 -1
  9. package/cli/src/devUi.mjs +2 -1
  10. package/cli/src/mcp.mjs +17 -3
  11. package/cli/src/muscatLocal.mjs +57 -21
  12. package/cli/src/pluginDevServer.mjs +1 -1
  13. package/cli/src/sim.mjs +163 -0
  14. package/cli/src/util.mjs +2 -0
  15. package/cli/storybook-dist/assets/{Color-YHDXOIA2-JZmvrBu2.js → Color-YHDXOIA2-DSB_yvex.js} +1 -1
  16. package/cli/storybook-dist/assets/{Docs-DjjxchlZ.js → Docs-C_90bDRy.js} +1 -1
  17. package/cli/storybook-dist/assets/{Docs-DrMHaIyt.js → Docs-CbW9HZd4.js} +1 -1
  18. package/cli/storybook-dist/assets/{Docs-GWKBJz_w.js → Docs-CoskoRMT.js} +1 -1
  19. package/cli/storybook-dist/assets/{Docs-CHh5amiz.js → Docs-Cy9XFeLJ.js} +1 -1
  20. package/cli/storybook-dist/assets/{Docs-DdU-wsGN.js → Docs-De1oc64k.js} +1 -1
  21. package/cli/storybook-dist/assets/{Docs-DXJ25Kqq.js → Docs-klD3F_Ai.js} +1 -1
  22. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-DNcXzd9o.js → DocsRenderer-CFRXHY34-Bkm4uV_D.js} +1 -1
  23. package/cli/storybook-dist/assets/{NxAvatar.stories-CwnK8AUF.js → NxAvatar.stories-Ctf5G2FQ.js} +1 -1
  24. package/cli/storybook-dist/assets/{NxButton.stories-ueYdosfQ.js → NxButton.stories-DeLObVSj.js} +1 -1
  25. package/cli/storybook-dist/assets/{NxNumberInput.stories-M5ZCLbrK.js → NxNumberInput.stories-LqoY0ed1.js} +1 -1
  26. package/cli/storybook-dist/assets/{NxPagination.stories-bxxUnHEL.js → NxPagination.stories-DI6bXuJM.js} +1 -1
  27. package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BHxB2kr5.js → NxPointerHoldButton.stories-3HSV1XmN.js} +1 -1
  28. package/cli/storybook-dist/assets/{NxRangeInput.stories-BKfr-fp4.js → NxRangeInput.stories-1pjgQVaG.js} +1 -1
  29. package/cli/storybook-dist/assets/{NxSelectBox.stories-CG8FuTBq.js → NxSelectBox.stories-bcQPZEuF.js} +1 -1
  30. package/cli/storybook-dist/assets/{NxSlideRange.stories-BRTFtMTL.js → NxSlideRange.stories-BosX4axk.js} +1 -1
  31. package/cli/storybook-dist/assets/{NxSwitch.stories-Bwuyzn-R.js → NxSwitch.stories-DxV6dS33.js} +1 -1
  32. package/cli/storybook-dist/assets/{NxTextArea.stories-COq2sGqu.js → NxTextArea.stories-CO545-LP.js} +1 -1
  33. package/cli/storybook-dist/assets/{NxTextInput.stories-CjsBLz05.js → NxTextInput.stories-BP0npai1.js} +1 -1
  34. package/cli/storybook-dist/assets/{RBAccordion.stories-BARwuZ0Q.js → RBAccordion.stories-fVR4Va2a.js} +1 -1
  35. package/cli/storybook-dist/assets/{RBAccordionGroup.stories-COrPxETW.js → RBAccordionGroup.stories-Du5bsckD.js} +1 -1
  36. package/cli/storybook-dist/assets/{RBAlert.stories-BQrkjZPt.js → RBAlert.stories-DNkMXUu6.js} +1 -1
  37. package/cli/storybook-dist/assets/{RBBadge.stories-BTsLL-lK.js → RBBadge.stories-C44pXt8D.js} +1 -1
  38. package/cli/storybook-dist/assets/{RBBattery.stories-DAeQdJbc.js → RBBattery.stories-DE2JHvfL.js} +1 -1
  39. package/cli/storybook-dist/assets/{RBCard.stories-HyhOE80R.js → RBCard.stories-CuIda7NI.js} +1 -1
  40. package/cli/storybook-dist/assets/{RBCheckBox.stories-KFk0WqX_.js → RBCheckBox.stories-D1qM1ayr.js} +1 -1
  41. package/cli/storybook-dist/assets/{RBCheckIconButton.stories-CXbbktF5.js → RBCheckIconButton.stories-e4gkliBO.js} +1 -1
  42. package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-DWfYz_9Z.js → RBCheckLabelButton.stories-CDzCtsMI.js} +1 -1
  43. package/cli/storybook-dist/assets/{RBChipTab.stories-JPEgVFAS.js → RBChipTab.stories-DBrc_3Mp.js} +1 -1
  44. package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-CTPuWXrQ.js → RBChipsBoxTab.stories-hlrsHjKY.js} +1 -1
  45. package/cli/storybook-dist/assets/{RBColorIcons.stories-DiFRZ5jk.js → RBColorIcons.stories-BHUvpkdu.js} +1 -1
  46. package/cli/storybook-dist/assets/{RBCommand.stories-Pf1HAVIR.js → RBCommand.stories-azAF41NP.js} +1 -1
  47. package/cli/storybook-dist/assets/{RBConfirm.stories-D1cEPUXk.js → RBConfirm.stories-BMNjwbmT.js} +1 -1
  48. package/cli/storybook-dist/assets/{RBDateInput.stories-RPYpCjig.js → RBDateInput.stories-D0VezDzv.js} +1 -1
  49. package/cli/storybook-dist/assets/{RBDialog.stories-DJH-0XSR.js → RBDialog.stories-AOJ975Fy.js} +1 -1
  50. package/cli/storybook-dist/assets/{RBDivider.stories-uEj1vOgK.js → RBDivider.stories-NSvnj43Z.js} +1 -1
  51. package/cli/storybook-dist/assets/{RBDrawer.stories-BiMzElb2.js → RBDrawer.stories-Du0oTvD1.js} +1 -1
  52. package/cli/storybook-dist/assets/{RBDropdownMenu.stories-Da_TBkla.js → RBDropdownMenu.stories-Hsxve4PW.js} +1 -1
  53. package/cli/storybook-dist/assets/{RBEmptyView.stories-BHeKGfPS.js → RBEmptyView.stories-BlwHx3zp.js} +1 -1
  54. package/cli/storybook-dist/assets/{RBIconButton.stories-NEsRFoYp.js → RBIconButton.stories-DAPDY63x.js} +1 -1
  55. package/cli/storybook-dist/assets/{RBIcons.stories-CcZ-TTLS.js → RBIcons.stories-W5ZUV2L1.js} +1 -1
  56. package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-B0wpdQyR.js → RBInlineLabelButton.stories-C99j0Y3g.js} +1 -1
  57. package/cli/storybook-dist/assets/{RBJoyStick.stories-DEgBou3K.js → RBJoyStick.stories-Du0H1iAS.js} +1 -1
  58. package/cli/storybook-dist/assets/{RBLabelButton.stories-DT8drIsF.js → RBLabelButton.stories-BlvHaU_Z.js} +1 -1
  59. package/cli/storybook-dist/assets/{RBLight.stories-Dh-aoeyG.js → RBLight.stories-BY6DKrre.js} +1 -1
  60. package/cli/storybook-dist/assets/{RBMenubar.stories-D-H7UqVh.js → RBMenubar.stories-D2PEGpM9.js} +1 -1
  61. package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-hlclv6jI.js → RBMultiRangeInput.stories-Bm2qSmjq.js} +1 -1
  62. package/cli/storybook-dist/assets/{RBPagination.stories-DWCeoZ_S.js → RBPagination.stories-DDleJa5N.js} +1 -1
  63. package/cli/storybook-dist/assets/{RBPopover.stories-C514M09_.js → RBPopover.stories-27YxPSFG.js} +1 -1
  64. package/cli/storybook-dist/assets/{RBPrompt.stories-BSItvfiT.js → RBPrompt.stories-BFaIOctr.js} +1 -1
  65. package/cli/storybook-dist/assets/{RBRadioBox.stories-BBb4YbFU.js → RBRadioBox.stories-BroDar0t.js} +1 -1
  66. package/cli/storybook-dist/assets/{RBRadioButton.stories-CXyOGJCv.js → RBRadioButton.stories-M3O-wX7V.js} +1 -1
  67. package/cli/storybook-dist/assets/{RBRangeDateInput.stories-qvhubu0e.js → RBRangeDateInput.stories-CWlM_D5X.js} +1 -1
  68. package/cli/storybook-dist/assets/{RBResizable.stories-CZKeyGjH.js → RBResizable.stories-ui8X1qnc.js} +1 -1
  69. package/cli/storybook-dist/assets/{RBSelectBox.stories-CRKP3ybf.js → RBSelectBox.stories-C8T6cfA0.js} +1 -1
  70. package/cli/storybook-dist/assets/{RBSheet.stories-sCM3ZJUa.js → RBSheet.stories-BCOo_Zcc.js} +1 -1
  71. package/cli/storybook-dist/assets/{RBSideNumberInput.stories-Bb7gw8RF.js → RBSideNumberInput.stories-CU2_MVlJ.js} +1 -1
  72. package/cli/storybook-dist/assets/{RBSlider.stories-23OAQU5z.js → RBSlider.stories-DgOp1TFc.js} +1 -1
  73. package/cli/storybook-dist/assets/{RBSpinner.stories-2xcQdcmt.js → RBSpinner.stories-qLScY3zZ.js} +1 -1
  74. package/cli/storybook-dist/assets/{RBSwitch.stories-BACUaJm_.js → RBSwitch.stories-SXSY1QMb.js} +1 -1
  75. package/cli/storybook-dist/assets/{RBTab.stories-CDHE3sVs.js → RBTab.stories-BzXXTsXP.js} +1 -1
  76. package/cli/storybook-dist/assets/{RBTable.stories-C0Cfhfl7.js → RBTable.stories-DHTeRNVk.js} +1 -1
  77. package/cli/storybook-dist/assets/{RBTableRow.stories-BF8kIkpp.js → RBTableRow.stories-DRNRsd15.js} +1 -1
  78. package/cli/storybook-dist/assets/{RBTabs.stories-BdJWRq1o.js → RBTabs.stories-DP0noBLp.js} +1 -1
  79. package/cli/storybook-dist/assets/{RBTextArea.stories-CGy2xeYH.js → RBTextArea.stories-TkaejfxJ.js} +1 -1
  80. package/cli/storybook-dist/assets/{RBTextField.stories-IfUOzxCM.js → RBTextField.stories-B6VpCGyt.js} +1 -1
  81. package/cli/storybook-dist/assets/{RBTextInput.stories-VjXNFo4R.js → RBTextInput.stories-D3hZ08e-.js} +1 -1
  82. package/cli/storybook-dist/assets/{RBToast.stories-BzcobblV.js → RBToast.stories-BQ2wS2U0.js} +1 -1
  83. package/cli/storybook-dist/assets/{RBToggle.stories-DuLE5mKv.js → RBToggle.stories-GVOjGqo3.js} +1 -1
  84. package/cli/storybook-dist/assets/{RBToggleGroup.stories-CuR0N0GJ.js → RBToggleGroup.stories-pzx0btOD.js} +1 -1
  85. package/cli/storybook-dist/assets/{RBTooltip.stories--aAgNC8_.js → RBTooltip.stories-jufB6fa1.js} +1 -1
  86. package/cli/storybook-dist/assets/{SideNumberInput-DyWQxYUj.js → SideNumberInput-BYlmNHEw.js} +1 -1
  87. package/cli/storybook-dist/assets/{entry-preview-Du01HPfn.js → entry-preview-DY0ZrYNE.js} +1 -1
  88. package/cli/storybook-dist/assets/{iframe-DFBATQRS.js → iframe-LwKb1w63.js} +2 -2
  89. package/cli/storybook-dist/assets/{index-DG8XddNr.js → index-BsAhjDKO.js} +3 -3
  90. package/cli/storybook-dist/assets/preview-BJ0FvQZh.js +3 -0
  91. package/cli/storybook-dist/assets/{preview-donYBfDJ.js → preview-DiY2_30w.js} +2 -2
  92. package/cli/storybook-dist/assets/{preview-CigNHo-I.css → preview-kTZPgP6D.css} +1 -1
  93. package/cli/storybook-dist/assets/{react-resizable-panels-Dp9zi-o3.css → react-resizable-panels-HsFbxPbn.css} +1 -1
  94. package/cli/storybook-dist/iframe.html +1 -1
  95. package/cli/storybook-dist/project.json +1 -1
  96. package/cli/templates/common/README.md +2 -3
  97. package/cli/templates/common/requirements.txt +3 -3
  98. package/cli/templates/cpp/cpp/CMakeLists.txt +1 -1
  99. package/cli/templates/readme-cpp/README.md +3 -4
  100. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +1 -1
  101. package/package.json +5 -4
  102. package/cli/storybook-dist/assets/preview-e0NAgyy_.js +0 -3
  103. /package/cli/storybook-dist/assets/{react-resizable-panels.browser-8hC8dr-6.js → react-resizable-panels.browser-DuxpZLkF.js} +0 -0
package/README.md CHANGED
@@ -1,50 +1,57 @@
1
1
  # @rainbow-robotics/muscat-plugin-kit
2
2
 
3
- CLI for developing Muscat plugins. Provides scaffolding, build/pack, and dev servers (UI + backend).
3
+ CLI for developing Muscat plugins (`muscat-pk`): scaffolding, dev servers (UI + backend), build/pack, and the SDK references a plugin author needs.
4
+
5
+ [한국어 문서 (README.ko.md)](./README.ko.md)
6
+
7
+ ## Contents
8
+
9
+ 1. [Install](#1-install)
10
+ 2. [Quick start](#2-quick-start)
11
+ 3. [Commands](#3-commands)
12
+ 4. [Project layout](#4-project-layout)
13
+ 5. [manifest.json](#5-manifestjson)
14
+ 6. [Plugin UI SDK (`useRb`)](#6-plugin-ui-sdk-userb)
15
+ 7. [Python plugin SDK](#7-python-plugin-sdk)
16
+ 8. [Other backend runtimes (C++ / ROS 2)](#8-other-backend-runtimes-c--ros-2)
17
+ 9. [Development environment](#9-development-environment)
18
+ 10. [Styles](#10-styles)
19
+ 11. [AI coding agent setup (`muscat-pk mcp`)](#11-ai-coding-agent-setup-muscat-pk-mcp)
20
+ 12. [Environment variables](#12-environment-variables)
21
+ 13. [Troubleshooting](#13-troubleshooting)
4
22
 
5
- ## Install
6
-
7
- ### Option 1 — npm (requires Node.js 20+)
23
+ ---
8
24
 
9
- ```bash
10
- # latest stable
11
- npm install -g @rainbow-robotics/muscat-plugin-kit
25
+ ## 1. Install
12
26
 
13
- # dev channel (latest dev build)
14
- npm install -g @rainbow-robotics/muscat-plugin-kit@dev
27
+ ### npm (Node.js 20+)
15
28
 
16
- # specific version
17
- npm install -g @rainbow-robotics/muscat-plugin-kit@1.2.3
29
+ ```bash
30
+ npm install -g @rainbow-robotics/muscat-plugin-kit # latest stable
31
+ npm install -g @rainbow-robotics/muscat-plugin-kit@dev # dev channel
32
+ npm install -g @rainbow-robotics/muscat-plugin-kit@1.2.3 # specific version
18
33
  ```
19
34
 
20
- ### Option 2 — S3 install script (no Node.js required)
35
+ ### Install script (no Node.js required)
21
36
 
22
37
  macOS / Linux / WSL:
23
38
 
24
39
  ```bash
25
- # latest stable
26
40
  curl -fsSL https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.sh | sh
27
-
28
- # dev channel
29
41
  curl -fsSL https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.sh | MUSCAT_PK_TAG=dev sh
30
-
31
- # specific version
32
42
  curl -fsSL https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.sh | MUSCAT_PK_TAG=1.2.3 sh
33
43
  ```
34
44
 
35
45
  Windows PowerShell:
36
46
 
37
47
  ```powershell
38
- # latest stable
39
48
  irm https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.ps1 | iex
40
-
41
- # dev channel
42
49
  $env:MUSCAT_PK_TAG="dev"; irm https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/install.ps1 | iex
43
50
  ```
44
51
 
45
- > The install script uses npm when Node.js 20+ is present, otherwise it downloads a self-contained binary from S3.
52
+ The script uses npm when Node.js 20+ is present, otherwise it downloads a self-contained binary.
46
53
 
47
- ### Option 3 — direct binary download
54
+ ### Direct binary download
48
55
 
49
56
  ```
50
57
  https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/v{version}/{binary}
@@ -59,129 +66,404 @@ https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/dev/{bi
59
66
  | macOS x64 | `muscat-pk-darwin-x64` |
60
67
  | Windows x64 | `muscat-pk-win-x64.exe` |
61
68
 
62
- Channel pointers:
69
+ Channel pointers: `.../muscat-plugin-kit/latest-version.txt` (stable), `.../muscat-plugin-kit/dev-version.txt` (dev).
63
70
 
64
- ```
65
- # latest stable version string
66
- https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/latest-version.txt
71
+ ### Requirements
67
72
 
68
- # latest dev version string
69
- https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit/dev-version.txt
70
- ```
73
+ | Tool | Needed for |
74
+ |---|---|
75
+ | Node.js 20+ (or the binary) | the CLI, UI build |
76
+ | Docker | every backend runtime except `none` (`dev:back`, `dev`, `build`) |
77
+ | `uv` or Python 3.12 (optional) | IDE-only `.venv` for Python linting |
71
78
 
72
79
  ### Uninstall
73
80
 
74
- Regardless of how it was installed, `muscat-pk` can remove itself:
75
-
76
81
  ```bash
77
- muscat-pk uninstall
82
+ muscat-pk uninstall # works for npm and binary installs
83
+ npm uninstall -g @rainbow-robotics/muscat-plugin-kit # manual, npm
84
+ sudo rm -f /usr/local/bin/muscat-pk # manual, binary
78
85
  ```
79
86
 
80
- Manual removal:
87
+ ---
81
88
 
82
- ```bash
83
- # if installed via npm
84
- npm uninstall -g @rainbow-robotics/muscat-plugin-kit
89
+ ## 2. Quick start
85
90
 
86
- # if installed via the S3 binary (default macOS/Linux path)
87
- sudo rm -f /usr/local/bin/muscat-pk
91
+ ```bash
92
+ muscat-pk new my-plugin # scaffold (asks author, email, description, type, package manager)
93
+ cd my-plugin
94
+ muscat-pk dev # UI preview + backend together
95
+ muscat-pk build # dist/ + distributable ZIP
88
96
  ```
89
97
 
98
+ Install the ZIP from the robot's Muscat plugin page.
99
+
90
100
  ---
91
101
 
92
- ## Quick start
102
+ ## 3. Commands
103
+
104
+ | Command | Description |
105
+ |---|---|
106
+ | `muscat-pk new [dir]` | Scaffold a plugin project |
107
+ | `muscat-pk type change` | Switch the backend type |
108
+ | `muscat-pk dev` | Local Muscat runtime + `dev:ui` + `dev:back` |
109
+ | `muscat-pk dev:ui` | UI preview in a mini-Muscat (preview-host) |
110
+ | `muscat-pk dev:back` | Backend only, in Docker, with hot reload |
111
+ | `muscat-pk build` | Build, lock Python deps, vendor wheels, ZIP |
112
+ | `muscat-pk update` | Update the kit, `plugin-ui` and `preview-host` |
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` |
115
+ | `muscat-pk storybook` | Browse the bundled rb-components Storybook |
116
+ | `muscat-pk mcp` | MCP server for AI coding agents |
117
+ | `muscat-pk uninstall` | Remove the CLI |
118
+ | `muscat-pk version` | Print the running version (`-v`, `--version`) |
119
+ | `muscat-pk completion [shell]` | Print a shell completion script |
120
+
121
+ `muscat-pk <command> --help` prints the options of a command.
122
+
123
+ ### 3.1 `new`
93
124
 
94
125
  ```bash
95
- muscat-pk new my-plugin # scaffold a plugin project (choose python / cpp / ros2 / ros2-cpp)
96
- cd my-plugin
126
+ muscat-pk new [dir] [--type python|cpp|ros2|ros2-cpp] [--pm npm|yarn|pnpm|bun]
127
+ [--author <name>] [--email <email>] [--description <text>]
128
+ [--yes] [--no-install]
129
+ ```
130
+
131
+ | Option | Meaning |
132
+ |---|---|
133
+ | `dir` | target directory (must be empty); asked when omitted |
134
+ | `--type` | backend type: `python` (default), `cpp`, `ros2` (rclpy), `ros2-cpp` (rclcpp) |
135
+ | `--pm` | front-end package manager; defaults to `yarn` inside a Yarn workspace, else `npm` |
136
+ | `--author` / `--email` / `--description` | manifest fields (email is required by the manifest) |
137
+ | `--yes` | no prompts; unspecified values use defaults |
138
+ | `--no-install` | skip `<pm> install`, the IDE `.venv` and the C++ dev image |
139
+ | `--help` | print the usage |
140
+
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).
97
147
 
98
- muscat-pk dev # run UI + backend together
99
- muscat-pk dev:ui # UI preview only
100
- muscat-pk dev:back # backend only (Docker: python/cpp/ros2)
148
+ ### 3.2 `type change`
101
149
 
102
- muscat-pk build # build dist/ and produce a distributable ZIP (skip the ZIP with --no-zip)
150
+ ```bash
151
+ muscat-pk type change [--to python|cpp|ros2|ros2-cpp] [--readme]
103
152
  ```
104
153
 
105
- ## Command list
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`) |
106
158
 
107
- | Command | Description |
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`.
160
+
161
+ ### 3.3 `dev`, `dev:ui`, `dev:back`
162
+
163
+ ```bash
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]
168
+ ```
169
+
170
+ | Command | What runs |
108
171
  |---|---|
109
- | `muscat-pk new [dir]` | Scaffold a plugin project (also creates an IDE-only python `.venv`) |
110
- | `muscat-pk type change` | Switch backend type (`python` / `cpp` / `ros2` / `ros2-cpp`) |
111
- | `muscat-pk build` | Build `ui/*.tsx` → `dist/*.js` (one per `contributes[].ui` entry in the manifest) and produce a distributable ZIP |
112
- | `muscat-pk dev` | UI + backend dev server together |
113
- | `muscat-pk dev:ui` | Mini-Muscat preview server |
114
- | `muscat-pk dev:back` | Backend dev server (Docker, hot-reload) |
115
- | `muscat-pk storybook` | Browse the bundled rb-components Storybook |
116
- | `muscat-pk mcp` | Run an MCP server for AI coding agents (exposes rb-components/design-token/SDK docs) |
117
- | `muscat-pk update` | Update the kit + plugin-ui + preview-host to the latest version; lists old-API UI code to fix |
118
- | `muscat-pk sdk <list\|update>` | Manage the `rainbow-rb-sdk` version — python: `requirements.txt`, C++ (cpp / ros2-cpp): `find_package(rb_sdk <ver> EXACT REQUIRED)` in `CMakeLists.txt` |
119
- | `muscat-pk version` | Print the installed CLI version (`-v` / `--version` also work) |
120
- | `muscat-pk completion [shell]` | Print a shell completion script (`bash` / `zsh` / `fish` / `powershell`; defaults to `$SHELL`) |
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 |
174
+ | `dev:back` | the backend in a Docker container matching the robot. Python restarts on `py/**/*.py` changes (watchfiles) |
121
175
 
122
- ### Update policy
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) |
123
184
 
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:
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]`).
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.
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.
126
191
 
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.
192
+ #### Developing without a robot (`--sim`)
132
193
 
133
- **`update` only touches what you're in.**
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.
134
195
 
135
- | Where you run it | What it updates |
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.
215
+
216
+ ### 3.4 `build`
217
+
218
+ ```bash
219
+ muscat-pk build [--arch amd64|arm64|both]
220
+ ```
221
+
222
+ Steps:
223
+
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.
235
+
236
+ | Option | Meaning |
136
237
  |---|---|
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) |
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>`.
241
+
242
+ ### 3.5 `update`
140
243
 
141
244
  ```bash
142
- muscat-pk update # this project only
245
+ muscat-pk update # this project only (when it has a local install)
143
246
  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
247
+ muscat-pk update dev # follow the dev channel
248
+ muscat-pk update 1.2.3 # a specific version (also --tag <tag> / --version <v>)
146
249
  ```
147
250
 
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.
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
+
256
+ | Where you run it | What it updates |
257
+ |---|---|
258
+ | Inside a project with a local install | that project's `muscat-plugin-kit` + `plugin-ui` (+ `preview-host` when listed in `package.json`) |
259
+ | Same, with `--global` | the above + the global CLI |
260
+ | Anywhere else | the global CLI (npm `-g` or the S3 binary) and local deps if inside a project |
150
261
 
151
- ### Offline / air-gapped use
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.
152
267
 
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.
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`).
157
269
 
158
- Move images to a machine with no internet:
270
+ ### 3.6 `sdk`
159
271
 
160
272
  ```bash
161
- # on a machine with network
162
- docker save muscat-pk-python-dev muscat-pk-runtime | gzip > muscat-pk-images.tar.gz
273
+ muscat-pk sdk list # 12 newest published versions
274
+ muscat-pk sdk update [version] # latest stable when omitted
275
+ ```
163
276
 
164
- # on the offline machine
165
- gunzip -c muscat-pk-images.tar.gz | docker load
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)`).
283
+
284
+ ### 3.7 `storybook`, `mcp`, `completion`, `version`
285
+
286
+ ```bash
287
+ muscat-pk storybook [--port 6007] [--rebuild]
288
+ muscat-pk mcp [--install codex]
289
+ muscat-pk completion [bash|zsh|fish|powershell]
290
+ muscat-pk version
291
+ muscat-pk uninstall
166
292
  ```
167
293
 
168
- Force a rebuild with `MUSCAT_PK_REBUILD_IMAGE=1`.
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 |
169
302
 
170
- ### Shell completion
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.
171
304
 
172
- | OS / shell | Command |
305
+ | OS / shell | Completion setup |
173
306
  |---|---|
174
307
  | macOS (zsh) | `muscat-pk completion zsh >> ~/.zshrc && source ~/.zshrc` |
175
308
  | Linux (bash) | `muscat-pk completion bash >> ~/.bashrc && source ~/.bashrc` |
176
309
  | fish | `muscat-pk completion fish > ~/.config/fish/completions/muscat-pk.fish` |
177
- | Windows (PowerShell) | `muscat-pk completion powershell >> $PROFILE; . $PROFILE` |
310
+ | PowerShell | `muscat-pk completion powershell >> $PROFILE; . $PROFILE` |
311
+
312
+ ---
313
+
314
+ ## 4. Project layout
315
+
316
+ ```
317
+ my-plugin/
318
+ ├─ ui/
319
+ │ ├─ content.tsx # UI entry (one file per contributes[].ui), `export default App`
320
+ │ └─ style.css # auto-loaded for every entry
321
+ ├─ py/main.py # python backend entry (runtime.entry)
322
+ ├─ cpp/ # cpp backend (CMake) — cpp type
323
+ ├─ ros2_ws/ # colcon workspace — ros2 / ros2-cpp type
324
+ ├─ assets/icon.svg
325
+ ├─ manifest.json
326
+ ├─ requirements.txt # python deps (locked into dist/requirements.lock on build)
327
+ ├─ package.json # UI deps (not shipped)
328
+ ├─ vite.config.ts # must use the muscat() preset
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)
334
+ ```
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
+
338
+ `vite.config.ts`:
339
+
340
+ ```ts
341
+ import { defineConfig } from "vite";
342
+ import tailwindcss from "@tailwindcss/vite";
343
+ import { muscat } from "@rainbow-robotics/muscat-plugin-kit/vite";
344
+
345
+ export default defineConfig({ plugins: [tailwindcss(), muscat()] });
346
+ ```
347
+
348
+ The ZIP contains only the plugin's own code and assets. `@rainbow-robotics/plugin-ui`, `react` and `react-dom` come from the host's import map.
178
349
 
179
- Windows `cmd.exe` has no completion mechanism to hook into — use PowerShell, Git Bash, or WSL.
350
+ ---
351
+
352
+ ## 5. manifest.json
353
+
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.
355
+
356
+ ### 5.1 Full example
357
+
358
+ ```json
359
+ {
360
+ "id": "com.example.my-plugin",
361
+ "name": "My Plugin",
362
+ "version": "1.0.0",
363
+ "minMuscatVersion": "1.0.0",
364
+ "author": "Kim",
365
+ "email": "kim@example.com",
366
+ "company": "Example Inc.",
367
+ "description": { "en": "Gripper helper", "ko": "그리퍼 도우미" },
368
+ "icons": "assets/icon.svg",
369
+ "targetModels": "all",
370
+ "targetCategorys": ["manipulate"],
371
+ "contributes": [
372
+ { "point": "toolbar", "icon": "assets/icon.svg", "label": { "en": "My Plugin" }, "ui": "content", "opens": "popover" }
373
+ ],
374
+ "variables": [
375
+ { "name": "RBP_grip_force", "type": "number", "init": 0.5, "description": { "en": "Gripper force (0–1)" } }
376
+ ],
377
+ "ports": [28600],
378
+ "runtime": { "type": "python", "entry": "main.py" }
379
+ }
380
+ ```
180
381
 
181
- ## Backend port policy
382
+ ### 5.2 Top-level fields
182
383
 
183
- A plugin backend that opens a TCP listener must declare every such port in
184
- `manifest.json`:
384
+ | Field | Required | Rule |
385
+ |---|---|---|
386
+ | `id` | yes | reverse-domain: `.`-separated parts of lowercase letters/digits joined by single `-`, each starting with a letter (`com.example.my-plugin`). No `_` |
387
+ | `name`, `version`, `minMuscatVersion` | yes | non-empty strings |
388
+ | `author`, `email` | yes | non-empty strings |
389
+ | `company` | no | string |
390
+ | `description` | yes | string or i18n object |
391
+ | `icons` | yes | SVG path string, or `{ "16", "48", "128" }` for PNG/JPG (all three) |
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) |
394
+ | `variables` | no | see 5.5 |
395
+ | `ports` | no | see 5.6 |
396
+ | `runtime` | yes | see 5.7 |
397
+ | `installedSize` | — | written by `build` (bytes) |
398
+
399
+ i18n object: `en` is required; allowed keys `ko`, `en`, `ja`, `zh`, `vi`, `th`, `es`.
400
+
401
+ ### 5.3 `contributes`
402
+
403
+ | Field | Rule |
404
+ |---|---|
405
+ | `point` | `toolbar`, `lnb`, `move.panel`, `program.panel`, `program.monitor` |
406
+ | `ui` | UI entry name under `ui/` without extension (default `content`); unique across entries |
407
+ | `label` | required, string or i18n object |
408
+ | `icon` | icon path |
409
+ | `opens` | toolbar only: `popover`, `panel`, `page` |
410
+ | `commandInfo` | required for `program.panel` (5.4) |
411
+
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`.
413
+
414
+ | Point | Where it shows |
415
+ |---|---|
416
+ | `toolbar` | header toolbar button; opens as popover / panel / page |
417
+ | `lnb` | left navigation |
418
+ | `move.panel` | Move page panel |
419
+ | `program.panel` | a custom command in the Program page's step tree; its `ui` is the step's edit form |
420
+ | `program.monitor` | a tab in the Program page's monitor panel (next to General / Digital I/O / Monitor / Log), rendered per task |
421
+
422
+ ### 5.4 `commandInfo` (`program.panel`)
423
+
424
+ ```json
425
+ {
426
+ "point": "program.panel",
427
+ "ui": "program",
428
+ "label": { "en": "My Step" },
429
+ "commandInfo": {
430
+ "name": "My Step",
431
+ "icon": "assets/icon.svg",
432
+ "funcName": "rb_program_sdk.plugin_execute",
433
+ "args": { "message": "hello" }
434
+ }
435
+ }
436
+ ```
437
+
438
+ | Field | Type | Meaning |
439
+ |---|---|---|
440
+ | `name` | string, required | command name in the tree |
441
+ | `icon` | string, required | icon path |
442
+ | `iconClassName` | string | extra icon class |
443
+ | `funcName` | string | function the executor calls (`rb_program_sdk.plugin_execute` for Python backends) |
444
+ | `args` | object | default step args, passed with `plugin_id` to the backend |
445
+ | `disabled`, `syncGroupId`, `disableCopy`, `disableDelete`, `requireChildren`, `requireParent` | boolean | tree editing rules |
446
+ | `allowChildren`, `allowParents`, `notAllowChildren` | string[] | allowed / forbidden neighbour commands |
447
+ | `childrenCommand` | string | command created as a child |
448
+
449
+ ### 5.5 `variables` (PyFM)
450
+
451
+ Variables the plugin exposes to PyFM programs. The Program page's variable picker lists only declared ones.
452
+
453
+ ```json
454
+ { "name": "RBP_target_pose", "type": "array", "init": [0, 0, 0], "description": { "en": "Target pose" } }
455
+ ```
456
+
457
+ | Field | Required | Rule |
458
+ |---|---|---|
459
+ | `name` | yes | `RBP_` + letters/digits/underscore; unique per plugin |
460
+ | `type` | yes | `number`, `string`, `boolean`, `array` |
461
+ | `init` | yes | initial value matching `type` |
462
+ | `description` | no | string or i18n object — write one, the UI shows nothing otherwise |
463
+
464
+ ### 5.6 `ports`
465
+
466
+ A backend that opens a TCP listener must declare every port:
185
467
 
186
468
  ```json
187
469
  { "ports": [28600, 28601] }
@@ -189,80 +471,658 @@ A plugin backend that opens a TCP listener must declare every such port in
189
471
 
190
472
  | Rule | Enforced by |
191
473
  |---|---|
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) |
474
+ | Only `28600-28999`, `number[]`, non-empty when present, no duplicates | manifest validation |
475
+ | Reserved ports rejected: `502`, `2222`, `4840`, `5170-5179`, `8000-8999`, `10000`, `30004`, `34964`, `44818`, `50000-50005`, `50051` | manifest validation |
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): …`) |
477
+ | No two enabled plugins share a port | the robot, at install (HTTP 409) |
196
478
 
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.
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`.
200
482
 
201
- `muscat-pk`'s own dev plumbing (the plugin gateway on `10000`, the Zenoh dev
202
- peer port) is excluded from the check — it isn't the plugin's traffic.
483
+ ### 5.7 `runtime`
203
484
 
204
- Under `muscat-pk dev` declared ports are published to `127.0.0.1` from the local
205
- Muscat container (the backend shares its network), so you can reach the backend
206
- from the host.
485
+ | runtime | Source | Required fields |
486
+ |---|---|---|
487
+ | `{ "type": "none" }` | — | UI-only plugin |
488
+ | `{ "type": "python", "entry": "main.py" }` | `py/` | `entry` |
489
+ | `{ "type": "cpp", "entry": "<executable>" }` | `cpp/` | `entry` = built executable name |
490
+ | `{ "type": "ros2", "rosClient": "rclpy", "package": "<pkg>", "launch": "<pkg>_node" }` | `ros2_ws/` | `rosClient`, `package`, `launch` |
491
+ | `{ "type": "ros2", "rosClient": "rclcpp", "package": "<pkg>", "launch": "<pkg>_node" }` | `ros2_ws/` | same |
207
492
 
208
- ## Variables (pyfm)
493
+ ---
209
494
 
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:
495
+ ## 6. Plugin UI SDK (`useRb`)
213
496
 
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
- ]
497
+ Package `@rainbow-robotics/plugin-ui`:
498
+
499
+ | Import | Contents |
500
+ |---|---|
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.
506
+
507
+ ### 6.1 Entry component
508
+
509
+ Each top-level `ui/*.tsx` default-exports a React component. The host mounts it and provides `rb` through context.
510
+
511
+ ```tsx
512
+ import { useRb } from "@rainbow-robotics/plugin-ui/sdk";
513
+ import { RBLabelButton } from "@rainbow-robotics/plugin-ui/react";
514
+
515
+ export default function App() {
516
+ const rb = useRb();
517
+ return <RBLabelButton onClick={() => rb.action("grip", { width: 20 })}>Grip</RBLabelButton>;
220
518
  }
221
519
  ```
222
520
 
223
- | Field | Required | Rule |
521
+ Use plugin-ui components instead of native `<button>`, `<input>`, `<select>`, `<textarea>`, `<table>`, `<dialog>`. Common mappings: button → `RBLabelButton` / `RBIconButton`, text input → `RBTextInput` / `RBTextField`, number → `RBSideNumberInput`, select → `RBSelectBox` + `Option`, checkbox → `RBCheckBox`, switch → `RBSwitch`, slider → `RBSlider`, table → `RBTable`, modal → `RBModal`, tabs → `RBTabs`. `muscat-pk storybook` and `muscat-pk mcp` show props and examples.
522
+
523
+ ### 6.2 Messaging with the backend
524
+
525
+ | UI | Backend (Python) | Pattern |
224
526
  |---|---|---|
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. |
527
+ | `rb.pub(topic, payload)` | `@plugin.subscribe(topic)` | fire-and-forget UI → backend |
528
+ | `rb.sub(topic, handler)` | `plugin.publish(topic, payload)` | backend → UI stream |
529
+ | `rb.action(topic, payload?)` | `@plugin.serve(topic)` | UI request, backend reply |
530
+ | `rb.serve(topic, handler)` | `plugin.call(topic, payload=...)` | backend request, UI reply |
229
531
 
230
- Validated by `muscat-pk build` / `dev` alongside the rest of the manifest.
532
+ ```tsx
533
+ const rb = useRb();
231
534
 
232
- ## IDE support (Python)
535
+ await rb.pub("event", { clicked: true });
233
536
 
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).
537
+ const sub = await rb.sub<{ ready: boolean }>("state", (s) => setReady(s.ready));
538
+ // cleanup: sub.undeclare()
235
539
 
236
- If it can't be created (no local Python 3 or `uv` found), scaffolding prints a warning and continues — Docker-based dev still works without it:
540
+ const res = await rb.action<{ ok: boolean }, { width: number }>("grip", { width: 20 });
237
541
 
542
+ await rb.serve("tool/ping", (payload) => ({ pong: payload }));
238
543
  ```
239
- Could not create .venv (no local Python/uv?) — skipping, Docker dev still works.
544
+
545
+ - Topics are scoped to the plugin id: `"state"` → `{pluginId}/state`. A leading `/` makes it absolute (`"/broadcast/x"`).
546
+ - All four take generics for payload / result types.
547
+ - Unsubscribe in a `useEffect` cleanup:
548
+
549
+ ```tsx
550
+ useEffect(() => {
551
+ let sub: { undeclare(): Promise<void> } | undefined;
552
+ rb.sub("state", setState).then((s) => (sub = s));
553
+ return () => void sub?.undeclare();
554
+ }, [rb]);
240
555
  ```
241
556
 
242
- Install `uv` (recommended) or a system Python 3.12, then re-run `muscat-pk new` (or `muscat-pk type change`) to create it.
557
+ ### 6.3 Host dialogs and toast
243
558
 
244
- ## AI coding agent setup (`muscat-pk mcp`)
559
+ | API | Resolves |
560
+ |---|---|
561
+ | `rb.toast({ title?, message, type?, actionLabel?, handleActionClick?, closeOnActionClick?, duration?, onClose? })` | — (`message`: string or ReactElement; `type`: `info`, `danger`, `warning`, `caution`, `success`) |
562
+ | `rb.alert({ title?, message, buttonText? })` | when closed |
563
+ | `rb.confirm({ title?, message, buttonText?: { yes, no } })` | `true` / `false` |
564
+ | `rb.prompt({ title?, message, value?, placeholder?, buttonText?: { yes, no } })` | entered string, or `undefined` when cancelled |
565
+
566
+ ```tsx
567
+ if (await rb.confirm({ title: "Run", message: "Start the backend action?" })) {
568
+ await rb.action("run");
569
+ rb.toast({ message: "Started", type: "success" });
570
+ }
571
+ ```
245
572
 
246
- `muscat-pk mcp` runs an MCP server (stdio) that gives an AI coding agent grounded info about `rb-components`, design tokens, and the Python `rainbow-rb-sdk` — actual prop types, real Storybook examples, and live-introspected SDK signatures/docstrings, instead of the agent guessing from training data. Use it while working inside a plugin project scaffolded by `muscat-pk new`.
573
+ ### 6.4 Program page (`rb.program`)
247
574
 
248
- ### Claude Code
575
+ | API | Meaning |
576
+ |---|---|
577
+ | `rb.program.getSteps(scope?)` | main step tree of the program open on the Program page |
578
+ | `rb.program.setSteps(steps, scope?)` | replace it — an unsaved edit the user then saves |
579
+ | `rb.program.getCatalog(scope?)` | the loaded robot's command catalog (names, default args, nesting rules) |
580
+
581
+ ```ts
582
+ type PluginProgramStep = {
583
+ command: string; // "MoveJ", "Point", "Wait", "Repeat", "Condition", ...
584
+ args?: Record<string, unknown>;
585
+ summary?: string;
586
+ fields?: Record<string, unknown>; // step-level fields, e.g. SubTask's subTaskType
587
+ children?: PluginProgramStep[];
588
+ };
589
+ type PluginProgramScope = { taskId?: string; robotModel?: string };
590
+ ```
249
591
 
250
- Claude Code reads a project-scoped `.mcp.json` at the plugin's root:
592
+ Without `scope` the host uses the first main task. In a `program.monitor` tab `rb.data` is `{ point: "program.monitor", taskId, robotModel }` — pass it as `scope` so a multi-component robot hits the right task.
251
593
 
252
- ```json
253
- {
254
- "mcpServers": {
255
- "muscat": {
256
- "command": "muscat-pk",
257
- "args": ["mcp"]
258
- }
259
- }
594
+ ### 6.5 Step form save/restore (`program.panel`)
595
+
596
+ | Hook | Meaning |
597
+ |---|---|
598
+ | `useRbFormData<T>()` | values saved in this step, `undefined` for a new step |
599
+ | `useRbSave(fn)` | registers the getter the host calls on Save; always sees current state. Return value must be JSON |
600
+
601
+ ```tsx
602
+ import { useState } from "react";
603
+ import { RBSlider, useRbFormData, useRbSave } from "@rainbow-robotics/plugin-ui/react";
604
+
605
+ type Args = { width: number };
606
+
607
+ export default function App() {
608
+ const saved = useRbFormData<Args>();
609
+ const [args, setArgs] = useState<Args>({ width: 20, ...saved });
610
+ useRbSave(() => ({ args, summary: `${args.width}mm` }));
611
+ return (
612
+ <RBSlider
613
+ value={[args.width]}
614
+ min={0}
615
+ max={100}
616
+ onValueChange={(values: number[]) => setArgs({ width: values[0] ?? 0 })}
617
+ />
618
+ );
260
619
  }
261
620
  ```
262
621
 
263
- ### Codex
622
+ The saved `args` reach the backend's `on_execute(args)` (see [7.3](#73-pluginprogramcontext--program-command)).
623
+
624
+ ### 6.6 Browser storage (`rb.storage`, `useRbStorage`, `createRbStore`)
625
+
626
+ Keep browser-side plugin data only here. Everything is namespaced `muscat-plugin:<pluginId>`, and the host deletes it when the plugin is uninstalled — data stored any other way (raw `localStorage`, your own IndexedDB) is never cleaned up.
264
627
 
265
- Codex has no project-scoped config file — it reads a single, per-user `~/.codex/config.toml`. Add:
628
+ | Backend | Use |
629
+ |---|---|
630
+ | `rb.storage` (= `rb.storage.indexedDB`) | default; structured values (objects, arrays, `Date`, `Blob`, …) |
631
+ | `rb.storage.local` | `localStorage`, JSON values, survives reloads |
632
+ | `rb.storage.session` | `sessionStorage`, JSON values, gone when the tab closes |
633
+ | `rb.storage.cookie` | ~4 KB per value and sent with every request to the host — tiny values only |
634
+
635
+ Every backend has the same API:
636
+
637
+ | Method | Meaning |
638
+ |---|---|
639
+ | `get<T>(key)` | value, or `undefined` |
640
+ | `set(key, value)` | save |
641
+ | `delete(key)` | remove |
642
+ | `keys()` | this plugin's keys |
643
+ | `subscribe(listener, key?)` | `({ kind, key, value }) => void` on every change made through this API — this screen, other screens of the plugin (toolbar panel, Program tab, …) and other browser tabs. `value` is `undefined` after a delete. Returns the unsubscribe function |
644
+ | `getItem` / `setItem` / `removeItem` | zustand `StateStorage` (raw strings) |
645
+
646
+ ```tsx
647
+ const rb = useRb();
648
+ await rb.storage.set("lastRun", { at: new Date(), ok: true });
649
+ const last = await rb.storage.get<{ at: Date; ok: boolean }>("lastRun");
650
+ await rb.storage.local.set("theme", "dark");
651
+ ```
652
+
653
+ #### `useRbStorage` — one stored value as React state
654
+
655
+ ```tsx
656
+ import { RBTextInput, useRbStorage } from "@rainbow-robotics/plugin-ui/react";
657
+
658
+ type Draft = { name: string };
659
+
660
+ function Editor() {
661
+ const [draft, setDraft] = useRbStorage<Draft>("draft", { kind: "local", initial: { name: "" } });
662
+ // setDraft(next) saves; setDraft(undefined) deletes
663
+ return <RBTextInput value={draft?.name ?? ""} handleChange={(name: string) => void setDraft({ name })} />;
664
+ }
665
+ ```
666
+
667
+ - `useRbStorage<T>(key, { kind?, initial? })` → `[value, setValue]`. `kind` defaults to `"indexeddb"`.
668
+ - Re-renders when the value changes anywhere (other screens, other tabs).
669
+ - `value` is `initial` until the stored value has loaded.
670
+
671
+ #### `createRbStore` — persisted zustand-style store
672
+
673
+ Define once at module level, use from any screen of the plugin:
674
+
675
+ ```tsx
676
+ import { createRbStore, RBSlider } from "@rainbow-robotics/plugin-ui/react";
677
+
678
+ type Settings = {
679
+ speed: number;
680
+ points: string[];
681
+ setSpeed: (v: number) => void;
682
+ addPoint: (p: string) => void;
683
+ };
684
+
685
+ export const useSettings = createRbStore<Settings>(
686
+ (set, get) => ({
687
+ speed: 50,
688
+ points: [],
689
+ setSpeed: (speed) => set({ speed }),
690
+ addPoint: (p) => set((s) => ({ points: [...s.points, p] })),
691
+ }),
692
+ { name: "settings", storage: "local" },
693
+ );
694
+
695
+ function SpeedPanel() {
696
+ const speed = useSettings((s) => s.speed); // re-renders only when speed changes
697
+ const setSpeed = useSettings((s) => s.setSpeed);
698
+ return <RBSlider value={[speed]} min={0} max={100} onValueChange={(v: number[]) => setSpeed(v[0] ?? 0)} />;
699
+ }
700
+ ```
701
+
702
+ | Member | Meaning |
703
+ |---|---|
704
+ | `createRbStore<T>(init, { name, storage? })` | `init(set, get)` returns initial state + actions. `name` is the storage key; `storage` is a backend kind (default `"indexeddb"`) |
705
+ | `useStore()` / `useStore(selector)` | whole state / a slice (re-renders only when the slice changes) |
706
+ | `useStore.getState()` | current state, outside React |
707
+ | `useStore.setState(partial \| (s) => partial)` | merge a change, outside React |
708
+ | `useStore.subscribe((next, prev) => …)` | listen outside React; returns unsubscribe |
709
+
710
+ - State changes only through `set` / `setState`. Every change is saved and re-renders every screen and tab using the store.
711
+ - Functions (actions) are not saved; only data fields are.
712
+ - The saved data loads when the first component using the store mounts (the plugin id is known only then). Before that `getState()` returns the initial values.
713
+ - Changes are merged shallowly: set a new array/object (`[...s.points, p]`) instead of mutating it.
714
+
715
+ #### Using zustand `persist` directly
716
+
717
+ Every backend is a zustand `StateStorage`:
718
+
719
+ ```ts
720
+ import { persist, createJSONStorage } from "zustand/middleware";
721
+ persist(fn, { name: "settings", storage: createJSONStorage(() => rb.storage.local) });
722
+ ```
723
+
724
+ `zustand` is then a dependency of your plugin; `createRbStore` needs nothing extra.
725
+
726
+ ### 6.7 Other
727
+
728
+ | API | Meaning |
729
+ |---|---|
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 |
732
+ | `rb.rosPeers.get()` | PCs allowed to reach this plugin's ROS 2 nodes |
733
+ | `rb.rosPeers.set(ips)` | save the list and restart the backend; rejects an invalid IP |
734
+
735
+ ---
736
+
737
+ ## 7. Python plugin SDK
738
+
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.
740
+
741
+ ```python
742
+ from rainbow.rb_sdk import RBPluginCall, PluginProgramContext, RBManipulateSDK, RBAmrSDK, RBRby1SDK
743
+ from rainbow.rb_sdk.rb_sdk_cpp import ConnectOptions, PluginRouteError, RBPluginGatewayError
744
+ ```
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
+
748
+ Which API depends on the contribution point:
749
+
750
+ | Contribution point | Backend |
751
+ |---|---|
752
+ | `toolbar` / `lnb` / `move.panel` / `program.monitor` | `RBPluginCall` + robot SDKs (`RBManipulateSDK`, `RBAmrSDK`) called directly. No stop/pause/variable context |
753
+ | `program.panel` | `PluginProgramContext` — runs as a program step, tied to stop/pause and program variables |
754
+
755
+ ### 7.1 `RBPluginCall` — UI ↔ backend
756
+
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.
758
+
759
+ ```python
760
+ plugin = RBPluginCall()
761
+
762
+ @plugin.serve("grip") # UI: await rb.action("grip", { width })
763
+ def grip(payload=None):
764
+ width = (payload or {}).get("width", 20)
765
+ plugin.publish("state", {"last_grip_width": width})
766
+ return {"ok": True, "width": width}
767
+
768
+ @plugin.subscribe("event") # UI: rb.pub("event", ...)
769
+ def on_event(payload=None):
770
+ print(payload)
771
+
772
+ plugin.publish("state", {"ready": True}) # UI: rb.sub("state", ...)
773
+ res = plugin.call("tool/ping", payload={"hi": 1}, timeout=3.0) # UI: rb.serve("tool/ping", ...)
774
+ ```
775
+
776
+ | Member | Meaning |
777
+ |---|---|
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 |
779
+ | `subscribe(topic, callback=None)` | receive publishes |
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 |
784
+ | `plugin_id` | the plugin id in use (dev id under `muscat-pk dev`) |
785
+ | `connected` | transport connected |
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 |
787
+ | `router` | external endpoints (7.2) |
788
+
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.
790
+
791
+ ### 7.2 `plugin.router` — external endpoints
792
+
793
+ Exposes `common/plugin/{plugin_id}/{path}` over REST, Socket.IO, TCP, MQTT and WebRTC. The running Muscat (`common`) picks it up without a restart; it disappears when the plugin stops. Requires `rainbow-rb-sdk` ≥ 0.0.9.dev5 on the robot (older: `AttributeError`).
794
+
795
+ ```python
796
+ @plugin.router.on("/hello") # every transport
797
+ def hello(payload, meta):
798
+ return {"msg": f"hi {payload.get('name')}", "via": meta["transport"]}
799
+
800
+ @plugin.router.on("/admin", explicit_routes=["api"]) # REST only
801
+ def admin(payload, meta):
802
+ if not (meta.get("principal") or {}).get("is_admin"):
803
+ raise PluginRouteError(403, "admin only")
804
+ return {"ok": True}
805
+
806
+ @plugin.router.subscribe("/cmd", explicit_events=["tcp"])
807
+ def on_cmd(payload):
808
+ ...
809
+
810
+ plugin.router.emit("/state", {"busy": True})
811
+ ```
812
+
813
+ | API | Meaning | External side |
814
+ |---|---|---|
815
+ | `router.on(path, explicit_routes=None)` | request/response | calls `common/plugin/{id}/{path}` (REST: `POST`) |
816
+ | `router.subscribe(topic, explicit_events=None)` | receive external publishes | publishes to `common/plugin/{id}/{topic}` |
817
+ | `router.emit(topic, payload=None)` | push to external subscribers | subscribes to `common/plugin/{id}/{topic}` |
818
+
819
+ Handler contract:
820
+
821
+ - Signature `(payload, meta)`, `(payload)` or `()` — only declared arguments are injected. Sync or async.
822
+ - `payload`: what the caller sent (REST: JSON body merged with query params).
823
+ - `meta`: `{"transport": "api"|"socket"|"tcp"|"mqtt"|"webrtc", "principal": dict | None}`. With the login policy on, `principal` is the token subject. Role checks are the plugin's job.
824
+ - Return value = response body (JSON). `None` → empty.
825
+ - `raise PluginRouteError(code, message)` sets the status; any other exception → 500 with `str(exc)`.
826
+ - `subscribe` handlers take `(payload)` and return nothing.
827
+
828
+ Rules:
829
+
830
+ - Write only the path; the SDK adds `plugin_id`. Multi-segment paths work (`/tool/echo`). Declaring a path twice → `ValueError`.
831
+ - `explicit_routes` / `explicit_events`: `None` = every transport. Names `api | socket | tcp | mqtt | webrtc`; `subscribe` accepts only `tcp | mqtt | webrtc`. Unknown → `ValueError`. A disallowed transport gets 404.
832
+ - `on` and `subscribe` may share a name.
833
+
834
+ Calling from outside:
835
+
836
+ | Transport | How |
837
+ |---|---|
838
+ | REST | `POST /common/plugin/{id}/{path}`, JSON body |
839
+ | Socket.IO | emit `common/plugin/{id}/{path}`; reply in the ack |
840
+ | TCP | topic `common/plugin/{id}/{path}` request / publish |
841
+ | MQTT | request `common/plugin/{id}/{path}/__req__`; publish `common/plugin/{id}/{topic}` |
842
+ | WebRTC | data channel topic `common/plugin/{id}/{path}` |
843
+
844
+ ```bash
845
+ curl -s -X POST http://<robot>/common/plugin/com.example.my-plugin/hello \
846
+ -H 'Content-Type: application/json' -d '{"name":"kim"}'
847
+ # under muscat-pk dev (the dev id is printed at start)
848
+ curl -s -X POST http://localhost:3000/common/plugin/<dev-id>/hello -H 'Content-Type: application/json' -d '{}'
849
+ ```
850
+
851
+ | Situation | Status |
852
+ |---|---|
853
+ | No such path / plugin down / transport not allowed | 404 |
854
+ | No answer within 10 s | 504 |
855
+ | `common` ↔ gateway not connected | 503 |
856
+ | Handler raised | 500 (`PluginRouteError` → its code) |
857
+
858
+ Auth: with the login policy on a token is required (any role); REST uses the same Bearer token as other Muscat APIs. Off: no token.
859
+
860
+ Known limits:
861
+
862
+ - A path named `install` can't be called over REST (taken by the registry install route).
863
+ - MQTT echoes to the sender: `emit` + `subscribe` on the same topic receives its own push over `mqtt`. Drop `mqtt` from `explicit_events` or use separate topics.
864
+ - After restarting the plugin, external calls may return 504 for up to a minute, then 404, until the gateway drops the old socket.
865
+
866
+ ### 7.3 `PluginProgramContext` — program command
867
+
868
+ Used only for `program.panel`. When the program reaches the step, the executor calls `commandInfo.funcName` with `plugin_id` + the step args, and the SDK calls your `on_execute`.
869
+
870
+ ```python
871
+ import asyncio
872
+ from rainbow.rb_sdk import PluginProgramContext
873
+
874
+ ctx = PluginProgramContext(plugin_id="com.example.my-plugin")
875
+
876
+ @ctx.on_execute
877
+ async def handle(args: dict) -> None:
878
+ count = await ctx.get_variable("counter")
879
+ await ctx.set_variable("counter", (count or 0) + 1)
880
+ await ctx.set_variable("RBP_my_plugin_result", 42) # new variable → RBP_ required
881
+
882
+ for _ in range(100):
883
+ await ctx.check_stop() # stop → CancelledError, pause → waits until resumed
884
+ await asyncio.sleep(0.5)
885
+
886
+ await ctx.done() # always; the step never finishes otherwise
887
+
888
+ @ctx.on_executor_state_change
889
+ async def on_change(robot_model: str, state: str) -> None:
890
+ if state == "paused":
891
+ print(f"{robot_model} paused")
892
+
893
+ async def background() -> None:
894
+ while True:
895
+ await asyncio.sleep(1.0)
896
+ await ctx.set_variable("RBP_my_plugin_heartbeat", 1) # ignored when no program runs
897
+
898
+ async def main() -> None:
899
+ asyncio.create_task(background())
900
+ await ctx.start() # stays resident
901
+
902
+ asyncio.run(main())
903
+ ```
904
+
905
+ | Member | Meaning |
906
+ |---|---|
907
+ | `on_execute(fn)` | decorator; called once per execution of this step with the step args |
908
+ | `done()` | step finished — always call |
909
+ | `check_stop(robot_model=None)` | stop → `CancelledError`, pause → waits. Call every loop iteration |
910
+ | `get_variable(name, robot_model=None, plugin_id=None)` | read; `None` when no program runs |
911
+ | `set_variable(name, value)` | write to every running main task; ignored when no program runs |
912
+ | `executor_state(robot_model=None)` | state string for a model, or `{model: state}` for all |
913
+ | `is_paused(robot_model=None)` / `is_stopped(robot_model=None)` | state checks |
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 |
917
+ | `start()` / `stop()` / `pause()` | lifecycle |
918
+ | `in_execute`, `exec_robot_model`, `plugin_id`, `started`, `control_topic` | properties |
919
+
920
+ #### Variables
921
+
922
+ - Existing variable: written by name as-is — exchanging values with the program is the point.
923
+ - New variable: must start with `RBP_` (`RuntimeError` otherwise), inside or outside a step. Use `RBP_{plugin}_{name}` to avoid collisions; `RBP_` alone doesn't prevent two plugins overwriting each other.
924
+ - Declare in `manifest.json` → `variables` what the Program page should offer (5.5).
925
+
926
+ `set_variable` targets every running main task:
927
+
928
+ | When | Own program | Other robots' main tasks |
929
+ |---|---|---|
930
+ | During own step | immediate | at their next step boundary |
931
+ | Outside a step | next step boundary | same |
932
+
933
+ `get_variable` options:
934
+
935
+ ```python
936
+ await ctx.get_variable("counter") # first value across main tasks
937
+ await ctx.get_variable("counter", robot_model="C500920") # one robot
938
+ await ctx.get_variable("RBP_TEST", plugin_id="A.plugin") # what that plugin last wrote
939
+ ```
940
+
941
+ Each variable keeps a per-plugin record (`meta.writers`), so a colliding name is still recoverable: `get_variable(..., plugin_id=...)` from the SDK, `get_var("A.plugin", "RBP_TEST")` in a program expression. A write can rarely be lost when it races the worker's flush — re-check important values.
942
+
943
+ #### Executor state
944
+
945
+ States: `idle | running | paused | waiting | stopped | post_start | completed | error`. A model with no running task is `None` / absent.
946
+
947
+ - Published every 0.8 s on `muscat/executor/state` — up to ~1 s latency.
948
+ - With `IS_DEV=true` there is no publish; always `None`.
949
+ - `on_executor_state_change` fires once per per-model change; handler exceptions are only logged.
950
+
951
+ Scope of `check_stop` / `is_paused` / `is_stopped`:
952
+
953
+ 1. `robot_model` given → that model
954
+ 2. omitted inside `on_execute` → the model executing this step
955
+ 3. omitted elsewhere → all models
956
+
957
+ ### 7.4 Robot SDKs from the backend
958
+
959
+ ```python
960
+ manipulate = RBManipulateSDK()
961
+ manipulate.move.call_move_j(
962
+ robot_model="C500920",
963
+ target={"tar_values": [0, 0, 0, 0, 0, 0, 0], "tar_frame": -1, "tar_unit": 0},
964
+ )
965
+ ```
966
+
967
+ Add your own safety checks before moving the robot. Exact signatures: `muscat-pk mcp` (`search_rb_sdk`, `get_rb_sdk_api`).
968
+
969
+ ### 7.5 Remote dev (laptop → robot)
970
+
971
+ ```bash
972
+ export MUSCAT_ROBOT_HOST=192.168.1.100
973
+ export MUSCAT_ROBOT_TOKEN=<service token> # only when the robot's login policy is on
974
+ muscat-pk dev:back
975
+ ```
976
+
977
+ ```python
978
+ async def main():
979
+ host = os.getenv("MUSCAT_ROBOT_HOST")
980
+ if host:
981
+ opts = ConnectOptions()
982
+ opts.host = host
983
+ opts.token = os.getenv("MUSCAT_ROBOT_TOKEN", "")
984
+ manipulate.connect(opts)
985
+ register_handlers() # after connect() — connect() reopens the session and drops earlier handlers
986
+ ...
987
+ if host:
988
+ await manipulate.disconnect()
989
+ ```
990
+
991
+ The scaffolded `py/main.py` already follows this.
992
+
993
+ ---
994
+
995
+ ## 8. Other backend runtimes (C++ / ROS 2)
996
+
997
+ All non-`none` runtimes run in Docker during `dev:back`, `dev` and `build`.
998
+
999
+ ### 8.1 C++ (`cpp`)
1000
+
1001
+ - Source in `cpp/`, CMake build in Docker against the C++ SDK (apt `rainbow-rb-sdk-dev`).
1002
+ - CMake: `find_package(rb_sdk <ver> EXACT REQUIRED)` + `target_link_libraries(<target> rb_sdk::rb_sdk)` (C++20).
1003
+ - Headers: `<rainbow/rb_sdk/manipulate/rb_manipulate_sdk.hpp>`, `<rainbow/rb_sdk/plugin/rb_plugin.hpp>` (`rb_plugin::RBPluginCall`), `<rainbow/rb_sdk/program/plugin_program_context.hpp>` (`rb_plugin::PluginProgramContext`).
1004
+ - Dynamic link; `build` copies `librb_sdk.so` into `dist/<arch>/lib/`.
1005
+
1006
+ Building outside the container (Ubuntu 24.04):
1007
+
1008
+ ```bash
1009
+ curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/rb-sdk-apt.gpg
1010
+ echo 'deb [signed-by=/usr/share/keyrings/rb-sdk-apt.gpg] https://rainbowrobotics.github.io/rb-sdk stable main' | sudo tee /etc/apt/sources.list.d/rb-sdk.list
1011
+ sudo apt update && sudo apt install rainbow-rb-sdk-dev
1012
+ ```
1013
+
1014
+ ### 8.2 ROS 2 (`ros2` + `rclpy` / `rclcpp`)
1015
+
1016
+ - Source in `ros2_ws/src/<package>/`; `runtime.package` = colcon package, `runtime.launch` = node executable.
1017
+ - Relative topic names live under the plugin's namespace: id with `.` → `/`, `-` → `_` (`com.example.my-plugin` → `/com/example/my_plugin`). Use absolute names (`/cmd_vel`) for shared topics.
1018
+ - `RBPluginCall` works in an rclpy node too.
1019
+ - ROS discovery stays on the robot (`ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST`). Allow external PCs from the UI with `rb.rosPeers.set([...])`; Muscat restarts the backend with `ROS_STATIC_PEERS`. In dev the list is in `.muscat-dev/ros-peers.json`.
1020
+
1021
+ ---
1022
+
1023
+ ## 9. Development environment
1024
+
1025
+ ### 9.1 Local Muscat runtime
1026
+
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):
1028
+
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.
1034
+
1035
+ ```bash
1036
+ docker rm -f muscat-pk-muscat # stop it
1037
+ MUSCAT_PK_NO_LOCAL_MUSCAT=1 muscat-pk dev # don't start it
1038
+ ```
1039
+
1040
+ Its API docs are at `http://localhost:3000/common/docs`.
1041
+
1042
+ ### 9.2 Developing on the robot PC
1043
+
1044
+ On the robot PC Muscat already serves the gateway on `10000`; the CLI uses it and starts no local Muscat.
1045
+
1046
+ ### 9.3 Several developers on one robot
1047
+
1048
+ Against a remote robot, `dev` / `dev:back` run the plugin as `<manifest id>.dev-<user>-<random>` (random part kept in `~/.muscat-pk/dev-id`), so two machines don't answer each other's UI. UI, backend, ROS namespace and external routes all use the dev id. `--keep-id` uses the manifest id (for program `execute` or external clients) — then only one machine may connect.
1049
+
1050
+ ### 9.4 Local vs global install
1051
+
1052
+ If the project has its own `muscat-plugin-kit` in `node_modules`, any `muscat-pk` hands off to it, so the CLI, `vite` preset and `preview-host` are always the project's pinned version. `muscat-pk version` prints the copy that actually ran.
1053
+
1054
+ ### 9.5 Offline / air-gapped
1055
+
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).
1057
+
1058
+ ```bash
1059
+ docker save muscat-pk-python-dev muscat-pk-runtime | gzip > muscat-pk-images.tar.gz # online machine
1060
+ gunzip -c muscat-pk-images.tar.gz | docker load # offline machine
1061
+ ```
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
+
1065
+ Force a rebuild with `MUSCAT_PK_REBUILD_IMAGE=1`.
1066
+
1067
+ ### 9.6 IDE support (Python)
1068
+
1069
+ `muscat-pk new` creates `.venv` with `requirements.txt` installed — for editor linting only; the backend runs in Docker. Without `uv` or Python 3 it prints `Could not create .venv (no local Python/uv?) — skipping, Docker dev still works.`; install one and re-run `new` or `type change`.
1070
+
1071
+ ### 9.7 JavaScript dependencies
1072
+
1073
+ `build` runs the project's own Vite build, so packages must be installed. Don't `npm install --omit=dev` before building — `vite`, `typescript`, `tailwindcss`, `sass`, `less` are devDependencies. The ZIP ships no `node_modules`.
1074
+
1075
+ ---
1076
+
1077
+ ## 10. Styles
1078
+
1079
+ - `muscat()` auto-loads `ui/style.{css,scss,sass,less}` / `ui/styles.*` for every entry and `ui/{entry}.*` for a matching entry, and injects the CSS into the JS bundle.
1080
+ - Built selectors are scoped under `[data-muscat-plugin-id="..."]`, so plugin CSS can't touch the host or other plugins.
1081
+ - Use design-token classes (`bg-bg-*`, `text-fg-*`, `typo-*`) rather than hex colors.
1082
+
1083
+ Tailwind (default, no preflight):
1084
+
1085
+ ```css
1086
+ @layer theme, utilities;
1087
+ @import "tailwindcss/theme.css" layer(theme);
1088
+ @import "tailwindcss/utilities.css" layer(utilities);
1089
+ @source "./**/*.{ts,tsx}";
1090
+ ```
1091
+
1092
+ SCSS / Less: `npm i -D sass` (or `less`) and create `ui/style.scss` (`ui/style.less`).
1093
+
1094
+ Emotion `styled` works as is. For the `css` prop:
1095
+
1096
+ ```bash
1097
+ npm i @emotion/react @emotion/styled && npm i -D @emotion/babel-plugin
1098
+ ```
1099
+
1100
+ ```ts
1101
+ muscat({ react: { jsxImportSource: "@emotion/react", babel: { plugins: ["@emotion/babel-plugin"] } } })
1102
+ ```
1103
+
1104
+ and `"jsxImportSource": "@emotion/react"` in `tsconfig.json`. Emotion `Global` is not scoped — avoid it.
1105
+
1106
+ ---
1107
+
1108
+ ## 11. AI coding agent setup (`muscat-pk mcp`)
1109
+
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.
1111
+
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):
1120
+
1121
+ ```json
1122
+ { "mcpServers": { "muscat": { "command": "muscat-pk", "args": ["mcp"] } } }
1123
+ ```
1124
+
1125
+ Codex — `muscat-pk mcp --install codex` (on Windows it writes `muscat-pk.cmd`), or `~/.codex/config.toml`:
266
1126
 
267
1127
  ```toml
268
1128
  [mcp_servers.muscat]
@@ -270,13 +1130,56 @@ command = "muscat-pk"
270
1130
  args = ["mcp"]
271
1131
  ```
272
1132
 
273
- You don't run `muscat-pk mcp` yourself either way — the agent spawns and manages it automatically once it's registered.
1133
+ The agent starts the server itself.
1134
+
1135
+ ---
274
1136
 
275
- ## Package roles
1137
+ ## 12. Environment variables
276
1138
 
277
- | Package | Role |
1139
+ | Variable | Effect |
278
1140
  |---|---|
279
- | `@rainbow-robotics/plugin-ui` | UI runtime contract: React direct mount, `useRb()` context, host services |
280
- | `@rainbow-robotics/muscat-plugin-kit` | CLI, templates, build/pack, preview host |
1141
+ | `MUSCAT_ROBOT_HOST` | robot IP for `dev` / `dev:ui` / `dev:back` (skips the prompt) |
1142
+ | `MUSCAT_ROBOT_TOKEN` | service token when the robot's login policy is on |
1143
+ | `MUSCAT_PK_NO_LOCAL_MUSCAT=1` | don't start the local Muscat runtime |
1144
+ | `MUSCAT_PK_REBUILD_IMAGE=1` | rebuild Docker images |
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) |
1153
+ | `MUSCAT_PK_NO_UPDATE_CHECK=1` | skip the update check on `dev` / `dev:ui` |
1154
+ | `RB_REGISTRY` | npm registry for the update check (default `https://registry.npmjs.org`) |
1155
+ | `MUSCAT_PK_TAG` | install script channel / version |
1156
+ | `CI` | set: `dev:ui` doesn't open the browser |
1157
+ | `NO_COLOR=1` / `FORCE_COLOR` | output colors |
1158
+
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).
281
1160
 
282
- A plugin ZIP only contains the plugin's own code and assets. `@rainbow-robotics/plugin-ui`, `react`, and `react-dom` are provided by the host's (Muscat/robot) import map.
1161
+ ---
1162
+
1163
+ ## 13. Troubleshooting
1164
+
1165
+ | Symptom | Fix |
1166
+ |---|---|
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 |
1175
+ | backend stops with an undeclared port | add the port (28600-28999) to `ports` |
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` |
1177
+ | `vite not found — UI will not load` | run `npm install` in the project |
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?) |
1180
+ | step never finishes | `on_execute` must `await ctx.done()` |
1181
+ | Stop doesn't stop the plugin | call `await ctx.check_stop()` in every loop |
1182
+ | `RuntimeError` on `set_variable` | new variable names need `RBP_` |
1183
+ | `get_variable` returns `None` | no program running (or `IS_DEV=true` for executor state) |
1184
+ | external call 504 after restart | wait up to a minute for the gateway to drop the old socket |
1185
+ | a newer/older local install answers | expected — the project's local kit wins; `muscat-pk update` |