@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/build.mjs
CHANGED
|
@@ -1,447 +1,447 @@
|
|
|
1
|
-
// muscat-pk build — bundle ui/content.tsx into dist/content.js, stage
|
|
2
|
-
// manifest.json and assets/ for distribution, and zip dist/ into a
|
|
3
|
-
// distributable ZIP (skip with --no-zip).
|
|
4
|
-
//
|
|
5
|
-
// sandbox.html is generated by the CLI — plugin authors never manage it.
|
|
6
|
-
// The import map always reflects the current plugin-ui entry set, so users
|
|
7
|
-
// don't have to update it when the runtime adds new entries.
|
|
8
|
-
//
|
|
9
|
-
// The plugin author's `@rainbow-robotics/plugin-ui` and React imports are left external.
|
|
10
|
-
// Muscat/robot and preview-host provide those modules through a sandbox import
|
|
11
|
-
// map, so plugin ZIPs do not carry the UI runtime.
|
|
12
|
-
import { chmodSync, cpSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { createRequire } from "node:module";
|
|
14
|
-
import { basename, dirname, resolve } from "node:path";
|
|
15
|
-
import { createInterface } from "node:readline/promises";
|
|
16
|
-
import { execSync, spawnSync } from "node:child_process";
|
|
17
|
-
import { ensureDocker, findPluginRoot, hasBin, humanSize, log, parseArgs, prompt } from "./util.mjs";
|
|
18
|
-
import { assertManifest, manifestRuntime } from "./manifest.mjs";
|
|
19
|
-
|
|
20
|
-
// Find a usable JS runtime for running vite as a subprocess.
|
|
21
|
-
// In binary (Bun) mode, try `bun` first (it can run vite directly), then node.
|
|
22
|
-
// In node mode, use the current process.
|
|
23
|
-
function findJsRuntime() {
|
|
24
|
-
if (typeof Bun === "undefined") return { bin: process.execPath, isBun: false };
|
|
25
|
-
for (const c of ["bun", "node", "nodejs"]) {
|
|
26
|
-
if (hasBin(c)) return { bin: c, isBun: c === "bun" };
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Resolve vite bin path from the plugin root, hoisting-aware.
|
|
32
|
-
function findViteBin(pluginRoot) {
|
|
33
|
-
try {
|
|
34
|
-
const req = createRequire(resolve(pluginRoot, "package.json"));
|
|
35
|
-
const vitePkg = req.resolve("vite/package.json");
|
|
36
|
-
return resolve(dirname(vitePkg), "bin", "vite.js");
|
|
37
|
-
} catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Run vite build in the plugin project. Without a JS runtime (binary CLI, no Node.js) it asks
|
|
43
|
-
// whether to build the plugin without its UI and returns false on yes; throws on no or if vite is missing.
|
|
44
|
-
async function runViteBuild(pluginRoot, { mode = "production" } = {}) {
|
|
45
|
-
const runtime = findJsRuntime();
|
|
46
|
-
if (!runtime) {
|
|
47
|
-
log.warn("Node.js 20+ (or Bun) not found — the UI cannot be built and will not be in the plugin.");
|
|
48
|
-
const answer = await prompt(" Build the plugin without its UI? [y/N]: ", "n");
|
|
49
|
-
if (answer.toLowerCase() !== "y")
|
|
50
|
-
throw new Error("build cancelled. Install Node.js 20+ (https://nodejs.org), run `npm install` in the plugin, then build again.");
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
const viteBin = findViteBin(pluginRoot);
|
|
54
|
-
if (!viteBin || !existsSync(viteBin)) {
|
|
55
|
-
throw new Error(
|
|
56
|
-
"vite not found in this plugin's node_modules.\n" +
|
|
57
|
-
` Run: npm install (inside ${pluginRoot})`,
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
log.step(`vite build (mode: ${mode})`);
|
|
62
|
-
// bun can run vite directly; node needs the explicit bin path
|
|
63
|
-
const args = runtime.isBun ? ["run", viteBin, "build", "--mode", mode]
|
|
64
|
-
: [viteBin, "build", "--mode", mode];
|
|
65
|
-
const r = spawnSync(runtime.bin, args, { cwd: pluginRoot, stdio: "inherit" });
|
|
66
|
-
if (r.status !== 0) throw new Error("vite build failed");
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// function isMainBranch() {
|
|
71
|
-
// const r = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" });
|
|
72
|
-
// const branch = r.stdout?.trim();
|
|
73
|
-
// return branch === "main" || branch === "master";
|
|
74
|
-
// }
|
|
75
|
-
|
|
76
|
-
// For ros2 plugins, compile the colcon workspace inside the Jazzy + Python 3.12 container.
|
|
77
|
-
// rclpy: arch-agnostic (pure Python) → dist/ros2_ws/install/ (built once)
|
|
78
|
-
// rclcpp: arch-specific C++ binaries → dist/ros2_ws/install-{arch}/ per arch
|
|
79
|
-
// Cross-arch C++ builds run inside an emulated container. Docker Desktop ships the
|
|
80
|
-
// emulation; native Linux needs binfmt/QEMU — check up front so the error says so.
|
|
81
|
-
function assertCanBuildArch(arch, dockerPlatform) {
|
|
82
|
-
const info = spawnSync("docker", ["info", "--format", "{{.Architecture}}"], { encoding: "utf8" });
|
|
83
|
-
const host = { x86_64: "amd64", aarch64: "arm64", arm64: "arm64" }[(info.stdout || "").trim()] ?? "";
|
|
84
|
-
if (arch === host) return;
|
|
85
|
-
const r = spawnSync("docker", ["run", "--rm", "--platform", dockerPlatform, "ubuntu:24.04", "true"], { stdio: "ignore" });
|
|
86
|
-
if (r.status === 0) return;
|
|
87
|
-
throw new Error(
|
|
88
|
-
`can't build ${arch} on this ${host || "machine"}: Docker has no ${arch} emulation.\n` +
|
|
89
|
-
` enable it once: docker run --privileged --rm tonistiigi/binfmt --install ${arch}\n` +
|
|
90
|
-
` or build only this machine's arch: muscat-pk build --arch ${host || "<arch>"}`,
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export async function exportRos2Build(root, archs = ["amd64", "arm64"]) {
|
|
95
|
-
if (manifestRuntime(root) !== "ros2") return;
|
|
96
|
-
const { assertDockerReady, ensureImage, colconBuild, ROS2_DEV_IMAGE, ARCH_DOCKER_PLATFORM } = await import("./ros2.mjs");
|
|
97
|
-
assertDockerReady();
|
|
98
|
-
|
|
99
|
-
const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8"));
|
|
100
|
-
if (manifest.runtime?.rosClient === "rclcpp") {
|
|
101
|
-
// C++ ROS2: compile per arch so each robot picks the right install-{arch}/
|
|
102
|
-
for (const arch of archs) {
|
|
103
|
-
const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
|
|
104
|
-
if (!dockerPlatform) throw new Error(`unknown arch '${arch}'`);
|
|
105
|
-
assertCanBuildArch(arch, dockerPlatform);
|
|
106
|
-
await ensureImage(`${ROS2_DEV_IMAGE}-${arch}`, { platform: dockerPlatform });
|
|
107
|
-
colconBuild(root, { symlink: false, arch });
|
|
108
|
-
log.ok(`dist/ros2_ws/install-${arch}/ ready`);
|
|
109
|
-
}
|
|
110
|
-
} else {
|
|
111
|
-
// rclpy / pure Python: arch-agnostic, build once
|
|
112
|
-
await ensureImage();
|
|
113
|
-
colconBuild(root, { symlink: false });
|
|
114
|
-
log.ok("dist/ros2_ws/install/ ready");
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// For cpp plugins, compile cpp/ inside the Ubuntu container (Release) per arch
|
|
119
|
-
// and copy each binary into dist/{arch}/{entry} so the ZIP carries relocatable
|
|
120
|
-
// robot-compatible executables.
|
|
121
|
-
export async function exportCppBuild(root, distDir, archs = ["amd64", "arm64"]) {
|
|
122
|
-
if (manifestRuntime(root) !== "cpp") return;
|
|
123
|
-
const { assertDockerReady, ensureImage, cmakeBuild, CPP_DEV_IMAGE, ARCH_DOCKER_PLATFORM } = await import("./cpp.mjs");
|
|
124
|
-
assertDockerReady();
|
|
125
|
-
|
|
126
|
-
const entry = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime?.entry;
|
|
127
|
-
if (!entry) throw new Error("manifest.json runtime.entry is required for cpp (the built executable name)");
|
|
128
|
-
|
|
129
|
-
for (const arch of archs) {
|
|
130
|
-
const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
|
|
131
|
-
if (!dockerPlatform) throw new Error(`unknown arch '${arch}'`);
|
|
132
|
-
assertCanBuildArch(arch, dockerPlatform);
|
|
133
|
-
await ensureImage(`${CPP_DEV_IMAGE}-${arch}`, { platform: dockerPlatform });
|
|
134
|
-
cmakeBuild(root, { buildType: "Release", arch });
|
|
135
|
-
const built = resolve(root, `cpp/build-${arch}`, entry);
|
|
136
|
-
if (!existsSync(built)) {
|
|
137
|
-
throw new Error(`cpp/build-${arch}/${entry} not found — the CMake target name must equal runtime.entry`);
|
|
138
|
-
}
|
|
139
|
-
const archDir = resolve(distDir, arch);
|
|
140
|
-
mkdirSync(archDir, { recursive: true });
|
|
141
|
-
const dest = resolve(archDir, entry);
|
|
142
|
-
copyFileSync(built, dest);
|
|
143
|
-
chmodSync(dest, 0o755);
|
|
144
|
-
const libDir = resolve(root, `cpp/build-${arch}`, "lib");
|
|
145
|
-
if (existsSync(libDir)) cpSync(libDir, resolve(archDir, "lib"), { recursive: true, verbatimSymlinks: true });
|
|
146
|
-
log.ok(`cpp binary → dist/${arch}/${entry}${existsSync(libDir) ? " (+ lib/)" : ""}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const PRERELEASE_RE = /\.(dev|a|b|rc|alpha|beta)\d*/i;
|
|
151
|
-
|
|
152
|
-
function isPrerelease(version) {
|
|
153
|
-
return PRERELEASE_RE.test(version);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Resolve the latest version of a PyPI package.
|
|
157
|
-
// stableOnly=true → skips pre-release (dev/a/b/rc) versions.
|
|
158
|
-
async function latestPypiVersion(pkg, { stableOnly = false } = {}) {
|
|
159
|
-
const res = await fetch(`https://pypi.org/pypi/${pkg}/json`, {
|
|
160
|
-
signal: new AbortController().signal.timeout(5000),
|
|
161
|
-
});
|
|
162
|
-
if (!res.ok) throw new Error(`pypi ${pkg}: ${res.status}`);
|
|
163
|
-
const data = await res.json();
|
|
164
|
-
|
|
165
|
-
if (!stableOnly) return data.info.version;
|
|
166
|
-
|
|
167
|
-
// data.info.version is latest stable when stable releases exist.
|
|
168
|
-
// If it's still a pre-release, scan all releases for the newest stable.
|
|
169
|
-
if (!isPrerelease(data.info.version)) return data.info.version;
|
|
170
|
-
|
|
171
|
-
const stable = Object.keys(data.releases)
|
|
172
|
-
.filter((v) => !isPrerelease(v))
|
|
173
|
-
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
174
|
-
.at(-1);
|
|
175
|
-
|
|
176
|
-
if (stable) return stable;
|
|
177
|
-
|
|
178
|
-
// No stable release exists — fall back with a warning.
|
|
179
|
-
log.warn(` ${pkg}: no stable release found, using ${data.info.version}`);
|
|
180
|
-
return data.info.version;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Pin unpinned (no version specifier) entries in requirements.txt to the
|
|
184
|
-
// latest PyPI release. Runs on explicit `build` only — keeps the shipped
|
|
185
|
-
// plugin reproducible without churning the source file on every dev rebuild.
|
|
186
|
-
// stableOnly=true is used on main-branch builds to avoid shipping dev versions.
|
|
187
|
-
export async function pinRequirements(root, { stableOnly = false } = {}) {
|
|
188
|
-
const reqPath = resolve(root, "requirements.txt");
|
|
189
|
-
if (!existsSync(reqPath)) return;
|
|
190
|
-
|
|
191
|
-
const lines = readFileSync(reqPath, "utf8").split("\n");
|
|
192
|
-
const out = [];
|
|
193
|
-
let pinned = 0;
|
|
194
|
-
for (const line of lines) {
|
|
195
|
-
const name = line.trim();
|
|
196
|
-
// skip blanks, comments, and already-specified versions
|
|
197
|
-
if (!name || name.startsWith("#") || /[=<>~!]/.test(name)) {
|
|
198
|
-
out.push(line);
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
try {
|
|
202
|
-
const v = await latestPypiVersion(name, { stableOnly });
|
|
203
|
-
out.push(`${name}==${v}`);
|
|
204
|
-
pinned++;
|
|
205
|
-
log.info(` pinned ${name}==${v}`);
|
|
206
|
-
} catch {
|
|
207
|
-
out.push(line);
|
|
208
|
-
log.warn(` could not resolve ${name} (offline?) — left unpinned`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (pinned > 0) writeFileSync(reqPath, out.join("\n"));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export async function buildPlugin({ root, minify = false } = {}) {
|
|
215
|
-
const pluginRoot = root ?? findPluginRoot();
|
|
216
|
-
assertManifest(pluginRoot);
|
|
217
|
-
const distDir = resolve(pluginRoot, "dist");
|
|
218
|
-
try {
|
|
219
|
-
rmSync(distDir, { recursive: true, force: true });
|
|
220
|
-
} catch (err) {
|
|
221
|
-
if (err?.code !== "EACCES" && err?.code !== "EPERM") throw err;
|
|
222
|
-
// 이전 버전이 컨테이너를 root로 돌려 남긴 dist/ — 한 번만 정리하면 된다
|
|
223
|
-
throw new Error(
|
|
224
|
-
`Permission denied when deleting dist/ (${err.path ?? distDir}).\n` +
|
|
225
|
-
`The previous build left files owned by root. Please clean it up once:\n` +
|
|
226
|
-
` sudo rm -rf ${distDir}`,
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
mkdirSync(distDir, { recursive: true });
|
|
230
|
-
|
|
231
|
-
const manifest = JSON.parse(readFileSync(resolve(pluginRoot, "manifest.json"), "utf8"));
|
|
232
|
-
const uiDir = resolve(pluginRoot, "ui");
|
|
233
|
-
const declaredUiEntries = [...new Set(
|
|
234
|
-
(manifest.contributes ?? []).map((c) => c.ui).filter(Boolean)
|
|
235
|
-
)];
|
|
236
|
-
// Validate each declared UI entry has a matching source file
|
|
237
|
-
for (const entry of declaredUiEntries) {
|
|
238
|
-
const exists = [".tsx", ".ts", ".jsx", ".js"].some(
|
|
239
|
-
(ext) => existsSync(resolve(uiDir, entry + ext))
|
|
240
|
-
);
|
|
241
|
-
if (!exists)
|
|
242
|
-
throw new Error(
|
|
243
|
-
`contributes declares ui "${entry}" but ui/${entry}.tsx (or .ts/.jsx/.js) does not exist.`
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
const hasUiFiles = existsSync(uiDir) && readdirSync(uiDir).some((f) => /\.[tj]sx?$/.test(f));
|
|
247
|
-
// A backend-only plugin (no contributes) ships no UI, so leftover ui/ files are not built either.
|
|
248
|
-
let uiBuilt = true;
|
|
249
|
-
if (declaredUiEntries.length > 0 || (hasUiFiles && manifest.contributes?.length > 0)) {
|
|
250
|
-
uiBuilt = await runViteBuild(pluginRoot, { mode: minify ? "production" : "development" });
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Stage distribution files.
|
|
254
|
-
const manifestSrc = resolve(pluginRoot, "manifest.json");
|
|
255
|
-
if (existsSync(manifestSrc)) copyFileSync(manifestSrc, resolve(distDir, "manifest.json"));
|
|
256
|
-
// Built without the UI: ship it as a backend-only plugin so the host lists no screen it cannot load.
|
|
257
|
-
if (!uiBuilt) {
|
|
258
|
-
const { contributes, ...backendOnly } = manifest;
|
|
259
|
-
writeFileSync(resolve(distDir, "manifest.json"), JSON.stringify(backendOnly, null, 2) + "\n");
|
|
260
|
-
log.warn(`packaged without UI: dropped ${contributes.length} screen area(s) from dist/manifest.json`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// No sandbox.html: the host dynamically imports dist/{ui}.js directly as a
|
|
264
|
-
// plain ESM module (see @rainbow-robotics/plugin-ui/host) — no iframe.
|
|
265
|
-
|
|
266
|
-
const assetsSrc = resolve(pluginRoot, "assets");
|
|
267
|
-
if (existsSync(assetsSrc)) {
|
|
268
|
-
cpSync(assetsSrc, resolve(distDir, "assets"), { recursive: true });
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// README.md, README.[lang].md, CHANGELOG.md → dist/
|
|
272
|
-
for (const f of readdirSync(pluginRoot)) {
|
|
273
|
-
if (/^README(\.[a-z]{2}(-[A-Z]{2})?)?\.md$/i.test(f) || /^CHANGELOG\.md$/i.test(f)) {
|
|
274
|
-
copyFileSync(resolve(pluginRoot, f), resolve(distDir, f));
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Backend runtime files — copy entire py/ into dist/, then requirements.txt from root.
|
|
279
|
-
const pySrc = resolve(pluginRoot, "py");
|
|
280
|
-
if (existsSync(pySrc)) {
|
|
281
|
-
cpSync(pySrc, distDir, { recursive: true });
|
|
282
|
-
}
|
|
283
|
-
const rootReqs = resolve(pluginRoot, "requirements.txt");
|
|
284
|
-
if (existsSync(rootReqs)) {
|
|
285
|
-
copyFileSync(rootReqs, resolve(distDir, "requirements.txt"));
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
log.ok(`built → ${distDir}`);
|
|
289
|
-
return { pluginRoot, distDir, uiBuilt };
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function dirSize(dir, skip = []) {
|
|
293
|
-
if (!existsSync(dir)) return 0;
|
|
294
|
-
let total = 0;
|
|
295
|
-
for (const entry of readdirSync(dir)) {
|
|
296
|
-
if (skip.includes(entry)) continue;
|
|
297
|
-
const full = resolve(dir, entry);
|
|
298
|
-
const st = statSync(full);
|
|
299
|
-
total += st.isDirectory() ? dirSize(full) : st.size;
|
|
300
|
-
}
|
|
301
|
-
return total;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Read the uncompressed byte total of a .whl (zip) file by parsing its central
|
|
305
|
-
// directory — no external library needed, no actual extraction.
|
|
306
|
-
|
|
307
|
-
// installedSize = "이 플러그인 하나를 지우면 되찾는 용량"
|
|
308
|
-
// = dist/(wheels/ 제외)
|
|
309
|
-
// ros2_ws/install* 은 이제 dist/ 안에 있으므로 별도 합산 불필요.
|
|
310
|
-
// 휠은 로봇 공유 스토어(store/py/)에 언팩되므로 포함하지 않는다.
|
|
311
|
-
function calcInstalledSize(pluginRoot) {
|
|
312
|
-
const distDir = resolve(pluginRoot, "dist");
|
|
313
|
-
|
|
314
|
-
const installedSize = dirSize(distDir, ["wheels"]);
|
|
315
|
-
|
|
316
|
-
// Write to root manifest.json
|
|
317
|
-
const rootManifestPath = resolve(pluginRoot, "manifest.json");
|
|
318
|
-
if (existsSync(rootManifestPath)) {
|
|
319
|
-
const m = JSON.parse(readFileSync(rootManifestPath, "utf8"));
|
|
320
|
-
m.installedSize = installedSize;
|
|
321
|
-
writeFileSync(rootManifestPath, JSON.stringify(m, null, 2) + "\n");
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Re-stage dist/manifest.json so the ZIP carries the updated value
|
|
325
|
-
const distManifestPath = resolve(distDir, "manifest.json");
|
|
326
|
-
if (existsSync(distManifestPath)) {
|
|
327
|
-
const m = JSON.parse(readFileSync(distManifestPath, "utf8"));
|
|
328
|
-
m.installedSize = installedSize;
|
|
329
|
-
writeFileSync(distManifestPath, JSON.stringify(m, null, 2) + "\n");
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
log.ok(`installedSize → ${humanSize(installedSize)}`);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Zip dist/ into a distributable ZIP with a size report.
|
|
336
|
-
// ros2_ws/install* is now inside dist/, so only "dist" is needed.
|
|
337
|
-
function packZip(pluginRoot) {
|
|
338
|
-
const manifest = JSON.parse(readFileSync(resolve(pluginRoot, "manifest.json"), "utf8"));
|
|
339
|
-
const zipName = `${basename(pluginRoot)}-${manifest.version || "0.0.0"}.zip`;
|
|
340
|
-
const zipPath = resolve(pluginRoot, zipName);
|
|
341
|
-
rmSync(zipPath, { force: true });
|
|
342
|
-
|
|
343
|
-
const entries = ["dist"];
|
|
344
|
-
|
|
345
|
-
log.step(`packing ${entries.join(", ")} → ${zipName}`);
|
|
346
|
-
try {
|
|
347
|
-
execSync(`zip -r -q "${zipPath}" ${entries.map((e) => `"${e}"`).join(" ")}`, {
|
|
348
|
-
cwd: pluginRoot,
|
|
349
|
-
stdio: "inherit",
|
|
350
|
-
});
|
|
351
|
-
} catch {
|
|
352
|
-
throw new Error("`zip` not found. Install zip or pack manually.");
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const zipBytes = statSync(zipPath).size;
|
|
356
|
-
log.ok(`${zipName} ${humanSize(zipBytes)}`);
|
|
357
|
-
for (const e of entries) {
|
|
358
|
-
const full = resolve(pluginRoot, e);
|
|
359
|
-
const size = statSync(full).isDirectory() ? dirSize(full) : statSync(full).size;
|
|
360
|
-
log.info(` ├─ ${e} ${humanSize(size)}`);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Ask which target arch(es) to build for.
|
|
366
|
-
* Non-TTY (CI): both, same as the prompt default.
|
|
367
|
-
* @returns {Promise<string[]>} e.g. ['amd64'] | ['arm64'] | ['amd64','arm64']
|
|
368
|
-
*/
|
|
369
|
-
async function promptArch(args = []) {
|
|
370
|
-
const archFlag = parseArgs(args).arch ?? null;
|
|
371
|
-
if (archFlag) {
|
|
372
|
-
if (archFlag === "both") return ["amd64", "arm64"];
|
|
373
|
-
if (archFlag === "amd64" || archFlag === "x86_64") return ["amd64"];
|
|
374
|
-
if (archFlag === "arm64" || archFlag === "aarch64") return ["arm64"];
|
|
375
|
-
throw new Error(`unknown --arch value '${archFlag}' (valid: amd64, arm64, both)`);
|
|
376
|
-
}
|
|
377
|
-
// Same default as the prompt: the target is the robot, not this machine
|
|
378
|
-
// (node's os.arch() is x64 under Rosetta on an arm64 Mac anyway).
|
|
379
|
-
if (!process.stdin.isTTY) {
|
|
380
|
-
log.info("arch auto-selected (non-TTY): both");
|
|
381
|
-
return ["amd64", "arm64"];
|
|
382
|
-
}
|
|
383
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
384
|
-
process.stdout.write(
|
|
385
|
-
"\n Target arch:\n" +
|
|
386
|
-
" 1) amd64\n" +
|
|
387
|
-
" 2) arm64\n" +
|
|
388
|
-
" 3) Both (fat ZIP)\n",
|
|
389
|
-
);
|
|
390
|
-
const answer = (await rl.question(" Select [1/2/3, default: 3]: ")).trim() || "3";
|
|
391
|
-
rl.close();
|
|
392
|
-
if (answer === "1") return ["amd64"];
|
|
393
|
-
if (answer === "2") return ["arm64"];
|
|
394
|
-
return ["amd64", "arm64"];
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async function promptVersion(root) {
|
|
398
|
-
if (!process.stdin.isTTY) return;
|
|
399
|
-
const manifestPath = resolve(root, "manifest.json");
|
|
400
|
-
if (!existsSync(manifestPath)) return;
|
|
401
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
402
|
-
const current = manifest.version ?? "1.0.0";
|
|
403
|
-
|
|
404
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
405
|
-
const input = (await rl.question(` Version (current: ${current}, leave blank to keep): `)).trim();
|
|
406
|
-
rl.close();
|
|
407
|
-
|
|
408
|
-
if (!input) return;
|
|
409
|
-
manifest.version = input;
|
|
410
|
-
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
411
|
-
log.ok(`manifest.json version → ${input}`);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
export async function runBuild(args) {
|
|
415
|
-
const noZip = args.includes("--no-zip");
|
|
416
|
-
const noLock = args.includes("--no-lock");
|
|
417
|
-
const root = findPluginRoot();
|
|
418
|
-
const rt = manifestRuntime(root);
|
|
419
|
-
|
|
420
|
-
// All runtimes except "none" need Docker
|
|
421
|
-
if (rt !== "none") ensureDocker();
|
|
422
|
-
|
|
423
|
-
await promptVersion(root);
|
|
424
|
-
const archs = await promptArch(args);
|
|
425
|
-
// Drop a previous ZIP of this version so a failed build can't leave a stale one to upload.
|
|
426
|
-
const version = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).version || "0.0.0";
|
|
427
|
-
rmSync(resolve(root, `${basename(root)}-${version}.zip`), { force: true });
|
|
428
|
-
|
|
429
|
-
const { pluginRoot, distDir } = await buildPlugin({ root, minify: true });
|
|
430
|
-
|
|
431
|
-
// python / rclpy: lock + wheel vendor in Ubuntu 24.04 container
|
|
432
|
-
if (!noLock) {
|
|
433
|
-
const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime ?? {};
|
|
434
|
-
const needsLock =
|
|
435
|
-
manifest.type === "python" ||
|
|
436
|
-
(manifest.type === "ros2" && manifest.rosClient === "rclpy");
|
|
437
|
-
if (needsLock) {
|
|
438
|
-
const { lockAndVendor } = await import("./python.mjs");
|
|
439
|
-
await lockAndVendor(pluginRoot, archs);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
await exportRos2Build(root, archs);
|
|
444
|
-
await exportCppBuild(root, distDir, archs);
|
|
445
|
-
calcInstalledSize(pluginRoot);
|
|
446
|
-
if (!noZip) packZip(pluginRoot);
|
|
447
|
-
}
|
|
1
|
+
// muscat-pk build — bundle ui/content.tsx into dist/content.js, stage
|
|
2
|
+
// manifest.json and assets/ for distribution, and zip dist/ into a
|
|
3
|
+
// distributable ZIP (skip with --no-zip).
|
|
4
|
+
//
|
|
5
|
+
// sandbox.html is generated by the CLI — plugin authors never manage it.
|
|
6
|
+
// The import map always reflects the current plugin-ui entry set, so users
|
|
7
|
+
// don't have to update it when the runtime adds new entries.
|
|
8
|
+
//
|
|
9
|
+
// The plugin author's `@rainbow-robotics/plugin-ui` and React imports are left external.
|
|
10
|
+
// Muscat/robot and preview-host provide those modules through a sandbox import
|
|
11
|
+
// map, so plugin ZIPs do not carry the UI runtime.
|
|
12
|
+
import { chmodSync, cpSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { basename, dirname, resolve } from "node:path";
|
|
15
|
+
import { createInterface } from "node:readline/promises";
|
|
16
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
17
|
+
import { ensureDocker, findPluginRoot, hasBin, humanSize, log, parseArgs, prompt } from "./util.mjs";
|
|
18
|
+
import { assertManifest, manifestRuntime } from "./manifest.mjs";
|
|
19
|
+
|
|
20
|
+
// Find a usable JS runtime for running vite as a subprocess.
|
|
21
|
+
// In binary (Bun) mode, try `bun` first (it can run vite directly), then node.
|
|
22
|
+
// In node mode, use the current process.
|
|
23
|
+
function findJsRuntime() {
|
|
24
|
+
if (typeof Bun === "undefined") return { bin: process.execPath, isBun: false };
|
|
25
|
+
for (const c of ["bun", "node", "nodejs"]) {
|
|
26
|
+
if (hasBin(c)) return { bin: c, isBun: c === "bun" };
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Resolve vite bin path from the plugin root, hoisting-aware.
|
|
32
|
+
function findViteBin(pluginRoot) {
|
|
33
|
+
try {
|
|
34
|
+
const req = createRequire(resolve(pluginRoot, "package.json"));
|
|
35
|
+
const vitePkg = req.resolve("vite/package.json");
|
|
36
|
+
return resolve(dirname(vitePkg), "bin", "vite.js");
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Run vite build in the plugin project. Without a JS runtime (binary CLI, no Node.js) it asks
|
|
43
|
+
// whether to build the plugin without its UI and returns false on yes; throws on no or if vite is missing.
|
|
44
|
+
async function runViteBuild(pluginRoot, { mode = "production" } = {}) {
|
|
45
|
+
const runtime = findJsRuntime();
|
|
46
|
+
if (!runtime) {
|
|
47
|
+
log.warn("Node.js 20+ (or Bun) not found — the UI cannot be built and will not be in the plugin.");
|
|
48
|
+
const answer = await prompt(" Build the plugin without its UI? [y/N]: ", "n");
|
|
49
|
+
if (answer.toLowerCase() !== "y")
|
|
50
|
+
throw new Error("build cancelled. Install Node.js 20+ (https://nodejs.org), run `npm install` in the plugin, then build again.");
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const viteBin = findViteBin(pluginRoot);
|
|
54
|
+
if (!viteBin || !existsSync(viteBin)) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"vite not found in this plugin's node_modules.\n" +
|
|
57
|
+
` Run: npm install (inside ${pluginRoot})`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
log.step(`vite build (mode: ${mode})`);
|
|
62
|
+
// bun can run vite directly; node needs the explicit bin path
|
|
63
|
+
const args = runtime.isBun ? ["run", viteBin, "build", "--mode", mode]
|
|
64
|
+
: [viteBin, "build", "--mode", mode];
|
|
65
|
+
const r = spawnSync(runtime.bin, args, { cwd: pluginRoot, stdio: "inherit" });
|
|
66
|
+
if (r.status !== 0) throw new Error("vite build failed");
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// function isMainBranch() {
|
|
71
|
+
// const r = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" });
|
|
72
|
+
// const branch = r.stdout?.trim();
|
|
73
|
+
// return branch === "main" || branch === "master";
|
|
74
|
+
// }
|
|
75
|
+
|
|
76
|
+
// For ros2 plugins, compile the colcon workspace inside the Jazzy + Python 3.12 container.
|
|
77
|
+
// rclpy: arch-agnostic (pure Python) → dist/ros2_ws/install/ (built once)
|
|
78
|
+
// rclcpp: arch-specific C++ binaries → dist/ros2_ws/install-{arch}/ per arch
|
|
79
|
+
// Cross-arch C++ builds run inside an emulated container. Docker Desktop ships the
|
|
80
|
+
// emulation; native Linux needs binfmt/QEMU — check up front so the error says so.
|
|
81
|
+
function assertCanBuildArch(arch, dockerPlatform) {
|
|
82
|
+
const info = spawnSync("docker", ["info", "--format", "{{.Architecture}}"], { encoding: "utf8" });
|
|
83
|
+
const host = { x86_64: "amd64", aarch64: "arm64", arm64: "arm64" }[(info.stdout || "").trim()] ?? "";
|
|
84
|
+
if (arch === host) return;
|
|
85
|
+
const r = spawnSync("docker", ["run", "--rm", "--platform", dockerPlatform, "ubuntu:24.04", "true"], { stdio: "ignore" });
|
|
86
|
+
if (r.status === 0) return;
|
|
87
|
+
throw new Error(
|
|
88
|
+
`can't build ${arch} on this ${host || "machine"}: Docker has no ${arch} emulation.\n` +
|
|
89
|
+
` enable it once: docker run --privileged --rm tonistiigi/binfmt --install ${arch}\n` +
|
|
90
|
+
` or build only this machine's arch: muscat-pk build --arch ${host || "<arch>"}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function exportRos2Build(root, archs = ["amd64", "arm64"]) {
|
|
95
|
+
if (manifestRuntime(root) !== "ros2") return;
|
|
96
|
+
const { assertDockerReady, ensureImage, colconBuild, ROS2_DEV_IMAGE, ARCH_DOCKER_PLATFORM } = await import("./ros2.mjs");
|
|
97
|
+
assertDockerReady();
|
|
98
|
+
|
|
99
|
+
const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8"));
|
|
100
|
+
if (manifest.runtime?.rosClient === "rclcpp") {
|
|
101
|
+
// C++ ROS2: compile per arch so each robot picks the right install-{arch}/
|
|
102
|
+
for (const arch of archs) {
|
|
103
|
+
const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
|
|
104
|
+
if (!dockerPlatform) throw new Error(`unknown arch '${arch}'`);
|
|
105
|
+
assertCanBuildArch(arch, dockerPlatform);
|
|
106
|
+
await ensureImage(`${ROS2_DEV_IMAGE}-${arch}`, { platform: dockerPlatform });
|
|
107
|
+
colconBuild(root, { symlink: false, arch });
|
|
108
|
+
log.ok(`dist/ros2_ws/install-${arch}/ ready`);
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// rclpy / pure Python: arch-agnostic, build once
|
|
112
|
+
await ensureImage();
|
|
113
|
+
colconBuild(root, { symlink: false });
|
|
114
|
+
log.ok("dist/ros2_ws/install/ ready");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// For cpp plugins, compile cpp/ inside the Ubuntu container (Release) per arch
|
|
119
|
+
// and copy each binary into dist/{arch}/{entry} so the ZIP carries relocatable
|
|
120
|
+
// robot-compatible executables.
|
|
121
|
+
export async function exportCppBuild(root, distDir, archs = ["amd64", "arm64"]) {
|
|
122
|
+
if (manifestRuntime(root) !== "cpp") return;
|
|
123
|
+
const { assertDockerReady, ensureImage, cmakeBuild, CPP_DEV_IMAGE, ARCH_DOCKER_PLATFORM } = await import("./cpp.mjs");
|
|
124
|
+
assertDockerReady();
|
|
125
|
+
|
|
126
|
+
const entry = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime?.entry;
|
|
127
|
+
if (!entry) throw new Error("manifest.json runtime.entry is required for cpp (the built executable name)");
|
|
128
|
+
|
|
129
|
+
for (const arch of archs) {
|
|
130
|
+
const dockerPlatform = ARCH_DOCKER_PLATFORM[arch];
|
|
131
|
+
if (!dockerPlatform) throw new Error(`unknown arch '${arch}'`);
|
|
132
|
+
assertCanBuildArch(arch, dockerPlatform);
|
|
133
|
+
await ensureImage(`${CPP_DEV_IMAGE}-${arch}`, { platform: dockerPlatform });
|
|
134
|
+
cmakeBuild(root, { buildType: "Release", arch });
|
|
135
|
+
const built = resolve(root, `cpp/build-${arch}`, entry);
|
|
136
|
+
if (!existsSync(built)) {
|
|
137
|
+
throw new Error(`cpp/build-${arch}/${entry} not found — the CMake target name must equal runtime.entry`);
|
|
138
|
+
}
|
|
139
|
+
const archDir = resolve(distDir, arch);
|
|
140
|
+
mkdirSync(archDir, { recursive: true });
|
|
141
|
+
const dest = resolve(archDir, entry);
|
|
142
|
+
copyFileSync(built, dest);
|
|
143
|
+
chmodSync(dest, 0o755);
|
|
144
|
+
const libDir = resolve(root, `cpp/build-${arch}`, "lib");
|
|
145
|
+
if (existsSync(libDir)) cpSync(libDir, resolve(archDir, "lib"), { recursive: true, verbatimSymlinks: true });
|
|
146
|
+
log.ok(`cpp binary → dist/${arch}/${entry}${existsSync(libDir) ? " (+ lib/)" : ""}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const PRERELEASE_RE = /\.(dev|a|b|rc|alpha|beta)\d*/i;
|
|
151
|
+
|
|
152
|
+
function isPrerelease(version) {
|
|
153
|
+
return PRERELEASE_RE.test(version);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Resolve the latest version of a PyPI package.
|
|
157
|
+
// stableOnly=true → skips pre-release (dev/a/b/rc) versions.
|
|
158
|
+
async function latestPypiVersion(pkg, { stableOnly = false } = {}) {
|
|
159
|
+
const res = await fetch(`https://pypi.org/pypi/${pkg}/json`, {
|
|
160
|
+
signal: new AbortController().signal.timeout(5000),
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) throw new Error(`pypi ${pkg}: ${res.status}`);
|
|
163
|
+
const data = await res.json();
|
|
164
|
+
|
|
165
|
+
if (!stableOnly) return data.info.version;
|
|
166
|
+
|
|
167
|
+
// data.info.version is latest stable when stable releases exist.
|
|
168
|
+
// If it's still a pre-release, scan all releases for the newest stable.
|
|
169
|
+
if (!isPrerelease(data.info.version)) return data.info.version;
|
|
170
|
+
|
|
171
|
+
const stable = Object.keys(data.releases)
|
|
172
|
+
.filter((v) => !isPrerelease(v))
|
|
173
|
+
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
174
|
+
.at(-1);
|
|
175
|
+
|
|
176
|
+
if (stable) return stable;
|
|
177
|
+
|
|
178
|
+
// No stable release exists — fall back with a warning.
|
|
179
|
+
log.warn(` ${pkg}: no stable release found, using ${data.info.version}`);
|
|
180
|
+
return data.info.version;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Pin unpinned (no version specifier) entries in requirements.txt to the
|
|
184
|
+
// latest PyPI release. Runs on explicit `build` only — keeps the shipped
|
|
185
|
+
// plugin reproducible without churning the source file on every dev rebuild.
|
|
186
|
+
// stableOnly=true is used on main-branch builds to avoid shipping dev versions.
|
|
187
|
+
export async function pinRequirements(root, { stableOnly = false } = {}) {
|
|
188
|
+
const reqPath = resolve(root, "requirements.txt");
|
|
189
|
+
if (!existsSync(reqPath)) return;
|
|
190
|
+
|
|
191
|
+
const lines = readFileSync(reqPath, "utf8").split("\n");
|
|
192
|
+
const out = [];
|
|
193
|
+
let pinned = 0;
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
const name = line.trim();
|
|
196
|
+
// skip blanks, comments, and already-specified versions
|
|
197
|
+
if (!name || name.startsWith("#") || /[=<>~!]/.test(name)) {
|
|
198
|
+
out.push(line);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const v = await latestPypiVersion(name, { stableOnly });
|
|
203
|
+
out.push(`${name}==${v}`);
|
|
204
|
+
pinned++;
|
|
205
|
+
log.info(` pinned ${name}==${v}`);
|
|
206
|
+
} catch {
|
|
207
|
+
out.push(line);
|
|
208
|
+
log.warn(` could not resolve ${name} (offline?) — left unpinned`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (pinned > 0) writeFileSync(reqPath, out.join("\n"));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function buildPlugin({ root, minify = false } = {}) {
|
|
215
|
+
const pluginRoot = root ?? findPluginRoot();
|
|
216
|
+
assertManifest(pluginRoot);
|
|
217
|
+
const distDir = resolve(pluginRoot, "dist");
|
|
218
|
+
try {
|
|
219
|
+
rmSync(distDir, { recursive: true, force: true });
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err?.code !== "EACCES" && err?.code !== "EPERM") throw err;
|
|
222
|
+
// 이전 버전이 컨테이너를 root로 돌려 남긴 dist/ — 한 번만 정리하면 된다
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Permission denied when deleting dist/ (${err.path ?? distDir}).\n` +
|
|
225
|
+
`The previous build left files owned by root. Please clean it up once:\n` +
|
|
226
|
+
` sudo rm -rf ${distDir}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
mkdirSync(distDir, { recursive: true });
|
|
230
|
+
|
|
231
|
+
const manifest = JSON.parse(readFileSync(resolve(pluginRoot, "manifest.json"), "utf8"));
|
|
232
|
+
const uiDir = resolve(pluginRoot, "ui");
|
|
233
|
+
const declaredUiEntries = [...new Set(
|
|
234
|
+
(manifest.contributes ?? []).map((c) => c.ui).filter(Boolean)
|
|
235
|
+
)];
|
|
236
|
+
// Validate each declared UI entry has a matching source file
|
|
237
|
+
for (const entry of declaredUiEntries) {
|
|
238
|
+
const exists = [".tsx", ".ts", ".jsx", ".js"].some(
|
|
239
|
+
(ext) => existsSync(resolve(uiDir, entry + ext))
|
|
240
|
+
);
|
|
241
|
+
if (!exists)
|
|
242
|
+
throw new Error(
|
|
243
|
+
`contributes declares ui "${entry}" but ui/${entry}.tsx (or .ts/.jsx/.js) does not exist.`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
const hasUiFiles = existsSync(uiDir) && readdirSync(uiDir).some((f) => /\.[tj]sx?$/.test(f));
|
|
247
|
+
// A backend-only plugin (no contributes) ships no UI, so leftover ui/ files are not built either.
|
|
248
|
+
let uiBuilt = true;
|
|
249
|
+
if (declaredUiEntries.length > 0 || (hasUiFiles && manifest.contributes?.length > 0)) {
|
|
250
|
+
uiBuilt = await runViteBuild(pluginRoot, { mode: minify ? "production" : "development" });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Stage distribution files.
|
|
254
|
+
const manifestSrc = resolve(pluginRoot, "manifest.json");
|
|
255
|
+
if (existsSync(manifestSrc)) copyFileSync(manifestSrc, resolve(distDir, "manifest.json"));
|
|
256
|
+
// Built without the UI: ship it as a backend-only plugin so the host lists no screen it cannot load.
|
|
257
|
+
if (!uiBuilt) {
|
|
258
|
+
const { contributes, ...backendOnly } = manifest;
|
|
259
|
+
writeFileSync(resolve(distDir, "manifest.json"), JSON.stringify(backendOnly, null, 2) + "\n");
|
|
260
|
+
log.warn(`packaged without UI: dropped ${contributes.length} screen area(s) from dist/manifest.json`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// No sandbox.html: the host dynamically imports dist/{ui}.js directly as a
|
|
264
|
+
// plain ESM module (see @rainbow-robotics/plugin-ui/host) — no iframe.
|
|
265
|
+
|
|
266
|
+
const assetsSrc = resolve(pluginRoot, "assets");
|
|
267
|
+
if (existsSync(assetsSrc)) {
|
|
268
|
+
cpSync(assetsSrc, resolve(distDir, "assets"), { recursive: true });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// README.md, README.[lang].md, CHANGELOG.md → dist/
|
|
272
|
+
for (const f of readdirSync(pluginRoot)) {
|
|
273
|
+
if (/^README(\.[a-z]{2}(-[A-Z]{2})?)?\.md$/i.test(f) || /^CHANGELOG\.md$/i.test(f)) {
|
|
274
|
+
copyFileSync(resolve(pluginRoot, f), resolve(distDir, f));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Backend runtime files — copy entire py/ into dist/, then requirements.txt from root.
|
|
279
|
+
const pySrc = resolve(pluginRoot, "py");
|
|
280
|
+
if (existsSync(pySrc)) {
|
|
281
|
+
cpSync(pySrc, distDir, { recursive: true });
|
|
282
|
+
}
|
|
283
|
+
const rootReqs = resolve(pluginRoot, "requirements.txt");
|
|
284
|
+
if (existsSync(rootReqs)) {
|
|
285
|
+
copyFileSync(rootReqs, resolve(distDir, "requirements.txt"));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
log.ok(`built → ${distDir}`);
|
|
289
|
+
return { pluginRoot, distDir, uiBuilt };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function dirSize(dir, skip = []) {
|
|
293
|
+
if (!existsSync(dir)) return 0;
|
|
294
|
+
let total = 0;
|
|
295
|
+
for (const entry of readdirSync(dir)) {
|
|
296
|
+
if (skip.includes(entry)) continue;
|
|
297
|
+
const full = resolve(dir, entry);
|
|
298
|
+
const st = statSync(full);
|
|
299
|
+
total += st.isDirectory() ? dirSize(full) : st.size;
|
|
300
|
+
}
|
|
301
|
+
return total;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Read the uncompressed byte total of a .whl (zip) file by parsing its central
|
|
305
|
+
// directory — no external library needed, no actual extraction.
|
|
306
|
+
|
|
307
|
+
// installedSize = "이 플러그인 하나를 지우면 되찾는 용량"
|
|
308
|
+
// = dist/(wheels/ 제외)
|
|
309
|
+
// ros2_ws/install* 은 이제 dist/ 안에 있으므로 별도 합산 불필요.
|
|
310
|
+
// 휠은 로봇 공유 스토어(store/py/)에 언팩되므로 포함하지 않는다.
|
|
311
|
+
function calcInstalledSize(pluginRoot) {
|
|
312
|
+
const distDir = resolve(pluginRoot, "dist");
|
|
313
|
+
|
|
314
|
+
const installedSize = dirSize(distDir, ["wheels"]);
|
|
315
|
+
|
|
316
|
+
// Write to root manifest.json
|
|
317
|
+
const rootManifestPath = resolve(pluginRoot, "manifest.json");
|
|
318
|
+
if (existsSync(rootManifestPath)) {
|
|
319
|
+
const m = JSON.parse(readFileSync(rootManifestPath, "utf8"));
|
|
320
|
+
m.installedSize = installedSize;
|
|
321
|
+
writeFileSync(rootManifestPath, JSON.stringify(m, null, 2) + "\n");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Re-stage dist/manifest.json so the ZIP carries the updated value
|
|
325
|
+
const distManifestPath = resolve(distDir, "manifest.json");
|
|
326
|
+
if (existsSync(distManifestPath)) {
|
|
327
|
+
const m = JSON.parse(readFileSync(distManifestPath, "utf8"));
|
|
328
|
+
m.installedSize = installedSize;
|
|
329
|
+
writeFileSync(distManifestPath, JSON.stringify(m, null, 2) + "\n");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
log.ok(`installedSize → ${humanSize(installedSize)}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Zip dist/ into a distributable ZIP with a size report.
|
|
336
|
+
// ros2_ws/install* is now inside dist/, so only "dist" is needed.
|
|
337
|
+
function packZip(pluginRoot) {
|
|
338
|
+
const manifest = JSON.parse(readFileSync(resolve(pluginRoot, "manifest.json"), "utf8"));
|
|
339
|
+
const zipName = `${basename(pluginRoot)}-${manifest.version || "0.0.0"}.zip`;
|
|
340
|
+
const zipPath = resolve(pluginRoot, zipName);
|
|
341
|
+
rmSync(zipPath, { force: true });
|
|
342
|
+
|
|
343
|
+
const entries = ["dist"];
|
|
344
|
+
|
|
345
|
+
log.step(`packing ${entries.join(", ")} → ${zipName}`);
|
|
346
|
+
try {
|
|
347
|
+
execSync(`zip -r -q "${zipPath}" ${entries.map((e) => `"${e}"`).join(" ")}`, {
|
|
348
|
+
cwd: pluginRoot,
|
|
349
|
+
stdio: "inherit",
|
|
350
|
+
});
|
|
351
|
+
} catch {
|
|
352
|
+
throw new Error("`zip` not found. Install zip or pack manually.");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const zipBytes = statSync(zipPath).size;
|
|
356
|
+
log.ok(`${zipName} ${humanSize(zipBytes)}`);
|
|
357
|
+
for (const e of entries) {
|
|
358
|
+
const full = resolve(pluginRoot, e);
|
|
359
|
+
const size = statSync(full).isDirectory() ? dirSize(full) : statSync(full).size;
|
|
360
|
+
log.info(` ├─ ${e} ${humanSize(size)}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Ask which target arch(es) to build for.
|
|
366
|
+
* Non-TTY (CI): both, same as the prompt default.
|
|
367
|
+
* @returns {Promise<string[]>} e.g. ['amd64'] | ['arm64'] | ['amd64','arm64']
|
|
368
|
+
*/
|
|
369
|
+
async function promptArch(args = []) {
|
|
370
|
+
const archFlag = parseArgs(args).arch ?? null;
|
|
371
|
+
if (archFlag) {
|
|
372
|
+
if (archFlag === "both") return ["amd64", "arm64"];
|
|
373
|
+
if (archFlag === "amd64" || archFlag === "x86_64") return ["amd64"];
|
|
374
|
+
if (archFlag === "arm64" || archFlag === "aarch64") return ["arm64"];
|
|
375
|
+
throw new Error(`unknown --arch value '${archFlag}' (valid: amd64, arm64, both)`);
|
|
376
|
+
}
|
|
377
|
+
// Same default as the prompt: the target is the robot, not this machine
|
|
378
|
+
// (node's os.arch() is x64 under Rosetta on an arm64 Mac anyway).
|
|
379
|
+
if (!process.stdin.isTTY) {
|
|
380
|
+
log.info("arch auto-selected (non-TTY): both");
|
|
381
|
+
return ["amd64", "arm64"];
|
|
382
|
+
}
|
|
383
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
384
|
+
process.stdout.write(
|
|
385
|
+
"\n Target arch:\n" +
|
|
386
|
+
" 1) amd64\n" +
|
|
387
|
+
" 2) arm64\n" +
|
|
388
|
+
" 3) Both (fat ZIP)\n",
|
|
389
|
+
);
|
|
390
|
+
const answer = (await rl.question(" Select [1/2/3, default: 3]: ")).trim() || "3";
|
|
391
|
+
rl.close();
|
|
392
|
+
if (answer === "1") return ["amd64"];
|
|
393
|
+
if (answer === "2") return ["arm64"];
|
|
394
|
+
return ["amd64", "arm64"];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function promptVersion(root) {
|
|
398
|
+
if (!process.stdin.isTTY) return;
|
|
399
|
+
const manifestPath = resolve(root, "manifest.json");
|
|
400
|
+
if (!existsSync(manifestPath)) return;
|
|
401
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
402
|
+
const current = manifest.version ?? "1.0.0";
|
|
403
|
+
|
|
404
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
405
|
+
const input = (await rl.question(` Version (current: ${current}, leave blank to keep): `)).trim();
|
|
406
|
+
rl.close();
|
|
407
|
+
|
|
408
|
+
if (!input) return;
|
|
409
|
+
manifest.version = input;
|
|
410
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
411
|
+
log.ok(`manifest.json version → ${input}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function runBuild(args) {
|
|
415
|
+
const noZip = args.includes("--no-zip");
|
|
416
|
+
const noLock = args.includes("--no-lock");
|
|
417
|
+
const root = findPluginRoot();
|
|
418
|
+
const rt = manifestRuntime(root);
|
|
419
|
+
|
|
420
|
+
// All runtimes except "none" need Docker
|
|
421
|
+
if (rt !== "none") ensureDocker();
|
|
422
|
+
|
|
423
|
+
await promptVersion(root);
|
|
424
|
+
const archs = await promptArch(args);
|
|
425
|
+
// Drop a previous ZIP of this version so a failed build can't leave a stale one to upload.
|
|
426
|
+
const version = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).version || "0.0.0";
|
|
427
|
+
rmSync(resolve(root, `${basename(root)}-${version}.zip`), { force: true });
|
|
428
|
+
|
|
429
|
+
const { pluginRoot, distDir } = await buildPlugin({ root, minify: true });
|
|
430
|
+
|
|
431
|
+
// python / rclpy: lock + wheel vendor in Ubuntu 24.04 container
|
|
432
|
+
if (!noLock) {
|
|
433
|
+
const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8")).runtime ?? {};
|
|
434
|
+
const needsLock =
|
|
435
|
+
manifest.type === "python" ||
|
|
436
|
+
(manifest.type === "ros2" && manifest.rosClient === "rclpy");
|
|
437
|
+
if (needsLock) {
|
|
438
|
+
const { lockAndVendor } = await import("./python.mjs");
|
|
439
|
+
await lockAndVendor(pluginRoot, archs);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
await exportRos2Build(root, archs);
|
|
444
|
+
await exportCppBuild(root, distDir, archs);
|
|
445
|
+
calcInstalledSize(pluginRoot);
|
|
446
|
+
if (!noZip) packZip(pluginRoot);
|
|
447
|
+
}
|