@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.
- package/cli/README.md +8 -3
- package/cli/bin/muscat-pk.mjs +5 -0
- package/cli/bundle-templates.mjs +6 -1
- package/cli/docker/Dockerfile.cpp +28 -6
- package/cli/docker/Dockerfile.dev +32 -0
- package/cli/docker/Dockerfile.python +19 -0
- package/cli/src/bridge.mjs +45 -12
- package/cli/src/build.mjs +48 -79
- package/cli/src/cpp.mjs +11 -5
- package/cli/src/dev.mjs +7 -2
- package/cli/src/devBack.mjs +97 -40
- package/cli/src/host_agent.py +143 -83
- package/cli/src/manifest.mjs +11 -0
- package/cli/src/mcp.mjs +398 -54
- package/cli/src/new.mjs +13 -3
- package/cli/src/ports.mjs +21 -7
- package/cli/src/python.mjs +16 -20
- package/cli/src/ros2.mjs +16 -8
- package/cli/src/sdk.mjs +23 -21
- package/cli/src/type.mjs +44 -10
- package/cli/src/uninstall.mjs +3 -20
- package/cli/src/update.mjs +14 -8
- package/cli/src/util.mjs +70 -9
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +13 -1
- package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +98 -1
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-Du8dJ-nM.js → Color-YHDXOIA2-CdTgUqrV.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-Sy5RDbtH.js → Docs-C8wx5zsC.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-guGflRzZ.js → Docs-CbRKwHpQ.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-Cb72AJnj.js → Docs-Cf0s04QW.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-pwGqGTV_.js → Docs-CsxMprX5.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-LdjYwuG4.js → Docs-FyPFrbUS.js} +1 -1
- package/cli/storybook-dist/assets/{Docs--Fm8069p.js → Docs-YtqU-aVu.js} +1 -1
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-YGBLQ4BD.js → DocsRenderer-CFRXHY34-VSENNRoY.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-C1d4n8cq.js → NxAvatar.stories-oCcYtLwq.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-gCBoZdMq.js → NxButton.stories-C5jUScIj.js} +1 -1
- package/cli/storybook-dist/assets/{NxIcons.stories-Dg42riT5.js → NxIcons.stories-DeqrVzVs.js} +1 -1
- package/cli/storybook-dist/assets/{NxNumberInput.stories-DB-vLM2J.js → NxNumberInput.stories-gjsEulIN.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-CS8iMIak.js → NxPagination.stories-BK6uSWIa.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-D66BopeS.js → NxPointerHoldButton.stories-BDD5CvSZ.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-CZxddsJ2.js → NxRangeInput.stories-C_nW26_x.js} +3 -3
- package/cli/storybook-dist/assets/{NxSelectBox.stories-CmezGGrm.js → NxSelectBox.stories-DqLfKU9H.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-CGAFMITi.js → NxSlideRange.stories-CQjHA8yv.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-B3QInkXX.js → NxSwitch.stories-BMHCiPoL.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-BgJcgZNe.js → NxTextArea.stories-k7r1WVQh.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-CwMQwI9L.js → NxTextInput.stories-Cqfe1I32.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories-BuBVNzJT.js → RBAccordion.stories-CJ3Ttuja.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-Crdb6kFF.js → RBAccordionGroup.stories-BThv7tM0.js} +1 -1
- package/cli/storybook-dist/assets/{RBAlert.stories-CqXgUvA2.js → RBAlert.stories-CSMP8Vun.js} +1 -1
- package/cli/storybook-dist/assets/{RBBadge.stories-BCw_ksML.js → RBBadge.stories-cBMA2R6F.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-0id4XYoc.js → RBBattery.stories-BKVOskqs.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-CgpG-7Lk.js → RBCard.stories-BTOfK0HC.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckBox.stories-BG02BoGX.js → RBCheckBox.stories-DyE912wG.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-vrjD4GQ4.js → RBCheckIconButton.stories-0sLFU0kM.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-POba8lU-.js → RBCheckLabelButton.stories-c8VecClE.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipTab.stories-XWJbFrHF.js → RBChipTab.stories-CQ9Uav5_.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-BdU0Dzdp.js → RBChipsBoxTab.stories-Ap4NlhAZ.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-C_0dCNXG.js → RBColorIcons.stories-BPPXjif2.js} +1 -1
- package/cli/storybook-dist/assets/{RBCommand.stories-Dvrko5No.js → RBCommand.stories-PDpYOKlm.js} +1 -1
- package/cli/storybook-dist/assets/{RBConfirm.stories-CkTn0sHa.js → RBConfirm.stories-BEWvMAYP.js} +1 -1
- package/cli/storybook-dist/assets/{RBDateInput.stories-BH3D08cs.js → RBDateInput.stories-DLdyNnVF.js} +1 -1
- package/cli/storybook-dist/assets/{RBDialog.stories-C2nNX9fl.js → RBDialog.stories-HsawMNOb.js} +1 -1
- package/cli/storybook-dist/assets/{RBDivider.stories-D-GrJ0QZ.js → RBDivider.stories-dlWwOxik.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-XLOnLOnh.js → RBDrawer.stories-C87iqGk0.js} +1 -1
- package/cli/storybook-dist/assets/{RBDropdownMenu.stories-BjBSeoV2.js → RBDropdownMenu.stories-B8EQhA0z.js} +1 -1
- package/cli/storybook-dist/assets/{RBEmptyView.stories-C6U2zKNE.js → RBEmptyView.stories-SzpIhNDw.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-MCJq5Hta.js → RBIconButton.stories-BQ0nM1gb.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-DxOCiCe0.js → RBIcons.stories-C19g5M5z.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-BVZi1aEs.js → RBInlineLabelButton.stories-DbgI4-1P.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-BDThbEek.js → RBJoyStick.stories-smF-vpGL.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-CaPEKUS6.js → RBLabelButton.stories-s53klwDv.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-CihQzyEL.js → RBLight.stories-BnzkoR51.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-J852XVFh.js → RBMenubar.stories-CW014klu.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-DXyX0cOZ.js → RBMultiRangeInput.stories-qa4tH_4Y.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-C9_49ymO.js → RBPagination.stories-YoQNUpQH.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-DxLE912-.js → RBPopover.stories-Bhpi0OaH.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-DlsfGqWb.js → RBPrompt.stories-iQM5iAka.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-hb5cNWW1.js → RBRadioBox.stories-BGyq4rzx.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-CSuOgeN-.js → RBRadioButton.stories-DF6BcUcJ.js} +1 -1
- package/cli/storybook-dist/assets/RBRangeDateInput.stories-COOQ_HNF.js +15 -0
- package/cli/storybook-dist/assets/{RBResizable.stories-BetjF9vG.js → RBResizable.stories-DSVHvwjp.js} +1 -1
- package/cli/storybook-dist/assets/RBSelectBox.stories-BookV8Sm.js +74 -0
- package/cli/storybook-dist/assets/{RBSheet.stories-D3QbcMKq.js → RBSheet.stories-CAK9hb-p.js} +1 -1
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-DcTh0IjM.js → RBSideNumberInput.stories-DUymnH-9.js} +1 -1
- package/cli/storybook-dist/assets/{RBSlider.stories-B59We7ei.js → RBSlider.stories-BwlDh9--.js} +1 -1
- package/cli/storybook-dist/assets/{RBSpinner.stories-9u3_ETbi.js → RBSpinner.stories-VOlkuP6t.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-DrVm1WGX.js → RBSwitch.stories-BiW2dQnT.js} +1 -1
- package/cli/storybook-dist/assets/{RBTab.stories-D8Dm7cRW.js → RBTab.stories-DFKVsy0C.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-dRdZ8g1j.js → RBTable.stories-BWG0ZSYI.js} +1 -1
- package/cli/storybook-dist/assets/{RBTableRow.stories-rZeoGwHZ.js → RBTableRow.stories-JVTD83Mh.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-STRV2lgO.js → RBTabs.stories-D-aN385u.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextArea.stories-CfgUbrbJ.js → RBTextArea.stories-Dx5xkmZS.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextField.stories-B5t8-Y97.js → RBTextField.stories-DmVciExd.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextInput.stories-Bf1edN6l.js → RBTextInput.stories-CCf0VGEv.js} +1 -1
- package/cli/storybook-dist/assets/{RBToast.stories-Cnm8JwQ6.js → RBToast.stories-DT0DGn05.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-B0OCo6zd.js → RBToggle.stories-Ce_q3EtP.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-GdYO2Fxg.js → RBToggleGroup.stories-CN40YHsR.js} +1 -1
- package/cli/storybook-dist/assets/{RBTooltip.stories-DXJTCScO.js → RBTooltip.stories-L7btDEpA.js} +1 -1
- package/cli/storybook-dist/assets/SideNumberInput-B5_xLK8z.js +1109 -0
- package/cli/storybook-dist/assets/browserUtils-CQM9Za7e.js +1 -0
- package/cli/storybook-dist/assets/{entry-preview-DSRwIaVJ.js → entry-preview-raBQdKjS.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-Cj53Wn3d.js → iframe-BmIue__l.js} +2 -2
- package/cli/storybook-dist/assets/{index-CkHtLEe8.js → index-CSIe-YDr.js} +3 -3
- package/cli/storybook-dist/assets/{index-C1OzuK6-.js → index-ClAc6FBW.js} +1 -1
- package/cli/storybook-dist/assets/{preview-CILRoSnB.js → preview-BVJwlGUF.js} +1 -1
- package/cli/storybook-dist/assets/{preview-D_2um8Sd.js → preview-CUgthyJT.js} +1 -1
- package/cli/storybook-dist/assets/{preview-CgOtgtCD.js → preview-DD51RTKv.js} +2 -2
- package/cli/storybook-dist/assets/preview-zuAvMksL.css +1 -0
- package/cli/storybook-dist/assets/react-resizable-panels.browser-CYcsrUgg.js +120 -0
- package/cli/storybook-dist/iframe.html +1 -1
- package/cli/storybook-dist/index.json +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/README.md +211 -7
- package/cli/templates/common/py/main.py +29 -2
- package/cli/templates/cpp/cpp/CMakeLists.txt +6 -7
- package/cli/templates/cpp/cpp/main.cpp +57 -10
- package/cli/templates/readme-cpp/README.md +993 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +4 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +5 -4
- package/package.json +3 -3
- package/cli/storybook-dist/assets/RBRangeDateInput.stories-KRg2glX8.js +0 -15
- package/cli/storybook-dist/assets/RBSelectBox.stories-ILLQQ8M8.js +0 -6
- package/cli/storybook-dist/assets/SideNumberInput-CrF-a1Nr.js +0 -1114
- package/cli/storybook-dist/assets/browserUtils-CxJ5gea3.js +0 -1
- package/cli/storybook-dist/assets/preview-CEfMaO1t.css +0 -1
- 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
|
|
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
|
-
|
|
128
|
-
|
|
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
|
|
145
|
-
//
|
|
146
|
-
|
|
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)]);
|
package/cli/src/python.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
13
|
+
async function fetchPypi() {
|
|
15
14
|
try {
|
|
16
15
|
const res = await fetch(`https://pypi.org/pypi/${PKG}/json`);
|
|
17
|
-
|
|
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
|
-
|
|
46
|
-
version =
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
content = (content ? content.trimEnd() + "\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 → ${
|
|
59
|
+
log.ok(`requirements.txt → ${bumped.map((n) => `${n}==${version}`).join(", ")}`);
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
log.
|
|
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 {
|
|
7
|
-
import {
|
|
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(`\
|
|
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("\
|
|
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
|
}
|
package/cli/src/uninstall.mjs
CHANGED
|
@@ -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,
|
|
4
|
-
import { dirname, join
|
|
5
|
-
import { KIT_PUBLISHED_NAME,
|
|
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" });
|
package/cli/src/update.mjs
CHANGED
|
@@ -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,
|
|
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:
|
|
95
|
-
const
|
|
96
|
-
writeFileSync(
|
|
97
|
-
chmodSync(
|
|
98
|
-
|
|
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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
};
|