@rainbow-robotics/muscat-plugin-kit 1.3.6-dev.8 → 1.3.7

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 (125) hide show
  1. package/cli/README.md +8 -3
  2. package/cli/bin/muscat-pk.mjs +5 -0
  3. package/cli/bundle-templates.mjs +6 -1
  4. package/cli/docker/Dockerfile.cpp +28 -6
  5. package/cli/docker/Dockerfile.dev +32 -0
  6. package/cli/docker/Dockerfile.python +19 -0
  7. package/cli/src/bridge.mjs +45 -12
  8. package/cli/src/build.mjs +48 -79
  9. package/cli/src/cpp.mjs +11 -5
  10. package/cli/src/dev.mjs +7 -2
  11. package/cli/src/devBack.mjs +97 -40
  12. package/cli/src/host_agent.py +143 -83
  13. package/cli/src/manifest.mjs +11 -0
  14. package/cli/src/mcp.mjs +398 -54
  15. package/cli/src/new.mjs +13 -3
  16. package/cli/src/ports.mjs +21 -7
  17. package/cli/src/python.mjs +16 -20
  18. package/cli/src/ros2.mjs +16 -8
  19. package/cli/src/sdk.mjs +23 -21
  20. package/cli/src/type.mjs +44 -10
  21. package/cli/src/uninstall.mjs +3 -20
  22. package/cli/src/update.mjs +14 -8
  23. package/cli/src/util.mjs +70 -9
  24. package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +13 -1
  25. package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +98 -1
  26. package/cli/storybook-dist/assets/{Color-YHDXOIA2-Du8dJ-nM.js → Color-YHDXOIA2-CdTgUqrV.js} +1 -1
  27. package/cli/storybook-dist/assets/{Docs-Sy5RDbtH.js → Docs-C8wx5zsC.js} +1 -1
  28. package/cli/storybook-dist/assets/{Docs-guGflRzZ.js → Docs-CbRKwHpQ.js} +1 -1
  29. package/cli/storybook-dist/assets/{Docs-Cb72AJnj.js → Docs-Cf0s04QW.js} +1 -1
  30. package/cli/storybook-dist/assets/{Docs-pwGqGTV_.js → Docs-CsxMprX5.js} +1 -1
  31. package/cli/storybook-dist/assets/{Docs-LdjYwuG4.js → Docs-FyPFrbUS.js} +1 -1
  32. package/cli/storybook-dist/assets/{Docs--Fm8069p.js → Docs-YtqU-aVu.js} +1 -1
  33. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-YGBLQ4BD.js → DocsRenderer-CFRXHY34-VSENNRoY.js} +1 -1
  34. package/cli/storybook-dist/assets/{NxAvatar.stories-C1d4n8cq.js → NxAvatar.stories-oCcYtLwq.js} +1 -1
  35. package/cli/storybook-dist/assets/{NxButton.stories-gCBoZdMq.js → NxButton.stories-C5jUScIj.js} +1 -1
  36. package/cli/storybook-dist/assets/{NxIcons.stories-Dg42riT5.js → NxIcons.stories-DeqrVzVs.js} +1 -1
  37. package/cli/storybook-dist/assets/{NxNumberInput.stories-DB-vLM2J.js → NxNumberInput.stories-gjsEulIN.js} +1 -1
  38. package/cli/storybook-dist/assets/{NxPagination.stories-CS8iMIak.js → NxPagination.stories-BK6uSWIa.js} +1 -1
  39. package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-D66BopeS.js → NxPointerHoldButton.stories-BDD5CvSZ.js} +1 -1
  40. package/cli/storybook-dist/assets/{NxRangeInput.stories-CZxddsJ2.js → NxRangeInput.stories-C_nW26_x.js} +3 -3
  41. package/cli/storybook-dist/assets/{NxSelectBox.stories-CmezGGrm.js → NxSelectBox.stories-DqLfKU9H.js} +1 -1
  42. package/cli/storybook-dist/assets/{NxSlideRange.stories-CGAFMITi.js → NxSlideRange.stories-CQjHA8yv.js} +1 -1
  43. package/cli/storybook-dist/assets/{NxSwitch.stories-B3QInkXX.js → NxSwitch.stories-BMHCiPoL.js} +1 -1
  44. package/cli/storybook-dist/assets/{NxTextArea.stories-BgJcgZNe.js → NxTextArea.stories-k7r1WVQh.js} +1 -1
  45. package/cli/storybook-dist/assets/{NxTextInput.stories-CwMQwI9L.js → NxTextInput.stories-Cqfe1I32.js} +1 -1
  46. package/cli/storybook-dist/assets/{RBAccordion.stories-BuBVNzJT.js → RBAccordion.stories-CJ3Ttuja.js} +1 -1
  47. package/cli/storybook-dist/assets/{RBAccordionGroup.stories-Crdb6kFF.js → RBAccordionGroup.stories-BThv7tM0.js} +1 -1
  48. package/cli/storybook-dist/assets/{RBAlert.stories-CqXgUvA2.js → RBAlert.stories-CSMP8Vun.js} +1 -1
  49. package/cli/storybook-dist/assets/{RBBadge.stories-BCw_ksML.js → RBBadge.stories-cBMA2R6F.js} +1 -1
  50. package/cli/storybook-dist/assets/{RBBattery.stories-0id4XYoc.js → RBBattery.stories-BKVOskqs.js} +1 -1
  51. package/cli/storybook-dist/assets/{RBCard.stories-CgpG-7Lk.js → RBCard.stories-BTOfK0HC.js} +1 -1
  52. package/cli/storybook-dist/assets/{RBCheckBox.stories-BG02BoGX.js → RBCheckBox.stories-DyE912wG.js} +1 -1
  53. package/cli/storybook-dist/assets/{RBCheckIconButton.stories-vrjD4GQ4.js → RBCheckIconButton.stories-0sLFU0kM.js} +1 -1
  54. package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-POba8lU-.js → RBCheckLabelButton.stories-c8VecClE.js} +1 -1
  55. package/cli/storybook-dist/assets/{RBChipTab.stories-XWJbFrHF.js → RBChipTab.stories-CQ9Uav5_.js} +1 -1
  56. package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-BdU0Dzdp.js → RBChipsBoxTab.stories-Ap4NlhAZ.js} +1 -1
  57. package/cli/storybook-dist/assets/{RBColorIcons.stories-C_0dCNXG.js → RBColorIcons.stories-BPPXjif2.js} +1 -1
  58. package/cli/storybook-dist/assets/{RBCommand.stories-Dvrko5No.js → RBCommand.stories-PDpYOKlm.js} +1 -1
  59. package/cli/storybook-dist/assets/{RBConfirm.stories-CkTn0sHa.js → RBConfirm.stories-BEWvMAYP.js} +1 -1
  60. package/cli/storybook-dist/assets/{RBDateInput.stories-BH3D08cs.js → RBDateInput.stories-DLdyNnVF.js} +1 -1
  61. package/cli/storybook-dist/assets/{RBDialog.stories-C2nNX9fl.js → RBDialog.stories-HsawMNOb.js} +1 -1
  62. package/cli/storybook-dist/assets/{RBDivider.stories-D-GrJ0QZ.js → RBDivider.stories-dlWwOxik.js} +1 -1
  63. package/cli/storybook-dist/assets/{RBDrawer.stories-XLOnLOnh.js → RBDrawer.stories-C87iqGk0.js} +1 -1
  64. package/cli/storybook-dist/assets/{RBDropdownMenu.stories-BjBSeoV2.js → RBDropdownMenu.stories-B8EQhA0z.js} +1 -1
  65. package/cli/storybook-dist/assets/{RBEmptyView.stories-C6U2zKNE.js → RBEmptyView.stories-SzpIhNDw.js} +1 -1
  66. package/cli/storybook-dist/assets/{RBIconButton.stories-MCJq5Hta.js → RBIconButton.stories-BQ0nM1gb.js} +1 -1
  67. package/cli/storybook-dist/assets/{RBIcons.stories-DxOCiCe0.js → RBIcons.stories-C19g5M5z.js} +1 -1
  68. package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-BVZi1aEs.js → RBInlineLabelButton.stories-DbgI4-1P.js} +1 -1
  69. package/cli/storybook-dist/assets/{RBJoyStick.stories-BDThbEek.js → RBJoyStick.stories-smF-vpGL.js} +1 -1
  70. package/cli/storybook-dist/assets/{RBLabelButton.stories-CaPEKUS6.js → RBLabelButton.stories-s53klwDv.js} +1 -1
  71. package/cli/storybook-dist/assets/{RBLight.stories-CihQzyEL.js → RBLight.stories-BnzkoR51.js} +1 -1
  72. package/cli/storybook-dist/assets/{RBMenubar.stories-J852XVFh.js → RBMenubar.stories-CW014klu.js} +1 -1
  73. package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-DXyX0cOZ.js → RBMultiRangeInput.stories-qa4tH_4Y.js} +1 -1
  74. package/cli/storybook-dist/assets/{RBPagination.stories-C9_49ymO.js → RBPagination.stories-YoQNUpQH.js} +1 -1
  75. package/cli/storybook-dist/assets/{RBPopover.stories-DxLE912-.js → RBPopover.stories-Bhpi0OaH.js} +1 -1
  76. package/cli/storybook-dist/assets/{RBPrompt.stories-DlsfGqWb.js → RBPrompt.stories-iQM5iAka.js} +1 -1
  77. package/cli/storybook-dist/assets/{RBRadioBox.stories-hb5cNWW1.js → RBRadioBox.stories-BGyq4rzx.js} +1 -1
  78. package/cli/storybook-dist/assets/{RBRadioButton.stories-CSuOgeN-.js → RBRadioButton.stories-DF6BcUcJ.js} +1 -1
  79. package/cli/storybook-dist/assets/RBRangeDateInput.stories-COOQ_HNF.js +15 -0
  80. package/cli/storybook-dist/assets/{RBResizable.stories-BetjF9vG.js → RBResizable.stories-DSVHvwjp.js} +1 -1
  81. package/cli/storybook-dist/assets/RBSelectBox.stories-BookV8Sm.js +74 -0
  82. package/cli/storybook-dist/assets/{RBSheet.stories-D3QbcMKq.js → RBSheet.stories-CAK9hb-p.js} +1 -1
  83. package/cli/storybook-dist/assets/{RBSideNumberInput.stories-DcTh0IjM.js → RBSideNumberInput.stories-DUymnH-9.js} +1 -1
  84. package/cli/storybook-dist/assets/{RBSlider.stories-B59We7ei.js → RBSlider.stories-BwlDh9--.js} +1 -1
  85. package/cli/storybook-dist/assets/{RBSpinner.stories-9u3_ETbi.js → RBSpinner.stories-VOlkuP6t.js} +1 -1
  86. package/cli/storybook-dist/assets/{RBSwitch.stories-DrVm1WGX.js → RBSwitch.stories-BiW2dQnT.js} +1 -1
  87. package/cli/storybook-dist/assets/{RBTab.stories-D8Dm7cRW.js → RBTab.stories-DFKVsy0C.js} +1 -1
  88. package/cli/storybook-dist/assets/{RBTable.stories-dRdZ8g1j.js → RBTable.stories-BWG0ZSYI.js} +1 -1
  89. package/cli/storybook-dist/assets/{RBTableRow.stories-rZeoGwHZ.js → RBTableRow.stories-JVTD83Mh.js} +1 -1
  90. package/cli/storybook-dist/assets/{RBTabs.stories-STRV2lgO.js → RBTabs.stories-D-aN385u.js} +1 -1
  91. package/cli/storybook-dist/assets/{RBTextArea.stories-CfgUbrbJ.js → RBTextArea.stories-Dx5xkmZS.js} +1 -1
  92. package/cli/storybook-dist/assets/{RBTextField.stories-B5t8-Y97.js → RBTextField.stories-DmVciExd.js} +1 -1
  93. package/cli/storybook-dist/assets/{RBTextInput.stories-Bf1edN6l.js → RBTextInput.stories-CCf0VGEv.js} +1 -1
  94. package/cli/storybook-dist/assets/{RBToast.stories-Cnm8JwQ6.js → RBToast.stories-DT0DGn05.js} +1 -1
  95. package/cli/storybook-dist/assets/{RBToggle.stories-B0OCo6zd.js → RBToggle.stories-Ce_q3EtP.js} +1 -1
  96. package/cli/storybook-dist/assets/{RBToggleGroup.stories-GdYO2Fxg.js → RBToggleGroup.stories-CN40YHsR.js} +1 -1
  97. package/cli/storybook-dist/assets/{RBTooltip.stories-DXJTCScO.js → RBTooltip.stories-L7btDEpA.js} +1 -1
  98. package/cli/storybook-dist/assets/SideNumberInput-B5_xLK8z.js +1109 -0
  99. package/cli/storybook-dist/assets/browserUtils-CQM9Za7e.js +1 -0
  100. package/cli/storybook-dist/assets/{entry-preview-DSRwIaVJ.js → entry-preview-raBQdKjS.js} +1 -1
  101. package/cli/storybook-dist/assets/{iframe-Cj53Wn3d.js → iframe-BmIue__l.js} +2 -2
  102. package/cli/storybook-dist/assets/{index-CkHtLEe8.js → index-CSIe-YDr.js} +3 -3
  103. package/cli/storybook-dist/assets/{index-C1OzuK6-.js → index-ClAc6FBW.js} +1 -1
  104. package/cli/storybook-dist/assets/{preview-CILRoSnB.js → preview-BVJwlGUF.js} +1 -1
  105. package/cli/storybook-dist/assets/{preview-D_2um8Sd.js → preview-CUgthyJT.js} +1 -1
  106. package/cli/storybook-dist/assets/{preview-CgOtgtCD.js → preview-DD51RTKv.js} +2 -2
  107. package/cli/storybook-dist/assets/preview-zuAvMksL.css +1 -0
  108. package/cli/storybook-dist/assets/react-resizable-panels.browser-CYcsrUgg.js +120 -0
  109. package/cli/storybook-dist/iframe.html +1 -1
  110. package/cli/storybook-dist/index.json +1 -1
  111. package/cli/storybook-dist/project.json +1 -1
  112. package/cli/templates/common/README.md +211 -7
  113. package/cli/templates/common/py/main.py +29 -2
  114. package/cli/templates/cpp/cpp/CMakeLists.txt +6 -7
  115. package/cli/templates/cpp/cpp/main.cpp +57 -10
  116. package/cli/templates/readme-cpp/README.md +993 -0
  117. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +4 -0
  118. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +5 -4
  119. package/package.json +3 -3
  120. package/cli/storybook-dist/assets/RBRangeDateInput.stories-KRg2glX8.js +0 -15
  121. package/cli/storybook-dist/assets/RBSelectBox.stories-ILLQQ8M8.js +0 -6
  122. package/cli/storybook-dist/assets/SideNumberInput-CrF-a1Nr.js +0 -1114
  123. package/cli/storybook-dist/assets/browserUtils-CxJ5gea3.js +0 -1
  124. package/cli/storybook-dist/assets/preview-CEfMaO1t.css +0 -1
  125. package/cli/storybook-dist/assets/react-resizable-panels.browser-CtORL3tG.js +0 -102
package/cli/src/ports.mjs CHANGED
@@ -4,7 +4,15 @@ import { usesHostNetwork } from "./zenohDev.mjs";
4
4
 
5
5
  export const PLUGIN_PORT_MIN = 28600;
6
6
  export const PLUGIN_PORT_MAX = 28999;
7
- export const INTERNAL_DEV_PORTS = new Set([10000]);
7
+ export const MEDIA_PORTS = [8554, 8889, 8189]; // MediaMTX: RTSP publish, WebRTC, WebRTC media
8
+ export const INTERNAL_DEV_PORTS = new Set([10000, ...MEDIA_PORTS]);
9
+
10
+ export function mediaPortFlags() {
11
+ return [
12
+ "-p", "127.0.0.1:8554:8554", "-p", "127.0.0.1:8889:8889",
13
+ "-p", "127.0.0.1:8189:8189/udp", "-p", "127.0.0.1:8189:8189/tcp",
14
+ ];
15
+ }
8
16
  export const RESERVED_BACKEND_PORTS = new Map([
9
17
  [502, "Modbus TCP"],
10
18
  [4840, "OPC-UA"],
@@ -118,14 +126,21 @@ export function publishPortFlags(ranges = []) {
118
126
  return ranges.flatMap((port) => ["-p", `127.0.0.1:${port}:${port}`]);
119
127
  }
120
128
 
129
+ // Skips OS-assigned loopback listeners (127.0.0.1:0, e.g. the SDK's Zenoh session).
121
130
  function listeningTcpPorts(container) {
122
131
  const script = `
132
+ read -r eph_lo eph_hi < /proc/sys/net/ipv4/ip_local_port_range 2>/dev/null || { eph_lo=32768; eph_hi=60999; }
123
133
  for f in /proc/net/tcp /proc/net/tcp6; do
124
134
  [ -r "$f" ] || continue
125
135
  tail -n +2 "$f" | while read -r sl local_address rem_address st rest; do
126
136
  [ "$st" = "0A" ] || continue
127
- hex_port="\${local_address##*:}"
128
- printf "%d\\n" "0x$hex_port"
137
+ hex_ip="\${local_address%%:*}"
138
+ port=$((0x\${local_address##*:}))
139
+ case "$hex_ip" in
140
+ 0100007F|00000000000000000000000001000000)
141
+ [ "$port" -ge "$eph_lo" ] && [ "$port" -le "$eph_hi" ] && continue ;;
142
+ esac
143
+ echo "$port"
129
144
  done
130
145
  done | sort -n -u
131
146
  `;
@@ -141,10 +156,9 @@ done | sort -n -u
141
156
  }
142
157
 
143
158
  export function startPortContractMonitor({ container, ranges, log, intervalMs = 1000, allow = [] }) {
144
- // Under --network host the container shares the host's netns, so
145
- // /proc/net/tcp shows every host/robot service port, not just the plugin's —
146
- // the contract can't be enforced. Skip it (no isolated ports to police).
147
- if (usesHostNetwork()) return () => false;
159
+ // Under --network host (or a local Muscat container's netns) /proc/net/tcp shows every
160
+ // host/robot service port, not just the plugin's — the contract can't be enforced.
161
+ if (usesHostNetwork() || process.env.MUSCAT_PK_MUSCAT_CONTAINER) return () => false;
148
162
  const declared = ranges ?? [];
149
163
  // muscat-pk's own dev plumbing shares this netns — not the plugin's doing.
150
164
  const ignored = new Set([...INTERNAL_DEV_PORTS, ...allow.map(Number).filter(Number.isInteger)]);
@@ -1,9 +1,9 @@
1
1
  // Shared Python Docker helpers (Ubuntu 24.04 + Python 3.12 — matches the robot).
2
2
  // Used by dev:back (hot-reload) and build (lock + wheel vendor).
3
3
  import { spawnSync } from "node:child_process";
4
- import { existsSync } from "node:fs";
4
+ import { existsSync, readFileSync } from "node:fs";
5
5
  import { dirname, resolve } from "node:path";
6
- import { canSkipImageBuild, dockerUserFlags, hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
6
+ import { canSkipImageBuild, imageKey, imageKeyLabel, dockerUserFlags, hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
7
7
 
8
8
  export const PYTHON_DEV_IMAGE = "muscat-pk-python-dev";
9
9
 
@@ -15,16 +15,17 @@ export function assertDockerReady() {
15
15
 
16
16
  /** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
17
17
  export async function ensureImage(image = PYTHON_DEV_IMAGE, { platform = null } = {}) {
18
- if (canSkipImageBuild(image)) {
18
+ const dockerfile = await resolveDockerfile("Dockerfile.python");
19
+ const key = imageKey(dockerfile);
20
+ if (canSkipImageBuild(image, key)) {
19
21
  log.step(`${image} already built — skipping docker build`);
20
22
  return;
21
23
  }
22
- const dockerfile = await resolveDockerfile("Dockerfile.python");
23
24
  log.step(`docker build -t ${image}${platform ? ` (${platform})` : ""} (first run is slow, then cached)…`);
24
25
  const platformFlags = platform ? ["--platform", platform] : [];
25
26
  const r = spawnSync(
26
27
  "docker",
27
- ["build", ...platformFlags, "-t", image, "-f", dockerfile, dirname(dockerfile)],
28
+ ["build", ...platformFlags, ...imageKeyLabel(key), "-t", image, "-f", dockerfile, dirname(dockerfile)],
28
29
  { stdio: "inherit" },
29
30
  );
30
31
  if (r.status !== 0) throw new Error("docker build failed.");
@@ -65,7 +66,11 @@ export async function lockAndVendor(pluginRoot, archs = ["amd64", "arm64"]) {
65
66
 
66
67
  await ensureImage();
67
68
 
68
- log.step("generating lock (uv pip compile)…");
69
+ // A pinned pre-release (e.g. rainbow-rb-sdk==0.1.4.dev1) pins its sibling rainbow-rb-*
70
+ // pre-releases too; uv rejects those transitive pre-releases unless allowed.
71
+ const reqText = readFileSync(resolve(pluginRoot, "requirements.txt"), "utf8");
72
+ const prerelease = /==\s*\S*\d(a|b|rc|\.dev)\d*\s*$/im.test(reqText) ? " --prerelease=allow" : "";
73
+ log.step(`generating lock (uv pip compile${prerelease})…`);
69
74
  const lockResult = spawnSync(
70
75
  "docker",
71
76
  [
@@ -74,17 +79,16 @@ export async function lockAndVendor(pluginRoot, archs = ["amd64", "arm64"]) {
74
79
  "-v", `${pluginRoot}:/plugin`,
75
80
  PYTHON_DEV_IMAGE,
76
81
  "bash", "-c",
77
- "set -e && cd /plugin && mkdir -p dist && uv pip compile requirements.txt --python-version 3.12 -o dist/requirements.lock",
82
+ "set -e && cd /plugin && mkdir -p dist && uv pip compile requirements.txt --python-version 3.12 -o dist/requirements.lock" + prerelease,
78
83
  ],
79
84
  { stdio: "inherit" },
80
85
  );
81
86
  if (lockResult.status !== 0) throw new Error("lock generation failed (uv pip compile)");
82
87
  log.ok("dist/requirements.lock ready");
83
88
 
84
- const failed = [];
85
89
  for (const arch of archs) {
86
90
  const pipPlatforms = PIP_PLATFORM[arch];
87
- if (!pipPlatforms) { log.warn(`unknown arch '${arch}' — skipped`); continue; }
91
+ if (!pipPlatforms) throw new Error(`unknown arch '${arch}'`);
88
92
  log.step(`vendoring wheels for ${arch}…`);
89
93
  const r = spawnSync(
90
94
  "docker",
@@ -102,16 +106,8 @@ export async function lockAndVendor(pluginRoot, archs = ["amd64", "arm64"]) {
102
106
  ],
103
107
  { stdio: "inherit" },
104
108
  );
105
- if (r.status !== 0) {
106
- log.warn(`wheel vendoring failed for ${arch}`);
107
- failed.push(arch);
108
- } else {
109
- log.ok(`dist/wheels/ ready (${arch})`);
110
- }
111
- }
112
-
113
- if (failed.length === archs.length) {
114
- throw new Error(`wheel vendoring failed for all archs: ${failed.join(", ")}`);
109
+ // A ZIP missing wheels fails to import on the robot — stop instead.
110
+ if (r.status !== 0) throw new Error(`wheel download failed for ${arch} (see pip output above)`);
111
+ log.ok(`dist/wheels/ ready (${arch})`);
115
112
  }
116
- return failed;
117
113
  }
package/cli/src/ros2.mjs CHANGED
@@ -3,7 +3,7 @@
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 { canSkipImageBuild, hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
6
+ import { canSkipImageBuild, imageKey, imageKeyLabel, dockerUserFlags, hasBin, log, pruneMuskatImages, resolveDockerfile } from "./util.mjs";
7
7
 
8
8
  export const ROS2_DEV_IMAGE = "muscat-pk-ros2-dev";
9
9
 
@@ -30,16 +30,19 @@ export const ARCH_DOCKER_PLATFORM = {
30
30
 
31
31
  /** Build the dev image from the kit's bundled Dockerfile (Docker layer-caches). */
32
32
  export async function ensureImage(image = ROS2_DEV_IMAGE, { platform = null } = {}) {
33
- if (canSkipImageBuild(image)) {
33
+ const dockerfile = await resolveDockerfile("Dockerfile.dev");
34
+ const sdkVer = process.env.MUSCAT_PK_RB_SDK_VERSION;
35
+ const key = imageKey(dockerfile, sdkVer ?? "");
36
+ if (canSkipImageBuild(image, key)) {
34
37
  log.step(`${image} already built — skipping docker build`);
35
38
  return;
36
39
  }
37
- const dockerfile = await resolveDockerfile("Dockerfile.dev");
38
40
  log.step(`docker build -t ${image}${platform ? ` (${platform})` : ""} (first run is slow, then cached)…`);
39
41
  const platformFlags = platform ? ["--platform", platform] : [];
42
+ const buildArgs = [...(sdkVer ? ["--build-arg", `RB_SDK_VERSION=${sdkVer}`] : []), ...imageKeyLabel(key)];
40
43
  const r = spawnSync(
41
44
  "docker",
42
- ["build", ...platformFlags, "-t", image, "-f", dockerfile, dirname(dockerfile)],
45
+ ["build", ...platformFlags, ...buildArgs, "-t", image, "-f", dockerfile, dirname(dockerfile)],
43
46
  { stdio: "inherit" },
44
47
  );
45
48
  if (r.status !== 0) throw new Error("docker build failed.");
@@ -71,12 +74,16 @@ export function colconBuild(root, { image = ROS2_DEV_IMAGE, symlink = false, arc
71
74
  installBase = "../dist/ros2_ws/install";
72
75
  }
73
76
  const flag = symlink ? "--symlink-install" : "";
77
+ const buildBase = symlink ? "build" : arch ? `build-${arch}` : "build-dist";
74
78
  const script = [
75
79
  "pip install -r requirements.txt --break-system-packages --ignore-installed -q || true",
76
80
  "cd ros2_ws",
77
- // ponytail: rm only on cross-arch; same-arch dev rebuilds keep the cache
78
- ...(arch ? ["rm -rf build"] : []),
79
- `colcon build ${flag} --install-base ${installBase}`.trim(),
81
+ // Export builds use their own build cache: sharing build/ with dev's --symlink-install
82
+ // (or another arch) breaks the next colcon build.
83
+ // rclcpp export: ship librb_sdk.so in <pkg>/lib/ (robot has no SDK) when the node links it.
84
+ `colcon build ${flag} --build-base ${buildBase} --install-base ${installBase}`.replace(" ", " ") +
85
+ (arch ? ` --cmake-args '-DCMAKE_INSTALL_RPATH=$ORIGIN/..'` : ""),
86
+ ...(arch ? [`for d in ${installBase}/*/lib; do if readelf -d $(find "$d" -mindepth 2 -type f -executable) 2>/dev/null | grep -q librb_sdk; then cp -a $(dpkg -L rainbow-rb-sdk | grep -E '/librb_sdk[.]so[.][0-9]') "$d"/; fi; done`] : []),
80
87
  ].join(" && ");
81
88
  const label = arch
82
89
  ? `colcon build (${arch}) → dist/ros2_ws/install-${arch}/…`
@@ -87,7 +94,8 @@ export function colconBuild(root, { image = ROS2_DEV_IMAGE, symlink = false, arc
87
94
  const platformFlags = dockerPlatform ? ["--platform", dockerPlatform] : [];
88
95
  const r = spawnSync(
89
96
  "docker",
90
- ["run", "--rm", ...platformFlags, "-e", "PIP_ROOT_USER_ACTION=ignore", "-v", `${root}:/plugin`, "-w", "/plugin", actualImage, "bash", "-lc", script],
97
+ // python/cpp 와 같이 호스트 uid 로 돌린다 — root 소유 산출물이 남으면 사용자가 지우지 못한다.
98
+ ["run", "--rm", ...platformFlags, ...dockerUserFlags(), "-e", "PIP_ROOT_USER_ACTION=ignore", "-v", `${root}:/plugin`, "-w", "/plugin", actualImage, "bash", "-lc", script],
91
99
  { cwd: root, stdio: "inherit" },
92
100
  );
93
101
  if (r.status !== 0) throw new Error(`colcon build failed${arch ? ` (${arch})` : ""}.`);
package/cli/src/sdk.mjs CHANGED
@@ -6,24 +6,24 @@
6
6
  // the new version immediately; this is best-effort only.
7
7
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
8
8
  import { resolve } from "node:path";
9
- import { spawnSync } from "node:child_process";
10
- import { findPluginRoot, log, venvPython } from "./util.mjs";
9
+ import { ensureIdeVenv, findPluginRoot, log, venvPython } from "./util.mjs";
11
10
 
12
11
  const PKG = "rainbow-rb-sdk";
13
12
 
14
- async function fetchVersions() {
13
+ async function fetchPypi() {
15
14
  try {
16
15
  const res = await fetch(`https://pypi.org/pypi/${PKG}/json`);
17
- if (!res.ok) return [];
18
- const data = await res.json();
19
- return Object.keys(data.releases || {}).sort((a, b) =>
20
- b.localeCompare(a, undefined, { numeric: true }),
21
- );
16
+ return res.ok ? await res.json() : null;
22
17
  } catch {
23
- return [];
18
+ return null;
24
19
  }
25
20
  }
26
21
 
22
+ async function fetchVersions() {
23
+ const data = await fetchPypi();
24
+ return Object.keys(data?.releases || {}).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
25
+ }
26
+
27
27
  export async function runSdk(args = []) {
28
28
  const sub = args[0];
29
29
  const root = findPluginRoot();
@@ -42,23 +42,25 @@ export async function runSdk(args = []) {
42
42
  if (sub === "update") {
43
43
  let version = args[1];
44
44
  if (!version) {
45
- const versions = await fetchVersions();
46
- version = versions[0] || "latest";
45
+ // info.version = latest stable
46
+ version = (await fetchPypi())?.info?.version;
47
+ if (!version) throw new Error("could not fetch the latest version (offline?). Pass one: muscat-pk sdk update <version>");
47
48
  }
49
+ // rainbow-rb-* pin each other exactly — bump them together.
48
50
  let content = existsSync(reqs) ? readFileSync(reqs, "utf8") : "";
49
- const line = `${PKG}==${version}`;
50
- if (new RegExp(`^${PKG}==`, "m").test(content)) {
51
- content = content.replace(new RegExp(`^${PKG}==.*$`, "m"), line);
52
- } else {
53
- content = (content ? content.trimEnd() + "\n" : "") + line + "\n";
51
+ const pinned = /^(rainbow-rb-[\w-]+)\s*==.*$/gim;
52
+ const bumped = [...content.matchAll(pinned)].map((m) => m[1]);
53
+ content = content.replace(pinned, (_m, name) => `${name}==${version}`);
54
+ if (!bumped.some((n) => n.toLowerCase() === PKG)) {
55
+ content = (content ? content.trimEnd() + "\n" : "") + `${PKG}==${version}\n`;
56
+ bumped.push(PKG);
54
57
  }
55
58
  writeFileSync(reqs, content);
56
- log.ok(`requirements.txt → ${line}`);
59
+ log.ok(`requirements.txt → ${bumped.map((n) => `${n}==${version}`).join(", ")}`);
57
60
 
58
- const py = venvPython(resolve(root, ".venv"));
59
- if (existsSync(py)) {
60
- log.step("pip install -r requirements.txt (.venv, for your editor)…");
61
- spawnSync(py, ["-m", "pip", "install", "-q", "-r", reqs], { cwd: root, stdio: "inherit" });
61
+ if (existsSync(venvPython(resolve(root, ".venv")))) {
62
+ log.step("refreshing .venv (for your editor)…");
63
+ if (!ensureIdeVenv(root)) log.warn(".venv refresh failed — the Docker dev container is unaffected.");
62
64
  } else {
63
65
  log.info("Installed automatically on the next `muscat-pk dev` / `dev:back` (Docker container).");
64
66
  }
package/cli/src/type.mjs CHANGED
@@ -2,12 +2,14 @@
2
2
  // python | cpp | ros2 (rclpy) | ros2-cpp (rclcpp).
3
3
  // Adds the matching backend overlay; never deletes existing user code
4
4
  // (e.g. ros2_ws/ or cpp/ are left in place when switching away).
5
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
- import { basename, resolve } from "node:path";
7
- import { findPluginRoot, log, prompt } from "./util.mjs";
5
+ import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { basename, join, resolve } from "node:path";
8
+ import { findPluginRoot, log, prompt, templatesDir } from "./util.mjs";
8
9
  import {
9
10
  TYPE_CHOICES,
10
11
  applyTokens,
12
+ copyTemplate,
11
13
  deriveNames,
12
14
  overlayCpp,
13
15
  overlayRos2,
@@ -28,6 +30,28 @@ function currentTypeKey(runtime) {
28
30
  return runtime?.type ?? "python";
29
31
  }
30
32
 
33
+ const isCpp = (key) => key === "cpp" || key === "ros2-cpp";
34
+
35
+ async function offerReadme(root, toCpp, force, tokens) {
36
+ const lang = toCpp ? "C++" : "Python";
37
+ const answer = force ? "y" : await prompt(`Replace README.md with the ${lang} backend version? (current file kept as README.md.bak) [y/N]: `, "n");
38
+ if (!/^y/i.test(answer)) {
39
+ log.info(`README.md kept — to swap it later: muscat-pk type change --to <type> --readme`);
40
+ return;
41
+ }
42
+ const tmp = mkdtempSync(join(tmpdir(), "muscat-pk-readme-"));
43
+ try {
44
+ await copyTemplate(resolve(templatesDir, toCpp ? "readme-cpp" : "common"), tmp);
45
+ applyTokens(tmp, tokens);
46
+ const readme = resolve(root, "README.md");
47
+ if (existsSync(readme)) copyFileSync(readme, resolve(root, "README.md.bak"));
48
+ copyFileSync(join(tmp, "README.md"), readme);
49
+ log.ok(`README.md → ${lang} version${existsSync(resolve(root, "README.md.bak")) ? " (previous file: README.md.bak)" : ""}`);
50
+ } finally {
51
+ rmSync(tmp, { recursive: true, force: true });
52
+ }
53
+ }
54
+
31
55
  export async function runType(args) {
32
56
  // accept `type change`, `type`, `type --to ros2-cpp`
33
57
  const sub = args[0];
@@ -39,17 +63,17 @@ export async function runType(args) {
39
63
  const current = currentTypeKey(json.runtime);
40
64
 
41
65
  if (sub && sub !== "change") {
42
- log.warn(`unknown subcommand '${sub}'. Usage: muscat-pk type change [--to python|cpp|ros2|ros2-cpp]`);
66
+ log.warn(`unknown subcommand '${sub}'. Usage: muscat-pk type change [--to python|cpp|ros2|ros2-cpp] [--readme]`);
43
67
  }
44
68
 
45
69
  if (!to) {
46
- console.log(`\n현재 타입: ${current}`);
47
- console.log("변경할 타입을 선택하세요:");
70
+ console.log(`\nCurrent type: ${current}`);
71
+ console.log("Select the new type:");
48
72
  console.log(" 1) Python");
49
73
  console.log(" 2) C++");
50
74
  console.log(" 3) ROS2 (rclpy / Python)");
51
75
  console.log(" 4) ROS2 (rclcpp / C++)");
52
- const choice = await prompt("\n선택 [1/2/3/4]: ", "");
76
+ const choice = await prompt("\nChoice [1/2/3/4]: ", "");
53
77
  to = { "1": "python", "2": "cpp", "3": "ros2", "4": "ros2-cpp" }[choice];
54
78
  }
55
79
 
@@ -62,7 +86,7 @@ export async function runType(args) {
62
86
  }
63
87
 
64
88
  const runtimeChoice = TYPE_CHOICES[to];
65
- const { slug, pluginName, pluginClass, pluginId } = deriveNames(root);
89
+ const { dir, slug, pluginName, pluginClass, pluginId } = deriveNames(root);
66
90
 
67
91
  if (runtimeChoice.type === "ros2") {
68
92
  if (existsSync(resolve(root, "ros2_ws"))) {
@@ -70,7 +94,7 @@ export async function runType(args) {
70
94
  } else {
71
95
  log.step(`adding ros2_ws/ overlay (${runtimeChoice.rosClient})`);
72
96
  await overlayRos2(root, slug, runtimeChoice.rosClient);
73
- applyTokens(root, {
97
+ applyTokens(resolve(root, "ros2_ws"), {
74
98
  __PLUGIN_ID__: pluginId,
75
99
  __PLUGIN_NAME__: pluginName,
76
100
  __PLUGIN_SLUG__: slug,
@@ -84,7 +108,7 @@ export async function runType(args) {
84
108
  } else {
85
109
  log.step("adding cpp/ overlay");
86
110
  await overlayCpp(root);
87
- applyTokens(root, {
111
+ applyTokens(resolve(root, "cpp"), {
88
112
  __PLUGIN_ID__: pluginId,
89
113
  __PLUGIN_NAME__: pluginName,
90
114
  __PLUGIN_SLUG__: slug,
@@ -102,4 +126,14 @@ export async function runType(args) {
102
126
  json.runtime = runtimeForChoice(runtimeChoice, slug);
103
127
  writeFileSync(path, JSON.stringify(json, null, 2) + "\n");
104
128
  log.ok(`${basename(root)} type: ${current} → ${to}`);
129
+
130
+ if (isCpp(current) !== isCpp(to)) {
131
+ await offerReadme(root, isCpp(to), args.includes("--readme"), {
132
+ __PLUGIN_ID__: pluginId,
133
+ __PLUGIN_NAME__: pluginName,
134
+ __PLUGIN_DIR__: dir,
135
+ __PLUGIN_SLUG__: slug,
136
+ __PLUGIN_CLASS__: pluginClass,
137
+ });
138
+ }
105
139
  }
@@ -1,28 +1,11 @@
1
1
  // muscat-pk uninstall - remove the installed CLI, whether npm-global or S3 binary.
2
2
  import { execSync, spawn, spawnSync } from "node:child_process";
3
- import { existsSync, realpathSync, rmSync } from "node:fs";
4
- import { dirname, join, relative } from "node:path";
5
- import { KIT_PUBLISHED_NAME, kitRoot, log, parseArgs } from "./util.mjs";
3
+ import { existsSync, rmSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { KIT_PUBLISHED_NAME, isNpmGlobalInstall, log, parseArgs } from "./util.mjs";
6
6
 
7
7
  const isBinaryMode = typeof Bun !== "undefined";
8
8
 
9
- function isInside(child, parent) {
10
- const rel = relative(parent, child);
11
- return rel === "" || (!!rel && !rel.startsWith("..") && !rel.includes(":"));
12
- }
13
-
14
- function npmGlobalRoot() {
15
- return execSync("npm root -g", { encoding: "utf8" }).trim();
16
- }
17
-
18
- function isNpmGlobalInstall() {
19
- try {
20
- return isInside(realpathSync(kitRoot), realpathSync(npmGlobalRoot()));
21
- } catch {
22
- return false;
23
- }
24
- }
25
-
26
9
  function uninstallNpmGlobal() {
27
10
  log.step(`npm uninstall -g ${KIT_PUBLISHED_NAME}`);
28
11
  execSync(`npm uninstall -g ${KIT_PUBLISHED_NAME}`, { stdio: "inherit" });
@@ -10,9 +10,8 @@
10
10
  //
11
11
  // After updating the CLI itself, also bumps the plugin project's local deps
12
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";
13
+ import { execSync, spawn, spawnSync } from "node:child_process";
14
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
16
15
  import { join, resolve } from "node:path";
17
16
  import { findPluginRoot, isLocalKitInstall, KIT_PUBLISHED_NAME, kitVersion, log, parseArgs, prompt } from "./util.mjs";
18
17
 
@@ -91,11 +90,18 @@ async function updateBinary(tag) {
91
90
 
92
91
  log.ok(`v${version} will be applied on next run.`);
93
92
  } 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);
93
+ // macOS / Linux: 실행 중인 파일에 덮어쓰면 Linux 는 ETXTBSY. 같은 디렉토리에 쓰고 rename 으로 바꿔치기한다.
94
+ const newPath = execPath + ".new";
95
+ writeFileSync(newPath, data);
96
+ chmodSync(newPath, 0o755);
97
+ try {
98
+ renameSync(newPath, execPath);
99
+ } catch (err) {
100
+ if (err?.code !== "EACCES" && err?.code !== "EPERM") throw err;
101
+ log.info(" sudo is required...");
102
+ const r = spawnSync("sudo", ["mv", "-f", newPath, execPath], { stdio: "inherit" });
103
+ if (r.status !== 0) throw new Error(`sudo mv failed: ${execPath}`);
104
+ }
99
105
  log.ok(`Updated to v${version}.`);
100
106
  }
101
107
  }
package/cli/src/util.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  // Shared helpers for muscat-pk.
2
- import { spawnSync } from "node:child_process";
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
- import { dirname, resolve, sep } from "node:path";
2
+ import { execSync, spawnSync } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
5
+ import { dirname, relative, resolve, sep } from "node:path";
5
6
  import { tmpdir } from "node:os";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { createInterface } from "node:readline/promises";
@@ -59,10 +60,43 @@ export function ensureIdeVenv(root) {
59
60
  if (created.status !== 0 || !existsSync(py)) return false;
60
61
  }
61
62
 
63
+ const pip = (pkgs) => spawnSync(py, ["-m", "pip", "install", "-q", ...pkgs], { cwd: root, stdio: "ignore" }).status === 0;
64
+
65
+ // host_agent.py only; not in requirements.txt, so it never ships in the ZIP.
66
+ if (!pip([HOST_AGENT_ZENOH])) return false;
67
+
62
68
  const reqs = resolve(root, "requirements.txt");
63
- const args = ["-m", "pip", "install", "-q"];
64
- if (existsSync(reqs)) args.push("-r", reqs);
65
- return spawnSync(py, args, { cwd: root, stdio: "ignore" }).status === 0;
69
+ if (!existsSync(reqs)) return true;
70
+ if (process.platform !== "darwin") return pip(["-r", reqs]);
71
+
72
+ // No macOS wheels for rainbow-*: install the rest + type stubs.
73
+ const lines = readFileSync(reqs, "utf8").split("\n");
74
+ const isRainbow = (l) => /^\s*rainbow-/i.test(l);
75
+ const sdkPin = lines.map((l) => l.match(/^\s*rainbow-rb-sdk\s*==\s*(\S+)/i)?.[1]).find(Boolean);
76
+ const rest = lines.filter((l) => l.trim() && !l.trim().startsWith("#") && !isRainbow(l)).map((l) => l.trim());
77
+ if (rest.length && !pip(rest)) return false;
78
+ if (!pip([`rainbow-rb-sdk-stubs${sdkPin ? `==${sdkPin}` : ""}`])) {
79
+ log.warn(`rainbow-rb-sdk-stubs${sdkPin ? `==${sdkPin}` : ""} not on PyPI yet — editor types for rainbow.* unavailable.`);
80
+ }
81
+ ensurePyrightMissingSourceOff(root);
82
+ return true;
83
+ }
84
+
85
+ export const HOST_AGENT_ZENOH = "eclipse-zenoh==1.8.0"; // match the robot's zenoh-c minor version
86
+
87
+ function ensurePyrightMissingSourceOff(root) {
88
+ const path = resolve(root, "pyrightconfig.json");
89
+ let cfg = {};
90
+ if (existsSync(path)) {
91
+ try {
92
+ cfg = JSON.parse(readFileSync(path, "utf8"));
93
+ } catch {
94
+ return; // user-edited / JSONC — leave it alone
95
+ }
96
+ }
97
+ if (cfg.reportMissingModuleSource === "none") return;
98
+ cfg.reportMissingModuleSource = "none";
99
+ writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
66
100
  }
67
101
 
68
102
  /** Published install/CLI package name. Author imports use @rainbow-robotics/plugin-ui separately. */
@@ -153,9 +187,21 @@ export function resolveLocalCliBin(start = process.cwd()) {
153
187
  }
154
188
  }
155
189
 
190
+ /** True if the code currently running was installed with `npm install -g`. */
191
+ export function isNpmGlobalInstall() {
192
+ try {
193
+ const globalRoot = realpathSync(execSync("npm root -g", { encoding: "utf8" }).trim());
194
+ const rel = relative(globalRoot, realpathSync(kitRoot));
195
+ return rel === "" || (!rel.startsWith("..") && !rel.includes(":"));
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+
156
201
  /** True if the code currently running is a project's local install (not global/binary). */
157
202
  export function isLocalKitInstall() {
158
- return kitRoot.includes(`${sep}node_modules${sep}`);
203
+ // npm -g 도 node_modules 아래에 놓이므로 global root 는 제외한다.
204
+ return kitRoot.includes(`${sep}node_modules${sep}`) && !isNpmGlobalInstall();
159
205
  }
160
206
 
161
207
  const useColor = process.env.NO_COLOR !== "1" && (process.stdout.isTTY || process.env.FORCE_COLOR);
@@ -227,9 +273,23 @@ export function dockerImageExists(image) {
227
273
  * rebuilding an image we already have breaks `dev` offline for no benefit.
228
274
  * Force a rebuild with MUSCAT_PK_REBUILD_IMAGE=1.
229
275
  */
230
- export function canSkipImageBuild(image) {
276
+ // Rebuild when the Dockerfile or build args change (image label holds the hash).
277
+ const IMAGE_KEY_LABEL = "com.rainbow-robotics.muscat-pk.key";
278
+
279
+ export function imageKey(dockerfile, extra = "") {
280
+ return createHash("sha256").update(readFileSync(dockerfile)).update(extra).digest("hex").slice(0, 16);
281
+ }
282
+
283
+ export function imageKeyLabel(key) {
284
+ return ["--label", `${IMAGE_KEY_LABEL}=${key}`];
285
+ }
286
+
287
+ export function canSkipImageBuild(image, key = null) {
231
288
  if (process.env.MUSCAT_PK_REBUILD_IMAGE === "1") return false;
232
- return dockerImageExists(image);
289
+ if (!dockerImageExists(image)) return false;
290
+ if (!key) return true;
291
+ const r = spawnSync("docker", ["image", "inspect", "-f", `{{index .Config.Labels "${IMAGE_KEY_LABEL}"}}`, image], { encoding: "utf8" });
292
+ return r.status === 0 && r.stdout.trim() === key;
233
293
  }
234
294
 
235
295
  // Linux에서 컨테이너가 마운트에 root 소유 파일을 남기면 다음 빌드의 dist/ 삭제가 EACCES로 막힌다.
@@ -269,6 +329,7 @@ export async function resolveDockerfile(name) {
269
329
 
270
330
  /** This kit's installed version (from its package.json). */
271
331
  export function kitVersion() {
332
+ if (process.env.MUSCAT_PK_VERSION) return process.env.MUSCAT_PK_VERSION;
272
333
  try {
273
334
  return JSON.parse(readFileSync(resolve(kitRoot, "package.json"), "utf8"))
274
335
  .version;
@@ -16,13 +16,25 @@ export default {
16
16
  },
17
17
  showPresets: { control: "boolean" },
18
18
  showFooter: { control: "boolean" },
19
+ min: { control: "date" },
20
+ max: { control: "date" },
21
+ maxDays: {
22
+ control: { type: "number", min: 0, step: 1 },
23
+ description: "선택 가능한 최대 일수. 0은 제한 없음.",
24
+ },
19
25
  },
26
+ args: { maxDays: 0 },
20
27
  } as Meta<typeof RBRangeDateInput>;
21
28
 
22
29
  const Template: StoryTemplate = (args) => {
23
30
  return (
24
31
  <div style={{ height: 700 }}>
25
- <RBRangeDateInput {...args} />
32
+ <RBRangeDateInput
33
+ {...args}
34
+ min={args.min != null ? new Date(args.min) : undefined}
35
+ max={args.max != null ? new Date(args.max) : undefined}
36
+ maxDays={(args.maxDays ?? 0) > 0 ? args.maxDays : undefined}
37
+ />
26
38
  </div>
27
39
  );
28
40
  };