@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/manifest.mjs
CHANGED
|
@@ -1,287 +1,287 @@
|
|
|
1
|
-
// manifest.mjs — manifest.json schema definition and validation
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { manifestPortRanges, validatePorts } from "./ports.mjs";
|
|
5
|
-
|
|
6
|
-
// 호스트가 이해하는 contribution point. plugin-ui의 CONTRIBUTION_POINTS와 동일해야 한다.
|
|
7
|
-
export const CONTRIBUTION_POINTS = [
|
|
8
|
-
"toolbar",
|
|
9
|
-
"lnb",
|
|
10
|
-
"move.panel",
|
|
11
|
-
"program.panel",
|
|
12
|
-
];
|
|
13
|
-
const ICON_SIZES = ["16", "48", "128"];
|
|
14
|
-
// Reverse-domain id: "."-separated parts of lowercase letters/digits joined by single "-",
|
|
15
|
-
// each part starting with a letter. Keeps the ROS namespace derived from it (rosNamespace)
|
|
16
|
-
// valid and collision-free (no "_" in ids, so "." and "-" map to distinct characters).
|
|
17
|
-
export const PLUGIN_ID_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)*$/;
|
|
18
|
-
|
|
19
|
-
/** ROS namespace for a plugin id: "." → "/", "-" → "_" (com.example.my-plugin → /com/example/my_plugin). */
|
|
20
|
-
export function rosNamespace(id) {
|
|
21
|
-
return "/" + id.replace(/\./g, "/").replace(/-/g, "_");
|
|
22
|
-
}
|
|
23
|
-
const RUNTIME_TYPES = ["none", "python", "cpp", "ros2"];
|
|
24
|
-
const ROS_CLIENTS = ["rclpy", "rclcpp"];
|
|
25
|
-
const I18N_LANGS = ["ko", "en", "ja", "zh", "vi", "th", "es"];
|
|
26
|
-
const TARGET_CATEGORIES = ["manipulate", "amr", "rby1", "vision"];
|
|
27
|
-
const VARIABLE_TYPES = ["number", "string", "boolean", "array"];
|
|
28
|
-
const VARIABLE_NAME_PREFIX = "RBP_";
|
|
29
|
-
const VARIABLE_NAME_RE = /^RBP_[A-Za-z0-9_]+$/;
|
|
30
|
-
|
|
31
|
-
function validateI18nLabel(value, field, errors) {
|
|
32
|
-
if (value === undefined || value === null) {
|
|
33
|
-
errors.push(`${field} is required`);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
if (typeof value === "string") return;
|
|
37
|
-
if (typeof value === "object") {
|
|
38
|
-
if (typeof value.en !== "string")
|
|
39
|
-
errors.push(`${field}: "en" is required when using object form`);
|
|
40
|
-
const unknown = Object.keys(value).filter((k) => !I18N_LANGS.includes(k));
|
|
41
|
-
if (unknown.length > 0)
|
|
42
|
-
errors.push(`${field}: unsupported language key(s) — ${unknown.map((k) => `"${k}"`).join(", ")} (allowed: ${I18N_LANGS.join(", ")})`);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
errors.push(`${field}: must be a string or { en (required), ko, ja, zh, vi, th, es }`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function readManifest(root) {
|
|
49
|
-
const path = resolve(root, "manifest.json");
|
|
50
|
-
if (!existsSync(path)) return {};
|
|
51
|
-
try {
|
|
52
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
53
|
-
} catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function manifestRuntime(root) {
|
|
59
|
-
return readManifest(root)?.runtime?.type ?? "python";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function validateIcons(icons, errors) {
|
|
63
|
-
if (icons === undefined || icons === null) {
|
|
64
|
-
errors.push("icons is required");
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (typeof icons === "string") {
|
|
68
|
-
if (!icons.toLowerCase().endsWith(".svg"))
|
|
69
|
-
errors.push(`icons: non-SVG images (PNG, JPG, etc.) must use the { "16": "...", "48": "...", "128": "..." } object form`);
|
|
70
|
-
} else if (typeof icons === "object") {
|
|
71
|
-
const missing = ICON_SIZES.filter((s) => typeof icons[s] !== "string" || icons[s].length === 0);
|
|
72
|
-
if (missing.length > 0)
|
|
73
|
-
errors.push(`icons: missing size(s) ${missing.map((s) => `"${s}"`).join(", ")} — all three (16, 48, 128) are required for image files`);
|
|
74
|
-
} else {
|
|
75
|
-
errors.push(`icons: must be a string (SVG path) or { "16", "48", "128" } object (PNG/JPG)`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function validateRuntime(runtime, errors) {
|
|
80
|
-
if (runtime === undefined || runtime === null) {
|
|
81
|
-
errors.push("runtime is required");
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (typeof runtime !== "object") {
|
|
85
|
-
errors.push("runtime must be an object");
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (!RUNTIME_TYPES.includes(runtime.type))
|
|
89
|
-
errors.push(`runtime.type must be one of ${RUNTIME_TYPES.map((t) => `"${t}"`).join(" | ")}`);
|
|
90
|
-
if (runtime.type === "python" && !runtime.entry)
|
|
91
|
-
errors.push("runtime.entry is required for python runtime");
|
|
92
|
-
if (runtime.type === "cpp" && !runtime.entry)
|
|
93
|
-
errors.push("runtime.entry is required for cpp runtime (the built executable name)");
|
|
94
|
-
if (runtime.type === "ros2") {
|
|
95
|
-
if (!runtime.package) errors.push("runtime.package is required for ros2 runtime");
|
|
96
|
-
if (!runtime.launch) errors.push("runtime.launch is required for ros2 runtime");
|
|
97
|
-
if (!ROS_CLIENTS.includes(runtime.rosClient))
|
|
98
|
-
errors.push(`runtime.rosClient must be one of ${ROS_CLIENTS.map((c) => `"${c}"`).join(" | ")}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function validateCommandInfo(info, prefix, errors) {
|
|
103
|
-
if (typeof info.name !== "string" || info.name.length === 0)
|
|
104
|
-
errors.push(`${prefix}.commandInfo.name is required`);
|
|
105
|
-
if (typeof info.icon !== "string" || info.icon.length === 0)
|
|
106
|
-
errors.push(`${prefix}.commandInfo.icon is required`);
|
|
107
|
-
if (info.iconClassName !== undefined && typeof info.iconClassName !== "string")
|
|
108
|
-
errors.push(`${prefix}.commandInfo.iconClassName must be a string`);
|
|
109
|
-
if (info.funcName !== undefined && typeof info.funcName !== "string")
|
|
110
|
-
errors.push(`${prefix}.commandInfo.funcName must be a string`);
|
|
111
|
-
if (info.args !== undefined && (typeof info.args !== "object" || Array.isArray(info.args)))
|
|
112
|
-
errors.push(`${prefix}.commandInfo.args must be an object`);
|
|
113
|
-
for (const key of ["disabled", "syncGroupId", "disableCopy", "disableDelete", "requireChildren", "requireParent"]) {
|
|
114
|
-
if (info[key] !== undefined && typeof info[key] !== "boolean")
|
|
115
|
-
errors.push(`${prefix}.commandInfo.${key} must be a boolean`);
|
|
116
|
-
}
|
|
117
|
-
for (const key of ["allowChildren", "allowParents", "notAllowChildren"]) {
|
|
118
|
-
if (info[key] !== undefined && (!Array.isArray(info[key]) || info[key].some((v) => typeof v !== "string")))
|
|
119
|
-
errors.push(`${prefix}.commandInfo.${key} must be a string[]`);
|
|
120
|
-
}
|
|
121
|
-
if (info.childrenCommand !== undefined && typeof info.childrenCommand !== "string")
|
|
122
|
-
errors.push(`${prefix}.commandInfo.childrenCommand must be a string`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// variables: pyfm에 노출할 변수 선언. 없으면 UI가 사용 가능한 변수를 보여줄 수 없다.
|
|
126
|
-
function validateVariables(variables, errors) {
|
|
127
|
-
if (variables === undefined || variables === null) return;
|
|
128
|
-
if (!Array.isArray(variables)) {
|
|
129
|
-
errors.push(`"variables" must be an array`);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
const names = variables.map((v) => v?.name).filter(Boolean);
|
|
133
|
-
const duplicates = names.filter((n, i) => names.indexOf(n) !== i);
|
|
134
|
-
if (duplicates.length > 0)
|
|
135
|
-
errors.push(`variables: "name" must be unique (duplicates: ${[...new Set(duplicates)].map((n) => `"${n}"`).join(", ")})`);
|
|
136
|
-
|
|
137
|
-
variables.forEach((v, i) => {
|
|
138
|
-
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
139
|
-
errors.push(`variables[${i}] must be an object ({ name, type, init, description? })`);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (typeof v.name !== "string" || v.name.length === 0)
|
|
143
|
-
errors.push(`variables[${i}].name is required`);
|
|
144
|
-
else if (!v.name.startsWith(VARIABLE_NAME_PREFIX))
|
|
145
|
-
errors.push(`variables[${i}].name: "${v.name}" must start with "${VARIABLE_NAME_PREFIX}" (e.g. "${VARIABLE_NAME_PREFIX}${v.name}")`);
|
|
146
|
-
else if (!VARIABLE_NAME_RE.test(v.name))
|
|
147
|
-
errors.push(`variables[${i}].name: "${v.name}" is not a valid identifier (after "${VARIABLE_NAME_PREFIX}": letters, digits, underscore only, at least one character)`);
|
|
148
|
-
if (!VARIABLE_TYPES.includes(v.type))
|
|
149
|
-
errors.push(`variables[${i}].type must be one of ${VARIABLE_TYPES.map((t) => `"${t}"`).join(" | ")}`);
|
|
150
|
-
if (v.init === undefined)
|
|
151
|
-
errors.push(`variables[${i}].init is required (initial value)`);
|
|
152
|
-
else if (v.type === "number" && typeof v.init !== "number")
|
|
153
|
-
errors.push(`variables[${i}].init must be a number when type is "number"`);
|
|
154
|
-
else if (v.type === "string" && typeof v.init !== "string")
|
|
155
|
-
errors.push(`variables[${i}].init must be a string when type is "string"`);
|
|
156
|
-
else if (v.type === "boolean" && typeof v.init !== "boolean")
|
|
157
|
-
errors.push(`variables[${i}].init must be a boolean when type is "boolean"`);
|
|
158
|
-
else if (v.type === "array" && !Array.isArray(v.init))
|
|
159
|
-
errors.push(`variables[${i}].init must be an array when type is "array"`);
|
|
160
|
-
if (v.description !== undefined && v.description !== null)
|
|
161
|
-
validateI18nLabel(v.description, `variables[${i}].description`, errors);
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// No contributes (missing or []) = a backend-only plugin: no screens, nothing for vite to build.
|
|
166
|
-
// A plugin with screens still needs a toolbar entry — that is where the host lists it.
|
|
167
|
-
function validateContributes(contributes, errors) {
|
|
168
|
-
if (contributes === undefined || contributes === null) return;
|
|
169
|
-
if (!Array.isArray(contributes)) {
|
|
170
|
-
errors.push("contributes must be an array");
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (contributes.length === 0) return;
|
|
174
|
-
if (!contributes.some((c) => c.point === "toolbar"))
|
|
175
|
-
errors.push(`contributes: at least one "toolbar" entry is required`);
|
|
176
|
-
|
|
177
|
-
const uiValues = contributes.map((c) => c.ui).filter(Boolean);
|
|
178
|
-
const duplicateUi = uiValues.filter((v, i) => uiValues.indexOf(v) !== i);
|
|
179
|
-
if (duplicateUi.length > 0)
|
|
180
|
-
errors.push(`contributes: "ui" values must be unique across all entries (duplicates: ${[...new Set(duplicateUi)].map((v) => `"${v}"`).join(", ")})`);
|
|
181
|
-
|
|
182
|
-
contributes.forEach((c, i) => {
|
|
183
|
-
if (!CONTRIBUTION_POINTS.includes(c.point))
|
|
184
|
-
errors.push(`contributes[${i}].point: unknown point ${JSON.stringify(c.point)} (allowed: ${CONTRIBUTION_POINTS.join(", ")})`);
|
|
185
|
-
validateI18nLabel(c.label, `contributes[${i}].label`, errors);
|
|
186
|
-
if (c.point === "program.panel") {
|
|
187
|
-
if (!c.commandInfo)
|
|
188
|
-
errors.push(`contributes[${i}]: "commandInfo" is required when point is "program.panel"`);
|
|
189
|
-
else
|
|
190
|
-
validateCommandInfo(c.commandInfo, `contributes[${i}]`, errors);
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Validate manifest.json. Returns an array of error messages (empty = valid).
|
|
197
|
-
*/
|
|
198
|
-
export function validateManifest(manifest) {
|
|
199
|
-
const errors = [];
|
|
200
|
-
|
|
201
|
-
if (typeof manifest !== "object" || manifest === null)
|
|
202
|
-
return ["manifest.json is not a valid JSON object"];
|
|
203
|
-
|
|
204
|
-
for (const key of ["id", "name", "version", "minMuscatVersion"]) {
|
|
205
|
-
if (typeof manifest[key] !== "string" || manifest[key].length === 0)
|
|
206
|
-
errors.push(`"${key}" is required`);
|
|
207
|
-
}
|
|
208
|
-
if (typeof manifest.id === "string" && manifest.id && !PLUGIN_ID_RE.test(manifest.id))
|
|
209
|
-
errors.push(`"id" must be "."-separated parts of lowercase letters/digits and single "-", each starting with a letter (e.g. com.example.my-plugin)`);
|
|
210
|
-
if (typeof manifest.author !== "string" || manifest.author.length === 0)
|
|
211
|
-
errors.push(`"author" is required`);
|
|
212
|
-
if (typeof manifest.email !== "string" || manifest.email.length === 0)
|
|
213
|
-
errors.push(`"email" is required`);
|
|
214
|
-
if (manifest.company !== undefined && manifest.company !== null && typeof manifest.company !== "string")
|
|
215
|
-
errors.push(`"company" must be a string`);
|
|
216
|
-
|
|
217
|
-
const hasModels = manifest.targetModels !== undefined && manifest.targetModels !== null;
|
|
218
|
-
const hasCategorys = manifest.targetCategorys !== undefined && manifest.targetCategorys !== null;
|
|
219
|
-
if (!hasModels && !hasCategorys) {
|
|
220
|
-
errors.push(`"targetModels" or "targetCategorys" is required`);
|
|
221
|
-
} else {
|
|
222
|
-
if (hasModels) {
|
|
223
|
-
if (manifest.targetModels !== "all" && (!Array.isArray(manifest.targetModels) || manifest.targetModels.length === 0))
|
|
224
|
-
errors.push(`"targetModels" must be "all" or a non-empty array of model strings`);
|
|
225
|
-
else if (Array.isArray(manifest.targetModels) && manifest.targetModels.some((m) => typeof m !== "string"))
|
|
226
|
-
errors.push(`"targetModels": all entries must be strings`);
|
|
227
|
-
}
|
|
228
|
-
if (hasCategorys) {
|
|
229
|
-
if (manifest.targetCategorys !== "all" && (!Array.isArray(manifest.targetCategorys) || manifest.targetCategorys.length === 0))
|
|
230
|
-
errors.push(`"targetCategorys" must be "all" or a non-empty array`);
|
|
231
|
-
else if (Array.isArray(manifest.targetCategorys)) {
|
|
232
|
-
const invalid = manifest.targetCategorys.filter((c) => !TARGET_CATEGORIES.includes(c));
|
|
233
|
-
if (invalid.length > 0)
|
|
234
|
-
errors.push(`"targetCategorys": invalid value(s) ${invalid.map((c) => `"${c}"`).join(", ")} — allowed: ${TARGET_CATEGORIES.join(", ")}`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
validateI18nLabel(manifest.description, "description", errors);
|
|
239
|
-
|
|
240
|
-
validateIcons(manifest.icons, errors);
|
|
241
|
-
validatePorts(manifest.ports, errors);
|
|
242
|
-
validateRuntime(manifest.runtime, errors);
|
|
243
|
-
validateContributes(manifest.contributes, errors);
|
|
244
|
-
validateVariables(manifest.variables, errors);
|
|
245
|
-
|
|
246
|
-
return errors;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Read and validate manifest.json. Throws if any errors are found.
|
|
251
|
-
*/
|
|
252
|
-
export function assertManifest(root) {
|
|
253
|
-
const manifest = readManifest(root);
|
|
254
|
-
if (manifest === null)
|
|
255
|
-
throw new Error("manifest.json parse failed — check JSON syntax");
|
|
256
|
-
if (Object.keys(manifest).length === 0)
|
|
257
|
-
return manifest;
|
|
258
|
-
const errors = validateManifest(manifest);
|
|
259
|
-
if (errors.length > 0)
|
|
260
|
-
throw new Error(`manifest.json errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
|
|
261
|
-
return manifest;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export function assertManifestVariables(root) {
|
|
265
|
-
const manifest = readManifest(root);
|
|
266
|
-
if (manifest === null)
|
|
267
|
-
throw new Error("manifest.json parse failed — check JSON syntax");
|
|
268
|
-
if (Object.keys(manifest).length === 0)
|
|
269
|
-
return;
|
|
270
|
-
const errors = [];
|
|
271
|
-
validateVariables(manifest.variables, errors);
|
|
272
|
-
if (errors.length > 0)
|
|
273
|
-
throw new Error(`manifest.json variable errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export function assertManifestPorts(root) {
|
|
277
|
-
const manifest = readManifest(root);
|
|
278
|
-
if (manifest === null)
|
|
279
|
-
throw new Error("manifest.json parse failed — check JSON syntax");
|
|
280
|
-
if (Object.keys(manifest).length === 0)
|
|
281
|
-
return [];
|
|
282
|
-
const errors = [];
|
|
283
|
-
validatePorts(manifest.ports, errors);
|
|
284
|
-
if (errors.length > 0)
|
|
285
|
-
throw new Error(`manifest.json port errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
|
|
286
|
-
return manifestPortRanges(manifest);
|
|
287
|
-
}
|
|
1
|
+
// manifest.mjs — manifest.json schema definition and validation
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { manifestPortRanges, validatePorts } from "./ports.mjs";
|
|
5
|
+
|
|
6
|
+
// 호스트가 이해하는 contribution point. plugin-ui의 CONTRIBUTION_POINTS와 동일해야 한다.
|
|
7
|
+
export const CONTRIBUTION_POINTS = [
|
|
8
|
+
"toolbar",
|
|
9
|
+
"lnb",
|
|
10
|
+
"move.panel",
|
|
11
|
+
"program.panel",
|
|
12
|
+
];
|
|
13
|
+
const ICON_SIZES = ["16", "48", "128"];
|
|
14
|
+
// Reverse-domain id: "."-separated parts of lowercase letters/digits joined by single "-",
|
|
15
|
+
// each part starting with a letter. Keeps the ROS namespace derived from it (rosNamespace)
|
|
16
|
+
// valid and collision-free (no "_" in ids, so "." and "-" map to distinct characters).
|
|
17
|
+
export const PLUGIN_ID_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)*$/;
|
|
18
|
+
|
|
19
|
+
/** ROS namespace for a plugin id: "." → "/", "-" → "_" (com.example.my-plugin → /com/example/my_plugin). */
|
|
20
|
+
export function rosNamespace(id) {
|
|
21
|
+
return "/" + id.replace(/\./g, "/").replace(/-/g, "_");
|
|
22
|
+
}
|
|
23
|
+
const RUNTIME_TYPES = ["none", "python", "cpp", "ros2"];
|
|
24
|
+
const ROS_CLIENTS = ["rclpy", "rclcpp"];
|
|
25
|
+
const I18N_LANGS = ["ko", "en", "ja", "zh", "vi", "th", "es"];
|
|
26
|
+
const TARGET_CATEGORIES = ["manipulate", "amr", "rby1", "vision"];
|
|
27
|
+
const VARIABLE_TYPES = ["number", "string", "boolean", "array"];
|
|
28
|
+
const VARIABLE_NAME_PREFIX = "RBP_";
|
|
29
|
+
const VARIABLE_NAME_RE = /^RBP_[A-Za-z0-9_]+$/;
|
|
30
|
+
|
|
31
|
+
function validateI18nLabel(value, field, errors) {
|
|
32
|
+
if (value === undefined || value === null) {
|
|
33
|
+
errors.push(`${field} is required`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "string") return;
|
|
37
|
+
if (typeof value === "object") {
|
|
38
|
+
if (typeof value.en !== "string")
|
|
39
|
+
errors.push(`${field}: "en" is required when using object form`);
|
|
40
|
+
const unknown = Object.keys(value).filter((k) => !I18N_LANGS.includes(k));
|
|
41
|
+
if (unknown.length > 0)
|
|
42
|
+
errors.push(`${field}: unsupported language key(s) — ${unknown.map((k) => `"${k}"`).join(", ")} (allowed: ${I18N_LANGS.join(", ")})`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
errors.push(`${field}: must be a string or { en (required), ko, ja, zh, vi, th, es }`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function readManifest(root) {
|
|
49
|
+
const path = resolve(root, "manifest.json");
|
|
50
|
+
if (!existsSync(path)) return {};
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function manifestRuntime(root) {
|
|
59
|
+
return readManifest(root)?.runtime?.type ?? "python";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function validateIcons(icons, errors) {
|
|
63
|
+
if (icons === undefined || icons === null) {
|
|
64
|
+
errors.push("icons is required");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (typeof icons === "string") {
|
|
68
|
+
if (!icons.toLowerCase().endsWith(".svg"))
|
|
69
|
+
errors.push(`icons: non-SVG images (PNG, JPG, etc.) must use the { "16": "...", "48": "...", "128": "..." } object form`);
|
|
70
|
+
} else if (typeof icons === "object") {
|
|
71
|
+
const missing = ICON_SIZES.filter((s) => typeof icons[s] !== "string" || icons[s].length === 0);
|
|
72
|
+
if (missing.length > 0)
|
|
73
|
+
errors.push(`icons: missing size(s) ${missing.map((s) => `"${s}"`).join(", ")} — all three (16, 48, 128) are required for image files`);
|
|
74
|
+
} else {
|
|
75
|
+
errors.push(`icons: must be a string (SVG path) or { "16", "48", "128" } object (PNG/JPG)`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function validateRuntime(runtime, errors) {
|
|
80
|
+
if (runtime === undefined || runtime === null) {
|
|
81
|
+
errors.push("runtime is required");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (typeof runtime !== "object") {
|
|
85
|
+
errors.push("runtime must be an object");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!RUNTIME_TYPES.includes(runtime.type))
|
|
89
|
+
errors.push(`runtime.type must be one of ${RUNTIME_TYPES.map((t) => `"${t}"`).join(" | ")}`);
|
|
90
|
+
if (runtime.type === "python" && !runtime.entry)
|
|
91
|
+
errors.push("runtime.entry is required for python runtime");
|
|
92
|
+
if (runtime.type === "cpp" && !runtime.entry)
|
|
93
|
+
errors.push("runtime.entry is required for cpp runtime (the built executable name)");
|
|
94
|
+
if (runtime.type === "ros2") {
|
|
95
|
+
if (!runtime.package) errors.push("runtime.package is required for ros2 runtime");
|
|
96
|
+
if (!runtime.launch) errors.push("runtime.launch is required for ros2 runtime");
|
|
97
|
+
if (!ROS_CLIENTS.includes(runtime.rosClient))
|
|
98
|
+
errors.push(`runtime.rosClient must be one of ${ROS_CLIENTS.map((c) => `"${c}"`).join(" | ")}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateCommandInfo(info, prefix, errors) {
|
|
103
|
+
if (typeof info.name !== "string" || info.name.length === 0)
|
|
104
|
+
errors.push(`${prefix}.commandInfo.name is required`);
|
|
105
|
+
if (typeof info.icon !== "string" || info.icon.length === 0)
|
|
106
|
+
errors.push(`${prefix}.commandInfo.icon is required`);
|
|
107
|
+
if (info.iconClassName !== undefined && typeof info.iconClassName !== "string")
|
|
108
|
+
errors.push(`${prefix}.commandInfo.iconClassName must be a string`);
|
|
109
|
+
if (info.funcName !== undefined && typeof info.funcName !== "string")
|
|
110
|
+
errors.push(`${prefix}.commandInfo.funcName must be a string`);
|
|
111
|
+
if (info.args !== undefined && (typeof info.args !== "object" || Array.isArray(info.args)))
|
|
112
|
+
errors.push(`${prefix}.commandInfo.args must be an object`);
|
|
113
|
+
for (const key of ["disabled", "syncGroupId", "disableCopy", "disableDelete", "requireChildren", "requireParent"]) {
|
|
114
|
+
if (info[key] !== undefined && typeof info[key] !== "boolean")
|
|
115
|
+
errors.push(`${prefix}.commandInfo.${key} must be a boolean`);
|
|
116
|
+
}
|
|
117
|
+
for (const key of ["allowChildren", "allowParents", "notAllowChildren"]) {
|
|
118
|
+
if (info[key] !== undefined && (!Array.isArray(info[key]) || info[key].some((v) => typeof v !== "string")))
|
|
119
|
+
errors.push(`${prefix}.commandInfo.${key} must be a string[]`);
|
|
120
|
+
}
|
|
121
|
+
if (info.childrenCommand !== undefined && typeof info.childrenCommand !== "string")
|
|
122
|
+
errors.push(`${prefix}.commandInfo.childrenCommand must be a string`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// variables: pyfm에 노출할 변수 선언. 없으면 UI가 사용 가능한 변수를 보여줄 수 없다.
|
|
126
|
+
function validateVariables(variables, errors) {
|
|
127
|
+
if (variables === undefined || variables === null) return;
|
|
128
|
+
if (!Array.isArray(variables)) {
|
|
129
|
+
errors.push(`"variables" must be an array`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const names = variables.map((v) => v?.name).filter(Boolean);
|
|
133
|
+
const duplicates = names.filter((n, i) => names.indexOf(n) !== i);
|
|
134
|
+
if (duplicates.length > 0)
|
|
135
|
+
errors.push(`variables: "name" must be unique (duplicates: ${[...new Set(duplicates)].map((n) => `"${n}"`).join(", ")})`);
|
|
136
|
+
|
|
137
|
+
variables.forEach((v, i) => {
|
|
138
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
139
|
+
errors.push(`variables[${i}] must be an object ({ name, type, init, description? })`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (typeof v.name !== "string" || v.name.length === 0)
|
|
143
|
+
errors.push(`variables[${i}].name is required`);
|
|
144
|
+
else if (!v.name.startsWith(VARIABLE_NAME_PREFIX))
|
|
145
|
+
errors.push(`variables[${i}].name: "${v.name}" must start with "${VARIABLE_NAME_PREFIX}" (e.g. "${VARIABLE_NAME_PREFIX}${v.name}")`);
|
|
146
|
+
else if (!VARIABLE_NAME_RE.test(v.name))
|
|
147
|
+
errors.push(`variables[${i}].name: "${v.name}" is not a valid identifier (after "${VARIABLE_NAME_PREFIX}": letters, digits, underscore only, at least one character)`);
|
|
148
|
+
if (!VARIABLE_TYPES.includes(v.type))
|
|
149
|
+
errors.push(`variables[${i}].type must be one of ${VARIABLE_TYPES.map((t) => `"${t}"`).join(" | ")}`);
|
|
150
|
+
if (v.init === undefined)
|
|
151
|
+
errors.push(`variables[${i}].init is required (initial value)`);
|
|
152
|
+
else if (v.type === "number" && typeof v.init !== "number")
|
|
153
|
+
errors.push(`variables[${i}].init must be a number when type is "number"`);
|
|
154
|
+
else if (v.type === "string" && typeof v.init !== "string")
|
|
155
|
+
errors.push(`variables[${i}].init must be a string when type is "string"`);
|
|
156
|
+
else if (v.type === "boolean" && typeof v.init !== "boolean")
|
|
157
|
+
errors.push(`variables[${i}].init must be a boolean when type is "boolean"`);
|
|
158
|
+
else if (v.type === "array" && !Array.isArray(v.init))
|
|
159
|
+
errors.push(`variables[${i}].init must be an array when type is "array"`);
|
|
160
|
+
if (v.description !== undefined && v.description !== null)
|
|
161
|
+
validateI18nLabel(v.description, `variables[${i}].description`, errors);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// No contributes (missing or []) = a backend-only plugin: no screens, nothing for vite to build.
|
|
166
|
+
// A plugin with screens still needs a toolbar entry — that is where the host lists it.
|
|
167
|
+
function validateContributes(contributes, errors) {
|
|
168
|
+
if (contributes === undefined || contributes === null) return;
|
|
169
|
+
if (!Array.isArray(contributes)) {
|
|
170
|
+
errors.push("contributes must be an array");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (contributes.length === 0) return;
|
|
174
|
+
if (!contributes.some((c) => c.point === "toolbar"))
|
|
175
|
+
errors.push(`contributes: at least one "toolbar" entry is required`);
|
|
176
|
+
|
|
177
|
+
const uiValues = contributes.map((c) => c.ui).filter(Boolean);
|
|
178
|
+
const duplicateUi = uiValues.filter((v, i) => uiValues.indexOf(v) !== i);
|
|
179
|
+
if (duplicateUi.length > 0)
|
|
180
|
+
errors.push(`contributes: "ui" values must be unique across all entries (duplicates: ${[...new Set(duplicateUi)].map((v) => `"${v}"`).join(", ")})`);
|
|
181
|
+
|
|
182
|
+
contributes.forEach((c, i) => {
|
|
183
|
+
if (!CONTRIBUTION_POINTS.includes(c.point))
|
|
184
|
+
errors.push(`contributes[${i}].point: unknown point ${JSON.stringify(c.point)} (allowed: ${CONTRIBUTION_POINTS.join(", ")})`);
|
|
185
|
+
validateI18nLabel(c.label, `contributes[${i}].label`, errors);
|
|
186
|
+
if (c.point === "program.panel") {
|
|
187
|
+
if (!c.commandInfo)
|
|
188
|
+
errors.push(`contributes[${i}]: "commandInfo" is required when point is "program.panel"`);
|
|
189
|
+
else
|
|
190
|
+
validateCommandInfo(c.commandInfo, `contributes[${i}]`, errors);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Validate manifest.json. Returns an array of error messages (empty = valid).
|
|
197
|
+
*/
|
|
198
|
+
export function validateManifest(manifest) {
|
|
199
|
+
const errors = [];
|
|
200
|
+
|
|
201
|
+
if (typeof manifest !== "object" || manifest === null)
|
|
202
|
+
return ["manifest.json is not a valid JSON object"];
|
|
203
|
+
|
|
204
|
+
for (const key of ["id", "name", "version", "minMuscatVersion"]) {
|
|
205
|
+
if (typeof manifest[key] !== "string" || manifest[key].length === 0)
|
|
206
|
+
errors.push(`"${key}" is required`);
|
|
207
|
+
}
|
|
208
|
+
if (typeof manifest.id === "string" && manifest.id && !PLUGIN_ID_RE.test(manifest.id))
|
|
209
|
+
errors.push(`"id" must be "."-separated parts of lowercase letters/digits and single "-", each starting with a letter (e.g. com.example.my-plugin)`);
|
|
210
|
+
if (typeof manifest.author !== "string" || manifest.author.length === 0)
|
|
211
|
+
errors.push(`"author" is required`);
|
|
212
|
+
if (typeof manifest.email !== "string" || manifest.email.length === 0)
|
|
213
|
+
errors.push(`"email" is required`);
|
|
214
|
+
if (manifest.company !== undefined && manifest.company !== null && typeof manifest.company !== "string")
|
|
215
|
+
errors.push(`"company" must be a string`);
|
|
216
|
+
|
|
217
|
+
const hasModels = manifest.targetModels !== undefined && manifest.targetModels !== null;
|
|
218
|
+
const hasCategorys = manifest.targetCategorys !== undefined && manifest.targetCategorys !== null;
|
|
219
|
+
if (!hasModels && !hasCategorys) {
|
|
220
|
+
errors.push(`"targetModels" or "targetCategorys" is required`);
|
|
221
|
+
} else {
|
|
222
|
+
if (hasModels) {
|
|
223
|
+
if (manifest.targetModels !== "all" && (!Array.isArray(manifest.targetModels) || manifest.targetModels.length === 0))
|
|
224
|
+
errors.push(`"targetModels" must be "all" or a non-empty array of model strings`);
|
|
225
|
+
else if (Array.isArray(manifest.targetModels) && manifest.targetModels.some((m) => typeof m !== "string"))
|
|
226
|
+
errors.push(`"targetModels": all entries must be strings`);
|
|
227
|
+
}
|
|
228
|
+
if (hasCategorys) {
|
|
229
|
+
if (manifest.targetCategorys !== "all" && (!Array.isArray(manifest.targetCategorys) || manifest.targetCategorys.length === 0))
|
|
230
|
+
errors.push(`"targetCategorys" must be "all" or a non-empty array`);
|
|
231
|
+
else if (Array.isArray(manifest.targetCategorys)) {
|
|
232
|
+
const invalid = manifest.targetCategorys.filter((c) => !TARGET_CATEGORIES.includes(c));
|
|
233
|
+
if (invalid.length > 0)
|
|
234
|
+
errors.push(`"targetCategorys": invalid value(s) ${invalid.map((c) => `"${c}"`).join(", ")} — allowed: ${TARGET_CATEGORIES.join(", ")}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
validateI18nLabel(manifest.description, "description", errors);
|
|
239
|
+
|
|
240
|
+
validateIcons(manifest.icons, errors);
|
|
241
|
+
validatePorts(manifest.ports, errors);
|
|
242
|
+
validateRuntime(manifest.runtime, errors);
|
|
243
|
+
validateContributes(manifest.contributes, errors);
|
|
244
|
+
validateVariables(manifest.variables, errors);
|
|
245
|
+
|
|
246
|
+
return errors;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Read and validate manifest.json. Throws if any errors are found.
|
|
251
|
+
*/
|
|
252
|
+
export function assertManifest(root) {
|
|
253
|
+
const manifest = readManifest(root);
|
|
254
|
+
if (manifest === null)
|
|
255
|
+
throw new Error("manifest.json parse failed — check JSON syntax");
|
|
256
|
+
if (Object.keys(manifest).length === 0)
|
|
257
|
+
return manifest;
|
|
258
|
+
const errors = validateManifest(manifest);
|
|
259
|
+
if (errors.length > 0)
|
|
260
|
+
throw new Error(`manifest.json errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
|
|
261
|
+
return manifest;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function assertManifestVariables(root) {
|
|
265
|
+
const manifest = readManifest(root);
|
|
266
|
+
if (manifest === null)
|
|
267
|
+
throw new Error("manifest.json parse failed — check JSON syntax");
|
|
268
|
+
if (Object.keys(manifest).length === 0)
|
|
269
|
+
return;
|
|
270
|
+
const errors = [];
|
|
271
|
+
validateVariables(manifest.variables, errors);
|
|
272
|
+
if (errors.length > 0)
|
|
273
|
+
throw new Error(`manifest.json variable errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function assertManifestPorts(root) {
|
|
277
|
+
const manifest = readManifest(root);
|
|
278
|
+
if (manifest === null)
|
|
279
|
+
throw new Error("manifest.json parse failed — check JSON syntax");
|
|
280
|
+
if (Object.keys(manifest).length === 0)
|
|
281
|
+
return [];
|
|
282
|
+
const errors = [];
|
|
283
|
+
validatePorts(manifest.ports, errors);
|
|
284
|
+
if (errors.length > 0)
|
|
285
|
+
throw new Error(`manifest.json port errors:\n${errors.map((e) => ` • ${e}`).join("\n")}`);
|
|
286
|
+
return manifestPortRanges(manifest);
|
|
287
|
+
}
|