@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.
- package/cli/README.md +4 -5
- package/cli/bin/create-muscat-plugin.mjs +1 -1
- package/cli/bin/muscat-pk.mjs +4 -8
- package/cli/docker/Dockerfile.bridge +16 -0
- package/cli/docker/Dockerfile.cpp +21 -0
- package/cli/docker/Dockerfile.python +18 -0
- package/cli/docker/bridge-package.json +8 -0
- package/cli/src/bridge.mjs +91 -0
- package/cli/src/build.mjs +84 -11
- package/cli/src/cpp.mjs +51 -0
- package/cli/src/dev.mjs +12 -6
- package/cli/src/devBack.mjs +145 -46
- package/cli/src/devUi.mjs +15 -11
- package/cli/src/manifest.mjs +33 -4
- package/cli/src/new.mjs +87 -57
- package/cli/src/python.mjs +31 -0
- package/cli/src/socketBridge.mjs +14 -1
- package/cli/src/type.mjs +51 -19
- package/cli/src/util.mjs +10 -0
- package/cli/templates/common/README.md +49 -25
- package/cli/templates/common/manifest.json +4 -1
- package/cli/templates/common/requirements.txt +3 -3
- package/cli/templates/cpp/cpp/CMakeLists.txt +14 -0
- package/cli/templates/cpp/cpp/main.cpp +25 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +17 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +22 -0
- 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
|
|
|
@@ -10,11 +10,10 @@ muscat-pk new my-plugin
|
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
muscat-pk new my-plugin [--type
|
|
14
|
-
muscat-pk build #
|
|
15
|
-
muscat-pk pack # build (minified) + distributable ZIP with a size report
|
|
13
|
+
muscat-pk new my-plugin [--type python|cpp|ros2|ros2-cpp] [--pm npm|yarn|pnpm|bun] [--yes]
|
|
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
|
-
muscat-pk dev:back # run the back end
|
|
16
|
+
muscat-pk dev:back # run the back end in a Docker container (python / cpp / ros2)
|
|
18
17
|
muscat-pk dev # dev:ui + dev:back
|
|
19
18
|
muscat-pk update # update @rainbow-robotics/muscat-plugin-kit and @rainbow-robotics/plugin-ui
|
|
20
19
|
muscat-pk sdk list | sdk update [version] # manage rainbow-rb-sdk (python)
|
package/cli/bin/muscat-pk.mjs
CHANGED
|
@@ -1,10 +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
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";
|
|
@@ -20,10 +19,9 @@ const HELP = `muscat-pk — Muscat plugin kit
|
|
|
20
19
|
Usage: muscat-pk <command> [options]
|
|
21
20
|
|
|
22
21
|
Commands:
|
|
23
|
-
new [dir] Scaffold a new plugin project
|
|
24
|
-
type change Switch backend type (python
|
|
25
|
-
build Build
|
|
26
|
-
pack Build then create a distributable ZIP (with size report)
|
|
22
|
+
new [dir] Scaffold a new plugin project (prompts: author, email, description, type, pm)
|
|
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)
|
|
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":
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Socket.IO bridge container — the frontend (preview-host) and the backend
|
|
2
|
+
# container rendezvous here during `muscat-pk dev`.
|
|
3
|
+
# - frontend (host) → published port localhost:10000
|
|
4
|
+
# - backend container → shares this container's netns (--network container:),
|
|
5
|
+
# so it reaches the bridge at localhost:10000 too
|
|
6
|
+
# Build context is the `cli/` dir so it can copy src/socketBridge.mjs.
|
|
7
|
+
FROM node:20-slim
|
|
8
|
+
|
|
9
|
+
WORKDIR /bridge
|
|
10
|
+
COPY docker/bridge-package.json package.json
|
|
11
|
+
RUN npm install --omit=dev --no-audit --no-fund
|
|
12
|
+
|
|
13
|
+
COPY src/socketBridge.mjs ./socketBridge.mjs
|
|
14
|
+
|
|
15
|
+
EXPOSE 10000
|
|
16
|
+
CMD ["node", "socketBridge.mjs"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Dev/build container for the pure C++ plugin back end.
|
|
2
|
+
# Matches the Muscat robot backend OS (Ubuntu 24.04) so the compiled binary
|
|
3
|
+
# is ABI-compatible with the robot. Used by `muscat-pk dev:back` and `build`
|
|
4
|
+
# — NOT shipped in the plugin ZIP.
|
|
5
|
+
FROM ubuntu:24.04
|
|
6
|
+
|
|
7
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
8
|
+
build-essential \
|
|
9
|
+
cmake \
|
|
10
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
11
|
+
|
|
12
|
+
# TODO(cpp-sdk): install the rainbow cpp SDK (dev package) here, pinned to the
|
|
13
|
+
# robot's version, so plugins build/link against the same SDK as production.
|
|
14
|
+
# Plan: apt package + dynamic link (the robot provides the .so at runtime;
|
|
15
|
+
# we do NOT bundle it into the plugin binary). e.g.:
|
|
16
|
+
# RUN apt-get update && apt-get install -y rainbow-cpp-sdk-dev=<robot-version> \
|
|
17
|
+
# && rm -rf /var/lib/apt/lists/*
|
|
18
|
+
|
|
19
|
+
WORKDIR /plugin
|
|
20
|
+
|
|
21
|
+
CMD ["bash"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Dev container for the Python plugin back end.
|
|
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).
|
|
5
|
+
FROM ubuntu:24.04
|
|
6
|
+
|
|
7
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
8
|
+
python3 \
|
|
9
|
+
python3-pip \
|
|
10
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
11
|
+
|
|
12
|
+
# TODO(cpp-sdk): the Python SDK is a pybind wrapper over the cpp SDK, so the
|
|
13
|
+
# cpp SDK shared lib must be present for the python module to load. Install the
|
|
14
|
+
# rainbow cpp SDK (apt, pinned to the robot's version) here once it exists.
|
|
15
|
+
|
|
16
|
+
WORKDIR /plugin
|
|
17
|
+
|
|
18
|
+
CMD ["bash"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Socket.IO bridge container lifecycle for `muscat-pk dev`.
|
|
2
|
+
// The bridge runs in Docker and publishes port 10000 to the host:
|
|
3
|
+
// - frontend (preview-host, on the host) connects to localhost:10000
|
|
4
|
+
// - the backend container joins via `--network container:<bridge>` and reaches
|
|
5
|
+
// it at localhost:10000 too — no host.docker.internal needed (robust cross-OS).
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { createServer, connect } from "node:net";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { hasBin, kitRoot, log } from "./util.mjs";
|
|
11
|
+
|
|
12
|
+
export const BRIDGE_IMAGE = "muscat-pk-bridge";
|
|
13
|
+
export const BRIDGE_CONTAINER = "muscat-pk-bridge-run";
|
|
14
|
+
export const BRIDGE_PORT = 10000;
|
|
15
|
+
|
|
16
|
+
const DOCKERFILE = resolve(kitRoot, "cli/docker/Dockerfile.bridge");
|
|
17
|
+
const BUILD_CONTEXT = resolve(kitRoot, "cli");
|
|
18
|
+
|
|
19
|
+
function assertDockerReady() {
|
|
20
|
+
if (!hasBin("docker")) {
|
|
21
|
+
throw new Error("Docker not found. `muscat-pk dev` runs the Socket.IO bridge in a container.");
|
|
22
|
+
}
|
|
23
|
+
if (!existsSync(DOCKERFILE)) {
|
|
24
|
+
throw new Error(`kit Dockerfile missing at ${DOCKERFILE} (reinstall muscat-plugin-kit).`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Is a host TCP port already bound? (Catches another bridge or a stray process.)
|
|
29
|
+
function isPortInUse(port) {
|
|
30
|
+
return new Promise((res) => {
|
|
31
|
+
const srv = createServer();
|
|
32
|
+
srv.once("error", (err) => res(err.code === "EADDRINUSE"));
|
|
33
|
+
srv.once("listening", () => srv.close(() => res(false)));
|
|
34
|
+
srv.listen(port, "0.0.0.0");
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Wait until the published port accepts a TCP connection (bridge is ready).
|
|
39
|
+
function waitForPort(port, timeoutMs = 15_000) {
|
|
40
|
+
const start = Date.now();
|
|
41
|
+
return new Promise((res, rej) => {
|
|
42
|
+
const tryOnce = () => {
|
|
43
|
+
const sock = connect(port, "127.0.0.1");
|
|
44
|
+
sock.once("connect", () => { sock.destroy(); res(); });
|
|
45
|
+
sock.once("error", () => {
|
|
46
|
+
sock.destroy();
|
|
47
|
+
if (Date.now() - start > timeoutMs) rej(new Error("bridge container did not become ready in time"));
|
|
48
|
+
else setTimeout(tryOnce, 300);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
tryOnce();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ensureImage() {
|
|
56
|
+
log.step(`docker build -t ${BRIDGE_IMAGE} (first run is slow, then cached)…`);
|
|
57
|
+
const r = spawnSync("docker", ["build", "-t", BRIDGE_IMAGE, "-f", DOCKERFILE, BUILD_CONTEXT], { stdio: "inherit" });
|
|
58
|
+
if (r.status !== 0) throw new Error("docker build failed (bridge image).");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Build the image, preflight the port, and start the bridge container. */
|
|
62
|
+
export async function startBridge() {
|
|
63
|
+
assertDockerReady();
|
|
64
|
+
|
|
65
|
+
// remove our own stale container first, then check the host port is free
|
|
66
|
+
spawnSync("docker", ["rm", "-f", BRIDGE_CONTAINER], { stdio: "ignore" });
|
|
67
|
+
if (await isPortInUse(BRIDGE_PORT)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`port ${BRIDGE_PORT} is already in use.\n` +
|
|
70
|
+
` Another 'muscat-pk dev' may be running, or a process is holding the port.\n` +
|
|
71
|
+
` Stop it, or free the port, then retry.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ensureImage();
|
|
76
|
+
|
|
77
|
+
log.step("starting Socket.IO bridge container…");
|
|
78
|
+
const up = spawnSync(
|
|
79
|
+
"docker",
|
|
80
|
+
["run", "-d", "--name", BRIDGE_CONTAINER, "-p", `${BRIDGE_PORT}:${BRIDGE_PORT}`, BRIDGE_IMAGE],
|
|
81
|
+
{ stdio: "inherit" },
|
|
82
|
+
);
|
|
83
|
+
if (up.status !== 0) throw new Error("failed to start bridge container.");
|
|
84
|
+
|
|
85
|
+
await waitForPort(BRIDGE_PORT);
|
|
86
|
+
log.step(`Socket.IO bridge listening on ws://127.0.0.1:${BRIDGE_PORT}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function stopBridge() {
|
|
90
|
+
spawnSync("docker", ["rm", "-f", BRIDGE_CONTAINER], { stdio: "ignore" });
|
|
91
|
+
}
|
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,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 { chmodSync, 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 { ensureDocker, 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).
|
|
@@ -83,6 +84,33 @@ export async function exportRos2Build(root) {
|
|
|
83
84
|
log.ok("ros2_ws/install ready (will be included in the ZIP)");
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// For cpp plugins, compile cpp/ inside the Ubuntu container (Release) and copy
|
|
88
|
+
// the binary into dist/ so the ZIP carries a robot-compatible executable.
|
|
89
|
+
// Skipped (with a warning) if Docker is unavailable.
|
|
90
|
+
export async function exportCppBuild(root, distDir) {
|
|
91
|
+
if (manifestRuntime(root) !== "cpp") return;
|
|
92
|
+
const { assertDockerReady, ensureImage, cmakeBuild } = await import("./cpp.mjs");
|
|
93
|
+
try {
|
|
94
|
+
assertDockerReady();
|
|
95
|
+
} catch (e) {
|
|
96
|
+
log.warn(`cpp build skipped: ${e.message}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
ensureImage();
|
|
100
|
+
cmakeBuild(root, { buildType: "Release" });
|
|
101
|
+
|
|
102
|
+
const entry = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime?.entry;
|
|
103
|
+
const built = resolve(root, "cpp/build", entry ?? "");
|
|
104
|
+
if (entry && existsSync(built)) {
|
|
105
|
+
const dest = resolve(distDir, entry);
|
|
106
|
+
copyFileSync(built, dest);
|
|
107
|
+
chmodSync(dest, 0o755);
|
|
108
|
+
log.ok(`cpp binary → dist/${entry} (will be included in the ZIP)`);
|
|
109
|
+
} else {
|
|
110
|
+
log.warn(`cpp binary not found at cpp/build/${entry} — skipped`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
86
114
|
const PRERELEASE_RE = /\.(dev|a|b|rc|alpha|beta)\d*/i;
|
|
87
115
|
|
|
88
116
|
function isPrerelease(version) {
|
|
@@ -117,7 +145,7 @@ async function latestPypiVersion(pkg, { stableOnly = false } = {}) {
|
|
|
117
145
|
}
|
|
118
146
|
|
|
119
147
|
// Pin unpinned (no version specifier) entries in requirements.txt to the
|
|
120
|
-
// latest PyPI release. Runs on explicit `build
|
|
148
|
+
// latest PyPI release. Runs on explicit `build` only — keeps the shipped
|
|
121
149
|
// plugin reproducible without churning the source file on every dev rebuild.
|
|
122
150
|
// stableOnly=true is used on main-branch builds to avoid shipping dev versions.
|
|
123
151
|
export async function pinRequirements(root, { stableOnly = false } = {}) {
|
|
@@ -198,9 +226,9 @@ export async function buildPlugin({ root, minify = false } = {}) {
|
|
|
198
226
|
cpSync(assetsSrc, resolve(distDir, "assets"), { recursive: true });
|
|
199
227
|
}
|
|
200
228
|
|
|
201
|
-
// README.md
|
|
229
|
+
// README.md, README.[lang].md, CHANGELOG.md → dist/
|
|
202
230
|
for (const f of readdirSync(pluginRoot)) {
|
|
203
|
-
if (/^README(\.[a-z]{2}(-[A-Z]{2})?)?\.md$/i.test(f)) {
|
|
231
|
+
if (/^README(\.[a-z]{2}(-[A-Z]{2})?)?\.md$/i.test(f) || /^CHANGELOG\.md$/i.test(f)) {
|
|
204
232
|
copyFileSync(resolve(pluginRoot, f), resolve(distDir, f));
|
|
205
233
|
}
|
|
206
234
|
}
|
|
@@ -219,14 +247,59 @@ export async function buildPlugin({ root, minify = false } = {}) {
|
|
|
219
247
|
return { pluginRoot, distDir };
|
|
220
248
|
}
|
|
221
249
|
|
|
250
|
+
function dirSize(dir) {
|
|
251
|
+
let total = 0;
|
|
252
|
+
for (const entry of readdirSync(dir)) {
|
|
253
|
+
const full = resolve(dir, entry);
|
|
254
|
+
const st = statSync(full);
|
|
255
|
+
total += st.isDirectory() ? dirSize(full) : st.size;
|
|
256
|
+
}
|
|
257
|
+
return total;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Zip dist/ (+ ros2_ws/install) into a distributable ZIP with a size report.
|
|
261
|
+
function packZip(pluginRoot) {
|
|
262
|
+
const manifest = JSON.parse(readFileSync(resolve(pluginRoot, "manifest.json"), "utf8"));
|
|
263
|
+
const zipName = `${basename(pluginRoot)}-${manifest.version || "0.0.0"}.zip`;
|
|
264
|
+
const zipPath = resolve(pluginRoot, zipName);
|
|
265
|
+
rmSync(zipPath, { force: true });
|
|
266
|
+
|
|
267
|
+
const entries = ["dist"];
|
|
268
|
+
if (existsSync(resolve(pluginRoot, "ros2_ws/install"))) entries.push("ros2_ws/install");
|
|
269
|
+
|
|
270
|
+
log.step(`packing ${entries.join(", ")} → ${zipName}`);
|
|
271
|
+
try {
|
|
272
|
+
execSync(`zip -r -q "${zipPath}" ${entries.map((e) => `"${e}"`).join(" ")}`, {
|
|
273
|
+
cwd: pluginRoot,
|
|
274
|
+
stdio: "inherit",
|
|
275
|
+
});
|
|
276
|
+
} catch {
|
|
277
|
+
throw new Error("`zip` not found. Install zip or pack manually.");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const zipBytes = statSync(zipPath).size;
|
|
281
|
+
log.ok(`${zipName} ${humanSize(zipBytes)}`);
|
|
282
|
+
for (const e of entries) {
|
|
283
|
+
const full = resolve(pluginRoot, e);
|
|
284
|
+
const size = statSync(full).isDirectory() ? dirSize(full) : statSync(full).size;
|
|
285
|
+
log.info(` ├─ ${e} ${humanSize(size)}`);
|
|
286
|
+
}
|
|
287
|
+
log.warn("robot install checks free space via GET /plugin/capacity (507 if low).");
|
|
288
|
+
}
|
|
289
|
+
|
|
222
290
|
export async function runBuild(args) {
|
|
223
|
-
const
|
|
291
|
+
const noZip = args.includes("--no-zip");
|
|
224
292
|
const root = findPluginRoot();
|
|
293
|
+
// cpp / ros2 compile inside Docker — require it up front (python/none don't).
|
|
294
|
+
const rt = manifestRuntime(root);
|
|
295
|
+
if (rt === "cpp" || rt === "ros2") ensureDocker();
|
|
225
296
|
if (!args.includes("--no-pin")) {
|
|
226
297
|
const stableOnly = isMainBranch();
|
|
227
298
|
log.step(`pinning requirements.txt to latest ${stableOnly ? "stable " : ""}PyPI versions`);
|
|
228
299
|
await pinRequirements(root, { stableOnly });
|
|
229
300
|
}
|
|
230
|
-
await buildPlugin({ root, minify });
|
|
301
|
+
const { pluginRoot, distDir } = await buildPlugin({ root, minify: true });
|
|
231
302
|
await exportRos2Build(root);
|
|
303
|
+
await exportCppBuild(root, distDir);
|
|
304
|
+
if (!noZip) packZip(pluginRoot);
|
|
232
305
|
}
|
package/cli/src/cpp.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Shared C++ Docker helpers (Ubuntu 24.04 + build-essential + cmake).
|
|
2
|
+
// Matches the robot OS so the compiled binary is relocatable to the robot.
|
|
3
|
+
// Used by dev:back (interactive dev loop) and build (export the binary into the ZIP).
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { dirname, resolve } from "node:path";
|
|
7
|
+
import { hasBin, kitRoot, log } from "./util.mjs";
|
|
8
|
+
|
|
9
|
+
export const CPP_DEV_IMAGE = "muscat-pk-cpp-dev";
|
|
10
|
+
|
|
11
|
+
// Dockerfile lives inside the kit — plugin authors never see or edit it.
|
|
12
|
+
const DOCKERFILE = resolve(kitRoot, "cli/docker/Dockerfile.cpp");
|
|
13
|
+
|
|
14
|
+
export function assertDockerReady() {
|
|
15
|
+
if (!hasBin("docker")) {
|
|
16
|
+
throw new Error("Docker not found. C++ builds need Docker (Ubuntu 24.04 + cmake container).");
|
|
17
|
+
}
|
|
18
|
+
if (!existsSync(DOCKERFILE)) {
|
|
19
|
+
throw new Error(`kit Dockerfile missing at ${DOCKERFILE} (reinstall muscat-plugin-kit).`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
|
|
24
|
+
export function ensureImage(image = CPP_DEV_IMAGE) {
|
|
25
|
+
log.step(`docker build -t ${image} (first run is slow, then cached)…`);
|
|
26
|
+
const r = spawnSync(
|
|
27
|
+
"docker",
|
|
28
|
+
["build", "-t", image, "-f", DOCKERFILE, dirname(DOCKERFILE)],
|
|
29
|
+
{ stdio: "inherit" },
|
|
30
|
+
);
|
|
31
|
+
if (r.status !== 0) throw new Error("docker build failed.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Configure + build cpp/ inside the container, producing cpp/build/<entry> on
|
|
36
|
+
* the host (the workspace is mounted). buildType "Debug" for dev, "Release" for
|
|
37
|
+
* export builds that ship in the ZIP.
|
|
38
|
+
*/
|
|
39
|
+
export function cmakeBuild(root, { image = CPP_DEV_IMAGE, buildType = "Debug" } = {}) {
|
|
40
|
+
const script = [
|
|
41
|
+
`cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=${buildType}`,
|
|
42
|
+
"cmake --build cpp/build -j",
|
|
43
|
+
].join(" && ");
|
|
44
|
+
log.step(`cmake build (${buildType}, in container)…`);
|
|
45
|
+
const r = spawnSync(
|
|
46
|
+
"docker",
|
|
47
|
+
["run", "--rm", "-v", `${root}:/plugin`, "-w", "/plugin", image, "bash", "-lc", script],
|
|
48
|
+
{ cwd: root, stdio: "inherit" },
|
|
49
|
+
);
|
|
50
|
+
if (r.status !== 0) throw new Error("cmake build failed.");
|
|
51
|
+
}
|
package/cli/src/dev.mjs
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
// muscat-pk dev — run Socket.IO bridge, dev:ui, and dev:back together.
|
|
1
|
+
// muscat-pk dev — run the Socket.IO bridge (Docker), dev:ui, and dev:back together.
|
|
2
2
|
import { runDevUi } from "./devUi.mjs";
|
|
3
3
|
import { runDevBack } from "./devBack.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import { log } from "./util.mjs";
|
|
4
|
+
import { startBridge, stopBridge, BRIDGE_CONTAINER } from "./bridge.mjs";
|
|
5
|
+
import { ensureDocker, log } from "./util.mjs";
|
|
6
6
|
|
|
7
7
|
export async function runDev(args = []) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
ensureDocker();
|
|
9
|
+
log.step("starting Socket.IO bridge (Docker) + dev:ui + dev:back");
|
|
10
|
+
await startBridge();
|
|
11
|
+
|
|
12
|
+
// Tell dev:back which bridge container to share a netns with, so the backend
|
|
13
|
+
// reaches the bridge at localhost:10000 (no host.docker.internal).
|
|
14
|
+
process.env.MUSCAT_PK_BRIDGE_CONTAINER = BRIDGE_CONTAINER;
|
|
15
|
+
|
|
16
|
+
const cleanup = () => stopBridge();
|
|
11
17
|
process.once("SIGINT", cleanup);
|
|
12
18
|
process.once("SIGTERM", cleanup);
|
|
13
19
|
process.once("exit", cleanup);
|