@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.
Files changed (236) hide show
  1. package/README.md +52 -0
  2. package/cli/commands/build.mjs +246 -0
  3. package/cli/commands/migrations.mjs +248 -0
  4. package/cli/commands/pack.mjs +42 -0
  5. package/cli/commands/sign.mjs +83 -0
  6. package/cli/commands/test.mjs +33 -0
  7. package/cli/commands/validate.mjs +215 -0
  8. package/cli/index.mjs +65 -0
  9. package/cli/lib/changelog.mjs +126 -0
  10. package/cli/lib/externals.mjs +46 -0
  11. package/cli/lib/plugin-dir.mjs +29 -0
  12. package/cli/lib/signing.mjs +86 -0
  13. package/cli/lib/static-url-imports.mjs +54 -0
  14. package/cli/lib/tmxplug.mjs +106 -0
  15. package/cli/patches/guacamole-common-js-flush.cjs +76 -0
  16. package/cli/patches/xterm-android-ime.cjs +130 -0
  17. package/client.d.ts +13 -0
  18. package/dist/backend.d.ts +1777 -0
  19. package/dist/backend.js +84 -0
  20. package/dist/capabilities.d.ts +38 -0
  21. package/dist/capabilities.js +71 -0
  22. package/dist/db.d.ts +112 -0
  23. package/dist/db.js +189 -0
  24. package/dist/ddl.d.ts +76 -0
  25. package/dist/ddl.js +678 -0
  26. package/dist/desktop.d.ts +346 -0
  27. package/dist/desktop.js +5 -0
  28. package/dist/frontend.d.ts +1206 -0
  29. package/dist/frontend.js +388 -0
  30. package/dist/host/chunk-F3SDZDDG.js +6017 -0
  31. package/dist/host/chunk-XGWE7BTJ.js +90 -0
  32. package/dist/host/chunk-XH5NXL6Y.js +56314 -0
  33. package/dist/host/chunk-YC2MAKSP.js +14 -0
  34. package/dist/host/linked-server-FZGWQW32.js +12 -0
  35. package/dist/host/rbac-api-6KDMBVDZ.js +52 -0
  36. package/dist/host/testing-host.js +414 -0
  37. package/dist/host/theme.css +91 -0
  38. package/dist/host/types/src/types/auth-protocols.d.ts +10 -0
  39. package/dist/host/types/src/types/connection-log.d.ts +13 -0
  40. package/dist/host/types/src/types/credential-sidebar-preferences.d.ts +38 -0
  41. package/dist/host/types/src/types/host-defaults.d.ts +60 -0
  42. package/dist/host/types/src/types/host-sidebar-preferences.d.ts +55 -0
  43. package/dist/host/types/src/types/index.d.ts +298 -0
  44. package/dist/host/types/src/types/keybindings.d.ts +27 -0
  45. package/dist/host/types/src/types/ui-preferences.d.ts +102 -0
  46. package/dist/host/types/src/types/ui-types.d.ts +260 -0
  47. package/dist/host/types/src/ui/api/admin-user-data-api.d.ts +11 -0
  48. package/dist/host/types/src/ui/api/auth-methods-api.d.ts +63 -0
  49. package/dist/host/types/src/ui/api/credential-sidebar-preferences-api.d.ts +3 -0
  50. package/dist/host/types/src/ui/api/credentials-api.d.ts +56 -0
  51. package/dist/host/types/src/ui/api/dashboard-api.d.ts +23 -0
  52. package/dist/host/types/src/ui/api/external-account-api.d.ts +8 -0
  53. package/dist/host/types/src/ui/api/host-sidebar-preferences-api.d.ts +3 -0
  54. package/dist/host/types/src/ui/api/host-status-api.d.ts +11 -0
  55. package/dist/host/types/src/ui/api/open-tabs-api.d.ts +74 -0
  56. package/dist/host/types/src/ui/api/plugins-api.d.ts +128 -0
  57. package/dist/host/types/src/ui/api/rbac-api.d.ts +167 -0
  58. package/dist/host/types/src/ui/api/settings-api.d.ts +22 -0
  59. package/dist/host/types/src/ui/api/ssh-host-management-api.d.ts +65 -0
  60. package/dist/host/types/src/ui/api/sync-api.d.ts +93 -0
  61. package/dist/host/types/src/ui/api/system-status-api.d.ts +49 -0
  62. package/dist/host/types/src/ui/api/ui-preferences-api.d.ts +8 -0
  63. package/dist/host/types/src/ui/api/user-management-api.d.ts +80 -0
  64. package/dist/host/types/src/ui/auth/Auth.d.ts +12 -0
  65. package/dist/host/types/src/ui/auth/auth-bits.d.ts +11 -0
  66. package/dist/host/types/src/ui/auth/silent-signin.d.ts +2 -0
  67. package/dist/host/types/src/ui/components/FullScreenAppWrapper.d.ts +9 -0
  68. package/dist/host/types/src/ui/components/alert-dialog.d.ts +14 -0
  69. package/dist/host/types/src/ui/components/alert.d.ts +9 -0
  70. package/dist/host/types/src/ui/components/badge.d.ts +9 -0
  71. package/dist/host/types/src/ui/components/button.d.ts +13 -0
  72. package/dist/host/types/src/ui/components/card-grid/CardGridCanvas.d.ts +22 -0
  73. package/dist/host/types/src/ui/components/card-grid/layout-utils.d.ts +26 -0
  74. package/dist/host/types/src/ui/components/card-grid/types.d.ts +29 -0
  75. package/dist/host/types/src/ui/components/card-masonry.d.ts +16 -0
  76. package/dist/host/types/src/ui/components/card.d.ts +11 -0
  77. package/dist/host/types/src/ui/components/charts/BarSeries.d.ts +16 -0
  78. package/dist/host/types/src/ui/components/charts/LineChart.d.ts +17 -0
  79. package/dist/host/types/src/ui/components/charts/RadialGauge.d.ts +18 -0
  80. package/dist/host/types/src/ui/components/charts/Sparkline.d.ts +22 -0
  81. package/dist/host/types/src/ui/components/charts/StatRow.d.ts +15 -0
  82. package/dist/host/types/src/ui/components/charts/geometry.d.ts +20 -0
  83. package/dist/host/types/src/ui/components/charts/index.d.ts +4 -0
  84. package/dist/host/types/src/ui/components/checkbox.d.ts +4 -0
  85. package/dist/host/types/src/ui/components/connection/ConnectionGate.d.ts +56 -0
  86. package/dist/host/types/src/ui/components/connection/ConnectionLogPanel.d.ts +9 -0
  87. package/dist/host/types/src/ui/components/connection/ConnectionScreen.d.ts +34 -0
  88. package/dist/host/types/src/ui/components/connection/connection-status.d.ts +1 -0
  89. package/dist/host/types/src/ui/components/data-view.d.ts +32 -0
  90. package/dist/host/types/src/ui/components/dialog.d.ts +17 -0
  91. package/dist/host/types/src/ui/components/dropdown-menu.d.ts +29 -0
  92. package/dist/host/types/src/ui/components/empty-state.d.ts +19 -0
  93. package/dist/host/types/src/ui/components/form-fields.d.ts +115 -0
  94. package/dist/host/types/src/ui/components/input.d.ts +3 -0
  95. package/dist/host/types/src/ui/components/kbd.d.ts +12 -0
  96. package/dist/host/types/src/ui/components/label.d.ts +4 -0
  97. package/dist/host/types/src/ui/components/manager-card.d.ts +25 -0
  98. package/dist/host/types/src/ui/components/meter.d.ts +14 -0
  99. package/dist/host/types/src/ui/components/metric-card.d.ts +18 -0
  100. package/dist/host/types/src/ui/components/panel-layout.d.ts +83 -0
  101. package/dist/host/types/src/ui/components/password-input.d.ts +4 -0
  102. package/dist/host/types/src/ui/components/popover.d.ts +7 -0
  103. package/dist/host/types/src/ui/components/scroll-area.d.ts +5 -0
  104. package/dist/host/types/src/ui/components/section-card.d.ts +29 -0
  105. package/dist/host/types/src/ui/components/select.d.ts +15 -0
  106. package/dist/host/types/src/ui/components/select2.d.ts +8 -0
  107. package/dist/host/types/src/ui/components/separator.d.ts +4 -0
  108. package/dist/host/types/src/ui/components/sheet.d.ts +14 -0
  109. package/dist/host/types/src/ui/components/sidebar.d.ts +69 -0
  110. package/dist/host/types/src/ui/components/skeleton.d.ts +2 -0
  111. package/dist/host/types/src/ui/components/slider.d.ts +4 -0
  112. package/dist/host/types/src/ui/components/sonner.d.ts +4 -0
  113. package/dist/host/types/src/ui/components/surface/prompt-styles.d.ts +4 -0
  114. package/dist/host/types/src/ui/components/surface/surface-scope.d.ts +79 -0
  115. package/dist/host/types/src/ui/components/switch.d.ts +4 -0
  116. package/dist/host/types/src/ui/components/tab-strip.d.ts +19 -0
  117. package/dist/host/types/src/ui/components/tabs.d.ts +7 -0
  118. package/dist/host/types/src/ui/components/textarea.d.ts +4 -0
  119. package/dist/host/types/src/ui/components/theme-provider.d.ts +14 -0
  120. package/dist/host/types/src/ui/components/tooltip.d.ts +7 -0
  121. package/dist/host/types/src/ui/contexts/BrandingContext.d.ts +15 -0
  122. package/dist/host/types/src/ui/contexts/UiPreferencesContext.d.ts +38 -0
  123. package/dist/host/types/src/ui/contexts/branding-document.d.ts +10 -0
  124. package/dist/host/types/src/ui/dashboard/dashboard-cards-registry.d.ts +30 -0
  125. package/dist/host/types/src/ui/hooks/use-action-slot.d.ts +10 -0
  126. package/dist/host/types/src/ui/hooks/use-adaptive-polling.d.ts +5 -0
  127. package/dist/host/types/src/ui/hooks/use-mobile.d.ts +1 -0
  128. package/dist/host/types/src/ui/hooks/use-permissions.d.ts +29 -0
  129. package/dist/host/types/src/ui/hooks/use-status-color-scheme.d.ts +12 -0
  130. package/dist/host/types/src/ui/hooks/use-sync-status.d.ts +4 -0
  131. package/dist/host/types/src/ui/hooks/useSshAuthProviders.d.ts +17 -0
  132. package/dist/host/types/src/ui/i18n/i18n.d.ts +52 -0
  133. package/dist/host/types/src/ui/lib/ServerStatusContext.d.ts +33 -0
  134. package/dist/host/types/src/ui/lib/adaptive-polling.d.ts +29 -0
  135. package/dist/host/types/src/ui/lib/adaptive-resource-budget.d.ts +39 -0
  136. package/dist/host/types/src/ui/lib/app-keyboard-shortcuts.d.ts +8 -0
  137. package/dist/host/types/src/ui/lib/base-path.d.ts +1 -0
  138. package/dist/host/types/src/ui/lib/clipboard-provider.d.ts +9 -0
  139. package/dist/host/types/src/ui/lib/clipboard.d.ts +20 -0
  140. package/dist/host/types/src/ui/lib/connection-origin.d.ts +47 -0
  141. package/dist/host/types/src/ui/lib/db-health-monitor.d.ts +34 -0
  142. package/dist/host/types/src/ui/lib/device-id.d.ts +1 -0
  143. package/dist/host/types/src/ui/lib/electron.d.ts +1 -0
  144. package/dist/host/types/src/ui/lib/embedded-server-status.d.ts +15 -0
  145. package/dist/host/types/src/ui/lib/error-message.d.ts +9 -0
  146. package/dist/host/types/src/ui/lib/frontend-logger.d.ts +67 -0
  147. package/dist/host/types/src/ui/lib/global-shortcut-handler.d.ts +3 -0
  148. package/dist/host/types/src/ui/lib/host-defaults-context.d.ts +48 -0
  149. package/dist/host/types/src/ui/lib/hosts-request-cache.d.ts +9 -0
  150. package/dist/host/types/src/ui/lib/keybinding-match.d.ts +3 -0
  151. package/dist/host/types/src/ui/lib/linked-server.d.ts +10 -0
  152. package/dist/host/types/src/ui/lib/local-adaptive-engine.d.ts +40 -0
  153. package/dist/host/types/src/ui/lib/permissions.d.ts +16 -0
  154. package/dist/host/types/src/ui/lib/plugin-i18n.d.ts +11 -0
  155. package/dist/host/types/src/ui/lib/plugin-icon.d.ts +11 -0
  156. package/dist/host/types/src/ui/lib/plugin-transport.d.ts +50 -0
  157. package/dist/host/types/src/ui/lib/registry.d.ts +29 -0
  158. package/dist/host/types/src/ui/lib/rem.d.ts +7 -0
  159. package/dist/host/types/src/ui/lib/remote-server-api.d.ts +11 -0
  160. package/dist/host/types/src/ui/lib/server-status-store.d.ts +37 -0
  161. package/dist/host/types/src/ui/lib/sync-events.d.ts +2 -0
  162. package/dist/host/types/src/ui/lib/sync-status.d.ts +6 -0
  163. package/dist/host/types/src/ui/lib/tab-jump-hotkey.d.ts +4 -0
  164. package/dist/host/types/src/ui/lib/theme.d.ts +21 -0
  165. package/dist/host/types/src/ui/lib/ttl-request-cache.d.ts +10 -0
  166. package/dist/host/types/src/ui/lib/usage-color.d.ts +5 -0
  167. package/dist/host/types/src/ui/lib/utils.d.ts +2 -0
  168. package/dist/host/types/src/ui/lib/visible-interval.d.ts +5 -0
  169. package/dist/host/types/src/ui/lib/widget-title.d.ts +8 -0
  170. package/dist/host/types/src/ui/lib/ws-auth.d.ts +1 -0
  171. package/dist/host/types/src/ui/main-axios.d.ts +170 -0
  172. package/dist/host/types/src/ui/plugin-host/PluginViewPlaceholder.d.ts +13 -0
  173. package/dist/host/types/src/ui/plugin-host/app.d.ts +18 -0
  174. package/dist/host/types/src/ui/plugin-host/auth-registry.d.ts +50 -0
  175. package/dist/host/types/src/ui/plugin-host/bridge.d.ts +26 -0
  176. package/dist/host/types/src/ui/plugin-host/component-registry.d.ts +24 -0
  177. package/dist/host/types/src/ui/plugin-host/desktop.d.ts +7 -0
  178. package/dist/host/types/src/ui/plugin-host/extension-registry.d.ts +16 -0
  179. package/dist/host/types/src/ui/plugin-host/host-scope.d.ts +26 -0
  180. package/dist/host/types/src/ui/plugin-host/loader.d.ts +52 -0
  181. package/dist/host/types/src/ui/plugin-host/plugin-store.d.ts +40 -0
  182. package/dist/host/types/src/ui/plugin-host/scope.d.ts +20 -0
  183. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +99 -0
  184. package/dist/host/types/src/ui/plugin-host/shell-bridge.d.ts +67 -0
  185. package/dist/host/types/src/ui/plugin-host/view-ownership.d.ts +31 -0
  186. package/dist/host/types/src/ui/settings/HostPluginSections.d.ts +31 -0
  187. package/dist/host/types/src/ui/settings/SettingsFields.d.ts +22 -0
  188. package/dist/host/types/src/ui/settings/host-feature-tabs.d.ts +11 -0
  189. package/dist/host/types/src/ui/settings/settings-components.d.ts +32 -0
  190. package/dist/host/types/src/ui/settings/settings-fields-util.d.ts +11 -0
  191. package/dist/host/types/src/ui/shell/ActionSlot.d.ts +41 -0
  192. package/dist/host/types/src/ui/shell/TabContext.d.ts +30 -0
  193. package/dist/host/types/src/ui/shell/action-registry.d.ts +99 -0
  194. package/dist/host/types/src/ui/shell/keybinding-registry.d.ts +62 -0
  195. package/dist/host/types/src/ui/shell/palette-registry.d.ts +55 -0
  196. package/dist/host/types/src/ui/shell/panel-registry.d.ts +24 -0
  197. package/dist/host/types/src/ui/shell/tab-registry.d.ts +92 -0
  198. package/dist/host/types/src/ui/sidebar/HostManagerTabs.d.ts +65 -0
  199. package/dist/host/types/src/ui/sidebar/host-contributions.d.ts +72 -0
  200. package/dist/host/types/src/ui/sidebar/host-defaults/catalog.d.ts +17 -0
  201. package/dist/host/types/src/ui/sidebar/host-protocols.d.ts +49 -0
  202. package/dist/host/types/src/ui/sidebar/quick-connect-host.d.ts +21 -0
  203. package/dist/host/types/src/ui/sidebar/rail-items.d.ts +103 -0
  204. package/dist/host/types/src/ui/ssh/connection-log/ConnectionLogContext.d.ts +17 -0
  205. package/dist/host/types/src/ui/ssh/dialogs/BrowserSignInDialog.d.ts +18 -0
  206. package/dist/host/types/src/ui/ssh/dialogs/HostKeyVerificationDialog.d.ts +17 -0
  207. package/dist/host/types/src/ui/ssh/dialogs/PassphraseDialog.d.ts +15 -0
  208. package/dist/host/types/src/ui/ssh/dialogs/SSHAuthDialog.d.ts +20 -0
  209. package/dist/host/types/src/ui/ssh/dialogs/TOTPDialog.d.ts +13 -0
  210. package/dist/host/ui.d.ts +1 -0
  211. package/dist/host/ui.js +31754 -0
  212. package/dist/host/use-permissions-NULZL43J.js +18 -0
  213. package/dist/host-commands.d.ts +89 -0
  214. package/dist/host-commands.js +387 -0
  215. package/dist/manifest.d.ts +461 -0
  216. package/dist/manifest.js +1499 -0
  217. package/dist/settings.d.ts +43 -0
  218. package/dist/settings.js +168 -0
  219. package/dist/ssh-certs.d.ts +25 -0
  220. package/dist/ssh-certs.js +234 -0
  221. package/dist/ssh-options.d.ts +23 -0
  222. package/dist/ssh-options.js +1 -0
  223. package/dist/table-builder.d.ts +26 -0
  224. package/dist/table-builder.js +87 -0
  225. package/dist/testing/safe-ports.d.ts +10 -0
  226. package/dist/testing/safe-ports.js +59 -0
  227. package/dist/testing/setup.d.ts +7 -0
  228. package/dist/testing/setup.js +146 -0
  229. package/dist/testing.d.ts +537 -0
  230. package/dist/testing.js +1451 -0
  231. package/dist/vitest-preset.d.ts +54 -0
  232. package/dist/vitest-preset.js +96 -0
  233. package/package.json +143 -0
  234. package/schema/changelog.schema.json +67 -0
  235. package/schema/manifest.schema.json +1160 -0
  236. 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
+ }