@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.4 → 0.1.2-dev.41
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/README.md +116 -16
- package/cli/README.md +5 -5
- package/cli/bin/create-muscat-plugin.mjs +1 -1
- package/cli/bin/muscat-pk.mjs +20 -10
- package/cli/bundle-templates.mjs +28 -0
- package/cli/docker/Dockerfile.bridge +17 -0
- package/cli/docker/Dockerfile.cpp +22 -0
- package/cli/docker/Dockerfile.dev +14 -0
- package/cli/docker/Dockerfile.python +20 -0
- package/cli/docker/bridge-package.json +8 -0
- package/cli/src/bridge.mjs +92 -0
- package/cli/src/build.mjs +398 -36
- package/cli/src/check-package.mjs +8 -23
- package/cli/src/cpp.mjs +61 -0
- package/cli/src/dev.mjs +16 -3
- package/cli/src/devBack.mjs +239 -53
- package/cli/src/devUi.mjs +118 -15
- package/cli/src/esbuild.mjs +51 -10
- package/cli/src/manifest.mjs +193 -0
- package/cli/src/new.mjs +168 -37
- package/cli/src/preview.mjs +91 -0
- package/cli/src/python.mjs +122 -0
- package/cli/src/ros2.mjs +90 -0
- package/cli/src/socketBridge.mjs +129 -0
- package/cli/src/templates-embedded.mjs +3 -0
- package/cli/src/type.mjs +105 -0
- package/cli/src/uninstall.mjs +98 -0
- package/cli/src/update.mjs +259 -30
- package/cli/src/util.mjs +97 -7
- package/cli/templates/common/README.md +383 -9
- package/cli/templates/common/manifest.json +13 -13
- package/cli/templates/common/package.json +5 -11
- package/cli/templates/common/py/main.py +88 -0
- package/cli/templates/common/requirements.txt +4 -0
- package/cli/templates/common/tsconfig.json +1 -1
- package/cli/templates/common/{src → ui}/content.tsx +3 -2
- package/cli/templates/common/vite.config.ts +22 -13
- package/cli/templates/cpp/cpp/CMakeLists.txt +14 -0
- package/cli/templates/cpp/cpp/main.cpp +25 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/__init__.py +0 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +25 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/package.xml +10 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/resource/__PLUGIN_SLUG__ +0 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/setup.py +18 -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 +4 -4
- package/cli/src/pack.mjs +0 -58
- package/cli/templates/sdk/main.py +0 -21
- package/cli/templates/sdk/requirements.txt +0 -1
package/cli/src/update.mjs
CHANGED
|
@@ -1,52 +1,281 @@
|
|
|
1
|
-
// muscat-pk update — update the
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// muscat-pk update — update the CLI itself AND the plugin project's JS deps.
|
|
2
|
+
//
|
|
3
|
+
// Three install modes are handled:
|
|
4
|
+
// binary : Bun self-contained binary downloaded from S3
|
|
5
|
+
// → re-download from S3, replace process.execPath
|
|
6
|
+
// npm -g : globally installed Node.js package
|
|
7
|
+
// → npm install -g @rainbow-robotics/muscat-plugin-kit@<tag>
|
|
8
|
+
// local : plugin project's node_modules (fallback)
|
|
9
|
+
// → <pm> add @rainbow-robotics/muscat-plugin-kit@<tag>
|
|
10
|
+
//
|
|
11
|
+
// After updating the CLI itself, also bumps the plugin project's local deps
|
|
12
|
+
// and migrates the project structure if it's outdated (src/ → ui/, root main.py → py/).
|
|
13
|
+
import { execSync, spawn } from "node:child_process";
|
|
14
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
17
|
+
import { findPluginRoot, KIT_PUBLISHED_NAME, kitVersion, log, parseArgs, prompt } from "./util.mjs";
|
|
6
18
|
|
|
7
19
|
const PLUGIN_UI_PACKAGE = "@rainbow-robotics/plugin-ui";
|
|
20
|
+
const S3_BASE = "https://rainbow-deploy.s3.ap-northeast-2.amazonaws.com/muscat-plugin-kit";
|
|
21
|
+
|
|
22
|
+
const isBinaryMode = typeof Bun !== "undefined";
|
|
23
|
+
|
|
24
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
8
25
|
|
|
9
26
|
function detectPm(root) {
|
|
10
27
|
try {
|
|
11
28
|
const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
|
|
12
29
|
if (pkg.packageManager) return pkg.packageManager.split("@")[0];
|
|
13
|
-
} catch {
|
|
14
|
-
/* ignore */
|
|
15
|
-
}
|
|
30
|
+
} catch { /* ignore */ }
|
|
16
31
|
if (existsSync(resolve(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
17
32
|
if (existsSync(resolve(root, "yarn.lock"))) return "yarn";
|
|
18
33
|
if (existsSync(resolve(root, "bun.lockb"))) return "bun";
|
|
19
34
|
return "npm";
|
|
20
35
|
}
|
|
21
36
|
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
async function fetchText(url) {
|
|
38
|
+
const res = await fetch(url);
|
|
39
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
|
|
40
|
+
return (await res.text()).trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fetchBinary(url) {
|
|
44
|
+
const res = await fetch(url);
|
|
45
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
|
|
46
|
+
return Buffer.from(await res.arrayBuffer());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function s3BinaryName() {
|
|
50
|
+
const platform = process.platform;
|
|
51
|
+
const arch = process.arch;
|
|
52
|
+
const plat = platform === "win32" ? "win" : platform === "darwin" ? "darwin" : "linux";
|
|
53
|
+
const cpu = arch === "arm64" ? "arm64" : "x64";
|
|
54
|
+
const ext = platform === "win32" ? ".exe" : "";
|
|
55
|
+
return `muscat-pk-${plat}-${cpu}${ext}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function resolveS3Version(tag) {
|
|
59
|
+
const pointer = tag === "dev" ? "dev-version.txt" : "latest-version.txt";
|
|
60
|
+
return fetchText(`${S3_BASE}/${pointer}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── self-update: binary ───────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
async function updateBinary(tag) {
|
|
66
|
+
const version = await resolveS3Version(tag);
|
|
67
|
+
const current = kitVersion();
|
|
68
|
+
if (version === current) {
|
|
69
|
+
log.ok(`Already up to date (v${current})`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
26
72
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
73
|
+
const binaryName = s3BinaryName();
|
|
74
|
+
const url = `${S3_BASE}/v${version}/${binaryName}`;
|
|
75
|
+
log.step(`Downloading binary: v${current} → v${version}`);
|
|
76
|
+
|
|
77
|
+
const data = await fetchBinary(url);
|
|
78
|
+
const execPath = process.execPath;
|
|
79
|
+
|
|
80
|
+
if (process.platform === "win32") {
|
|
81
|
+
// Windows: cannot overwrite a running .exe — write .new then schedule replacement
|
|
82
|
+
const newPath = execPath + ".new";
|
|
83
|
+
writeFileSync(newPath, data);
|
|
84
|
+
|
|
85
|
+
// PowerShell snippet that waits for this process to exit, then renames
|
|
86
|
+
const ps = `Start-Sleep -Milliseconds 500; Move-Item -Force '${newPath}' '${execPath}'`;
|
|
87
|
+
spawn("powershell", ["-NoProfile", "-Command", ps], {
|
|
88
|
+
detached: true,
|
|
89
|
+
stdio: "ignore",
|
|
90
|
+
}).unref();
|
|
91
|
+
|
|
92
|
+
log.ok(`v${version} will be applied on next run.`);
|
|
93
|
+
} else {
|
|
94
|
+
// macOS / Linux: can replace the file while it's mapped into memory
|
|
95
|
+
const tmp = join(tmpdir(), `muscat-pk-${version}`);
|
|
96
|
+
writeFileSync(tmp, data);
|
|
97
|
+
chmodSync(tmp, 0o755);
|
|
98
|
+
copyFileSync(tmp, execPath);
|
|
99
|
+
log.ok(`Updated to v${version}.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── self-update: npm global ───────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
function updateNpmGlobal(tag) {
|
|
106
|
+
log.step(`npm install -g ${KIT_PUBLISHED_NAME}@${tag}`);
|
|
107
|
+
execSync(`npm install -g ${KIT_PUBLISHED_NAME}@${tag}`, { stdio: "inherit" });
|
|
108
|
+
log.ok("Global CLI updated.");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── project structure migration ───────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
function detectOldStructure(root) {
|
|
114
|
+
const issues = [];
|
|
115
|
+
|
|
116
|
+
// src/ exists (old UI dir) or vite.config references src/content.tsx
|
|
117
|
+
const hasSrcDir = existsSync(resolve(root, "src"));
|
|
118
|
+
const hasUiDir = existsSync(resolve(root, "ui"));
|
|
119
|
+
const viteRef = (() => {
|
|
120
|
+
try {
|
|
121
|
+
return readFileSync(resolve(root, "vite.config.ts"), "utf8").includes("src/content.tsx");
|
|
122
|
+
} catch { return false; }
|
|
123
|
+
})();
|
|
124
|
+
if ((hasSrcDir || viteRef) && !hasUiDir) {
|
|
125
|
+
issues.push({ kind: "src→ui", desc: "src/ → ui/ (+ vite.config.ts, tsconfig.json)" });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (existsSync(resolve(root, "main.py")) && !existsSync(resolve(root, "py"))) {
|
|
129
|
+
issues.push({ kind: "main→py", desc: "main.py → py/main.py" });
|
|
130
|
+
}
|
|
131
|
+
if (existsSync(resolve(root, "backend"))) {
|
|
132
|
+
issues.push({ kind: "backend→py", desc: "backend/ → py/" });
|
|
133
|
+
}
|
|
134
|
+
return issues;
|
|
135
|
+
}
|
|
37
136
|
|
|
137
|
+
async function migrateStructure(root, issues) {
|
|
138
|
+
log.warn("This plugin project's structure is outdated:");
|
|
139
|
+
for (const issue of issues) log.info(" • " + issue.desc);
|
|
140
|
+
log.warn("Files will be moved and modified. Back up or commit your work before proceeding.");
|
|
141
|
+
|
|
142
|
+
const answer = await prompt("Migrate project structure? [y/N] ");
|
|
143
|
+
if (answer.toLowerCase() !== "y") {
|
|
144
|
+
log.info("Structure migration skipped.");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const issue of issues) {
|
|
149
|
+
if (issue.kind === "src→ui") {
|
|
150
|
+
if (existsSync(resolve(root, "src"))) {
|
|
151
|
+
renameSync(resolve(root, "src"), resolve(root, "ui"));
|
|
152
|
+
log.ok("src/ → ui/");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const vitePath = resolve(root, "vite.config.ts");
|
|
156
|
+
if (existsSync(vitePath)) {
|
|
157
|
+
writeFileSync(vitePath, readFileSync(vitePath, "utf8").replace(/["']src\/content\.tsx["']/g, '"ui/content.tsx"'));
|
|
158
|
+
log.ok("vite.config.ts entry updated");
|
|
159
|
+
}
|
|
160
|
+
const tscPath = resolve(root, "tsconfig.json");
|
|
161
|
+
if (existsSync(tscPath)) {
|
|
162
|
+
writeFileSync(tscPath, readFileSync(tscPath, "utf8").replace(/"src"/g, '"ui"'));
|
|
163
|
+
log.ok("tsconfig.json include updated");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (issue.kind === "main→py") {
|
|
168
|
+
mkdirSync(resolve(root, "py"), { recursive: true });
|
|
169
|
+
renameSync(resolve(root, "main.py"), resolve(root, "py", "main.py"));
|
|
170
|
+
log.ok("main.py → py/main.py");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (issue.kind === "backend→py") {
|
|
174
|
+
renameSync(resolve(root, "backend"), resolve(root, "py"));
|
|
175
|
+
log.ok("backend/ → py/");
|
|
176
|
+
const pyReqs = resolve(root, "py", "requirements.txt");
|
|
177
|
+
const rootReqs = resolve(root, "requirements.txt");
|
|
178
|
+
if (existsSync(pyReqs) && !existsSync(rootReqs)) {
|
|
179
|
+
renameSync(pyReqs, rootReqs);
|
|
180
|
+
log.ok("py/requirements.txt → requirements.txt (project root)");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
log.ok("Migration complete. Run `muscat-pk build` to verify.");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── plugin project local deps ─────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
function updateLocalDeps(root, tag) {
|
|
191
|
+
const pm = detectPm(root);
|
|
192
|
+
const list = [`${KIT_PUBLISHED_NAME}@${tag}`, `${PLUGIN_UI_PACKAGE}@${tag}`];
|
|
38
193
|
const cmd =
|
|
39
|
-
pm === "npm"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
? `yarn add ${list.join(" ")}`
|
|
43
|
-
: `${pm} add ${list.join(" ")}`;
|
|
194
|
+
pm === "npm" ? `npm install ${list.join(" ")}`
|
|
195
|
+
: pm === "yarn" ? `yarn add ${list.join(" ")}`
|
|
196
|
+
: `${pm} add ${list.join(" ")}`;
|
|
44
197
|
|
|
45
|
-
log.step(
|
|
198
|
+
log.step(`Updating plugin local deps (${pm}): ${list.join(", ")}`);
|
|
46
199
|
try {
|
|
47
200
|
execSync(cmd, { cwd: root, stdio: "inherit" });
|
|
48
|
-
log.ok("
|
|
201
|
+
log.ok("Local deps updated.");
|
|
49
202
|
} catch {
|
|
50
|
-
log.warn("update failed (offline?). Run manually:\n " + cmd);
|
|
203
|
+
log.warn("Local deps update failed (offline?). Run manually:\n " + cmd);
|
|
51
204
|
}
|
|
52
205
|
}
|
|
206
|
+
|
|
207
|
+
// ── main ─────────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
async function resolveTag(explicitTag) {
|
|
210
|
+
if (explicitTag) return explicitTag;
|
|
211
|
+
|
|
212
|
+
const current = kitVersion();
|
|
213
|
+
const isDevVersion = current.includes("-dev.");
|
|
214
|
+
|
|
215
|
+
if (!isDevVersion) return "latest";
|
|
216
|
+
|
|
217
|
+
const answer = await prompt(
|
|
218
|
+
`You are on a dev version (v${current}).\n` +
|
|
219
|
+
` [1] Stay on dev — update to latest dev build\n` +
|
|
220
|
+
` [2] Switch to stable — update to latest stable release\n` +
|
|
221
|
+
`Choice [1/2, default: 1]: `
|
|
222
|
+
);
|
|
223
|
+
return answer.trim() === "2" ? "latest" : "dev";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function runUpdate(args = []) {
|
|
227
|
+
const opts = parseArgs(args);
|
|
228
|
+
const explicitTag = opts._[0] || opts.version || opts.tag || "";
|
|
229
|
+
const tag = await resolveTag(explicitTag);
|
|
230
|
+
|
|
231
|
+
// 1. Update the CLI itself
|
|
232
|
+
if (isBinaryMode) {
|
|
233
|
+
log.step("Updating binary…");
|
|
234
|
+
try {
|
|
235
|
+
await updateBinary(tag);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
log.warn(`Binary update failed: ${err.message}`);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
const isGlobal = (() => {
|
|
241
|
+
try {
|
|
242
|
+
const prefix = execSync("npm prefix -g", { encoding: "utf8" }).trim();
|
|
243
|
+
return (
|
|
244
|
+
// Unix: {prefix}/bin/muscat-pk
|
|
245
|
+
existsSync(join(prefix, "bin", "muscat-pk")) ||
|
|
246
|
+
// Windows npm: {prefix}/muscat-pk.cmd (no bin/ subdir)
|
|
247
|
+
existsSync(join(prefix, "muscat-pk.cmd")) ||
|
|
248
|
+
existsSync(join(prefix, "bin", "muscat-pk.cmd"))
|
|
249
|
+
);
|
|
250
|
+
} catch { return false; }
|
|
251
|
+
})();
|
|
252
|
+
|
|
253
|
+
if (isGlobal) {
|
|
254
|
+
try {
|
|
255
|
+
updateNpmGlobal(tag);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
log.warn(`Global CLI update failed: ${err.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 2. Plugin project: structure migration + local deps
|
|
263
|
+
let root;
|
|
264
|
+
try {
|
|
265
|
+
root = findPluginRoot();
|
|
266
|
+
} catch {
|
|
267
|
+
// Not inside a plugin project — skip
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const structureIssues = detectOldStructure(root);
|
|
273
|
+
if (structureIssues.length > 0) {
|
|
274
|
+
await migrateStructure(root, structureIssues);
|
|
275
|
+
}
|
|
276
|
+
} catch (err) {
|
|
277
|
+
log.warn(`Structure check failed: ${err.message}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
updateLocalDeps(root, tag);
|
|
281
|
+
}
|
package/cli/src/util.mjs
CHANGED
|
@@ -1,7 +1,43 @@
|
|
|
1
1
|
// Shared helpers for muscat-pk.
|
|
2
|
-
import {
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
|
|
9
|
+
/** True if an executable is on PATH (cross-platform). */
|
|
10
|
+
export function hasBin(bin) {
|
|
11
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
12
|
+
return spawnSync(probe, [bin]).status === 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Find a Python 3.12 interpreter (robot targets 3.12), cross-platform.
|
|
17
|
+
* Returns the command as an argv array, e.g. ["py","-3.12"] or ["python3.12"].
|
|
18
|
+
* Falls back to a generic python3/python with a warning flag.
|
|
19
|
+
*/
|
|
20
|
+
export function detectPythonCmd() {
|
|
21
|
+
const candidates =
|
|
22
|
+
process.platform === "win32"
|
|
23
|
+
? [["py", "-3.12"], ["python3.12"], ["py", "-3"], ["python"]]
|
|
24
|
+
: [["python3.12"], ["python3"], ["python"]];
|
|
25
|
+
for (const cmd of candidates) {
|
|
26
|
+
const r = spawnSync(cmd[0], [...cmd.slice(1), "--version"]);
|
|
27
|
+
if (r.status === 0) {
|
|
28
|
+
const ver = String(r.stdout || r.stderr || "");
|
|
29
|
+
return { cmd, is312: ver.includes("3.12") };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { cmd: process.platform === "win32" ? ["python"] : ["python3"], is312: false };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Path to the venv's python interpreter (cross-platform). */
|
|
36
|
+
export function venvPython(venvDir) {
|
|
37
|
+
return process.platform === "win32"
|
|
38
|
+
? resolve(venvDir, "Scripts", "python.exe")
|
|
39
|
+
: resolve(venvDir, "bin", "python");
|
|
40
|
+
}
|
|
5
41
|
|
|
6
42
|
/** Published install/CLI package name. Author imports use @rainbow-robotics/plugin-ui separately. */
|
|
7
43
|
export const KIT_PUBLISHED_NAME = "@rainbow-robotics/muscat-plugin-kit";
|
|
@@ -31,6 +67,15 @@ export function parseArgs(args, { booleans = [] } = {}) {
|
|
|
31
67
|
return out;
|
|
32
68
|
}
|
|
33
69
|
|
|
70
|
+
/** Prompt user for input. Returns trimmed answer or fallback (non-TTY or empty). */
|
|
71
|
+
export async function prompt(question, fallback = "") {
|
|
72
|
+
if (!process.stdin.isTTY) return fallback;
|
|
73
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
74
|
+
const answer = (await rl.question(question)).trim();
|
|
75
|
+
rl.close();
|
|
76
|
+
return answer || fallback;
|
|
77
|
+
}
|
|
78
|
+
|
|
34
79
|
/** Walk up from cwd to find the plugin project root (has manifest.json). */
|
|
35
80
|
export function findPluginRoot(start = process.cwd()) {
|
|
36
81
|
let dir = resolve(start);
|
|
@@ -45,19 +90,64 @@ export function findPluginRoot(start = process.cwd()) {
|
|
|
45
90
|
);
|
|
46
91
|
}
|
|
47
92
|
|
|
93
|
+
const RED = "\x1b[31m";
|
|
94
|
+
const ORANGE = "\x1b[33m";
|
|
95
|
+
const RESET = "\x1b[0m";
|
|
96
|
+
|
|
48
97
|
export const log = {
|
|
49
|
-
info:
|
|
50
|
-
step:
|
|
51
|
-
ok:
|
|
52
|
-
warn:
|
|
98
|
+
info: (...m) => console.log(...m),
|
|
99
|
+
step: (...m) => console.log("›", ...m),
|
|
100
|
+
ok: (...m) => console.log("✓", ...m),
|
|
101
|
+
warn: (...m) => console.warn(`${ORANGE}⚠${RESET}`, ...m.map(s => `${ORANGE}${s}${RESET}`)),
|
|
102
|
+
error: (...m) => console.error(`${RED}✖${RESET}`, ...m.map(s => `${RED}${s}${RESET}`)),
|
|
53
103
|
};
|
|
54
104
|
|
|
105
|
+
// dev / dev:back / build run the plugin back end, the dev bridge, and cpp/ros2
|
|
106
|
+
// compilation in Docker. Show a clear install hint when Docker is missing.
|
|
107
|
+
export function ensureDocker() {
|
|
108
|
+
if (hasBin("docker")) return;
|
|
109
|
+
log.warn("Docker not found.");
|
|
110
|
+
log.warn(" muscat-pk runs the plugin back end, the dev bridge, and cpp/ros2 builds in Docker.");
|
|
111
|
+
log.warn(" Install Docker, then retry: https://docs.docker.com/get-docker/");
|
|
112
|
+
throw new Error("Docker is required for dev / dev:back / build.");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Remove dangling (untagged) muscat-pk images left behind after a rebuild.
|
|
117
|
+
* Uses label filter so only our images are touched — other Docker images are safe.
|
|
118
|
+
*/
|
|
119
|
+
export function pruneMuskatImages() {
|
|
120
|
+
spawnSync(
|
|
121
|
+
"docker",
|
|
122
|
+
["image", "prune", "--force", "--filter", "label=com.rainbow-robotics.muscat-pk=true"],
|
|
123
|
+
{ stdio: "ignore" },
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
55
127
|
export function humanSize(bytes) {
|
|
56
128
|
if (bytes < 1024) return `${bytes} B`;
|
|
57
129
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
58
130
|
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
59
131
|
}
|
|
60
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
|
+
|
|
61
151
|
/** This kit's installed version (from its package.json). */
|
|
62
152
|
export function kitVersion() {
|
|
63
153
|
try {
|
|
@@ -107,8 +197,8 @@ export async function checkKitUpdate({ timeoutMs = 2000 } = {}) {
|
|
|
107
197
|
[
|
|
108
198
|
"",
|
|
109
199
|
line,
|
|
110
|
-
` muscat-plugin-kit
|
|
111
|
-
`
|
|
200
|
+
` muscat-plugin-kit update available: ${current} → ${latest}`,
|
|
201
|
+
` Update: muscat-pk update`,
|
|
112
202
|
line,
|
|
113
203
|
"",
|
|
114
204
|
].join("\n"),
|