@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.4 → 0.1.2-dev.6
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 +95 -16
- package/cli/bin/muscat-pk.mjs +5 -1
- package/cli/docker/Dockerfile.dev +13 -0
- package/cli/src/build.mjs +105 -2
- package/cli/src/devBack.mjs +118 -42
- package/cli/src/devUi.mjs +46 -12
- package/cli/src/esbuild.mjs +24 -0
- package/cli/src/new.mjs +109 -27
- package/cli/src/pack.mjs +4 -1
- package/cli/src/ros2.mjs +65 -0
- package/cli/src/type.mjs +82 -0
- package/cli/src/util.mjs +39 -2
- package/cli/templates/common/README.md +254 -9
- package/cli/templates/common/main.py +24 -0
- package/cli/templates/common/package.json +0 -7
- package/cli/templates/common/requirements.txt +2 -0
- package/cli/templates/common/src/content.tsx +3 -2
- 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/package.json +1 -1
- package/cli/templates/sdk/main.py +0 -21
- package/cli/templates/sdk/requirements.txt +0 -1
package/cli/src/new.mjs
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
// muscat-pk new — scaffold a new plugin project from templates.
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
2
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
3
3
|
import {
|
|
4
4
|
cpSync,
|
|
5
5
|
existsSync,
|
|
6
6
|
mkdirSync,
|
|
7
7
|
readFileSync,
|
|
8
8
|
readdirSync,
|
|
9
|
+
renameSync,
|
|
9
10
|
statSync,
|
|
10
11
|
writeFileSync,
|
|
11
12
|
} from "node:fs";
|
|
12
13
|
import { createInterface } from "node:readline/promises";
|
|
13
14
|
import { basename, resolve } from "node:path";
|
|
14
|
-
import { parseArgs, templatesDir, log } from "./util.mjs";
|
|
15
|
+
import { detectPythonCmd, hasBin, parseArgs, templatesDir, log, venvPython } from "./util.mjs";
|
|
15
16
|
|
|
16
|
-
const TYPES = ["
|
|
17
|
+
const TYPES = ["python", "ros2"];
|
|
17
18
|
const PMS = ["npm", "yarn", "pnpm", "bun"];
|
|
18
|
-
const DEFAULT_SDK_VERSION = "2.4.1";
|
|
19
19
|
|
|
20
20
|
async function prompt(question, fallback) {
|
|
21
21
|
if (!process.stdin.isTTY) return fallback;
|
|
@@ -25,7 +25,7 @@ async function prompt(question, fallback) {
|
|
|
25
25
|
return answer || fallback;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function applyTokens(dir, tokens) {
|
|
28
|
+
export function applyTokens(dir, tokens) {
|
|
29
29
|
for (const entry of readdirSync(dir)) {
|
|
30
30
|
const full = resolve(dir, entry);
|
|
31
31
|
if (statSync(full).isDirectory()) {
|
|
@@ -45,12 +45,46 @@ function applyTokens(dir, tokens) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Derive naming tokens from a plugin directory name.
|
|
49
|
+
export function deriveNames(target) {
|
|
50
|
+
const base = basename(target);
|
|
51
|
+
const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
52
|
+
const pluginName = base.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
53
|
+
return {
|
|
54
|
+
dir: base,
|
|
55
|
+
slug,
|
|
56
|
+
pluginName,
|
|
57
|
+
pluginClass: pluginName.replace(/\s+/g, ""),
|
|
58
|
+
pluginId: `com.example.${base.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Recursively rename any file/dir whose name contains __PLUGIN_SLUG__.
|
|
63
|
+
function renameSlugPaths(dir, slug) {
|
|
64
|
+
for (const entry of readdirSync(dir)) {
|
|
65
|
+
let full = resolve(dir, entry);
|
|
66
|
+
if (entry.includes("__PLUGIN_SLUG__")) {
|
|
67
|
+
full = resolve(dir, entry.split("__PLUGIN_SLUG__").join(slug));
|
|
68
|
+
renameSync(resolve(dir, entry), full);
|
|
69
|
+
}
|
|
70
|
+
if (statSync(full).isDirectory()) renameSlugPaths(full, slug);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Copy the ros2_ws/ overlay into a plugin and substitute the package slug into
|
|
75
|
+
// every path segment. The dev Dockerfile is NOT scaffolded here — it lives in
|
|
76
|
+
// the kit (cli/docker/) so authors can't edit it. Caller runs applyTokens after.
|
|
77
|
+
export function overlayRos2(target, slug) {
|
|
78
|
+
cpSync(resolve(templatesDir, "ros2"), target, { recursive: true });
|
|
79
|
+
renameSlugPaths(resolve(target, "ros2_ws"), slug);
|
|
80
|
+
}
|
|
81
|
+
|
|
48
82
|
export async function runNew(args) {
|
|
49
83
|
const opts = parseArgs(args, { booleans: ["yes", "no-install", "help"] });
|
|
50
84
|
if (opts.help) {
|
|
51
85
|
console.log(
|
|
52
|
-
"muscat-pk new [dir] [--type
|
|
53
|
-
" [--
|
|
86
|
+
"muscat-pk new [dir] [--type python|ros2] [--pm npm|yarn|pnpm|bun]\n" +
|
|
87
|
+
" [--yes] [--no-install]",
|
|
54
88
|
);
|
|
55
89
|
return;
|
|
56
90
|
}
|
|
@@ -65,9 +99,13 @@ export async function runNew(args) {
|
|
|
65
99
|
|
|
66
100
|
let type = opts.type;
|
|
67
101
|
if (!type && !opts.yes) {
|
|
68
|
-
|
|
102
|
+
console.log("\n플러그인 타입을 선택하세요:");
|
|
103
|
+
console.log(" 1) Python");
|
|
104
|
+
console.log(" 2) ROS2(rclpy)");
|
|
105
|
+
const choice = await prompt("\n선택 [1/2, default: 1]: ", "1");
|
|
106
|
+
type = choice === "2" ? "ros2" : "python";
|
|
69
107
|
}
|
|
70
|
-
type = TYPES.includes(type) ? type : "
|
|
108
|
+
type = TYPES.includes(type) ? type : "python";
|
|
71
109
|
|
|
72
110
|
let pm = opts.pm;
|
|
73
111
|
if (!pm && !opts.yes) {
|
|
@@ -75,32 +113,24 @@ export async function runNew(args) {
|
|
|
75
113
|
}
|
|
76
114
|
pm = PMS.includes(pm) ? pm : "npm";
|
|
77
115
|
|
|
78
|
-
const
|
|
79
|
-
const pluginName = basename(target)
|
|
80
|
-
.replace(/[-_]/g, " ")
|
|
81
|
-
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
82
|
-
const pluginId = `com.example.${basename(target).toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
|
|
116
|
+
const { dir, slug, pluginName, pluginClass, pluginId } = deriveNames(target);
|
|
83
117
|
|
|
84
118
|
mkdirSync(target, { recursive: true });
|
|
85
119
|
|
|
120
|
+
// common(UI + main.py + requirements.txt + manifest/config)은 항상 포함.
|
|
121
|
+
// ros2면 ros2_ws/ 오버레이만 추가로 얹는다. python은 ros2_ws/가 없을 뿐 구조는 동일.
|
|
86
122
|
log.step(`scaffolding ${type} plugin → ${target}`);
|
|
87
123
|
cpSync(resolve(templatesDir, "common"), target, { recursive: true });
|
|
88
|
-
if (type === "
|
|
89
|
-
cpSync(resolve(templatesDir, "sdk"), target, { recursive: true });
|
|
90
|
-
} else if (type === "ros2") {
|
|
91
|
-
if (existsSync(resolve(templatesDir, "ros2-rclpy"))) {
|
|
92
|
-
cpSync(resolve(templatesDir, "ros2-rclpy"), target, { recursive: true });
|
|
93
|
-
} else {
|
|
94
|
-
log.warn("ros2 template not bundled; scaffolding UI-only base");
|
|
95
|
-
}
|
|
96
|
-
}
|
|
124
|
+
if (type === "ros2") overlayRos2(target, slug);
|
|
97
125
|
|
|
98
126
|
applyTokens(target, {
|
|
99
127
|
__PLUGIN_ID__: pluginId,
|
|
100
128
|
__PLUGIN_NAME__: pluginName,
|
|
101
|
-
|
|
129
|
+
__PLUGIN_DIR__: dir,
|
|
130
|
+
__PLUGIN_SLUG__: slug,
|
|
131
|
+
__PLUGIN_CLASS__: pluginClass,
|
|
102
132
|
__PM__: pm,
|
|
103
|
-
__RUNTIME_TYPE__: type
|
|
133
|
+
__RUNTIME_TYPE__: type,
|
|
104
134
|
});
|
|
105
135
|
|
|
106
136
|
// gitignore (templates can't ship a literal .gitignore via npm reliably)
|
|
@@ -118,8 +148,60 @@ export async function runNew(args) {
|
|
|
118
148
|
}
|
|
119
149
|
}
|
|
120
150
|
|
|
151
|
+
let noPython = false;
|
|
152
|
+
let venvFailed = false;
|
|
153
|
+
|
|
154
|
+
if (!opts["no-install"]) {
|
|
155
|
+
// Python venv (robot targets 3.12) — cross-platform.
|
|
156
|
+
// uv is preferred: handles uv-managed Pythons that break standard ensurepip.
|
|
157
|
+
try {
|
|
158
|
+
const venv = resolve(target, ".venv");
|
|
159
|
+
log.step("creating .venv + installing requirements…");
|
|
160
|
+
if (hasBin("uv")) {
|
|
161
|
+
execFileSync("uv", ["venv", venv, "--python", "3.12", "--seed"], { cwd: target, stdio: "inherit" });
|
|
162
|
+
} else if (hasBin("conda")) {
|
|
163
|
+
execFileSync("conda", ["create", "-p", venv, "python=3.12", "-y", "--quiet"], { cwd: target, stdio: "inherit" });
|
|
164
|
+
} else if (hasBin("python3.12") || hasBin("python3") || hasBin("python")) {
|
|
165
|
+
const { cmd, is312 } = detectPythonCmd();
|
|
166
|
+
if (!is312) log.warn("python3.12 not found; venv uses fallback python (robot targets 3.12).");
|
|
167
|
+
execFileSync(cmd[0], [...cmd.slice(1), "-m", "venv", venv], { cwd: target, stdio: "inherit" });
|
|
168
|
+
} else {
|
|
169
|
+
noPython = true;
|
|
170
|
+
}
|
|
171
|
+
if (!noPython) {
|
|
172
|
+
const py = venvPython(venv);
|
|
173
|
+
if (existsSync(resolve(target, "requirements.txt"))) {
|
|
174
|
+
execFileSync(py, ["-m", "pip", "install", "-q", "-r", "requirements.txt"], { cwd: target, stdio: "inherit" });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
venvFailed = true;
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
noPython = !hasBin("uv") && !hasBin("conda") && !hasBin("python3.12") && !hasBin("python3") && !hasBin("python");
|
|
182
|
+
}
|
|
183
|
+
|
|
121
184
|
log.ok(`created ${targetName} (type: ${type}, pm: ${pm})`);
|
|
122
185
|
log.info(`\n cd ${targetName}`);
|
|
123
|
-
log.info(`
|
|
124
|
-
log.info(`
|
|
186
|
+
log.info(` muscat-pk dev # preview in mini-Muscat + back end`);
|
|
187
|
+
log.info(` muscat-pk pack # build a distributable ZIP\n`);
|
|
188
|
+
|
|
189
|
+
// ── post-scaffold environment warnings ───────────────────────────────────
|
|
190
|
+
const noNode = !hasBin("node");
|
|
191
|
+
if (noNode || noPython || venvFailed) {
|
|
192
|
+
log.info("");
|
|
193
|
+
if (noNode) {
|
|
194
|
+
log.warn("Node.js not found. Install Node.js 20+ to run JS deps and dev:ui.");
|
|
195
|
+
log.warn(" https://nodejs.org");
|
|
196
|
+
}
|
|
197
|
+
if (noPython) {
|
|
198
|
+
log.warn("Python not found. Install Python 3.12 to run the backend.");
|
|
199
|
+
log.warn(" https://www.python.org/downloads/ (or: brew install python@3.12 / uv / conda)");
|
|
200
|
+
log.warn(` Then: cd ${targetName} && python3.12 -m venv .venv && pip install -r requirements.txt`);
|
|
201
|
+
} else if (venvFailed) {
|
|
202
|
+
log.warn("venv setup failed. Create it manually:");
|
|
203
|
+
log.warn(` cd ${targetName} && python3.12 -m venv .venv && pip install -r requirements.txt`);
|
|
204
|
+
}
|
|
205
|
+
log.info("");
|
|
206
|
+
}
|
|
125
207
|
}
|
package/cli/src/pack.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
4
4
|
import { basename, resolve } from "node:path";
|
|
5
|
-
import { buildPlugin } from "./build.mjs";
|
|
5
|
+
import { buildPlugin, exportRos2Build, pinRequirements } from "./build.mjs";
|
|
6
6
|
import { findPluginRoot, humanSize, log } from "./util.mjs";
|
|
7
7
|
|
|
8
8
|
function dirSize(dir) {
|
|
@@ -17,7 +17,10 @@ function dirSize(dir) {
|
|
|
17
17
|
|
|
18
18
|
export async function runPack() {
|
|
19
19
|
const pluginRoot = findPluginRoot();
|
|
20
|
+
log.step("pinning requirements.txt to latest PyPI versions");
|
|
21
|
+
await pinRequirements(pluginRoot);
|
|
20
22
|
await buildPlugin({ root: pluginRoot, minify: true });
|
|
23
|
+
await exportRos2Build(pluginRoot);
|
|
21
24
|
|
|
22
25
|
const manifest = JSON.parse(
|
|
23
26
|
readFileSync(resolve(pluginRoot, "manifest.json"), "utf8"),
|
package/cli/src/ros2.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Shared ROS2 Docker helpers (Jazzy + Python 3.12 — matches the robot backend).
|
|
2
|
+
// Used by dev:back (interactive dev loop) and build (export install/ into the ZIP).
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { hasBin, kitRoot, log } from "./util.mjs";
|
|
7
|
+
|
|
8
|
+
export const ROS2_DEV_IMAGE = "muscat-pk-ros2-dev";
|
|
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
|
+
/** First package directory under ros2_ws/src (the colcon package name). */
|
|
15
|
+
export function ros2PackageName(root) {
|
|
16
|
+
const srcDir = resolve(root, "ros2_ws/src");
|
|
17
|
+
if (!existsSync(srcDir)) return null;
|
|
18
|
+
const dirs = readdirSync(srcDir, { withFileTypes: true })
|
|
19
|
+
.filter((d) => d.isDirectory())
|
|
20
|
+
.map((d) => d.name);
|
|
21
|
+
return dirs[0] ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function assertDockerReady() {
|
|
25
|
+
if (!hasBin("docker")) {
|
|
26
|
+
throw new Error("Docker not found. ROS2 needs Docker (Jazzy + Python 3.12 container).");
|
|
27
|
+
}
|
|
28
|
+
if (!existsSync(DOCKERFILE)) {
|
|
29
|
+
throw new Error(`kit Dockerfile missing at ${DOCKERFILE} (reinstall muscat-plugin-kit).`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
|
|
34
|
+
export function ensureImage(image = ROS2_DEV_IMAGE) {
|
|
35
|
+
log.step(`docker build -t ${image} (first run is slow, then cached)…`);
|
|
36
|
+
const r = spawnSync(
|
|
37
|
+
"docker",
|
|
38
|
+
["build", "-t", image, "-f", DOCKERFILE, dirname(DOCKERFILE)],
|
|
39
|
+
{ stdio: "inherit" },
|
|
40
|
+
);
|
|
41
|
+
if (r.status !== 0) throw new Error("docker build failed.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Run `colcon build` inside the container, producing ros2_ws/install on the host
|
|
46
|
+
* (the workspace is mounted). symlink=true for dev (fast, edit-in-place);
|
|
47
|
+
* symlink=false for export builds that ship in the ZIP (real copies, relocatable).
|
|
48
|
+
*/
|
|
49
|
+
export function colconBuild(root, { image = ROS2_DEV_IMAGE, symlink = false } = {}) {
|
|
50
|
+
const flag = symlink ? "--symlink-install" : "";
|
|
51
|
+
const script = [
|
|
52
|
+
// --ignore-installed: don't try to uninstall debian-managed packages
|
|
53
|
+
// (e.g. psutil), which fails with "RECORD file not found".
|
|
54
|
+
"pip install -r requirements.txt --break-system-packages --ignore-installed -q || true",
|
|
55
|
+
"cd ros2_ws",
|
|
56
|
+
`colcon build ${flag}`.trim(),
|
|
57
|
+
].join(" && ");
|
|
58
|
+
log.step(`colcon build${symlink ? " --symlink-install" : ""} (in container)…`);
|
|
59
|
+
const r = spawnSync(
|
|
60
|
+
"docker",
|
|
61
|
+
["run", "--rm", "-v", `${root}:/plugin`, "-w", "/plugin", image, "bash", "-lc", script],
|
|
62
|
+
{ cwd: root, stdio: "inherit" },
|
|
63
|
+
);
|
|
64
|
+
if (r.status !== 0) throw new Error("colcon build failed.");
|
|
65
|
+
}
|
package/cli/src/type.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
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).
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { basename, resolve } from "node:path";
|
|
7
|
+
import { findPluginRoot, log } from "./util.mjs";
|
|
8
|
+
import { applyTokens, deriveNames, overlayRos2 } from "./new.mjs";
|
|
9
|
+
|
|
10
|
+
const TYPES = ["python", "ros2"];
|
|
11
|
+
|
|
12
|
+
async function prompt(question, fallback) {
|
|
13
|
+
if (!process.stdin.isTTY) return fallback;
|
|
14
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
15
|
+
const answer = (await rl.question(question)).trim();
|
|
16
|
+
rl.close();
|
|
17
|
+
return answer || fallback;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readManifest(root) {
|
|
21
|
+
const path = resolve(root, "manifest.json");
|
|
22
|
+
if (!existsSync(path)) throw new Error(`manifest.json not found in ${root}`);
|
|
23
|
+
return { path, json: JSON.parse(readFileSync(path, "utf8")) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runType(args) {
|
|
27
|
+
// accept `type change`, `type`, `type --to ros2`
|
|
28
|
+
const sub = args[0];
|
|
29
|
+
const toFlagIdx = args.indexOf("--to");
|
|
30
|
+
let to = toFlagIdx >= 0 ? args[toFlagIdx + 1] : undefined;
|
|
31
|
+
|
|
32
|
+
const root = findPluginRoot();
|
|
33
|
+
const { path, json } = readManifest(root);
|
|
34
|
+
const current = json.runtime?.type ?? "python";
|
|
35
|
+
|
|
36
|
+
if (sub && sub !== "change") {
|
|
37
|
+
log.warn(`unknown subcommand '${sub}'. Usage: muscat-pk type change [--to python|ros2]`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!to) {
|
|
41
|
+
console.log(`\n현재 타입: ${current}`);
|
|
42
|
+
console.log("변경할 타입을 선택하세요:");
|
|
43
|
+
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;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!TYPES.includes(to)) {
|
|
50
|
+
throw new Error(`invalid type. choose python or ros2.`);
|
|
51
|
+
}
|
|
52
|
+
if (to === current) {
|
|
53
|
+
log.info(`already '${current}' — nothing to change.`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (to === "ros2") {
|
|
58
|
+
const { slug, pluginName, pluginClass, pluginId } = deriveNames(root);
|
|
59
|
+
if (existsSync(resolve(root, "ros2_ws"))) {
|
|
60
|
+
log.warn("ros2_ws/ already exists — keeping it, only flipping manifest type.");
|
|
61
|
+
} else {
|
|
62
|
+
log.step("adding ros2_ws/ overlay");
|
|
63
|
+
overlayRos2(root, slug);
|
|
64
|
+
applyTokens(root, {
|
|
65
|
+
__PLUGIN_ID__: pluginId,
|
|
66
|
+
__PLUGIN_NAME__: pluginName,
|
|
67
|
+
__PLUGIN_SLUG__: slug,
|
|
68
|
+
__PLUGIN_CLASS__: pluginClass,
|
|
69
|
+
__RUNTIME_TYPE__: "ros2",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
} 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.");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
json.runtime = { ...json.runtime, type: to };
|
|
80
|
+
writeFileSync(path, JSON.stringify(json, null, 2) + "\n");
|
|
81
|
+
log.ok(`${basename(root)} type: ${current} → ${to}`);
|
|
82
|
+
}
|
package/cli/src/util.mjs
CHANGED
|
@@ -1,8 +1,42 @@
|
|
|
1
1
|
// Shared helpers for muscat-pk.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { dirname, resolve } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
|
|
7
|
+
/** True if an executable is on PATH (cross-platform). */
|
|
8
|
+
export function hasBin(bin) {
|
|
9
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
10
|
+
return spawnSync(probe, [bin]).status === 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find a Python 3.12 interpreter (robot targets 3.12), cross-platform.
|
|
15
|
+
* Returns the command as an argv array, e.g. ["py","-3.12"] or ["python3.12"].
|
|
16
|
+
* Falls back to a generic python3/python with a warning flag.
|
|
17
|
+
*/
|
|
18
|
+
export function detectPythonCmd() {
|
|
19
|
+
const candidates =
|
|
20
|
+
process.platform === "win32"
|
|
21
|
+
? [["py", "-3.12"], ["python3.12"], ["py", "-3"], ["python"]]
|
|
22
|
+
: [["python3.12"], ["python3"], ["python"]];
|
|
23
|
+
for (const cmd of candidates) {
|
|
24
|
+
const r = spawnSync(cmd[0], [...cmd.slice(1), "--version"]);
|
|
25
|
+
if (r.status === 0) {
|
|
26
|
+
const ver = String(r.stdout || r.stderr || "");
|
|
27
|
+
return { cmd, is312: ver.includes("3.12") };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { cmd: process.platform === "win32" ? ["python"] : ["python3"], is312: false };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Path to the venv's python interpreter (cross-platform). */
|
|
34
|
+
export function venvPython(venvDir) {
|
|
35
|
+
return process.platform === "win32"
|
|
36
|
+
? resolve(venvDir, "Scripts", "python.exe")
|
|
37
|
+
: resolve(venvDir, "bin", "python");
|
|
38
|
+
}
|
|
39
|
+
|
|
6
40
|
/** Published install/CLI package name. Author imports use @rainbow-robotics/plugin-ui separately. */
|
|
7
41
|
export const KIT_PUBLISHED_NAME = "@rainbow-robotics/muscat-plugin-kit";
|
|
8
42
|
|
|
@@ -45,11 +79,14 @@ export function findPluginRoot(start = process.cwd()) {
|
|
|
45
79
|
);
|
|
46
80
|
}
|
|
47
81
|
|
|
82
|
+
const ORANGE = "\x1b[33m";
|
|
83
|
+
const RESET = "\x1b[0m";
|
|
84
|
+
|
|
48
85
|
export const log = {
|
|
49
86
|
info: (...m) => console.log(...m),
|
|
50
87
|
step: (...m) => console.log("›", ...m),
|
|
51
|
-
ok:
|
|
52
|
-
warn: (...m) => console.warn(
|
|
88
|
+
ok: (...m) => console.log("✓", ...m),
|
|
89
|
+
warn: (...m) => console.warn(`${ORANGE}⚠${RESET}`, ...m.map(s => `${ORANGE}${s}${RESET}`)),
|
|
53
90
|
};
|
|
54
91
|
|
|
55
92
|
export function humanSize(bytes) {
|