@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.22 → 0.1.2-dev.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/README.md +2 -3
- package/cli/bin/muscat-pk.mjs +5 -7
- package/cli/src/build.mjs +60 -9
- package/cli/src/devBack.mjs +9 -0
- package/cli/src/devUi.mjs +15 -11
- package/cli/src/manifest.mjs +183 -0
- package/cli/src/new.mjs +2 -2
- package/cli/src/util.mjs +6 -4
- package/cli/templates/common/README.md +133 -34
- package/cli/templates/common/manifest.json +30 -6
- package/cli/templates/common/requirements.txt +3 -3
- package/package.json +2 -2
- package/cli/src/pack.mjs +0 -58
package/cli/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @rainbow-robotics/muscat-plugin-kit (`muscat-pk`)
|
|
2
2
|
|
|
3
|
-
CLI to scaffold, build,
|
|
3
|
+
CLI to scaffold, build, and preview Muscat plugins.
|
|
4
4
|
|
|
5
5
|
Install/use it the same way an external plugin author will:
|
|
6
6
|
|
|
@@ -11,8 +11,7 @@ muscat-pk new my-plugin
|
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
muscat-pk new my-plugin [--type sdk|ros2|ui] [--pm npm|yarn|pnpm|bun] [--yes]
|
|
14
|
-
muscat-pk build #
|
|
15
|
-
muscat-pk pack # build (minified) + distributable ZIP with a size report
|
|
14
|
+
muscat-pk build # ui/content.tsx -> dist/ (minified) + distributable ZIP (--no-zip to skip)
|
|
16
15
|
muscat-pk dev:ui # preview the plugin in a mini-Muscat (preview-host)
|
|
17
16
|
muscat-pk dev:back # run the back end (.venv + python main.py)
|
|
18
17
|
muscat-pk dev # dev:ui + dev:back
|
package/cli/bin/muscat-pk.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// muscat-pk — Muscat plugin developer CLI.
|
|
3
|
-
// Subcommands: new, build,
|
|
3
|
+
// Subcommands: new, build, 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
|
-
import { runPack } from "../src/pack.mjs";
|
|
7
7
|
import { runDevUi } from "../src/devUi.mjs";
|
|
8
8
|
import { runDevBack } from "../src/devBack.mjs";
|
|
9
9
|
import { runDev } from "../src/dev.mjs";
|
|
@@ -21,8 +21,7 @@ Usage: muscat-pk <command> [options]
|
|
|
21
21
|
Commands:
|
|
22
22
|
new [dir] Scaffold a new plugin project
|
|
23
23
|
type change Switch backend type (python <-> ros2)
|
|
24
|
-
build Build
|
|
25
|
-
pack Build then create a distributable ZIP (with size report)
|
|
24
|
+
build Build ui/content.tsx -> dist/ and zip into a distributable ZIP (--no-zip to skip)
|
|
26
25
|
dev Run Socket.IO bridge, dev:ui, and dev:back together
|
|
27
26
|
dev:ui Preview the plugin in a mini-Muscat (preview-host)
|
|
28
27
|
dev:back Run the plugin back end (python: watchfiles reload / ros2: Docker)
|
|
@@ -41,8 +40,6 @@ async function main() {
|
|
|
41
40
|
return await runType(args);
|
|
42
41
|
case "build":
|
|
43
42
|
return await runBuild(args);
|
|
44
|
-
case "pack":
|
|
45
|
-
return await runPack(args);
|
|
46
43
|
case "dev":
|
|
47
44
|
return await runDev(args);
|
|
48
45
|
case "dev:ui":
|
|
@@ -67,7 +64,8 @@ async function main() {
|
|
|
67
64
|
process.exitCode = 1;
|
|
68
65
|
}
|
|
69
66
|
} catch (err) {
|
|
70
|
-
console.error(
|
|
67
|
+
console.error();
|
|
68
|
+
log.error(err?.message || String(err));
|
|
71
69
|
process.exitCode = 1;
|
|
72
70
|
}
|
|
73
71
|
}
|
package/cli/src/build.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// muscat-pk build — bundle
|
|
2
|
-
// manifest.json and assets/ for distribution
|
|
1
|
+
// muscat-pk build — bundle ui/content.tsx into dist/content.js, stage
|
|
2
|
+
// manifest.json and assets/ for distribution, and zip dist/ into a
|
|
3
|
+
// distributable ZIP (skip with --no-zip).
|
|
3
4
|
//
|
|
4
5
|
// sandbox.html is generated by the CLI — plugin authors never manage it.
|
|
5
6
|
// The import map always reflects the current plugin-ui entry set, so users
|
|
@@ -8,11 +9,12 @@
|
|
|
8
9
|
// The plugin author's `@rainbow-robotics/plugin-ui` and React imports are left external.
|
|
9
10
|
// Muscat/robot and preview-host provide those modules through a sandbox import
|
|
10
11
|
// map, so plugin ZIPs do not carry the UI runtime.
|
|
11
|
-
import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { cpSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
12
13
|
import { createRequire } from "node:module";
|
|
13
|
-
import { dirname, resolve } from "node:path";
|
|
14
|
-
import { spawnSync } from "node:child_process";
|
|
15
|
-
import { findPluginRoot, hasBin, log } from "./util.mjs";
|
|
14
|
+
import { basename, dirname, resolve } from "node:path";
|
|
15
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
16
|
+
import { findPluginRoot, hasBin, humanSize, log } from "./util.mjs";
|
|
17
|
+
import { assertManifest } from "./manifest.mjs";
|
|
16
18
|
|
|
17
19
|
// Find a usable node binary (for running vite as subprocess).
|
|
18
20
|
function findNodeBin() {
|
|
@@ -116,7 +118,7 @@ async function latestPypiVersion(pkg, { stableOnly = false } = {}) {
|
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
// Pin unpinned (no version specifier) entries in requirements.txt to the
|
|
119
|
-
// latest PyPI release. Runs on explicit `build
|
|
121
|
+
// latest PyPI release. Runs on explicit `build` only — keeps the shipped
|
|
120
122
|
// plugin reproducible without churning the source file on every dev rebuild.
|
|
121
123
|
// stableOnly=true is used on main-branch builds to avoid shipping dev versions.
|
|
122
124
|
export async function pinRequirements(root, { stableOnly = false } = {}) {
|
|
@@ -174,6 +176,7 @@ export const SANDBOX_HTML = `<!doctype html>
|
|
|
174
176
|
|
|
175
177
|
export async function buildPlugin({ root, minify = false } = {}) {
|
|
176
178
|
const pluginRoot = root ?? findPluginRoot();
|
|
179
|
+
assertManifest(pluginRoot);
|
|
177
180
|
const distDir = resolve(pluginRoot, "dist");
|
|
178
181
|
rmSync(distDir, { recursive: true, force: true });
|
|
179
182
|
mkdirSync(distDir, { recursive: true });
|
|
@@ -196,6 +199,13 @@ export async function buildPlugin({ root, minify = false } = {}) {
|
|
|
196
199
|
cpSync(assetsSrc, resolve(distDir, "assets"), { recursive: true });
|
|
197
200
|
}
|
|
198
201
|
|
|
202
|
+
// README.md and README.[lang].md → dist/
|
|
203
|
+
for (const f of readdirSync(pluginRoot)) {
|
|
204
|
+
if (/^README(\.[a-z]{2}(-[A-Z]{2})?)?\.md$/i.test(f)) {
|
|
205
|
+
copyFileSync(resolve(pluginRoot, f), resolve(distDir, f));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
199
209
|
// Backend runtime files — copy entire py/ into dist/, then requirements.txt from root.
|
|
200
210
|
const pySrc = resolve(pluginRoot, "py");
|
|
201
211
|
if (existsSync(pySrc)) {
|
|
@@ -210,14 +220,55 @@ export async function buildPlugin({ root, minify = false } = {}) {
|
|
|
210
220
|
return { pluginRoot, distDir };
|
|
211
221
|
}
|
|
212
222
|
|
|
223
|
+
function dirSize(dir) {
|
|
224
|
+
let total = 0;
|
|
225
|
+
for (const entry of readdirSync(dir)) {
|
|
226
|
+
const full = resolve(dir, entry);
|
|
227
|
+
const st = statSync(full);
|
|
228
|
+
total += st.isDirectory() ? dirSize(full) : st.size;
|
|
229
|
+
}
|
|
230
|
+
return total;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Zip dist/ (+ ros2_ws/install) into a distributable ZIP with a size report.
|
|
234
|
+
function packZip(pluginRoot) {
|
|
235
|
+
const manifest = JSON.parse(readFileSync(resolve(pluginRoot, "manifest.json"), "utf8"));
|
|
236
|
+
const zipName = `${basename(pluginRoot)}-${manifest.version || "0.0.0"}.zip`;
|
|
237
|
+
const zipPath = resolve(pluginRoot, zipName);
|
|
238
|
+
rmSync(zipPath, { force: true });
|
|
239
|
+
|
|
240
|
+
const entries = ["dist"];
|
|
241
|
+
if (existsSync(resolve(pluginRoot, "ros2_ws/install"))) entries.push("ros2_ws/install");
|
|
242
|
+
|
|
243
|
+
log.step(`packing ${entries.join(", ")} → ${zipName}`);
|
|
244
|
+
try {
|
|
245
|
+
execSync(`zip -r -q "${zipPath}" ${entries.map((e) => `"${e}"`).join(" ")}`, {
|
|
246
|
+
cwd: pluginRoot,
|
|
247
|
+
stdio: "inherit",
|
|
248
|
+
});
|
|
249
|
+
} catch {
|
|
250
|
+
throw new Error("`zip` not found. Install zip or pack manually.");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const zipBytes = statSync(zipPath).size;
|
|
254
|
+
log.ok(`${zipName} ${humanSize(zipBytes)}`);
|
|
255
|
+
for (const e of entries) {
|
|
256
|
+
const full = resolve(pluginRoot, e);
|
|
257
|
+
const size = statSync(full).isDirectory() ? dirSize(full) : statSync(full).size;
|
|
258
|
+
log.info(` ├─ ${e} ${humanSize(size)}`);
|
|
259
|
+
}
|
|
260
|
+
log.warn("robot install checks free space via GET /plugin/capacity (507 if low).");
|
|
261
|
+
}
|
|
262
|
+
|
|
213
263
|
export async function runBuild(args) {
|
|
214
|
-
const
|
|
264
|
+
const noZip = args.includes("--no-zip");
|
|
215
265
|
const root = findPluginRoot();
|
|
216
266
|
if (!args.includes("--no-pin")) {
|
|
217
267
|
const stableOnly = isMainBranch();
|
|
218
268
|
log.step(`pinning requirements.txt to latest ${stableOnly ? "stable " : ""}PyPI versions`);
|
|
219
269
|
await pinRequirements(root, { stableOnly });
|
|
220
270
|
}
|
|
221
|
-
await buildPlugin({ root, minify });
|
|
271
|
+
const { pluginRoot } = await buildPlugin({ root, minify: true });
|
|
222
272
|
await exportRos2Build(root);
|
|
273
|
+
if (!noZip) packZip(pluginRoot);
|
|
223
274
|
}
|
package/cli/src/devBack.mjs
CHANGED
|
@@ -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);
|
package/cli/src/devUi.mjs
CHANGED
|
@@ -13,6 +13,10 @@ const PREVIEW_PKG = "@rainbow-robotics/preview-host";
|
|
|
13
13
|
const CACHE_DIR = resolve(homedir(), ".muscat-pk");
|
|
14
14
|
const CACHE_ROOT = resolve(CACHE_DIR, "node_modules", "@rainbow-robotics", "preview-host");
|
|
15
15
|
|
|
16
|
+
// Dev builds go to a throwaway dir, not dist/ — dist/ is reserved for
|
|
17
|
+
// build/pack output (the shippable artifact).
|
|
18
|
+
const DEV_OUT = ".muscat-dev";
|
|
19
|
+
|
|
16
20
|
function findNodeBin() {
|
|
17
21
|
if (typeof Bun === "undefined") return process.execPath;
|
|
18
22
|
for (const c of ["node", "node20", "node18"]) {
|
|
@@ -52,14 +56,14 @@ function findPreviewHostRoot() {
|
|
|
52
56
|
return CACHE_ROOT;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
// Write sandbox.html + copy manifest into
|
|
59
|
+
// Write sandbox.html + copy manifest into the dev out dir (vite doesn't generate these).
|
|
56
60
|
function stageDevAssets(pluginRoot) {
|
|
57
|
-
const
|
|
58
|
-
mkdirSync(
|
|
59
|
-
writeFileSync(resolve(
|
|
61
|
+
const outDir = resolve(pluginRoot, DEV_OUT);
|
|
62
|
+
mkdirSync(outDir, { recursive: true });
|
|
63
|
+
writeFileSync(resolve(outDir, "sandbox.html"), SANDBOX_HTML, "utf8");
|
|
60
64
|
const manifestSrc = resolve(pluginRoot, "manifest.json");
|
|
61
|
-
if (existsSync(manifestSrc)) copyFileSync(manifestSrc, resolve(
|
|
62
|
-
return
|
|
65
|
+
if (existsSync(manifestSrc)) copyFileSync(manifestSrc, resolve(outDir, "manifest.json"));
|
|
66
|
+
return outDir;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
// Resolve vite bin path from the plugin root, hoisting-aware.
|
|
@@ -79,8 +83,8 @@ function spawnViteWatch(pluginRoot, node) {
|
|
|
79
83
|
const viteBin = findViteBin(pluginRoot);
|
|
80
84
|
if (!node || !viteBin || !existsSync(viteBin)) return null;
|
|
81
85
|
|
|
82
|
-
log.step(
|
|
83
|
-
return spawn(node, [viteBin, "build", "--watch", "--mode", "development"], {
|
|
86
|
+
log.step(`vite build --watch (ui/content.tsx → ${DEV_OUT}/content.js)`);
|
|
87
|
+
return spawn(node, [viteBin, "build", "--watch", "--mode", "development", "--outDir", DEV_OUT], {
|
|
84
88
|
cwd: pluginRoot,
|
|
85
89
|
stdio: "inherit",
|
|
86
90
|
});
|
|
@@ -106,14 +110,14 @@ export async function runDevUi(args = []) {
|
|
|
106
110
|
|
|
107
111
|
await checkKitUpdate();
|
|
108
112
|
const pluginRoot = findPluginRoot();
|
|
109
|
-
const
|
|
113
|
+
const outDir = stageDevAssets(pluginRoot);
|
|
110
114
|
|
|
111
115
|
const node = findNodeBin();
|
|
112
116
|
const viteProc = spawnViteWatch(pluginRoot, node);
|
|
113
117
|
|
|
114
118
|
if (viteProc) {
|
|
115
119
|
log.step("waiting for initial vite build…");
|
|
116
|
-
await waitForInitialBuild(
|
|
120
|
+
await waitForInitialBuild(outDir);
|
|
117
121
|
} else {
|
|
118
122
|
log.warn("vite not found — UI will not be bundled. Run `npm install` in the plugin project.");
|
|
119
123
|
}
|
|
@@ -133,7 +137,7 @@ export async function runDevUi(args = []) {
|
|
|
133
137
|
log.step(`starting preview-host on :${port}`);
|
|
134
138
|
const child = spawn(
|
|
135
139
|
node ?? process.execPath,
|
|
136
|
-
[serveEntry, "--plugin",
|
|
140
|
+
[serveEntry, "--plugin", outDir, "--port", String(port)],
|
|
137
141
|
{ stdio: "inherit" },
|
|
138
142
|
);
|
|
139
143
|
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
const TARGET_CATEGORIES = ["manipulate", "amr", "rby1", "vision"];
|
|
11
|
+
|
|
12
|
+
function validateI18nLabel(value, field, errors) {
|
|
13
|
+
if (value === undefined || value === null) {
|
|
14
|
+
errors.push(`${field} is required`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === "string") return;
|
|
18
|
+
if (typeof value === "object") {
|
|
19
|
+
if (typeof value.en !== "string")
|
|
20
|
+
errors.push(`${field}: "en" is required when using object form`);
|
|
21
|
+
const unknown = Object.keys(value).filter((k) => !I18N_LANGS.includes(k));
|
|
22
|
+
if (unknown.length > 0)
|
|
23
|
+
errors.push(`${field}: unsupported language key(s) — ${unknown.map((k) => `"${k}"`).join(", ")} (allowed: ${I18N_LANGS.join(", ")})`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
errors.push(`${field}: must be a string or { en (required), ko, ja, zh, vi, th, es }`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readManifest(root) {
|
|
30
|
+
const path = resolve(root, "manifest.json");
|
|
31
|
+
if (!existsSync(path)) return {};
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateIcons(icons, errors) {
|
|
40
|
+
if (icons === undefined || icons === null) {
|
|
41
|
+
errors.push("icons is required");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (typeof icons === "string") {
|
|
45
|
+
if (!icons.toLowerCase().endsWith(".svg"))
|
|
46
|
+
errors.push(`icons: non-SVG images (PNG, JPG, etc.) must use the { "16": "...", "48": "...", "128": "..." } object form`);
|
|
47
|
+
} else if (typeof icons === "object") {
|
|
48
|
+
const missing = ICON_SIZES.filter((s) => typeof icons[s] !== "string" || icons[s].length === 0);
|
|
49
|
+
if (missing.length > 0)
|
|
50
|
+
errors.push(`icons: missing size(s) ${missing.map((s) => `"${s}"`).join(", ")} — all three (16, 48, 128) are required for image files`);
|
|
51
|
+
} else {
|
|
52
|
+
errors.push(`icons: must be a string (SVG path) or { "16", "48", "128" } object (PNG/JPG)`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateRuntime(runtime, errors) {
|
|
57
|
+
if (runtime === undefined || runtime === null) {
|
|
58
|
+
errors.push("runtime is required");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (typeof runtime !== "object") {
|
|
62
|
+
errors.push("runtime must be an object");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!RUNTIME_TYPES.includes(runtime.type))
|
|
66
|
+
errors.push(`runtime.type must be one of ${RUNTIME_TYPES.map((t) => `"${t}"`).join(" | ")}`);
|
|
67
|
+
if (runtime.type === "python" && !runtime.entry)
|
|
68
|
+
errors.push("runtime.entry is required for python runtime");
|
|
69
|
+
if (runtime.type === "ros2") {
|
|
70
|
+
if (!runtime.package) errors.push("runtime.package is required for ros2 runtime");
|
|
71
|
+
if (!runtime.launch) errors.push("runtime.launch is required for ros2 runtime");
|
|
72
|
+
if (!ROS_CLIENTS.includes(runtime.rosClient))
|
|
73
|
+
errors.push(`runtime.rosClient must be one of ${ROS_CLIENTS.map((c) => `"${c}"`).join(" | ")}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateCommandInfo(info, prefix, errors) {
|
|
78
|
+
if (typeof info.name !== "string" || info.name.length === 0)
|
|
79
|
+
errors.push(`${prefix}.commandInfo.name is required`);
|
|
80
|
+
if (typeof info.icon !== "string" || info.icon.length === 0)
|
|
81
|
+
errors.push(`${prefix}.commandInfo.icon is required`);
|
|
82
|
+
if (info.iconClassName !== undefined && typeof info.iconClassName !== "string")
|
|
83
|
+
errors.push(`${prefix}.commandInfo.iconClassName must be a string`);
|
|
84
|
+
if (info.funcName !== undefined && typeof info.funcName !== "string")
|
|
85
|
+
errors.push(`${prefix}.commandInfo.funcName must be a string`);
|
|
86
|
+
if (info.args !== undefined && (typeof info.args !== "object" || Array.isArray(info.args)))
|
|
87
|
+
errors.push(`${prefix}.commandInfo.args must be an object`);
|
|
88
|
+
for (const key of ["disabled", "syncGroupId", "disableCopy", "disableDelete", "requireChildren", "requireParent"]) {
|
|
89
|
+
if (info[key] !== undefined && typeof info[key] !== "boolean")
|
|
90
|
+
errors.push(`${prefix}.commandInfo.${key} must be a boolean`);
|
|
91
|
+
}
|
|
92
|
+
for (const key of ["allowChildren", "allowParents", "notAllowChildren"]) {
|
|
93
|
+
if (info[key] !== undefined && (!Array.isArray(info[key]) || info[key].some((v) => typeof v !== "string")))
|
|
94
|
+
errors.push(`${prefix}.commandInfo.${key} must be a string[]`);
|
|
95
|
+
}
|
|
96
|
+
if (info.childrenCommand !== undefined && typeof info.childrenCommand !== "string")
|
|
97
|
+
errors.push(`${prefix}.commandInfo.childrenCommand must be a string`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateContributes(contributes, errors) {
|
|
101
|
+
if (!Array.isArray(contributes) || contributes.length === 0) {
|
|
102
|
+
errors.push("contributes is required and must be a non-empty array");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!contributes.some((c) => c.point === "toolbar"))
|
|
106
|
+
errors.push(`contributes: at least one "toolbar" entry is required`);
|
|
107
|
+
|
|
108
|
+
const uiValues = contributes.map((c) => c.ui).filter(Boolean);
|
|
109
|
+
const duplicateUi = uiValues.filter((v, i) => uiValues.indexOf(v) !== i);
|
|
110
|
+
if (duplicateUi.length > 0)
|
|
111
|
+
errors.push(`contributes: "ui" values must be unique across all entries (duplicates: ${[...new Set(duplicateUi)].map((v) => `"${v}"`).join(", ")})`);
|
|
112
|
+
|
|
113
|
+
contributes.forEach((c, i) => {
|
|
114
|
+
validateI18nLabel(c.label, `contributes[${i}].label`, errors);
|
|
115
|
+
if (c.point === "program.command") {
|
|
116
|
+
if (!c.commandInfo)
|
|
117
|
+
errors.push(`contributes[${i}]: "commandInfo" is required when point is "program.command"`);
|
|
118
|
+
else
|
|
119
|
+
validateCommandInfo(c.commandInfo, `contributes[${i}]`, errors);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Validate manifest.json. Returns an array of error messages (empty = valid).
|
|
126
|
+
*/
|
|
127
|
+
export function validateManifest(manifest) {
|
|
128
|
+
const errors = [];
|
|
129
|
+
|
|
130
|
+
if (typeof manifest !== "object" || manifest === null)
|
|
131
|
+
return ["manifest.json is not a valid JSON object"];
|
|
132
|
+
|
|
133
|
+
for (const key of ["id", "name", "version", "minMuscatVersion"]) {
|
|
134
|
+
if (typeof manifest[key] !== "string" || manifest[key].length === 0)
|
|
135
|
+
errors.push(`"${key}" is required`);
|
|
136
|
+
}
|
|
137
|
+
if (typeof manifest.author !== "string" || manifest.author.length === 0)
|
|
138
|
+
errors.push(`"author" is required`);
|
|
139
|
+
|
|
140
|
+
const hasModels = manifest.targetModels !== undefined && manifest.targetModels !== null;
|
|
141
|
+
const hasCategorys = manifest.targetCategorys !== undefined && manifest.targetCategorys !== null;
|
|
142
|
+
if (!hasModels && !hasCategorys) {
|
|
143
|
+
errors.push(`"targetModels" or "targetCategorys" is required`);
|
|
144
|
+
} else {
|
|
145
|
+
if (hasModels) {
|
|
146
|
+
if (manifest.targetModels !== "all" && (!Array.isArray(manifest.targetModels) || manifest.targetModels.length === 0))
|
|
147
|
+
errors.push(`"targetModels" must be "all" or a non-empty array of model strings`);
|
|
148
|
+
else if (Array.isArray(manifest.targetModels) && manifest.targetModels.some((m) => typeof m !== "string"))
|
|
149
|
+
errors.push(`"targetModels": all entries must be strings`);
|
|
150
|
+
}
|
|
151
|
+
if (hasCategorys) {
|
|
152
|
+
if (manifest.targetCategorys !== "all" && (!Array.isArray(manifest.targetCategorys) || manifest.targetCategorys.length === 0))
|
|
153
|
+
errors.push(`"targetCategorys" must be "all" or a non-empty array`);
|
|
154
|
+
else if (Array.isArray(manifest.targetCategorys)) {
|
|
155
|
+
const invalid = manifest.targetCategorys.filter((c) => !TARGET_CATEGORIES.includes(c));
|
|
156
|
+
if (invalid.length > 0)
|
|
157
|
+
errors.push(`"targetCategorys": invalid value(s) ${invalid.map((c) => `"${c}"`).join(", ")} — allowed: ${TARGET_CATEGORIES.join(", ")}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
validateI18nLabel(manifest.description, "description", errors);
|
|
162
|
+
|
|
163
|
+
validateIcons(manifest.icons, errors);
|
|
164
|
+
validateRuntime(manifest.runtime, errors);
|
|
165
|
+
validateContributes(manifest.contributes, errors);
|
|
166
|
+
|
|
167
|
+
return errors;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Read and validate manifest.json. Throws if any errors are found.
|
|
172
|
+
*/
|
|
173
|
+
export function assertManifest(root) {
|
|
174
|
+
const manifest = readManifest(root);
|
|
175
|
+
if (manifest === null)
|
|
176
|
+
throw new Error("manifest.json parse failed — check JSON syntax");
|
|
177
|
+
if (Object.keys(manifest).length === 0)
|
|
178
|
+
return manifest;
|
|
179
|
+
const errors = validateManifest(manifest);
|
|
180
|
+
if (errors.length > 0)
|
|
181
|
+
throw new Error(`manifest.json errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
|
|
182
|
+
return manifest;
|
|
183
|
+
}
|
package/cli/src/new.mjs
CHANGED
|
@@ -149,7 +149,7 @@ export async function runNew(args) {
|
|
|
149
149
|
// gitignore (templates can't ship a literal .gitignore via npm reliably)
|
|
150
150
|
writeFileSync(
|
|
151
151
|
resolve(target, ".gitignore"),
|
|
152
|
-
["node_modules", "dist", ".venv", "*.zip", "ros2_ws/install", ""].join("\n"),
|
|
152
|
+
["node_modules", "dist", ".muscat-dev", ".venv", "*.zip", "ros2_ws/install", ""].join("\n"),
|
|
153
153
|
);
|
|
154
154
|
|
|
155
155
|
if (!opts["no-install"]) {
|
|
@@ -197,7 +197,7 @@ export async function runNew(args) {
|
|
|
197
197
|
log.ok(`created ${targetName} (type: ${type}, pm: ${pm})`);
|
|
198
198
|
log.info(`\n cd ${targetName}`);
|
|
199
199
|
log.info(` muscat-pk dev # preview in mini-Muscat + back end`);
|
|
200
|
-
log.info(` muscat-pk
|
|
200
|
+
log.info(` muscat-pk build # build + distributable ZIP\n`);
|
|
201
201
|
|
|
202
202
|
// ── post-scaffold environment warnings ───────────────────────────────────
|
|
203
203
|
const noNode = !hasBin("node");
|
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:
|
|
87
|
-
step:
|
|
88
|
-
ok:
|
|
89
|
-
warn:
|
|
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) {
|
|
@@ -8,7 +8,7 @@ A Muscat plugin project. Build, develop, and ship with `muscat-pk` CLI.
|
|
|
8
8
|
muscat-pk dev # UI preview + backend together
|
|
9
9
|
muscat-pk dev:ui # UI preview only
|
|
10
10
|
muscat-pk dev:back # backend only (Python watchfiles / ROS2 Docker)
|
|
11
|
-
muscat-pk
|
|
11
|
+
muscat-pk build # build + distributable ZIP
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
---
|
|
@@ -17,20 +17,22 @@ muscat-pk pack # build a distributable ZIP
|
|
|
17
17
|
|
|
18
18
|
```
|
|
19
19
|
__PLUGIN_DIR__/
|
|
20
|
-
├─
|
|
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
|
-
├─
|
|
26
|
-
└─ requirements.txt # Python deps (auto-pinned to latest on
|
|
27
|
+
├─ README.md # this file (copied to dist/ on build)
|
|
28
|
+
└─ requirements.txt # Python deps (auto-pinned to latest on build)
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
> ROS2 plugins additionally include a `ros2_ws/` directory.
|
|
30
32
|
|
|
31
33
|
---
|
|
32
34
|
|
|
33
|
-
## Frontend (`
|
|
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
|
-
|
|
104
|
-
|
|
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,125 @@ class __PLUGIN_CLASS__Node(Node):
|
|
|
173
173
|
|
|
174
174
|
```jsonc
|
|
175
175
|
{
|
|
176
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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" },
|
|
194
|
+
|
|
195
|
+
// targetModels and/or targetCategorys — at least one is required
|
|
196
|
+
// "all" = every target, or list specific values
|
|
197
|
+
"targetModels": "all",
|
|
198
|
+
// "targetModels": ["C500920", "C500930"],
|
|
199
|
+
"targetCategorys": "all",
|
|
200
|
+
// "targetCategorys": ["manipulate", "amr", "rby1", "vision"],
|
|
181
201
|
|
|
182
202
|
"contributes": [
|
|
183
|
-
|
|
184
|
-
{
|
|
185
|
-
|
|
203
|
+
// program.command — adds a step node to the program tree
|
|
204
|
+
{
|
|
205
|
+
"point": "program.command",
|
|
206
|
+
"icon": "assets/icon.svg",
|
|
207
|
+
"label": { "ko": "내 플러그인", "en": "My Plugin" },
|
|
208
|
+
"ui": "program/content", // must be unique across all contributions
|
|
209
|
+
"commandInfo": {
|
|
210
|
+
"name": "My Plugin",
|
|
211
|
+
"icon": "assets/icon.svg",
|
|
212
|
+
"funcName": "plugin.run",
|
|
213
|
+
"args": {}
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
// toolbar — adds a button to the top toolbar (required)
|
|
217
|
+
{
|
|
218
|
+
"point": "toolbar",
|
|
219
|
+
"icon": "assets/icon.svg",
|
|
220
|
+
"label": { "ko": "내 플러그인", "en": "My Plugin" },
|
|
221
|
+
"ui": "content", // must be unique across all contributions
|
|
222
|
+
"opens": "popover"
|
|
223
|
+
},
|
|
224
|
+
// move.rnb — adds a panel to the Move screen right-side area
|
|
225
|
+
{
|
|
226
|
+
"point": "move.rnb",
|
|
227
|
+
"icon": "assets/icon.svg",
|
|
228
|
+
"label": { "ko": "내 플러그인", "en": "My Plugin" },
|
|
229
|
+
"ui": "move/content" // must be unique across all contributions
|
|
230
|
+
}
|
|
186
231
|
],
|
|
187
232
|
|
|
188
|
-
// "python" | "ros2"
|
|
233
|
+
// "none" | "python" | "ros2"
|
|
189
234
|
"runtime": { "type": "python", "entry": "main.py" }
|
|
190
235
|
}
|
|
191
236
|
```
|
|
192
237
|
|
|
193
238
|
**Contribution points:**
|
|
194
239
|
|
|
195
|
-
| point | Where it appears |
|
|
196
|
-
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
240
|
+
| point | Where it appears | `commandInfo` required |
|
|
241
|
+
|---|---|:---:|
|
|
242
|
+
| `toolbar` | Top toolbar button (must have at least one) | — |
|
|
243
|
+
| `program.command` | Program tree step node | ✓ |
|
|
244
|
+
| `move.rnb` | Move screen right-side panel | — |
|
|
245
|
+
|
|
246
|
+
**`commandInfo` fields** (for `program.command`):
|
|
247
|
+
|
|
248
|
+
| field | type | description |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| `name` | `string` | Display name of the step node |
|
|
251
|
+
| `icon` | `string` | Icon path |
|
|
252
|
+
| `iconClassName` | `string?` | Optional CSS class for the icon |
|
|
253
|
+
| `funcName` | `string?` | Backend function name to call |
|
|
254
|
+
| `args` | `Record<string, any>?` | Default arguments |
|
|
255
|
+
| `disabled` | `boolean?` | Whether the node is disabled |
|
|
256
|
+
| `allowChildren` | `string[]?` | Allowed child commands |
|
|
257
|
+
| `allowParents` | `string[]?` | Allowed parent commands |
|
|
258
|
+
| `notAllowChildren` | `string[]?` | Disallowed child commands |
|
|
259
|
+
| `syncGroupId` | `boolean?` | Sync group ID across children |
|
|
260
|
+
| `disableCopy` | `boolean?` | Prevent copy |
|
|
261
|
+
| `disableDelete` | `boolean?` | Prevent delete |
|
|
262
|
+
| `requireChildren` | `boolean?` | Whether a child command is required |
|
|
263
|
+
| `requireParent` | `boolean?` | Whether a parent command is required |
|
|
264
|
+
| `childrenCommand` | `string?` | Default command type for children |
|
|
265
|
+
|
|
266
|
+
**Validation rules:**
|
|
267
|
+
- `id`, `name`, `version`, `minMuscatVersion` — required non-empty strings
|
|
268
|
+
- `author` — required non-empty string
|
|
269
|
+
- `description` — required (`string` or `{ en (required), ko?, ja?, zh?, vi?, th?, es? }`)
|
|
270
|
+
- `icons` — SVG: single string path ending in `.svg`; PNG/JPG: object with `"16"`, `"48"`, `"128"` all present
|
|
271
|
+
- `targetModels` — `"all"` or a non-empty array of robot model ID strings (at least one of `targetModels` / `targetCategorys` required)
|
|
272
|
+
- `targetCategorys` — `"all"` or a non-empty array of `"manipulate" | "amr" | "rby1" | "vision"` (at least one of `targetModels` / `targetCategorys` required)
|
|
273
|
+
- `contributes` — required array; must contain at least one `toolbar` entry
|
|
274
|
+
- `ui` — each contribution must have a unique value
|
|
275
|
+
- `commandInfo` — required when `point` is `"program.command"`
|
|
276
|
+
|
|
277
|
+
`muscat-pk build` throws on any validation error. `muscat-pk dev` prints warnings.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Localized documentation
|
|
282
|
+
|
|
283
|
+
You can ship per-language README files alongside the main `README.md`. Place them in the project root:
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
README.md # default / English fallback
|
|
287
|
+
README.ko.md # Korean
|
|
288
|
+
README.en.md # explicit English (optional)
|
|
289
|
+
README.ja.md # Japanese
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
All `README*.md` files found at the project root are automatically copied to `dist/` on
|
|
293
|
+
`muscat-pk build`. Muscat displays the file matching the user's locale,
|
|
294
|
+
falling back to `README.md` if none matches.
|
|
200
295
|
|
|
201
296
|
---
|
|
202
297
|
|
|
@@ -211,14 +306,12 @@ numpy==1.26.4
|
|
|
211
306
|
opencv-python
|
|
212
307
|
```
|
|
213
308
|
|
|
214
|
-
|
|
309
|
+
Run `muscat-pk dev` or `muscat-pk dev:back` — dependencies are installed automatically on every startup.
|
|
215
310
|
|
|
216
311
|
- **Python plugin**: installed into `.venv` via `pip install -r requirements.txt`
|
|
217
312
|
- **ROS2 plugin**: installed inside the Docker container via `pip install -r requirements.txt`
|
|
218
313
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
> `muscat-pk build` / `muscat-pk pack` only **pins** versions in `requirements.txt` — it does not run `pip install`.
|
|
314
|
+
> `muscat-pk build` only **pins** versions in `requirements.txt` — it does not run `pip install`.
|
|
222
315
|
|
|
223
316
|
---
|
|
224
317
|
|
|
@@ -237,11 +330,13 @@ muscat-pk type change --to python # ros2 → python
|
|
|
237
330
|
## Shipping
|
|
238
331
|
|
|
239
332
|
```bash
|
|
240
|
-
muscat-pk
|
|
333
|
+
muscat-pk build # build + ZIP
|
|
334
|
+
muscat-pk build --no-zip # build only, no ZIP
|
|
241
335
|
```
|
|
242
336
|
|
|
243
|
-
- Builds `
|
|
337
|
+
- Builds `ui/content.tsx` → `dist/content.js` (minified)
|
|
244
338
|
- Pins `requirements.txt` to latest versions (stable-only on `main` branch)
|
|
339
|
+
- Copies `assets/`, `manifest.json`, `README*.md`, `py/`, and `requirements.txt` into `dist/`
|
|
245
340
|
- ROS2: runs `colcon build` inside the Jazzy container; includes `ros2_ws/install/`
|
|
246
341
|
- Output: `__PLUGIN_DIR__-{version}.zip`
|
|
247
342
|
|
|
@@ -249,10 +344,15 @@ ZIP layout:
|
|
|
249
344
|
|
|
250
345
|
```
|
|
251
346
|
__PLUGIN_DIR__-1.0.0.zip
|
|
252
|
-
├─ dist/
|
|
253
|
-
├─
|
|
254
|
-
├─
|
|
255
|
-
|
|
347
|
+
├─ dist/
|
|
348
|
+
│ ├─ content.js # built UI
|
|
349
|
+
│ ├─ manifest.json
|
|
350
|
+
│ ├─ assets/
|
|
351
|
+
│ ├─ README.md
|
|
352
|
+
│ ├─ README.ko.md # if present
|
|
353
|
+
│ ├─ main.py # from py/
|
|
354
|
+
│ └─ requirements.txt # pinned
|
|
355
|
+
└─ ros2_ws/install/ # ROS2 only
|
|
256
356
|
```
|
|
257
357
|
|
|
258
358
|
---
|
|
@@ -263,8 +363,7 @@ __PLUGIN_DIR__-1.0.0.zip
|
|
|
263
363
|
muscat-pk dev # UI + backend
|
|
264
364
|
muscat-pk dev:ui # UI preview only (preview-host)
|
|
265
365
|
muscat-pk dev:back # backend only (Python: watchfiles / ROS2: Docker)
|
|
266
|
-
muscat-pk build #
|
|
267
|
-
muscat-pk pack # build + ZIP
|
|
366
|
+
muscat-pk build # build → dist/ + distributable ZIP (--no-zip to skip)
|
|
268
367
|
```
|
|
269
368
|
|
|
270
369
|
```bash
|
|
@@ -2,16 +2,40 @@
|
|
|
2
2
|
"id": "__PLUGIN_ID__",
|
|
3
3
|
"name": "__PLUGIN_NAME__",
|
|
4
4
|
"version": "1.0.0",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
5
|
+
"minMuscatVersion": "1.0.0",
|
|
6
|
+
"author": "",
|
|
7
|
+
"description": {
|
|
8
|
+
"ko": "",
|
|
9
|
+
"en": ""
|
|
10
|
+
},
|
|
11
|
+
"icons": "assets/icon.svg",
|
|
12
|
+
"targetModels": "all",
|
|
13
|
+
"targetCategorys": "all",
|
|
7
14
|
"contributes": [
|
|
8
|
-
{ "point": "program.panel", "label": "__PLUGIN_NAME__", "icon": "assets/icon.svg" },
|
|
9
|
-
{ "point": "lnb", "label": "__PLUGIN_NAME__", "icon": "assets/icon.svg" },
|
|
10
15
|
{
|
|
11
|
-
"point": "
|
|
12
|
-
"label": "__PLUGIN_NAME__",
|
|
16
|
+
"point": "program.command",
|
|
13
17
|
"icon": "assets/icon.svg",
|
|
18
|
+
"label": { "ko": "__PLUGIN_NAME__", "en": "__PLUGIN_NAME__" },
|
|
19
|
+
"ui": "program/content",
|
|
20
|
+
"commandInfo": {
|
|
21
|
+
"name": "__PLUGIN_NAME__",
|
|
22
|
+
"icon": "assets/icon.svg",
|
|
23
|
+
"funcName": "plugin.run",
|
|
24
|
+
"args": {}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"point": "toolbar",
|
|
29
|
+
"icon": "assets/icon.svg",
|
|
30
|
+
"label": { "ko": "__PLUGIN_NAME__", "en": "__PLUGIN_NAME__" },
|
|
31
|
+
"ui": "content",
|
|
14
32
|
"opens": "popover"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"point": "move.rnb",
|
|
36
|
+
"icon": "assets/icon.svg",
|
|
37
|
+
"label": { "ko": "__PLUGIN_NAME__", "en": "__PLUGIN_NAME__" },
|
|
38
|
+
"ui": "move/content"
|
|
15
39
|
}
|
|
16
40
|
],
|
|
17
41
|
"runtime": { "type": "__RUNTIME_TYPE__", "entry": "main.py" }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
rainbow-rb-sdk==0.0.9.
|
|
2
|
-
rainbow-rb-flat-buffers==0.0.9.
|
|
3
|
-
rainbow-rb-zenoh==0.0.9.
|
|
1
|
+
rainbow-rb-sdk==0.0.9.dev43
|
|
2
|
+
rainbow-rb-flat-buffers==0.0.9.dev43
|
|
3
|
+
rainbow-rb-zenoh==0.0.9.dev43
|
|
4
4
|
requests==2.34.2
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rainbow-robotics/muscat-plugin-kit",
|
|
3
|
-
"version": "0.1.2-dev.
|
|
3
|
+
"version": "0.1.2-dev.24",
|
|
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.
|
|
34
|
+
"@rainbow-robotics/preview-host": "0.1.2-dev.24",
|
|
35
35
|
"socket.io": "^4.8.1"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
package/cli/src/pack.mjs
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
// muscat-pk pack — build, then create a distributable ZIP with a size report.
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
4
|
-
import { basename, resolve } from "node:path";
|
|
5
|
-
import { buildPlugin, exportRos2Build, pinRequirements } from "./build.mjs";
|
|
6
|
-
import { findPluginRoot, humanSize, log } from "./util.mjs";
|
|
7
|
-
|
|
8
|
-
function dirSize(dir) {
|
|
9
|
-
let total = 0;
|
|
10
|
-
for (const entry of readdirSync(dir)) {
|
|
11
|
-
const full = resolve(dir, entry);
|
|
12
|
-
const st = statSync(full);
|
|
13
|
-
total += st.isDirectory() ? dirSize(full) : st.size;
|
|
14
|
-
}
|
|
15
|
-
return total;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function runPack() {
|
|
19
|
-
const pluginRoot = findPluginRoot();
|
|
20
|
-
log.step("pinning requirements.txt to latest PyPI versions");
|
|
21
|
-
await pinRequirements(pluginRoot);
|
|
22
|
-
await buildPlugin({ root: pluginRoot, minify: true });
|
|
23
|
-
await exportRos2Build(pluginRoot);
|
|
24
|
-
|
|
25
|
-
const manifest = JSON.parse(
|
|
26
|
-
readFileSync(resolve(pluginRoot, "manifest.json"), "utf8"),
|
|
27
|
-
);
|
|
28
|
-
const zipName = `${basename(pluginRoot)}-${manifest.version || "0.0.0"}.zip`;
|
|
29
|
-
const zipPath = resolve(pluginRoot, zipName);
|
|
30
|
-
rmSync(zipPath, { force: true });
|
|
31
|
-
|
|
32
|
-
// dist/ already contains main.py, requirements.txt, manifest.json, assets/.
|
|
33
|
-
const entries = ["dist"];
|
|
34
|
-
if (existsSync(resolve(pluginRoot, "ros2_ws/install"))) {
|
|
35
|
-
entries.push("ros2_ws/install");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
log.step(`packing ${entries.join(", ")} → ${zipName}`);
|
|
39
|
-
try {
|
|
40
|
-
execSync(`zip -r -q "${zipPath}" ${entries.map((e) => `"${e}"`).join(" ")}`, {
|
|
41
|
-
cwd: pluginRoot,
|
|
42
|
-
stdio: "inherit",
|
|
43
|
-
});
|
|
44
|
-
} catch {
|
|
45
|
-
throw new Error("`zip` not found. Install zip or pack manually.");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const zipBytes = statSync(zipPath).size;
|
|
49
|
-
log.ok(`${zipName} ${humanSize(zipBytes)}`);
|
|
50
|
-
for (const e of entries) {
|
|
51
|
-
const full = resolve(pluginRoot, e);
|
|
52
|
-
const size = statSync(full).isDirectory() ? dirSize(full) : statSync(full).size;
|
|
53
|
-
log.info(` ├─ ${e} ${humanSize(size)}`);
|
|
54
|
-
}
|
|
55
|
-
log.warn(
|
|
56
|
-
"robot install checks free space via GET /plugin/capacity (507 if low).",
|
|
57
|
-
);
|
|
58
|
-
}
|