@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.45 → 0.1.2-dev.46

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.
@@ -10,15 +10,7 @@
10
10
  import { spawn, spawnSync } from "node:child_process";
11
11
  import { existsSync, watch } from "node:fs";
12
12
  import { resolve } from "node:path";
13
- import {
14
- detectPythonCmd,
15
- ensureDocker,
16
- findPluginRoot,
17
- hasBin,
18
- log,
19
- promptRobotConnection,
20
- venvPython,
21
- } from "./util.mjs";
13
+ import { ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection } from "./util.mjs";
22
14
  import { assertManifestPorts, manifestRuntime, readManifest, validateManifest } from "./manifest.mjs";
23
15
  import {
24
16
  formatPortRanges,
@@ -43,33 +35,16 @@ import {
43
35
  } from "./cpp.mjs";
44
36
 
45
37
  // ── IDE tooling venv (best-effort, non-blocking) ────────────────────────────
46
- // Docker is the source of truth for actually RUNNING the backend — this just
47
- // gives editors/linters (pylint, pyright, mypy, ...) a real interpreter with
48
- // the plugin's deps installed, since they inspect the host filesystem, not the
49
- // container. Runs in the background so it never delays `dev:back` starting,
50
- // and any failure here (no local Python toolchain, offline, ...) is silent —
51
- // Docker works fine with zero local Python.
38
+ // `muscat-pk new` already creates this up front (see new.mjs); this just
39
+ // refreshes it on every `dev`/`dev:back` in case requirements.txt changed.
40
+ // Docker remains the actual execution environment — this never affects it.
41
+ // Runs in the background so it never delays `dev:back` starting, and any
42
+ // failure here (no local Python toolchain, offline, ...) is silent.
52
43
  function syncIdeVenv(root) {
53
- const venvDir = resolve(root, ".venv");
54
- const py = venvPython(venvDir);
55
-
56
44
  setImmediate(() => {
57
45
  try {
58
- if (!existsSync(py)) {
59
- let created;
60
- if (hasBin("uv")) {
61
- created = spawnSync("uv", ["venv", venvDir, "--python", "3.12", "--seed"], { cwd: root, stdio: "ignore" });
62
- } else {
63
- const { cmd } = detectPythonCmd();
64
- created = spawnSync(cmd[0], [...cmd.slice(1), "-m", "venv", venvDir], { cwd: root, stdio: "ignore" });
65
- }
66
- if (created.status !== 0 || !existsSync(py)) return;
67
- }
68
- const reqs = resolve(root, "requirements.txt");
69
- const args = ["-m", "pip", "install", "-q"];
70
- if (existsSync(reqs)) args.push("-r", reqs);
71
- if (spawnSync(py, args, { cwd: root, stdio: "ignore" }).status === 0) {
72
- log.info(`IDE venv ready → .venv (point your editor's Python interpreter there for pylint/pyright)`);
46
+ if (ensureIdeVenv(root)) {
47
+ log.info("IDE venv ready → .venv (point your editor's Python interpreter there for pylint/pyright)");
73
48
  }
74
49
  } catch {
75
50
  /* best-effort only */
@@ -82,9 +57,11 @@ function syncIdeVenv(root) {
82
57
  // Under `muscat-pk dev`, it shares the bridge container's netns, so the SDK
83
58
  // reaches the Socket.IO bridge at the default localhost:10000.
84
59
  async function runPython(root, portRanges) {
60
+ // Independent of Docker readiness — must still happen if Docker isn't
61
+ // installed/running, since that's exactly when local IDE tooling matters most.
62
+ syncIdeVenv(root);
85
63
  pyAssertDockerReady();
86
64
  await pyEnsureImage();
87
- syncIdeVenv(root);
88
65
 
89
66
  const manifest = readManifest(root);
90
67
  const pluginId = manifest.id ?? "";
@@ -154,16 +131,19 @@ async function runPython(root, portRanges) {
154
131
 
155
132
  // ── ros2 ───────────────────────────────────────────────────────────────────────
156
133
  async function runRos2(root, portRanges) {
134
+ const manifest = readManifest(root);
135
+ const isRclcpp = manifest.runtime?.rosClient === "rclcpp";
136
+ // Independent of Docker readiness — must still happen if Docker isn't
137
+ // installed/running, since that's exactly when local IDE tooling matters most.
138
+ if (!isRclcpp) syncIdeVenv(root); // rclpy is plain Python — rclcpp is C++, no venv needed
139
+
157
140
  assertDockerReady();
158
141
  const pkg = ros2PackageName(root);
159
142
  if (!pkg) throw new Error("no ROS2 package found under ros2_ws/src.");
160
143
 
161
144
  await ensureImage();
162
145
 
163
- const manifest = readManifest(root);
164
146
  const pluginId = manifest.id ?? "";
165
- const isRclcpp = manifest.runtime?.rosClient === "rclcpp";
166
- if (!isRclcpp) syncIdeVenv(root); // rclpy is plain Python — rclcpp is C++, no venv needed
167
147
  const container = `${ROS2_DEV_IMAGE}-run`;
168
148
  const robotIp = process.env.MUSCAT_ROBOT_HOST ?? "";
169
149
  const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
package/cli/src/mcp.mjs CHANGED
@@ -305,16 +305,27 @@ export async function runMcp() {
305
305
  "Get the real Storybook story source for a component (e.g. RBSlider) — actual working " +
306
306
  "props, imports, and layout, not a guessed shape. Prefer this over get_component_props when " +
307
307
  "you just need to know how to use a component, not its full type.",
308
- inputSchema: { name: z.string().describe('Component name, e.g. "RBSlider"') },
308
+ // `component` accepted as an alias — agents often guess it over `name`.
309
+ inputSchema: {
310
+ name: z.string().optional().describe('Component name, e.g. "RBSlider"'),
311
+ component: z.string().optional().describe("Alias for `name`."),
312
+ },
309
313
  },
310
- async ({ name }) => {
311
- const source = findStorySource(name);
314
+ async ({ name, component }) => {
315
+ const target = name ?? component;
316
+ if (!target) {
317
+ return {
318
+ isError: true,
319
+ content: [{ type: "text", text: 'Missing required argument: "name" (component name, e.g. "RBSlider").' }],
320
+ };
321
+ }
322
+ const source = findStorySource(target);
312
323
  if (!source) {
313
324
  return {
314
325
  content: [
315
326
  {
316
327
  type: "text",
317
- text: `No Storybook example found for "${name}". Run "muscat-pk storybook" to browse all components live, or use get_component_props for its type.`,
328
+ text: `No Storybook example found for "${target}". Run "muscat-pk storybook" to browse all components live, or use get_component_props for its type.`,
318
329
  },
319
330
  ],
320
331
  };
package/cli/src/new.mjs CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  writeFileSync,
13
13
  } from "node:fs";
14
14
  import { basename, dirname, resolve } from "node:path";
15
- import { hasBin, kitVersion, parseArgs, prompt, templatesDir, log } from "./util.mjs";
15
+ import { ensureIdeVenv, hasBin, kitVersion, parseArgs, prompt, templatesDir, log } from "./util.mjs";
16
16
 
17
17
  // Like cpSync(src, dst, {recursive:true}) but reads from the embedded snapshot
18
18
  // when the templates dir doesn't exist on disk (compiled binary mode).
@@ -227,6 +227,21 @@ export async function runNew(args) {
227
227
  } catch {
228
228
  log.warn(`'${pm} install' failed; run it manually in ${targetName}`);
229
229
  }
230
+
231
+ // Local .venv is for your editor/linter (pylint, pyright, ...) only — the
232
+ // backend actually RUNS in Docker (see README), this never affects that.
233
+ // rclcpp is C++, so no venv applies there.
234
+ const needsIdeVenv =
235
+ runtimeChoice.type === "python" ||
236
+ (runtimeChoice.type === "ros2" && runtimeChoice.rosClient === "rclpy");
237
+ if (needsIdeVenv) {
238
+ log.step("creating .venv for your editor (pylint/pyright)…");
239
+ if (ensureIdeVenv(target)) {
240
+ log.ok(".venv ready — point your editor's Python interpreter there.");
241
+ } else {
242
+ log.warn("Could not create .venv (no local Python/uv?) — skipping, Docker dev still works.");
243
+ }
244
+ }
230
245
  }
231
246
 
232
247
  // The back end (python / cpp / ros2) runs in a Docker container that matches
package/cli/src/util.mjs CHANGED
@@ -39,6 +39,35 @@ export function venvPython(venvDir) {
39
39
  : resolve(venvDir, "bin", "python");
40
40
  }
41
41
 
42
+ /**
43
+ * Create/refresh a LOCAL `.venv` with the plugin's requirements.txt installed —
44
+ * purely so an editor/linter (pylint, pyright, mypy, ...) can resolve imports.
45
+ * Docker remains the actual execution environment for `dev`/`dev:back`; this
46
+ * never affects it. Uses `uv` if available (fast, pins Python 3.12 — matches
47
+ * the robot), else falls back to the detected system Python.
48
+ * Returns true if the venv is ready with deps installed.
49
+ */
50
+ export function ensureIdeVenv(root) {
51
+ const venvDir = resolve(root, ".venv");
52
+ const py = venvPython(venvDir);
53
+
54
+ if (!existsSync(py)) {
55
+ let created;
56
+ if (hasBin("uv")) {
57
+ created = spawnSync("uv", ["venv", venvDir, "--python", "3.12", "--seed"], { cwd: root, stdio: "ignore" });
58
+ } else {
59
+ const { cmd } = detectPythonCmd();
60
+ created = spawnSync(cmd[0], [...cmd.slice(1), "-m", "venv", venvDir], { cwd: root, stdio: "ignore" });
61
+ }
62
+ if (created.status !== 0 || !existsSync(py)) return false;
63
+ }
64
+
65
+ const reqs = resolve(root, "requirements.txt");
66
+ const args = ["-m", "pip", "install", "-q"];
67
+ if (existsSync(reqs)) args.push("-r", reqs);
68
+ return spawnSync(py, args, { cwd: root, stdio: "ignore" }).status === 0;
69
+ }
70
+
42
71
  /** Published install/CLI package name. Author imports use @rainbow-robotics/plugin-ui separately. */
43
72
  export const KIT_PUBLISHED_NAME = "@rainbow-robotics/muscat-plugin-kit";
44
73
 
@@ -1 +1 @@
1
- {"generatedAt":1783605738795,"userSince":1750932757926,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":false,"hasStorybookEslint":true,"refCount":0,"testPackages":{"@vitest/browser":"3.2.4","@vitest/coverage-v8":"3.2.4","playwright":"1.59.1","vitest":"3.2.4"},"hasRouterPackage":false,"monorepo":"Turborepo","packageManager":{"type":"yarn","version":"berry","agent":"yarn@berry"},"preview":{"usesGlobals":false},"framework":{"name":"@storybook/react-vite","options":{}},"builder":"@storybook/builder-vite","renderer":"@storybook/react","portableStoriesFileCount":0,"applicationFileCount":0,"storybookVersion":"8.6.18","storybookVersionSpecifier":"^8.6.4","language":"typescript","storybookPackages":{"@chromatic-com/storybook":{"version":"3.2.7"},"@storybook/addon-essentials":{"version":"8.6.18"},"@storybook/addon-links":{"version":"8.6.18"},"@storybook/addon-onboarding":{"version":"8.6.18"},"@storybook/addon-themes":{"version":"8.6.18"},"@storybook/blocks":{"version":"8.6.18"},"@storybook/experimental-addon-test":{"version":"8.6.18"},"@storybook/react":{"version":"8.6.18"},"@storybook/react-vite":{"version":"8.6.18"},"@storybook/test":{"version":"8.6.18"},"@storybook/theming":{"version":"8.6.18"},"eslint-plugin-storybook":{"version":"0.11.6"},"storybook":{"version":"8.6.18"},"storybook-addon-react-docgen":{"version":"1.2.44"}},"addons":{"$SNIP/node_modules/@storybook/addon-links":{"version":null},"$SNIP/node_modules/@storybook/addon-essentials":{"version":null},"$SNIP/node_modules/@chromatic-com/storybook":{"version":null},"$SNIP/node_modules/@storybook/addon-themes":{"version":null},"$SNIP/node_modules/@storybook/experimental-addon-test":{"version":null}}}
1
+ {"generatedAt":1783623695483,"userSince":1750932757926,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":false,"hasStorybookEslint":true,"refCount":0,"testPackages":{"@vitest/browser":"3.2.4","@vitest/coverage-v8":"3.2.4","playwright":"1.59.1","vitest":"3.2.4"},"hasRouterPackage":false,"monorepo":"Turborepo","packageManager":{"type":"yarn","version":"berry","agent":"yarn@berry"},"preview":{"usesGlobals":false},"framework":{"name":"@storybook/react-vite","options":{}},"builder":"@storybook/builder-vite","renderer":"@storybook/react","portableStoriesFileCount":0,"applicationFileCount":0,"storybookVersion":"8.6.18","storybookVersionSpecifier":"^8.6.4","language":"typescript","storybookPackages":{"@chromatic-com/storybook":{"version":"3.2.7"},"@storybook/addon-essentials":{"version":"8.6.18"},"@storybook/addon-links":{"version":"8.6.18"},"@storybook/addon-onboarding":{"version":"8.6.18"},"@storybook/addon-themes":{"version":"8.6.18"},"@storybook/blocks":{"version":"8.6.18"},"@storybook/experimental-addon-test":{"version":"8.6.18"},"@storybook/react":{"version":"8.6.18"},"@storybook/react-vite":{"version":"8.6.18"},"@storybook/test":{"version":"8.6.18"},"@storybook/theming":{"version":"8.6.18"},"eslint-plugin-storybook":{"version":"0.11.6"},"storybook":{"version":"8.6.18"},"storybook-addon-react-docgen":{"version":"1.2.44"}},"addons":{"$SNIP/node_modules/@storybook/addon-links":{"version":null},"$SNIP/node_modules/@storybook/addon-essentials":{"version":null},"$SNIP/node_modules/@chromatic-com/storybook":{"version":null},"$SNIP/node_modules/@storybook/addon-themes":{"version":null},"$SNIP/node_modules/@storybook/experimental-addon-test":{"version":null}}}
@@ -10,6 +10,8 @@
10
10
  "@tailwindcss/vite": "^4.1.0",
11
11
  "@rainbow-robotics/muscat-plugin-kit": "__RB_VERSION__",
12
12
  "@rainbow-robotics/preview-host": "__RB_VERSION__",
13
+ "@types/react": "^19.0.10",
14
+ "@types/react-dom": "^19.0.4",
13
15
  "@vitejs/plugin-react": "^4.0.0",
14
16
  "tailwindcss": "^4.1.0",
15
17
  "typescript": "^5.8.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-robotics/muscat-plugin-kit",
3
- "version": "0.1.2-dev.45",
3
+ "version": "0.1.2-dev.46",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=20"
@@ -53,8 +53,8 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@modelcontextprotocol/sdk": "^1.29.0",
56
- "@rainbow-robotics/plugin-ui": "0.1.2-dev.45",
57
- "@rainbow-robotics/preview-host": "0.1.2-dev.45",
56
+ "@rainbow-robotics/plugin-ui": "0.1.2-dev.46",
57
+ "@rainbow-robotics/preview-host": "0.1.2-dev.46",
58
58
  "postcss": "^8.5.8",
59
59
  "socket.io": "^4.8.1",
60
60
  "typescript": "^5.8.0",