@rainbow-robotics/muscat-plugin-kit 1.3.3 → 1.3.6-dev.0

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.
Files changed (98) hide show
  1. package/cli/docker/Dockerfile.cpp +1 -1
  2. package/cli/docker/Dockerfile.dev +1 -1
  3. package/cli/src/build.mjs +17 -0
  4. package/cli/src/devBack.mjs +52 -6
  5. package/cli/src/manifest.mjs +11 -0
  6. package/cli/src/mcp.mjs +2 -1
  7. package/cli/src/new.mjs +7 -1
  8. package/cli/src/templates-embedded.mjs +2 -1
  9. package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +98 -1
  10. package/cli/storybook-dist/assets/{Color-YHDXOIA2-DPI4Is-F.js → Color-YHDXOIA2-C5ttlJWI.js} +1 -1
  11. package/cli/storybook-dist/assets/{Docs-Dow4_Rnl.js → Docs-B3AV-YvP.js} +1 -1
  12. package/cli/storybook-dist/assets/{Docs-CbCty_Qg.js → Docs-BN206lKl.js} +1 -1
  13. package/cli/storybook-dist/assets/{Docs-CQnf2HJu.js → Docs-BqH2prGO.js} +1 -1
  14. package/cli/storybook-dist/assets/{Docs-TMe-1-I2.js → Docs-CCfiEyzU.js} +1 -1
  15. package/cli/storybook-dist/assets/{Docs-DDYksosp.js → Docs-CGN1_Ua8.js} +1 -1
  16. package/cli/storybook-dist/assets/{Docs-CKTHz6Db.js → Docs-DhncK7Aw.js} +1 -1
  17. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-DSTzrCAK.js → DocsRenderer-CFRXHY34-D-w8Kq64.js} +1 -1
  18. package/cli/storybook-dist/assets/{NxAvatar.stories-B3OxXBgc.js → NxAvatar.stories-oCcYtLwq.js} +1 -1
  19. package/cli/storybook-dist/assets/{NxButton.stories-UTqX8ZkA.js → NxButton.stories-C5jUScIj.js} +1 -1
  20. package/cli/storybook-dist/assets/{NxNumberInput.stories-GoLEe-uJ.js → NxNumberInput.stories-gjsEulIN.js} +1 -1
  21. package/cli/storybook-dist/assets/{NxPagination.stories-Jj6dt6eG.js → NxPagination.stories-BK6uSWIa.js} +1 -1
  22. package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-tLN9u4nC.js → NxPointerHoldButton.stories-BDD5CvSZ.js} +1 -1
  23. package/cli/storybook-dist/assets/{NxRangeInput.stories-C-MK73xb.js → NxRangeInput.stories-C_nW26_x.js} +1 -1
  24. package/cli/storybook-dist/assets/{NxSelectBox.stories-CBcFcOIX.js → NxSelectBox.stories-DqLfKU9H.js} +1 -1
  25. package/cli/storybook-dist/assets/{NxSlideRange.stories-oNQkoW66.js → NxSlideRange.stories-CQjHA8yv.js} +1 -1
  26. package/cli/storybook-dist/assets/{NxSwitch.stories-CqBkO5GC.js → NxSwitch.stories-BMHCiPoL.js} +1 -1
  27. package/cli/storybook-dist/assets/{NxTextArea.stories-DtysSyl4.js → NxTextArea.stories-k7r1WVQh.js} +1 -1
  28. package/cli/storybook-dist/assets/{NxTextInput.stories-BOLDbSgF.js → NxTextInput.stories-Cqfe1I32.js} +1 -1
  29. package/cli/storybook-dist/assets/{RBAccordion.stories-D9-wu9LY.js → RBAccordion.stories-CJ3Ttuja.js} +1 -1
  30. package/cli/storybook-dist/assets/{RBAccordionGroup.stories-BPfnIKm6.js → RBAccordionGroup.stories-BThv7tM0.js} +1 -1
  31. package/cli/storybook-dist/assets/{RBAlert.stories-CGn-c2ac.js → RBAlert.stories-CSMP8Vun.js} +1 -1
  32. package/cli/storybook-dist/assets/{RBBadge.stories-CBLmeoq1.js → RBBadge.stories-cBMA2R6F.js} +1 -1
  33. package/cli/storybook-dist/assets/{RBBattery.stories-oo47rbDh.js → RBBattery.stories-BKVOskqs.js} +1 -1
  34. package/cli/storybook-dist/assets/{RBCard.stories-SiD2VfQ4.js → RBCard.stories-BTOfK0HC.js} +1 -1
  35. package/cli/storybook-dist/assets/{RBCheckBox.stories-sEEVrjSg.js → RBCheckBox.stories-DyE912wG.js} +1 -1
  36. package/cli/storybook-dist/assets/{RBCheckIconButton.stories-C5GQUjZf.js → RBCheckIconButton.stories-0sLFU0kM.js} +1 -1
  37. package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-DTaTqBvJ.js → RBCheckLabelButton.stories-c8VecClE.js} +1 -1
  38. package/cli/storybook-dist/assets/{RBChipTab.stories-3AZr4kOz.js → RBChipTab.stories-CQ9Uav5_.js} +1 -1
  39. package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-hCJnBYQl.js → RBChipsBoxTab.stories-Ap4NlhAZ.js} +1 -1
  40. package/cli/storybook-dist/assets/{RBColorIcons.stories-BX4RJygu.js → RBColorIcons.stories-BPPXjif2.js} +1 -1
  41. package/cli/storybook-dist/assets/{RBCommand.stories-BidZ4H8f.js → RBCommand.stories-PDpYOKlm.js} +1 -1
  42. package/cli/storybook-dist/assets/{RBConfirm.stories-D3Txw1kT.js → RBConfirm.stories-BEWvMAYP.js} +1 -1
  43. package/cli/storybook-dist/assets/{RBDateInput.stories-CLxRaN6T.js → RBDateInput.stories-DLdyNnVF.js} +1 -1
  44. package/cli/storybook-dist/assets/{RBDialog.stories-CEhmUGp-.js → RBDialog.stories-HsawMNOb.js} +1 -1
  45. package/cli/storybook-dist/assets/{RBDivider.stories-peXP2KG9.js → RBDivider.stories-dlWwOxik.js} +1 -1
  46. package/cli/storybook-dist/assets/{RBDrawer.stories-38iDf6uC.js → RBDrawer.stories-C87iqGk0.js} +1 -1
  47. package/cli/storybook-dist/assets/{RBDropdownMenu.stories-Dsi8tqSy.js → RBDropdownMenu.stories-B8EQhA0z.js} +1 -1
  48. package/cli/storybook-dist/assets/{RBEmptyView.stories-VMxWhzLP.js → RBEmptyView.stories-SzpIhNDw.js} +1 -1
  49. package/cli/storybook-dist/assets/{RBIconButton.stories-BPLhceGO.js → RBIconButton.stories-BQ0nM1gb.js} +1 -1
  50. package/cli/storybook-dist/assets/{RBIcons.stories-DuYdO951.js → RBIcons.stories-C19g5M5z.js} +1 -1
  51. package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-CGNYhQyl.js → RBInlineLabelButton.stories-DbgI4-1P.js} +1 -1
  52. package/cli/storybook-dist/assets/{RBJoyStick.stories-CCmn-LMy.js → RBJoyStick.stories-smF-vpGL.js} +1 -1
  53. package/cli/storybook-dist/assets/{RBLabelButton.stories-IkKjthGk.js → RBLabelButton.stories-s53klwDv.js} +1 -1
  54. package/cli/storybook-dist/assets/{RBLight.stories-eJFQOjPH.js → RBLight.stories-BnzkoR51.js} +1 -1
  55. package/cli/storybook-dist/assets/{RBMenubar.stories-kg-xXGrW.js → RBMenubar.stories-CW014klu.js} +1 -1
  56. package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-Czr1r3gd.js → RBMultiRangeInput.stories-qa4tH_4Y.js} +1 -1
  57. package/cli/storybook-dist/assets/{RBPagination.stories-Dyu6Khnl.js → RBPagination.stories-YoQNUpQH.js} +1 -1
  58. package/cli/storybook-dist/assets/{RBPopover.stories-CAM6hpf2.js → RBPopover.stories-Bhpi0OaH.js} +1 -1
  59. package/cli/storybook-dist/assets/{RBPrompt.stories-B4M-veaf.js → RBPrompt.stories-iQM5iAka.js} +1 -1
  60. package/cli/storybook-dist/assets/{RBRadioBox.stories-DeAOKvil.js → RBRadioBox.stories-BGyq4rzx.js} +1 -1
  61. package/cli/storybook-dist/assets/{RBRadioButton.stories-DjXwYXFy.js → RBRadioButton.stories-DF6BcUcJ.js} +1 -1
  62. package/cli/storybook-dist/assets/{RBRangeDateInput.stories-9pzKhXGf.js → RBRangeDateInput.stories-COOQ_HNF.js} +1 -1
  63. package/cli/storybook-dist/assets/{RBResizable.stories-B4Hi4DUD.js → RBResizable.stories-DSVHvwjp.js} +1 -1
  64. package/cli/storybook-dist/assets/RBSelectBox.stories-BookV8Sm.js +74 -0
  65. package/cli/storybook-dist/assets/{RBSheet.stories-BwOCmaNN.js → RBSheet.stories-CAK9hb-p.js} +1 -1
  66. package/cli/storybook-dist/assets/{RBSideNumberInput.stories-BbGOIitU.js → RBSideNumberInput.stories-DUymnH-9.js} +1 -1
  67. package/cli/storybook-dist/assets/{RBSlider.stories-yIDdawYQ.js → RBSlider.stories-BwlDh9--.js} +1 -1
  68. package/cli/storybook-dist/assets/{RBSpinner.stories-CIJvpbgK.js → RBSpinner.stories-VOlkuP6t.js} +1 -1
  69. package/cli/storybook-dist/assets/{RBSwitch.stories-Bo8hrSiq.js → RBSwitch.stories-BiW2dQnT.js} +1 -1
  70. package/cli/storybook-dist/assets/{RBTab.stories-Cq3N4Dgb.js → RBTab.stories-DFKVsy0C.js} +1 -1
  71. package/cli/storybook-dist/assets/{RBTable.stories-DDeZf-Wb.js → RBTable.stories-BWG0ZSYI.js} +1 -1
  72. package/cli/storybook-dist/assets/{RBTableRow.stories-BeKVMSo3.js → RBTableRow.stories-JVTD83Mh.js} +1 -1
  73. package/cli/storybook-dist/assets/{RBTabs.stories-DovB7PPB.js → RBTabs.stories-D-aN385u.js} +1 -1
  74. package/cli/storybook-dist/assets/{RBTextArea.stories-D7H4RvP8.js → RBTextArea.stories-Dx5xkmZS.js} +1 -1
  75. package/cli/storybook-dist/assets/{RBTextField.stories-CmkNXeEk.js → RBTextField.stories-DmVciExd.js} +1 -1
  76. package/cli/storybook-dist/assets/{RBTextInput.stories-hnxbQJsb.js → RBTextInput.stories-CCf0VGEv.js} +1 -1
  77. package/cli/storybook-dist/assets/{RBToast.stories-BklGGkNh.js → RBToast.stories-DT0DGn05.js} +1 -1
  78. package/cli/storybook-dist/assets/{RBToggle.stories-C56AXyxq.js → RBToggle.stories-Ce_q3EtP.js} +1 -1
  79. package/cli/storybook-dist/assets/{RBToggleGroup.stories-CpLPdAF0.js → RBToggleGroup.stories-CN40YHsR.js} +1 -1
  80. package/cli/storybook-dist/assets/{RBTooltip.stories-BdOnCwGk.js → RBTooltip.stories-L7btDEpA.js} +1 -1
  81. package/cli/storybook-dist/assets/{SideNumberInput-DLmM2_Dy.js → SideNumberInput-B5_xLK8z.js} +119 -119
  82. package/cli/storybook-dist/assets/{entry-preview-tBizwvut.js → entry-preview-D_nGAXUk.js} +1 -1
  83. package/cli/storybook-dist/assets/{iframe-BVXraKDc.js → iframe-23VS_ZeI.js} +2 -2
  84. package/cli/storybook-dist/assets/{index-CI3FxSvF.js → index-Ckae_HWe.js} +3 -3
  85. package/cli/storybook-dist/assets/{index-C1OzuK6-.js → index-ClAc6FBW.js} +1 -1
  86. package/cli/storybook-dist/assets/{preview-CILRoSnB.js → preview-BVJwlGUF.js} +1 -1
  87. package/cli/storybook-dist/assets/{preview-BUOha-H6.js → preview-CD0LINug.js} +2 -2
  88. package/cli/storybook-dist/assets/preview-P8yWWsME.js +3 -0
  89. package/cli/storybook-dist/iframe.html +1 -1
  90. package/cli/storybook-dist/index.json +1 -1
  91. package/cli/storybook-dist/project.json +1 -1
  92. package/cli/templates/common/README.md +27 -2
  93. package/cli/templates/common/requirements.txt +3 -3
  94. package/cli/templates/readme-cpp/README.md +26 -2
  95. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +3 -4
  96. package/package.json +3 -3
  97. package/cli/storybook-dist/assets/RBSelectBox.stories-tVVv4c6z.js +0 -6
  98. package/cli/storybook-dist/assets/preview-DGIRF99V.js +0 -3
@@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
12
12
 
13
13
  # Rainbow Robotics C++ SDK for find_package(rb_sdk).
14
14
  # Override with MUSCAT_PK_RB_SDK_VERSION=<ver> (+ MUSCAT_PK_REBUILD_IMAGE=1).
15
- ARG RB_SDK_VERSION=0.1.3
15
+ ARG RB_SDK_VERSION=0.1.6
16
16
  RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
17
17
  && install -d /etc/apt/keyrings \
18
18
  && curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key -o /etc/apt/keyrings/rb-sdk.asc \
@@ -11,7 +11,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
11
11
 
12
12
  # Rainbow Robotics C++ SDK for find_package(rb_sdk).
13
13
  # Override with MUSCAT_PK_RB_SDK_VERSION=<ver> (+ MUSCAT_PK_REBUILD_IMAGE=1).
14
- ARG RB_SDK_VERSION=0.1.3
14
+ ARG RB_SDK_VERSION=0.1.6
15
15
  RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
16
16
  && install -d /etc/apt/keyrings \
17
17
  && curl -fsSL https://rainbowrobotics.github.io/rb-sdk/rb-sdk-apt-repo.gpg.key -o /etc/apt/keyrings/rb-sdk.asc \
package/cli/src/build.mjs CHANGED
@@ -74,6 +74,21 @@ function runViteBuild(pluginRoot, { mode = "production" } = {}) {
74
74
  // For ros2 plugins, compile the colcon workspace inside the Jazzy + Python 3.12 container.
75
75
  // rclpy: arch-agnostic (pure Python) → dist/ros2_ws/install/ (built once)
76
76
  // rclcpp: arch-specific C++ binaries → dist/ros2_ws/install-{arch}/ per arch
77
+ // Cross-arch C++ builds run inside an emulated container. Docker Desktop ships the
78
+ // emulation; native Linux needs binfmt/QEMU — check up front so the error says so.
79
+ function assertCanBuildArch(arch, dockerPlatform) {
80
+ const info = spawnSync("docker", ["info", "--format", "{{.Architecture}}"], { encoding: "utf8" });
81
+ const host = { x86_64: "amd64", aarch64: "arm64", arm64: "arm64" }[(info.stdout || "").trim()] ?? "";
82
+ if (arch === host) return;
83
+ const r = spawnSync("docker", ["run", "--rm", "--platform", dockerPlatform, "ubuntu:24.04", "true"], { stdio: "ignore" });
84
+ if (r.status === 0) return;
85
+ throw new Error(
86
+ `can't build ${arch} on this ${host || "machine"}: Docker has no ${arch} emulation.\n` +
87
+ ` enable it once: docker run --privileged --rm tonistiigi/binfmt --install ${arch}\n` +
88
+ ` or build only this machine's arch: muscat-pk build --arch ${host || "<arch>"}`,
89
+ );
90
+ }
91
+
77
92
  export async function exportRos2Build(root, archs = ["amd64", "arm64"]) {
78
93
  if (manifestRuntime(root) !== "ros2") return;
79
94
  const { assertDockerReady, ensureImage, colconBuild, ROS2_DEV_IMAGE, ARCH_DOCKER_PLATFORM } = await import("./ros2.mjs");
@@ -85,6 +100,7 @@ export async function exportRos2Build(root, archs = ["amd64", "arm64"]) {
85
100
  for (const arch of archs) {
86
101
  const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
87
102
  if (!dockerPlatform) throw new Error(`unknown arch '${arch}'`);
103
+ assertCanBuildArch(arch, dockerPlatform);
88
104
  await ensureImage(`${ROS2_DEV_IMAGE}-${arch}`, { platform: dockerPlatform });
89
105
  colconBuild(root, { symlink: false, arch });
90
106
  log.ok(`dist/ros2_ws/install-${arch}/ ready`);
@@ -111,6 +127,7 @@ export async function exportCppBuild(root, distDir, archs = ["amd64", "arm64"])
111
127
  for (const arch of archs) {
112
128
  const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
113
129
  if (!dockerPlatform) throw new Error(`unknown arch '${arch}'`);
130
+ assertCanBuildArch(arch, dockerPlatform);
114
131
  await ensureImage(`${CPP_DEV_IMAGE}-${arch}`, { platform: dockerPlatform });
115
132
  cmakeBuild(root, { buildType: "Release", arch });
116
133
  const built = resolve(root, `cpp/build-${arch}`, entry);
@@ -8,11 +8,12 @@
8
8
  // ros2 : Jazzy + Python 3.12 container, colcon build, run the node, restart
9
9
  // on source changes (rclcpp rebuilds on change).
10
10
  import { spawn, spawnSync } from "node:child_process";
11
- import { existsSync, watch } from "node:fs";
12
- import { resolve } from "node:path";
11
+ import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from "node:fs";
12
+ import { isIP } from "node:net";
13
+ import { dirname, resolve } from "node:path";
13
14
  import { ensureDocker, ensureIdeVenv, findPluginRoot, log, promptRobotConnection, realtimeFlags } from "./util.mjs";
14
15
  import { ensureRemoteZenohEnv, usesHostNetwork } from "./zenohDev.mjs";
15
- import { assertManifestPorts, assertManifestVariables, manifestRuntime, readManifest, validateManifest } from "./manifest.mjs";
16
+ import { PLUGIN_ID_RE, assertManifestPorts, assertManifestVariables, manifestRuntime, readManifest, rosNamespace, validateManifest } from "./manifest.mjs";
16
17
  import {
17
18
  formatPortRanges,
18
19
  portEnvFlags,
@@ -69,6 +70,43 @@ function backendNetwork(portRanges) {
69
70
  };
70
71
  }
71
72
 
73
+ // rb.rosPeers in dev: answer muscat/plugin/<id>/ros_peers/{get,set} on the gateway the way
74
+ // Muscat (common) does on the robot — keep the list in .muscat-dev/ and restart the node
75
+ // with ROS_STATIC_PEERS.
76
+ const ROS_PEERS_FILE = ".muscat-dev/ros-peers.json";
77
+
78
+ function readRosPeers(root) {
79
+ try {
80
+ return JSON.parse(readFileSync(resolve(root, ROS_PEERS_FILE), "utf8"));
81
+ } catch {
82
+ return [];
83
+ }
84
+ }
85
+
86
+ async function serveRosPeers(root, pluginId, restart) {
87
+ if (!pluginId) return;
88
+ const { RbWs } = await import("@rainbow-robotics/plugin-ui/ws");
89
+ const ws = await Promise.race([
90
+ RbWs.connect("http://127.0.0.1:10000", pluginId).catch(() => null),
91
+ new Promise((r) => setTimeout(() => r(null), 5000)),
92
+ ]);
93
+ if (!ws) return log.warn("rb.rosPeers unavailable in dev: plugin gateway not reachable on :10000");
94
+ const base = `/muscat/plugin/${pluginId}/ros_peers`;
95
+ await ws.serve(`${base}/get`, () => ({ peers: readRosPeers(root) }));
96
+ await ws.serve(`${base}/set`, (payload) => {
97
+ const peers = payload?.peers;
98
+ if (!Array.isArray(peers) || !peers.every((p) => typeof p === "string" && isIP(p))) {
99
+ return { ok: false, error: "peers must be a list of IP addresses" };
100
+ }
101
+ const file = resolve(root, ROS_PEERS_FILE);
102
+ mkdirSync(dirname(file), { recursive: true });
103
+ writeFileSync(file, JSON.stringify(peers) + "\n");
104
+ log.step(`ROS_STATIC_PEERS → ${peers.join(";") || "(none)"} — restarting node`);
105
+ restart();
106
+ return { ok: true, peers };
107
+ });
108
+ }
109
+
72
110
  // Our own MediaMTX, only when `dev` found its ports free (a local Muscat brings its own).
73
111
  function startMediaServer(container) {
74
112
  if (process.env.MUSCAT_PK_MEDIA !== "1") return;
@@ -435,6 +473,8 @@ async function runRos2(root, portRanges) {
435
473
  // Same rationale as runPython(): no -t on docker exec below means
436
474
  // rclpy's Python process stdout is block-buffered, not line-buffered.
437
475
  "-e", "PYTHONUNBUFFERED=1",
476
+ // Same as the robot: ROS discovery stays on this host (allow a PC with ROS_STATIC_PEERS).
477
+ "-e", "ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST",
438
478
  ...net.env,
439
479
  ...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
440
480
  ...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
@@ -463,18 +503,24 @@ async function runRos2(root, portRanges) {
463
503
  let child = null;
464
504
  const startNode = () => {
465
505
  if (child) child.kill();
466
- // Same command the robot runs: rclpy execs the console script, rclcpp uses `ros2 run`.
506
+ // killing the `docker exec` client doesn't stop the node inside the container
507
+ spawnSync("docker", ["exec", container, "pkill", "-f", `${pkg}_node|${manifest.runtime?.launch || pkg}`], { stdio: "ignore" });
508
+ const peers = readRosPeers(root);
509
+ // Same command the robot runs: rclpy execs the console script, rclcpp uses `ros2 run`;
510
+ // relative topics land under the plugin's namespace (rosNamespace).
467
511
  const launch = manifest.runtime?.launch || `${pkg}_node`;
468
- const run = isRclcpp ? `ros2 run ${pkg} ${launch}` : `exec ${launch}`;
512
+ const nsArgs = PLUGIN_ID_RE.test(pluginId) ? ` --ros-args -r __ns:=${rosNamespace(pluginId)}` : "";
513
+ const run = (isRclcpp ? `ros2 run ${pkg} ${launch}` : `exec ${launch}`) + nsArgs;
469
514
  log.step(run);
470
515
  child = spawn(
471
516
  "docker",
472
- ["exec", ...envFlags, container, "bash", "-lc",
517
+ ["exec", ...envFlags, ...(peers.length ? ["-e", `ROS_STATIC_PEERS=${peers.join(";")}`] : []), container, "bash", "-lc",
473
518
  `cd ros2_ws && source install/setup.bash && ${run}`],
474
519
  { cwd: root, stdio: "inherit" },
475
520
  );
476
521
  };
477
522
  startNode();
523
+ serveRosPeers(root, pluginId, startNode);
478
524
 
479
525
  // restart node on source change. rclpy reflects .py edits live (symlink-install);
480
526
  // rclcpp (C++) and package-metadata changes require a colcon rebuild.
@@ -11,6 +11,15 @@ export const CONTRIBUTION_POINTS = [
11
11
  "program.panel",
12
12
  ];
13
13
  const ICON_SIZES = ["16", "48", "128"];
14
+ // Reverse-domain id: "."-separated parts of lowercase letters/digits joined by single "-",
15
+ // each part starting with a letter. Keeps the ROS namespace derived from it (rosNamespace)
16
+ // valid and collision-free (no "_" in ids, so "." and "-" map to distinct characters).
17
+ export const PLUGIN_ID_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)*$/;
18
+
19
+ /** ROS namespace for a plugin id: "." → "/", "-" → "_" (com.example.my-plugin → /com/example/my_plugin). */
20
+ export function rosNamespace(id) {
21
+ return "/" + id.replace(/\./g, "/").replace(/-/g, "_");
22
+ }
14
23
  const RUNTIME_TYPES = ["none", "python", "cpp", "ros2"];
15
24
  const ROS_CLIENTS = ["rclpy", "rclcpp"];
16
25
  const I18N_LANGS = ["ko", "en", "ja", "zh", "vi", "th", "es"];
@@ -192,6 +201,8 @@ export function validateManifest(manifest) {
192
201
  if (typeof manifest[key] !== "string" || manifest[key].length === 0)
193
202
  errors.push(`"${key}" is required`);
194
203
  }
204
+ if (typeof manifest.id === "string" && manifest.id && !PLUGIN_ID_RE.test(manifest.id))
205
+ errors.push(`"id" must be "."-separated parts of lowercase letters/digits and single "-", each starting with a letter (e.g. com.example.my-plugin)`);
195
206
  if (typeof manifest.author !== "string" || manifest.author.length === 0)
196
207
  errors.push(`"author" is required`);
197
208
  if (typeof manifest.email !== "string" || manifest.email.length === 0)
package/cli/src/mcp.mjs CHANGED
@@ -683,7 +683,8 @@ export async function runMcp() {
683
683
  "host), manifest.json (including the \"variables\" declaration for the pyfm variable picker — " +
684
684
  "names require the RBP_ prefix), program variables & executor state (ctx.get_variable/" +
685
685
  "set_variable/executor_state/check_stop), video streaming (MediaMTX: backend publishes RTSP to " +
686
- "127.0.0.1:8554, UI plays WebRTC from :8889), contribution points, and backend runtimes " +
686
+ "127.0.0.1:8554, UI plays WebRTC from :8889), ROS 2 topics (plugin namespace, rb.rosPeers to allow " +
687
+ "an external PC), contribution points, and backend runtimes " +
687
688
  "(Python/C++/ROS2). Covers BOTH backends: the Python README (py/main.py, rainbow.rb_sdk) and the " +
688
689
  "C++ README (cpp/main.cpp or an rclcpp node: rb_plugin::RBPluginCall serve/subscribe/publish/" +
689
690
  "router(), rb_plugin::PluginProgramContext, RBManipulateSDK, CMake rb_sdk::rb_sdk). Pass lang " +
package/cli/src/new.mjs CHANGED
@@ -106,6 +106,12 @@ export function applyTokens(dir, tokens) {
106
106
  }
107
107
 
108
108
  // Derive naming tokens from a plugin directory name.
109
+ // One id part (see PLUGIN_ID_RE): lowercase, single "-", starts with a letter.
110
+ function idPart(name) {
111
+ const part = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
112
+ return /^[a-z]/.test(part) ? part : part ? `plugin-${part}` : "plugin";
113
+ }
114
+
109
115
  export function deriveNames(target) {
110
116
  const base = basename(target);
111
117
  const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "_");
@@ -115,7 +121,7 @@ export function deriveNames(target) {
115
121
  slug,
116
122
  pluginName,
117
123
  pluginClass: pluginName.replace(/\s+/g, ""),
118
- pluginId: `com.example.${base.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
124
+ pluginId: `com.example.${idPart(base)}`,
119
125
  };
120
126
  }
121
127