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

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,7 +1,14 @@
1
1
  import { createServer } from "node:http";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { Server } from "socket.io";
4
- import { log } from "./util.mjs";
5
+
6
+ // Self-contained logger — this module also runs standalone inside the bridge
7
+ // Docker container, where ./util.mjs is not available.
8
+ const log = {
9
+ step: (...m) => console.log("›", ...m),
10
+ warn: (...m) => console.warn("⚠", ...m),
11
+ };
5
12
 
6
13
  const DEFAULT_ACTION_TIMEOUT_MS = 10_000;
7
14
 
@@ -114,3 +121,9 @@ export function startSocketBridge({ port = 10000, host = "0.0.0.0" } = {}) {
114
121
  });
115
122
  });
116
123
  }
124
+
125
+ // When executed directly (inside the bridge Docker container), start the server.
126
+ if (process.argv[1] && process.argv[1] === fileURLToPath(import.meta.url)) {
127
+ const port = Number(process.env.PORT) || 10000;
128
+ startSocketBridge({ port });
129
+ }
package/cli/src/type.mjs CHANGED
@@ -1,13 +1,21 @@
1
1
  // muscat-pk type change — switch an existing plugin's backend type.
2
- // python ↔ ros2. python→ros2 adds the ros2_ws/ overlay; ros2→python flips
3
- // the manifest type and leaves ros2_ws/ in place (never deletes user code).
2
+ // python | cpp | ros2 (rclpy) | ros2-cpp (rclcpp).
3
+ // Adds the matching backend overlay; never deletes existing user code
4
+ // (e.g. ros2_ws/ or cpp/ are left in place when switching away).
4
5
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
6
  import { createInterface } from "node:readline/promises";
6
7
  import { basename, resolve } from "node:path";
7
8
  import { findPluginRoot, log } from "./util.mjs";
8
- import { applyTokens, deriveNames, overlayRos2 } from "./new.mjs";
9
+ import {
10
+ TYPE_CHOICES,
11
+ applyTokens,
12
+ deriveNames,
13
+ overlayCpp,
14
+ overlayRos2,
15
+ runtimeForChoice,
16
+ } from "./new.mjs";
9
17
 
10
- const TYPES = ["python", "ros2"];
18
+ const TYPES = Object.keys(TYPE_CHOICES);
11
19
 
12
20
  async function prompt(question, fallback) {
13
21
  if (!process.stdin.isTTY) return fallback;
@@ -23,44 +31,54 @@ function readManifest(root) {
23
31
  return { path, json: JSON.parse(readFileSync(path, "utf8")) };
24
32
  }
25
33
 
34
+ // Derive the current `--type`-style key from a manifest runtime block.
35
+ function currentTypeKey(runtime) {
36
+ if (runtime?.type === "ros2") return runtime.rosClient === "rclcpp" ? "ros2-cpp" : "ros2";
37
+ return runtime?.type ?? "python";
38
+ }
39
+
26
40
  export async function runType(args) {
27
- // accept `type change`, `type`, `type --to ros2`
41
+ // accept `type change`, `type`, `type --to ros2-cpp`
28
42
  const sub = args[0];
29
43
  const toFlagIdx = args.indexOf("--to");
30
44
  let to = toFlagIdx >= 0 ? args[toFlagIdx + 1] : undefined;
31
45
 
32
46
  const root = findPluginRoot();
33
47
  const { path, json } = readManifest(root);
34
- const current = json.runtime?.type ?? "python";
48
+ const current = currentTypeKey(json.runtime);
35
49
 
36
50
  if (sub && sub !== "change") {
37
- log.warn(`unknown subcommand '${sub}'. Usage: muscat-pk type change [--to python|ros2]`);
51
+ log.warn(`unknown subcommand '${sub}'. Usage: muscat-pk type change [--to python|cpp|ros2|ros2-cpp]`);
38
52
  }
39
53
 
40
54
  if (!to) {
41
55
  console.log(`\n현재 타입: ${current}`);
42
56
  console.log("변경할 타입을 선택하세요:");
43
57
  console.log(" 1) Python");
44
- console.log(" 2) ROS2(rclpy)");
45
- const choice = await prompt("\n선택 [1/2]: ", "");
46
- to = choice === "1" ? "python" : choice === "2" ? "ros2" : undefined;
58
+ console.log(" 2) C++");
59
+ console.log(" 3) ROS2 (rclpy / Python)");
60
+ console.log(" 4) ROS2 (rclcpp / C++)");
61
+ const choice = await prompt("\n선택 [1/2/3/4]: ", "");
62
+ to = { "1": "python", "2": "cpp", "3": "ros2", "4": "ros2-cpp" }[choice];
47
63
  }
48
64
 
49
65
  if (!TYPES.includes(to)) {
50
- throw new Error(`invalid type. choose python or ros2.`);
66
+ throw new Error(`invalid type. choose python | cpp | ros2 | ros2-cpp.`);
51
67
  }
52
68
  if (to === current) {
53
69
  log.info(`already '${current}' — nothing to change.`);
54
70
  return;
55
71
  }
56
72
 
57
- if (to === "ros2") {
58
- const { slug, pluginName, pluginClass, pluginId } = deriveNames(root);
73
+ const runtimeChoice = TYPE_CHOICES[to];
74
+ const { slug, pluginName, pluginClass, pluginId } = deriveNames(root);
75
+
76
+ if (runtimeChoice.type === "ros2") {
59
77
  if (existsSync(resolve(root, "ros2_ws"))) {
60
78
  log.warn("ros2_ws/ already exists — keeping it, only flipping manifest type.");
61
79
  } else {
62
- log.step("adding ros2_ws/ overlay");
63
- overlayRos2(root, slug);
80
+ log.step(`adding ros2_ws/ overlay (${runtimeChoice.rosClient})`);
81
+ overlayRos2(root, slug, runtimeChoice.rosClient);
64
82
  applyTokens(root, {
65
83
  __PLUGIN_ID__: pluginId,
66
84
  __PLUGIN_NAME__: pluginName,
@@ -69,14 +87,28 @@ export async function runType(args) {
69
87
  __RUNTIME_TYPE__: "ros2",
70
88
  });
71
89
  }
90
+ } else if (runtimeChoice.type === "cpp") {
91
+ if (existsSync(resolve(root, "cpp"))) {
92
+ log.warn("cpp/ already exists — keeping it, only flipping manifest type.");
93
+ } else {
94
+ log.step("adding cpp/ overlay");
95
+ overlayCpp(root);
96
+ applyTokens(root, {
97
+ __PLUGIN_ID__: pluginId,
98
+ __PLUGIN_NAME__: pluginName,
99
+ __PLUGIN_SLUG__: slug,
100
+ __PLUGIN_CLASS__: pluginClass,
101
+ __RUNTIME_TYPE__: "cpp",
102
+ });
103
+ }
72
104
  } else {
73
- // ros2 → python
74
- if (existsSync(resolve(root, "ros2_ws"))) {
75
- log.warn("ros2_ws/ left in place (not deleted). Remove it manually if unused.");
105
+ // → python. Leave any ros2_ws/ or cpp/ in place (never delete user code).
106
+ if (existsSync(resolve(root, "ros2_ws")) || existsSync(resolve(root, "cpp"))) {
107
+ log.warn("existing backend dir left in place (not deleted). Remove it manually if unused.");
76
108
  }
77
109
  }
78
110
 
79
- json.runtime = { ...json.runtime, type: to };
111
+ json.runtime = runtimeForChoice(runtimeChoice, slug);
80
112
  writeFileSync(path, JSON.stringify(json, null, 2) + "\n");
81
113
  log.ok(`${basename(root)} type: ${current} → ${to}`);
82
114
  }
package/cli/src/util.mjs CHANGED
@@ -91,6 +91,16 @@ export const log = {
91
91
  error: (...m) => console.error(`${RED}✖${RESET}`, ...m.map(s => `${RED}${s}${RESET}`)),
92
92
  };
93
93
 
94
+ // dev / dev:back / build run the plugin back end, the dev bridge, and cpp/ros2
95
+ // compilation in Docker. Show a clear install hint when Docker is missing.
96
+ export function ensureDocker() {
97
+ if (hasBin("docker")) return;
98
+ log.warn("Docker not found.");
99
+ log.warn(" muscat-pk runs the plugin back end, the dev bridge, and cpp/ros2 builds in Docker.");
100
+ log.warn(" Install Docker, then retry: https://docs.docker.com/get-docker/");
101
+ throw new Error("Docker is required for dev / dev:back / build.");
102
+ }
103
+
94
104
  export function humanSize(bytes) {
95
105
  if (bytes < 1024) return `${bytes} B`;
96
106
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -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.
@@ -102,8 +102,9 @@ connect(App);
102
102
  ## Backend (`py/main.py`)
103
103
 
104
104
  The backend communicates with the frontend using `rainbow.rb_sdk.RBPluginCall`.
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.
105
+ `muscat-pk dev` starts the Socket.IO bridge in Docker (published on port 10000) and
106
+ runs the backend container sharing its network, so the SDK connects at localhost:10000.
107
+ `py/main.py` should contain plugin logic only and must not start its own server.
107
108
 
108
109
  ### Python plugin
109
110
 
@@ -192,6 +193,13 @@ class __PLUGIN_CLASS__Node(Node):
192
193
  "icons": "assets/icon.svg",
193
194
  // "icons": { "16": "assets/icon-16.png", "48": "assets/icon-48.png", "128": "assets/icon-128.png" },
194
195
 
196
+ // targetModels and/or targetCategorys — at least one is required
197
+ // "all" = every target, or list specific values
198
+ "targetModels": "all",
199
+ // "targetModels": ["C500920", "C500930"],
200
+ "targetCategorys": "all",
201
+ // "targetCategorys": ["manipulate", "amr", "rby1", "vision"],
202
+
195
203
  "contributes": [
196
204
  // program.command — adds a step node to the program tree
197
205
  {
@@ -223,11 +231,26 @@ class __PLUGIN_CLASS__Node(Node):
223
231
  }
224
232
  ],
225
233
 
226
- // "none" | "python" | "ros2"
234
+ // runtime — one of:
235
+ // { "type": "none" }
236
+ // { "type": "python", "entry": "main.py" }
237
+ // { "type": "cpp", "entry": "<executable name>" }
238
+ // { "type": "ros2", "rosClient": "rclpy" | "rclcpp", "package": "...", "launch": "..._node" }
227
239
  "runtime": { "type": "python", "entry": "main.py" }
228
240
  }
229
241
  ```
230
242
 
243
+ **Backend types:**
244
+
245
+ | type | source dir | build / run | entry |
246
+ |---|---|---|---|
247
+ | `python` | `py/` | pip + run in Docker | `main.py` |
248
+ | `cpp` | `cpp/` | CMake in Docker | built executable name |
249
+ | `ros2` (rclpy) | `ros2_ws/` | colcon (ament_python) in Docker | — |
250
+ | `ros2` (rclcpp) | `ros2_ws/` | colcon (ament_cmake) in Docker | — |
251
+
252
+ > Every back end runs in a Docker container matching the robot (Ubuntu 24.04 + Python 3.12), so dev behaves like production and compiled binaries are ABI-compatible. **Docker is required** for all backend types (only a frontend-only `none` plugin skips it). No local Python/toolchain setup is needed.
253
+
231
254
  **Contribution points:**
232
255
 
233
256
  | point | Where it appears | `commandInfo` required |
@@ -258,9 +281,11 @@ class __PLUGIN_CLASS__Node(Node):
258
281
 
259
282
  **Validation rules:**
260
283
  - `id`, `name`, `version`, `minMuscatVersion` — required non-empty strings
261
- - `author` — required (empty string is fine)
284
+ - `author` — required non-empty string
262
285
  - `description` — required (`string` or `{ en (required), ko?, ja?, zh?, vi?, th?, es? }`)
263
286
  - `icons` — SVG: single string path ending in `.svg`; PNG/JPG: object with `"16"`, `"48"`, `"128"` all present
287
+ - `targetModels` — `"all"` or a non-empty array of robot model ID strings (at least one of `targetModels` / `targetCategorys` required)
288
+ - `targetCategorys` — `"all"` or a non-empty array of `"manipulate" | "amr" | "rby1" | "vision"` (at least one of `targetModels` / `targetCategorys` required)
264
289
  - `contributes` — required array; must contain at least one `toolbar` entry
265
290
  - `ui` — each contribution must have a unique value
266
291
  - `commandInfo` — required when `point` is `"program.command"`
@@ -281,7 +306,7 @@ README.ja.md # Japanese
281
306
  ```
282
307
 
283
308
  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,
309
+ `muscat-pk build`. Muscat displays the file matching the user's locale,
285
310
  falling back to `README.md` if none matches.
286
311
 
287
312
  ---
@@ -297,36 +322,36 @@ numpy==1.26.4
297
322
  opencv-python
298
323
  ```
299
324
 
300
- Run `muscat-pk dev` or `muscat-pk dev:back` — dependencies are installed automatically on every startup.
301
-
302
- - **Python plugin**: installed into `.venv` via `pip install -r requirements.txt`
303
- - **ROS2 plugin**: installed inside the Docker container via `pip install -r requirements.txt`
325
+ Run `muscat-pk dev` or `muscat-pk dev:back` — dependencies are installed automatically inside the dev container (`pip install -r requirements.txt`) on every startup. No local Python or venv needed.
304
326
 
305
- > `muscat-pk build` / `muscat-pk pack` only **pins** versions in `requirements.txt` — it does not run `pip install`.
327
+ > `muscat-pk build` only **pins** versions in `requirements.txt` — it does not run `pip install` (the robot installs them at load time).
306
328
 
307
329
  ---
308
330
 
309
331
  ## Switching backend type
310
332
 
311
333
  ```bash
312
- muscat-pk type change # interactive
313
- muscat-pk type change --to ros2 # python → ros2
314
- muscat-pk type change --to python # ros2 → python
334
+ muscat-pk type change # interactive
335
+ muscat-pk type change --to cpp # → C++
336
+ muscat-pk type change --to ros2 # → ROS2 (rclpy / Python)
337
+ muscat-pk type change --to ros2-cpp # → ROS2 (rclcpp / C++)
338
+ muscat-pk type change --to python # → Python
315
339
  ```
316
340
 
317
- > `ros2_ws/` is not deleted when switching back to python. Remove it manually if no longer needed.
341
+ > Existing backend dirs (`ros2_ws/`, `cpp/`) are not deleted when switching away. Remove them manually if no longer needed.
318
342
 
319
343
  ---
320
344
 
321
345
  ## Shipping
322
346
 
323
347
  ```bash
324
- muscat-pk pack
348
+ muscat-pk build # build + ZIP
349
+ muscat-pk build --no-zip # build only, no ZIP
325
350
  ```
326
351
 
327
- - Builds `ui/content.tsx` → `dist/content.js`
352
+ - Builds `ui/content.tsx` → `dist/content.js` (minified)
328
353
  - Pins `requirements.txt` to latest versions (stable-only on `main` branch)
329
- - Copies `assets/`, `manifest.json`, and all `README*.md` files to `dist/`
354
+ - Copies `assets/`, `manifest.json`, `README*.md`, `py/`, and `requirements.txt` into `dist/`
330
355
  - ROS2: runs `colcon build` inside the Jazzy container; includes `ros2_ws/install/`
331
356
  - Output: `__PLUGIN_DIR__-{version}.zip`
332
357
 
@@ -339,10 +364,10 @@ __PLUGIN_DIR__-1.0.0.zip
339
364
  │ ├─ manifest.json
340
365
  │ ├─ assets/
341
366
  │ ├─ README.md
342
- │ └─ README.ko.md # if present
343
- ├─ py/
344
- │ └─ main.py
345
- └─ requirements.txt # pinned
367
+ │ ├─ README.ko.md # if present
368
+ │ ├─ main.py # from py/
369
+ │ └─ requirements.txt # pinned
370
+ └─ ros2_ws/install/ # ROS2 only
346
371
  ```
347
372
 
348
373
  ---
@@ -353,8 +378,7 @@ __PLUGIN_DIR__-1.0.0.zip
353
378
  muscat-pk dev # UI + backend
354
379
  muscat-pk dev:ui # UI preview only (preview-host)
355
380
  muscat-pk dev:back # backend only (Python: watchfiles / ROS2: Docker)
356
- muscat-pk build # UI build only → dist/
357
- muscat-pk pack # build + ZIP
381
+ muscat-pk build # build → dist/ + distributable ZIP (--no-zip to skip)
358
382
  ```
359
383
 
360
384
  ```bash
@@ -3,12 +3,15 @@
3
3
  "name": "__PLUGIN_NAME__",
4
4
  "version": "1.0.0",
5
5
  "minMuscatVersion": "1.0.0",
6
- "author": "",
6
+ "author": "__PLUGIN_AUTHOR__",
7
+ "email": "__PLUGIN_EMAIL__",
7
8
  "description": {
8
9
  "ko": "",
9
10
  "en": ""
10
11
  },
11
12
  "icons": "assets/icon.svg",
13
+ "targetModels": "all",
14
+ "targetCategorys": "all",
12
15
  "contributes": [
13
16
  {
14
17
  "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
@@ -0,0 +1,14 @@
1
+ cmake_minimum_required(VERSION 3.16)
2
+ project(__PLUGIN_SLUG__ CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 17)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+
7
+ # The executable name must match runtime.entry in manifest.json.
8
+ add_executable(__PLUGIN_SLUG__ main.cpp)
9
+
10
+ # TODO(cpp-sdk): link the rainbow cpp SDK dynamically (the robot provides the
11
+ # .so at runtime — do NOT bundle/static-link it). Once the SDK ships an apt
12
+ # package with CMake config, enable:
13
+ # find_package(rainbow_cpp_sdk REQUIRED)
14
+ # target_link_libraries(__PLUGIN_SLUG__ PRIVATE rainbow::cpp_sdk)
@@ -0,0 +1,25 @@
1
+ // __PLUGIN_NAME__ — C++ plugin back end.
2
+ //
3
+ // The C++ plugin SDK is not available yet, so this is a build/run skeleton.
4
+ // Once the SDK lands, connect to the plugin gateway here (the host exposes it
5
+ // at the RB_PLUGIN_GATEWAY / RB_PLUGIN_ID environment variables) and wire up
6
+ // pub / sub / serve, mirroring the Python rainbow.rb_sdk.RBPluginCall API.
7
+
8
+ #include <chrono>
9
+ #include <cstdlib>
10
+ #include <iostream>
11
+ #include <thread>
12
+
13
+ int main() {
14
+ const char* plugin_id = std::getenv("RB_PLUGIN_ID");
15
+ std::cout << "[__PLUGIN_NAME__] backend ready"
16
+ << (plugin_id ? std::string(" (id=") + plugin_id + ")" : "")
17
+ << std::endl;
18
+
19
+ // TODO: connect to the plugin gateway and register handlers once the
20
+ // C++ SDK is available. For now, just keep the process alive.
21
+ while (true) {
22
+ std::this_thread::sleep_for(std::chrono::seconds(1));
23
+ }
24
+ return 0;
25
+ }
@@ -0,0 +1,17 @@
1
+ cmake_minimum_required(VERSION 3.16)
2
+ project(__PLUGIN_SLUG__)
3
+
4
+ if(NOT CMAKE_CXX_STANDARD)
5
+ set(CMAKE_CXX_STANDARD 17)
6
+ endif()
7
+
8
+ find_package(ament_cmake REQUIRED)
9
+ find_package(rclcpp REQUIRED)
10
+
11
+ add_executable(__PLUGIN_SLUG___node src/node.cpp)
12
+ ament_target_dependencies(__PLUGIN_SLUG___node rclcpp)
13
+
14
+ install(TARGETS __PLUGIN_SLUG___node
15
+ DESTINATION lib/${PROJECT_NAME})
16
+
17
+ ament_package()
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0"?>
2
+ <package format="3">
3
+ <name>__PLUGIN_SLUG__</name>
4
+ <version>0.0.1</version>
5
+ <description>__PLUGIN_NAME__ ROS2 node (rclcpp)</description>
6
+ <maintainer email="todo@example.com">maintainer</maintainer>
7
+ <license>Proprietary</license>
8
+ <buildtool_depend>ament_cmake</buildtool_depend>
9
+ <depend>rclcpp</depend>
10
+ <export><build_type>ament_cmake</build_type></export>
11
+ </package>
@@ -0,0 +1,22 @@
1
+ // __PLUGIN_NAME__ — ROS2 node (rclcpp).
2
+ //
3
+ // The node is launched with `--ros-args -r __ns:=/{slug}` automatically by
4
+ // dev:back. ROS2 topics are bridged to the plugin communication layer, so a
5
+ // plain rclcpp node is all you need — relative topic names get the namespace
6
+ // prefix applied automatically.
7
+ #include "rclcpp/rclcpp.hpp"
8
+
9
+ class __PLUGIN_CLASS__Node : public rclcpp::Node {
10
+ public:
11
+ __PLUGIN_CLASS__Node() : Node("__PLUGIN_SLUG___node") {
12
+ RCLCPP_INFO(this->get_logger(), "__PLUGIN_NAME__ node started");
13
+ // create publishers / subscribers / services here
14
+ }
15
+ };
16
+
17
+ int main(int argc, char** argv) {
18
+ rclcpp::init(argc, argv);
19
+ rclcpp::spin(std::make_shared<__PLUGIN_CLASS__Node>());
20
+ rclcpp::shutdown();
21
+ return 0;
22
+ }
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.25",
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.25",
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
- }