@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.22 → 0.1.2-dev.23

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.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // muscat-pk — Muscat plugin developer CLI.
3
3
  // Subcommands: new, build, pack, dev, dev:ui, dev:back, update, sdk.
4
+ import { log } from "../src/util.mjs";
4
5
  import { runNew } from "../src/new.mjs";
5
6
  import { runBuild } from "../src/build.mjs";
6
7
  import { runPack } from "../src/pack.mjs";
@@ -67,7 +68,8 @@ async function main() {
67
68
  process.exitCode = 1;
68
69
  }
69
70
  } catch (err) {
70
- console.error(`\n✖ ${err?.message || err}`);
71
+ console.error();
72
+ log.error(err?.message || String(err));
71
73
  process.exitCode = 1;
72
74
  }
73
75
  }
package/cli/src/build.mjs CHANGED
@@ -8,11 +8,12 @@
8
8
  // The plugin author's `@rainbow-robotics/plugin-ui` and React imports are left external.
9
9
  // Muscat/robot and preview-host provide those modules through a sandbox import
10
10
  // map, so plugin ZIPs do not carry the UI runtime.
11
- import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11
+ import { cpSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
12
12
  import { createRequire } from "node:module";
13
13
  import { dirname, resolve } from "node:path";
14
14
  import { spawnSync } from "node:child_process";
15
15
  import { findPluginRoot, hasBin, log } from "./util.mjs";
16
+ import { assertManifest } from "./manifest.mjs";
16
17
 
17
18
  // Find a usable node binary (for running vite as subprocess).
18
19
  function findNodeBin() {
@@ -174,6 +175,7 @@ export const SANDBOX_HTML = `<!doctype html>
174
175
 
175
176
  export async function buildPlugin({ root, minify = false } = {}) {
176
177
  const pluginRoot = root ?? findPluginRoot();
178
+ assertManifest(pluginRoot);
177
179
  const distDir = resolve(pluginRoot, "dist");
178
180
  rmSync(distDir, { recursive: true, force: true });
179
181
  mkdirSync(distDir, { recursive: true });
@@ -196,6 +198,13 @@ export async function buildPlugin({ root, minify = false } = {}) {
196
198
  cpSync(assetsSrc, resolve(distDir, "assets"), { recursive: true });
197
199
  }
198
200
 
201
+ // README.md and README.[lang].md → dist/
202
+ for (const f of readdirSync(pluginRoot)) {
203
+ if (/^README(\.[a-z]{2}(-[A-Z]{2})?)?\.md$/i.test(f)) {
204
+ copyFileSync(resolve(pluginRoot, f), resolve(distDir, f));
205
+ }
206
+ }
207
+
199
208
  // Backend runtime files — copy entire py/ into dist/, then requirements.txt from root.
200
209
  const pySrc = resolve(pluginRoot, "py");
201
210
  if (existsSync(pySrc)) {
@@ -8,6 +8,7 @@ import { spawn, spawnSync } from "node:child_process";
8
8
  import { existsSync, readFileSync, watch } from "node:fs";
9
9
  import { resolve } from "node:path";
10
10
  import { detectPythonCmd, hasBin, findPluginRoot, log, venvPython } from "./util.mjs";
11
+ import { readManifest as readManifestSchema, validateManifest } from "./manifest.mjs";
11
12
  import {
12
13
  ROS2_DEV_IMAGE,
13
14
  assertDockerReady,
@@ -146,8 +147,16 @@ function runRos2(root) {
146
147
  process.on("SIGTERM", cleanup);
147
148
  }
148
149
 
150
+ function warnManifest(root) {
151
+ const manifest = readManifestSchema(root);
152
+ if (!manifest || Object.keys(manifest).length === 0) return;
153
+ const errors = validateManifest(manifest);
154
+ for (const e of errors) log.warn(`manifest.json: ${e}`);
155
+ }
156
+
149
157
  export async function runDevBack() {
150
158
  const root = findPluginRoot();
159
+ warnManifest(root);
151
160
  const type = manifestRuntime(root);
152
161
  if (type === "ros2") return runRos2(root);
153
162
  return runPython(root);
@@ -0,0 +1,160 @@
1
+ // manifest.mjs — manifest.json schema definition and validation
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+
5
+ export const CONTRIBUTION_POINTS = ["program.command", "toolbar", "move.rnb"];
6
+ const ICON_SIZES = ["16", "48", "128"];
7
+ const RUNTIME_TYPES = ["none", "python", "ros2"];
8
+ const ROS_CLIENTS = ["rclpy", "rclcpp"];
9
+ const I18N_LANGS = ["ko", "en", "ja", "zh", "vi", "th", "es"];
10
+
11
+ function validateI18nLabel(value, field, errors) {
12
+ if (value === undefined || value === null) {
13
+ errors.push(`${field} is required`);
14
+ return;
15
+ }
16
+ if (typeof value === "string") return;
17
+ if (typeof value === "object") {
18
+ if (typeof value.en !== "string" || value.en.length === 0)
19
+ errors.push(`${field}: "en" is required when using object form`);
20
+ const unknown = Object.keys(value).filter((k) => !I18N_LANGS.includes(k));
21
+ if (unknown.length > 0)
22
+ errors.push(`${field}: unsupported language key(s) — ${unknown.map((k) => `"${k}"`).join(", ")} (allowed: ${I18N_LANGS.join(", ")})`);
23
+ return;
24
+ }
25
+ errors.push(`${field}: must be a string or { en (required), ko, ja, zh, vi, th, es }`);
26
+ }
27
+
28
+ export function readManifest(root) {
29
+ const path = resolve(root, "manifest.json");
30
+ if (!existsSync(path)) return {};
31
+ try {
32
+ return JSON.parse(readFileSync(path, "utf8"));
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function validateIcons(icons, errors) {
39
+ if (icons === undefined || icons === null) {
40
+ errors.push("icons is required");
41
+ return;
42
+ }
43
+ if (typeof icons === "string") {
44
+ if (!icons.toLowerCase().endsWith(".svg"))
45
+ errors.push(`icons: non-SVG images (PNG, JPG, etc.) must use the { "16": "...", "48": "...", "128": "..." } object form`);
46
+ } else if (typeof icons === "object") {
47
+ const missing = ICON_SIZES.filter((s) => typeof icons[s] !== "string" || icons[s].length === 0);
48
+ if (missing.length > 0)
49
+ errors.push(`icons: missing size(s) ${missing.map((s) => `"${s}"`).join(", ")} — all three (16, 48, 128) are required for image files`);
50
+ } else {
51
+ errors.push(`icons: must be a string (SVG path) or { "16", "48", "128" } object (PNG/JPG)`);
52
+ }
53
+ }
54
+
55
+ function validateRuntime(runtime, errors) {
56
+ if (runtime === undefined || runtime === null) {
57
+ errors.push("runtime is required");
58
+ return;
59
+ }
60
+ if (typeof runtime !== "object") {
61
+ errors.push("runtime must be an object");
62
+ return;
63
+ }
64
+ if (!RUNTIME_TYPES.includes(runtime.type))
65
+ errors.push(`runtime.type must be one of ${RUNTIME_TYPES.map((t) => `"${t}"`).join(" | ")}`);
66
+ if (runtime.type === "python" && !runtime.entry)
67
+ errors.push("runtime.entry is required for python runtime");
68
+ if (runtime.type === "ros2") {
69
+ if (!runtime.package) errors.push("runtime.package is required for ros2 runtime");
70
+ if (!runtime.launch) errors.push("runtime.launch is required for ros2 runtime");
71
+ if (!ROS_CLIENTS.includes(runtime.rosClient))
72
+ errors.push(`runtime.rosClient must be one of ${ROS_CLIENTS.map((c) => `"${c}"`).join(" | ")}`);
73
+ }
74
+ }
75
+
76
+ function validateCommandInfo(info, prefix, errors) {
77
+ if (typeof info.name !== "string" || info.name.length === 0)
78
+ errors.push(`${prefix}.commandInfo.name is required`);
79
+ if (typeof info.icon !== "string" || info.icon.length === 0)
80
+ errors.push(`${prefix}.commandInfo.icon is required`);
81
+ if (info.iconClassName !== undefined && typeof info.iconClassName !== "string")
82
+ errors.push(`${prefix}.commandInfo.iconClassName must be a string`);
83
+ if (info.funcName !== undefined && typeof info.funcName !== "string")
84
+ errors.push(`${prefix}.commandInfo.funcName must be a string`);
85
+ if (info.args !== undefined && (typeof info.args !== "object" || Array.isArray(info.args)))
86
+ errors.push(`${prefix}.commandInfo.args must be an object`);
87
+ for (const key of ["disabled", "syncGroupId", "disableCopy", "disableDelete", "requireChildren", "requireParent"]) {
88
+ if (info[key] !== undefined && typeof info[key] !== "boolean")
89
+ errors.push(`${prefix}.commandInfo.${key} must be a boolean`);
90
+ }
91
+ for (const key of ["allowChildren", "allowParents", "notAllowChildren"]) {
92
+ if (info[key] !== undefined && (!Array.isArray(info[key]) || info[key].some((v) => typeof v !== "string")))
93
+ errors.push(`${prefix}.commandInfo.${key} must be a string[]`);
94
+ }
95
+ if (info.childrenCommand !== undefined && typeof info.childrenCommand !== "string")
96
+ errors.push(`${prefix}.commandInfo.childrenCommand must be a string`);
97
+ }
98
+
99
+ function validateContributes(contributes, errors) {
100
+ if (!Array.isArray(contributes) || contributes.length === 0) {
101
+ errors.push("contributes is required and must be a non-empty array");
102
+ return;
103
+ }
104
+ if (!contributes.some((c) => c.point === "toolbar"))
105
+ errors.push(`contributes: at least one "toolbar" entry is required`);
106
+
107
+ const uiValues = contributes.map((c) => c.ui).filter(Boolean);
108
+ const duplicateUi = uiValues.filter((v, i) => uiValues.indexOf(v) !== i);
109
+ if (duplicateUi.length > 0)
110
+ errors.push(`contributes: "ui" values must be unique across all entries (duplicates: ${[...new Set(duplicateUi)].map((v) => `"${v}"`).join(", ")})`);
111
+
112
+ contributes.forEach((c, i) => {
113
+ validateI18nLabel(c.label, `contributes[${i}].label`, errors);
114
+ if (c.point === "program.command") {
115
+ if (!c.commandInfo)
116
+ errors.push(`contributes[${i}]: "commandInfo" is required when point is "program.command"`);
117
+ else
118
+ validateCommandInfo(c.commandInfo, `contributes[${i}]`, errors);
119
+ }
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Validate manifest.json. Returns an array of error messages (empty = valid).
125
+ */
126
+ export function validateManifest(manifest) {
127
+ const errors = [];
128
+
129
+ if (typeof manifest !== "object" || manifest === null)
130
+ return ["manifest.json is not a valid JSON object"];
131
+
132
+ for (const key of ["id", "name", "version", "minMuscatVersion"]) {
133
+ if (typeof manifest[key] !== "string" || manifest[key].length === 0)
134
+ errors.push(`"${key}" is required`);
135
+ }
136
+ if (typeof manifest.author !== "string")
137
+ errors.push(`"author" is required (empty string is allowed)`);
138
+ validateI18nLabel(manifest.description, "description", errors);
139
+
140
+ validateIcons(manifest.icons, errors);
141
+ validateRuntime(manifest.runtime, errors);
142
+ validateContributes(manifest.contributes, errors);
143
+
144
+ return errors;
145
+ }
146
+
147
+ /**
148
+ * Read and validate manifest.json. Throws if any errors are found.
149
+ */
150
+ export function assertManifest(root) {
151
+ const manifest = readManifest(root);
152
+ if (manifest === null)
153
+ throw new Error("manifest.json parse failed — check JSON syntax");
154
+ if (Object.keys(manifest).length === 0)
155
+ return manifest;
156
+ const errors = validateManifest(manifest);
157
+ if (errors.length > 0)
158
+ throw new Error(`manifest.json errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
159
+ return manifest;
160
+ }
package/cli/src/util.mjs CHANGED
@@ -79,14 +79,16 @@ export function findPluginRoot(start = process.cwd()) {
79
79
  );
80
80
  }
81
81
 
82
+ const RED = "\x1b[31m";
82
83
  const ORANGE = "\x1b[33m";
83
84
  const RESET = "\x1b[0m";
84
85
 
85
86
  export const log = {
86
- info: (...m) => console.log(...m),
87
- step: (...m) => console.log("›", ...m),
88
- ok: (...m) => console.log("✓", ...m),
89
- warn: (...m) => console.warn(`${ORANGE}⚠${RESET}`, ...m.map(s => `${ORANGE}${s}${RESET}`)),
87
+ info: (...m) => console.log(...m),
88
+ step: (...m) => console.log("›", ...m),
89
+ ok: (...m) => console.log("✓", ...m),
90
+ warn: (...m) => console.warn(`${ORANGE}⚠${RESET}`, ...m.map(s => `${ORANGE}${s}${RESET}`)),
91
+ error: (...m) => console.error(`${RED}✖${RESET}`, ...m.map(s => `${RED}${s}${RESET}`)),
90
92
  };
91
93
 
92
94
  export function humanSize(bytes) {
@@ -17,12 +17,14 @@ muscat-pk pack # build a distributable ZIP
17
17
 
18
18
  ```
19
19
  __PLUGIN_DIR__/
20
- ├─ src/
20
+ ├─ ui/
21
21
  │ └─ content.tsx # plugin UI entry point
22
+ ├─ py/
23
+ │ └─ main.py # Python backend entry point
22
24
  ├─ assets/
23
25
  │ └─ icon.svg # icon referenced from manifest
24
26
  ├─ manifest.json # metadata, contribution points, runtime config
25
- ├─ main.py # backend (Python or ROS2)
27
+ ├─ README.md # this file (copied to dist/ on build)
26
28
  └─ requirements.txt # Python deps (auto-pinned to latest on pack)
27
29
  ```
28
30
 
@@ -30,7 +32,7 @@ __PLUGIN_DIR__/
30
32
 
31
33
  ---
32
34
 
33
- ## Frontend (`src/content.tsx`)
35
+ ## Frontend (`ui/content.tsx`)
34
36
 
35
37
  ### useRb() hook
36
38
 
@@ -97,12 +99,11 @@ connect(App);
97
99
 
98
100
  ---
99
101
 
100
- ## Backend (`main.py`)
102
+ ## Backend (`py/main.py`)
101
103
 
102
104
  The backend communicates with the frontend using `rainbow.rb_sdk.RBPluginCall`.
103
- Relative topics and plugin context are handled by the SDK.
104
- `muscat-pk dev` starts the local Socket.IO bridge separately; `main.py`
105
- should contain plugin logic only and should not start its own server.
105
+ `muscat-pk dev` starts the local Socket.IO bridge; `py/main.py` should contain plugin
106
+ logic only and must not start its own server.
106
107
 
107
108
  ### Python plugin
108
109
 
@@ -148,7 +149,6 @@ if __name__ == "__main__":
148
149
  ### ROS2 plugin
149
150
 
150
151
  The node is launched with `--ros-args -r __ns:=/{slug}` automatically by `dev:back`.
151
- ROS2 topics are bridged to the plugin communication layer — no extra code needed.
152
152
 
153
153
  ```python
154
154
  # ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py
@@ -173,30 +173,116 @@ class __PLUGIN_CLASS__Node(Node):
173
173
 
174
174
  ```jsonc
175
175
  {
176
- "id": "com.example.my-plugin", // unique plugin ID (reverse-domain recommended)
176
+ // unique plugin ID — reverse-domain format recommended
177
+ "id": "com.example.my-plugin",
177
178
  "name": "My Plugin",
178
179
  "version": "1.0.0",
179
- "minAppVersion": "2.0.0",
180
- "icon": "assets/icon.svg",
180
+
181
+ // minimum Muscat version required to load this plugin
182
+ "minMuscatVersion": "1.0.0",
183
+
184
+ // plugin author (empty string is fine)
185
+ "author": "Your Name",
186
+
187
+ // displayed in the Muscat plugin store / info panel
188
+ // string or { en (required), ko, ja, zh, vi, th, es }
189
+ "description": { "en": "Plugin description", "ko": "플러그인 설명" },
190
+
191
+ // SVG string (single file) or PNG/JPG size map (all three sizes required)
192
+ "icons": "assets/icon.svg",
193
+ // "icons": { "16": "assets/icon-16.png", "48": "assets/icon-48.png", "128": "assets/icon-128.png" },
181
194
 
182
195
  "contributes": [
183
- { "point": "program.panel", "label": "My Plugin", "icon": "assets/icon.svg" },
184
- { "point": "lnb", "label": "My Plugin", "icon": "assets/icon.svg" },
185
- { "point": "header.toolbar","label": "My Plugin", "icon": "assets/icon.svg", "opens": "popover" }
196
+ // program.command — adds a step node to the program tree
197
+ {
198
+ "point": "program.command",
199
+ "icon": "assets/icon.svg",
200
+ "label": { "ko": "내 플러그인", "en": "My Plugin" },
201
+ "ui": "program/content", // must be unique across all contributions
202
+ "commandInfo": {
203
+ "name": "My Plugin",
204
+ "icon": "assets/icon.svg",
205
+ "funcName": "plugin.run",
206
+ "args": {}
207
+ }
208
+ },
209
+ // toolbar — adds a button to the top toolbar (required)
210
+ {
211
+ "point": "toolbar",
212
+ "icon": "assets/icon.svg",
213
+ "label": { "ko": "내 플러그인", "en": "My Plugin" },
214
+ "ui": "content", // must be unique across all contributions
215
+ "opens": "popover"
216
+ },
217
+ // move.rnb — adds a panel to the Move screen right-side area
218
+ {
219
+ "point": "move.rnb",
220
+ "icon": "assets/icon.svg",
221
+ "label": { "ko": "내 플러그인", "en": "My Plugin" },
222
+ "ui": "move/content" // must be unique across all contributions
223
+ }
186
224
  ],
187
225
 
188
- // "python" | "ros2"
226
+ // "none" | "python" | "ros2"
189
227
  "runtime": { "type": "python", "entry": "main.py" }
190
228
  }
191
229
  ```
192
230
 
193
231
  **Contribution points:**
194
232
 
195
- | point | Where it appears |
196
- |---|---|
197
- | `program.panel` | Program panel tab |
198
- | `lnb` | Left navigation bar |
199
- | `header.toolbar` | Header toolbar button |
233
+ | point | Where it appears | `commandInfo` required |
234
+ |---|---|:---:|
235
+ | `toolbar` | Top toolbar button (must have at least one) | — |
236
+ | `program.command` | Program tree step node | ✓ |
237
+ | `move.rnb` | Move screen right-side panel | — |
238
+
239
+ **`commandInfo` fields** (for `program.command`):
240
+
241
+ | field | type | description |
242
+ |---|---|---|
243
+ | `name` | `string` | Display name of the step node |
244
+ | `icon` | `string` | Icon path |
245
+ | `iconClassName` | `string?` | Optional CSS class for the icon |
246
+ | `funcName` | `string?` | Backend function name to call |
247
+ | `args` | `Record<string, any>?` | Default arguments |
248
+ | `disabled` | `boolean?` | Whether the node is disabled |
249
+ | `allowChildren` | `string[]?` | Allowed child commands |
250
+ | `allowParents` | `string[]?` | Allowed parent commands |
251
+ | `notAllowChildren` | `string[]?` | Disallowed child commands |
252
+ | `syncGroupId` | `boolean?` | Sync group ID across children |
253
+ | `disableCopy` | `boolean?` | Prevent copy |
254
+ | `disableDelete` | `boolean?` | Prevent delete |
255
+ | `requireChildren` | `boolean?` | Whether a child command is required |
256
+ | `requireParent` | `boolean?` | Whether a parent command is required |
257
+ | `childrenCommand` | `string?` | Default command type for children |
258
+
259
+ **Validation rules:**
260
+ - `id`, `name`, `version`, `minMuscatVersion` — required non-empty strings
261
+ - `author` — required (empty string is fine)
262
+ - `description` — required (`string` or `{ en (required), ko?, ja?, zh?, vi?, th?, es? }`)
263
+ - `icons` — SVG: single string path ending in `.svg`; PNG/JPG: object with `"16"`, `"48"`, `"128"` all present
264
+ - `contributes` — required array; must contain at least one `toolbar` entry
265
+ - `ui` — each contribution must have a unique value
266
+ - `commandInfo` — required when `point` is `"program.command"`
267
+
268
+ `muscat-pk build` throws on any validation error. `muscat-pk dev` prints warnings.
269
+
270
+ ---
271
+
272
+ ## Localized documentation
273
+
274
+ You can ship per-language README files alongside the main `README.md`. Place them in the project root:
275
+
276
+ ```
277
+ README.md # default / English fallback
278
+ README.ko.md # Korean
279
+ README.en.md # explicit English (optional)
280
+ README.ja.md # Japanese
281
+ ```
282
+
283
+ All `README*.md` files found at the project root are automatically copied to `dist/` on
284
+ `muscat-pk build` / `muscat-pk pack`. Muscat displays the file matching the user's locale,
285
+ falling back to `README.md` if none matches.
200
286
 
201
287
  ---
202
288
 
@@ -211,13 +297,11 @@ numpy==1.26.4
211
297
  opencv-python
212
298
  ```
213
299
 
214
- Then just run `muscat-pk dev` or `muscat-pk dev:back` — dependencies are installed automatically on every startup:
300
+ Run `muscat-pk dev` or `muscat-pk dev:back` — dependencies are installed automatically on every startup.
215
301
 
216
302
  - **Python plugin**: installed into `.venv` via `pip install -r requirements.txt`
217
303
  - **ROS2 plugin**: installed inside the Docker container via `pip install -r requirements.txt`
218
304
 
219
- No extra steps needed.
220
-
221
305
  > `muscat-pk build` / `muscat-pk pack` only **pins** versions in `requirements.txt` — it does not run `pip install`.
222
306
 
223
307
  ---
@@ -240,8 +324,9 @@ muscat-pk type change --to python # ros2 → python
240
324
  muscat-pk pack
241
325
  ```
242
326
 
243
- - Builds `src/content.tsx` → `dist/content.js`
327
+ - Builds `ui/content.tsx` → `dist/content.js`
244
328
  - Pins `requirements.txt` to latest versions (stable-only on `main` branch)
329
+ - Copies `assets/`, `manifest.json`, and all `README*.md` files to `dist/`
245
330
  - ROS2: runs `colcon build` inside the Jazzy container; includes `ros2_ws/install/`
246
331
  - Output: `__PLUGIN_DIR__-{version}.zip`
247
332
 
@@ -249,10 +334,15 @@ ZIP layout:
249
334
 
250
335
  ```
251
336
  __PLUGIN_DIR__-1.0.0.zip
252
- ├─ dist/ # built UI
253
- ├─ main.py
254
- ├─ requirements.txt # pinned
255
- └─ ros2_ws/install/ # ROS2 only
337
+ ├─ dist/
338
+ │ ├─ content.js # built UI
339
+ │ ├─ manifest.json
340
+ │ ├─ assets/
341
+ │ ├─ README.md
342
+ │ └─ README.ko.md # if present
343
+ ├─ py/
344
+ │ └─ main.py
345
+ └─ requirements.txt # pinned
256
346
  ```
257
347
 
258
348
  ---
@@ -2,16 +2,38 @@
2
2
  "id": "__PLUGIN_ID__",
3
3
  "name": "__PLUGIN_NAME__",
4
4
  "version": "1.0.0",
5
- "minAppVersion": "2.0.0",
6
- "icon": "assets/icon.svg",
5
+ "minMuscatVersion": "1.0.0",
6
+ "author": "",
7
+ "description": {
8
+ "ko": "",
9
+ "en": ""
10
+ },
11
+ "icons": "assets/icon.svg",
7
12
  "contributes": [
8
- { "point": "program.panel", "label": "__PLUGIN_NAME__", "icon": "assets/icon.svg" },
9
- { "point": "lnb", "label": "__PLUGIN_NAME__", "icon": "assets/icon.svg" },
10
13
  {
11
- "point": "header.toolbar",
12
- "label": "__PLUGIN_NAME__",
14
+ "point": "program.command",
13
15
  "icon": "assets/icon.svg",
16
+ "label": { "ko": "__PLUGIN_NAME__", "en": "__PLUGIN_NAME__" },
17
+ "ui": "program/content",
18
+ "commandInfo": {
19
+ "name": "__PLUGIN_NAME__",
20
+ "icon": "assets/icon.svg",
21
+ "funcName": "plugin.run",
22
+ "args": {}
23
+ }
24
+ },
25
+ {
26
+ "point": "toolbar",
27
+ "icon": "assets/icon.svg",
28
+ "label": { "ko": "__PLUGIN_NAME__", "en": "__PLUGIN_NAME__" },
29
+ "ui": "content",
14
30
  "opens": "popover"
31
+ },
32
+ {
33
+ "point": "move.rnb",
34
+ "icon": "assets/icon.svg",
35
+ "label": { "ko": "__PLUGIN_NAME__", "en": "__PLUGIN_NAME__" },
36
+ "ui": "move/content"
15
37
  }
16
38
  ],
17
39
  "runtime": { "type": "__RUNTIME_TYPE__", "entry": "main.py" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-robotics/muscat-plugin-kit",
3
- "version": "0.1.2-dev.22",
3
+ "version": "0.1.2-dev.23",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=20"
@@ -31,7 +31,7 @@
31
31
  "@types/react-dom": "^19.0.4"
32
32
  },
33
33
  "dependencies": {
34
- "@rainbow-robotics/preview-host": "0.1.2-dev.22",
34
+ "@rainbow-robotics/preview-host": "0.1.2-dev.23",
35
35
  "socket.io": "^4.8.1"
36
36
  },
37
37
  "optionalDependencies": {