waybetter 0.1.1 → 0.7.0
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 +139 -6
- package/bin/waybetter.mjs +43 -0
- package/build/runtime.mjs +105 -0
- package/cli/buildenv.mjs +130 -0
- package/cli/dev.mjs +97 -0
- package/cli/i18n.mjs +382 -0
- package/cli/manifests.mjs +383 -0
- package/cli/release.mjs +437 -0
- package/cli/releasecheck.mjs +15 -0
- package/cli/remote.mjs +260 -0
- package/cli/store.mjs +378 -0
- package/cli/strings.mjs +123 -0
- package/dist/runtime/column.d.ts +93 -0
- package/dist/runtime/column.js +451 -0
- package/dist/runtime/column.js.map +1 -0
- package/dist/runtime/commandReport.d.ts +19 -0
- package/dist/runtime/commandReport.js +47 -0
- package/dist/runtime/commandReport.js.map +1 -0
- package/dist/runtime/commands.d.ts +72 -0
- package/dist/runtime/commands.js +214 -0
- package/dist/runtime/commands.js.map +1 -0
- package/dist/runtime/devErrors.d.ts +16 -0
- package/dist/runtime/devErrors.js +30 -0
- package/dist/runtime/devErrors.js.map +1 -0
- package/dist/runtime/domHub.d.ts +21 -0
- package/dist/runtime/domHub.js +139 -4
- package/dist/runtime/domHub.js.map +1 -1
- package/dist/runtime/featureModules.d.ts +157 -0
- package/dist/runtime/featureModules.js +484 -0
- package/dist/runtime/featureModules.js.map +1 -0
- package/dist/runtime/featureTable.d.ts +144 -0
- package/dist/runtime/featureTable.js +223 -0
- package/dist/runtime/featureTable.js.map +1 -0
- package/dist/runtime/fonts.d.ts +22 -0
- package/dist/runtime/fonts.js +72 -0
- package/dist/runtime/fonts.js.map +1 -0
- package/dist/runtime/i18n.d.ts +138 -0
- package/dist/runtime/i18n.js +614 -0
- package/dist/runtime/i18n.js.map +1 -0
- package/dist/runtime/i18nCheck.d.ts +30 -0
- package/dist/runtime/i18nCheck.js +224 -0
- package/dist/runtime/i18nCheck.js.map +1 -0
- package/dist/runtime/pageLang.d.ts +8 -0
- package/dist/runtime/pageLang.js +64 -0
- package/dist/runtime/pageLang.js.map +1 -0
- package/dist/runtime/pageStrings.d.ts +35 -0
- package/dist/runtime/pageStrings.js +76 -0
- package/dist/runtime/pageStrings.js.map +1 -0
- package/dist/runtime/panelButton.d.ts +33 -0
- package/dist/runtime/panelButton.js +83 -0
- package/dist/runtime/panelButton.js.map +1 -0
- package/dist/runtime/permissions.d.ts +47 -0
- package/dist/runtime/permissions.js +141 -0
- package/dist/runtime/permissions.js.map +1 -0
- package/dist/runtime/pluginApi.d.ts +51 -2
- package/dist/runtime/pluginApi.js +137 -22
- package/dist/runtime/pluginApi.js.map +1 -1
- package/dist/runtime/pluginI18n.d.ts +29 -0
- package/dist/runtime/pluginI18n.js +119 -0
- package/dist/runtime/pluginI18n.js.map +1 -0
- package/dist/runtime/settingsForm.d.ts +66 -0
- package/dist/runtime/settingsForm.js +114 -0
- package/dist/runtime/settingsForm.js.map +1 -0
- package/dist/runtime/site.d.ts +5 -0
- package/dist/runtime/site.js +1 -1
- package/dist/runtime/site.js.map +1 -1
- package/dist/runtime/siteRuntime.d.ts +144 -0
- package/dist/runtime/siteRuntime.js +175 -0
- package/dist/runtime/siteRuntime.js.map +1 -0
- package/dist/runtime/stringPackHook.d.ts +84 -0
- package/dist/runtime/stringPackHook.js +220 -0
- package/dist/runtime/stringPackHook.js.map +1 -0
- package/dist/runtime/stringPackTranslate.d.ts +61 -0
- package/dist/runtime/stringPackTranslate.js +267 -0
- package/dist/runtime/stringPackTranslate.js.map +1 -0
- package/dist/runtime/stringPacks.d.ts +248 -0
- package/dist/runtime/stringPacks.js +610 -0
- package/dist/runtime/stringPacks.js.map +1 -0
- package/dist/runtime/styles.d.ts +15 -0
- package/dist/runtime/styles.js +81 -0
- package/dist/runtime/styles.js.map +1 -0
- package/dist/runtime/textNet.d.ts +15 -0
- package/dist/runtime/textNet.js +131 -0
- package/dist/runtime/textNet.js.map +1 -0
- package/dist/runtime/themeApi.d.ts +121 -0
- package/dist/runtime/themeApi.js +220 -0
- package/dist/runtime/themeApi.js.map +1 -0
- package/dist/runtime/themeBar.d.ts +28 -0
- package/dist/runtime/themeBar.js +82 -0
- package/dist/runtime/themeBar.js.map +1 -0
- package/dist/runtime/themeChoice.d.ts +52 -0
- package/dist/runtime/themeChoice.js +69 -0
- package/dist/runtime/themeChoice.js.map +1 -0
- package/dist/runtime/themeComponents.d.ts +435 -0
- package/dist/runtime/themeComponents.js +476 -0
- package/dist/runtime/themeComponents.js.map +1 -0
- package/dist/runtime/themeDecor.d.ts +73 -0
- package/dist/runtime/themeDecor.js +133 -0
- package/dist/runtime/themeDecor.js.map +1 -0
- package/dist/runtime/themeDerive.d.ts +69 -0
- package/dist/runtime/themeDerive.js +117 -0
- package/dist/runtime/themeDerive.js.map +1 -0
- package/dist/runtime/themeDoctor.d.ts +52 -0
- package/dist/runtime/themeDoctor.js +112 -0
- package/dist/runtime/themeDoctor.js.map +1 -0
- package/dist/runtime/themeGuards.d.ts +60 -0
- package/dist/runtime/themeGuards.js +235 -0
- package/dist/runtime/themeGuards.js.map +1 -0
- package/dist/runtime/themeIcons.d.ts +130 -0
- package/dist/runtime/themeIcons.js +271 -0
- package/dist/runtime/themeIcons.js.map +1 -0
- package/dist/runtime/themeInk.d.ts +37 -0
- package/dist/runtime/themeInk.js +111 -0
- package/dist/runtime/themeInk.js.map +1 -0
- package/dist/runtime/themeInvariants.d.ts +39 -0
- package/dist/runtime/themeInvariants.js +273 -0
- package/dist/runtime/themeInvariants.js.map +1 -0
- package/dist/runtime/themeRuntime.d.ts +114 -0
- package/dist/runtime/themeRuntime.js +570 -0
- package/dist/runtime/themeRuntime.js.map +1 -0
- package/dist/runtime/themeSchema.d.ts +10 -0
- package/dist/runtime/themeSchema.js +150 -0
- package/dist/runtime/themeSchema.js.map +1 -0
- package/dist/runtime/themeSpec.d.ts +263 -0
- package/dist/runtime/themeSpec.js +435 -0
- package/dist/runtime/themeSpec.js.map +1 -0
- package/dist/runtime/themeTokens.d.ts +12 -0
- package/dist/runtime/themeTokens.js +14 -6
- package/dist/runtime/themeTokens.js.map +1 -1
- package/dist/runtime/themeVariants.d.ts +30 -0
- package/dist/runtime/themeVariants.js +53 -0
- package/dist/runtime/themeVariants.js.map +1 -0
- package/dist/runtime/ui.d.ts +192 -0
- package/dist/runtime/ui.js +926 -0
- package/dist/runtime/ui.js.map +1 -0
- package/dist/shell/InstallDialog.d.ts +40 -0
- package/dist/shell/InstallDialog.js +176 -0
- package/dist/shell/InstallDialog.js.map +1 -0
- package/dist/shell/StoreView.d.ts +78 -0
- package/dist/shell/StoreView.js +108 -0
- package/dist/shell/StoreView.js.map +1 -0
- package/dist/shell/addressModel.js.map +1 -1
- package/dist/shell/chrome.js.map +1 -1
- package/dist/shell/deckModel.d.ts +117 -0
- package/dist/shell/deckModel.js +267 -0
- package/dist/shell/deckModel.js.map +1 -0
- package/dist/shell/frame/AccountSwitcher.d.ts +42 -0
- package/dist/shell/frame/AccountSwitcher.js +243 -0
- package/dist/shell/frame/AccountSwitcher.js.map +1 -0
- package/dist/shell/frame/AddColumnDialog.d.ts +8 -0
- package/dist/shell/frame/AddColumnDialog.js +81 -0
- package/dist/shell/frame/AddColumnDialog.js.map +1 -0
- package/dist/shell/frame/AppMenu.d.ts +44 -0
- package/dist/shell/frame/AppMenu.js +721 -0
- package/dist/shell/frame/AppMenu.js.map +1 -0
- package/dist/shell/frame/CommandPalette.d.ts +7 -0
- package/dist/shell/frame/CommandPalette.js +118 -0
- package/dist/shell/frame/CommandPalette.js.map +1 -0
- package/dist/shell/frame/DeckView.d.ts +13 -0
- package/dist/shell/frame/DeckView.js +287 -0
- package/dist/shell/frame/DeckView.js.map +1 -0
- package/dist/shell/frame/FindBar.d.ts +10 -0
- package/dist/shell/frame/FindBar.js +108 -0
- package/dist/shell/frame/FindBar.js.map +1 -0
- package/dist/shell/frame/MediaPanel.d.ts +8 -0
- package/dist/shell/frame/MediaPanel.js +76 -0
- package/dist/shell/frame/MediaPanel.js.map +1 -0
- package/dist/shell/frame/NewTabPage.d.ts +9 -0
- package/dist/shell/frame/NewTabPage.js +71 -0
- package/dist/shell/frame/NewTabPage.js.map +1 -0
- package/dist/shell/frame/PaneTheme.d.ts +16 -0
- package/dist/shell/frame/PaneTheme.js +67 -0
- package/dist/shell/frame/PaneTheme.js.map +1 -0
- package/dist/shell/frame/ReportDialog.d.ts +3 -0
- package/dist/shell/frame/ReportDialog.js +95 -0
- package/dist/shell/frame/ReportDialog.js.map +1 -0
- package/dist/shell/frame/Shell.d.ts +9 -0
- package/dist/shell/frame/Shell.js +1139 -0
- package/dist/shell/frame/Shell.js.map +1 -0
- package/dist/shell/frame/Titlebar.d.ts +17 -0
- package/dist/shell/frame/Titlebar.js +79 -0
- package/dist/shell/frame/Titlebar.js.map +1 -0
- package/dist/shell/frame/TitlebarExtras.d.ts +4 -0
- package/dist/shell/frame/TitlebarExtras.js +202 -0
- package/dist/shell/frame/TitlebarExtras.js.map +1 -0
- package/dist/shell/frame/WebPanel.d.ts +4 -0
- package/dist/shell/frame/WebPanel.js +185 -0
- package/dist/shell/frame/WebPanel.js.map +1 -0
- package/dist/shell/frame/accountModel.d.ts +49 -0
- package/dist/shell/frame/accountModel.js +67 -0
- package/dist/shell/frame/accountModel.js.map +1 -0
- package/dist/shell/frame/accountSwitcher.css +91 -0
- package/dist/shell/frame/appI18n.d.ts +44 -0
- package/dist/shell/frame/appI18n.js +64 -0
- package/dist/shell/frame/appI18n.js.map +1 -0
- package/dist/shell/frame/appMenu.css +127 -0
- package/dist/shell/frame/backupModel.d.ts +38 -0
- package/dist/shell/frame/backupModel.js +192 -0
- package/dist/shell/frame/backupModel.js.map +1 -0
- package/dist/shell/frame/boot.d.ts +8 -0
- package/dist/shell/frame/boot.js +37 -0
- package/dist/shell/frame/boot.js.map +1 -0
- package/dist/shell/frame/brand.d.ts +1 -0
- package/dist/shell/frame/brand.js +4 -0
- package/dist/shell/frame/brand.js.map +1 -0
- package/dist/shell/frame/bridge.d.ts +35 -0
- package/dist/shell/frame/bridge.js +70 -0
- package/dist/shell/frame/bridge.js.map +1 -0
- package/dist/shell/frame/chromeTheme.d.ts +47 -0
- package/dist/shell/frame/chromeTheme.js +118 -0
- package/dist/shell/frame/chromeTheme.js.map +1 -0
- package/dist/shell/frame/deck.css +209 -0
- package/dist/shell/frame/deckStore.d.ts +89 -0
- package/dist/shell/frame/deckStore.js +239 -0
- package/dist/shell/frame/deckStore.js.map +1 -0
- package/dist/shell/frame/devHost.d.ts +35 -0
- package/dist/shell/frame/devHost.js +80 -0
- package/dist/shell/frame/devHost.js.map +1 -0
- package/dist/shell/frame/devModel.d.ts +7 -0
- package/dist/shell/frame/devModel.js +17 -0
- package/dist/shell/frame/devModel.js.map +1 -0
- package/dist/shell/frame/diag.d.ts +7 -0
- package/dist/shell/frame/diag.js +39 -0
- package/dist/shell/frame/diag.js.map +1 -0
- package/dist/shell/frame/edges.css +159 -0
- package/dist/shell/frame/findBar.css +14 -0
- package/dist/shell/frame/frameMessages.d.ts +248 -0
- package/dist/shell/frame/frameMessages.js +252 -0
- package/dist/shell/frame/frameMessages.js.map +1 -0
- package/dist/shell/frame/hooks.d.ts +39 -0
- package/dist/shell/frame/hooks.js +97 -0
- package/dist/shell/frame/hooks.js.map +1 -0
- package/dist/shell/frame/i18n.d.ts +27 -0
- package/dist/shell/frame/i18n.js +66 -0
- package/dist/shell/frame/i18n.js.map +1 -0
- package/dist/shell/frame/iconPack.d.ts +11 -0
- package/dist/shell/frame/iconPack.js +45 -0
- package/dist/shell/frame/iconPack.js.map +1 -0
- package/dist/shell/frame/icons.d.ts +71 -0
- package/dist/shell/frame/icons.js +75 -0
- package/dist/shell/frame/icons.js.map +1 -0
- package/dist/shell/frame/index.d.ts +34 -0
- package/dist/shell/frame/index.js +43 -0
- package/dist/shell/frame/index.js.map +1 -0
- package/dist/shell/frame/language.d.ts +58 -0
- package/dist/shell/frame/language.js +165 -0
- package/dist/shell/frame/language.js.map +1 -0
- package/dist/shell/frame/mediaModel.d.ts +47 -0
- package/dist/shell/frame/mediaModel.js +67 -0
- package/dist/shell/frame/mediaModel.js.map +1 -0
- package/dist/shell/frame/menuMessages.en.json +96 -0
- package/dist/shell/frame/menuModel.d.ts +319 -0
- package/dist/shell/frame/menuModel.js +301 -0
- package/dist/shell/frame/menuModel.js.map +1 -0
- package/dist/shell/frame/nativeMenu.d.ts +5 -0
- package/dist/shell/frame/nativeMenu.js +178 -0
- package/dist/shell/frame/nativeMenu.js.map +1 -0
- package/dist/shell/frame/nativeMenuModel.d.ts +102 -0
- package/dist/shell/frame/nativeMenuModel.js +186 -0
- package/dist/shell/frame/nativeMenuModel.js.map +1 -0
- package/dist/shell/frame/paletteModel.d.ts +139 -0
- package/dist/shell/frame/paletteModel.js +114 -0
- package/dist/shell/frame/paletteModel.js.map +1 -0
- package/dist/shell/frame/paneTheme.css +9 -0
- package/dist/shell/frame/pluginCommandStore.d.ts +23 -0
- package/dist/shell/frame/pluginCommandStore.js +70 -0
- package/dist/shell/frame/pluginCommandStore.js.map +1 -0
- package/dist/shell/frame/reportModel.d.ts +52 -0
- package/dist/shell/frame/reportModel.js +105 -0
- package/dist/shell/frame/reportModel.js.map +1 -0
- package/dist/shell/frame/safeStore.d.ts +7 -0
- package/dist/shell/frame/safeStore.js +40 -0
- package/dist/shell/frame/safeStore.js.map +1 -0
- package/dist/shell/frame/searchInput.d.ts +2 -0
- package/dist/shell/frame/searchInput.js +11 -0
- package/dist/shell/frame/searchInput.js.map +1 -0
- package/dist/shell/frame/shell.css +634 -0
- package/dist/shell/frame/site.d.ts +206 -0
- package/dist/shell/frame/site.js +22 -0
- package/dist/shell/frame/site.js.map +1 -0
- package/dist/shell/frame/state.d.ts +101 -0
- package/dist/shell/frame/state.js +286 -0
- package/dist/shell/frame/state.js.map +1 -0
- package/dist/shell/frame/syncGate.d.ts +10 -0
- package/dist/shell/frame/syncGate.js +36 -0
- package/dist/shell/frame/syncGate.js.map +1 -0
- package/dist/shell/frame/syncHost.d.ts +35 -0
- package/dist/shell/frame/syncHost.js +85 -0
- package/dist/shell/frame/syncHost.js.map +1 -0
- package/dist/shell/frame/tabStill.d.ts +2 -0
- package/dist/shell/frame/tabStill.js +62 -0
- package/dist/shell/frame/tabStill.js.map +1 -0
- package/dist/shell/frame/themeScopes.d.ts +29 -0
- package/dist/shell/frame/themeScopes.js +61 -0
- package/dist/shell/frame/themeScopes.js.map +1 -0
- package/dist/shell/frame/themeStore.d.ts +99 -0
- package/dist/shell/frame/themeStore.js +267 -0
- package/dist/shell/frame/themeStore.js.map +1 -0
- package/dist/shell/frame/windowBinding.d.ts +2 -0
- package/dist/shell/frame/windowBinding.js +7 -0
- package/dist/shell/frame/windowBinding.js.map +1 -0
- package/dist/shell/frame/workspaceModel.d.ts +82 -0
- package/dist/shell/frame/workspaceModel.js +250 -0
- package/dist/shell/frame/workspaceModel.js.map +1 -0
- package/dist/shell/frame/workspaces.css +84 -0
- package/dist/shell/frame/workspaces.d.ts +22 -0
- package/dist/shell/frame/workspaces.js +125 -0
- package/dist/shell/frame/workspaces.js.map +1 -0
- package/dist/shell/frame/zoomStore.d.ts +10 -0
- package/dist/shell/frame/zoomStore.js +65 -0
- package/dist/shell/frame/zoomStore.js.map +1 -0
- package/dist/shell/installController.d.ts +137 -0
- package/dist/shell/installController.js +172 -0
- package/dist/shell/installController.js.map +1 -0
- package/dist/shell/installerMessages.d.ts +30 -0
- package/dist/shell/installerMessages.js +36 -0
- package/dist/shell/installerMessages.js.map +1 -0
- package/dist/shell/linkDrop.js.map +1 -1
- package/dist/shell/messages.d.ts +16 -0
- package/dist/shell/messages.js +27 -0
- package/dist/shell/messages.js.map +1 -0
- package/dist/shell/palette.js.map +1 -1
- package/dist/shell/paneModel.d.ts +22 -0
- package/dist/shell/paneModel.js +24 -1
- package/dist/shell/paneModel.js.map +1 -1
- package/dist/shell/panel/Appearance.d.ts +8 -0
- package/dist/shell/panel/Appearance.js +265 -0
- package/dist/shell/panel/Appearance.js.map +1 -0
- package/dist/shell/panel/BackupPanel.d.ts +5 -0
- package/dist/shell/panel/BackupPanel.js +198 -0
- package/dist/shell/panel/BackupPanel.js.map +1 -0
- package/dist/shell/panel/DeveloperPanel.d.ts +4 -0
- package/dist/shell/panel/DeveloperPanel.js +51 -0
- package/dist/shell/panel/DeveloperPanel.js.map +1 -0
- package/dist/shell/panel/FeatureToggles.d.ts +13 -0
- package/dist/shell/panel/FeatureToggles.js +42 -0
- package/dist/shell/panel/FeatureToggles.js.map +1 -0
- package/dist/shell/panel/Home.d.ts +3 -0
- package/dist/shell/panel/Home.js +23 -0
- package/dist/shell/panel/Home.js.map +1 -0
- package/dist/shell/panel/IconPackInstall.d.ts +15 -0
- package/dist/shell/panel/IconPackInstall.js +41 -0
- package/dist/shell/panel/IconPackInstall.js.map +1 -0
- package/dist/shell/panel/LanguageSettings.d.ts +1 -0
- package/dist/shell/panel/LanguageSettings.js +54 -0
- package/dist/shell/panel/LanguageSettings.js.map +1 -0
- package/dist/shell/panel/MakePanel.d.ts +3 -0
- package/dist/shell/panel/MakePanel.js +237 -0
- package/dist/shell/panel/MakePanel.js.map +1 -0
- package/dist/shell/panel/MarkdownDoc.d.ts +6 -0
- package/dist/shell/panel/MarkdownDoc.js +35 -0
- package/dist/shell/panel/MarkdownDoc.js.map +1 -0
- package/dist/shell/panel/Modes.d.ts +40 -0
- package/dist/shell/panel/Modes.js +270 -0
- package/dist/shell/panel/Modes.js.map +1 -0
- package/dist/shell/panel/NavIcon.d.ts +4 -0
- package/dist/shell/panel/NavIcon.js +6 -0
- package/dist/shell/panel/NavIcon.js.map +1 -0
- package/dist/shell/panel/Panel.d.ts +7 -0
- package/dist/shell/panel/Panel.js +139 -0
- package/dist/shell/panel/Panel.js.map +1 -0
- package/dist/shell/panel/PluginFeatures.d.ts +24 -0
- package/dist/shell/panel/PluginFeatures.js +165 -0
- package/dist/shell/panel/PluginFeatures.js.map +1 -0
- package/dist/shell/panel/Plugins.d.ts +33 -0
- package/dist/shell/panel/Plugins.js +112 -0
- package/dist/shell/panel/Plugins.js.map +1 -0
- package/dist/shell/panel/PowerSettings.d.ts +4 -0
- package/dist/shell/panel/PowerSettings.js +52 -0
- package/dist/shell/panel/PowerSettings.js.map +1 -0
- package/dist/shell/panel/Settings.d.ts +23 -0
- package/dist/shell/panel/Settings.js +117 -0
- package/dist/shell/panel/Settings.js.map +1 -0
- package/dist/shell/panel/ShareSetup.d.ts +8 -0
- package/dist/shell/panel/ShareSetup.js +130 -0
- package/dist/shell/panel/ShareSetup.js.map +1 -0
- package/dist/shell/panel/StoreSection.d.ts +7 -0
- package/dist/shell/panel/StoreSection.js +78 -0
- package/dist/shell/panel/StoreSection.js.map +1 -0
- package/dist/shell/panel/StringPacks.d.ts +20 -0
- package/dist/shell/panel/StringPacks.js +131 -0
- package/dist/shell/panel/StringPacks.js.map +1 -0
- package/dist/shell/panel/ThemeBuilder.d.ts +5 -0
- package/dist/shell/panel/ThemeBuilder.js +205 -0
- package/dist/shell/panel/ThemeBuilder.js.map +1 -0
- package/dist/shell/panel/backup.css +34 -0
- package/dist/shell/panel/builder.css +240 -0
- package/dist/shell/panel/builderModel.d.ts +55 -0
- package/dist/shell/panel/builderModel.js +95 -0
- package/dist/shell/panel/builderModel.js.map +1 -0
- package/dist/shell/panel/compare.d.ts +10 -0
- package/dist/shell/panel/compare.js +51 -0
- package/dist/shell/panel/compare.js.map +1 -0
- package/dist/shell/panel/controls.d.ts +135 -0
- package/dist/shell/panel/controls.js +213 -0
- package/dist/shell/panel/controls.js.map +1 -0
- package/dist/shell/panel/featureModel.d.ts +69 -0
- package/dist/shell/panel/featureModel.js +79 -0
- package/dist/shell/panel/featureModel.js.map +1 -0
- package/dist/shell/panel/fonts/FontBrowser.d.ts +10 -0
- package/dist/shell/panel/fonts/FontBrowser.js +189 -0
- package/dist/shell/panel/fonts/FontBrowser.js.map +1 -0
- package/dist/shell/panel/fonts/FontRoles.d.ts +20 -0
- package/dist/shell/panel/fonts/FontRoles.js +77 -0
- package/dist/shell/panel/fonts/FontRoles.js.map +1 -0
- package/dist/shell/panel/fonts/catalog.d.ts +37 -0
- package/dist/shell/panel/fonts/catalog.js +68 -0
- package/dist/shell/panel/fonts/catalog.js.map +1 -0
- package/dist/shell/panel/fonts/googleFontsCatalog.json +1 -0
- package/dist/shell/panel/index.d.ts +20 -0
- package/dist/shell/panel/index.js +34 -0
- package/dist/shell/panel/index.js.map +1 -0
- package/dist/shell/panel/languageModel.d.ts +5 -0
- package/dist/shell/panel/languageModel.js +7 -0
- package/dist/shell/panel/languageModel.js.map +1 -0
- package/dist/shell/panel/make.css +73 -0
- package/dist/shell/panel/makeModel.d.ts +32 -0
- package/dist/shell/panel/makeModel.js +84 -0
- package/dist/shell/panel/makeModel.js.map +1 -0
- package/dist/shell/panel/markdown.d.ts +46 -0
- package/dist/shell/panel/markdown.js +100 -0
- package/dist/shell/panel/markdown.js.map +1 -0
- package/dist/shell/panel/nav.d.ts +12 -0
- package/dist/shell/panel/nav.js +50 -0
- package/dist/shell/panel/nav.js.map +1 -0
- package/dist/shell/panel/panel.css +401 -0
- package/dist/shell/panel/panelMessages.d.ts +803 -0
- package/dist/shell/panel/panelMessages.js +779 -0
- package/dist/shell/panel/panelMessages.js.map +1 -0
- package/dist/shell/panel/plugins.css +59 -0
- package/dist/shell/panel/setupModel.d.ts +87 -0
- package/dist/shell/panel/setupModel.js +152 -0
- package/dist/shell/panel/setupModel.js.map +1 -0
- package/dist/shell/panel/site.d.ts +259 -0
- package/dist/shell/panel/site.js +25 -0
- package/dist/shell/panel/site.js.map +1 -0
- package/dist/shell/panel/strings.css +7 -0
- package/dist/shell/panel/themes.d.ts +10 -0
- package/dist/shell/panel/themes.js +8 -0
- package/dist/shell/panel/themes.js.map +1 -0
- package/dist/shell/profileModel.d.ts +3 -2
- package/dist/shell/profileModel.js +7 -5
- package/dist/shell/profileModel.js.map +1 -1
- package/dist/shell/store.css +250 -0
- package/dist/shell/storeMessages.d.ts +193 -0
- package/dist/shell/storeMessages.js +196 -0
- package/dist/shell/storeMessages.js.map +1 -0
- package/dist/shell/storeModel.d.ts +205 -0
- package/dist/shell/storeModel.js +146 -0
- package/dist/shell/storeModel.js.map +1 -0
- package/dist/shell/stringPacksModel.d.ts +44 -0
- package/dist/shell/stringPacksModel.js +99 -0
- package/dist/shell/stringPacksModel.js.map +1 -0
- package/dist/shell/updateModel.d.ts +2 -1
- package/dist/shell/updateModel.js +5 -4
- package/dist/shell/updateModel.js.map +1 -1
- package/dist/shell/windowControls.d.ts +4 -0
- package/dist/shell/windowControls.js +5 -0
- package/dist/shell/windowControls.js.map +1 -1
- package/i18n-config.schema.json +29 -0
- package/i18n-meta.schema.json +26 -0
- package/i18n.schema.json +42 -0
- package/icons.schema.json +655 -0
- package/languages.schema.json +23 -0
- package/locales/ar-x-fm.json +11 -0
- package/locales/ar.json +1388 -0
- package/locales/bg.json +1387 -0
- package/locales/bn.json +1387 -0
- package/locales/ca.json +1387 -0
- package/locales/cs.json +1387 -0
- package/locales/da.json +1387 -0
- package/locales/de.json +1387 -0
- package/locales/el.json +1387 -0
- package/locales/en-GB.json +27 -0
- package/locales/en.json +1386 -0
- package/locales/en.meta.json +51 -0
- package/locales/es.json +1387 -0
- package/locales/eu.json +1387 -0
- package/locales/fa.json +1388 -0
- package/locales/fi.json +1387 -0
- package/locales/fil.json +1387 -0
- package/locales/fr.json +1387 -0
- package/locales/ga.json +1387 -0
- package/locales/gl.json +1387 -0
- package/locales/gu.json +1387 -0
- package/locales/ha.json +1387 -0
- package/locales/he.json +1388 -0
- package/locales/hi.json +1387 -0
- package/locales/hr.json +1387 -0
- package/locales/hu.json +1387 -0
- package/locales/id.json +1387 -0
- package/locales/ig.json +1387 -0
- package/locales/it.json +1387 -0
- package/locales/ja.json +1387 -0
- package/locales/kn.json +1387 -0
- package/locales/ko.json +1387 -0
- package/locales/mr.json +1387 -0
- package/locales/ms.json +1387 -0
- package/locales/nb.json +1387 -0
- package/locales/nl.json +1387 -0
- package/locales/pl.json +1387 -0
- package/locales/pt.json +1387 -0
- package/locales/ro.json +1387 -0
- package/locales/ru.json +1387 -0
- package/locales/sk.json +1387 -0
- package/locales/sr.json +1387 -0
- package/locales/sv.json +1387 -0
- package/locales/ta.json +1387 -0
- package/locales/th.json +1387 -0
- package/locales/tr.json +1387 -0
- package/locales/uk.json +1387 -0
- package/locales/ur.json +1388 -0
- package/locales/vi.json +1387 -0
- package/locales/yo.json +1387 -0
- package/locales/zh-Hans.json +1387 -0
- package/locales/zh-Hant.json +1387 -0
- package/package.json +138 -5
- package/store-catalog.schema.json +78 -0
- package/theme.schema.json +4668 -0
- package/windows/installer.nsi +946 -0
- package/windows/languages/Basque.nsh +28 -0
- package/windows/languages/Catalan.nsh +28 -0
- package/windows/languages/Croatian.nsh +28 -0
- package/windows/languages/Czech.nsh +28 -0
- package/windows/languages/Danish.nsh +28 -0
- package/windows/languages/Finnish.nsh +28 -0
- package/windows/languages/Galician.nsh +28 -0
- package/windows/languages/Greek.nsh +28 -0
- package/windows/languages/Hungarian.nsh +28 -0
- package/windows/languages/Indonesian.nsh +28 -0
- package/windows/languages/Irish.nsh +28 -0
- package/windows/languages/Malay.nsh +28 -0
- package/windows/languages/Polish.nsh +28 -0
- package/windows/languages/Romanian.nsh +28 -0
- package/windows/languages/Serbian.nsh +28 -0
- package/windows/languages/Slovak.nsh +28 -0
- package/windows/languages/Thai.nsh +28 -0
package/cli/remote.mjs
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Remote and WSL builds for `waybetter release macos|linux`: the committed state of an app (its
|
|
2
|
+
// git HEAD plus every submodule at the commit the app records) is exported, sent to the machine
|
|
3
|
+
// that builds it, built there through `waybetter build-env` (no build path in the binary) and the
|
|
4
|
+
// artifacts come back to be checked (the optional release check) and, later, signed on this machine.
|
|
5
|
+
// Nothing here touches the working tree the developer has open, and no key ever leaves this
|
|
6
|
+
// machine: the remote build is unsigned (updater artifacts off), signing is `waybetter release`.
|
|
7
|
+
//
|
|
8
|
+
// The export is `git archive` of the app at HEAD plus `git archive` of each submodule at the commit
|
|
9
|
+
// HEAD points to, read from the local submodule repository. A submodule's newest commits do not
|
|
10
|
+
// have to be on any remote, so nothing is ever fetched from origin.
|
|
11
|
+
//
|
|
12
|
+
// Where it builds is never written down here: an SSH target (user@host, WAYBETTER_MAC_SSH or
|
|
13
|
+
// --remote), its key (WAYBETTER_MAC_SSH_KEY or --ssh-key) and a WSL distro (--wsl,
|
|
14
|
+
// WAYBETTER_WSL_DISTRO) come from the caller. The remote scripts use a login shell (macOS: bun,
|
|
15
|
+
// cargo and node exist only there) and a clean PATH (WSL: no Windows programs shadowing the
|
|
16
|
+
// Linux ones).
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
import { basename, join, relative, resolve } from 'node:path';
|
|
21
|
+
import { runReleaseCheck } from './releasecheck.mjs';
|
|
22
|
+
|
|
23
|
+
class RemoteError extends Error {}
|
|
24
|
+
const fail = message => { throw new RemoteError(message); };
|
|
25
|
+
|
|
26
|
+
/** Single-quotes a value for a POSIX shell. */
|
|
27
|
+
export const shq = value => `'${String(value).replaceAll("'", `'\\''`)}'`;
|
|
28
|
+
|
|
29
|
+
function run(command, args, options = {}) {
|
|
30
|
+
const result = spawnSync(command, args, { encoding: 'utf8', windowsHide: true, maxBuffer: 1 << 28, ...options });
|
|
31
|
+
if (result.error) fail(`${command}: ${result.error.message}`);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function git(root, args, options) {
|
|
36
|
+
const result = run('git', ['-C', root, ...args], options);
|
|
37
|
+
if (result.status !== 0) fail(`git ${args[0]} failed: ${(result.stderr || '').trim().split('\n')[0]}`);
|
|
38
|
+
return result.stdout ?? '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A file of the committed state (`git show <ref>:<path>`), for the version and product name. */
|
|
42
|
+
export const committedFile = (root, path, ref = 'HEAD') => git(root, ['show', `${ref}:${path}`]);
|
|
43
|
+
|
|
44
|
+
/** The submodules `ref` (default HEAD) records: [{ path, sha }]. */
|
|
45
|
+
export function committedSubmodules(root, ref = 'HEAD') {
|
|
46
|
+
return git(root, ['ls-tree', '-r', ref])
|
|
47
|
+
.split('\n')
|
|
48
|
+
.map(line => /^160000 commit ([0-9a-f]{40})\t(.+)$/.exec(line))
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.map(([, sha, path]) => ({ path, sha }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Exports the committed state of `root` (HEAD, and each submodule at its recorded commit) to a
|
|
55
|
+
* `.tar.gz` at `file`. Returns `{ sha, submodules, file, bytes, dirty }`; `dirty` lists tracked
|
|
56
|
+
* files changed in the working tree, which the export ignores by design.
|
|
57
|
+
*/
|
|
58
|
+
export function exportCommitted({ root, file, ref = 'HEAD', exclude = [], log = () => {} }) {
|
|
59
|
+
const sha = git(root, ['rev-parse', `${ref}^{commit}`]).trim();
|
|
60
|
+
const submodules = committedSubmodules(root, sha);
|
|
61
|
+
const dirty = git(root, ['status', '--porcelain', '--untracked-files=no']).split('\n').filter(Boolean).length;
|
|
62
|
+
const staging = mkdtempSync(join(tmpdir(), 'wb-export-'));
|
|
63
|
+
try {
|
|
64
|
+
const parts = [{ repo: root, ref: sha, prefix: '' }, ...submodules.map(({ path, sha: ref }) => ({ repo: join(root, path), ref, prefix: `${path}/`, path }))];
|
|
65
|
+
for (const [index, part] of parts.entries()) {
|
|
66
|
+
if (part.path && !existsSync(join(part.repo, '.git'))) fail(`submodule ${part.path} is not checked out here (needed for commit ${part.ref.slice(0, 12)})`);
|
|
67
|
+
const tarName = `part-${index}.tar`;
|
|
68
|
+
// `exclude` (paths of the app repo the build never reads, e.g. screenshots) keeps the upload small.
|
|
69
|
+
const skip = part.path ? [] : ['--', '.', ...exclude.map(path => `:(exclude)${path}`)];
|
|
70
|
+
const args = ['archive', '--format=tar', ...(part.prefix ? [`--prefix=${part.prefix}`] : []), '-o', join(staging, tarName), part.ref, ...skip];
|
|
71
|
+
// Blobs as committed: the developer's core.autocrlf must not decide the line endings of the build.
|
|
72
|
+
const archived = run('git', ['-C', part.repo, '-c', 'core.autocrlf=false', ...args]);
|
|
73
|
+
if (archived.status !== 0) fail(`git archive ${part.path ?? 'app'} at ${part.ref.slice(0, 12)} failed: ${(archived.stderr || '').trim().split('\n')[0]} (is that commit in the local repository?)`);
|
|
74
|
+
// Relative names and cwd: a drive letter in an argument reads as a remote host to GNU tar.
|
|
75
|
+
mkdirSync(join(staging, 'tree'), { recursive: true });
|
|
76
|
+
const unpacked = run('tar', ['-xf', tarName, '-C', 'tree'], { cwd: staging });
|
|
77
|
+
if (unpacked.status !== 0) fail(`tar -x failed: ${(unpacked.stderr || '').trim().split('\n')[0]}`);
|
|
78
|
+
rmSync(join(staging, tarName));
|
|
79
|
+
}
|
|
80
|
+
mkdirSync(resolve(file, '..'), { recursive: true });
|
|
81
|
+
const outName = 'export.tgz';
|
|
82
|
+
const packed = run('tar', ['-czf', outName, '-C', 'tree', '.'], { cwd: staging });
|
|
83
|
+
if (packed.status !== 0) fail(`tar -c failed: ${(packed.stderr || '').trim().split('\n')[0]}`);
|
|
84
|
+
writeFileSync(file, readFileSync(join(staging, outName)));
|
|
85
|
+
} finally {
|
|
86
|
+
rmSync(staging, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
const bytes = statSync(file).size;
|
|
89
|
+
log(`export: ${sha.slice(0, 12)}${submodules.map(s => ` + ${s.path}@${s.sha.slice(0, 12)}`).join('')} -> ${(bytes / 1048576).toFixed(1)} MB${dirty ? ` (${dirty} tracked file(s) changed in the working tree are NOT included)` : ''}`);
|
|
90
|
+
return { sha, submodules, file, bytes, dirty };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Remote work folders are relative to the login home. */
|
|
94
|
+
const workDirOf = (dir, artifact) => (dir ?? `waybetter-builds/${artifact}`).replace(/^~\//, '').replace(/^\/+/, '') || `waybetter-builds/${artifact}`;
|
|
95
|
+
const sha256Line = 'if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@"; else sha256sum "$@"; fi';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The macOS build script (bash, run in a login shell on the Mac): unpack, install, universal
|
|
99
|
+
* ad-hoc-signed .app and .dmg through build-env, the updater bundle as a tar.gz of the .app; the
|
|
100
|
+
* dmg and the tar.gz under space-free names, and the .app itself, in `out/`. It never installs,
|
|
101
|
+
* quits or replaces an app on the Mac: whoever runs the app there swaps it in.
|
|
102
|
+
*/
|
|
103
|
+
export function macBuildScript({ workDir, sha, artifact, productName, version, desktopDir, prepareCommand }) {
|
|
104
|
+
const app = `${productName}.app`;
|
|
105
|
+
return `#!/bin/bash
|
|
106
|
+
set -eu
|
|
107
|
+
# Low priority: the Mac may be recording or running an app while this compiles.
|
|
108
|
+
renice -n 10 $$ >/dev/null 2>&1 || true
|
|
109
|
+
# rustup first: Homebrew's Rust has no x86_64 standard library, which a universal build needs.
|
|
110
|
+
export PATH="$HOME/.cargo/bin:$PATH"
|
|
111
|
+
WORK="$HOME/${workDir}"
|
|
112
|
+
SRC="$WORK/src"
|
|
113
|
+
OUT="$WORK/out"
|
|
114
|
+
cd "$WORK"
|
|
115
|
+
rm -rf "$SRC" "$OUT"
|
|
116
|
+
# -m: files get the time of unpacking. git archive stamps every file with its commit time, which is older
|
|
117
|
+
# than the cached build in CARGO_TARGET_DIR: cargo would take a changed source for unchanged and reuse stale objects.
|
|
118
|
+
mkdir -p "$SRC" "$OUT"
|
|
119
|
+
tar -xzmf "$WORK/source-${sha.slice(0, 12)}.tgz" -C "$SRC"
|
|
120
|
+
cd "$SRC"
|
|
121
|
+
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
|
122
|
+
${prepareCommand}
|
|
123
|
+
cd ${shq(desktopDir)}
|
|
124
|
+
# Ad-hoc signature (no Developer ID yet: Gatekeeper's "Open Anyway" path, not "is damaged"), no
|
|
125
|
+
# updater bundle from Tauri (it needs the signing key, which never leaves the release machine).
|
|
126
|
+
printf '%s' '{"bundle":{"createUpdaterArtifacts":false,"macOS":{"signingIdentity":"-"}}}' > "$WORK/override.json"
|
|
127
|
+
export CARGO_TARGET_DIR="$WORK/target"
|
|
128
|
+
bun "$SRC/waybetter/packages/waybetter/bin/waybetter.mjs" build-env -- bunx tauri build --target universal-apple-darwin --bundles app,dmg --ci --config "$WORK/override.json"
|
|
129
|
+
BUNDLE="$CARGO_TARGET_DIR/universal-apple-darwin/release/bundle"
|
|
130
|
+
DMG="$(ls "$BUNDLE"/dmg/*_${version}_universal.dmg | head -1)"
|
|
131
|
+
cp "$DMG" "$OUT/${artifact}_${version}_universal.dmg"
|
|
132
|
+
# Owner 0/root: the archive must not carry the Mac account name in its entry headers.
|
|
133
|
+
COPYFILE_DISABLE=1 tar --uid 0 --gid 0 --uname root --gname wheel -czf "$OUT/${artifact}_${version}_universal.app.tar.gz" -C "$BUNDLE/macos" ${shq(app)}
|
|
134
|
+
ditto "$BUNDLE/macos/"${shq(app)} "$OUT/"${shq(app)}
|
|
135
|
+
cd "$OUT"
|
|
136
|
+
set -- *.dmg *.tar.gz
|
|
137
|
+
${sha256Line} > SHA256SUMS
|
|
138
|
+
ls -ln "$OUT"
|
|
139
|
+
echo "REMOTE_BUILD_OK ${sha.slice(0, 12)}"
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The Linux build script (bash, run in WSL or on any Ubuntu 22.04): clean PATH, unpack, install,
|
|
145
|
+
* AppImage and .deb through build-env with Tauri's updater bundle off (on Linux the signed
|
|
146
|
+
* AppImage itself is the update payload), copied to `out/` under space-free names.
|
|
147
|
+
*/
|
|
148
|
+
export function linuxBuildScript({ workDir, sha, artifact, version, desktopDir, prepareCommand, tarball, outDir }) {
|
|
149
|
+
return `#!/bin/bash
|
|
150
|
+
set -eu
|
|
151
|
+
# A clean PATH: under WSL the Windows programs (a Windows bun, node, cargo) would shadow these.
|
|
152
|
+
export PATH="$HOME/.bun/bin:$HOME/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
153
|
+
export APPIMAGE_EXTRACT_AND_RUN=1
|
|
154
|
+
WORK="$HOME/${workDir}"
|
|
155
|
+
SRC="$WORK/src"
|
|
156
|
+
cd "$HOME"
|
|
157
|
+
mkdir -p "$WORK"
|
|
158
|
+
rm -rf "$SRC"
|
|
159
|
+
mkdir -p "$SRC"
|
|
160
|
+
tar -xzmf "$(wslpath -u ${shq(tarball)} 2>/dev/null || echo ${shq(tarball)})" -C "$SRC"
|
|
161
|
+
cd "$SRC"
|
|
162
|
+
${prepareCommand}
|
|
163
|
+
cd ${shq(desktopDir)}
|
|
164
|
+
printf '%s' '{"bundle":{"createUpdaterArtifacts":false}}' > "$WORK/override.json"
|
|
165
|
+
export CARGO_TARGET_DIR="$WORK/target"
|
|
166
|
+
bun "$SRC/waybetter/packages/waybetter/bin/waybetter.mjs" build-env -- bunx tauri build --bundles appimage,deb --ci --config "$WORK/override.json"
|
|
167
|
+
BUNDLE="$CARGO_TARGET_DIR/release/bundle"
|
|
168
|
+
OUTDIR="$(wslpath -u ${shq(outDir)} 2>/dev/null || echo ${shq(outDir)})"
|
|
169
|
+
mkdir -p "$OUTDIR"
|
|
170
|
+
cp "$(ls "$BUNDLE"/appimage/*_${version}_amd64.AppImage | head -1)" "$OUTDIR/${artifact}_${version}_amd64.AppImage"
|
|
171
|
+
cp "$(ls "$BUNDLE"/deb/*_${version}_amd64.deb | head -1)" "$OUTDIR/${artifact}_${version}_amd64.deb"
|
|
172
|
+
ls -ln "$OUTDIR"
|
|
173
|
+
echo "REMOTE_BUILD_OK ${sha.slice(0, 12)}"
|
|
174
|
+
`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const sshOptions = ({ key }) => ['-o', 'BatchMode=yes', '-o', 'IdentitiesOnly=yes', '-o', 'ConnectTimeout=15', '-o', 'ServerAliveInterval=30', ...(key ? ['-i', key] : [])];
|
|
178
|
+
|
|
179
|
+
/** Runs a command and streams its output; the exit code decides. */
|
|
180
|
+
function stream(command, args, { log, label }) {
|
|
181
|
+
log(`${label}: ${command} ${args.map(a => (/\s/.test(a) ? '"…"' : a)).join(' ').slice(0, 160)}`);
|
|
182
|
+
const result = spawnSync(command, args, { stdio: 'inherit', windowsHide: true });
|
|
183
|
+
if (result.error) fail(`${command}: ${result.error.message}`);
|
|
184
|
+
if (result.status !== 0) fail(`${label} failed (exit ${result.status})`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Builds the committed state of the app on a Mac over SSH and brings the .dmg and the .app.tar.gz
|
|
189
|
+
* back to `<outDir>/build/macos`. Returns `{ version, sha, dir, dmg, updater }`.
|
|
190
|
+
*/
|
|
191
|
+
export function buildMacRemote({ config, target, key, remoteDir, ref = 'HEAD', env = process.env, log = console.log }) {
|
|
192
|
+
if (!target || !/^[A-Za-z0-9._-]+@[A-Za-z0-9._:-]+$/.test(target)) fail('a Mac needs --remote user@host (or WAYBETTER_MAC_SSH)');
|
|
193
|
+
const { root } = config;
|
|
194
|
+
const version = JSON.parse(committedFile(root, 'package.json', ref)).version;
|
|
195
|
+
const productName = JSON.parse(committedFile(root, relTauri(config, 'tauri.conf.json'), ref)).productName;
|
|
196
|
+
const workDir = workDirOf(remoteDir, config.artifact);
|
|
197
|
+
const dir = join(config.outDir, 'build', 'macos');
|
|
198
|
+
mkdirSync(dir, { recursive: true });
|
|
199
|
+
for (const name of readdirSync(dir)) rmSync(join(dir, name), { force: true, recursive: true });
|
|
200
|
+
const scratch = mkdtempSync(join(tmpdir(), 'wb-mac-'));
|
|
201
|
+
try {
|
|
202
|
+
const exported = exportCommitted({ root, file: join(scratch, 'source.tgz'), ref, exclude: config.exportExclude, log });
|
|
203
|
+
const short = exported.sha.slice(0, 12);
|
|
204
|
+
const script = macBuildScript({ workDir, sha: exported.sha, artifact: config.artifact, productName, version, desktopDir: relative(root, config.desktopDir).replaceAll('\\', '/'), prepareCommand: config.prepareCommand });
|
|
205
|
+
writeFileSync(join(scratch, 'build.sh'), script.replaceAll('\r\n', '\n'));
|
|
206
|
+
const ssh = sshOptions({ key });
|
|
207
|
+
stream('ssh', [...ssh, target, `mkdir -p "$HOME/${workDir}" && find "$HOME/${workDir}" -maxdepth 1 -name 'source-*.tgz' -delete`], { log, label: 'mac: prepare' });
|
|
208
|
+
stream('scp', [...ssh, join(scratch, 'source.tgz'), `${target}:${workDir}/source-${short}.tgz`], { log, label: 'mac: upload' });
|
|
209
|
+
stream('scp', [...ssh, join(scratch, 'build.sh'), `${target}:${workDir}/build.sh`], { log, label: 'mac: upload script' });
|
|
210
|
+
const started = Date.now();
|
|
211
|
+
// A login shell: bun, cargo and node are set up in the Mac account's profile only.
|
|
212
|
+
stream('ssh', [...ssh, target, `zsh -lic "bash $HOME/${workDir}/build.sh"`], { log, label: 'mac: build' });
|
|
213
|
+
log(`mac: build took ${Math.round((Date.now() - started) / 1000)} s`);
|
|
214
|
+
stream('scp', [...ssh, `${target}:${workDir}/out/${config.artifact}_${version}_universal.dmg`, `${target}:${workDir}/out/${config.artifact}_${version}_universal.app.tar.gz`, dir], { log, label: 'mac: fetch' });
|
|
215
|
+
return { version, sha: exported.sha, dir, dmg: join(dir, `${config.artifact}_${version}_universal.dmg`), updater: join(dir, `${config.artifact}_${version}_universal.app.tar.gz`) };
|
|
216
|
+
} finally {
|
|
217
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Builds the committed state of the app in a WSL distro (Ubuntu 22.04: the glibc floor) and puts
|
|
223
|
+
* the AppImage and the .deb in `<outDir>/build/linux`. Returns `{ version, sha, dir, appimage, deb }`.
|
|
224
|
+
*/
|
|
225
|
+
export function buildLinuxWsl({ config, distro = 'Ubuntu-22.04', remoteDir, ref = 'HEAD', env = process.env, log = console.log }) {
|
|
226
|
+
if (process.platform !== 'win32') fail('--wsl runs from Windows; on Linux run the build on the machine itself');
|
|
227
|
+
if (!/^[A-Za-z0-9._-]+$/.test(distro)) fail('bad WSL distro name');
|
|
228
|
+
const { root } = config;
|
|
229
|
+
const version = JSON.parse(committedFile(root, 'package.json', ref)).version;
|
|
230
|
+
const workDir = workDirOf(remoteDir, config.artifact);
|
|
231
|
+
const dir = join(config.outDir, 'build', 'linux');
|
|
232
|
+
mkdirSync(dir, { recursive: true });
|
|
233
|
+
for (const name of readdirSync(dir)) rmSync(join(dir, name), { force: true, recursive: true });
|
|
234
|
+
const scratch = mkdtempSync(join(tmpdir(), 'wb-linux-'));
|
|
235
|
+
try {
|
|
236
|
+
const exported = exportCommitted({ root, file: join(scratch, 'source.tgz'), ref, exclude: config.exportExclude, log });
|
|
237
|
+
const script = linuxBuildScript({ workDir, sha: exported.sha, artifact: config.artifact, version, desktopDir: relative(root, config.desktopDir).replaceAll('\\', '/'), prepareCommand: config.prepareCommand, tarball: join(scratch, 'source.tgz'), outDir: dir });
|
|
238
|
+
writeFileSync(join(scratch, 'build.sh'), script.replaceAll('\r\n', '\n'));
|
|
239
|
+
const started = Date.now();
|
|
240
|
+
// MSYS_NO_PATHCONV: Git Bash must not rewrite the /mnt paths of the script.
|
|
241
|
+
const wslScript = `/mnt/${scratch[0].toLowerCase()}/${scratch.slice(3).replaceAll('\\', '/')}/build.sh`;
|
|
242
|
+
const result = spawnSync('wsl', ['-d', distro, '--', 'bash', wslScript], { stdio: 'inherit', windowsHide: true, env: { ...env, MSYS_NO_PATHCONV: '1' } });
|
|
243
|
+
if (result.status !== 0) fail(`linux build in WSL ${distro} failed (exit ${result.status})`);
|
|
244
|
+
log(`linux: build took ${Math.round((Date.now() - started) / 1000)} s`);
|
|
245
|
+
return { version, sha: exported.sha, dir, appimage: join(dir, `${config.artifact}_${version}_amd64.AppImage`), deb: join(dir, `${config.artifact}_${version}_amd64.deb`) };
|
|
246
|
+
} finally {
|
|
247
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const relTauri = (config, name) => `${relative(config.root, config.tauriDir).replaceAll('\\', '/')}/${name}`;
|
|
252
|
+
|
|
253
|
+
/** Prints the built artifacts' sizes and runs the optional release check on them here. */
|
|
254
|
+
export function scanBuilt(config, files, { env = process.env, log = console.log } = {}) {
|
|
255
|
+
for (const file of files) {
|
|
256
|
+
if (!existsSync(file)) fail(`expected build output missing: ${file}`);
|
|
257
|
+
log(`built: ${basename(file)} ${(statSync(file).size / 1048576).toFixed(1)} MB`);
|
|
258
|
+
}
|
|
259
|
+
runReleaseCheck(files, { env, log, label: 'release: check' });
|
|
260
|
+
}
|
package/cli/store.mjs
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// `waybetter store build|sign|verify`: builds an app's part of the shared wayBetter Store catalog,
|
|
2
|
+
// signs it offline with the store's Ed25519 key, and verifies it. The rules are the ones the apps
|
|
3
|
+
// enforce natively (waybetter-host `store::catalog`); docs/STORE.md in the framework has the format.
|
|
4
|
+
//
|
|
5
|
+
// waybetter store build --config store.config.mjs [--out dir] [--base https://…/store/]
|
|
6
|
+
// waybetter store sign [catalog.json] --key <private key> [--pub <public key file>]
|
|
7
|
+
// waybetter store sign --init --key <private key> --pub <public key file> (once; never overwrites)
|
|
8
|
+
// waybetter store verify [catalog.json] --pub <public key file> [--app id]
|
|
9
|
+
//
|
|
10
|
+
// The private key is a PKCS#8 PEM that stays where it is: nothing here prints, copies or moves it.
|
|
11
|
+
// `--key` defaults to $WAYBETTER_STORE_KEY. The signature file `<catalog>.sig` is the base64 of the
|
|
12
|
+
// 64-byte signature over the exact catalog bytes; a public key file is the base64 of the raw 32 bytes.
|
|
13
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from 'node:crypto';
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Two catalog files, one source. `catalog.json` is format 2 and lists only the kinds every app
|
|
20
|
+
* since the first release knows (theme, plugin, strings): an app from before a kind existed refuses
|
|
21
|
+
* a whole catalog that lists it. `catalog.v3.json` is format 3, lists everything (icons, builtin, ...)
|
|
22
|
+
* and is read first by apps that know it; they fall back to `catalog.json` when it is not published.
|
|
23
|
+
* `waybetter store build` writes both from the same items; `sign` and `verify` handle both.
|
|
24
|
+
*/
|
|
25
|
+
export const CATALOG_FORMAT = 2;
|
|
26
|
+
export const CATALOG_FORMAT_NEXT = 3;
|
|
27
|
+
export const CATALOG_FILE = 'catalog.json';
|
|
28
|
+
export const CATALOG_FILE_NEXT = 'catalog.v3.json';
|
|
29
|
+
/** The kinds format 2 may list; a newer kind is format 3 only. */
|
|
30
|
+
export const LEGACY_KINDS = ['theme', 'plugin', 'strings'];
|
|
31
|
+
export const DEFAULT_BASE = 'https://waybetter.fyi/store/';
|
|
32
|
+
/** The framework's download hosts (waybetter-host `DEFAULT_HOSTS`). */
|
|
33
|
+
export const DEFAULT_HOSTS = [
|
|
34
|
+
{ host: 'waybetter.fyi', rawFilesOnly: false },
|
|
35
|
+
{ host: 'cdn1.crt.fyi', rawFilesOnly: false },
|
|
36
|
+
{ host: 'git.crt.fyi', rawFilesOnly: true },
|
|
37
|
+
];
|
|
38
|
+
export const PERMISSIONS = ['storage', 'ui', 'clipboard', 'account', 'users', 'commands', 'network', 'network:none'];
|
|
39
|
+
const MAX_BYTES = 2 * 1024 * 1024;
|
|
40
|
+
/** An icon pack file (the runtime's `ICON_LIMITS.packBytes`). */
|
|
41
|
+
const MAX_ICONS_BYTES = 120_000;
|
|
42
|
+
const KINDS = ['theme', 'plugin', 'strings', 'icons', 'builtin'];
|
|
43
|
+
const MAX_ITEMS = 500;
|
|
44
|
+
const MAX_CATALOG_BYTES = 1024 * 1024;
|
|
45
|
+
// SPKI DER prefix of an Ed25519 public key; the raw 32 bytes follow it.
|
|
46
|
+
const SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
47
|
+
|
|
48
|
+
export const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
49
|
+
|
|
50
|
+
/** A plugin header (waybetter-host `parse_meta`), enough for the catalog: name, description, version, author, permissions. */
|
|
51
|
+
export function pluginHeader(source) {
|
|
52
|
+
const meta = { name: '', description: '', version: '', author: '', permissions: null };
|
|
53
|
+
const body = /^?\s*\/\*\*([\s\S]*?)\*\//.exec(source)?.[1];
|
|
54
|
+
if (!body) return meta;
|
|
55
|
+
for (const raw of body.split(/\r?\n/)) {
|
|
56
|
+
const line = raw.trim().replace(/^\*+/, '').trim();
|
|
57
|
+
const match = /^@(\w+)\s*(.*)$/.exec(line);
|
|
58
|
+
if (!match) continue;
|
|
59
|
+
const [, key, value] = match;
|
|
60
|
+
if (key === 'permissions') {
|
|
61
|
+
const list = value.split(/[\s,]+/).map(token => token.toLowerCase()).filter(token => token && token !== 'none');
|
|
62
|
+
for (const token of list) if (!PERMISSIONS.includes(token)) throw new Error(`unknown permission ${token}`);
|
|
63
|
+
meta.permissions = [...new Set([...(meta.permissions ?? []), ...list])];
|
|
64
|
+
// Same cap as the plugin store's parse_meta (300 characters, no control characters).
|
|
65
|
+
} else if (key in meta) meta[key] = [...value.trim()].filter(ch => ch >= ' ').slice(0, 300).join('');
|
|
66
|
+
}
|
|
67
|
+
return meta;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- Validation (the same rules as waybetter-host store::catalog) ------------------------------
|
|
71
|
+
|
|
72
|
+
const validAppId = id => typeof id === 'string' && /^[a-z0-9-]{1,32}$/.test(id);
|
|
73
|
+
const validId = id => typeof id === 'string' && /^[a-z0-9-]{1,40}$/.test(id);
|
|
74
|
+
const text = (value, max) => typeof value === 'string' && [...value].length <= max && !/[\p{Cc}]/u.test(value);
|
|
75
|
+
const hex = value => typeof value === 'string' && /^#[0-9a-fA-F]{6}$/.test(value);
|
|
76
|
+
export const validFileName = file => typeof file === 'string' && file.length > '.plugin.js'.length && file.length <= 128
|
|
77
|
+
&& file.endsWith('.plugin.js') && !file.startsWith('.') && /^[A-Za-z0-9_\-. ]+$/.test(file);
|
|
78
|
+
|
|
79
|
+
/** An https URL native code may download from (waybetter-host `allowed_download_url`). */
|
|
80
|
+
export function allowedUrl(value, hosts = DEFAULT_HOSTS) {
|
|
81
|
+
let url;
|
|
82
|
+
try { url = new URL(value); } catch { return false; }
|
|
83
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.port || url.hash || url.href.length > 2048) return false;
|
|
84
|
+
const rule = hosts.find(h => h.host === url.hostname);
|
|
85
|
+
if (!rule) return false;
|
|
86
|
+
if (rule.rawFilesOnly) {
|
|
87
|
+
const segments = url.pathname.split('/').slice(1);
|
|
88
|
+
return segments.length >= 4 && segments[2] === 'raw' && segments.every(s => s && s !== '..');
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const ITEM_FIELDS = new Set(['apps', 'kind', 'id', 'name', 'author', 'version', 'description', 'category', 'tags', 'url', 'file', 'sha256', 'size', 'preview', 'permissions', 'colors', 'language', 'license', 'credits']);
|
|
94
|
+
const LANGUAGE = /^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$/;
|
|
95
|
+
|
|
96
|
+
/** Problems with one item; `hosts` checks the download hosts (the reading app's own items). */
|
|
97
|
+
export function itemProblems(item, hosts) {
|
|
98
|
+
const bad = [];
|
|
99
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return ['not an object'];
|
|
100
|
+
for (const key of Object.keys(item)) if (!ITEM_FIELDS.has(key)) bad.push(`unknown field ${key}`);
|
|
101
|
+
if (!Array.isArray(item.apps) || !item.apps.length || item.apps.length > 16 || !item.apps.every(validAppId) || new Set(item.apps).size !== item.apps.length) bad.push('apps');
|
|
102
|
+
if (!KINDS.includes(item.kind)) bad.push('kind');
|
|
103
|
+
// Any item may name its licence and credit its authors; only a string pack has a language.
|
|
104
|
+
if ((item.license !== undefined && (!text(item.license, 60) || !String(item.license).trim())) || (item.credits !== undefined && !text(item.credits, 600))) bad.push('license or credits');
|
|
105
|
+
if (item.kind !== 'strings' && item.language !== undefined) bad.push('language');
|
|
106
|
+
if (!validId(item.id)) bad.push('id');
|
|
107
|
+
if (!text(item.name, 60) || !String(item.name).trim() || !text(item.author ?? '', 60) || !text(item.description ?? '', 600)) bad.push('name, author or description');
|
|
108
|
+
if (!text(item.version, 32) || !item.version || !text(item.category ?? '', 32)) bad.push('version or category');
|
|
109
|
+
if (item.tags !== undefined && (!Array.isArray(item.tags) || item.tags.length > 12 || !item.tags.every(tag => text(tag, 32)))) bad.push('tags');
|
|
110
|
+
if (item.kind === 'builtin') {
|
|
111
|
+
// A feature that ships inside the app (an area plugin): nothing to download, so nothing to point at or hash.
|
|
112
|
+
for (const key of ['url', 'sha256', 'size', 'file', 'permissions', 'colors', 'language']) if (item[key] !== undefined) bad.push('built-in item fields');
|
|
113
|
+
} else {
|
|
114
|
+
if (typeof item.url !== 'string' || !item.url.startsWith('https://') || (hosts && !allowedUrl(item.url, hosts))) bad.push('url (host not allowed)');
|
|
115
|
+
}
|
|
116
|
+
if (item.preview !== undefined && (typeof item.preview !== 'string' || !item.preview.startsWith('https://') || (hosts && !allowedUrl(item.preview, hosts)))) bad.push('preview');
|
|
117
|
+
if (item.kind !== 'builtin') {
|
|
118
|
+
if (typeof item.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(item.sha256)) bad.push('sha256');
|
|
119
|
+
if (!Number.isInteger(item.size) || item.size <= 0 || item.size > (item.kind === 'icons' ? MAX_ICONS_BYTES : MAX_BYTES)) bad.push('size');
|
|
120
|
+
}
|
|
121
|
+
if (item.kind === 'icons' && (item.file !== undefined || item.permissions !== undefined || item.colors !== undefined)) bad.push('icon pack fields');
|
|
122
|
+
if (item.kind === 'plugin') {
|
|
123
|
+
if (!validFileName(item.file) || item.colors != null) bad.push('plugin file');
|
|
124
|
+
const p = item.permissions;
|
|
125
|
+
if (p != null && (!Array.isArray(p) || !p.every(x => PERMISSIONS.includes(x)) || new Set(p).size !== p.length || (p.includes('network') && p.includes('network:none')))) bad.push('permissions');
|
|
126
|
+
}
|
|
127
|
+
if (item.kind === 'strings') {
|
|
128
|
+
if (item.file !== undefined || item.permissions !== undefined || item.colors !== undefined) bad.push('string pack fields');
|
|
129
|
+
if (typeof item.language !== 'string' || !LANGUAGE.test(item.language)) bad.push('language');
|
|
130
|
+
}
|
|
131
|
+
if (item.kind === 'theme') {
|
|
132
|
+
if (item.file !== undefined || item.permissions !== undefined) bad.push('theme fields');
|
|
133
|
+
const c = item.colors;
|
|
134
|
+
if (c != null && (typeof c !== 'object' || Object.keys(c).some(k => !['bg', 'surface', 'text', 'accent'].includes(k)) || !hex(c.bg) || !hex(c.text) || !hex(c.accent) || (c.surface !== undefined && !hex(c.surface)))) bad.push('colors');
|
|
135
|
+
}
|
|
136
|
+
return bad;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Problems with a whole catalog (empty = valid). `app`: that app's items are held to `hosts`. */
|
|
140
|
+
export function catalogProblems(catalog, { app, hosts = DEFAULT_HOSTS } = {}) {
|
|
141
|
+
const out = [];
|
|
142
|
+
if (!catalog || typeof catalog !== 'object') return ['catalog: not an object'];
|
|
143
|
+
for (const key of Object.keys(catalog)) if (!['format', 'name', 'generatedAt', 'items'].includes(key)) out.push(`catalog: unknown field ${key}`);
|
|
144
|
+
if (catalog.format !== CATALOG_FORMAT && catalog.format !== CATALOG_FORMAT_NEXT) out.push(`catalog: format must be ${CATALOG_FORMAT} or ${CATALOG_FORMAT_NEXT}`);
|
|
145
|
+
if (typeof catalog.name !== 'string' || typeof catalog.generatedAt !== 'string') out.push('catalog: name and generatedAt are strings');
|
|
146
|
+
if (!Array.isArray(catalog.items)) return [...out, 'catalog: items must be a list'];
|
|
147
|
+
if (catalog.items.length > MAX_ITEMS) out.push(`catalog: more than ${MAX_ITEMS} items`);
|
|
148
|
+
const ids = new Set();
|
|
149
|
+
const files = new Set();
|
|
150
|
+
for (const item of catalog.items) {
|
|
151
|
+
const where = `item ${item?.kind}:${item?.id}`;
|
|
152
|
+
for (const problem of itemProblems(item, app && item?.apps?.includes(app) ? hosts : undefined)) out.push(`${where}: invalid ${problem}`);
|
|
153
|
+
if (catalog.format === CATALOG_FORMAT && !LEGACY_KINDS.includes(item?.kind)) out.push(`${where}: a format ${CATALOG_FORMAT} catalog lists only ${LEGACY_KINDS.join(', ')} (older apps refuse the whole file otherwise); this kind belongs in ${CATALOG_FILE_NEXT}`);
|
|
154
|
+
for (const a of Array.isArray(item?.apps) ? item.apps : []) {
|
|
155
|
+
const id = `${a}/${item.kind}/${item.id}`;
|
|
156
|
+
if (ids.has(id)) out.push(`${where}: duplicate for app ${a}`);
|
|
157
|
+
ids.add(id);
|
|
158
|
+
if (item.file) {
|
|
159
|
+
const file = `${a}/${String(item.file).toLowerCase()}`;
|
|
160
|
+
if (files.has(file)) out.push(`${where}: duplicate plugin file for app ${a}`);
|
|
161
|
+
files.add(file);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---- Build ------------------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Builds `config.app`'s part of the shared catalog in `out`: writes the files the config returns,
|
|
172
|
+
* replaces the app's items (other apps' items stay as they are), removes files only the app's old
|
|
173
|
+
* items used, validates the whole catalog and writes `catalog.v3.json` (every item) and `catalog.json`
|
|
174
|
+
* (the kinds older apps know), sign both afterwards.
|
|
175
|
+
*
|
|
176
|
+
* config: { app, name?, base?, out?, hosts?, items(context) → [{ kind, id, name, author, version,
|
|
177
|
+
* description, category, tags, file (plugins), permissions (plugins), colors (themes), language
|
|
178
|
+
* (string packs), license, credits,
|
|
179
|
+
* path ('plugins/x.plugin.js'), bytes (Buffer|string), preview?: { path, bytes } }] }
|
|
180
|
+
* Kinds: theme, plugin, strings, icons (an icon pack, `icons/<id>.icons.json`) and builtin (a
|
|
181
|
+
* feature that ships inside the app, shown in the Store as built in: no path or bytes).
|
|
182
|
+
*/
|
|
183
|
+
export async function buildStore(config, options = {}) {
|
|
184
|
+
const app = config.app;
|
|
185
|
+
if (!validAppId(app)) throw new Error('config.app must be an app id (a-z, 0-9, -)');
|
|
186
|
+
const base = options.base ?? config.base ?? DEFAULT_BASE;
|
|
187
|
+
if (!/^https:\/\/[^/]+\/(.*\/)?$/.test(base)) throw new Error('base must be an https URL ending in /');
|
|
188
|
+
const out = resolve(options.out ?? config.out ?? '.');
|
|
189
|
+
const hosts = config.hosts ?? DEFAULT_HOSTS;
|
|
190
|
+
const catalogPath = join(out, CATALOG_FILE);
|
|
191
|
+
const nextPath = join(out, CATALOG_FILE_NEXT);
|
|
192
|
+
// The full list (format 3) is what to start from: it has every app's items of every kind.
|
|
193
|
+
const source = existsSync(nextPath) ? nextPath : catalogPath;
|
|
194
|
+
const previous = existsSync(source) ? JSON.parse(readFileSync(source, 'utf8')) : null;
|
|
195
|
+
const kept = [];
|
|
196
|
+
const oldFiles = new Set();
|
|
197
|
+
for (const item of previous?.format === CATALOG_FORMAT || previous?.format === CATALOG_FORMAT_NEXT ? previous.items : []) {
|
|
198
|
+
if (!item.apps?.includes(app)) { kept.push(item); continue; }
|
|
199
|
+
for (const url of [item.url, item.preview]) if (url?.startsWith(base)) oldFiles.add(url.slice(base.length));
|
|
200
|
+
const others = item.apps.filter(a => a !== app);
|
|
201
|
+
if (others.length) kept.push({ ...item, apps: others });
|
|
202
|
+
}
|
|
203
|
+
const inside = path => {
|
|
204
|
+
const target = resolve(out, path);
|
|
205
|
+
const rel = relative(out, target);
|
|
206
|
+
if (!rel || rel.startsWith('..') || isAbsolute(rel) || !/^[\w.\-/]+$/.test(path)) throw new Error(`file path must stay inside the store folder: ${path}`);
|
|
207
|
+
return target;
|
|
208
|
+
};
|
|
209
|
+
const entries = await config.items({ app, base, out });
|
|
210
|
+
const items = [];
|
|
211
|
+
const files = new Map();
|
|
212
|
+
const toBytes = value => (Buffer.isBuffer(value) ? value : Buffer.from(String(value), 'utf8'));
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
const item = { apps: [app], kind: entry.kind, id: entry.id, name: entry.name, author: entry.author ?? '', version: entry.version, description: entry.description ?? '', category: entry.category ?? '', tags: entry.tags ?? [] };
|
|
215
|
+
// A built-in item has no file: the app already has it.
|
|
216
|
+
if (entry.kind !== 'builtin') {
|
|
217
|
+
const bytes = toBytes(entry.bytes);
|
|
218
|
+
files.set(entry.path, { target: inside(entry.path), bytes });
|
|
219
|
+
item.url = `${base}${entry.path}`;
|
|
220
|
+
if (entry.kind === 'plugin') item.file = entry.file;
|
|
221
|
+
item.sha256 = sha256(bytes);
|
|
222
|
+
item.size = bytes.length;
|
|
223
|
+
}
|
|
224
|
+
if (entry.preview) {
|
|
225
|
+
files.set(entry.preview.path, { target: inside(entry.preview.path), bytes: toBytes(entry.preview.bytes) });
|
|
226
|
+
item.preview = `${base}${entry.preview.path}`;
|
|
227
|
+
}
|
|
228
|
+
if (entry.kind === 'plugin') item.permissions = entry.permissions ?? null;
|
|
229
|
+
if (entry.kind === 'theme' && entry.colors) item.colors = entry.colors;
|
|
230
|
+
if (entry.kind === 'strings') item.language = entry.language;
|
|
231
|
+
if (entry.license !== undefined) item.license = entry.license;
|
|
232
|
+
if (entry.credits !== undefined) item.credits = entry.credits;
|
|
233
|
+
items.push(item);
|
|
234
|
+
}
|
|
235
|
+
const catalog = {
|
|
236
|
+
format: CATALOG_FORMAT_NEXT,
|
|
237
|
+
name: config.name ?? previous?.name ?? 'wayBetter Store',
|
|
238
|
+
generatedAt: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
239
|
+
items: [...kept, ...items],
|
|
240
|
+
};
|
|
241
|
+
// Nothing is written unless the whole catalog is valid.
|
|
242
|
+
// The same items twice: everything for apps that know the newer kinds, and what every app knows.
|
|
243
|
+
const legacy = { ...catalog, format: CATALOG_FORMAT, items: catalog.items.filter(item => LEGACY_KINDS.includes(item.kind)) };
|
|
244
|
+
const problems = [...catalogProblems(catalog, { app, hosts }), ...catalogProblems(legacy, { app, hosts })];
|
|
245
|
+
if (problems.length) throw new Error(`the catalog would be invalid:\n ${problems.join('\n ')}`);
|
|
246
|
+
const json = `${JSON.stringify(catalog, null, 2)}\n`;
|
|
247
|
+
const legacyJson = `${JSON.stringify(legacy, null, 2)}\n`;
|
|
248
|
+
if (Buffer.byteLength(json) > MAX_CATALOG_BYTES) throw new Error('the catalog is larger than apps accept (1 MiB)');
|
|
249
|
+
for (const { target, bytes } of files.values()) { mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, bytes); }
|
|
250
|
+
// Files this app's previous build wrote and no item uses any more (never another app's files).
|
|
251
|
+
const stillUsed = new Set(kept.flatMap(item => [item.url, item.preview]).filter(url => url?.startsWith(base)).map(url => url.slice(base.length)));
|
|
252
|
+
for (const path of oldFiles) if (!files.has(path) && !stillUsed.has(path)) rmSync(inside(path), { force: true });
|
|
253
|
+
mkdirSync(out, { recursive: true });
|
|
254
|
+
writeFileSync(nextPath, json);
|
|
255
|
+
writeFileSync(catalogPath, legacyJson);
|
|
256
|
+
return { catalog, legacy, path: catalogPath, nextPath, items };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---- Keys and signatures ----------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
export function rawPublicKey(publicKey) {
|
|
262
|
+
const der = publicKey.export({ format: 'der', type: 'spki' });
|
|
263
|
+
if (der.length !== 44 || !der.subarray(0, 12).equals(SPKI_PREFIX)) throw new Error('not an Ed25519 public key');
|
|
264
|
+
return der.subarray(12);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function publicKeyFromBase64(value) {
|
|
268
|
+
const raw = Buffer.from(value.trim(), 'base64');
|
|
269
|
+
if (raw.length !== 32) throw new Error('a public key file must be the base64 of 32 raw bytes');
|
|
270
|
+
return createPublicKey({ key: Buffer.concat([SPKI_PREFIX, raw]), format: 'der', type: 'spki' });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Creates the store key once (0600, never overwritten) and writes its public key file. */
|
|
274
|
+
export function initKey(keyPath, pubPath) {
|
|
275
|
+
if (existsSync(keyPath)) {
|
|
276
|
+
const base64 = rawPublicKey(createPublicKey(createPrivateKey(readFileSync(keyPath)))).toString('base64');
|
|
277
|
+
if (!existsSync(pubPath) || readFileSync(pubPath, 'utf8').trim() !== base64) { mkdirSync(dirname(pubPath), { recursive: true }); writeFileSync(pubPath, `${base64}\n`); }
|
|
278
|
+
return { created: false, publicKey: base64 };
|
|
279
|
+
}
|
|
280
|
+
mkdirSync(dirname(keyPath), { recursive: true });
|
|
281
|
+
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
282
|
+
writeFileSync(keyPath, privateKey.export({ format: 'pem', type: 'pkcs8' }), { mode: 0o600, flag: 'wx' });
|
|
283
|
+
const base64 = rawPublicKey(publicKey).toString('base64');
|
|
284
|
+
mkdirSync(dirname(pubPath), { recursive: true });
|
|
285
|
+
writeFileSync(pubPath, `${base64}\n`);
|
|
286
|
+
return { created: true, publicKey: base64 };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Signs the exact bytes of `file` into `<file>.sig`; returns the public key (base64) that verifies it. */
|
|
290
|
+
export function signFile(file, keyPath) {
|
|
291
|
+
const privateKey = createPrivateKey(readFileSync(keyPath));
|
|
292
|
+
const signature = sign(null, readFileSync(file), privateKey).toString('base64');
|
|
293
|
+
writeFileSync(`${file}.sig`, `${signature}\n`);
|
|
294
|
+
return rawPublicKey(createPublicKey(privateKey)).toString('base64');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Whether `<file>.sig` verifies with any of the public keys (base64 strings). */
|
|
298
|
+
export function verifyFile(file, publicKeys) {
|
|
299
|
+
const signature = Buffer.from(readFileSync(`${file}.sig`, 'utf8').trim(), 'base64');
|
|
300
|
+
if (signature.length !== 64) return false;
|
|
301
|
+
const body = readFileSync(file);
|
|
302
|
+
return publicKeys.some(key => verify(null, body, publicKeyFromBase64(key), signature));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---- Command line -----------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
const USAGE = `Usage:
|
|
308
|
+
waybetter store build --config <store.config.mjs> [--out <dir>] [--base <https://…/store/>]
|
|
309
|
+
waybetter store sign [catalog.json] --key <private key> [--pub <public key file>]
|
|
310
|
+
waybetter store sign --init --key <private key> --pub <public key file>
|
|
311
|
+
waybetter store verify [catalog.json] --pub <public key file>... [--app <id>]`;
|
|
312
|
+
|
|
313
|
+
function parse(argv) {
|
|
314
|
+
const out = { positional: [], pub: [], init: false };
|
|
315
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
316
|
+
const arg = argv[i];
|
|
317
|
+
const value = () => { const next = argv[++i]; if (next === undefined) throw new Error(`${arg} needs a value`); return next; };
|
|
318
|
+
if (arg === '--init') out.init = true;
|
|
319
|
+
else if (arg === '--pub') out.pub.push(resolve(value()));
|
|
320
|
+
else if (arg === '--key' || arg === '--config' || arg === '--out' || arg === '--base' || arg === '--app') out[arg.slice(2)] = value();
|
|
321
|
+
else if (arg.startsWith('--')) throw new Error(`unknown option ${arg}`);
|
|
322
|
+
else out.positional.push(arg);
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** `catalog.json` and, next to it, `catalog.v3.json` when it exists (one source, two files); any other file alone. */
|
|
328
|
+
export function catalogFiles(file) {
|
|
329
|
+
const next = join(dirname(file), CATALOG_FILE_NEXT);
|
|
330
|
+
return file.endsWith(CATALOG_FILE) && !file.endsWith(CATALOG_FILE_NEXT) && existsSync(next) ? [file, next] : [file];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Runs `waybetter store …`; returns the lines to print. Throws on errors. */
|
|
334
|
+
export async function storeCommand(argv) {
|
|
335
|
+
const [command, ...rest] = argv;
|
|
336
|
+
const options = parse(rest);
|
|
337
|
+
const catalog = resolve(options.positional[0] ?? 'catalog.json');
|
|
338
|
+
const key = options.key ? resolve(options.key) : process.env.WAYBETTER_STORE_KEY;
|
|
339
|
+
if (command === 'build') {
|
|
340
|
+
if (!options.config) throw new Error(`build needs --config\n${USAGE}`);
|
|
341
|
+
const config = (await import(pathToFileURL(resolve(options.config)).href)).default;
|
|
342
|
+
const { path, items } = await buildStore(config, { out: options.out, base: options.base });
|
|
343
|
+
const count = kind => items.filter(item => item.kind === kind).length;
|
|
344
|
+
return [`store: ${config.app}: ${count('plugin')} plugins, ${count('theme')} themes, ${count('icons')} icon packs, ${count('builtin')} built-in → ${path} and ${CATALOG_FILE_NEXT}`, 'Now sign both: waybetter store sign <catalog.json> --key <private key>'];
|
|
345
|
+
}
|
|
346
|
+
if (command === 'sign') {
|
|
347
|
+
if (!key) throw new Error('sign needs --key (or WAYBETTER_STORE_KEY)');
|
|
348
|
+
if (options.init) {
|
|
349
|
+
if (!options.pub.length) throw new Error('--init needs --pub');
|
|
350
|
+
const { created, publicKey } = initKey(key, options.pub[0]);
|
|
351
|
+
return [`${created ? 'Created the store key' : 'Store key already exists'}; public key ${publicKey} → ${options.pub[0]}`];
|
|
352
|
+
}
|
|
353
|
+
if (!existsSync(key)) throw new Error(`no store key at ${key} (create it once with --init)`);
|
|
354
|
+
const lines = [];
|
|
355
|
+
for (const file of catalogFiles(catalog)) {
|
|
356
|
+
const publicKey = signFile(file, key);
|
|
357
|
+
const keys = options.pub.length ? options.pub.map(pub => readFileSync(pub, 'utf8').trim()) : [publicKey];
|
|
358
|
+
if (!verifyFile(file, keys)) throw new Error(`signature of ${file} does NOT verify with ${options.pub.join(', ') || 'the key'}`);
|
|
359
|
+
lines.push(`Signed and verified ${file}.sig`);
|
|
360
|
+
}
|
|
361
|
+
return lines;
|
|
362
|
+
}
|
|
363
|
+
if (command === 'verify') {
|
|
364
|
+
if (!options.pub.length) throw new Error('verify needs --pub');
|
|
365
|
+
const keys = options.pub.map(file => readFileSync(file, 'utf8').trim());
|
|
366
|
+
const lines = [];
|
|
367
|
+
for (const file of catalogFiles(catalog)) {
|
|
368
|
+
if (!existsSync(`${file}.sig`) || !verifyFile(file, keys)) throw new Error(`signature of ${file} does NOT verify with ${options.pub.join(', ')}`);
|
|
369
|
+
const problems = catalogProblems(JSON.parse(readFileSync(file, 'utf8')), { app: options.app });
|
|
370
|
+
if (problems.length) throw new Error(`${file} is signed but invalid:\n ${problems.join('\n ')}`);
|
|
371
|
+
lines.push(`Verified ${file}.sig`);
|
|
372
|
+
}
|
|
373
|
+
return lines;
|
|
374
|
+
}
|
|
375
|
+
throw new Error(USAGE);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export { USAGE as STORE_USAGE };
|