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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.ko.md +186 -60
  2. package/README.md +186 -60
  3. package/cli/README.md +3 -1
  4. package/cli/bin/muscat-pk.mjs +5 -1
  5. package/cli/src/completion.mjs +2 -0
  6. package/cli/src/dev.mjs +19 -2
  7. package/cli/src/devBack.mjs +1 -1
  8. package/cli/src/devUi.mjs +2 -1
  9. package/cli/src/muscatLocal.mjs +55 -19
  10. package/cli/src/sim.mjs +163 -0
  11. package/cli/src/util.mjs +2 -0
  12. package/cli/storybook-dist/assets/{Color-YHDXOIA2-Cp8LHsF-.js → Color-YHDXOIA2-DSB_yvex.js} +1 -1
  13. package/cli/storybook-dist/assets/{Docs-Kg1Im4XT.js → Docs-C_90bDRy.js} +1 -1
  14. package/cli/storybook-dist/assets/{Docs-UJwiVwO2.js → Docs-CbW9HZd4.js} +1 -1
  15. package/cli/storybook-dist/assets/{Docs-LeW_BIsw.js → Docs-CoskoRMT.js} +1 -1
  16. package/cli/storybook-dist/assets/{Docs-B2ox3o_G.js → Docs-Cy9XFeLJ.js} +1 -1
  17. package/cli/storybook-dist/assets/{Docs-By11aG_M.js → Docs-De1oc64k.js} +1 -1
  18. package/cli/storybook-dist/assets/{Docs-ik2AhpV0.js → Docs-klD3F_Ai.js} +1 -1
  19. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-DPFneFUi.js → DocsRenderer-CFRXHY34-Bkm4uV_D.js} +1 -1
  20. package/cli/storybook-dist/assets/{NxAvatar.stories-CwnK8AUF.js → NxAvatar.stories-Ctf5G2FQ.js} +1 -1
  21. package/cli/storybook-dist/assets/{NxButton.stories-ueYdosfQ.js → NxButton.stories-DeLObVSj.js} +1 -1
  22. package/cli/storybook-dist/assets/{NxNumberInput.stories-M5ZCLbrK.js → NxNumberInput.stories-LqoY0ed1.js} +1 -1
  23. package/cli/storybook-dist/assets/{NxPagination.stories-bxxUnHEL.js → NxPagination.stories-DI6bXuJM.js} +1 -1
  24. package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BHxB2kr5.js → NxPointerHoldButton.stories-3HSV1XmN.js} +1 -1
  25. package/cli/storybook-dist/assets/{NxRangeInput.stories-BKfr-fp4.js → NxRangeInput.stories-1pjgQVaG.js} +1 -1
  26. package/cli/storybook-dist/assets/{NxSelectBox.stories-CG8FuTBq.js → NxSelectBox.stories-bcQPZEuF.js} +1 -1
  27. package/cli/storybook-dist/assets/{NxSlideRange.stories-BRTFtMTL.js → NxSlideRange.stories-BosX4axk.js} +1 -1
  28. package/cli/storybook-dist/assets/{NxSwitch.stories-Bwuyzn-R.js → NxSwitch.stories-DxV6dS33.js} +1 -1
  29. package/cli/storybook-dist/assets/{NxTextArea.stories-COq2sGqu.js → NxTextArea.stories-CO545-LP.js} +1 -1
  30. package/cli/storybook-dist/assets/{NxTextInput.stories-CjsBLz05.js → NxTextInput.stories-BP0npai1.js} +1 -1
  31. package/cli/storybook-dist/assets/{RBAccordion.stories-BARwuZ0Q.js → RBAccordion.stories-fVR4Va2a.js} +1 -1
  32. package/cli/storybook-dist/assets/{RBAccordionGroup.stories-COrPxETW.js → RBAccordionGroup.stories-Du5bsckD.js} +1 -1
  33. package/cli/storybook-dist/assets/{RBAlert.stories-BQrkjZPt.js → RBAlert.stories-DNkMXUu6.js} +1 -1
  34. package/cli/storybook-dist/assets/{RBBadge.stories-BTsLL-lK.js → RBBadge.stories-C44pXt8D.js} +1 -1
  35. package/cli/storybook-dist/assets/{RBBattery.stories-DAeQdJbc.js → RBBattery.stories-DE2JHvfL.js} +1 -1
  36. package/cli/storybook-dist/assets/{RBCard.stories-HyhOE80R.js → RBCard.stories-CuIda7NI.js} +1 -1
  37. package/cli/storybook-dist/assets/{RBCheckBox.stories-KFk0WqX_.js → RBCheckBox.stories-D1qM1ayr.js} +1 -1
  38. package/cli/storybook-dist/assets/{RBCheckIconButton.stories-CXbbktF5.js → RBCheckIconButton.stories-e4gkliBO.js} +1 -1
  39. package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-DWfYz_9Z.js → RBCheckLabelButton.stories-CDzCtsMI.js} +1 -1
  40. package/cli/storybook-dist/assets/{RBChipTab.stories-JPEgVFAS.js → RBChipTab.stories-DBrc_3Mp.js} +1 -1
  41. package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-CTPuWXrQ.js → RBChipsBoxTab.stories-hlrsHjKY.js} +1 -1
  42. package/cli/storybook-dist/assets/{RBColorIcons.stories-DiFRZ5jk.js → RBColorIcons.stories-BHUvpkdu.js} +1 -1
  43. package/cli/storybook-dist/assets/{RBCommand.stories-Pf1HAVIR.js → RBCommand.stories-azAF41NP.js} +1 -1
  44. package/cli/storybook-dist/assets/{RBConfirm.stories-D1cEPUXk.js → RBConfirm.stories-BMNjwbmT.js} +1 -1
  45. package/cli/storybook-dist/assets/{RBDateInput.stories-RPYpCjig.js → RBDateInput.stories-D0VezDzv.js} +1 -1
  46. package/cli/storybook-dist/assets/{RBDialog.stories-DJH-0XSR.js → RBDialog.stories-AOJ975Fy.js} +1 -1
  47. package/cli/storybook-dist/assets/{RBDivider.stories-uEj1vOgK.js → RBDivider.stories-NSvnj43Z.js} +1 -1
  48. package/cli/storybook-dist/assets/{RBDrawer.stories-BiMzElb2.js → RBDrawer.stories-Du0oTvD1.js} +1 -1
  49. package/cli/storybook-dist/assets/{RBDropdownMenu.stories-Da_TBkla.js → RBDropdownMenu.stories-Hsxve4PW.js} +1 -1
  50. package/cli/storybook-dist/assets/{RBEmptyView.stories-BHeKGfPS.js → RBEmptyView.stories-BlwHx3zp.js} +1 -1
  51. package/cli/storybook-dist/assets/{RBIconButton.stories-NEsRFoYp.js → RBIconButton.stories-DAPDY63x.js} +1 -1
  52. package/cli/storybook-dist/assets/{RBIcons.stories-CcZ-TTLS.js → RBIcons.stories-W5ZUV2L1.js} +1 -1
  53. package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-B0wpdQyR.js → RBInlineLabelButton.stories-C99j0Y3g.js} +1 -1
  54. package/cli/storybook-dist/assets/{RBJoyStick.stories-DEgBou3K.js → RBJoyStick.stories-Du0H1iAS.js} +1 -1
  55. package/cli/storybook-dist/assets/{RBLabelButton.stories-DT8drIsF.js → RBLabelButton.stories-BlvHaU_Z.js} +1 -1
  56. package/cli/storybook-dist/assets/{RBLight.stories-Dh-aoeyG.js → RBLight.stories-BY6DKrre.js} +1 -1
  57. package/cli/storybook-dist/assets/{RBMenubar.stories-D-H7UqVh.js → RBMenubar.stories-D2PEGpM9.js} +1 -1
  58. package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-hlclv6jI.js → RBMultiRangeInput.stories-Bm2qSmjq.js} +1 -1
  59. package/cli/storybook-dist/assets/{RBPagination.stories-DWCeoZ_S.js → RBPagination.stories-DDleJa5N.js} +1 -1
  60. package/cli/storybook-dist/assets/{RBPopover.stories-C514M09_.js → RBPopover.stories-27YxPSFG.js} +1 -1
  61. package/cli/storybook-dist/assets/{RBPrompt.stories-BSItvfiT.js → RBPrompt.stories-BFaIOctr.js} +1 -1
  62. package/cli/storybook-dist/assets/{RBRadioBox.stories-BBb4YbFU.js → RBRadioBox.stories-BroDar0t.js} +1 -1
  63. package/cli/storybook-dist/assets/{RBRadioButton.stories-CXyOGJCv.js → RBRadioButton.stories-M3O-wX7V.js} +1 -1
  64. package/cli/storybook-dist/assets/{RBRangeDateInput.stories-qvhubu0e.js → RBRangeDateInput.stories-CWlM_D5X.js} +1 -1
  65. package/cli/storybook-dist/assets/{RBResizable.stories-CZKeyGjH.js → RBResizable.stories-ui8X1qnc.js} +1 -1
  66. package/cli/storybook-dist/assets/{RBSelectBox.stories-CRKP3ybf.js → RBSelectBox.stories-C8T6cfA0.js} +1 -1
  67. package/cli/storybook-dist/assets/{RBSheet.stories-sCM3ZJUa.js → RBSheet.stories-BCOo_Zcc.js} +1 -1
  68. package/cli/storybook-dist/assets/{RBSideNumberInput.stories-Bb7gw8RF.js → RBSideNumberInput.stories-CU2_MVlJ.js} +1 -1
  69. package/cli/storybook-dist/assets/{RBSlider.stories-23OAQU5z.js → RBSlider.stories-DgOp1TFc.js} +1 -1
  70. package/cli/storybook-dist/assets/{RBSpinner.stories-2xcQdcmt.js → RBSpinner.stories-qLScY3zZ.js} +1 -1
  71. package/cli/storybook-dist/assets/{RBSwitch.stories-BACUaJm_.js → RBSwitch.stories-SXSY1QMb.js} +1 -1
  72. package/cli/storybook-dist/assets/{RBTab.stories-CDHE3sVs.js → RBTab.stories-BzXXTsXP.js} +1 -1
  73. package/cli/storybook-dist/assets/{RBTable.stories-C0Cfhfl7.js → RBTable.stories-DHTeRNVk.js} +1 -1
  74. package/cli/storybook-dist/assets/{RBTableRow.stories-BF8kIkpp.js → RBTableRow.stories-DRNRsd15.js} +1 -1
  75. package/cli/storybook-dist/assets/{RBTabs.stories-BdJWRq1o.js → RBTabs.stories-DP0noBLp.js} +1 -1
  76. package/cli/storybook-dist/assets/{RBTextArea.stories-CGy2xeYH.js → RBTextArea.stories-TkaejfxJ.js} +1 -1
  77. package/cli/storybook-dist/assets/{RBTextField.stories-IfUOzxCM.js → RBTextField.stories-B6VpCGyt.js} +1 -1
  78. package/cli/storybook-dist/assets/{RBTextInput.stories-VjXNFo4R.js → RBTextInput.stories-D3hZ08e-.js} +1 -1
  79. package/cli/storybook-dist/assets/{RBToast.stories-BzcobblV.js → RBToast.stories-BQ2wS2U0.js} +1 -1
  80. package/cli/storybook-dist/assets/{RBToggle.stories-DuLE5mKv.js → RBToggle.stories-GVOjGqo3.js} +1 -1
  81. package/cli/storybook-dist/assets/{RBToggleGroup.stories-CuR0N0GJ.js → RBToggleGroup.stories-pzx0btOD.js} +1 -1
  82. package/cli/storybook-dist/assets/{RBTooltip.stories--aAgNC8_.js → RBTooltip.stories-jufB6fa1.js} +1 -1
  83. package/cli/storybook-dist/assets/{SideNumberInput-DyWQxYUj.js → SideNumberInput-BYlmNHEw.js} +1 -1
  84. package/cli/storybook-dist/assets/{entry-preview-BTw3Jh8E.js → entry-preview-DY0ZrYNE.js} +1 -1
  85. package/cli/storybook-dist/assets/{iframe-BqPQIkPQ.js → iframe-LwKb1w63.js} +2 -2
  86. package/cli/storybook-dist/assets/{index-k_VkBflK.js → index-BsAhjDKO.js} +3 -3
  87. package/cli/storybook-dist/assets/{preview-Cog7BnbD.js → preview-BJ0FvQZh.js} +2 -2
  88. package/cli/storybook-dist/assets/{preview-C3ctYQQj.js → preview-DiY2_30w.js} +2 -2
  89. package/cli/storybook-dist/assets/{preview-CigNHo-I.css → preview-kTZPgP6D.css} +1 -1
  90. package/cli/storybook-dist/assets/{react-resizable-panels-Dp9zi-o3.css → react-resizable-panels-HsFbxPbn.css} +1 -1
  91. package/cli/storybook-dist/iframe.html +1 -1
  92. package/cli/storybook-dist/project.json +1 -1
  93. package/package.json +3 -3
  94. /package/cli/storybook-dist/assets/{react-resizable-panels.browser-8hC8dr-6.js → react-resizable-panels.browser-DuxpZLkF.js} +0 -0
package/README.ko.md CHANGED
@@ -111,6 +111,7 @@ muscat-pk build # dist/ + 배포용 ZIP
111
111
  | `muscat-pk build` | 빌드, Python 의존성 lock, wheel 동봉, ZIP |
112
112
  | `muscat-pk update` | kit, `plugin-ui`, `preview-host` 업데이트 |
113
113
  | `muscat-pk sdk <list\|update [version]>` | `rainbow-rb-sdk` 버전 관리 |
114
+ | `muscat-pk sim list` | `dev --sim`에 쓸 수 있는 로봇 시뮬레이터 목록 |
114
115
  | `muscat-pk storybook` | 번들된 rb-components Storybook 보기 |
115
116
  | `muscat-pk mcp` | AI 코딩 에이전트용 MCP 서버 |
116
117
  | `muscat-pk uninstall` | CLI 삭제 |
@@ -134,9 +135,15 @@ muscat-pk new [dir] [--type python|cpp|ros2|ros2-cpp] [--pm npm|yarn|pnpm|bun]
134
135
  | `--pm` | 프론트 패키지 매니저. Yarn 워크스페이스 안이면 기본 `yarn`, 아니면 `npm` |
135
136
  | `--author` / `--email` / `--description` | manifest 필드 (email은 manifest 필수값) |
136
137
  | `--yes` | 질문 없이 진행. 지정 안 한 값은 기본값 |
137
- | `--no-install` | `npm install` 생략 |
138
+ | `--no-install` | `<pm> install`, IDE `.venv`, C++ dev 이미지 준비를 생략 |
139
+ | `--help` | 사용법 출력 |
138
140
 
139
- Python 프로젝트는 IDE 전용 `.venv`도 만들어집니다 (9.6 참고).
141
+ 플러그인 id는 `com.example.<dir>`(소문자)로 만들어지며 `manifest.json`에서 바꿉니다. `new`는 `.gitignore`, `AGENTS.md`, `CLAUDE.md`, `.mcp.json`(11장 참고)도 만들고, `git init` 후 첫 커밋을 남깁니다 (원치 않으면 `.git`을 지우세요).
142
+
143
+ `--no-install`이 아니면:
144
+
145
+ - python / ros2 (rclpy): IDE 전용 `.venv`를 만듭니다 (9.6 참고).
146
+ - cpp / ros2-cpp: 에디터가 SDK 헤더를 찾도록 C++ dev 이미지를 빌드합니다 (Docker가 없으면 경고 후 건너뜀).
140
147
 
141
148
  ### 3.2 `type change`
142
149
 
@@ -144,25 +151,67 @@ Python 프로젝트는 IDE 전용 `.venv`도 만들어집니다 (9.6 참고).
144
151
  muscat-pk type change [--to python|cpp|ros2|ros2-cpp] [--readme]
145
152
  ```
146
153
 
147
- `manifest.json`의 `runtime`을 바꾸고 새 백엔드 파일을 추가합니다. 이전 타입의 파일(`cpp/`, `ros2_ws/` 등)은 지우지 않습니다. `--readme`를 주면 README도 새 타입용으로 교체합니다.
154
+ | 옵션 | 의미 |
155
+ |---|---|
156
+ | `--to` | 새 타입. 생략하면 번호로 물어봄 |
157
+ | `--readme` | 묻지 않고 `README.md`를 새 타입용으로 교체 (기존 파일 → `README.md.bak`) |
158
+
159
+ `manifest.json`의 `runtime`을 바꾸고, 해당 디렉터리가 없으면 새 백엔드 파일(`cpp/` 또는 `ros2_ws/`)을 추가합니다. 이전 타입의 파일(`cpp/`, `ros2_ws/` 등)은 지우지 않습니다. Python ↔ C++ 백엔드를 오갈 때는 README 교체를 물어봅니다 (`--readme`면 예로 처리). C++ 타입으로 바꾸면 `new`처럼 C++ dev 이미지를 준비합니다.
148
160
 
149
161
  ### 3.3 `dev`, `dev:ui`, `dev:back`
150
162
 
151
163
  ```bash
152
- muscat-pk dev [--robot-ip <ip>] [--keep-id]
153
- muscat-pk dev:ui [--robot-ip <ip>] [--port <n>] [--keep-id]
154
- muscat-pk dev:back [--keep-id]
164
+ muscat-pk dev [--robot-ip <ip>] [--robot-token <token>] [--keep-id] [--sim <model>]
165
+ [--port <n>] [--no-open]
166
+ muscat-pk dev:ui [--robot-ip <ip>] [--port <n>] [--keep-id] [--no-open]
167
+ muscat-pk dev:back [--robot-ip <ip>] [--robot-token <token>] [--keep-id]
155
168
  ```
156
169
 
157
170
  | 명령 | 실행되는 것 |
158
171
  |---|---|
159
- | `dev` | 로봇이 없으면 로컬 Muscat 런타임, 그리고 `dev:ui` + `dev:back` |
160
- | `dev:ui` | 플러그인 Vite 개발 서버 + preview-host `http://localhost:4180/plugin-dev` (사용 중이면 다음 빈 포트) |
172
+ | `dev` | 로봇이 없거나 로봇에 Muscat이 없으면 로컬 Muscat 런타임, 그리고 `dev:ui` + `dev:back` |
173
+ | `dev:ui` | 플러그인 Vite 개발 서버 + preview-host `http://localhost:4180` (사용 중이면 다음 빈 포트). 브라우저로 엶 |
161
174
  | `dev:back` | 로봇 환경과 같은 Docker 컨테이너에서 백엔드 실행. Python은 `py/**/*.py`가 바뀌면 재시작 (watchfiles) |
162
175
 
163
- - 로봇 IP는 `--robot-ip`, `MUSCAT_ROBOT_HOST`, 또는 프롬프트 입력 순입니다 (비우면 로컬).
176
+ | 옵션 | 의미 |
177
+ |---|---|
178
+ | `--robot-ip` | 개발 대상 로봇. 없으면 `MUSCAT_ROBOT_HOST`, 그것도 없으면 프롬프트 (비우면 로봇 없음) |
179
+ | `--robot-token` | 로그인 정책이 켜진 로봇의 service token. 없으면 `MUSCAT_ROBOT_TOKEN`, 그것도 없으면 프롬프트 (비우면 없음) |
180
+ | `--keep-id` | dev id 대신 manifest id로 실행 (9.3 참고) |
181
+ | `--sim` | `dev` 전용, 로봇 없음: 로봇 시뮬레이터 실행 (아래). 로봇 IP 프롬프트를 건너뜀 |
182
+ | `--port` | preview-host 포트 (기본 `4180`, 사용 중이면 다음 빈 포트) |
183
+ | `--no-open` | 브라우저를 열지 않음 (CI이거나 TTY가 아니어도 열지 않음) |
184
+
185
+ - dev 환경은 PC당 하나입니다. 백엔드 컨테이너, 로컬 Muscat과 그 포트를 같이 쓰기 때문입니다. 두 번째 `dev` / `dev:ui` / `dev:back`은 실행 중인 것을 멈출지 묻습니다 (`Stop it and start this one? [y/N]`).
164
186
  - 원격 로봇에 붙으면 플러그인이 dev id로 실행됩니다 (9.3 참고). `--keep-id`는 manifest id를 그대로 씁니다.
165
- - 시작 전에 manifest를 검증하고, 선언하지 않은 포트를 열면 백엔드를 멈춥니다 (5.6 참고).
187
+ - 시작 전에 `dev`와 `dev:back`이 `manifest.json`의 `variables`, `ports`를 검사합니다 (오류면 중단). `dev:back`은 나머지 manifest 오류도 경고로 출력합니다. 전체 검증은 `build`에서 합니다. 선언하지 않은 포트를 열면 백엔드를 멈춥니다 (5.6 참고).
188
+ - Muscat이 도는 로봇에 붙으면 UI가 그 로봇의 Muscat 화면이 되고 백엔드는 그 gateway를 씁니다. 로컬에 `muscat-dev` 컨테이너가 있거나 `--sim`이면 로컬 실제 Muscat 화면, 아니면 mock 프리뷰입니다.
189
+ - `dev` / `dev:ui`는 새 kit이 배포돼 있으면 알려 줍니다 (`MUSCAT_PK_NO_UPDATE_CHECK=1`로 생략).
190
+ - `dev`를 멈추면(Ctrl+C) 백엔드 컨테이너와, 자신이 띄운 로컬 Muscat·시뮬레이터를 지웁니다.
191
+
192
+ #### 로봇 없이 개발 (`--sim`)
193
+
194
+ 로봇 IP 없이 `dev --sim <기종>`을 실행하면 로컬 Muscat 옆에 로봇 시뮬레이터를 같이 띄웁니다. 플러그인 SDK의 로봇 호출이 실제 coreSW에 닿습니다.
195
+
196
+ ```bash
197
+ muscat-pk sim list # 한 줄에 "<name>\t<실행되는 것>"
198
+ muscat-pk dev --sim 500920
199
+ muscat-pk dev --sim rby1-m_v1.2
200
+ ```
201
+
202
+ | `--sim` | 함께 뜨는 Muscat 서비스 | 시뮬레이터 |
203
+ |---|---|---|
204
+ | `500880`, `500920` | common, log, manipulate | `rainbowroboticsofficial/cobot-sim` 1개 |
205
+ | `RBY2` | common, log, manipulate | cobot-sim 3개 (C500880, C501880, C502880) |
206
+ | `rby1-a`, `rby1-m`, `rby1-ub` (`rby1-m_v1.2`처럼 버전 지정 가능) | common, log, rby1 | `rainbowroboticsofficial/rby1-sim` + Xvfb (로컬 빌드) |
207
+
208
+ - 시뮬레이터 기종이 로컬 Muscat의 로봇 모델로 등록됩니다(`500880`은 C500880, `500920`은 C500920, `rby1-m`은 m). UI는 mock 프리뷰가 아니라 실제 Muscat 화면(대시보드, 컴포넌트, Move, Log, Settings)으로 뜹니다.
209
+ - 시뮬레이터는 `--sim`을 줄 때만 뜹니다. 그 밖의 값이면 `--sim takes one of: 500880, 500920, RBY2, rby1-a, rby1-m, rby1-ub (rby1 also takes rby1-m_v1.2)` 오류가 납니다. manifest의 `targetModels`에 없는 기종이면 경고만 합니다.
210
+ - 시뮬레이터 이미지는 Docker Hub의 최신 태그를 씁니다 (rby1에 버전을 주면 그 버전 태그). 오프라인이면 이미 받아 둔 이미지를 씁니다.
211
+ - rby1-sim은 화면을 계속 렌더링하므로 CPU를 1코어 정도 씁니다. 상한은 `MUSCAT_PK_RBY1_CPUS`(기본 1.5)입니다.
212
+ - `muscat-dev` 같은 다른 Muscat이 떠 있으면 시뮬레이터를 붙이지 않습니다.
213
+ - amr 시뮬레이터는 아직 없습니다.
214
+ - 로컬 Muscat, 시뮬레이터, 백엔드는 로봇처럼 모두 `--ipc host`로 돕니다. 따라서 zenoh SHM과 `shm_open`이 서로 통합니다.
166
215
 
167
216
  ### 3.4 `build`
168
217
 
@@ -172,18 +221,23 @@ muscat-pk build [--arch amd64|arm64|both]
172
221
 
173
222
  순서:
174
223
 
175
- 1. `manifest.json`의 새 버전을 물어봅니다 (비우면 그대로).
176
- 2. manifest를 검증합니다.
177
- 3. `contributes[].ui` 엔트리마다 `vite build` → `dist/<entry>.js`.
178
- 4. 백엔드:
179
- - python: `requirements.txt`에서 버전이 없는 줄을 PyPI 최신 버전으로 고정하고, `dist/requirements.lock`(`uv pip compile`)을 만들고, arch별 wheel을 `dist/wheels/`에 넣습니다.
180
- - cpp: Docker에서 CMake 빌드 → `dist/<arch>/<entry>` + `dist/<arch>/lib/librb_sdk.so`.
181
- - ros2: Docker에서 colcon 빌드 → `dist/ros2_ws/install-<arch>/`.
182
- 5. `dist/`를 프로젝트 루트의 `<프로젝트 디렉터리>-<version>.zip`으로 묶습니다.
224
+ 1. `manifest.json`의 새 버전을 물어봅니다 (비우면 그대로, TTY일 때만).
225
+ 2. 대상 아키텍처를 묻고(`--arch`가 없을 때) 같은 버전의 기존 ZIP을 지웁니다. 빌드가 실패해도 예전 ZIP이 남지 않습니다.
226
+ 3. manifest를 검증하고(5장 전체), `contributes[].ui`마다 `ui/<entry>.tsx`(또는 `.ts` / `.jsx` / `.js`)가 있는지 확인합니다.
227
+ 4. `contributes[].ui` 엔트리마다 `vite build` → `dist/<entry>.js`. `contributes`가 없는 플러그인은 UI를 빌드하지 않습니다. Node.js 20 이상이 없으면(바이너리 CLI) `Build the plugin without its UI? [y/N]`를 묻고, 예면 `dist/manifest.json`에서 `contributes`를 뺍니다.
228
+ 5. `manifest.json`, `assets/`, `README.md`, `README.<lang>.md`, `CHANGELOG.md`, `py/`, `requirements.txt`를 `dist/`에 복사합니다.
229
+ 6. 백엔드:
230
+ - python / ros2 (rclpy): `requirements.txt`를 `dist/requirements.lock`으로 해석하고(`uv pip compile`, Python 3.12, 고정된 줄 중 pre-release가 있으면 pre-release 허용), arch별 바이너리 wheel(cp312, `--only-binary=:all:`)을 `dist/wheels/`에 받습니다. 맞는 wheel이 없는 패키지가 있으면 빌드가 멈춥니다.
231
+ - cpp: Docker에서 CMake Release 빌드 → `dist/<arch>/<entry>` + `dist/<arch>/lib/` (`librb_sdk.so`). CMake 타깃 이름이 `runtime.entry`와 같아야 합니다.
232
+ - ros2: Docker에서 colcon 빌드 → `dist/ros2_ws/install/` (rclpy, 한 번) 또는 `dist/ros2_ws/install-<arch>/` (rclcpp, arch별).
233
+ 7. `installedSize`(`wheels/`를 뺀 `dist/` 바이트 수)를 `manifest.json`과 `dist/manifest.json`에 씁니다.
234
+ 8. `dist/`를 프로젝트 루트의 `<프로젝트 디렉터리>-<version>.zip`으로 묶습니다.
183
235
 
184
236
  | 옵션 | 의미 |
185
237
  |---|---|
186
- | `--arch` | 대상 아키텍처. TTY면 물어보고, 아니면 `both` |
238
+ | `--arch` | `amd64`(`x86_64`), `arm64`(`aarch64`), `both`. TTY면 물어보고(기본 both), 아니면 `both` |
239
+
240
+ `none`을 제외한 모든 런타임은 Docker가 필요합니다. 다른 arch를 빌드하려면 Docker 에뮬레이션이 필요하며, 네이티브 Linux에서는 `docker run --privileged --rm tonistiigi/binfmt --install <arch>`로 한 번 켜 둡니다.
187
241
 
188
242
  ### 3.5 `update`
189
243
 
@@ -191,25 +245,41 @@ muscat-pk build [--arch amd64|arm64|both]
191
245
  muscat-pk update # 현재 프로젝트만 (로컬 설치가 있을 때)
192
246
  muscat-pk update --global # 현재 프로젝트 + 전역 CLI
193
247
  muscat-pk update dev # dev 채널 따라가기
248
+ muscat-pk update 1.2.3 # 특정 버전 (--tag <tag> / --version <v>도 가능)
194
249
  ```
195
250
 
251
+ | 옵션 | 의미 |
252
+ |---|---|
253
+ | `[tag]` | npm dist-tag 또는 버전 (`latest`, `dev`, `1.2.3`). 생략하면 `latest`. dev 빌드에서는 dev 유지(기본)와 stable 전환 중 물어봄 |
254
+ | `--global` | 프로젝트 로컬 설치에서 실행해도 전역 CLI까지 업데이트 |
255
+
196
256
  | 실행 위치 | 업데이트 대상 |
197
257
  |---|---|
198
- | 로컬 설치가 있는 프로젝트 안 | 그 프로젝트의 `muscat-plugin-kit` + `plugin-ui` |
258
+ | 로컬 설치가 있는 프로젝트 안 | 그 프로젝트의 `muscat-plugin-kit` + `plugin-ui` (+ `package.json`에 있으면 `preview-host`) |
199
259
  | 위와 같고 `--global` | 위 + 전역 CLI |
200
- | 그 밖 | 전역 CLI (프로젝트 안이면 로컬 의존성도) |
260
+ | 그 밖 | 전역 CLI (npm `-g` 또는 S3 바이너리), 프로젝트 안이면 로컬 의존성도 |
261
+
262
+ - 프로젝트 로컬 설치가 실행한 CLI보다 오래됐으면, 실행한 CLI가 대신 프로젝트를 업데이트합니다 (프로젝트만).
263
+ - 패키지는 원래 있던 `dependencies` / `devDependencies` 구역에 그대로 둡니다. 패키지 매니저는 `packageManager` 필드나 lock 파일로 판단합니다.
264
+ - 예전 프로젝트 구조는 `[y/N]` 확인 후 옮깁니다: `src/` → `ui/` (+ `vite.config.ts`, `tsconfig.json`), `main.py` → `py/main.py`, `backend/` → `py/`.
265
+ - `.mcp.json`, `AGENTS.md`, `CLAUDE.md`가 없으면 추가하고, 있으면 그대로 둡니다.
266
+ - Windows에서는 새 바이너리가 다음 실행 때 적용됩니다.
201
267
 
202
- 업데이트 후 제거된 API를 아직 쓰는 UI 코드를 목록으로 보여 줍니다. 목록대로 고치거나 `muscat-pk mcp`의 `check_project`로 확인하세요.
268
+ 업데이트 후 새 `plugin-ui`에 맞춰 고쳐야 할 코드를 목록으로 보여 줍니다 (``This project needs changes for the updated plugin-ui. Fix these before `muscat-pk dev` / `build`:``). 목록대로 고치거나 `muscat-pk mcp`의 `check_project`로 확인하세요.
203
269
 
204
270
  ### 3.6 `sdk`
205
271
 
206
272
  ```bash
207
- muscat-pk sdk list # 배포된 rainbow-rb-sdk 버전 목록
208
- muscat-pk sdk update [version] # 생략하면 최신
273
+ muscat-pk sdk list # 최신 배포 버전 12개
274
+ muscat-pk sdk update [version] # 생략하면 최신 stable
209
275
  ```
210
276
 
211
- - python: `requirements.txt`의 `rainbow-rb-*==` 핀을 모두 같은 버전으로 바꿉니다 (서로 정확히 같은 버전을 요구함).
212
- - C++ (`cpp`, `ros2-cpp`): `CMakeLists.txt`의 `find_package(rb_sdk <ver> EXACT REQUIRED)`를 바꿉니다.
277
+ | 백엔드 | `list` 출처 | `update`가 바꾸는 것 |
278
+ |---|---|---|
279
+ | python, ros2 (rclpy) | PyPI `rainbow-rb-sdk` | `requirements.txt`의 `rainbow-rb-*==` 핀을 모두 같은 버전으로 (서로 정확히 같은 버전을 요구함). 없으면 `rainbow-rb-sdk==<ver>` 추가. `.venv`가 있으면 갱신하고, dev 컨테이너는 다음 `dev` / `dev:back`에서 설치 |
280
+ | C++ (`cpp`, `ros2-cpp`) | apt `rainbow-rb-sdk-dev` (stable, 숫자 버전만) | `cpp/CMakeLists.txt` / `ros2_ws/src/*/CMakeLists.txt`의 `find_package(rb_sdk <ver> EXACT REQUIRED)`. dev 이미지는 다음 `build` / `dev:back`에서 그 버전으로 다시 빌드 |
281
+
282
+ C++ 버전은 숫자와 점만 됩니다 (`"<v>" is not a CMake version (digits and dots only)`).
213
283
 
214
284
  ### 3.7 `storybook`, `mcp`, `completion`, `version`
215
285
 
@@ -218,9 +288,19 @@ muscat-pk storybook [--port 6007] [--rebuild]
218
288
  muscat-pk mcp [--install codex]
219
289
  muscat-pk completion [bash|zsh|fish|powershell]
220
290
  muscat-pk version
291
+ muscat-pk uninstall
221
292
  ```
222
293
 
223
- `completion`은 셸 Tab 자동완성 스크립트를 출력합니다. 한 번 등록하면 `muscat-pk d<Tab>`에서 `dev`, `dev:ui`, `dev:back`이 후보로 나옵니다. 스크립트를 출력만 하므로 아래처럼 셸 rc 파일에 붙여 넣어야 합니다. `[shell]`은 `bash`, `zsh`, `fish`, `powershell` 중 하나이고 생략하면 `$SHELL`을 따릅니다. Windows `cmd.exe`는 자동완성을 지원하지 않으니 PowerShell, Git Bash, WSL을 쓰세요. 편의 기능이라 쓰지 않아도 됩니다.
294
+ | 명령 / 옵션 | 의미 |
295
+ |---|---|
296
+ | `storybook --port` | 서비스 포트 (기본 `6007`). 사용 중이면 `port <n> is in use — pass a free one with --port <n>.` |
297
+ | `storybook --rebuild` | Storybook을 다시 빌드 (repo 체크아웃에서만. 배포된 kit에는 빌드된 것이 들어 있음) |
298
+ | `mcp` | stdio MCP 서버 (11장). 실제 서버(`mcp --serve`)로 중계하고 kit이 업데이트되면 서버를 재시작하므로, 실행 중인 에이전트 세션도 새 도구를 씁니다 |
299
+ | `mcp --install codex` | `~/.codex/config.toml`에 서버 등록 (이미 있으면 그대로) |
300
+ | `version` | 실제로 실행된 쪽의 버전 (`-v`, `--version`도 같음) |
301
+ | `uninstall` | npm 전역: `npm uninstall -g`, 바이너리: 실행 파일 삭제. 프로젝트 로컬 설치는 프로젝트 패키지 매니저로 지웁니다 |
302
+
303
+ `completion`은 셸 Tab 자동완성 스크립트를 출력합니다. 한 번 등록하면 `muscat-pk d<Tab>`에서 `dev`, `dev:ui`, `dev:back`이 후보로 나옵니다. 스크립트를 출력만 하므로 아래처럼 셸 rc 파일에 붙여 넣어야 합니다. `[shell]`은 `bash`, `zsh`, `fish`, `powershell` 중 하나이고 생략하면 `$SHELL`을 따릅니다 (없으면 Windows는 `powershell`, 그 밖은 `bash`). Windows `cmd.exe`는 자동완성을 지원하지 않으니 PowerShell, Git Bash, WSL을 쓰세요. 편의 기능이라 쓰지 않아도 됩니다.
224
304
 
225
305
  | OS / 셸 | 자동완성 설정 |
226
306
  |---|---|
@@ -243,12 +323,18 @@ my-plugin/
243
323
  ├─ ros2_ws/ # colcon 워크스페이스 — ros2 / ros2-cpp 타입
244
324
  ├─ assets/icon.svg
245
325
  ├─ manifest.json
246
- ├─ requirements.txt # python 의존성 (빌드 시 버전 고정)
326
+ ├─ requirements.txt # python 의존성 (빌드 시 dist/requirements.lock으로 고정)
247
327
  ├─ package.json # UI 의존성 (배포물에 포함 안 됨)
248
328
  ├─ vite.config.ts # muscat() 프리셋 필수
249
- └─ README.md # dist/에 복사됨
329
+ ├─ tsconfig.json
330
+ ├─ AGENTS.md, CLAUDE.md # AI 코딩 에이전트 규칙 (CLAUDE.md → @AGENTS.md)
331
+ ├─ .mcp.json # Claude Code용 `muscat-pk mcp` 등록
332
+ ├─ .gitignore
333
+ └─ README.md # dist/에 복사됨 (README.<lang>.md, CHANGELOG.md도)
250
334
  ```
251
335
 
336
+ 스캐폴드된 `requirements.txt`는 `rainbow-rb-sdk`, `rainbow-rb-flat-buffers`, `rainbow-rb-zenoh`를 같은 버전으로 고정합니다 (`muscat-pk sdk update`로 변경). 생성물이라 커밋하지 않는 것: `node_modules/`, `dist/`, `.muscat-dev/`, `.venv/`, `*.zip`, `cpp/build*`, `ros2_ws/{install,build*,log}`.
337
+
252
338
  `vite.config.ts`:
253
339
 
254
340
  ```ts
@@ -265,7 +351,7 @@ ZIP에는 플러그인 자체 코드와 에셋만 들어갑니다. `@rainbow-rob
265
351
 
266
352
  ## 5. manifest.json
267
353
 
268
- `muscat-pk dev`, `dev:back`, `build`가 검증합니다.
354
+ 전체 검증은 `muscat-pk build`가 합니다 (오류: `manifest.json errors: …`). `dev` / `dev:back`은 시작 전에 `variables`와 `ports`를 검사하고, `dev:back`은 나머지를 경고로 출력합니다.
269
355
 
270
356
  ### 5.1 전체 예시
271
357
 
@@ -303,11 +389,12 @@ ZIP에는 플러그인 자체 코드와 에셋만 들어갑니다. `@rainbow-rob
303
389
  | `company` | 아니오 | 문자열 |
304
390
  | `description` | 예 | 문자열 또는 다국어 객체 |
305
391
  | `icons` | 예 | SVG 경로 문자열, 또는 PNG/JPG면 `{ "16", "48", "128" }` (세 개 모두) |
306
- | `targetModels` / `targetCategorys` | 둘 중 하나 이상 | `"all"` 또는 비어 있지 않은 배열. 카테고리: `manipulate`, `amr`, `rby1`, `vision` |
307
- | `contributes` | 예 | 5.3 |
392
+ | `targetModels` / `targetCategorys` | 둘 중 하나 이상 | `"all"` 또는 비어 있지 않은 문자열 배열. 카테고리: `manipulate`, `amr`, `rby1`, `vision` |
393
+ | `contributes` | 아니오 | 5.3. 없거나 `[]`이면 백엔드 전용 플러그인 (화면 없음, UI 빌드 없음) |
308
394
  | `variables` | 아니오 | 5.5 |
309
395
  | `ports` | 아니오 | 5.6 |
310
396
  | `runtime` | 예 | 5.7 |
397
+ | `installedSize` | — | `build`가 기록 (바이트) |
311
398
 
312
399
  다국어 객체: `en` 필수, 허용 키 `ko`, `en`, `ja`, `zh`, `vi`, `th`, `es`.
313
400
 
@@ -322,7 +409,7 @@ ZIP에는 플러그인 자체 코드와 에셋만 들어갑니다. `@rainbow-rob
322
409
  | `opens` | toolbar 전용: `popover`, `panel`, `page` |
323
410
  | `commandInfo` | `program.panel`이면 필수 (5.4) |
324
411
 
325
- `toolbar` 항목이 최소 하나 있어야 합니다.
412
+ `contributes`가 비어 있지 않으면 `toolbar` 항목이 최소 하나 있어야 합니다 (호스트가 플러그인을 표시하는 곳). 각 `ui`에는 같은 이름의 `ui/<entry>.tsx`(또는 `.ts` / `.jsx` / `.js`)가 있어야 하며 `build`에서 확인합니다.
326
413
 
327
414
  | point | 표시 위치 |
328
415
  |---|---|
@@ -384,12 +471,14 @@ TCP 포트를 여는 백엔드는 모든 포트를 선언해야 합니다:
384
471
 
385
472
  | 규칙 | 검사 주체 |
386
473
  |---|---|
387
- | `28600-28999`만, `number[]` | manifest 검증 |
474
+ | `28600-28999`만, `number[]`, 있으면 비어 있으면 안 됨, 중복 불가 | manifest 검증 |
388
475
  | 예약 포트 금지: `502`, `2222`, `4840`, `5170-5179`, `8000-8999`, `10000`, `30004`, `34964`, `44818`, `50000-50005`, `50051` | manifest 검증 |
389
- | 열린 포트는 전부 선언돼야 함 | `dev` / `dev:back` 런타임 모니터가 선언 안 된 포트 발견 즉시 백엔드 중지 |
476
+ | 열린 포트는 전부 선언돼야 함 | `dev` / `dev:back` 런타임 모니터가 선언 안 된 포트 발견 즉시 백엔드 중지 (`backend opened undeclared TCP port(s): …`) |
390
477
  | 활성 플러그인끼리 포트 중복 불가 | 로봇, 설치 시 (HTTP 409) |
391
478
 
392
- kit 자체 개발용 포트(gateway `10000`, Zenoh dev peer 포트)는 검사에서 제외됩니다. `muscat-pk dev`에서는 선언한 포트가 `127.0.0.1`에 노출되며, 호스트에서 이미 쓰는 포트는 경고 후 건너뜁니다.
479
+ - kit 자체 개발용 포트(gateway `10000`, MediaMTX `8554` / `8889` / `8189`, Zenoh dev peer 포트)와 OS가 배정한 loopback 포트는 검사에서 제외됩니다.
480
+ - 백엔드가 다른 서비스와 network namespace를 같이 쓰면 플러그인 포트를 구분할 수 없어서, 네이티브 Linux(`--network host`)와 백엔드가 로컬 Muscat 컨테이너에 붙는 경우(로봇 없는 일반적인 `dev`)에는 모니터가 꺼집니다. 그래도 선언하세요 — 로봇이 설치 시 확인합니다.
481
+ - 선언한 포트는 `127.0.0.1`에 노출되며(`dev`에서는 로컬 Muscat에서), 호스트에서 이미 쓰는 포트는 경고 후 건너뜁니다. 백엔드는 이 목록을 `RB_PLUGIN_PORTS`로 받습니다.
393
482
 
394
483
  ### 5.7 `runtime`
395
484
 
@@ -409,8 +498,11 @@ kit 자체 개발용 포트(gateway `10000`, Zenoh dev peer 포트)는 검사에
409
498
 
410
499
  | import 경로 | 내용 |
411
500
  |---|---|
412
- | `@rainbow-robotics/plugin-ui/react` | Muscat 컴포넌트(`RB*`), `useRb`, `useRbFormData`, `useRbSave`, `useRbStorage`, `createRbStore` |
413
- | `@rainbow-robotics/plugin-ui/sdk` | 컴포넌트를 뺀 같은 훅들, `RbSdk` / `RbStore` / `RbSetState` 타입 |
501
+ | `@rainbow-robotics/plugin-ui/react` | Muscat 컴포넌트(`RB*`), `useRb`, `useRbFormData`, `useRbSave`, `useRbStorage`, `createRbStore`, `RbSdk` / `RbStore` / `RbSetState` 타입 |
502
+ | `@rainbow-robotics/plugin-ui/sdk` | 컴포넌트를 뺀 같은 훅들, 그리고 `RbContext`와 `RbNotifyType` 타입 |
503
+ | `@rainbow-robotics/plugin-ui/manifest` | manifest 타입(`PluginManifest`, `PluginContribution`, `PluginVariable`, `PluginRuntime`), `CONTRIBUTION_POINTS`, `VARIABLE_TYPES`, `validateManifest` |
504
+
505
+ Muscat 호스트 밖(단위 테스트, Storybook)에서는 `useRb()`가 stub을 돌려줍니다: 호출하면 `[plugin-ui] rb.<name> called outside of a host context`를 로그로 남기고 빈 값으로 resolve합니다.
414
506
 
415
507
  ### 6.1 엔트리 컴포넌트
416
508
 
@@ -561,7 +653,7 @@ await rb.storage.local.set("theme", "dark");
561
653
  #### `useRbStorage` — 저장된 값 하나를 React state로
562
654
 
563
655
  ```tsx
564
- import { useRbStorage } from "@rainbow-robotics/plugin-ui/react";
656
+ import { RBTextInput, useRbStorage } from "@rainbow-robotics/plugin-ui/react";
565
657
 
566
658
  type Draft = { name: string };
567
659
 
@@ -581,7 +673,7 @@ function Editor() {
581
673
  모듈 최상위에서 한 번 만들고 플러그인의 어느 화면에서든 씁니다:
582
674
 
583
675
  ```tsx
584
- import { createRbStore } from "@rainbow-robotics/plugin-ui/react";
676
+ import { createRbStore, RBSlider } from "@rainbow-robotics/plugin-ui/react";
585
677
 
586
678
  type Settings = {
587
679
  speed: number;
@@ -635,7 +727,8 @@ persist(fn, { name: "settings", storage: createJSONStorage(() => rb.storage.loca
635
727
 
636
728
  | API | 의미 |
637
729
  |---|---|
638
- | `rb.data` | 호스트가 마운트 시 넘기는 데이터 (`program.monitor` scope, 저장된 폼 값) |
730
+ | `rb.data` | 호스트가 마운트 시 넘기는 데이터 (`program.monitor` scope, 저장된 폼 값). 새로 마운트되면 `undefined` |
731
+ | `rb.onSave(getPayload)` | `useRbSave`의 저수준 형태. 등록 해제 함수 반환 |
639
732
  | `rb.rosPeers.get()` | 이 플러그인의 ROS 2 노드에 접근을 허용한 PC 목록 |
640
733
  | `rb.rosPeers.set(ips)` | 목록 저장 후 백엔드 재시작. 잘못된 IP면 reject |
641
734
 
@@ -646,11 +739,12 @@ persist(fn, { name: "settings", storage: createJSONStorage(() => rb.storage.loca
646
739
  Python 백엔드(`runtime.type: "python"`, 엔트리 `py/main.py`)는 `rainbow-rb-sdk`를 씁니다. 버전은 `requirements.txt`에 고정하고 `muscat-pk sdk update`로 바꿉니다. 실행 시 SDK는 로봇의 플러그인 컨테이너가 제공합니다.
647
740
 
648
741
  ```python
649
- from rainbow.rb_sdk import RBPluginCall, PluginProgramContext, RBManipulateSDK, RBAmrSDK
650
- from rainbow.rb_sdk.plugin import PluginRouteError
651
- from rainbow.rb_sdk.rb_sdk_cpp import ConnectOptions
742
+ from rainbow.rb_sdk import RBPluginCall, PluginProgramContext, RBManipulateSDK, RBAmrSDK, RBRby1SDK
743
+ from rainbow.rb_sdk.rb_sdk_cpp import ConnectOptions, PluginRouteError, RBPluginGatewayError
652
744
  ```
653
745
 
746
+ `rainbow.rb_sdk`는 `RBPluginCall`, `PluginProgramContext`, `RBManipulateSDK`, `RBAmrSDK`, `RBRby1SDK`, `RBProgramSDK`, `RBBaseSDK`, `rby1`(벤더링된 rby1-sdk 타입)을 export합니다. 나머지(`ConnectOptions`, `PluginRouter`, `PluginRouteError`, `RBPluginGatewayError` 등)는 `rainbow.rb_sdk.rb_sdk_cpp`에 있습니다.
747
+
654
748
  어떤 API를 쓸지는 contribution point로 정해집니다:
655
749
 
656
750
  | contribution point | 백엔드 |
@@ -660,7 +754,7 @@ from rainbow.rb_sdk.rb_sdk_cpp import ConnectOptions
660
754
 
661
755
  ### 7.1 `RBPluginCall` — UI ↔ 백엔드
662
756
 
663
- 내부 전용입니다 (UI ↔ 백엔드, 플러그인 ↔ 플러그인). Muscat 밖에서는 접근할 수 없습니다. 플러그인 id는 환경 변수 `RB_PLUGIN_ID`에서 가져옵니다.
757
+ 내부 전용입니다 (UI ↔ 백엔드, 플러그인 ↔ 플러그인). Muscat 밖에서는 접근할 수 없습니다. `RBPluginCall(plugin_id=None)`: id는 인자, 없으면 환경 변수 `RB_PLUGIN_ID`, 그것도 없으면 `PLUGIN_ID`입니다 (앞뒤 `/` 제거). 프로세스·플러그인 id마다 인스턴스가 하나라서 `RBPluginCall()`을 다시 불러도 같은 객체가 나옵니다.
664
758
 
665
759
  ```python
666
760
  plugin = RBPluginCall()
@@ -681,18 +775,18 @@ res = plugin.call("tool/ping", payload={"hi": 1}, timeout=3.0) # UI: rb.serve(
681
775
 
682
776
  | 멤버 | 의미 |
683
777
  |---|---|
684
- | `serve(keyexpr, handler=None)` | 요청/응답 핸들러. 데코레이터 또는 직접 호출. 반환값이 응답 |
778
+ | `serve(keyexpr, handler=None)` | 요청/응답 핸들러. 데코레이터 또는 직접 호출. 반환값이 응답. 키마다 핸들러 하나 — 다시 serve하면 이전 것을 교체 |
685
779
  | `subscribe(topic, callback=None)` | publish 수신 |
686
- | `publish(topic, payload=None)` | 구독자에게 발행 |
687
- | `call(keyexpr, *, payload, timeout=3.0)` | 상대편에 요청하고 응답 대기 |
688
- | `unserve(keyexpr=None)` | serve 핸들러 하나 / 전체 해제 |
689
- | `unsubscribe(topic=None, callback=None)` | 구독 해제 |
780
+ | `publish(topic, payload=None)` | 구독자에게 발행. 연결이 끊긴 동안에는 조용히 버려짐 |
781
+ | `call(keyexpr, *, payload, timeout=3.0)` | 상대편에 요청하고 응답 대기. 연결이 없거나 시간 초과면 `RBPluginGatewayError` |
782
+ | `unserve(keyexpr=None)` / `undeclare_queryable(keyexpr=None)` | serve 핸들러 하나 / 전체 해제 |
783
+ | `unsubscribe(topic=None, callback=None)` / `unsubscribe_topic(topic=None, callback=None)` | 콜백 하나, 토픽의 구독 전체, 또는 모든 구독 해제 |
690
784
  | `plugin_id` | 사용 중인 플러그인 id (`muscat-pk dev`에서는 dev id) |
691
785
  | `connected` | 연결 여부 |
692
- | `data_dir` | 영구 데이터 디렉터리 (접근 시 생성). 업데이트·재시작에도 유지되고 플러그인 삭제 시 제거. 설치 디렉터리(cwd)는 업데이트마다 교체되므로 상태·캐시·설정은 여기에 저장 |
786
+ | `data_dir` | 영구 데이터 디렉터리 (접근 시 생성): `RB_PLUGIN_DATA_DIR`(Muscat이 설정), 없으면 `/app/data/runtime/plugins/data/{plugin_id}`. 업데이트·재시작에도 유지되고 플러그인 삭제 시 제거. 설치 디렉터리(cwd)는 업데이트마다 교체되므로 상태·캐시·설정은 여기에 저장 |
693
787
  | `router` | 외부 엔드포인트 (7.2) |
694
788
 
695
- payload는 JSON 직렬화 가능해야 합니다. 핸들러는 sync/async 모두 됩니다.
789
+ payload는 JSON 직렬화 가능해야 합니다. 핸들러는 sync/async 모두 됩니다. 토픽은 UI와 같은 규칙으로 격리됩니다: `"state"` → `{plugin_id}/state`, 앞에 `/`를 붙이면 절대 경로.
696
790
 
697
791
  ### 7.2 `plugin.router` — 외부 엔드포인트
698
792
 
@@ -818,6 +912,8 @@ asyncio.run(main())
818
912
  | `executor_state(robot_model=None)` | 모델 하나의 상태 문자열, 또는 전체 `{model: state}` |
819
913
  | `is_paused(robot_model=None)` / `is_stopped(robot_model=None)` | 상태 확인 |
820
914
  | `on_executor_state_change(fn)` | 데코레이터. 상태가 바뀔 때마다 `(robot_model, state)`로 1회 |
915
+ | `set_execute_handler(fn)` / `set_state_change_handler(fn)` | 위 두 등록을 데코레이터 없이 |
916
+ | `begin_execution(robot_model=None)` / `end_execution()` | SDK가 `on_execute` 앞뒤로 부르는 실행 수명 주기 hook. 일반 플러그인에서는 필요 없음 |
821
917
  | `start()` / `stop()` / `pause()` | 수명 주기 |
822
918
  | `in_execute`, `exec_robot_model`, `plugin_id`, `started`, `control_topic` | 속성 |
823
919
 
@@ -928,16 +1024,21 @@ sudo apt update && sudo apt install rainbow-rb-sdk-dev
928
1024
 
929
1025
  ### 9.1 로컬 Muscat 런타임
930
1026
 
931
- 로봇 IP 없이 `dev` / `dev:ui`를 돌리면 `localhost:3000`에 Muscat이 필요합니다:
1027
+ 로봇 IP 없이(또는 Muscat이 없는 로봇에 붙어) `dev` / `dev:ui`를 돌리면 `localhost:3000`(API)과 `:10000`(플러그인 gateway)에 Muscat이 필요합니다:
932
1028
 
933
- - 이미 `:10000`을 쓰는 컨테이너(`muscat`, `muscat-dev` 등)가 있으면 재사용하고,
934
- - 없으면 `muscat-pk-muscat`을 띄웁니다 — `common`만 돌리는 S3 런타임 이미지이며 `~/.muscat-pk/muscat/`에 캐시됩니다. 실행 사이에도 계속 떠 있습니다.
1029
+ - 이미 `:10000`을 노출하는 컨테이너(`muscat`, `muscat-dev` 등)가 있으면 재사용합니다. kit이 띄우지 않은 컨테이너는 플러그인 포트를 노출하지 않습니다.
1030
+ - 컨테이너 없이 그 포트를 이미 쓰는 Muscat(로봇 PC, host networking)이 있으면 그대로 씁니다.
1031
+ - 없으면 `muscat-pk-muscat`을 띄웁니다 — `common`(+ `--sim`에 필요한 서비스)과 MediaMTX(`8554`, `8889`, `8189`)를 돌리는 S3 런타임 이미지(`muscat-pk-runtime:<version>`, `version.json` 기준)이며 `~/.muscat-pk/muscat/`에 캐시됩니다. 노출할 플러그인 포트나 서비스가 바뀌면 다시 만들고, Muscat 새 버전이 나오면 이전 이미지를 교체합니다.
1032
+ - `dev`는 종료할 때 자신이 띄운 `muscat-pk-muscat`을 지웁니다. `dev:ui` 단독으로 띄운 것은 계속 떠 있습니다.
1033
+ - 백엔드는 로컬 Muscat의 network namespace에 붙으므로 `execute`와 SDK가 로봇에서처럼 닿습니다.
935
1034
 
936
1035
  ```bash
937
1036
  docker rm -f muscat-pk-muscat # 중지
938
1037
  MUSCAT_PK_NO_LOCAL_MUSCAT=1 muscat-pk dev # 띄우지 않기
939
1038
  ```
940
1039
 
1040
+ API 문서는 `http://localhost:3000/common/docs`에 있습니다.
1041
+
941
1042
  ### 9.2 로봇 PC에서 직접 개발
942
1043
 
943
1044
  로봇 PC에서는 Muscat이 이미 `10000`에서 gateway를 열고 있으므로 CLI가 그것을 쓰고 로컬 Muscat을 띄우지 않습니다.
@@ -952,13 +1053,15 @@ MUSCAT_PK_NO_LOCAL_MUSCAT=1 muscat-pk dev # 띄우지 않기
952
1053
 
953
1054
  ### 9.5 오프라인 / 폐쇄망
954
1055
 
955
- Docker 이미지는 없을 때만 빌드합니다 (재빌드는 캐시가 있어도 레지스트리에 접속함).
1056
+ Docker 이미지는 없거나 kit의 Dockerfile / 빌드 인자가 바뀌었을 때만 빌드합니다 (재빌드는 캐시가 있어도 레지스트리에 접속함).
956
1057
 
957
1058
  ```bash
958
1059
  docker save muscat-pk-python-dev muscat-pk-runtime | gzip > muscat-pk-images.tar.gz # 인터넷 되는 PC
959
1060
  gunzip -c muscat-pk-images.tar.gz | docker load # 오프라인 PC
960
1061
  ```
961
1062
 
1063
+ C++ / ROS 2 프로젝트는 `muscat-pk-cpp-dev` / `muscat-pk-ros2-dev`(그리고 `build`가 쓰는 arch별 `-amd64` / `-arm64` 이미지)도 필요합니다. 오프라인에서 로컬 Muscat은 캐시된 `version.json`을, 시뮬레이터는 이미 받아 둔 이미지를 씁니다.
1064
+
962
1065
  강제 재빌드: `MUSCAT_PK_REBUILD_IMAGE=1`.
963
1066
 
964
1067
  ### 9.6 IDE 지원 (Python)
@@ -1006,13 +1109,20 @@ muscat({ react: { jsxImportSource: "@emotion/react", babel: { plugins: ["@emotio
1006
1109
 
1007
1110
  stdio MCP 서버입니다. rb-components(실제 props, Storybook 예시), 디자인 토큰, Python `rainbow-rb-sdk`(실제 시그니처/docstring)를 에이전트에 제공하고, 오래된 프로젝트 업그레이드용 `check_project`도 있습니다.
1008
1111
 
1009
- Claude Code — 플러그인 루트의 `.mcp.json`:
1112
+ | 도구 | 반환 |
1113
+ |---|---|
1114
+ | `list_components`, `get_component_props`, `get_component_example` | plugin-ui 컴포넌트, prop 타입, 실제 Storybook 사용 예 |
1115
+ | `list_design_tokens`, `search_design_tokens` | 디자인 토큰 Tailwind class (`bg-bg-*`, `text-fg-*`, `typo-*`) |
1116
+ | `list_rb_sdk`, `search_rb_sdk`, `get_rb_sdk_api` | Python `rainbow-rb-sdk`와 C++ SDK의 클래스 / 시그니처 / docstring |
1117
+ | `check_project` | 오래된 프로젝트가 `muscat-pk update` 후 고쳐야 할 것 |
1118
+
1119
+ Claude Code — 플러그인 루트의 `.mcp.json` (`new`가 만들고, 없으면 `update`가 추가):
1010
1120
 
1011
1121
  ```json
1012
1122
  { "mcpServers": { "muscat": { "command": "muscat-pk", "args": ["mcp"] } } }
1013
1123
  ```
1014
1124
 
1015
- Codex — `muscat-pk mcp --install codex`, 또는 `~/.codex/config.toml`:
1125
+ Codex — `muscat-pk mcp --install codex` (Windows에서는 `muscat-pk.cmd`로 기록), 또는 `~/.codex/config.toml`:
1016
1126
 
1017
1127
  ```toml
1018
1128
  [mcp_servers.muscat]
@@ -1033,12 +1143,20 @@ args = ["mcp"]
1033
1143
  | `MUSCAT_PK_NO_LOCAL_MUSCAT=1` | 로컬 Muscat 런타임을 띄우지 않음 |
1034
1144
  | `MUSCAT_PK_REBUILD_IMAGE=1` | Docker 이미지 재빌드 |
1035
1145
  | `MUSCAT_PK_RB_SDK_VERSION` | C++ 이미지에 넣을 C++ SDK 버전 (`MUSCAT_PK_REBUILD_IMAGE=1`과 함께) |
1146
+ | `MUSCAT_PK_NO_BUILD_CACHE=1` | Docker 이미지를 `--no-cache`로 빌드 (CI) |
1147
+ | `MUSCAT_PK_HOST_NETWORK=0` | 네이티브 Linux에서도 `--network host` 대신 Docker Desktop(bridge) 경로 사용 (CI) |
1148
+ | `MUSCAT_PK_RBY1_CPUS` | rby1 시뮬레이터 CPU 상한 (기본 `1.5`) |
1149
+ | `MUSCAT_PK_MUSCAT_S3` | 로컬 Muscat 런타임 다운로드 위치 (기본 `https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat/main`) |
1150
+ | `MUSCAT_HOST_AGENT_DISABLE=1` | 호스트 Zenoh byte-proxy를 띄우지 않음 (macOS / Windows / WSL에서 원격 로봇에 붙을 때) |
1151
+ | `MUSCAT_PROXY_PORT` | 그 proxy의 포트 (기본 `47100`) |
1152
+ | `MUSCAT_ZENOH_ADVERTISE_HOST` / `MUSCAT_ZENOH_PORT` | 원격 로봇이 되접속할 LAN IP / 포트 (기본: 로봇으로 가는 경로의 IP / `53917` 또는 다음 빈 포트) |
1036
1153
  | `MUSCAT_PK_NO_UPDATE_CHECK=1` | `dev` / `dev:ui`의 업데이트 확인 생략 |
1037
1154
  | `RB_REGISTRY` | 업데이트 확인용 npm 레지스트리 (기본 `https://registry.npmjs.org`) |
1038
1155
  | `MUSCAT_PK_TAG` | 설치 스크립트 채널 / 버전 |
1039
- | `NO_COLOR` / `FORCE_COLOR` | 출력 색상 |
1156
+ | `CI` | 설정되면 `dev:ui`가 브라우저를 열지 않음 |
1157
+ | `NO_COLOR=1` / `FORCE_COLOR` | 출력 색상 |
1040
1158
 
1041
- 백엔드 컨테이너 안: `RB_PLUGIN_ID` (플러그인 id / dev id).
1159
+ 백엔드 컨테이너 안: `RB_PLUGIN_ID` (플러그인 id / dev id), `RB_PLUGIN_PORTS` (선언한 `ports`, 쉼표 구분), `MUSCAT_ROBOT_HOST` / `MUSCAT_ROBOT_TOKEN` (원격 개발). 로봇에서는 Muscat이 `RB_PLUGIN_DATA_DIR`도 설정합니다 (`data_dir`, 7.1 참고).
1042
1160
 
1043
1161
  ---
1044
1162
 
@@ -1047,10 +1165,18 @@ args = ["mcp"]
1047
1165
  | 증상 | 해결 |
1048
1166
  |---|---|
1049
1167
  | `manifest.json errors: …` | 나열된 필드를 고칩니다 (5장) |
1168
+ | `dev`에서 `manifest.json variable errors: …` / `manifest.json port errors: …` | `variables`(5.5) / `ports`(5.6) 수정 |
1169
+ | `A Muscat plugin dev environment is already running: …` | `y`로 멈추거나 다른 `dev`를 먼저 종료 |
1170
+ | `Docker is installed but the daemon is not running` | Docker Desktop 실행 (또는 `sudo systemctl start docker`) |
1171
+ | `contributes declares ui "<entry>" but ui/<entry>.tsx … does not exist.` | 파일을 추가하거나 `contributes[].ui` 수정 |
1172
+ | `wheel download failed for <arch>` | 그 arch용 cp312 바이너리 wheel이 없는 의존성 — 다른 버전이나 패키지 선택 |
1173
+ | `can't build <arch> on this <host>: Docker has no <arch> emulation.` | `docker run --privileged --rm tonistiigi/binfmt --install <arch>`, 또는 `--arch <호스트 arch>` |
1174
+ | `Permission denied when deleting dist/` | 예전 빌드가 root 소유 파일을 남김: `sudo rm -rf dist` 한 번 |
1050
1175
  | 선언 안 된 포트로 백엔드가 멈춤 | 포트(28600-28999)를 `ports`에 추가 |
1051
1176
  | UI 프리뷰에 "no default export" | 엔트리가 `export default App`인지, `vite.config.ts`가 `muscat()`을 쓰는지 확인. `muscat-pk mcp`의 `check_project` 실행 |
1052
1177
  | `vite not found — UI will not load` | 프로젝트에서 `npm install` |
1053
1178
  | `AttributeError: router` | 로봇 SDK가 0.0.9.dev5보다 낮음 |
1179
+ | `plugin.call`에서 `RBPluginGatewayError` | gateway 연결이 끊겼거나 `timeout` 안에 응답이 없음 (UI에 `rb.serve`가 등록돼 있는지 확인) |
1054
1180
  | 스텝이 끝나지 않음 | `on_execute`에서 `await ctx.done()` 호출 |
1055
1181
  | 정지를 눌러도 플러그인이 계속 돎 | 루프마다 `await ctx.check_stop()` 호출 |
1056
1182
  | `set_variable`에서 `RuntimeError` | 새 변수 이름은 `RBP_`로 시작 |