@rainbow-robotics/muscat-plugin-kit 1.10.1-dev.1 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +282 -282
- package/cli/README.md +274 -274
- package/cli/bin/create-muscat-plugin.mjs +8 -8
- package/cli/bin/muscat-pk.mjs +155 -155
- package/cli/bundle-templates.mjs +31 -31
- package/cli/docker/Dockerfile.cpp +28 -28
- package/cli/docker/Dockerfile.dev +30 -30
- package/cli/docker/Dockerfile.python +21 -21
- package/cli/m5-e2e.mjs +69 -69
- package/cli/prepack.mjs +10 -10
- package/cli/src/ai-rules.mjs +70 -70
- package/cli/src/build.mjs +447 -447
- package/cli/src/check-package.mjs +13 -13
- package/cli/src/completion.mjs +166 -166
- package/cli/src/cpp.mjs +78 -78
- package/cli/src/cppIde.mjs +75 -75
- package/cli/src/dev.mjs +165 -165
- package/cli/src/devBack.mjs +734 -727
- package/cli/src/devUi.mjs +247 -243
- package/cli/src/esbuild.mjs +56 -56
- package/cli/src/host_agent.py +223 -223
- package/cli/src/manifest.mjs +287 -287
- package/cli/src/mcp.mjs +1071 -1071
- package/cli/src/muscatLocal.mjs +209 -209
- package/cli/src/new.mjs +343 -343
- package/cli/src/pluginDevServer.mjs +111 -111
- package/cli/src/ports.mjs +193 -193
- package/cli/src/python.mjs +112 -112
- package/cli/src/ros2.mjs +109 -109
- package/cli/src/sdk.mjs +117 -117
- package/cli/src/storybook.mjs +168 -168
- package/cli/src/type.mjs +141 -141
- package/cli/src/uninstall.mjs +81 -81
- package/cli/src/update.mjs +295 -295
- package/cli/src/util.mjs +441 -441
- package/cli/src/zenohDev.mjs +83 -81
- package/cli/storybook-dist/_design-tokens.css +4957 -4957
- package/cli/storybook-dist/_stories-src/NxAvatar.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/NxPagination.stories.tsx +28 -28
- package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordion.stories.tsx +46 -46
- package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordionGroup.stories.tsx +73 -73
- package/cli/storybook-dist/_stories-src/RBBadge/RBBadge.stories.tsx +93 -93
- package/cli/storybook-dist/_stories-src/RBBattery/RBBattery.stories.tsx +74 -74
- package/cli/storybook-dist/_stories-src/RBButton/RBCheckIconButton.stories.tsx +28 -28
- package/cli/storybook-dist/_stories-src/RBButton/RBCheckLabelButton.stories.tsx +84 -84
- package/cli/storybook-dist/_stories-src/RBButton/RBIconButton.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/RBButton/RBInlineLabelButton.stories.tsx +41 -41
- package/cli/storybook-dist/_stories-src/RBButton/RBLabelButton.stories.tsx +60 -60
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBDateInput.stories.tsx +44 -44
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +56 -56
- package/cli/storybook-dist/_stories-src/RBDialog/RBAlert.stories.tsx +51 -51
- package/cli/storybook-dist/_stories-src/RBDialog/RBConfirm.stories.tsx +55 -55
- package/cli/storybook-dist/_stories-src/RBDialog/RBPrompt.stories.tsx +52 -52
- package/cli/storybook-dist/_stories-src/RBDivider/RBDivider.stories.tsx +93 -93
- package/cli/storybook-dist/_stories-src/RBEmptyView/RBEmptyView.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBIcons/RBColorIcons.stories.tsx +108 -108
- package/cli/storybook-dist/_stories-src/RBIcons/RBIcons.stories.tsx +106 -106
- package/cli/storybook-dist/_stories-src/RBInput/RBCheckBox.stories.tsx +89 -89
- package/cli/storybook-dist/_stories-src/RBInput/RBMultiRangeInput.stories.tsx +64 -64
- package/cli/storybook-dist/_stories-src/RBInput/RBRadioBox.stories.tsx +65 -65
- package/cli/storybook-dist/_stories-src/RBInput/RBRadioButton.stories.tsx +68 -68
- package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +216 -216
- package/cli/storybook-dist/_stories-src/RBInput/RBSideNumberInput.stories.tsx +90 -90
- package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextArea.stories.tsx +45 -45
- package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextInput.stories.tsx +50 -50
- package/cli/storybook-dist/_stories-src/RBInput/RBTextField.stories.tsx +102 -102
- package/cli/storybook-dist/_stories-src/RBJoyStick/RBJoyStick.stories.tsx +31 -31
- package/cli/storybook-dist/_stories-src/RBLight/RBLight.stories.tsx +36 -36
- package/cli/storybook-dist/_stories-src/RBPagination/RBPagination.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBShadcn/RBCard/RBCard.stories.tsx +89 -89
- package/cli/storybook-dist/_stories-src/RBShadcn/RBCommand/RBCommand.stories.tsx +116 -116
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDialog/RBDialog.stories.tsx +74 -74
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDrawer/RBDrawer.stories.tsx +99 -99
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDropdownMenu/RBDropdownMenu.stories.tsx +123 -123
- package/cli/storybook-dist/_stories-src/RBShadcn/RBMenubar/RBMenubar.stories.tsx +135 -135
- package/cli/storybook-dist/_stories-src/RBShadcn/RBPopover/RBPopover.stories.tsx +69 -69
- package/cli/storybook-dist/_stories-src/RBShadcn/RBResizable/RBResizable.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSheet/RBSheet.stories.tsx +91 -91
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSlider/RBSlider.stories.tsx +38 -38
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSwitch/RBSwitch.stories.tsx +112 -112
- package/cli/storybook-dist/_stories-src/RBShadcn/RBTabs/RBTabs.stories.tsx +75 -75
- package/cli/storybook-dist/_stories-src/RBShadcn/RBToggle/RBToggle.stories.tsx +51 -51
- package/cli/storybook-dist/_stories-src/RBShadcn/RBToggleGroup/RBToggleGroup.stories.tsx +84 -84
- package/cli/storybook-dist/_stories-src/RBTab/RBChipTab.stories.tsx +102 -102
- package/cli/storybook-dist/_stories-src/RBTab/RBChipsBoxTab.stories.tsx +78 -78
- package/cli/storybook-dist/_stories-src/RBTab/RBTab.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/RBTable/RBTable.stories.tsx +177 -177
- package/cli/storybook-dist/_stories-src/RBTable/RBTableRow.stories.tsx +64 -64
- package/cli/storybook-dist/_stories-src/RBToast/RBToast.stories.tsx +117 -117
- package/cli/storybook-dist/_stories-src/RBTooltip/RBTooltip.stories.tsx +50 -50
- package/cli/storybook-dist/_stories-src/loading/RBSpinner.stories.tsx +30 -30
- package/cli/storybook-dist/_stories-src/nx-button/NxButton.stories.tsx +54 -54
- package/cli/storybook-dist/_stories-src/nx-button/NxPointerHoldButton.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/nx-icons/NxIcons.stories.tsx +48 -48
- package/cli/storybook-dist/_stories-src/nx-inputs/NxNumberInput.stories.tsx +103 -103
- package/cli/storybook-dist/_stories-src/nx-inputs/NxRangeInput.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSelectBox.stories.tsx +67 -67
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSlideRange.stories.tsx +45 -45
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSwitch.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/nx-inputs/NxTextArea.stories.tsx +48 -48
- package/cli/storybook-dist/_stories-src/nx-inputs/NxTextInput.stories.tsx +46 -46
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-D6JqZzDJ.js → Color-YHDXOIA2-B4IER17l.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-ESb7JAHg.js → Docs-C8GCHFtK.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-6gD2kUo1.js → Docs-CgdEMPQP.js} +2 -2
- package/cli/storybook-dist/assets/{Docs-B9wd8Ok7.js → Docs-Ck3WHvk6.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-CgFbWWXM.js → Docs-D9NHsoeZ.js} +2 -2
- package/cli/storybook-dist/assets/{Docs-DBQup8Bu.js → Docs-DNYQC7nl.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-CMjovnSY.js → Docs-DUfeiM4I.js} +4 -4
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-BOUI1AHi.js → DocsRenderer-CFRXHY34-C0WhyDoR.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-DsYFMUh3.js → NxAvatar.stories-CwnK8AUF.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-CHfNcqsc.js → NxButton.stories-ueYdosfQ.js} +8 -8
- package/cli/storybook-dist/assets/{NxNumberInput.stories-BFw_4vFE.js → NxNumberInput.stories-M5ZCLbrK.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-DVfxyw4M.js → NxPagination.stories-bxxUnHEL.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BzyxUWoZ.js → NxPointerHoldButton.stories-BHxB2kr5.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-DmRdzBc1.js → NxRangeInput.stories-BKfr-fp4.js} +6 -6
- package/cli/storybook-dist/assets/{NxSelectBox.stories-B1KkNUOf.js → NxSelectBox.stories-CG8FuTBq.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-CyOdvfQ6.js → NxSlideRange.stories-BRTFtMTL.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-DrDBw0qp.js → NxSwitch.stories-Bwuyzn-R.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-DTB2aqNS.js → NxTextArea.stories-COq2sGqu.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-DU0rucmM.js → NxTextInput.stories-CjsBLz05.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories-DTfOtXtp.js → RBAccordion.stories-BARwuZ0Q.js} +3 -3
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-9y9BjrUe.js → RBAccordionGroup.stories-COrPxETW.js} +10 -10
- package/cli/storybook-dist/assets/{RBAlert.stories-DLTAv9fT.js → RBAlert.stories-BQrkjZPt.js} +3 -3
- package/cli/storybook-dist/assets/{RBBadge.stories-BNMACewA.js → RBBadge.stories-BTsLL-lK.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-B3kXMAt5.js → RBBattery.stories-DAeQdJbc.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-Y-s8R2n7.js → RBCard.stories-HyhOE80R.js} +39 -39
- package/cli/storybook-dist/assets/{RBCheckBox.stories-DBuT9tL4.js → RBCheckBox.stories-KFk0WqX_.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-CWu1TL56.js → RBCheckIconButton.stories-CXbbktF5.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-sCu2MGCX.js → RBCheckLabelButton.stories-DWfYz_9Z.js} +17 -17
- package/cli/storybook-dist/assets/{RBChipTab.stories-BX9qLJkR.js → RBChipTab.stories-JPEgVFAS.js} +5 -5
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-35u1aJ-P.js → RBChipsBoxTab.stories-CTPuWXrQ.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-DAOZ5Ies.js → RBColorIcons.stories-DiFRZ5jk.js} +1 -1
- package/cli/storybook-dist/assets/RBCommand.stories-Pf1HAVIR.js +73 -0
- package/cli/storybook-dist/assets/{RBConfirm.stories-iMXcUh7Z.js → RBConfirm.stories-D1cEPUXk.js} +3 -3
- package/cli/storybook-dist/assets/{RBDateInput.stories-hK5rXECf.js → RBDateInput.stories-RPYpCjig.js} +1 -1
- package/cli/storybook-dist/assets/RBDialog.stories-DJH-0XSR.js +38 -0
- package/cli/storybook-dist/assets/{RBDivider.stories-BAK-iZ1Z.js → RBDivider.stories-uEj1vOgK.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-B2nqf2Nh.js → RBDrawer.stories-BiMzElb2.js} +12 -12
- package/cli/storybook-dist/assets/RBDropdownMenu.stories-Da_TBkla.js +86 -0
- package/cli/storybook-dist/assets/{RBEmptyView.stories-nCenrgBh.js → RBEmptyView.stories-BHeKGfPS.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-DRH7O3sp.js → RBIconButton.stories-NEsRFoYp.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-D3PyJXG8.js → RBIcons.stories-CcZ-TTLS.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-modQbt10.js → RBInlineLabelButton.stories-B0wpdQyR.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-BY14qVgR.js → RBJoyStick.stories-DEgBou3K.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-DzF7vGwm.js → RBLabelButton.stories-DT8drIsF.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-lF4FRmVh.js → RBLight.stories-Dh-aoeyG.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-XhrI7bsV.js → RBMenubar.stories-D-H7UqVh.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-DprllQDl.js → RBMultiRangeInput.stories-hlclv6jI.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-2v2gBJfC.js → RBPagination.stories-DWCeoZ_S.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-C8iLIZpv.js → RBPopover.stories-C514M09_.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-Th_HrWHI.js → RBPrompt.stories-BSItvfiT.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-CoHuLaN3.js → RBRadioBox.stories-BBb4YbFU.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-DULxE_ff.js → RBRadioButton.stories-CXyOGJCv.js} +1 -1
- package/cli/storybook-dist/assets/{RBRangeDateInput.stories-D8A9KBSS.js → RBRangeDateInput.stories-qvhubu0e.js} +1 -1
- package/cli/storybook-dist/assets/{RBResizable.stories-C41QfrQ3.js → RBResizable.stories-CZKeyGjH.js} +14 -14
- package/cli/storybook-dist/assets/{RBSelectBox.stories-C1gNmwzw.js → RBSelectBox.stories-CRKP3ybf.js} +1 -1
- package/cli/storybook-dist/assets/RBSheet.stories-sCM3ZJUa.js +39 -0
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-9LlgQ8nh.js → RBSideNumberInput.stories-Bb7gw8RF.js} +3 -3
- package/cli/storybook-dist/assets/{RBSlider.stories-oUgo5K9d.js → RBSlider.stories-23OAQU5z.js} +3 -3
- package/cli/storybook-dist/assets/{RBSpinner.stories-DDpohbY2.js → RBSpinner.stories-2xcQdcmt.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-gW-GN7QO.js → RBSwitch.stories-BACUaJm_.js} +7 -7
- package/cli/storybook-dist/assets/{RBTab.stories-CN-PV3HH.js → RBTab.stories-CDHE3sVs.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-BAZLpxtM.js → RBTable.stories-C0Cfhfl7.js} +29 -29
- package/cli/storybook-dist/assets/{RBTableRow.stories-CwjVrszB.js → RBTableRow.stories-BF8kIkpp.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-JuEo2sUL.js → RBTabs.stories-BdJWRq1o.js} +7 -7
- package/cli/storybook-dist/assets/{RBTextArea.stories-D0_Pv4qZ.js → RBTextArea.stories-CGy2xeYH.js} +3 -3
- package/cli/storybook-dist/assets/{RBTextField.stories-CCsOvZqV.js → RBTextField.stories-IfUOzxCM.js} +15 -15
- package/cli/storybook-dist/assets/{RBTextInput.stories-Cccn3VNm.js → RBTextInput.stories-VjXNFo4R.js} +3 -3
- package/cli/storybook-dist/assets/{RBToast.stories-I5617t0K.js → RBToast.stories-BzcobblV.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-DsgKjGsJ.js → RBToggle.stories-DuLE5mKv.js} +4 -4
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-BZEzSDuz.js → RBToggleGroup.stories-CuR0N0GJ.js} +5 -5
- package/cli/storybook-dist/assets/{RBTooltip.stories-4oSyAWsA.js → RBTooltip.stories--aAgNC8_.js} +1 -1
- package/cli/storybook-dist/assets/{SideNumberInput-DSKbd0bG.js → SideNumberInput-DyWQxYUj.js} +743 -743
- package/cli/storybook-dist/assets/{entry-preview-B35XREGs.js → entry-preview-D7IHsza_.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-BB797lmL.js → iframe-CbaIp5I-.js} +2 -2
- package/cli/storybook-dist/assets/{index-BU2567EP.js → index-C6CBgMuG.js} +3 -3
- package/cli/storybook-dist/assets/{preview-C7DaSWyc.js → preview-B-iYLDuW.js} +2 -2
- package/cli/storybook-dist/assets/{preview-CbuRHit4.js → preview-CRWqc9DH.js} +1 -1
- package/cli/storybook-dist/assets/{react-resizable-panels.browser-ChIvlwo7.js → react-resizable-panels.browser-8hC8dr-6.js} +8 -8
- package/cli/storybook-dist/iframe.html +2 -2
- package/cli/storybook-dist/index.html +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/.mcp.json +8 -8
- package/cli/templates/common/README.md +1231 -1231
- package/cli/templates/common/assets/icon.svg +45 -45
- package/cli/templates/common/manifest.json +26 -26
- package/cli/templates/common/package.json +22 -22
- package/cli/templates/common/py/main.py +125 -125
- package/cli/templates/common/tsconfig.json +13 -13
- package/cli/templates/common/ui/content.tsx +216 -216
- package/cli/templates/common/ui/style.css +6 -6
- package/cli/templates/common/vite.config.ts +10 -10
- package/cli/templates/cpp/cpp/main.cpp +73 -73
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +25 -25
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/package.xml +10 -10
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/setup.py +18 -18
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -11
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +23 -23
- package/cli/vite.d.mts +25 -25
- package/cli/vite.mjs +292 -292
- package/package.json +3 -3
- package/cli/storybook-dist/assets/RBCommand.stories-BSqXDkKw.js +0 -73
- package/cli/storybook-dist/assets/RBDialog.stories-BAktz-ku.js +0 -38
- package/cli/storybook-dist/assets/RBDropdownMenu.stories-DbW2fWAj.js +0 -86
- package/cli/storybook-dist/assets/RBSheet.stories-_6VH-RUm.js +0 -39
package/cli/src/util.mjs
CHANGED
|
@@ -1,441 +1,441 @@
|
|
|
1
|
-
// Shared helpers for muscat-pk.
|
|
2
|
-
import { execSync, spawnSync } from "node:child_process";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { dirname, relative, resolve, sep } from "node:path";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { createInterface } from "node:readline/promises";
|
|
9
|
-
|
|
10
|
-
/** True if an executable is on PATH (cross-platform). */
|
|
11
|
-
export function hasBin(bin) {
|
|
12
|
-
const probe = process.platform === "win32" ? "where" : "which";
|
|
13
|
-
return spawnSync(probe, [bin]).status === 0;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Find a Python 3.12 interpreter (robot targets 3.12), cross-platform.
|
|
18
|
-
* Returns the command as an argv array, e.g. ["py","-3.12"] or ["python3.12"].
|
|
19
|
-
* Falls back to a generic python3/python with a warning flag.
|
|
20
|
-
*/
|
|
21
|
-
export function detectPythonCmd() {
|
|
22
|
-
const candidates =
|
|
23
|
-
process.platform === "win32"
|
|
24
|
-
? [["py", "-3.12"], ["python3.12"], ["py", "-3"], ["python"]]
|
|
25
|
-
: [["python3.12"], ["python3"], ["python"]];
|
|
26
|
-
for (const cmd of candidates) {
|
|
27
|
-
const r = spawnSync(cmd[0], [...cmd.slice(1), "--version"]);
|
|
28
|
-
if (r.status === 0) {
|
|
29
|
-
const ver = String(r.stdout || r.stderr || "");
|
|
30
|
-
return { cmd, is312: ver.includes("3.12") };
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return { cmd: process.platform === "win32" ? ["python"] : ["python3"], is312: false };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Path to the venv's python interpreter (cross-platform). */
|
|
37
|
-
export function venvPython(venvDir) {
|
|
38
|
-
return process.platform === "win32"
|
|
39
|
-
? resolve(venvDir, "Scripts", "python.exe")
|
|
40
|
-
: resolve(venvDir, "bin", "python");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Create/refresh a local `.venv` with requirements.txt installed, for the
|
|
45
|
-
* editor/linter only (the backend actually runs in Docker). Uses `uv` if
|
|
46
|
-
* present, else the system Python. Returns true if the venv is ready.
|
|
47
|
-
*/
|
|
48
|
-
export function ensureIdeVenv(root) {
|
|
49
|
-
const venvDir = resolve(root, ".venv");
|
|
50
|
-
const py = venvPython(venvDir);
|
|
51
|
-
|
|
52
|
-
if (!existsSync(py)) {
|
|
53
|
-
let created;
|
|
54
|
-
if (hasBin("uv")) {
|
|
55
|
-
created = spawnSync("uv", ["venv", venvDir, "--python", "3.12", "--seed"], { cwd: root, stdio: "ignore" });
|
|
56
|
-
} else {
|
|
57
|
-
const { cmd } = detectPythonCmd();
|
|
58
|
-
created = spawnSync(cmd[0], [...cmd.slice(1), "-m", "venv", venvDir], { cwd: root, stdio: "ignore" });
|
|
59
|
-
}
|
|
60
|
-
if (created.status !== 0 || !existsSync(py)) return false;
|
|
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
|
-
|
|
68
|
-
const reqs = resolve(root, "requirements.txt");
|
|
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
|
-
pip([`rainbow-rb-sdk-stubs${sdkPin ? `==${sdkPin}` : ""}`]); // not on PyPI for every SDK version yet
|
|
79
|
-
ensurePyrightMissingSourceOff(root);
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export const HOST_AGENT_ZENOH = "eclipse-zenoh==1.8.0"; // match the robot's zenoh-c minor version
|
|
84
|
-
|
|
85
|
-
function ensurePyrightMissingSourceOff(root) {
|
|
86
|
-
const path = resolve(root, "pyrightconfig.json");
|
|
87
|
-
let cfg = {};
|
|
88
|
-
if (existsSync(path)) {
|
|
89
|
-
try {
|
|
90
|
-
cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
91
|
-
} catch {
|
|
92
|
-
return; // user-edited / JSONC — leave it alone
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (cfg.reportMissingModuleSource === "none") return;
|
|
96
|
-
cfg.reportMissingModuleSource = "none";
|
|
97
|
-
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Published install/CLI package name. Author imports use @rainbow-robotics/plugin-ui separately. */
|
|
101
|
-
export const KIT_PUBLISHED_NAME = "@rainbow-robotics/muscat-plugin-kit";
|
|
102
|
-
|
|
103
|
-
// This file lives at <pkg>/cli/src/util.mjs → package root is two levels up.
|
|
104
|
-
export const kitRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
105
|
-
export const templatesDir = resolve(kitRoot, "cli/templates");
|
|
106
|
-
|
|
107
|
-
/** Parse `--flag value` and `--bool` from argv into an object. */
|
|
108
|
-
export function parseArgs(args, { booleans = [] } = {}) {
|
|
109
|
-
const out = { _: [] };
|
|
110
|
-
for (let i = 0; i < args.length; i++) {
|
|
111
|
-
const a = args[i];
|
|
112
|
-
if (a.startsWith("--")) {
|
|
113
|
-
const key = a.slice(2);
|
|
114
|
-
if (booleans.includes(key)) {
|
|
115
|
-
out[key] = true;
|
|
116
|
-
} else if (args[i + 1] && !args[i + 1].startsWith("--")) {
|
|
117
|
-
out[key] = args[++i];
|
|
118
|
-
} else {
|
|
119
|
-
out[key] = true;
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
122
|
-
out._.push(a);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return out;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Prompt user for input. Returns trimmed answer or fallback (non-TTY or empty). */
|
|
129
|
-
export async function prompt(question, fallback = "") {
|
|
130
|
-
if (!process.stdin.isTTY) return fallback;
|
|
131
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
132
|
-
const answer = (await rl.question(question)).trim();
|
|
133
|
-
rl.close();
|
|
134
|
-
return answer || fallback;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const LOCAL_HOSTS = new Set(["", "localhost", "127.0.0.1", "::1"]);
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Ask for robot IP (and token if remote). Returns { robotIp, robotToken }.
|
|
141
|
-
* Skips prompts if already set in process.env (from a parent command like `dev`).
|
|
142
|
-
* Supports --robot-ip / --robot-token flags for non-interactive use.
|
|
143
|
-
*/
|
|
144
|
-
export async function promptRobotConnection(args = []) {
|
|
145
|
-
const parsed = parseArgs(args);
|
|
146
|
-
const robotIp =
|
|
147
|
-
parsed["robot-ip"] ??
|
|
148
|
-
process.env.MUSCAT_ROBOT_HOST ??
|
|
149
|
-
(await prompt(" Robot IP (skip if no robot): "));
|
|
150
|
-
|
|
151
|
-
if (LOCAL_HOSTS.has(robotIp)) return { robotIp, robotToken: "" };
|
|
152
|
-
|
|
153
|
-
const robotToken =
|
|
154
|
-
parsed["robot-token"] ??
|
|
155
|
-
process.env.MUSCAT_ROBOT_TOKEN ??
|
|
156
|
-
(await prompt(" SDK Token (required for remote): "));
|
|
157
|
-
|
|
158
|
-
if (!robotToken) throw new Error("SDK token is required for remote robot connection.");
|
|
159
|
-
return { robotIp, robotToken };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** Walk up from cwd to find the plugin project root (has manifest.json). */
|
|
163
|
-
// C++ backends pin the SDK where they declare it: find_package(rb_sdk <version> EXACT REQUIRED) in
|
|
164
|
-
// cpp/CMakeLists.txt or ros2_ws/src/<pkg>/CMakeLists.txt. The dev image installs that version.
|
|
165
|
-
const RB_SDK_FIND = /find_package\(\s*rb_sdk(\s+[0-9][\w.]*)?/;
|
|
166
|
-
|
|
167
|
-
export function cppSdkFiles(root) {
|
|
168
|
-
const files = [resolve(root, "cpp", "CMakeLists.txt")];
|
|
169
|
-
const src = resolve(root, "ros2_ws", "src");
|
|
170
|
-
if (existsSync(src)) for (const pkg of readdirSync(src)) files.push(resolve(src, pkg, "CMakeLists.txt"));
|
|
171
|
-
return files.filter((f) => existsSync(f) && RB_SDK_FIND.test(readFileSync(f, "utf8")));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function cppSdkPin(root) {
|
|
175
|
-
for (const f of cppSdkFiles(root)) {
|
|
176
|
-
const version = readFileSync(f, "utf8").match(RB_SDK_FIND)[1]?.trim();
|
|
177
|
-
if (version) return version;
|
|
178
|
-
}
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** SDK version for the C++ dev image: MUSCAT_PK_RB_SDK_VERSION overrides the project's CMake pin. */
|
|
183
|
-
export function cppSdkVersion(root = pluginRootOrNull()) {
|
|
184
|
-
if (process.env.MUSCAT_PK_RB_SDK_VERSION) return process.env.MUSCAT_PK_RB_SDK_VERSION;
|
|
185
|
-
return root ? cppSdkPin(root) : undefined; // not in a plugin project: the Dockerfile default
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function pluginRootOrNull() {
|
|
189
|
-
try {
|
|
190
|
-
return findPluginRoot();
|
|
191
|
-
} catch {
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** Rewrite the pinned version in every CMakeLists.txt that finds rb_sdk; returns the files changed. */
|
|
197
|
-
export function setCppSdkPin(root, version) {
|
|
198
|
-
const files = cppSdkFiles(root);
|
|
199
|
-
for (const f of files) {
|
|
200
|
-
const text = readFileSync(f, "utf8").replace(RB_SDK_FIND, (m, old) => `find_package(rb_sdk ${version}${old ? "" : " EXACT"}`);
|
|
201
|
-
writeFileSync(f, text);
|
|
202
|
-
}
|
|
203
|
-
return files;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function findPluginRoot(start = process.cwd()) {
|
|
207
|
-
let dir = resolve(start);
|
|
208
|
-
for (;;) {
|
|
209
|
-
if (existsSync(resolve(dir, "manifest.json"))) return dir;
|
|
210
|
-
const parent = dirname(dir);
|
|
211
|
-
if (parent === dir) break;
|
|
212
|
-
dir = parent;
|
|
213
|
-
}
|
|
214
|
-
throw new Error(
|
|
215
|
-
"Not a muscat plugin project (no manifest.json found). Run from your plugin directory.",
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** Walk up from cwd looking for a project-local muscat-pk install. */
|
|
220
|
-
export function resolveLocalCliBin(start = process.cwd()) {
|
|
221
|
-
let dir = resolve(start);
|
|
222
|
-
for (;;) {
|
|
223
|
-
const candidate = resolve(dir, "node_modules/@rainbow-robotics/muscat-plugin-kit/cli/bin/muscat-pk.mjs");
|
|
224
|
-
if (existsSync(candidate)) return candidate;
|
|
225
|
-
const parent = dirname(dir);
|
|
226
|
-
if (parent === dir) return null;
|
|
227
|
-
dir = parent;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** True if the code currently running was installed with `npm install -g`. */
|
|
232
|
-
export function isNpmGlobalInstall() {
|
|
233
|
-
try {
|
|
234
|
-
const globalRoot = realpathSync(execSync("npm root -g", { encoding: "utf8" }).trim());
|
|
235
|
-
const rel = relative(globalRoot, realpathSync(kitRoot));
|
|
236
|
-
return rel === "" || (!rel.startsWith("..") && !rel.includes(":"));
|
|
237
|
-
} catch {
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/** True if the code currently running is a project's local install (not global/binary). */
|
|
243
|
-
export function isLocalKitInstall() {
|
|
244
|
-
// npm -g 도 node_modules 아래에 놓이므로 global root 는 제외한다.
|
|
245
|
-
return kitRoot.includes(`${sep}node_modules${sep}`) && !isNpmGlobalInstall();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const useColor = process.env.NO_COLOR !== "1" && (process.stdout.isTTY || process.env.FORCE_COLOR);
|
|
249
|
-
const ansi = (code) => (s) => useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s);
|
|
250
|
-
|
|
251
|
-
export const color = {
|
|
252
|
-
bold: ansi("1"),
|
|
253
|
-
dim: ansi("2"),
|
|
254
|
-
red: ansi("31"),
|
|
255
|
-
green: ansi("32"),
|
|
256
|
-
yellow: ansi("33"),
|
|
257
|
-
blue: ansi("34"),
|
|
258
|
-
magenta: ansi("35"),
|
|
259
|
-
cyan: ansi("36"),
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
export const log = {
|
|
263
|
-
info: (...m) => console.log(...m),
|
|
264
|
-
step: (...m) => console.log(color.cyan("›"), ...m.map(s => color.cyan(s))),
|
|
265
|
-
ok: (...m) => console.log(color.green("✓"), ...m),
|
|
266
|
-
warn: (...m) => console.warn(color.yellow("⚠"), ...m.map(s => color.yellow(s))),
|
|
267
|
-
error: (...m) => console.error(color.red("✖"), ...m.map(s => color.red(s))),
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
// dev / dev:back / build run the plugin back end, the local Muscat runtime, and cpp/ros2
|
|
271
|
-
// compilation in Docker. Show a clear install hint when Docker is missing.
|
|
272
|
-
export function ensureDocker() {
|
|
273
|
-
if (!hasBin("docker")) {
|
|
274
|
-
log.warn("Docker not found.");
|
|
275
|
-
log.warn(" muscat-pk runs the plugin back end, the local Muscat runtime, and cpp/ros2 builds in Docker.");
|
|
276
|
-
log.warn(" Install Docker, then retry: https://docs.docker.com/get-docker/");
|
|
277
|
-
throw new Error("Docker is required for dev / dev:back / build.");
|
|
278
|
-
}
|
|
279
|
-
// The binary alone is not enough: with the daemon down every docker step fails one by one
|
|
280
|
-
// (Docker Desktop not started is the usual case on macOS/Windows).
|
|
281
|
-
if (spawnSync("docker", ["info", "--format", "{{.ServerVersion}}"], { stdio: "ignore", timeout: 15_000 }).status === 0) return;
|
|
282
|
-
const hint = process.platform === "linux" ? "start it (e.g. `sudo systemctl start docker`)" : "start Docker Desktop and wait until it reports running";
|
|
283
|
-
throw new Error(`Docker is installed but the daemon is not running — ${hint}, then retry.`);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Remove dangling (untagged) muscat-pk images left behind after a rebuild.
|
|
288
|
-
* Uses label filter so only our images are touched — other Docker images are safe.
|
|
289
|
-
*/
|
|
290
|
-
export function pruneMuskatImages() {
|
|
291
|
-
spawnSync(
|
|
292
|
-
"docker",
|
|
293
|
-
["image", "prune", "--force", "--filter", "label=com.rainbow-robotics.muscat-pk=true"],
|
|
294
|
-
{ stdio: "ignore" },
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Docker flags that let the backend take a realtime scheduling priority
|
|
300
|
-
* (SCHED_FIFO/RR). Neither CAP_SYS_NICE nor a non-zero RLIMIT_RTPRIO is in
|
|
301
|
-
* Docker's default set, so without these sched_setscheduler() fails with EPERM.
|
|
302
|
-
* Mirrors the robot's `muscat` container (rby2 backend/docker-compose.yml) so
|
|
303
|
-
* dev and the robot behave the same.
|
|
304
|
-
*/
|
|
305
|
-
export function realtimeFlags() {
|
|
306
|
-
// memlock is bounded (not unlimited): mlockall'd pages can't be swapped out,
|
|
307
|
-
// so an unbounded limit lets a plugin pin RAM until the robot OOMs.
|
|
308
|
-
return ["--cap-add", "SYS_NICE", "--ulimit", "rtprio=99", "--ulimit", "memlock=536870912"];
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/** True if the image already exists in the local Docker image store. */
|
|
312
|
-
export function dockerImageExists(image) {
|
|
313
|
-
return spawnSync("docker", ["image", "inspect", image], { stdio: "ignore" }).status === 0;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* True if `docker build` should be skipped because the image is already built.
|
|
318
|
-
* BuildKit resolves the FROM tag over the network on every build (the
|
|
319
|
-
* "load metadata for docker.io/..." step) even when all layers are cached, so
|
|
320
|
-
* rebuilding an image we already have breaks `dev` offline for no benefit.
|
|
321
|
-
* Force a rebuild with MUSCAT_PK_REBUILD_IMAGE=1.
|
|
322
|
-
*/
|
|
323
|
-
// Rebuild when the Dockerfile or build args change (image label holds the hash).
|
|
324
|
-
const IMAGE_KEY_LABEL = "com.rainbow-robotics.muscat-pk.key";
|
|
325
|
-
|
|
326
|
-
export function imageKey(dockerfile, extra = "") {
|
|
327
|
-
return createHash("sha256").update(readFileSync(dockerfile)).update(extra).digest("hex").slice(0, 16);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// MUSCAT_PK_NO_BUILD_CACHE=1 (CI): build images without Docker's layer cache, so an apt/PyPI/GitHub
|
|
331
|
-
// change that breaks a fresh build fails the test too instead of hiding behind a cached layer.
|
|
332
|
-
export const buildCacheFlags = () => (process.env.MUSCAT_PK_NO_BUILD_CACHE === "1" ? ["--no-cache"] : []);
|
|
333
|
-
|
|
334
|
-
export function imageKeyLabel(key) {
|
|
335
|
-
return ["--label", `${IMAGE_KEY_LABEL}=${key}`];
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export function canSkipImageBuild(image, key = null) {
|
|
339
|
-
if (process.env.MUSCAT_PK_REBUILD_IMAGE === "1") return false;
|
|
340
|
-
if (!dockerImageExists(image)) return false;
|
|
341
|
-
if (!key) return true;
|
|
342
|
-
const r = spawnSync("docker", ["image", "inspect", "-f", `{{index .Config.Labels "${IMAGE_KEY_LABEL}"}}`, image], { encoding: "utf8" });
|
|
343
|
-
return r.status === 0 && r.stdout.trim() === key;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Linux에서 컨테이너가 마운트에 root 소유 파일을 남기면 다음 빌드의 dist/ 삭제가 EACCES로 막힌다.
|
|
347
|
-
// (macOS/Windows는 Docker Desktop이 소유권을 호스트 유저로 매핑하므로 불필요)
|
|
348
|
-
export function dockerUserFlags() {
|
|
349
|
-
if (process.platform !== "linux" || typeof process.getuid !== "function") return [];
|
|
350
|
-
return [
|
|
351
|
-
"--user", `${process.getuid()}:${process.getgid()}`,
|
|
352
|
-
"-e", "HOME=/tmp",
|
|
353
|
-
"-e", "XDG_CACHE_HOME=/tmp/.cache",
|
|
354
|
-
];
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
export function humanSize(bytes) {
|
|
358
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
359
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
360
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Return the on-disk path of a bundled Dockerfile.
|
|
365
|
-
* In npm-install mode the real file exists under cli/docker/.
|
|
366
|
-
* In compiled-binary mode it doesn't — extract from the embedded snapshot to tmpdir.
|
|
367
|
-
*/
|
|
368
|
-
export async function resolveDockerfile(name) {
|
|
369
|
-
const real = resolve(kitRoot, "cli/docker", name);
|
|
370
|
-
if (existsSync(real)) return real;
|
|
371
|
-
const { TEMPLATES } = await import("./templates-embedded.mjs");
|
|
372
|
-
const b64 = TEMPLATES[`docker/${name}`];
|
|
373
|
-
if (!b64) throw new Error(`Embedded Dockerfile missing: docker/${name}. Reinstall muscat-plugin-kit.`);
|
|
374
|
-
const dir = resolve(tmpdir(), "muscat-pk-docker");
|
|
375
|
-
mkdirSync(dir, { recursive: true });
|
|
376
|
-
const tmp = resolve(dir, name);
|
|
377
|
-
writeFileSync(tmp, Buffer.from(b64, "base64"));
|
|
378
|
-
return tmp;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/** This kit's installed version (from its package.json). */
|
|
382
|
-
export function kitVersion() {
|
|
383
|
-
if (process.env.MUSCAT_PK_VERSION) return process.env.MUSCAT_PK_VERSION;
|
|
384
|
-
try {
|
|
385
|
-
return JSON.parse(readFileSync(resolve(kitRoot, "package.json"), "utf8"))
|
|
386
|
-
.version;
|
|
387
|
-
} catch {
|
|
388
|
-
return "0.0.0";
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/** Compare semver-ish "x.y.z[-pre]". Returns true if `a` > `b` (release parts). */
|
|
393
|
-
export function isNewer(a, b) {
|
|
394
|
-
const parse = (v) =>
|
|
395
|
-
String(v)
|
|
396
|
-
.split("-")[0]
|
|
397
|
-
.split(".")
|
|
398
|
-
.map((n) => parseInt(n, 10) || 0);
|
|
399
|
-
const [a1, a2, a3] = parse(a);
|
|
400
|
-
const [b1, b2, b3] = parse(b);
|
|
401
|
-
if (a1 !== b1) return a1 > b1;
|
|
402
|
-
if (a2 !== b2) return a2 > b2;
|
|
403
|
-
return a3 > b3;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Non-blocking check for a newer published kit. Prints an update suggestion if
|
|
408
|
-
* one exists. Fails silently when offline. Honors `RB_REGISTRY` and is skipped
|
|
409
|
-
* by `MUSCAT_PK_NO_UPDATE_CHECK=1`.
|
|
410
|
-
*/
|
|
411
|
-
export async function checkKitUpdate({ timeoutMs = 2000 } = {}) {
|
|
412
|
-
if (process.env.MUSCAT_PK_NO_UPDATE_CHECK === "1") return;
|
|
413
|
-
const current = kitVersion();
|
|
414
|
-
const registry = (process.env.RB_REGISTRY || "https://registry.npmjs.org").replace(/\/$/, "");
|
|
415
|
-
try {
|
|
416
|
-
const ac = new AbortController();
|
|
417
|
-
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
418
|
-
const res = await fetch(
|
|
419
|
-
`${registry}/${encodeURIComponent(KIT_PUBLISHED_NAME)}/latest`,
|
|
420
|
-
{ signal: ac.signal },
|
|
421
|
-
);
|
|
422
|
-
clearTimeout(timer);
|
|
423
|
-
if (!res.ok) return;
|
|
424
|
-
const { version: latest } = await res.json();
|
|
425
|
-
if (latest && isNewer(latest, current)) {
|
|
426
|
-
const line = "─".repeat(58);
|
|
427
|
-
console.log(
|
|
428
|
-
[
|
|
429
|
-
"",
|
|
430
|
-
line,
|
|
431
|
-
` muscat-plugin-kit update available: ${current} → ${latest}`,
|
|
432
|
-
` Update: muscat-pk update`,
|
|
433
|
-
line,
|
|
434
|
-
"",
|
|
435
|
-
].join("\n"),
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
} catch {
|
|
439
|
-
/* offline or registry unreachable — ignore */
|
|
440
|
-
}
|
|
441
|
-
}
|
|
1
|
+
// Shared helpers for muscat-pk.
|
|
2
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
|
|
10
|
+
/** True if an executable is on PATH (cross-platform). */
|
|
11
|
+
export function hasBin(bin) {
|
|
12
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
13
|
+
return spawnSync(probe, [bin]).status === 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Find a Python 3.12 interpreter (robot targets 3.12), cross-platform.
|
|
18
|
+
* Returns the command as an argv array, e.g. ["py","-3.12"] or ["python3.12"].
|
|
19
|
+
* Falls back to a generic python3/python with a warning flag.
|
|
20
|
+
*/
|
|
21
|
+
export function detectPythonCmd() {
|
|
22
|
+
const candidates =
|
|
23
|
+
process.platform === "win32"
|
|
24
|
+
? [["py", "-3.12"], ["python3.12"], ["py", "-3"], ["python"]]
|
|
25
|
+
: [["python3.12"], ["python3"], ["python"]];
|
|
26
|
+
for (const cmd of candidates) {
|
|
27
|
+
const r = spawnSync(cmd[0], [...cmd.slice(1), "--version"]);
|
|
28
|
+
if (r.status === 0) {
|
|
29
|
+
const ver = String(r.stdout || r.stderr || "");
|
|
30
|
+
return { cmd, is312: ver.includes("3.12") };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { cmd: process.platform === "win32" ? ["python"] : ["python3"], is312: false };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Path to the venv's python interpreter (cross-platform). */
|
|
37
|
+
export function venvPython(venvDir) {
|
|
38
|
+
return process.platform === "win32"
|
|
39
|
+
? resolve(venvDir, "Scripts", "python.exe")
|
|
40
|
+
: resolve(venvDir, "bin", "python");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create/refresh a local `.venv` with requirements.txt installed, for the
|
|
45
|
+
* editor/linter only (the backend actually runs in Docker). Uses `uv` if
|
|
46
|
+
* present, else the system Python. Returns true if the venv is ready.
|
|
47
|
+
*/
|
|
48
|
+
export function ensureIdeVenv(root) {
|
|
49
|
+
const venvDir = resolve(root, ".venv");
|
|
50
|
+
const py = venvPython(venvDir);
|
|
51
|
+
|
|
52
|
+
if (!existsSync(py)) {
|
|
53
|
+
let created;
|
|
54
|
+
if (hasBin("uv")) {
|
|
55
|
+
created = spawnSync("uv", ["venv", venvDir, "--python", "3.12", "--seed"], { cwd: root, stdio: "ignore" });
|
|
56
|
+
} else {
|
|
57
|
+
const { cmd } = detectPythonCmd();
|
|
58
|
+
created = spawnSync(cmd[0], [...cmd.slice(1), "-m", "venv", venvDir], { cwd: root, stdio: "ignore" });
|
|
59
|
+
}
|
|
60
|
+
if (created.status !== 0 || !existsSync(py)) return false;
|
|
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
|
+
|
|
68
|
+
const reqs = resolve(root, "requirements.txt");
|
|
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
|
+
pip([`rainbow-rb-sdk-stubs${sdkPin ? `==${sdkPin}` : ""}`]); // not on PyPI for every SDK version yet
|
|
79
|
+
ensurePyrightMissingSourceOff(root);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const HOST_AGENT_ZENOH = "eclipse-zenoh==1.8.0"; // match the robot's zenoh-c minor version
|
|
84
|
+
|
|
85
|
+
function ensurePyrightMissingSourceOff(root) {
|
|
86
|
+
const path = resolve(root, "pyrightconfig.json");
|
|
87
|
+
let cfg = {};
|
|
88
|
+
if (existsSync(path)) {
|
|
89
|
+
try {
|
|
90
|
+
cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
91
|
+
} catch {
|
|
92
|
+
return; // user-edited / JSONC — leave it alone
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (cfg.reportMissingModuleSource === "none") return;
|
|
96
|
+
cfg.reportMissingModuleSource = "none";
|
|
97
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Published install/CLI package name. Author imports use @rainbow-robotics/plugin-ui separately. */
|
|
101
|
+
export const KIT_PUBLISHED_NAME = "@rainbow-robotics/muscat-plugin-kit";
|
|
102
|
+
|
|
103
|
+
// This file lives at <pkg>/cli/src/util.mjs → package root is two levels up.
|
|
104
|
+
export const kitRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
105
|
+
export const templatesDir = resolve(kitRoot, "cli/templates");
|
|
106
|
+
|
|
107
|
+
/** Parse `--flag value` and `--bool` from argv into an object. */
|
|
108
|
+
export function parseArgs(args, { booleans = [] } = {}) {
|
|
109
|
+
const out = { _: [] };
|
|
110
|
+
for (let i = 0; i < args.length; i++) {
|
|
111
|
+
const a = args[i];
|
|
112
|
+
if (a.startsWith("--")) {
|
|
113
|
+
const key = a.slice(2);
|
|
114
|
+
if (booleans.includes(key)) {
|
|
115
|
+
out[key] = true;
|
|
116
|
+
} else if (args[i + 1] && !args[i + 1].startsWith("--")) {
|
|
117
|
+
out[key] = args[++i];
|
|
118
|
+
} else {
|
|
119
|
+
out[key] = true;
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
out._.push(a);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Prompt user for input. Returns trimmed answer or fallback (non-TTY or empty). */
|
|
129
|
+
export async function prompt(question, fallback = "") {
|
|
130
|
+
if (!process.stdin.isTTY) return fallback;
|
|
131
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
132
|
+
const answer = (await rl.question(question)).trim();
|
|
133
|
+
rl.close();
|
|
134
|
+
return answer || fallback;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const LOCAL_HOSTS = new Set(["", "localhost", "127.0.0.1", "::1"]);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Ask for robot IP (and token if remote). Returns { robotIp, robotToken }.
|
|
141
|
+
* Skips prompts if already set in process.env (from a parent command like `dev`).
|
|
142
|
+
* Supports --robot-ip / --robot-token flags for non-interactive use.
|
|
143
|
+
*/
|
|
144
|
+
export async function promptRobotConnection(args = []) {
|
|
145
|
+
const parsed = parseArgs(args);
|
|
146
|
+
const robotIp =
|
|
147
|
+
parsed["robot-ip"] ??
|
|
148
|
+
process.env.MUSCAT_ROBOT_HOST ??
|
|
149
|
+
(await prompt(" Robot IP (skip if no robot): "));
|
|
150
|
+
|
|
151
|
+
if (LOCAL_HOSTS.has(robotIp)) return { robotIp, robotToken: "" };
|
|
152
|
+
|
|
153
|
+
const robotToken =
|
|
154
|
+
parsed["robot-token"] ??
|
|
155
|
+
process.env.MUSCAT_ROBOT_TOKEN ??
|
|
156
|
+
(await prompt(" SDK Token (required for remote): "));
|
|
157
|
+
|
|
158
|
+
if (!robotToken) throw new Error("SDK token is required for remote robot connection.");
|
|
159
|
+
return { robotIp, robotToken };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Walk up from cwd to find the plugin project root (has manifest.json). */
|
|
163
|
+
// C++ backends pin the SDK where they declare it: find_package(rb_sdk <version> EXACT REQUIRED) in
|
|
164
|
+
// cpp/CMakeLists.txt or ros2_ws/src/<pkg>/CMakeLists.txt. The dev image installs that version.
|
|
165
|
+
const RB_SDK_FIND = /find_package\(\s*rb_sdk(\s+[0-9][\w.]*)?/;
|
|
166
|
+
|
|
167
|
+
export function cppSdkFiles(root) {
|
|
168
|
+
const files = [resolve(root, "cpp", "CMakeLists.txt")];
|
|
169
|
+
const src = resolve(root, "ros2_ws", "src");
|
|
170
|
+
if (existsSync(src)) for (const pkg of readdirSync(src)) files.push(resolve(src, pkg, "CMakeLists.txt"));
|
|
171
|
+
return files.filter((f) => existsSync(f) && RB_SDK_FIND.test(readFileSync(f, "utf8")));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function cppSdkPin(root) {
|
|
175
|
+
for (const f of cppSdkFiles(root)) {
|
|
176
|
+
const version = readFileSync(f, "utf8").match(RB_SDK_FIND)[1]?.trim();
|
|
177
|
+
if (version) return version;
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** SDK version for the C++ dev image: MUSCAT_PK_RB_SDK_VERSION overrides the project's CMake pin. */
|
|
183
|
+
export function cppSdkVersion(root = pluginRootOrNull()) {
|
|
184
|
+
if (process.env.MUSCAT_PK_RB_SDK_VERSION) return process.env.MUSCAT_PK_RB_SDK_VERSION;
|
|
185
|
+
return root ? cppSdkPin(root) : undefined; // not in a plugin project: the Dockerfile default
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function pluginRootOrNull() {
|
|
189
|
+
try {
|
|
190
|
+
return findPluginRoot();
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Rewrite the pinned version in every CMakeLists.txt that finds rb_sdk; returns the files changed. */
|
|
197
|
+
export function setCppSdkPin(root, version) {
|
|
198
|
+
const files = cppSdkFiles(root);
|
|
199
|
+
for (const f of files) {
|
|
200
|
+
const text = readFileSync(f, "utf8").replace(RB_SDK_FIND, (m, old) => `find_package(rb_sdk ${version}${old ? "" : " EXACT"}`);
|
|
201
|
+
writeFileSync(f, text);
|
|
202
|
+
}
|
|
203
|
+
return files;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function findPluginRoot(start = process.cwd()) {
|
|
207
|
+
let dir = resolve(start);
|
|
208
|
+
for (;;) {
|
|
209
|
+
if (existsSync(resolve(dir, "manifest.json"))) return dir;
|
|
210
|
+
const parent = dirname(dir);
|
|
211
|
+
if (parent === dir) break;
|
|
212
|
+
dir = parent;
|
|
213
|
+
}
|
|
214
|
+
throw new Error(
|
|
215
|
+
"Not a muscat plugin project (no manifest.json found). Run from your plugin directory.",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Walk up from cwd looking for a project-local muscat-pk install. */
|
|
220
|
+
export function resolveLocalCliBin(start = process.cwd()) {
|
|
221
|
+
let dir = resolve(start);
|
|
222
|
+
for (;;) {
|
|
223
|
+
const candidate = resolve(dir, "node_modules/@rainbow-robotics/muscat-plugin-kit/cli/bin/muscat-pk.mjs");
|
|
224
|
+
if (existsSync(candidate)) return candidate;
|
|
225
|
+
const parent = dirname(dir);
|
|
226
|
+
if (parent === dir) return null;
|
|
227
|
+
dir = parent;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** True if the code currently running was installed with `npm install -g`. */
|
|
232
|
+
export function isNpmGlobalInstall() {
|
|
233
|
+
try {
|
|
234
|
+
const globalRoot = realpathSync(execSync("npm root -g", { encoding: "utf8" }).trim());
|
|
235
|
+
const rel = relative(globalRoot, realpathSync(kitRoot));
|
|
236
|
+
return rel === "" || (!rel.startsWith("..") && !rel.includes(":"));
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** True if the code currently running is a project's local install (not global/binary). */
|
|
243
|
+
export function isLocalKitInstall() {
|
|
244
|
+
// npm -g 도 node_modules 아래에 놓이므로 global root 는 제외한다.
|
|
245
|
+
return kitRoot.includes(`${sep}node_modules${sep}`) && !isNpmGlobalInstall();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const useColor = process.env.NO_COLOR !== "1" && (process.stdout.isTTY || process.env.FORCE_COLOR);
|
|
249
|
+
const ansi = (code) => (s) => useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s);
|
|
250
|
+
|
|
251
|
+
export const color = {
|
|
252
|
+
bold: ansi("1"),
|
|
253
|
+
dim: ansi("2"),
|
|
254
|
+
red: ansi("31"),
|
|
255
|
+
green: ansi("32"),
|
|
256
|
+
yellow: ansi("33"),
|
|
257
|
+
blue: ansi("34"),
|
|
258
|
+
magenta: ansi("35"),
|
|
259
|
+
cyan: ansi("36"),
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const log = {
|
|
263
|
+
info: (...m) => console.log(...m),
|
|
264
|
+
step: (...m) => console.log(color.cyan("›"), ...m.map(s => color.cyan(s))),
|
|
265
|
+
ok: (...m) => console.log(color.green("✓"), ...m),
|
|
266
|
+
warn: (...m) => console.warn(color.yellow("⚠"), ...m.map(s => color.yellow(s))),
|
|
267
|
+
error: (...m) => console.error(color.red("✖"), ...m.map(s => color.red(s))),
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// dev / dev:back / build run the plugin back end, the local Muscat runtime, and cpp/ros2
|
|
271
|
+
// compilation in Docker. Show a clear install hint when Docker is missing.
|
|
272
|
+
export function ensureDocker() {
|
|
273
|
+
if (!hasBin("docker")) {
|
|
274
|
+
log.warn("Docker not found.");
|
|
275
|
+
log.warn(" muscat-pk runs the plugin back end, the local Muscat runtime, and cpp/ros2 builds in Docker.");
|
|
276
|
+
log.warn(" Install Docker, then retry: https://docs.docker.com/get-docker/");
|
|
277
|
+
throw new Error("Docker is required for dev / dev:back / build.");
|
|
278
|
+
}
|
|
279
|
+
// The binary alone is not enough: with the daemon down every docker step fails one by one
|
|
280
|
+
// (Docker Desktop not started is the usual case on macOS/Windows).
|
|
281
|
+
if (spawnSync("docker", ["info", "--format", "{{.ServerVersion}}"], { stdio: "ignore", timeout: 15_000 }).status === 0) return;
|
|
282
|
+
const hint = process.platform === "linux" ? "start it (e.g. `sudo systemctl start docker`)" : "start Docker Desktop and wait until it reports running";
|
|
283
|
+
throw new Error(`Docker is installed but the daemon is not running — ${hint}, then retry.`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Remove dangling (untagged) muscat-pk images left behind after a rebuild.
|
|
288
|
+
* Uses label filter so only our images are touched — other Docker images are safe.
|
|
289
|
+
*/
|
|
290
|
+
export function pruneMuskatImages() {
|
|
291
|
+
spawnSync(
|
|
292
|
+
"docker",
|
|
293
|
+
["image", "prune", "--force", "--filter", "label=com.rainbow-robotics.muscat-pk=true"],
|
|
294
|
+
{ stdio: "ignore" },
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Docker flags that let the backend take a realtime scheduling priority
|
|
300
|
+
* (SCHED_FIFO/RR). Neither CAP_SYS_NICE nor a non-zero RLIMIT_RTPRIO is in
|
|
301
|
+
* Docker's default set, so without these sched_setscheduler() fails with EPERM.
|
|
302
|
+
* Mirrors the robot's `muscat` container (rby2 backend/docker-compose.yml) so
|
|
303
|
+
* dev and the robot behave the same.
|
|
304
|
+
*/
|
|
305
|
+
export function realtimeFlags() {
|
|
306
|
+
// memlock is bounded (not unlimited): mlockall'd pages can't be swapped out,
|
|
307
|
+
// so an unbounded limit lets a plugin pin RAM until the robot OOMs.
|
|
308
|
+
return ["--cap-add", "SYS_NICE", "--ulimit", "rtprio=99", "--ulimit", "memlock=536870912"];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** True if the image already exists in the local Docker image store. */
|
|
312
|
+
export function dockerImageExists(image) {
|
|
313
|
+
return spawnSync("docker", ["image", "inspect", image], { stdio: "ignore" }).status === 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* True if `docker build` should be skipped because the image is already built.
|
|
318
|
+
* BuildKit resolves the FROM tag over the network on every build (the
|
|
319
|
+
* "load metadata for docker.io/..." step) even when all layers are cached, so
|
|
320
|
+
* rebuilding an image we already have breaks `dev` offline for no benefit.
|
|
321
|
+
* Force a rebuild with MUSCAT_PK_REBUILD_IMAGE=1.
|
|
322
|
+
*/
|
|
323
|
+
// Rebuild when the Dockerfile or build args change (image label holds the hash).
|
|
324
|
+
const IMAGE_KEY_LABEL = "com.rainbow-robotics.muscat-pk.key";
|
|
325
|
+
|
|
326
|
+
export function imageKey(dockerfile, extra = "") {
|
|
327
|
+
return createHash("sha256").update(readFileSync(dockerfile)).update(extra).digest("hex").slice(0, 16);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// MUSCAT_PK_NO_BUILD_CACHE=1 (CI): build images without Docker's layer cache, so an apt/PyPI/GitHub
|
|
331
|
+
// change that breaks a fresh build fails the test too instead of hiding behind a cached layer.
|
|
332
|
+
export const buildCacheFlags = () => (process.env.MUSCAT_PK_NO_BUILD_CACHE === "1" ? ["--no-cache"] : []);
|
|
333
|
+
|
|
334
|
+
export function imageKeyLabel(key) {
|
|
335
|
+
return ["--label", `${IMAGE_KEY_LABEL}=${key}`];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function canSkipImageBuild(image, key = null) {
|
|
339
|
+
if (process.env.MUSCAT_PK_REBUILD_IMAGE === "1") return false;
|
|
340
|
+
if (!dockerImageExists(image)) return false;
|
|
341
|
+
if (!key) return true;
|
|
342
|
+
const r = spawnSync("docker", ["image", "inspect", "-f", `{{index .Config.Labels "${IMAGE_KEY_LABEL}"}}`, image], { encoding: "utf8" });
|
|
343
|
+
return r.status === 0 && r.stdout.trim() === key;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Linux에서 컨테이너가 마운트에 root 소유 파일을 남기면 다음 빌드의 dist/ 삭제가 EACCES로 막힌다.
|
|
347
|
+
// (macOS/Windows는 Docker Desktop이 소유권을 호스트 유저로 매핑하므로 불필요)
|
|
348
|
+
export function dockerUserFlags() {
|
|
349
|
+
if (process.platform !== "linux" || typeof process.getuid !== "function") return [];
|
|
350
|
+
return [
|
|
351
|
+
"--user", `${process.getuid()}:${process.getgid()}`,
|
|
352
|
+
"-e", "HOME=/tmp",
|
|
353
|
+
"-e", "XDG_CACHE_HOME=/tmp/.cache",
|
|
354
|
+
];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function humanSize(bytes) {
|
|
358
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
359
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
360
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Return the on-disk path of a bundled Dockerfile.
|
|
365
|
+
* In npm-install mode the real file exists under cli/docker/.
|
|
366
|
+
* In compiled-binary mode it doesn't — extract from the embedded snapshot to tmpdir.
|
|
367
|
+
*/
|
|
368
|
+
export async function resolveDockerfile(name) {
|
|
369
|
+
const real = resolve(kitRoot, "cli/docker", name);
|
|
370
|
+
if (existsSync(real)) return real;
|
|
371
|
+
const { TEMPLATES } = await import("./templates-embedded.mjs");
|
|
372
|
+
const b64 = TEMPLATES[`docker/${name}`];
|
|
373
|
+
if (!b64) throw new Error(`Embedded Dockerfile missing: docker/${name}. Reinstall muscat-plugin-kit.`);
|
|
374
|
+
const dir = resolve(tmpdir(), "muscat-pk-docker");
|
|
375
|
+
mkdirSync(dir, { recursive: true });
|
|
376
|
+
const tmp = resolve(dir, name);
|
|
377
|
+
writeFileSync(tmp, Buffer.from(b64, "base64"));
|
|
378
|
+
return tmp;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** This kit's installed version (from its package.json). */
|
|
382
|
+
export function kitVersion() {
|
|
383
|
+
if (process.env.MUSCAT_PK_VERSION) return process.env.MUSCAT_PK_VERSION;
|
|
384
|
+
try {
|
|
385
|
+
return JSON.parse(readFileSync(resolve(kitRoot, "package.json"), "utf8"))
|
|
386
|
+
.version;
|
|
387
|
+
} catch {
|
|
388
|
+
return "0.0.0";
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Compare semver-ish "x.y.z[-pre]". Returns true if `a` > `b` (release parts). */
|
|
393
|
+
export function isNewer(a, b) {
|
|
394
|
+
const parse = (v) =>
|
|
395
|
+
String(v)
|
|
396
|
+
.split("-")[0]
|
|
397
|
+
.split(".")
|
|
398
|
+
.map((n) => parseInt(n, 10) || 0);
|
|
399
|
+
const [a1, a2, a3] = parse(a);
|
|
400
|
+
const [b1, b2, b3] = parse(b);
|
|
401
|
+
if (a1 !== b1) return a1 > b1;
|
|
402
|
+
if (a2 !== b2) return a2 > b2;
|
|
403
|
+
return a3 > b3;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Non-blocking check for a newer published kit. Prints an update suggestion if
|
|
408
|
+
* one exists. Fails silently when offline. Honors `RB_REGISTRY` and is skipped
|
|
409
|
+
* by `MUSCAT_PK_NO_UPDATE_CHECK=1`.
|
|
410
|
+
*/
|
|
411
|
+
export async function checkKitUpdate({ timeoutMs = 2000 } = {}) {
|
|
412
|
+
if (process.env.MUSCAT_PK_NO_UPDATE_CHECK === "1") return;
|
|
413
|
+
const current = kitVersion();
|
|
414
|
+
const registry = (process.env.RB_REGISTRY || "https://registry.npmjs.org").replace(/\/$/, "");
|
|
415
|
+
try {
|
|
416
|
+
const ac = new AbortController();
|
|
417
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
418
|
+
const res = await fetch(
|
|
419
|
+
`${registry}/${encodeURIComponent(KIT_PUBLISHED_NAME)}/latest`,
|
|
420
|
+
{ signal: ac.signal },
|
|
421
|
+
);
|
|
422
|
+
clearTimeout(timer);
|
|
423
|
+
if (!res.ok) return;
|
|
424
|
+
const { version: latest } = await res.json();
|
|
425
|
+
if (latest && isNewer(latest, current)) {
|
|
426
|
+
const line = "─".repeat(58);
|
|
427
|
+
console.log(
|
|
428
|
+
[
|
|
429
|
+
"",
|
|
430
|
+
line,
|
|
431
|
+
` muscat-plugin-kit update available: ${current} → ${latest}`,
|
|
432
|
+
` Update: muscat-pk update`,
|
|
433
|
+
line,
|
|
434
|
+
"",
|
|
435
|
+
].join("\n"),
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
/* offline or registry unreachable — ignore */
|
|
440
|
+
}
|
|
441
|
+
}
|