@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.39 → 0.1.2-dev.40

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.
@@ -6,19 +6,19 @@ import { resolve, relative } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
8
  const root = resolve(fileURLToPath(import.meta.url), "..");
9
- const templatesDir = resolve(root, "templates");
10
9
  const out = resolve(root, "src/templates-embedded.mjs");
11
10
 
12
11
  const files = {};
13
- function walk(dir) {
14
- for (const name of readdirSync(dir)) {
15
- const full = resolve(dir, name);
16
- if (statSync(full).isDirectory()) { walk(full); continue; }
17
- const key = relative(templatesDir, full);
12
+ function walk(baseDir, prefix) {
13
+ for (const name of readdirSync(baseDir)) {
14
+ const full = resolve(baseDir, name);
15
+ const key = prefix + name;
16
+ if (statSync(full).isDirectory()) { walk(full + "/", key + "/"); continue; }
18
17
  files[key] = readFileSync(full, "base64");
19
18
  }
20
19
  }
21
- walk(templatesDir);
20
+ walk(resolve(root, "templates") + "/", "");
21
+ walk(resolve(root, "docker") + "/", "docker/");
22
22
 
23
23
  writeFileSync(out,
24
24
  `// AUTO-GENERATED by cli/bundle-templates.mjs — do not edit.\n` +
package/cli/src/build.mjs CHANGED
@@ -17,11 +17,13 @@ import { execSync, spawnSync } from "node:child_process";
17
17
  import { ensureDocker, findPluginRoot, hasBin, humanSize, log, parseArgs } from "./util.mjs";
18
18
  import { assertManifest, manifestRuntime } from "./manifest.mjs";
19
19
 
20
- // Find a usable node binary (for running vite as subprocess).
21
- function findNodeBin() {
22
- if (typeof Bun === "undefined") return process.execPath; // already node
23
- for (const c of ["node", "node20", "node18"]) {
24
- if (hasBin(c)) return c;
20
+ // Find a usable JS runtime for running vite as a subprocess.
21
+ // In binary (Bun) mode, try `bun` first (it can run vite directly), then node.
22
+ // In node mode, use the current process.
23
+ function findJsRuntime() {
24
+ if (typeof Bun === "undefined") return { bin: process.execPath, isBun: false };
25
+ for (const c of ["bun", "node", "node20", "node18"]) {
26
+ if (hasBin(c)) return { bin: c, isBun: c === "bun" };
25
27
  }
26
28
  return null;
27
29
  }
@@ -37,21 +39,30 @@ function findViteBin(pluginRoot) {
37
39
  }
38
40
  }
39
41
 
40
- // Run vite build in the plugin project.
41
- // Returns true on success, false if vite/node not available.
42
+ // Run vite build in the plugin project. Throws if runtime or vite not found.
42
43
  function runViteBuild(pluginRoot, { mode = "production" } = {}) {
43
- const node = findNodeBin();
44
- if (!node) return false;
44
+ const runtime = findJsRuntime();
45
+ if (!runtime) {
46
+ throw new Error(
47
+ "Node.js 20+ (or Bun) is required to build the plugin UI.\n" +
48
+ " Install Node.js: https://nodejs.org\n" +
49
+ " Then run: npm install (inside the plugin directory)",
50
+ );
51
+ }
45
52
  const viteBin = findViteBin(pluginRoot);
46
- if (!viteBin || !existsSync(viteBin)) return false;
53
+ if (!viteBin || !existsSync(viteBin)) {
54
+ throw new Error(
55
+ "vite not found in this plugin's node_modules.\n" +
56
+ ` Run: npm install (inside ${pluginRoot})`,
57
+ );
58
+ }
47
59
 
48
60
  log.step(`vite build (mode: ${mode})`);
49
- const r = spawnSync(node, [viteBin, "build", "--mode", mode], {
50
- cwd: pluginRoot,
51
- stdio: "inherit",
52
- });
61
+ // bun can run vite directly; node needs the explicit bin path
62
+ const args = runtime.isBun ? ["run", viteBin, "build", "--mode", mode]
63
+ : [viteBin, "build", "--mode", mode];
64
+ const r = spawnSync(runtime.bin, args, { cwd: pluginRoot, stdio: "inherit" });
53
65
  if (r.status !== 0) throw new Error("vite build failed");
54
- return true;
55
66
  }
56
67
 
57
68
  function isMainBranch() {
@@ -82,13 +93,13 @@ export async function exportRos2Build(root, archs = ["amd64", "arm64"]) {
82
93
  const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
83
94
  if (!dockerPlatform) { log.warn(`unknown arch '${arch}' — skipped`); continue; }
84
95
  const archImage = `${ROS2_DEV_IMAGE}-${arch}`;
85
- ensureImage(archImage, { platform: dockerPlatform });
96
+ await ensureImage(archImage, { platform: dockerPlatform });
86
97
  colconBuild(root, { symlink: false, arch });
87
98
  log.ok(`dist/ros2_ws/install-${arch}/ ready`);
88
99
  }
89
100
  } else {
90
101
  // rclpy / pure Python: arch-agnostic, build once
91
- ensureImage();
102
+ await ensureImage();
92
103
  colconBuild(root, { symlink: false });
93
104
  log.ok("dist/ros2_ws/install/ ready");
94
105
  }
@@ -119,7 +130,7 @@ export async function exportCppBuild(root, distDir, archs = ["amd64", "arm64"])
119
130
  const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
120
131
  if (!dockerPlatform) { log.warn(`unknown arch '${arch}' — skipped`); continue; }
121
132
  const archImage = `${CPP_DEV_IMAGE}-${arch}`;
122
- ensureImage(archImage, { platform: dockerPlatform });
133
+ await ensureImage(archImage, { platform: dockerPlatform });
123
134
  cmakeBuild(root, { buildType: "Release", arch });
124
135
  const buildDir = `cpp/build-${arch}`;
125
136
  const built = resolve(root, buildDir, entry);
@@ -233,9 +244,12 @@ export async function buildPlugin({ root, minify = false } = {}) {
233
244
  rmSync(distDir, { recursive: true, force: true });
234
245
  mkdirSync(distDir, { recursive: true });
235
246
 
236
- const viteOk = runViteBuild(pluginRoot, { mode: minify ? "production" : "development" });
237
- if (!viteOk) {
238
- log.warn("vite not found — UI bundle skipped. Install Node.js and run: npm install");
247
+ const manifest = JSON.parse(readFileSync(resolve(pluginRoot, "manifest.json"), "utf8"));
248
+ const contributesUi = (manifest.contributes ?? []).some((c) => c.ui);
249
+ const uiDir = resolve(pluginRoot, "ui");
250
+ const hasUiFiles = existsSync(uiDir) && readdirSync(uiDir).some((f) => /\.[tj]sx?$/.test(f));
251
+ if (contributesUi || hasUiFiles) {
252
+ runViteBuild(pluginRoot, { mode: minify ? "production" : "development" });
239
253
  }
240
254
 
241
255
  // Stage distribution files.
@@ -417,7 +431,7 @@ export async function runBuild(args) {
417
431
  if (needsLock) {
418
432
  const { lockAndVendor } = await import("./python.mjs");
419
433
  try {
420
- lockAndVendor(pluginRoot, archs);
434
+ await lockAndVendor(pluginRoot, archs);
421
435
  } catch (e) {
422
436
  log.warn(`python lock/vendor skipped: ${e.message}`);
423
437
  }
package/cli/src/cpp.mjs CHANGED
@@ -2,31 +2,25 @@
2
2
  // Matches the robot OS so the compiled binary is relocatable to the robot.
3
3
  // Used by dev:back (interactive dev loop) and build (export the binary into the ZIP).
4
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, pruneMuskatImages } from "./util.mjs";
5
+ import { dirname } from "node:path";
6
+ import { hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
8
7
 
9
8
  export const CPP_DEV_IMAGE = "muscat-pk-cpp-dev";
10
9
 
11
- // Dockerfile lives inside the kit — plugin authors never see or edit it.
12
- const DOCKERFILE = resolve(kitRoot, "cli/docker/Dockerfile.cpp");
13
-
14
10
  export function assertDockerReady() {
15
11
  if (!hasBin("docker")) {
16
12
  throw new Error("Docker not found. C++ builds need Docker (Ubuntu 24.04 + cmake container).");
17
13
  }
18
- if (!existsSync(DOCKERFILE)) {
19
- throw new Error(`kit Dockerfile missing at ${DOCKERFILE} (reinstall muscat-plugin-kit).`);
20
- }
21
14
  }
22
15
 
23
16
  /** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
24
- export function ensureImage(image = CPP_DEV_IMAGE, { platform = null } = {}) {
17
+ export async function ensureImage(image = CPP_DEV_IMAGE, { platform = null } = {}) {
18
+ const dockerfile = await resolveDockerfile("Dockerfile.cpp");
25
19
  log.step(`docker build -t ${image}${platform ? ` (${platform})` : ""} (first run is slow, then cached)…`);
26
20
  const platformFlags = platform ? ["--platform", platform] : [];
27
21
  const r = spawnSync(
28
22
  "docker",
29
- ["build", ...platformFlags, "-t", image, "-f", DOCKERFILE, dirname(DOCKERFILE)],
23
+ ["build", ...platformFlags, "-t", image, "-f", dockerfile, dirname(dockerfile)],
30
24
  { stdio: "inherit" },
31
25
  );
32
26
  if (r.status !== 0) throw new Error("docker build failed.");
@@ -33,9 +33,9 @@ import {
33
33
  // Runs in a warm Ubuntu 24.04 + Python 3.12 container (matches the robot).
34
34
  // Under `muscat-pk dev`, it shares the bridge container's netns, so the SDK
35
35
  // reaches the Socket.IO bridge at the default localhost:10000.
36
- function runPython(root) {
36
+ async function runPython(root) {
37
37
  pyAssertDockerReady();
38
- pyEnsureImage();
38
+ await pyEnsureImage();
39
39
 
40
40
  const manifest = readManifest(root);
41
41
  const pluginId = manifest.id ?? "";
@@ -94,12 +94,12 @@ function runPython(root) {
94
94
  }
95
95
 
96
96
  // ── ros2 ───────────────────────────────────────────────────────────────────────
97
- function runRos2(root) {
97
+ async function runRos2(root) {
98
98
  assertDockerReady();
99
99
  const pkg = ros2PackageName(root);
100
100
  if (!pkg) throw new Error("no ROS2 package found under ros2_ws/src.");
101
101
 
102
- ensureImage();
102
+ await ensureImage();
103
103
 
104
104
  const manifest = readManifest(root);
105
105
  const pluginId = manifest.id ?? "";
@@ -167,13 +167,13 @@ function runRos2(root) {
167
167
  }
168
168
 
169
169
  // ── cpp ──────────────────────────────────────────────────────────────────────
170
- function runCpp(root) {
170
+ async function runCpp(root) {
171
171
  cppAssertDockerReady();
172
172
  const manifest = readManifest(root);
173
173
  const entry = manifest.runtime?.entry;
174
174
  if (!entry) throw new Error("runtime.entry (the built executable name) is required for cpp.");
175
175
 
176
- cppEnsureImage();
176
+ await cppEnsureImage();
177
177
 
178
178
  const pluginId = manifest.id ?? "";
179
179
  const container = `${CPP_DEV_IMAGE}-run`;
@@ -245,7 +245,7 @@ export async function runDevBack() {
245
245
  const root = findPluginRoot();
246
246
  warnManifest(root);
247
247
  const type = manifestRuntime(root);
248
- if (type === "ros2") return runRos2(root);
249
- if (type === "cpp") return runCpp(root);
250
- return runPython(root);
248
+ if (type === "ros2") return await runRos2(root);
249
+ if (type === "cpp") return await runCpp(root);
250
+ return await runPython(root);
251
251
  }
@@ -2,30 +2,25 @@
2
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
- import { dirname, resolve } from "node:path";
6
- import { hasBin, kitRoot, log, pruneMuskatImages } from "./util.mjs";
5
+ import { dirname } from "node:path";
6
+ import { hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
7
7
 
8
8
  export const PYTHON_DEV_IMAGE = "muscat-pk-python-dev";
9
9
 
10
- // Dockerfile lives inside the kit — plugin authors never see or edit it.
11
- const DOCKERFILE = resolve(kitRoot, "cli/docker/Dockerfile.python");
12
-
13
10
  export function assertDockerReady() {
14
11
  if (!hasBin("docker")) {
15
12
  throw new Error("Docker not found. The Python backend runs in a container (Ubuntu 24.04 + Python 3.12).");
16
13
  }
17
- if (!existsSync(DOCKERFILE)) {
18
- throw new Error(`kit Dockerfile missing at ${DOCKERFILE} (reinstall muscat-plugin-kit).`);
19
- }
20
14
  }
21
15
 
22
16
  /** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
23
- export function ensureImage(image = PYTHON_DEV_IMAGE, { platform = null } = {}) {
17
+ export async function ensureImage(image = PYTHON_DEV_IMAGE, { platform = null } = {}) {
18
+ const dockerfile = await resolveDockerfile("Dockerfile.python");
24
19
  log.step(`docker build -t ${image}${platform ? ` (${platform})` : ""} (first run is slow, then cached)…`);
25
20
  const platformFlags = platform ? ["--platform", platform] : [];
26
21
  const r = spawnSync(
27
22
  "docker",
28
- ["build", ...platformFlags, "-t", image, "-f", DOCKERFILE, dirname(DOCKERFILE)],
23
+ ["build", ...platformFlags, "-t", image, "-f", dockerfile, dirname(dockerfile)],
29
24
  { stdio: "inherit" },
30
25
  );
31
26
  if (r.status !== 0) throw new Error("docker build failed.");
@@ -47,14 +42,14 @@ const ARCH_DOCKER_PLATFORM = {
47
42
  * none-any 휠은 pip이 두 번째 arch에서 자동 skip → 중복 없음.
48
43
  * requirements.txt 없으면 조용히 건너뜀.
49
44
  */
50
- export function lockAndVendor(pluginRoot, archs = ["x86_64", "aarch64"]) {
45
+ export async function lockAndVendor(pluginRoot, archs = ["x86_64", "aarch64"]) {
51
46
  const reqTxt = resolve(pluginRoot, "requirements.txt");
52
47
  if (!existsSync(reqTxt)) {
53
48
  log.warn("requirements.txt not found — lock/vendor skipped");
54
49
  return;
55
50
  }
56
51
 
57
- ensureImage();
52
+ await ensureImage();
58
53
 
59
54
  log.step("generating lock (uv pip compile)…");
60
55
  const lockResult = spawnSync(
@@ -82,7 +77,7 @@ export function lockAndVendor(pluginRoot, archs = ["x86_64", "aarch64"]) {
82
77
  }
83
78
  // arch별 이미지 빌드 (--platform 지정) → pip이 glibc+arch 자동 감지 → 올바른 휠 선택.
84
79
  const archImage = `${PYTHON_DEV_IMAGE}-${arch}`;
85
- ensureImage(archImage, { platform: dockerPlatform });
80
+ await ensureImage(archImage, { platform: dockerPlatform });
86
81
 
87
82
  log.step(`vendoring wheels for ${arch} (${dockerPlatform})…`);
88
83
  // 패키지별 개별 download로 private/미공개 패키지 실패 시에도 나머지 계속 진행.
package/cli/src/ros2.mjs CHANGED
@@ -3,14 +3,10 @@
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, pruneMuskatImages } from "./util.mjs";
6
+ import { hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
7
7
 
8
8
  export const ROS2_DEV_IMAGE = "muscat-pk-ros2-dev";
9
9
 
10
- // Dockerfile lives inside the kit — plugin authors never see or edit it.
11
- // It has no COPY/ADD, so the build context is irrelevant (its own dir is used).
12
- const DOCKERFILE = resolve(kitRoot, "cli/docker/Dockerfile.dev");
13
-
14
10
  /** First package directory under ros2_ws/src (the colcon package name). */
15
11
  export function ros2PackageName(root) {
16
12
  const srcDir = resolve(root, "ros2_ws/src");
@@ -25,9 +21,6 @@ export function assertDockerReady() {
25
21
  if (!hasBin("docker")) {
26
22
  throw new Error("Docker not found. ROS2 needs Docker (Jazzy + Python 3.12 container).");
27
23
  }
28
- if (!existsSync(DOCKERFILE)) {
29
- throw new Error(`kit Dockerfile missing at ${DOCKERFILE} (reinstall muscat-plugin-kit).`);
30
- }
31
24
  }
32
25
 
33
26
  export const ARCH_DOCKER_PLATFORM = {
@@ -36,12 +29,13 @@ export const ARCH_DOCKER_PLATFORM = {
36
29
  };
37
30
 
38
31
  /** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
39
- export function ensureImage(image = ROS2_DEV_IMAGE, { platform = null } = {}) {
32
+ export async function ensureImage(image = ROS2_DEV_IMAGE, { platform = null } = {}) {
33
+ const dockerfile = await resolveDockerfile("Dockerfile.dev");
40
34
  log.step(`docker build -t ${image}${platform ? ` (${platform})` : ""} (first run is slow, then cached)…`);
41
35
  const platformFlags = platform ? ["--platform", platform] : [];
42
36
  const r = spawnSync(
43
37
  "docker",
44
- ["build", ...platformFlags, "-t", image, "-f", DOCKERFILE, dirname(DOCKERFILE)],
38
+ ["build", ...platformFlags, "-t", image, "-f", dockerfile, dirname(dockerfile)],
45
39
  { stdio: "inherit" },
46
40
  );
47
41
  if (r.status !== 0) throw new Error("docker build failed.");
package/cli/src/util.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  // Shared helpers for muscat-pk.
2
2
  import { spawnSync } from "node:child_process";
3
- import { existsSync, readFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
+ import { tmpdir } from "node:os";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { createInterface } from "node:readline/promises";
7
8
 
@@ -129,6 +130,24 @@ export function humanSize(bytes) {
129
130
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
130
131
  }
131
132
 
133
+ /**
134
+ * Return the on-disk path of a bundled Dockerfile.
135
+ * In npm-install mode the real file exists under cli/docker/.
136
+ * In compiled-binary mode it doesn't — extract from the embedded snapshot to tmpdir.
137
+ */
138
+ export async function resolveDockerfile(name) {
139
+ const real = resolve(kitRoot, "cli/docker", name);
140
+ if (existsSync(real)) return real;
141
+ const { TEMPLATES } = await import("./templates-embedded.mjs");
142
+ const b64 = TEMPLATES[`docker/${name}`];
143
+ if (!b64) throw new Error(`Embedded Dockerfile missing: docker/${name}. Reinstall muscat-plugin-kit.`);
144
+ const dir = resolve(tmpdir(), "muscat-pk-docker");
145
+ mkdirSync(dir, { recursive: true });
146
+ const tmp = resolve(dir, name);
147
+ writeFileSync(tmp, Buffer.from(b64, "base64"));
148
+ return tmp;
149
+ }
150
+
132
151
  /** This kit's installed version (from its package.json). */
133
152
  export function kitVersion() {
134
153
  try {
@@ -178,8 +197,8 @@ export async function checkKitUpdate({ timeoutMs = 2000 } = {}) {
178
197
  [
179
198
  "",
180
199
  line,
181
- ` muscat-plugin-kit 새 버전이 있습니다: ${current} → ${latest}`,
182
- ` 업데이트: muscat-pk update`,
200
+ ` muscat-plugin-kit update available: ${current} → ${latest}`,
201
+ ` Update: muscat-pk update`,
183
202
  line,
184
203
  "",
185
204
  ].join("\n"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-robotics/muscat-plugin-kit",
3
- "version": "0.1.2-dev.39",
3
+ "version": "0.1.2-dev.40",
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.39",
34
+ "@rainbow-robotics/preview-host": "0.1.2-dev.40",
35
35
  "socket.io": "^4.8.1"
36
36
  },
37
37
  "optionalDependencies": {