@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.23 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @rainbow-robotics/muscat-plugin-kit (`muscat-pk`)
2
2
 
3
- CLI to scaffold, build, preview and pack Muscat plugins.
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 # src/content.tsx -> dist/content.js (+ manifest, sandbox, assets)
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
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // muscat-pk — Muscat plugin developer CLI.
3
- // Subcommands: new, build, pack, dev, dev:ui, dev:back, update, sdk.
3
+ // Subcommands: new, build, dev, dev:ui, dev:back, update, sdk.
4
4
  import { log } from "../src/util.mjs";
5
5
  import { runNew } from "../src/new.mjs";
6
6
  import { runBuild } from "../src/build.mjs";
7
- import { runPack } from "../src/pack.mjs";
8
7
  import { runDevUi } from "../src/devUi.mjs";
9
8
  import { runDevBack } from "../src/devBack.mjs";
10
9
  import { runDev } from "../src/dev.mjs";
@@ -22,8 +21,7 @@ Usage: muscat-pk <command> [options]
22
21
  Commands:
23
22
  new [dir] Scaffold a new plugin project
24
23
  type change Switch backend type (python <-> ros2)
25
- build Build src/content.tsx -> dist/content.js (+ manifest, assets)
26
- 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)
27
25
  dev Run Socket.IO bridge, dev:ui, and dev:back together
28
26
  dev:ui Preview the plugin in a mini-Muscat (preview-host)
29
27
  dev:back Run the plugin back end (python: watchfiles reload / ros2: Docker)
@@ -42,8 +40,6 @@ async function main() {
42
40
  return await runType(args);
43
41
  case "build":
44
42
  return await runBuild(args);
45
- case "pack":
46
- return await runPack(args);
47
43
  case "dev":
48
44
  return await runDev(args);
49
45
  case "dev:ui":
package/cli/src/build.mjs CHANGED
@@ -1,5 +1,6 @@
1
- // muscat-pk build — bundle src/content.tsx into dist/content.js and stage
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,11 @@
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, readdirSync, 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";
16
17
  import { assertManifest } from "./manifest.mjs";
17
18
 
18
19
  // Find a usable node binary (for running vite as subprocess).
@@ -117,7 +118,7 @@ async function latestPypiVersion(pkg, { stableOnly = false } = {}) {
117
118
  }
118
119
 
119
120
  // Pin unpinned (no version specifier) entries in requirements.txt to the
120
- // latest PyPI release. Runs on explicit `build`/`pack` only — keeps the shipped
121
+ // latest PyPI release. Runs on explicit `build` only — keeps the shipped
121
122
  // plugin reproducible without churning the source file on every dev rebuild.
122
123
  // stableOnly=true is used on main-branch builds to avoid shipping dev versions.
123
124
  export async function pinRequirements(root, { stableOnly = false } = {}) {
@@ -219,14 +220,55 @@ export async function buildPlugin({ root, minify = false } = {}) {
219
220
  return { pluginRoot, distDir };
220
221
  }
221
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
+
222
263
  export async function runBuild(args) {
223
- const minify = args.includes("--minify");
264
+ const noZip = args.includes("--no-zip");
224
265
  const root = findPluginRoot();
225
266
  if (!args.includes("--no-pin")) {
226
267
  const stableOnly = isMainBranch();
227
268
  log.step(`pinning requirements.txt to latest ${stableOnly ? "stable " : ""}PyPI versions`);
228
269
  await pinRequirements(root, { stableOnly });
229
270
  }
230
- await buildPlugin({ root, minify });
271
+ const { pluginRoot } = await buildPlugin({ root, minify: true });
231
272
  await exportRos2Build(root);
273
+ if (!noZip) packZip(pluginRoot);
232
274
  }
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 dist/ (vite doesn't generate these).
59
+ // Write sandbox.html + copy manifest into the dev out dir (vite doesn't generate these).
56
60
  function stageDevAssets(pluginRoot) {
57
- const distDir = resolve(pluginRoot, "dist");
58
- mkdirSync(distDir, { recursive: true });
59
- writeFileSync(resolve(distDir, "sandbox.html"), SANDBOX_HTML, "utf8");
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(distDir, "manifest.json"));
62
- return distDir;
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("vite build --watch (ui/content.tsx → dist/content.js)");
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 distDir = stageDevAssets(pluginRoot);
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(distDir);
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", distDir, "--port", String(port)],
140
+ [serveEntry, "--plugin", outDir, "--port", String(port)],
137
141
  { stdio: "inherit" },
138
142
  );
139
143
 
@@ -7,6 +7,7 @@ const ICON_SIZES = ["16", "48", "128"];
7
7
  const RUNTIME_TYPES = ["none", "python", "ros2"];
8
8
  const ROS_CLIENTS = ["rclpy", "rclcpp"];
9
9
  const I18N_LANGS = ["ko", "en", "ja", "zh", "vi", "th", "es"];
10
+ const TARGET_CATEGORIES = ["manipulate", "amr", "rby1", "vision"];
10
11
 
11
12
  function validateI18nLabel(value, field, errors) {
12
13
  if (value === undefined || value === null) {
@@ -15,7 +16,7 @@ function validateI18nLabel(value, field, errors) {
15
16
  }
16
17
  if (typeof value === "string") return;
17
18
  if (typeof value === "object") {
18
- if (typeof value.en !== "string" || value.en.length === 0)
19
+ if (typeof value.en !== "string")
19
20
  errors.push(`${field}: "en" is required when using object form`);
20
21
  const unknown = Object.keys(value).filter((k) => !I18N_LANGS.includes(k));
21
22
  if (unknown.length > 0)
@@ -133,8 +134,30 @@ export function validateManifest(manifest) {
133
134
  if (typeof manifest[key] !== "string" || manifest[key].length === 0)
134
135
  errors.push(`"${key}" is required`);
135
136
  }
136
- if (typeof manifest.author !== "string")
137
- errors.push(`"author" is required (empty string is allowed)`);
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
+ }
138
161
  validateI18nLabel(manifest.description, "description", errors);
139
162
 
140
163
  validateIcons(manifest.icons, errors);
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 pack # build a distributable ZIP\n`);
200
+ log.info(` muscat-pk build # build + distributable ZIP\n`);
201
201
 
202
202
  // ── post-scaffold environment warnings ───────────────────────────────────
203
203
  const noNode = !hasBin("node");
@@ -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 pack # build a distributable ZIP
11
+ muscat-pk build # build + distributable ZIP
12
12
  ```
13
13
 
14
14
  ---
@@ -25,7 +25,7 @@ __PLUGIN_DIR__/
25
25
  │ └─ icon.svg # icon referenced from manifest
26
26
  ├─ manifest.json # metadata, contribution points, runtime config
27
27
  ├─ README.md # this file (copied to dist/ on build)
28
- └─ requirements.txt # Python deps (auto-pinned to latest on pack)
28
+ └─ requirements.txt # Python deps (auto-pinned to latest on build)
29
29
  ```
30
30
 
31
31
  > ROS2 plugins additionally include a `ros2_ws/` directory.
@@ -192,6 +192,13 @@ class __PLUGIN_CLASS__Node(Node):
192
192
  "icons": "assets/icon.svg",
193
193
  // "icons": { "16": "assets/icon-16.png", "48": "assets/icon-48.png", "128": "assets/icon-128.png" },
194
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"],
201
+
195
202
  "contributes": [
196
203
  // program.command — adds a step node to the program tree
197
204
  {
@@ -258,9 +265,11 @@ class __PLUGIN_CLASS__Node(Node):
258
265
 
259
266
  **Validation rules:**
260
267
  - `id`, `name`, `version`, `minMuscatVersion` — required non-empty strings
261
- - `author` — required (empty string is fine)
268
+ - `author` — required non-empty string
262
269
  - `description` — required (`string` or `{ en (required), ko?, ja?, zh?, vi?, th?, es? }`)
263
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)
264
273
  - `contributes` — required array; must contain at least one `toolbar` entry
265
274
  - `ui` — each contribution must have a unique value
266
275
  - `commandInfo` — required when `point` is `"program.command"`
@@ -281,7 +290,7 @@ README.ja.md # Japanese
281
290
  ```
282
291
 
283
292
  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,
293
+ `muscat-pk build`. Muscat displays the file matching the user's locale,
285
294
  falling back to `README.md` if none matches.
286
295
 
287
296
  ---
@@ -302,7 +311,7 @@ Run `muscat-pk dev` or `muscat-pk dev:back` — dependencies are installed autom
302
311
  - **Python plugin**: installed into `.venv` via `pip install -r requirements.txt`
303
312
  - **ROS2 plugin**: installed inside the Docker container via `pip install -r requirements.txt`
304
313
 
305
- > `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`.
306
315
 
307
316
  ---
308
317
 
@@ -321,12 +330,13 @@ muscat-pk type change --to python # ros2 → python
321
330
  ## Shipping
322
331
 
323
332
  ```bash
324
- muscat-pk pack
333
+ muscat-pk build # build + ZIP
334
+ muscat-pk build --no-zip # build only, no ZIP
325
335
  ```
326
336
 
327
- - Builds `ui/content.tsx` → `dist/content.js`
337
+ - Builds `ui/content.tsx` → `dist/content.js` (minified)
328
338
  - Pins `requirements.txt` to latest versions (stable-only on `main` branch)
329
- - Copies `assets/`, `manifest.json`, and all `README*.md` files to `dist/`
339
+ - Copies `assets/`, `manifest.json`, `README*.md`, `py/`, and `requirements.txt` into `dist/`
330
340
  - ROS2: runs `colcon build` inside the Jazzy container; includes `ros2_ws/install/`
331
341
  - Output: `__PLUGIN_DIR__-{version}.zip`
332
342
 
@@ -339,10 +349,10 @@ __PLUGIN_DIR__-1.0.0.zip
339
349
  │ ├─ manifest.json
340
350
  │ ├─ assets/
341
351
  │ ├─ README.md
342
- │ └─ README.ko.md # if present
343
- ├─ py/
344
- │ └─ main.py
345
- └─ requirements.txt # pinned
352
+ │ ├─ README.ko.md # if present
353
+ │ ├─ main.py # from py/
354
+ │ └─ requirements.txt # pinned
355
+ └─ ros2_ws/install/ # ROS2 only
346
356
  ```
347
357
 
348
358
  ---
@@ -353,8 +363,7 @@ __PLUGIN_DIR__-1.0.0.zip
353
363
  muscat-pk dev # UI + backend
354
364
  muscat-pk dev:ui # UI preview only (preview-host)
355
365
  muscat-pk dev:back # backend only (Python: watchfiles / ROS2: Docker)
356
- muscat-pk build # UI build only → dist/
357
- muscat-pk pack # build + ZIP
366
+ muscat-pk build # build → dist/ + distributable ZIP (--no-zip to skip)
358
367
  ```
359
368
 
360
369
  ```bash
@@ -9,6 +9,8 @@
9
9
  "en": ""
10
10
  },
11
11
  "icons": "assets/icon.svg",
12
+ "targetModels": "all",
13
+ "targetCategorys": "all",
12
14
  "contributes": [
13
15
  {
14
16
  "point": "program.command",
@@ -1,4 +1,4 @@
1
- rainbow-rb-sdk==0.0.9.dev42
2
- rainbow-rb-flat-buffers==0.0.9.dev42
3
- rainbow-rb-zenoh==0.0.9.dev42
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.23",
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.23",
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
- }