@termix-ssh/plugin-sdk 1.0.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 +52 -0
- package/cli/commands/build.mjs +246 -0
- package/cli/commands/migrations.mjs +248 -0
- package/cli/commands/pack.mjs +42 -0
- package/cli/commands/sign.mjs +83 -0
- package/cli/commands/test.mjs +33 -0
- package/cli/commands/validate.mjs +215 -0
- package/cli/index.mjs +65 -0
- package/cli/lib/changelog.mjs +126 -0
- package/cli/lib/externals.mjs +46 -0
- package/cli/lib/plugin-dir.mjs +29 -0
- package/cli/lib/signing.mjs +86 -0
- package/cli/lib/static-url-imports.mjs +54 -0
- package/cli/lib/tmxplug.mjs +106 -0
- package/cli/patches/guacamole-common-js-flush.cjs +76 -0
- package/cli/patches/xterm-android-ime.cjs +130 -0
- package/client.d.ts +13 -0
- package/dist/backend.d.ts +1777 -0
- package/dist/backend.js +84 -0
- package/dist/capabilities.d.ts +38 -0
- package/dist/capabilities.js +71 -0
- package/dist/db.d.ts +112 -0
- package/dist/db.js +189 -0
- package/dist/ddl.d.ts +76 -0
- package/dist/ddl.js +678 -0
- package/dist/desktop.d.ts +346 -0
- package/dist/desktop.js +5 -0
- package/dist/frontend.d.ts +1206 -0
- package/dist/frontend.js +388 -0
- package/dist/host/chunk-F3SDZDDG.js +6017 -0
- package/dist/host/chunk-XGWE7BTJ.js +90 -0
- package/dist/host/chunk-XH5NXL6Y.js +56314 -0
- package/dist/host/chunk-YC2MAKSP.js +14 -0
- package/dist/host/linked-server-FZGWQW32.js +12 -0
- package/dist/host/rbac-api-6KDMBVDZ.js +52 -0
- package/dist/host/testing-host.js +414 -0
- package/dist/host/theme.css +91 -0
- package/dist/host/types/src/types/auth-protocols.d.ts +10 -0
- package/dist/host/types/src/types/connection-log.d.ts +13 -0
- package/dist/host/types/src/types/credential-sidebar-preferences.d.ts +38 -0
- package/dist/host/types/src/types/host-defaults.d.ts +60 -0
- package/dist/host/types/src/types/host-sidebar-preferences.d.ts +55 -0
- package/dist/host/types/src/types/index.d.ts +298 -0
- package/dist/host/types/src/types/keybindings.d.ts +27 -0
- package/dist/host/types/src/types/ui-preferences.d.ts +102 -0
- package/dist/host/types/src/types/ui-types.d.ts +260 -0
- package/dist/host/types/src/ui/api/admin-user-data-api.d.ts +11 -0
- package/dist/host/types/src/ui/api/auth-methods-api.d.ts +63 -0
- package/dist/host/types/src/ui/api/credential-sidebar-preferences-api.d.ts +3 -0
- package/dist/host/types/src/ui/api/credentials-api.d.ts +56 -0
- package/dist/host/types/src/ui/api/dashboard-api.d.ts +23 -0
- package/dist/host/types/src/ui/api/external-account-api.d.ts +8 -0
- package/dist/host/types/src/ui/api/host-sidebar-preferences-api.d.ts +3 -0
- package/dist/host/types/src/ui/api/host-status-api.d.ts +11 -0
- package/dist/host/types/src/ui/api/open-tabs-api.d.ts +74 -0
- package/dist/host/types/src/ui/api/plugins-api.d.ts +128 -0
- package/dist/host/types/src/ui/api/rbac-api.d.ts +167 -0
- package/dist/host/types/src/ui/api/settings-api.d.ts +22 -0
- package/dist/host/types/src/ui/api/ssh-host-management-api.d.ts +65 -0
- package/dist/host/types/src/ui/api/sync-api.d.ts +93 -0
- package/dist/host/types/src/ui/api/system-status-api.d.ts +49 -0
- package/dist/host/types/src/ui/api/ui-preferences-api.d.ts +8 -0
- package/dist/host/types/src/ui/api/user-management-api.d.ts +80 -0
- package/dist/host/types/src/ui/auth/Auth.d.ts +12 -0
- package/dist/host/types/src/ui/auth/auth-bits.d.ts +11 -0
- package/dist/host/types/src/ui/auth/silent-signin.d.ts +2 -0
- package/dist/host/types/src/ui/components/FullScreenAppWrapper.d.ts +9 -0
- package/dist/host/types/src/ui/components/alert-dialog.d.ts +14 -0
- package/dist/host/types/src/ui/components/alert.d.ts +9 -0
- package/dist/host/types/src/ui/components/badge.d.ts +9 -0
- package/dist/host/types/src/ui/components/button.d.ts +13 -0
- package/dist/host/types/src/ui/components/card-grid/CardGridCanvas.d.ts +22 -0
- package/dist/host/types/src/ui/components/card-grid/layout-utils.d.ts +26 -0
- package/dist/host/types/src/ui/components/card-grid/types.d.ts +29 -0
- package/dist/host/types/src/ui/components/card-masonry.d.ts +16 -0
- package/dist/host/types/src/ui/components/card.d.ts +11 -0
- package/dist/host/types/src/ui/components/charts/BarSeries.d.ts +16 -0
- package/dist/host/types/src/ui/components/charts/LineChart.d.ts +17 -0
- package/dist/host/types/src/ui/components/charts/RadialGauge.d.ts +18 -0
- package/dist/host/types/src/ui/components/charts/Sparkline.d.ts +22 -0
- package/dist/host/types/src/ui/components/charts/StatRow.d.ts +15 -0
- package/dist/host/types/src/ui/components/charts/geometry.d.ts +20 -0
- package/dist/host/types/src/ui/components/charts/index.d.ts +4 -0
- package/dist/host/types/src/ui/components/checkbox.d.ts +4 -0
- package/dist/host/types/src/ui/components/connection/ConnectionGate.d.ts +56 -0
- package/dist/host/types/src/ui/components/connection/ConnectionLogPanel.d.ts +9 -0
- package/dist/host/types/src/ui/components/connection/ConnectionScreen.d.ts +34 -0
- package/dist/host/types/src/ui/components/connection/connection-status.d.ts +1 -0
- package/dist/host/types/src/ui/components/data-view.d.ts +32 -0
- package/dist/host/types/src/ui/components/dialog.d.ts +17 -0
- package/dist/host/types/src/ui/components/dropdown-menu.d.ts +29 -0
- package/dist/host/types/src/ui/components/empty-state.d.ts +19 -0
- package/dist/host/types/src/ui/components/form-fields.d.ts +115 -0
- package/dist/host/types/src/ui/components/input.d.ts +3 -0
- package/dist/host/types/src/ui/components/kbd.d.ts +12 -0
- package/dist/host/types/src/ui/components/label.d.ts +4 -0
- package/dist/host/types/src/ui/components/manager-card.d.ts +25 -0
- package/dist/host/types/src/ui/components/meter.d.ts +14 -0
- package/dist/host/types/src/ui/components/metric-card.d.ts +18 -0
- package/dist/host/types/src/ui/components/panel-layout.d.ts +83 -0
- package/dist/host/types/src/ui/components/password-input.d.ts +4 -0
- package/dist/host/types/src/ui/components/popover.d.ts +7 -0
- package/dist/host/types/src/ui/components/scroll-area.d.ts +5 -0
- package/dist/host/types/src/ui/components/section-card.d.ts +29 -0
- package/dist/host/types/src/ui/components/select.d.ts +15 -0
- package/dist/host/types/src/ui/components/select2.d.ts +8 -0
- package/dist/host/types/src/ui/components/separator.d.ts +4 -0
- package/dist/host/types/src/ui/components/sheet.d.ts +14 -0
- package/dist/host/types/src/ui/components/sidebar.d.ts +69 -0
- package/dist/host/types/src/ui/components/skeleton.d.ts +2 -0
- package/dist/host/types/src/ui/components/slider.d.ts +4 -0
- package/dist/host/types/src/ui/components/sonner.d.ts +4 -0
- package/dist/host/types/src/ui/components/surface/prompt-styles.d.ts +4 -0
- package/dist/host/types/src/ui/components/surface/surface-scope.d.ts +79 -0
- package/dist/host/types/src/ui/components/switch.d.ts +4 -0
- package/dist/host/types/src/ui/components/tab-strip.d.ts +19 -0
- package/dist/host/types/src/ui/components/tabs.d.ts +7 -0
- package/dist/host/types/src/ui/components/textarea.d.ts +4 -0
- package/dist/host/types/src/ui/components/theme-provider.d.ts +14 -0
- package/dist/host/types/src/ui/components/tooltip.d.ts +7 -0
- package/dist/host/types/src/ui/contexts/BrandingContext.d.ts +15 -0
- package/dist/host/types/src/ui/contexts/UiPreferencesContext.d.ts +38 -0
- package/dist/host/types/src/ui/contexts/branding-document.d.ts +10 -0
- package/dist/host/types/src/ui/dashboard/dashboard-cards-registry.d.ts +30 -0
- package/dist/host/types/src/ui/hooks/use-action-slot.d.ts +10 -0
- package/dist/host/types/src/ui/hooks/use-adaptive-polling.d.ts +5 -0
- package/dist/host/types/src/ui/hooks/use-mobile.d.ts +1 -0
- package/dist/host/types/src/ui/hooks/use-permissions.d.ts +29 -0
- package/dist/host/types/src/ui/hooks/use-status-color-scheme.d.ts +12 -0
- package/dist/host/types/src/ui/hooks/use-sync-status.d.ts +4 -0
- package/dist/host/types/src/ui/hooks/useSshAuthProviders.d.ts +17 -0
- package/dist/host/types/src/ui/i18n/i18n.d.ts +52 -0
- package/dist/host/types/src/ui/lib/ServerStatusContext.d.ts +33 -0
- package/dist/host/types/src/ui/lib/adaptive-polling.d.ts +29 -0
- package/dist/host/types/src/ui/lib/adaptive-resource-budget.d.ts +39 -0
- package/dist/host/types/src/ui/lib/app-keyboard-shortcuts.d.ts +8 -0
- package/dist/host/types/src/ui/lib/base-path.d.ts +1 -0
- package/dist/host/types/src/ui/lib/clipboard-provider.d.ts +9 -0
- package/dist/host/types/src/ui/lib/clipboard.d.ts +20 -0
- package/dist/host/types/src/ui/lib/connection-origin.d.ts +47 -0
- package/dist/host/types/src/ui/lib/db-health-monitor.d.ts +34 -0
- package/dist/host/types/src/ui/lib/device-id.d.ts +1 -0
- package/dist/host/types/src/ui/lib/electron.d.ts +1 -0
- package/dist/host/types/src/ui/lib/embedded-server-status.d.ts +15 -0
- package/dist/host/types/src/ui/lib/error-message.d.ts +9 -0
- package/dist/host/types/src/ui/lib/frontend-logger.d.ts +67 -0
- package/dist/host/types/src/ui/lib/global-shortcut-handler.d.ts +3 -0
- package/dist/host/types/src/ui/lib/host-defaults-context.d.ts +48 -0
- package/dist/host/types/src/ui/lib/hosts-request-cache.d.ts +9 -0
- package/dist/host/types/src/ui/lib/keybinding-match.d.ts +3 -0
- package/dist/host/types/src/ui/lib/linked-server.d.ts +10 -0
- package/dist/host/types/src/ui/lib/local-adaptive-engine.d.ts +40 -0
- package/dist/host/types/src/ui/lib/permissions.d.ts +16 -0
- package/dist/host/types/src/ui/lib/plugin-i18n.d.ts +11 -0
- package/dist/host/types/src/ui/lib/plugin-icon.d.ts +11 -0
- package/dist/host/types/src/ui/lib/plugin-transport.d.ts +50 -0
- package/dist/host/types/src/ui/lib/registry.d.ts +29 -0
- package/dist/host/types/src/ui/lib/rem.d.ts +7 -0
- package/dist/host/types/src/ui/lib/remote-server-api.d.ts +11 -0
- package/dist/host/types/src/ui/lib/server-status-store.d.ts +37 -0
- package/dist/host/types/src/ui/lib/sync-events.d.ts +2 -0
- package/dist/host/types/src/ui/lib/sync-status.d.ts +6 -0
- package/dist/host/types/src/ui/lib/tab-jump-hotkey.d.ts +4 -0
- package/dist/host/types/src/ui/lib/theme.d.ts +21 -0
- package/dist/host/types/src/ui/lib/ttl-request-cache.d.ts +10 -0
- package/dist/host/types/src/ui/lib/usage-color.d.ts +5 -0
- package/dist/host/types/src/ui/lib/utils.d.ts +2 -0
- package/dist/host/types/src/ui/lib/visible-interval.d.ts +5 -0
- package/dist/host/types/src/ui/lib/widget-title.d.ts +8 -0
- package/dist/host/types/src/ui/lib/ws-auth.d.ts +1 -0
- package/dist/host/types/src/ui/main-axios.d.ts +170 -0
- package/dist/host/types/src/ui/plugin-host/PluginViewPlaceholder.d.ts +13 -0
- package/dist/host/types/src/ui/plugin-host/app.d.ts +18 -0
- package/dist/host/types/src/ui/plugin-host/auth-registry.d.ts +50 -0
- package/dist/host/types/src/ui/plugin-host/bridge.d.ts +26 -0
- package/dist/host/types/src/ui/plugin-host/component-registry.d.ts +24 -0
- package/dist/host/types/src/ui/plugin-host/desktop.d.ts +7 -0
- package/dist/host/types/src/ui/plugin-host/extension-registry.d.ts +16 -0
- package/dist/host/types/src/ui/plugin-host/host-scope.d.ts +26 -0
- package/dist/host/types/src/ui/plugin-host/loader.d.ts +52 -0
- package/dist/host/types/src/ui/plugin-host/plugin-store.d.ts +40 -0
- package/dist/host/types/src/ui/plugin-host/scope.d.ts +20 -0
- package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +99 -0
- package/dist/host/types/src/ui/plugin-host/shell-bridge.d.ts +67 -0
- package/dist/host/types/src/ui/plugin-host/view-ownership.d.ts +31 -0
- package/dist/host/types/src/ui/settings/HostPluginSections.d.ts +31 -0
- package/dist/host/types/src/ui/settings/SettingsFields.d.ts +22 -0
- package/dist/host/types/src/ui/settings/host-feature-tabs.d.ts +11 -0
- package/dist/host/types/src/ui/settings/settings-components.d.ts +32 -0
- package/dist/host/types/src/ui/settings/settings-fields-util.d.ts +11 -0
- package/dist/host/types/src/ui/shell/ActionSlot.d.ts +41 -0
- package/dist/host/types/src/ui/shell/TabContext.d.ts +30 -0
- package/dist/host/types/src/ui/shell/action-registry.d.ts +99 -0
- package/dist/host/types/src/ui/shell/keybinding-registry.d.ts +62 -0
- package/dist/host/types/src/ui/shell/palette-registry.d.ts +55 -0
- package/dist/host/types/src/ui/shell/panel-registry.d.ts +24 -0
- package/dist/host/types/src/ui/shell/tab-registry.d.ts +92 -0
- package/dist/host/types/src/ui/sidebar/HostManagerTabs.d.ts +65 -0
- package/dist/host/types/src/ui/sidebar/host-contributions.d.ts +72 -0
- package/dist/host/types/src/ui/sidebar/host-defaults/catalog.d.ts +17 -0
- package/dist/host/types/src/ui/sidebar/host-protocols.d.ts +49 -0
- package/dist/host/types/src/ui/sidebar/quick-connect-host.d.ts +21 -0
- package/dist/host/types/src/ui/sidebar/rail-items.d.ts +103 -0
- package/dist/host/types/src/ui/ssh/connection-log/ConnectionLogContext.d.ts +17 -0
- package/dist/host/types/src/ui/ssh/dialogs/BrowserSignInDialog.d.ts +18 -0
- package/dist/host/types/src/ui/ssh/dialogs/HostKeyVerificationDialog.d.ts +17 -0
- package/dist/host/types/src/ui/ssh/dialogs/PassphraseDialog.d.ts +15 -0
- package/dist/host/types/src/ui/ssh/dialogs/SSHAuthDialog.d.ts +20 -0
- package/dist/host/types/src/ui/ssh/dialogs/TOTPDialog.d.ts +13 -0
- package/dist/host/ui.d.ts +1 -0
- package/dist/host/ui.js +31754 -0
- package/dist/host/use-permissions-NULZL43J.js +18 -0
- package/dist/host-commands.d.ts +89 -0
- package/dist/host-commands.js +387 -0
- package/dist/manifest.d.ts +461 -0
- package/dist/manifest.js +1499 -0
- package/dist/settings.d.ts +43 -0
- package/dist/settings.js +168 -0
- package/dist/ssh-certs.d.ts +25 -0
- package/dist/ssh-certs.js +234 -0
- package/dist/ssh-options.d.ts +23 -0
- package/dist/ssh-options.js +1 -0
- package/dist/table-builder.d.ts +26 -0
- package/dist/table-builder.js +87 -0
- package/dist/testing/safe-ports.d.ts +10 -0
- package/dist/testing/safe-ports.js +59 -0
- package/dist/testing/setup.d.ts +7 -0
- package/dist/testing/setup.js +146 -0
- package/dist/testing.d.ts +537 -0
- package/dist/testing.js +1451 -0
- package/dist/vitest-preset.d.ts +54 -0
- package/dist/vitest-preset.js +96 -0
- package/package.json +143 -0
- package/schema/changelog.schema.json +67 -0
- package/schema/manifest.schema.json +1160 -0
- package/tsconfig.plugin.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.svg" width="120" height="120" alt="Termix Logo" />
|
|
4
|
+
|
|
5
|
+
<h1>Termix Plugin SDK</h1>
|
|
6
|
+
|
|
7
|
+
<p>Everything you need to build a Termix plugin</p>
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<br />
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
`@termix-ssh/plugin-sdk` is what every [Termix](https://github.com/Termix-SSH/Termix) plugin builds against, including the ones that ship with Termix. It has the types for a plugin's backend and frontend, table definitions, the manifest schema, test helpers, and the `termix-plugin` CLI. The easiest way to start is the [Termix Plugin Template](https://github.com/Termix-SSH/Termix-Plugin-Template).
|
|
16
|
+
|
|
17
|
+
<br />
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install --save-dev @termix-ssh/plugin-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
<br />
|
|
26
|
+
|
|
27
|
+
## CLI
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx termix-plugin build # bundle dist/backend.js and dist/frontend.js
|
|
31
|
+
npx termix-plugin test # run the plugin's Vitest suite
|
|
32
|
+
npx termix-plugin validate # check manifest.json and the files it names
|
|
33
|
+
npx termix-plugin migrations # write migrations from the table definitions
|
|
34
|
+
npx termix-plugin pack # write <id>-<version>.tmxplug
|
|
35
|
+
npx termix-plugin sign <file.tmxplug> # sign it, needs TERMIX_PLUGIN_SIGNING_KEY
|
|
36
|
+
npx termix-plugin verify <file.tmxplug> --key <base64> # check a signature
|
|
37
|
+
npx termix-plugin keygen --out <dir> # make a new signing key pair
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
<br />
|
|
41
|
+
|
|
42
|
+
## Support
|
|
43
|
+
|
|
44
|
+
To report a bug or request a feature, open a [support ticket](https://github.com/Termix-SSH/Support/issues/new/choose). You need to be logged in to GitHub. Please be as detailed as possible, preferably in English.
|
|
45
|
+
|
|
46
|
+
For discussions and questions, join the [Discord](https://discord.gg/jVQGdvHDrf) server.
|
|
47
|
+
|
|
48
|
+
<br />
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
Distributed under the Apache License Version 2.0. See [LICENSE](https://github.com/Termix-SSH/Termix/blob/main/LICENSE) for more information.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import * as esbuild from "esbuild";
|
|
6
|
+
import { BACKEND_EXTERNALS, FRONTEND_EXTERNALS } from "../lib/externals.mjs";
|
|
7
|
+
import { staticUrlImports } from "../lib/static-url-imports.mjs";
|
|
8
|
+
import { readManifest, resolveEntry, copyDir } from "../lib/plugin-dir.mjs";
|
|
9
|
+
|
|
10
|
+
const BACKEND_ENTRIES = [
|
|
11
|
+
"src/backend/index.ts",
|
|
12
|
+
"src/backend/index.mjs",
|
|
13
|
+
"src/backend/index.js",
|
|
14
|
+
];
|
|
15
|
+
const FRONTEND_ENTRIES = [
|
|
16
|
+
"src/frontend/index.tsx",
|
|
17
|
+
"src/frontend/index.ts",
|
|
18
|
+
"src/frontend/index.mjs",
|
|
19
|
+
"src/frontend/index.js",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A bundled CommonJS package that calls require("react/jsx-runtime") (or any
|
|
24
|
+
* other host-provided package) cannot reach it: the package is external, and
|
|
25
|
+
* esbuild's ESM output has no require, so the plugin throws "Dynamic require
|
|
26
|
+
* is not supported" as soon as it loads. Each such require is pointed at a
|
|
27
|
+
* small ES module that imports the external and re-exports it, which esbuild
|
|
28
|
+
* can hand to CommonJS code.
|
|
29
|
+
*/
|
|
30
|
+
export function externalRequireInterop(externals) {
|
|
31
|
+
const names = new Set(externals.filter((name) => !name.includes("*")));
|
|
32
|
+
return {
|
|
33
|
+
name: "termix-external-require",
|
|
34
|
+
setup(build) {
|
|
35
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
36
|
+
if (args.kind !== "require-call" || !names.has(args.path)) return;
|
|
37
|
+
return { path: args.path, namespace: "termix-external-require" };
|
|
38
|
+
});
|
|
39
|
+
build.onLoad(
|
|
40
|
+
{ filter: /.*/, namespace: "termix-external-require" },
|
|
41
|
+
(args) => {
|
|
42
|
+
const id = JSON.stringify(args.path);
|
|
43
|
+
return {
|
|
44
|
+
contents: `import * as mod from ${id};\nexport * from ${id};\nexport default mod.default ?? mod;\n`,
|
|
45
|
+
loader: "js",
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const SDK_PATCHES = path.join(
|
|
54
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
55
|
+
"..",
|
|
56
|
+
"patches",
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Patches a dependency before it is bundled, so the fix ships inside the
|
|
61
|
+
* plugin rather than depending on whoever ran npm install.
|
|
62
|
+
*
|
|
63
|
+
* package.json "termix": { "patches": ["xterm-android-ime"] } names patches
|
|
64
|
+
* the SDK ships for libraries several plugins share. Any .cjs file in the
|
|
65
|
+
* plugin's own patches/ folder runs too. Every patch must be safe to run
|
|
66
|
+
* twice.
|
|
67
|
+
*/
|
|
68
|
+
export function applyPatches(cwd, pluginId) {
|
|
69
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
70
|
+
const pkg = fs.existsSync(pkgPath)
|
|
71
|
+
? JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
|
72
|
+
: {};
|
|
73
|
+
const scripts = [];
|
|
74
|
+
for (const name of pkg.termix?.patches ?? []) {
|
|
75
|
+
const file = path.join(SDK_PATCHES, `${name}.cjs`);
|
|
76
|
+
if (!/^[a-z0-9-]+$/.test(name) || !fs.existsSync(file)) {
|
|
77
|
+
throw new Error(`${pluginId}: the SDK has no patch named "${name}"`);
|
|
78
|
+
}
|
|
79
|
+
scripts.push(file);
|
|
80
|
+
}
|
|
81
|
+
const own = path.join(cwd, "patches");
|
|
82
|
+
if (fs.existsSync(own)) {
|
|
83
|
+
for (const file of fs.readdirSync(own).sort()) {
|
|
84
|
+
if (file.endsWith(".cjs")) scripts.push(path.join(own, file));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const file of scripts) {
|
|
88
|
+
const result = spawnSync(process.execPath, [file], {
|
|
89
|
+
cwd,
|
|
90
|
+
env: { ...process.env, TERMIX_PATCH_ROOT: cwd },
|
|
91
|
+
stdio: "inherit",
|
|
92
|
+
});
|
|
93
|
+
if (result.status !== 0) {
|
|
94
|
+
throw new Error(`${pluginId}: patch ${path.basename(file)} failed`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const THEME_CSS = path.join(
|
|
100
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
101
|
+
"..",
|
|
102
|
+
"..",
|
|
103
|
+
"dist",
|
|
104
|
+
"host",
|
|
105
|
+
"theme.css",
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Compiles the Tailwind classes the plugin's frontend uses into
|
|
110
|
+
* dist/frontend.css, against the theme core ships in the SDK. Core only
|
|
111
|
+
* compiles its own classes, so a class no core file happens to use would
|
|
112
|
+
* otherwise have no CSS at all. Appended to whatever CSS the bundle already
|
|
113
|
+
* produced (a library's stylesheet the plugin imports).
|
|
114
|
+
*/
|
|
115
|
+
/**
|
|
116
|
+
* Moves a plugin's Tailwind layers under their own names. Core orders
|
|
117
|
+
* termix-plugin-utilities above its base styles and below its own utilities,
|
|
118
|
+
* so a plugin's copy of `.hidden` can never beat core's `md:flex` just
|
|
119
|
+
* because the plugin's file loaded later.
|
|
120
|
+
*/
|
|
121
|
+
export function lowerPluginLayers(css) {
|
|
122
|
+
return css.replace(
|
|
123
|
+
/@layer (properties|utilities)(?=[\s{;,])/g,
|
|
124
|
+
"@layer termix-plugin-$1",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function buildTailwind(cwd, outDir) {
|
|
129
|
+
const { compile, optimize } = await import("@tailwindcss/node");
|
|
130
|
+
const { Scanner } = await import("@tailwindcss/oxide");
|
|
131
|
+
if (!fs.existsSync(THEME_CSS)) {
|
|
132
|
+
throw new Error("The SDK theme is missing. Run: npm run build:sdk");
|
|
133
|
+
}
|
|
134
|
+
const input = [
|
|
135
|
+
'@import "tailwindcss/theme.css" theme(reference);',
|
|
136
|
+
`@import ${JSON.stringify(THEME_CSS.replaceAll("\\", "/"))};`,
|
|
137
|
+
'@import "tailwindcss/utilities.css" layer(utilities);',
|
|
138
|
+
].join("\n");
|
|
139
|
+
const compiler = await compile(input, {
|
|
140
|
+
base: path.dirname(THEME_CSS),
|
|
141
|
+
onDependency: () => {},
|
|
142
|
+
});
|
|
143
|
+
const scanner = new Scanner({
|
|
144
|
+
sources: [
|
|
145
|
+
{
|
|
146
|
+
base: path.join(cwd, "src", "frontend"),
|
|
147
|
+
pattern: "**/*",
|
|
148
|
+
negated: false,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
// Flattened and minified the way core's own CSS is, so it does not rely
|
|
153
|
+
// on native CSS nesting.
|
|
154
|
+
const css = lowerPluginLayers(
|
|
155
|
+
optimize(compiler.build(scanner.scan()), { minify: true }).code.trim(),
|
|
156
|
+
);
|
|
157
|
+
if (!css) return;
|
|
158
|
+
const target = path.join(outDir, "frontend.css");
|
|
159
|
+
const existing = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
|
|
160
|
+
fs.writeFileSync(target, `${existing}${existing ? "\n" : ""}${css}\n`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function build({ cwd }) {
|
|
164
|
+
const manifest = readManifest(cwd);
|
|
165
|
+
const pluginId = manifest.id ?? path.basename(cwd);
|
|
166
|
+
const outDir = path.join(cwd, "dist");
|
|
167
|
+
|
|
168
|
+
applyPatches(cwd, pluginId);
|
|
169
|
+
|
|
170
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
171
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
172
|
+
|
|
173
|
+
const backendEntry = resolveEntry(cwd, BACKEND_ENTRIES);
|
|
174
|
+
if (!backendEntry) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`${pluginId}: no backend entry (looked for ${BACKEND_ENTRIES.join(", ")})`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await esbuild.build({
|
|
181
|
+
entryPoints: [backendEntry],
|
|
182
|
+
outfile: path.join(outDir, "backend.js"),
|
|
183
|
+
bundle: true,
|
|
184
|
+
format: "esm",
|
|
185
|
+
platform: "node",
|
|
186
|
+
target: "node22",
|
|
187
|
+
sourcemap: true,
|
|
188
|
+
logLevel: "warning",
|
|
189
|
+
// A native dependency's compiled .node binary is resolved by the
|
|
190
|
+
// package's own relative paths, which break once esbuild inlines its JS
|
|
191
|
+
// elsewhere. Declaring it in nativeDependencies keeps it a real
|
|
192
|
+
// node_modules import instead, resolved at runtime like a host-provided
|
|
193
|
+
// package.
|
|
194
|
+
external: [...BACKEND_EXTERNALS, ...(manifest.nativeDependencies ?? [])],
|
|
195
|
+
// Bundled CJS deps still call require() for Node builtins at runtime.
|
|
196
|
+
// ESM has no ambient require, so give esbuild's require shim a real one.
|
|
197
|
+
banner: {
|
|
198
|
+
js: "import { createRequire as __termixCreateRequire } from 'node:module'; const require = __termixCreateRequire(import.meta.url);",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Without this Node finds the host's typeless package.json, tries backend.js
|
|
203
|
+
// as CommonJS first and warns before reparsing it as ESM.
|
|
204
|
+
fs.writeFileSync(
|
|
205
|
+
path.join(outDir, "package.json"),
|
|
206
|
+
`${JSON.stringify({ type: "module" }, null, 2)}\n`,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const frontendEntry = resolveEntry(cwd, FRONTEND_ENTRIES);
|
|
210
|
+
if (frontendEntry) {
|
|
211
|
+
await esbuild.build({
|
|
212
|
+
entryPoints: [frontendEntry],
|
|
213
|
+
outfile: path.join(outDir, "frontend.js"),
|
|
214
|
+
bundle: true,
|
|
215
|
+
format: "esm",
|
|
216
|
+
platform: "browser",
|
|
217
|
+
target: "es2022",
|
|
218
|
+
jsx: "automatic",
|
|
219
|
+
sourcemap: true,
|
|
220
|
+
logLevel: "warning",
|
|
221
|
+
external: FRONTEND_EXTERNALS,
|
|
222
|
+
// Vite replaces these in the dev server; a plugin bundle has to have
|
|
223
|
+
// them too, or code that reads them throws when the plugin loads.
|
|
224
|
+
define: {
|
|
225
|
+
"process.env.NODE_ENV": '"production"',
|
|
226
|
+
"import.meta.env.DEV": "false",
|
|
227
|
+
"import.meta.env.PROD": "true",
|
|
228
|
+
"import.meta.env.MODE": '"production"',
|
|
229
|
+
"import.meta.env.SSR": "false",
|
|
230
|
+
"import.meta.env":
|
|
231
|
+
'{"DEV":false,"PROD":true,"MODE":"production","SSR":false,"BASE_URL":"/"}',
|
|
232
|
+
},
|
|
233
|
+
plugins: [
|
|
234
|
+
externalRequireInterop(FRONTEND_EXTERNALS),
|
|
235
|
+
staticUrlImports({ outDir }),
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (frontendEntry) await buildTailwind(cwd, outDir);
|
|
241
|
+
|
|
242
|
+
copyDir(path.join(cwd, "locales"), path.join(outDir, "locales"));
|
|
243
|
+
copyDir(path.join(cwd, "migrations"), path.join(outDir, "migrations"));
|
|
244
|
+
|
|
245
|
+
console.log(`built ${pluginId}`);
|
|
246
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a plugin's migrations from its table definitions.
|
|
3
|
+
*
|
|
4
|
+
* Not drizzle-kit. Core's own SQLite schema is hand-written DDL and only the
|
|
5
|
+
* client-server engines use drizzle migrations, so a plugin on drizzle-kit
|
|
6
|
+
* would still need a hand-rolled SQLite path, three configs and three
|
|
7
|
+
* generated schema modules. This diffs the definitions against a committed
|
|
8
|
+
* snapshot and writes plain SQL for all three dialects, which is what the
|
|
9
|
+
* server's runner applies.
|
|
10
|
+
*
|
|
11
|
+
* Usage, from a plugin directory:
|
|
12
|
+
* termix-plugin migrations [name]
|
|
13
|
+
* termix-plugin migrations --check verifies nothing is undeclared
|
|
14
|
+
*
|
|
15
|
+
* The plugin exports its definitions from src/backend/tables.ts as `tables`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import * as esbuild from "esbuild";
|
|
22
|
+
import { BACKEND_EXTERNALS } from "../lib/externals.mjs";
|
|
23
|
+
import { readManifest, resolveEntry } from "../lib/plugin-dir.mjs";
|
|
24
|
+
|
|
25
|
+
const DIALECTS = ["sqlite", "postgres", "mysql"];
|
|
26
|
+
|
|
27
|
+
const TABLE_ENTRIES = [
|
|
28
|
+
"src/backend/tables.ts",
|
|
29
|
+
"src/backend/tables.mjs",
|
|
30
|
+
"src/backend/tables.js",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** Loads the plugin's table definitions by bundling them to a temp file. */
|
|
34
|
+
async function loadDefinitions(cwd) {
|
|
35
|
+
const entry = resolveEntry(cwd, TABLE_ENTRIES);
|
|
36
|
+
if (!entry) return null;
|
|
37
|
+
|
|
38
|
+
// Emitted inside the plugin rather than in the system temp dir: the SDK
|
|
39
|
+
// stays external to the bundle, so the output has to sit somewhere node can
|
|
40
|
+
// resolve @termix-ssh/plugin-sdk from.
|
|
41
|
+
const outdir = fs.mkdtempSync(path.join(cwd, ".termix-tables-"));
|
|
42
|
+
const outfile = path.join(outdir, "tables.mjs");
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await esbuild.build({
|
|
46
|
+
entryPoints: [entry],
|
|
47
|
+
outfile,
|
|
48
|
+
bundle: true,
|
|
49
|
+
format: "esm",
|
|
50
|
+
platform: "node",
|
|
51
|
+
target: "node22",
|
|
52
|
+
logLevel: "warning",
|
|
53
|
+
external: BACKEND_EXTERNALS,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const module = await import(`file://${outfile.split(path.sep).join("/")}`);
|
|
57
|
+
const tables = module.tables ?? module.default;
|
|
58
|
+
if (!Array.isArray(tables)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`${path.relative(cwd, entry)} must export a "tables" array of defineTable() results`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return tables;
|
|
64
|
+
} finally {
|
|
65
|
+
fs.rmSync(outdir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function snapshotPath(cwd) {
|
|
70
|
+
return path.join(cwd, "migrations", "snapshot.json");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readSnapshot(cwd) {
|
|
74
|
+
const file = snapshotPath(cwd);
|
|
75
|
+
if (!fs.existsSync(file)) return { version: 1, tables: {} };
|
|
76
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** What a definition looks like in the snapshot, so a diff can compare them. */
|
|
80
|
+
function snapshotTable(definition) {
|
|
81
|
+
return {
|
|
82
|
+
columns: definition.columns,
|
|
83
|
+
indexes: definition.indexes,
|
|
84
|
+
...(definition.adopts ? { adopts: definition.adopts } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function nextSequence(cwd) {
|
|
89
|
+
let highest = 0;
|
|
90
|
+
for (const dialect of DIALECTS) {
|
|
91
|
+
const dir = path.join(cwd, "migrations", dialect);
|
|
92
|
+
if (!fs.existsSync(dir)) continue;
|
|
93
|
+
for (const file of fs.readdirSync(dir)) {
|
|
94
|
+
const match = /^(\d{4})_/.exec(file);
|
|
95
|
+
if (match) highest = Math.max(highest, Number(match[1]));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return highest + 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* What changed since the snapshot.
|
|
103
|
+
*
|
|
104
|
+
* New tables and new columns are generated. A changed or removed column is
|
|
105
|
+
* reported rather than guessed at: renames and type changes need a data
|
|
106
|
+
* decision this cannot make, so the author writes that migration by hand.
|
|
107
|
+
*/
|
|
108
|
+
function diff(previous, definitions) {
|
|
109
|
+
const created = [];
|
|
110
|
+
const added = [];
|
|
111
|
+
const manual = [];
|
|
112
|
+
|
|
113
|
+
for (const definition of definitions) {
|
|
114
|
+
const before = previous.tables[definition.name];
|
|
115
|
+
if (!before) {
|
|
116
|
+
created.push(definition);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const [property, column] of Object.entries(definition.columns)) {
|
|
121
|
+
const existing = before.columns[property];
|
|
122
|
+
if (!existing) {
|
|
123
|
+
added.push({ definition, property, column });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (JSON.stringify(existing) !== JSON.stringify(column)) {
|
|
127
|
+
manual.push(`${definition.name}.${property} changed type or modifiers`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const property of Object.keys(before.columns)) {
|
|
132
|
+
if (!definition.columns[property]) {
|
|
133
|
+
manual.push(`${definition.name}.${property} was removed`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const name of Object.keys(previous.tables)) {
|
|
139
|
+
if (!definitions.some((definition) => definition.name === name)) {
|
|
140
|
+
manual.push(`table ${name} was removed`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { created, added, manual };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function migrations({ cwd, args }) {
|
|
148
|
+
const manifest = readManifest(cwd);
|
|
149
|
+
const pluginId = manifest.id ?? path.basename(cwd);
|
|
150
|
+
const check = args.includes("--check");
|
|
151
|
+
const name = args.find((arg) => !arg.startsWith("--")) ?? "update";
|
|
152
|
+
|
|
153
|
+
const definitions = await loadDefinitions(cwd);
|
|
154
|
+
if (definitions === null) {
|
|
155
|
+
console.log(`${pluginId}: no table definitions, nothing to generate`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const { LEGACY_TABLE_OWNERS } = await import(
|
|
160
|
+
new URL("../../dist/db.js", import.meta.url).href
|
|
161
|
+
);
|
|
162
|
+
for (const definition of definitions) {
|
|
163
|
+
if (
|
|
164
|
+
definition.adopts &&
|
|
165
|
+
LEGACY_TABLE_OWNERS[definition.adopts] !== pluginId
|
|
166
|
+
) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`${pluginId}: table ${definition.name} adopts "${definition.adopts}", which belongs to the "${LEGACY_TABLE_OWNERS[definition.adopts]}" plugin`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { createTableSql, adoptTableSql, addColumnSql } = await loadEmitter();
|
|
174
|
+
|
|
175
|
+
const previous = readSnapshot(cwd);
|
|
176
|
+
const { created, added, manual } = diff(previous, definitions);
|
|
177
|
+
|
|
178
|
+
if (manual.length > 0) {
|
|
179
|
+
for (const problem of manual) console.error(` ${problem}`);
|
|
180
|
+
throw new Error(
|
|
181
|
+
`${pluginId}: these changes need a hand-written migration, because a rename or a type change is a data decision.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (created.length === 0 && added.length === 0) {
|
|
186
|
+
console.log(`${pluginId}: migrations are up to date`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (check) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`${pluginId}: table definitions have changed but no migration was generated. Run: termix-plugin migrations`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const sequence = String(nextSequence(cwd)).padStart(4, "0");
|
|
197
|
+
const file = `${sequence}_${name.replace(/[^a-z0-9_]/gi, "_").toLowerCase()}.sql`;
|
|
198
|
+
|
|
199
|
+
for (const dialect of DIALECTS) {
|
|
200
|
+
const statements = [];
|
|
201
|
+
for (const definition of created) {
|
|
202
|
+
// An adopted table renames the legacy one into place, creating it first
|
|
203
|
+
// on a fresh install, so existing rows are kept rather than copied.
|
|
204
|
+
statements.push(
|
|
205
|
+
...(definition.adopts
|
|
206
|
+
? adoptTableSql(dialect, pluginId, definition)
|
|
207
|
+
: createTableSql(dialect, pluginId, definition)),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
for (const entry of added) {
|
|
211
|
+
statements.push(
|
|
212
|
+
addColumnSql(dialect, pluginId, entry.definition, entry.property),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const dir = path.join(cwd, "migrations", dialect);
|
|
217
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
218
|
+
fs.writeFileSync(
|
|
219
|
+
path.join(dir, file),
|
|
220
|
+
`-- ${pluginId} ${sequence}: ${name}\n-- Generated by termix-plugin migrations. Review before committing.\n\n${statements.join("\n")}\n`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const snapshot = { version: 1, tables: {} };
|
|
225
|
+
for (const definition of definitions) {
|
|
226
|
+
snapshot.tables[definition.name] = snapshotTable(definition);
|
|
227
|
+
}
|
|
228
|
+
fs.writeFileSync(snapshotPath(cwd), `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
229
|
+
|
|
230
|
+
console.log(
|
|
231
|
+
`${pluginId}: wrote ${DIALECTS.length} dialect files for ${sequence}_${name}`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The DDL emitter, shared with the server.
|
|
237
|
+
*
|
|
238
|
+
* One mapping, two consumers: the SQL written here is the SQL the server's
|
|
239
|
+
* migration runner applies, so a second copy could drift into producing a
|
|
240
|
+
* migration the runner cannot reproduce.
|
|
241
|
+
*/
|
|
242
|
+
async function loadEmitter() {
|
|
243
|
+
const entry = new URL("../../dist/ddl.js", import.meta.url);
|
|
244
|
+
if (!fs.existsSync(fileURLToPath(entry))) {
|
|
245
|
+
throw new Error("The plugin SDK is not built yet. Run: npm run build:sdk");
|
|
246
|
+
}
|
|
247
|
+
return import(entry.href);
|
|
248
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readManifest } from "../lib/plugin-dir.mjs";
|
|
4
|
+
import { collectFiles, createTmxplug } from "../lib/tmxplug.mjs";
|
|
5
|
+
import { sha256 } from "../lib/signing.mjs";
|
|
6
|
+
import { validate } from "./validate.mjs";
|
|
7
|
+
|
|
8
|
+
function readOption(args, name) {
|
|
9
|
+
const index = args.indexOf(name);
|
|
10
|
+
if (index === -1) return null;
|
|
11
|
+
const value = args[index + 1];
|
|
12
|
+
if (!value) throw new Error(`${name} needs a value`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Writes <id>-<version>.tmxplug. Validates first and refuses on any error,
|
|
18
|
+
* so a broken manifest never becomes a release.
|
|
19
|
+
*/
|
|
20
|
+
export async function pack({ cwd, args = [] }) {
|
|
21
|
+
const manifest = readManifest(cwd);
|
|
22
|
+
const pluginId = manifest.id ?? path.basename(cwd);
|
|
23
|
+
const version = manifest.version ?? "0.0.0";
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(path.join(cwd, "dist", "backend.js"))) {
|
|
26
|
+
throw new Error(`${pluginId}: nothing built. Run termix-plugin build.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await validate({ cwd });
|
|
30
|
+
|
|
31
|
+
const outDir = path.resolve(cwd, readOption(args, "--out") ?? ".");
|
|
32
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
33
|
+
const outfile = path.join(outDir, `${pluginId}-${version}.tmxplug`);
|
|
34
|
+
|
|
35
|
+
const archive = createTmxplug(cwd, collectFiles(cwd));
|
|
36
|
+
fs.writeFileSync(outfile, archive);
|
|
37
|
+
|
|
38
|
+
console.log(`packed ${path.basename(outfile)}`);
|
|
39
|
+
console.log(` size ${archive.length}`);
|
|
40
|
+
console.log(` sha256 ${sha256(archive).toString("hex")}`);
|
|
41
|
+
return outfile;
|
|
42
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
SIGNING_KEY_ENV,
|
|
5
|
+
generateKeyPair,
|
|
6
|
+
loadPrivateKey,
|
|
7
|
+
sha256,
|
|
8
|
+
signArtifact,
|
|
9
|
+
verifyArtifact,
|
|
10
|
+
} from "../lib/signing.mjs";
|
|
11
|
+
|
|
12
|
+
function readOption(args, name) {
|
|
13
|
+
const index = args.indexOf(name);
|
|
14
|
+
if (index === -1) return null;
|
|
15
|
+
const value = args[index + 1];
|
|
16
|
+
if (!value) throw new Error(`${name} needs a value`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function artifactArg(cwd, args) {
|
|
21
|
+
const file = args.find((arg) => arg.endsWith(".tmxplug"));
|
|
22
|
+
if (!file) throw new Error("Pass the .tmxplug file to use");
|
|
23
|
+
const full = path.resolve(cwd, file);
|
|
24
|
+
if (!fs.existsSync(full)) throw new Error(`${file} does not exist`);
|
|
25
|
+
return full;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Writes <file>.sig: base64 Ed25519 over the sha256 of the .tmxplug. */
|
|
29
|
+
export async function sign({ cwd, args = [], env = process.env }) {
|
|
30
|
+
const file = artifactArg(cwd, args);
|
|
31
|
+
const key = loadPrivateKey(env[SIGNING_KEY_ENV]);
|
|
32
|
+
const buffer = fs.readFileSync(file);
|
|
33
|
+
const signature = signArtifact(buffer, key);
|
|
34
|
+
fs.writeFileSync(`${file}.sig`, `${signature}\n`);
|
|
35
|
+
console.log(`signed ${path.basename(file)}`);
|
|
36
|
+
console.log(` sha256 ${sha256(buffer).toString("hex")}`);
|
|
37
|
+
console.log(` signature ${signature}`);
|
|
38
|
+
return signature;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Checks a .tmxplug against its .sig and one or more public keys. */
|
|
42
|
+
export async function verify({ cwd, args = [] }) {
|
|
43
|
+
const file = artifactArg(cwd, args);
|
|
44
|
+
const keys = (readOption(args, "--key") ?? "")
|
|
45
|
+
.split(",")
|
|
46
|
+
.map((key) => key.trim())
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
if (keys.length === 0) throw new Error("Pass --key <base64 public key>");
|
|
49
|
+
const sigFile = readOption(args, "--sig") ?? `${file}.sig`;
|
|
50
|
+
if (!fs.existsSync(sigFile)) throw new Error(`${sigFile} does not exist`);
|
|
51
|
+
|
|
52
|
+
const matched = verifyArtifact(
|
|
53
|
+
fs.readFileSync(file),
|
|
54
|
+
fs.readFileSync(sigFile, "utf8"),
|
|
55
|
+
keys,
|
|
56
|
+
);
|
|
57
|
+
if (!matched)
|
|
58
|
+
throw new Error(`${path.basename(file)}: signature is not valid`);
|
|
59
|
+
console.log(`ok ${path.basename(file)} signed by key ${matched}`);
|
|
60
|
+
return matched;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Writes a new private key to a file and prints the public half. The
|
|
65
|
+
* private key never goes to stdout, so it does not end up in a CI log.
|
|
66
|
+
*/
|
|
67
|
+
export async function keygen({ cwd, args = [] }) {
|
|
68
|
+
const outDir = path.resolve(cwd, readOption(args, "--out") ?? ".");
|
|
69
|
+
const keyFile = path.join(outDir, "termix-plugin-signing.key");
|
|
70
|
+
if (fs.existsSync(keyFile)) {
|
|
71
|
+
throw new Error(`${keyFile} already exists; move it first`);
|
|
72
|
+
}
|
|
73
|
+
const pair = generateKeyPair();
|
|
74
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
75
|
+
fs.writeFileSync(keyFile, `${pair.privateKey}\n`, { mode: 0o600 });
|
|
76
|
+
console.log(`private key ${keyFile}`);
|
|
77
|
+
console.log(`public key ${pair.publicKey}`);
|
|
78
|
+
console.log(`key id ${pair.keyId}`);
|
|
79
|
+
console.log(
|
|
80
|
+
`Store the private key as ${SIGNING_KEY_ENV}, then delete the file.`,
|
|
81
|
+
);
|
|
82
|
+
return { keyFile, publicKey: pair.publicKey, keyId: pair.keyId };
|
|
83
|
+
}
|