@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.27 → 0.1.2-dev.28

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.
@@ -21,7 +21,7 @@ Usage: muscat-pk <command> [options]
21
21
  Commands:
22
22
  new [dir] Scaffold a new plugin project (prompts: author, email, description, type, pm)
23
23
  type change Switch backend type (python | cpp | ros2 | ros2-cpp)
24
- build Build ui/content.tsx -> dist/ and zip into a distributable ZIP (--no-zip to skip)
24
+ build Build + lock deps + vendor wheels + zip (--no-zip: skip zip, --no-lock: skip python lock/vendor)
25
25
  dev Run Socket.IO bridge, dev:ui, and dev:back together
26
26
  dev:ui Preview the plugin in a mini-Muscat (preview-host)
27
27
  dev:back Run the plugin back end (python: watchfiles reload / ros2: Docker)
@@ -5,6 +5,7 @@
5
5
  # so it reaches the bridge at localhost:10000 too
6
6
  # Build context is the `cli/` dir so it can copy src/socketBridge.mjs.
7
7
  FROM node:20-slim
8
+ LABEL com.rainbow-robotics.muscat-pk="true"
8
9
 
9
10
  WORKDIR /bridge
10
11
  COPY docker/bridge-package.json package.json
@@ -3,6 +3,7 @@
3
3
  # is ABI-compatible with the robot. Used by `muscat-pk dev:back` and `build`
4
4
  # — NOT shipped in the plugin ZIP.
5
5
  FROM ubuntu:24.04
6
+ LABEL com.rainbow-robotics.muscat-pk="true"
6
7
 
7
8
  RUN apt-get update && apt-get install -y --no-install-recommends \
8
9
  build-essential \
@@ -2,6 +2,7 @@
2
2
  # Matches the Muscat robot backend: ROS2 Jazzy + Python 3.12 (Ubuntu 24.04).
3
3
  # Used by `muscat-pk dev:back` — NOT shipped in the plugin ZIP.
4
4
  FROM ros:jazzy-ros-base
5
+ LABEL com.rainbow-robotics.muscat-pk="true"
5
6
 
6
7
  RUN apt-get update && apt-get install -y --no-install-recommends \
7
8
  python3-pip \
@@ -1,13 +1,15 @@
1
- # Dev container for the Python plugin back end.
1
+ # Dev/build container for the Python plugin back end.
2
2
  # Matches the Muscat robot backend: Ubuntu 24.04 + Python 3.12.
3
- # Used by `muscat-pk dev:back` — NOT shipped in the plugin ZIP (the robot
4
- # pip-installs requirements itself at load time).
3
+ # Used by `muscat-pk dev:back` (hot-reload) and `muscat-pk build` (lock + wheel vendor).
4
+ # NOT shipped in the plugin ZIP — the robot installs deps from dist/wheels/ offline.
5
5
  FROM ubuntu:24.04
6
+ LABEL com.rainbow-robotics.muscat-pk="true"
6
7
 
7
8
  RUN apt-get update && apt-get install -y --no-install-recommends \
8
9
  python3 \
9
10
  python3-pip \
10
- && rm -rf /var/lib/apt/lists/*
11
+ && rm -rf /var/lib/apt/lists/* \
12
+ && pip3 install uv --break-system-packages
11
13
 
12
14
  # TODO(cpp-sdk): the Python SDK is a pybind wrapper over the cpp SDK, so the
13
15
  # cpp SDK shared lib must be present for the python module to load. Install the
@@ -7,7 +7,7 @@ import { spawnSync } from "node:child_process";
7
7
  import { createServer, connect } from "node:net";
8
8
  import { existsSync } from "node:fs";
9
9
  import { resolve } from "node:path";
10
- import { hasBin, kitRoot, log } from "./util.mjs";
10
+ import { hasBin, kitRoot, log, pruneMuskatImages } from "./util.mjs";
11
11
 
12
12
  export const BRIDGE_IMAGE = "muscat-pk-bridge";
13
13
  export const BRIDGE_CONTAINER = "muscat-pk-bridge-run";
@@ -56,6 +56,7 @@ function ensureImage() {
56
56
  log.step(`docker build -t ${BRIDGE_IMAGE} (first run is slow, then cached)…`);
57
57
  const r = spawnSync("docker", ["build", "-t", BRIDGE_IMAGE, "-f", DOCKERFILE, BUILD_CONTEXT], { stdio: "inherit" });
58
58
  if (r.status !== 0) throw new Error("docker build failed (bridge image).");
59
+ pruneMuskatImages();
59
60
  }
60
61
 
61
62
  /** Build the image, preflight the port, and start the bridge container. */
package/cli/src/build.mjs CHANGED
@@ -12,6 +12,7 @@
12
12
  import { chmodSync, cpSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
13
13
  import { createRequire } from "node:module";
14
14
  import { basename, dirname, resolve } from "node:path";
15
+ import { createInterface } from "node:readline/promises";
15
16
  import { execSync, spawnSync } from "node:child_process";
16
17
  import { ensureDocker, findPluginRoot, hasBin, humanSize, log } from "./util.mjs";
17
18
  import { assertManifest } from "./manifest.mjs";
@@ -287,18 +288,79 @@ function packZip(pluginRoot) {
287
288
  log.warn("robot install checks free space via GET /plugin/capacity (507 if low).");
288
289
  }
289
290
 
291
+ /**
292
+ * Ask which target arch(es) to build for.
293
+ * Non-TTY (CI): auto-selects current machine arch.
294
+ * @returns {Promise<string[]>} e.g. ['x86_64'] | ['aarch64'] | ['x86_64','aarch64']
295
+ */
296
+ async function promptArch() {
297
+ if (!process.stdin.isTTY) {
298
+ const { arch } = await import("node:os");
299
+ const machine = arch() === "arm64" ? "aarch64" : "x86_64";
300
+ log.info(`arch auto-selected (non-TTY): ${machine}`);
301
+ return [machine];
302
+ }
303
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
304
+ process.stdout.write(
305
+ "\n Target arch:\n" +
306
+ " 1) amd64 (x86_64)\n" +
307
+ " 2) arm64 (aarch64)\n" +
308
+ " 3) Both (fat ZIP)\n",
309
+ );
310
+ const answer = (await rl.question(" Select [1/2/3, default: 3]: ")).trim() || "3";
311
+ rl.close();
312
+ if (answer === "1") return ["x86_64"];
313
+ if (answer === "2") return ["aarch64"];
314
+ return ["x86_64", "aarch64"];
315
+ }
316
+
317
+ async function promptVersion(root) {
318
+ if (!process.stdin.isTTY) return;
319
+ const manifestPath = resolve(root, "manifest.json");
320
+ if (!existsSync(manifestPath)) return;
321
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
322
+ const current = manifest.version ?? "1.0.0";
323
+
324
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
325
+ const input = (await rl.question(` Version (current: ${current}, leave blank to keep): `)).trim();
326
+ rl.close();
327
+
328
+ if (!input) return;
329
+ manifest.version = input;
330
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
331
+ log.ok(`manifest.json version → ${input}`);
332
+ }
333
+
290
334
  export async function runBuild(args) {
291
335
  const noZip = args.includes("--no-zip");
336
+ const noLock = args.includes("--no-lock");
292
337
  const root = findPluginRoot();
293
- // cpp / ros2 compile inside Docker — require it up front (python/none don't).
294
338
  const rt = manifestRuntime(root);
295
- if (rt === "cpp" || rt === "ros2") ensureDocker();
296
- if (!args.includes("--no-pin")) {
297
- const stableOnly = isMainBranch();
298
- log.step(`pinning requirements.txt to latest ${stableOnly ? "stable " : ""}PyPI versions`);
299
- await pinRequirements(root, { stableOnly });
300
- }
339
+
340
+ // All runtimes except "none" need Docker
341
+ if (rt !== "none") ensureDocker();
342
+
343
+ await promptVersion(root);
344
+ const archs = await promptArch();
345
+
301
346
  const { pluginRoot, distDir } = await buildPlugin({ root, minify: true });
347
+
348
+ // python / rclpy: lock + wheel vendor in Ubuntu 24.04 container
349
+ if (!noLock) {
350
+ const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime ?? {};
351
+ const needsLock =
352
+ manifest.type === "python" ||
353
+ (manifest.type === "ros2" && manifest.rosClient === "rclpy");
354
+ if (needsLock) {
355
+ const { lockAndVendor } = await import("./python.mjs");
356
+ try {
357
+ lockAndVendor(pluginRoot);
358
+ } catch (e) {
359
+ log.warn(`python lock/vendor skipped: ${e.message}`);
360
+ }
361
+ }
362
+ }
363
+
302
364
  await exportRos2Build(root);
303
365
  await exportCppBuild(root, distDir);
304
366
  if (!noZip) packZip(pluginRoot);
package/cli/src/cpp.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { existsSync } from "node:fs";
6
6
  import { dirname, resolve } from "node:path";
7
- import { hasBin, kitRoot, log } from "./util.mjs";
7
+ import { hasBin, kitRoot, log, pruneMuskatImages } from "./util.mjs";
8
8
 
9
9
  export const CPP_DEV_IMAGE = "muscat-pk-cpp-dev";
10
10
 
@@ -29,6 +29,7 @@ export function ensureImage(image = CPP_DEV_IMAGE) {
29
29
  { stdio: "inherit" },
30
30
  );
31
31
  if (r.status !== 0) throw new Error("docker build failed.");
32
+ pruneMuskatImages();
32
33
  }
33
34
 
34
35
  /**
@@ -1,9 +1,9 @@
1
1
  // Shared Python Docker helpers (Ubuntu 24.04 + Python 3.12 — matches the robot).
2
- // Used by dev:back so the dev backend runs in the same environment as the robot.
2
+ // Used by dev:back (hot-reload) and build (lock + wheel vendor).
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { existsSync } from "node:fs";
5
5
  import { dirname, resolve } from "node:path";
6
- import { hasBin, kitRoot, log } from "./util.mjs";
6
+ import { hasBin, kitRoot, log, pruneMuskatImages } from "./util.mjs";
7
7
 
8
8
  export const PYTHON_DEV_IMAGE = "muscat-pk-python-dev";
9
9
 
@@ -28,4 +28,54 @@ export function ensureImage(image = PYTHON_DEV_IMAGE) {
28
28
  { stdio: "inherit" },
29
29
  );
30
30
  if (r.status !== 0) throw new Error("docker build failed.");
31
+ pruneMuskatImages();
32
+ }
33
+
34
+ /**
35
+ * Ubuntu 24.04 + py3.12 컨테이너 안에서:
36
+ * 1. uv pip compile requirements.txt → dist/requirements.lock (전이 포함 완전 lock)
37
+ * 2. pip download -r dist/requirements.lock → dist/wheels/ (타겟 플랫폼 휠 벤더링)
38
+ *
39
+ * 컨테이너 arch = 빌드 머신 arch = 로봇 arch 가 동일해야 네이티브 휠이 일치한다.
40
+ * 순수 Python 패키지(py3-none-any)는 arch 무관.
41
+ * requirements.txt 없으면 조용히 건너뜀.
42
+ */
43
+ export function lockAndVendor(pluginRoot) {
44
+ const reqTxt = resolve(pluginRoot, "requirements.txt");
45
+ if (!existsSync(reqTxt)) {
46
+ log.warn("requirements.txt not found — lock/vendor skipped");
47
+ return;
48
+ }
49
+
50
+ ensureImage();
51
+
52
+ log.step("generating lock (uv pip compile)…");
53
+ const lockResult = spawnSync(
54
+ "docker",
55
+ [
56
+ "run", "--rm",
57
+ "-v", `${pluginRoot}:/plugin`,
58
+ PYTHON_DEV_IMAGE,
59
+ "bash", "-c",
60
+ "set -e && cd /plugin && mkdir -p dist && uv pip compile requirements.txt --python-version 3.12 -o dist/requirements.lock",
61
+ ],
62
+ { stdio: "inherit" },
63
+ );
64
+ if (lockResult.status !== 0) throw new Error("lock generation failed (uv pip compile)");
65
+ log.ok("dist/requirements.lock ready");
66
+
67
+ log.step("vendoring wheels (pip download)…");
68
+ const vendorResult = spawnSync(
69
+ "docker",
70
+ [
71
+ "run", "--rm",
72
+ "-v", `${pluginRoot}:/plugin`,
73
+ PYTHON_DEV_IMAGE,
74
+ "bash", "-c",
75
+ "set -e && cd /plugin && mkdir -p dist/wheels && pip3 download -r dist/requirements.lock --only-binary=:all: -d dist/wheels",
76
+ ],
77
+ { stdio: "inherit" },
78
+ );
79
+ if (vendorResult.status !== 0) throw new Error("wheel vendoring failed (pip download)");
80
+ log.ok("dist/wheels/ ready");
31
81
  }
package/cli/src/ros2.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { existsSync, readdirSync } from "node:fs";
5
5
  import { dirname, resolve } from "node:path";
6
- import { hasBin, kitRoot, log } from "./util.mjs";
6
+ import { hasBin, kitRoot, log, pruneMuskatImages } from "./util.mjs";
7
7
 
8
8
  export const ROS2_DEV_IMAGE = "muscat-pk-ros2-dev";
9
9
 
@@ -39,6 +39,7 @@ export function ensureImage(image = ROS2_DEV_IMAGE) {
39
39
  { stdio: "inherit" },
40
40
  );
41
41
  if (r.status !== 0) throw new Error("docker build failed.");
42
+ pruneMuskatImages();
42
43
  }
43
44
 
44
45
  /**
package/cli/src/util.mjs CHANGED
@@ -101,6 +101,18 @@ export function ensureDocker() {
101
101
  throw new Error("Docker is required for dev / dev:back / build.");
102
102
  }
103
103
 
104
+ /**
105
+ * Remove dangling (untagged) muscat-pk images left behind after a rebuild.
106
+ * Uses label filter so only our images are touched — other Docker images are safe.
107
+ */
108
+ export function pruneMuskatImages() {
109
+ spawnSync(
110
+ "docker",
111
+ ["image", "prune", "--force", "--filter", "label=com.rainbow-robotics.muscat-pk=true"],
112
+ { stdio: "ignore" },
113
+ );
114
+ }
115
+
104
116
  export function humanSize(bytes) {
105
117
  if (bytes < 1024) return `${bytes} B`;
106
118
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-robotics/muscat-plugin-kit",
3
- "version": "0.1.2-dev.27",
3
+ "version": "0.1.2-dev.28",
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.27",
34
+ "@rainbow-robotics/preview-host": "0.1.2-dev.28",
35
35
  "socket.io": "^4.8.1"
36
36
  },
37
37
  "optionalDependencies": {