@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
@@ -0,0 +1,33 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+
5
+ /**
6
+ * Runs vitest through its own entry rather than the npx shim, so no shell is
7
+ * involved and paths with spaces work the same on every platform.
8
+ */
9
+ export async function test({ cwd, args }) {
10
+ const require = createRequire(import.meta.url);
11
+
12
+ let vitestBin;
13
+ try {
14
+ // vitest.mjs is the package bin, not an exported subpath, so resolve the
15
+ // manifest and walk to it.
16
+ const manifest = require.resolve("vitest/package.json", { paths: [cwd] });
17
+ vitestBin = path.join(path.dirname(manifest), "vitest.mjs");
18
+ } catch {
19
+ throw new Error(
20
+ "vitest is not installed. Run npm install at the repo root.",
21
+ );
22
+ }
23
+
24
+ const result = spawnSync(
25
+ process.execPath,
26
+ [vitestBin, "run", "--config", "vitest.config.ts", ...args],
27
+ { cwd, stdio: "inherit" },
28
+ );
29
+
30
+ if (result.status !== 0) {
31
+ throw new Error(`vitest exited with ${result.status}`);
32
+ }
33
+ }
@@ -0,0 +1,215 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { readManifest } from "../lib/plugin-dir.mjs";
5
+ import { validateChangelog } from "../lib/changelog.mjs";
6
+
7
+ /**
8
+ * The manifest rules live in src/manifest.ts, which the server uses too, so
9
+ * there is one implementation rather than a copy here that drifts.
10
+ */
11
+ async function loadParseManifest() {
12
+ const entry = new URL("../../dist/manifest.js", import.meta.url);
13
+ if (!fs.existsSync(fileURLToPath(entry))) {
14
+ throw new Error("The plugin SDK is not built yet. Run: npm run build:sdk");
15
+ }
16
+ const mod = await import(entry.href);
17
+ return mod.parseManifest;
18
+ }
19
+
20
+ export async function validate({ cwd }) {
21
+ const raw = readManifest(cwd);
22
+ const parseManifest = await loadParseManifest();
23
+ const { errors } = parseManifest(raw);
24
+
25
+ const problems = [...errors];
26
+
27
+ // The manifest names files. They have to be there.
28
+ const referenced = [
29
+ raw.backend ?? "dist/backend.js",
30
+ raw.frontend ?? "dist/frontend.js",
31
+ raw.locales ?? "locales",
32
+ ];
33
+ for (const rel of referenced) {
34
+ if (!fs.existsSync(path.join(cwd, rel))) {
35
+ problems.push(`manifest references ${rel}, which does not exist`);
36
+ }
37
+ }
38
+
39
+ problems.push(
40
+ ...(await validateMigrations(cwd, raw.id ?? path.basename(cwd))),
41
+ );
42
+ problems.push(...validateNativeDependencies(cwd, raw));
43
+ problems.push(...validatePackage(cwd, raw));
44
+ problems.push(...validateChangelogFile(cwd, raw));
45
+
46
+ if (problems.length > 0) {
47
+ for (const problem of problems) console.error(` ${problem}`);
48
+ throw new Error(`${raw.id ?? path.basename(cwd)}: manifest is not valid.`);
49
+ }
50
+
51
+ console.log(`ok ${raw.id ?? path.basename(cwd)}`);
52
+ }
53
+
54
+ /**
55
+ * package.json has to agree with the manifest: the same version, and a real
56
+ * SDK range rather than "*", so a plugin built against a newer SDK says so.
57
+ */
58
+ function validatePackage(cwd, raw) {
59
+ const problems = [];
60
+ const pkgPath = path.join(cwd, "package.json");
61
+ if (!fs.existsSync(pkgPath)) return problems;
62
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
63
+
64
+ if (pkg.version && raw.version && pkg.version !== raw.version) {
65
+ problems.push(
66
+ `package.json version ${pkg.version} does not match manifest version ${raw.version}`,
67
+ );
68
+ }
69
+
70
+ const sdkRange =
71
+ pkg.peerDependencies?.["@termix-ssh/plugin-sdk"] ??
72
+ pkg.dependencies?.["@termix-ssh/plugin-sdk"] ??
73
+ pkg.devDependencies?.["@termix-ssh/plugin-sdk"];
74
+ if (!sdkRange) {
75
+ problems.push("package.json does not depend on @termix-ssh/plugin-sdk");
76
+ } else if (sdkRange === "*" || sdkRange === "latest") {
77
+ problems.push(
78
+ `@termix-ssh/plugin-sdk is "${sdkRange}"; pin a range such as "^1.0.0"`,
79
+ );
80
+ }
81
+ return problems;
82
+ }
83
+
84
+ /**
85
+ * CHANGELOG.json is optional, but when it is there it has to parse and its
86
+ * newest release has to match the manifest version.
87
+ */
88
+ function validateChangelogFile(cwd, raw) {
89
+ const jsonPath = path.join(cwd, "CHANGELOG.json");
90
+ if (!fs.existsSync(jsonPath)) {
91
+ return fs.existsSync(path.join(cwd, "CHANGELOG.md"))
92
+ ? [
93
+ "CHANGELOG.md is no longer shipped; move the release notes to CHANGELOG.json",
94
+ ]
95
+ : [];
96
+ }
97
+ let changelog;
98
+ try {
99
+ changelog = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
100
+ } catch (error) {
101
+ return [`CHANGELOG.json is not valid JSON: ${error.message}`];
102
+ }
103
+ return validateChangelog(changelog, raw.version).map(
104
+ (problem) => `CHANGELOG.json: ${problem}`,
105
+ );
106
+ }
107
+
108
+ /**
109
+ * A native dependency has to actually be a real dependency of the plugin, or
110
+ * the build's external declaration points at nothing node_modules can
111
+ * resolve at runtime.
112
+ */
113
+ function validateNativeDependencies(cwd, raw) {
114
+ const problems = [];
115
+ const nativeDependencies = raw.nativeDependencies ?? [];
116
+ if (nativeDependencies.length === 0) return problems;
117
+
118
+ const pkgPath = path.join(cwd, "package.json");
119
+ const pkg = fs.existsSync(pkgPath)
120
+ ? JSON.parse(fs.readFileSync(pkgPath, "utf8"))
121
+ : {};
122
+ const deps = { ...pkg.dependencies };
123
+
124
+ for (const name of nativeDependencies) {
125
+ if (!(name in deps)) {
126
+ problems.push(
127
+ `nativeDependencies names "${name}", which is not in this plugin's own package.json dependencies`,
128
+ );
129
+ }
130
+ }
131
+
132
+ return problems;
133
+ }
134
+
135
+ const DIALECTS = ["sqlite", "postgres", "mysql"];
136
+
137
+ /**
138
+ * Checks that a plugin's migrations only touch tables it owns.
139
+ *
140
+ * The server enforces this too, because a plugin installed from a tarball
141
+ * never ran this command. Here it is a build-time error with a file name
142
+ * attached, rather than a plugin that fails to activate later.
143
+ */
144
+ async function validateMigrations(cwd, pluginId) {
145
+ const { LEGACY_TABLE_OWNERS: LEGACY_TABLES } =
146
+ await import("../../dist/db.js");
147
+ const { collectOwnedIndexes, findUnownedTableWrites } =
148
+ await import("../../dist/ddl.js");
149
+
150
+ const problems = [];
151
+ const legacy = new Set(
152
+ Object.entries(LEGACY_TABLES)
153
+ .filter(([, owner]) => owner === pluginId)
154
+ .map(([table]) => table),
155
+ );
156
+ const root = path.join(cwd, "migrations");
157
+ if (!fs.existsSync(root)) return problems;
158
+
159
+ const present = DIALECTS.filter((dialect) =>
160
+ fs.existsSync(path.join(root, dialect)),
161
+ );
162
+
163
+ const byDialect = new Map();
164
+ for (const dialect of present) {
165
+ const dir = path.join(root, dialect);
166
+ const files = fs
167
+ .readdirSync(dir)
168
+ .filter((file) => file.endsWith(".sql"))
169
+ .sort();
170
+ byDialect.set(dialect, files);
171
+ const sqls = files.map((file) =>
172
+ fs.readFileSync(path.join(dir, file), "utf8"),
173
+ );
174
+ const indexes = collectOwnedIndexes(pluginId, sqls, legacy);
175
+
176
+ for (const [index, file] of files.entries()) {
177
+ if (!/^\d{4}_[a-z0-9_]+\.sql$/.test(file)) {
178
+ problems.push(
179
+ `migrations/${dialect}/${file} must be named NNNN_name.sql, lower snake_case`,
180
+ );
181
+ }
182
+
183
+ const sql = sqls[index];
184
+ for (const problem of findUnownedTableWrites(
185
+ pluginId,
186
+ sql,
187
+ legacy,
188
+ indexes,
189
+ )) {
190
+ problems.push(`migrations/${dialect}/${file} ${problem}`);
191
+ }
192
+ }
193
+ }
194
+
195
+ // A migration that exists for one engine and not another leaves that
196
+ // deployment without the table, which is how alert_rules ended up
197
+ // SQLite-only in core.
198
+ const [first, ...rest] = present;
199
+ for (const dialect of rest) {
200
+ const a = byDialect.get(first);
201
+ const b = byDialect.get(dialect);
202
+ for (const file of a) {
203
+ if (!b.includes(file)) {
204
+ problems.push(`migrations/${dialect}/${file} is missing`);
205
+ }
206
+ }
207
+ for (const file of b) {
208
+ if (!a.includes(file)) {
209
+ problems.push(`migrations/${first}/${file} is missing`);
210
+ }
211
+ }
212
+ }
213
+
214
+ return problems;
215
+ }
package/cli/index.mjs ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * termix-plugin: build, validate, test, pack and sign a Termix plugin.
4
+ *
5
+ * Run from a plugin directory (a plugin repo's npm run build does).
6
+ */
7
+
8
+ import process from "node:process";
9
+ import { applyPatches, build } from "./commands/build.mjs";
10
+ import { readManifest } from "./lib/plugin-dir.mjs";
11
+ import { validate } from "./commands/validate.mjs";
12
+ import { test } from "./commands/test.mjs";
13
+ import { pack } from "./commands/pack.mjs";
14
+ import { sign, verify, keygen } from "./commands/sign.mjs";
15
+ import { migrations } from "./commands/migrations.mjs";
16
+
17
+ const COMMANDS = {
18
+ build,
19
+ patch: async ({ cwd }) => applyPatches(cwd, readManifest(cwd).id),
20
+ validate,
21
+ // The tests exercise patched dependencies, so patch first like build does.
22
+ test: async (options) => {
23
+ applyPatches(options.cwd, readManifest(options.cwd).id);
24
+ return test(options);
25
+ },
26
+ pack,
27
+ sign,
28
+ verify,
29
+ keygen,
30
+ migrations,
31
+ };
32
+
33
+ const [command, ...args] = process.argv.slice(2);
34
+
35
+ if (!command || command === "--help" || command === "-h") {
36
+ console.log(
37
+ [
38
+ "Usage: termix-plugin <command>",
39
+ "",
40
+ " build Bundle the plugin into dist/",
41
+ " patch Apply the plugin's dependency patches (build does this too)",
42
+ " validate Check manifest.json and the files it names",
43
+ " test Apply the patches, then run the plugin's vitest suite",
44
+ " pack Write <id>-<version>.tmxplug of the built plugin [--out dir]",
45
+ " sign Sign a .tmxplug with TERMIX_PLUGIN_SIGNING_KEY <file>",
46
+ " verify Check a .tmxplug's .sig <file> --key <base64>[,...]",
47
+ " keygen Write a new signing key pair [--out dir]",
48
+ " migrations Generate migrations from the plugin's table definitions",
49
+ ].join("\n"),
50
+ );
51
+ process.exit(command ? 0 : 1);
52
+ }
53
+
54
+ const run = COMMANDS[command];
55
+ if (!run) {
56
+ console.error(`Unknown command: ${command}`);
57
+ process.exit(1);
58
+ }
59
+
60
+ try {
61
+ await run({ cwd: process.cwd(), args });
62
+ } catch (error) {
63
+ console.error(error?.message ?? error);
64
+ process.exit(1);
65
+ }
@@ -0,0 +1,126 @@
1
+ export const CHANGE_TYPES = [
2
+ "added",
3
+ "changed",
4
+ "fixed",
5
+ "removed",
6
+ "deprecated",
7
+ "security",
8
+ ];
9
+
10
+ const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
11
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
12
+ const TOP_KEYS = ["$schema", "unreleased", "releases"];
13
+ const RELEASE_KEYS = ["version", "date", "notes", "changes"];
14
+ const CHANGE_KEYS = ["type", "text"];
15
+
16
+ function compareVersions(a, b) {
17
+ const pa = SEMVER.exec(a);
18
+ const pb = SEMVER.exec(b);
19
+ for (let i = 1; i <= 3; i++) {
20
+ const diff = Number(pa[i]) - Number(pb[i]);
21
+ if (diff !== 0) return diff;
22
+ }
23
+ if (pa[4] === pb[4]) return 0;
24
+ if (!pa[4]) return 1;
25
+ if (!pb[4]) return -1;
26
+ return pa[4] < pb[4] ? -1 : 1;
27
+ }
28
+
29
+ function unknownKeys(value, allowed, where, problems) {
30
+ for (const key of Object.keys(value)) {
31
+ if (!allowed.includes(key))
32
+ problems.push(`${where} has unknown key "${key}"`);
33
+ }
34
+ }
35
+
36
+ function checkChanges(changes, where, problems) {
37
+ if (!Array.isArray(changes)) {
38
+ problems.push(`${where} must be an array`);
39
+ return;
40
+ }
41
+ changes.forEach((change, i) => {
42
+ const at = `${where}[${i}]`;
43
+ if (!change || typeof change !== "object" || Array.isArray(change)) {
44
+ problems.push(`${at} must be an object`);
45
+ return;
46
+ }
47
+ unknownKeys(change, CHANGE_KEYS, at, problems);
48
+ if (!CHANGE_TYPES.includes(change.type)) {
49
+ problems.push(`${at}.type must be one of ${CHANGE_TYPES.join(", ")}`);
50
+ }
51
+ if (typeof change.text !== "string" || change.text.trim() === "") {
52
+ problems.push(`${at}.text must be a non-empty string`);
53
+ }
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Checks a parsed CHANGELOG.json. The newest release has to be the version
59
+ * in the manifest, so a release never ships without its notes.
60
+ */
61
+ export function validateChangelog(changelog, manifestVersion) {
62
+ const problems = [];
63
+ if (!changelog || typeof changelog !== "object" || Array.isArray(changelog)) {
64
+ return ["CHANGELOG.json must be an object"];
65
+ }
66
+ unknownKeys(changelog, TOP_KEYS, "CHANGELOG.json", problems);
67
+
68
+ if (changelog.unreleased !== undefined) {
69
+ checkChanges(changelog.unreleased, "unreleased", problems);
70
+ }
71
+
72
+ const releases = changelog.releases;
73
+ if (!Array.isArray(releases) || releases.length === 0) {
74
+ problems.push("releases must be a non-empty array");
75
+ return problems;
76
+ }
77
+
78
+ const seen = new Set();
79
+ releases.forEach((release, i) => {
80
+ const at = `releases[${i}]`;
81
+ if (!release || typeof release !== "object" || Array.isArray(release)) {
82
+ problems.push(`${at} must be an object`);
83
+ return;
84
+ }
85
+ unknownKeys(release, RELEASE_KEYS, at, problems);
86
+ if (typeof release.version !== "string" || !SEMVER.test(release.version)) {
87
+ problems.push(`${at}.version must be a version like 1.2.3`);
88
+ } else if (seen.has(release.version)) {
89
+ problems.push(`${at}.version ${release.version} is listed twice`);
90
+ } else {
91
+ seen.add(release.version);
92
+ }
93
+ if (
94
+ release.date !== undefined &&
95
+ (typeof release.date !== "string" || !DATE.test(release.date))
96
+ ) {
97
+ problems.push(`${at}.date must be a date like 2026-10-01`);
98
+ }
99
+ if (release.notes !== undefined && typeof release.notes !== "string") {
100
+ problems.push(`${at}.notes must be a string`);
101
+ }
102
+ checkChanges(release.changes, `${at}.changes`, problems);
103
+ if (Array.isArray(release.changes) && release.changes.length === 0) {
104
+ problems.push(`${at}.changes must list at least one change`);
105
+ }
106
+ });
107
+
108
+ const versions = releases
109
+ .map((release) => release?.version)
110
+ .filter((v) => typeof v === "string" && SEMVER.test(v));
111
+ for (let i = 1; i < versions.length; i++) {
112
+ if (compareVersions(versions[i - 1], versions[i]) < 0) {
113
+ problems.push("releases must be listed newest first");
114
+ break;
115
+ }
116
+ }
117
+
118
+ const newest = releases[0]?.version;
119
+ if (manifestVersion && newest !== manifestVersion) {
120
+ problems.push(
121
+ `the newest release is ${newest}, but manifest version is ${manifestVersion}`,
122
+ );
123
+ }
124
+
125
+ return problems;
126
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Host-provided packages.
3
+ *
4
+ * These ship with the Termix server and are never bundled into a plugin:
5
+ * bundling them would give each plugin its own copy of express or React, and
6
+ * a second ssh2 would mean a second set of native bindings. A plugin may
7
+ * depend on anything outside these lists, and that does get bundled.
8
+ *
9
+ * A plugin lists the ones it uses as peerDependencies. Only add a package here
10
+ * when core itself ships it; a library one plugin needs belongs in that
11
+ * plugin's own dependencies, so it can update without a core release.
12
+ *
13
+ * Node builtins are external automatically through platform: "node".
14
+ */
15
+
16
+ export const BACKEND_EXTERNALS = [
17
+ "@termix-ssh/plugin-sdk",
18
+ "@termix-ssh/plugin-sdk/*",
19
+ "express",
20
+ "ssh2",
21
+ "ws",
22
+ "multer",
23
+ "cookie-parser",
24
+ "axios",
25
+ "drizzle-orm",
26
+ "drizzle-orm/*",
27
+ // Native image processing, loaded on first use by the terminal's image upload.
28
+ "sharp",
29
+ ];
30
+
31
+ /**
32
+ * Resolved by the page's import map to the shell's own copies, because these
33
+ * must be one instance: React, i18next, the toast store, and the SDK entries
34
+ * that share state with core. Anything else a plugin imports is bundled.
35
+ */
36
+ export const FRONTEND_EXTERNALS = [
37
+ "react",
38
+ "react-dom",
39
+ "react/jsx-runtime",
40
+ "react-dom/client",
41
+ "i18next",
42
+ "react-i18next",
43
+ "sonner",
44
+ "@termix-ssh/plugin-sdk/frontend",
45
+ "@termix-ssh/plugin-sdk/ui",
46
+ ];
@@ -0,0 +1,29 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function readManifest(cwd) {
5
+ const manifestPath = path.join(cwd, "manifest.json");
6
+ if (!fs.existsSync(manifestPath)) {
7
+ throw new Error(`No manifest.json in ${cwd}`);
8
+ }
9
+ try {
10
+ return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
11
+ } catch (error) {
12
+ throw new Error(`manifest.json is not valid JSON: ${error.message}`);
13
+ }
14
+ }
15
+
16
+ /** The first of these that exists, or null. */
17
+ export function resolveEntry(cwd, candidates) {
18
+ for (const candidate of candidates) {
19
+ const full = path.join(cwd, candidate);
20
+ if (fs.existsSync(full)) return full;
21
+ }
22
+ return null;
23
+ }
24
+
25
+ export function copyDir(from, to) {
26
+ if (!fs.existsSync(from)) return false;
27
+ fs.cpSync(from, to, { recursive: true });
28
+ return true;
29
+ }
@@ -0,0 +1,86 @@
1
+ import crypto from "node:crypto";
2
+
3
+ /** The env var that holds the base64 PKCS8 private key. Never commit it. */
4
+ export const SIGNING_KEY_ENV = "TERMIX_PLUGIN_SIGNING_KEY";
5
+
6
+ // DER prefix of an Ed25519 SubjectPublicKeyInfo; the raw 32-byte key follows.
7
+ const SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
8
+
9
+ export function sha256(buffer) {
10
+ return crypto.createHash("sha256").update(buffer).digest();
11
+ }
12
+
13
+ export function publicKeyFromRaw(base64) {
14
+ const raw = Buffer.from(base64.trim(), "base64");
15
+ if (raw.length !== 32) throw new Error("A public key is 32 bytes, base64");
16
+ return crypto.createPublicKey({
17
+ key: Buffer.concat([SPKI_PREFIX, raw]),
18
+ format: "der",
19
+ type: "spki",
20
+ });
21
+ }
22
+
23
+ export function rawPublicKey(keyObject) {
24
+ const der = keyObject.export({ format: "der", type: "spki" });
25
+ return der.subarray(SPKI_PREFIX.length).toString("base64");
26
+ }
27
+
28
+ /** First 16 hex chars of the sha256 of the raw public key. */
29
+ export function keyId(publicKeyBase64) {
30
+ return sha256(Buffer.from(publicKeyBase64, "base64"))
31
+ .toString("hex")
32
+ .slice(0, 16);
33
+ }
34
+
35
+ export function generateKeyPair() {
36
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
37
+ const publicBase64 = rawPublicKey(publicKey);
38
+ return {
39
+ publicKey: publicBase64,
40
+ privateKey: privateKey
41
+ .export({ format: "der", type: "pkcs8" })
42
+ .toString("base64"),
43
+ keyId: keyId(publicBase64),
44
+ };
45
+ }
46
+
47
+ /** Accepts base64 PKCS8 DER (what keygen writes) or a PEM block. */
48
+ export function loadPrivateKey(value) {
49
+ if (!value || !value.trim()) {
50
+ throw new Error(`${SIGNING_KEY_ENV} is not set`);
51
+ }
52
+ const trimmed = value.trim();
53
+ const key = trimmed.startsWith("-----BEGIN")
54
+ ? crypto.createPrivateKey(trimmed)
55
+ : crypto.createPrivateKey({
56
+ key: Buffer.from(trimmed, "base64"),
57
+ format: "der",
58
+ type: "pkcs8",
59
+ });
60
+ if (key.asymmetricKeyType !== "ed25519") {
61
+ throw new Error("The signing key must be an Ed25519 key");
62
+ }
63
+ return key;
64
+ }
65
+
66
+ /** Ed25519 over the raw 32-byte sha256 of the artifact, base64. */
67
+ export function signArtifact(buffer, privateKey) {
68
+ return crypto.sign(null, sha256(buffer), privateKey).toString("base64");
69
+ }
70
+
71
+ /** The id of the first key that verifies the signature, or null. */
72
+ export function verifyArtifact(buffer, signatureBase64, publicKeys) {
73
+ const digest = sha256(buffer);
74
+ const signature = Buffer.from(signatureBase64.trim(), "base64");
75
+ if (signature.length !== 64) return null;
76
+ for (const key of publicKeys) {
77
+ try {
78
+ if (crypto.verify(null, digest, publicKeyFromRaw(key), signature)) {
79
+ return keyId(key);
80
+ }
81
+ } catch {
82
+ // A malformed key is just a key that does not match.
83
+ }
84
+ }
85
+ return null;
86
+ }
@@ -0,0 +1,54 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * `import url from "some/file.js?url"` in a plugin frontend, as core's Vite
7
+ * build allows: the file is copied next to the bundle under assets/ and the
8
+ * import becomes its URL, resolved against the bundle's own URL so it works
9
+ * under /plugin-assets/<id>/. Only scripts, since that is all the asset
10
+ * route serves (a pdfjs or Monaco worker, say).
11
+ */
12
+ export function staticUrlImports({ outDir }) {
13
+ return {
14
+ name: "static-url-imports",
15
+ setup(build) {
16
+ build.onResolve({ filter: /\?url$/ }, async (args) => {
17
+ const target = args.path.slice(0, -"?url".length);
18
+ const resolved = await build.resolve(target, {
19
+ kind: args.kind,
20
+ resolveDir: args.resolveDir,
21
+ importer: args.importer,
22
+ });
23
+ if (resolved.errors.length > 0) return { errors: resolved.errors };
24
+ return { path: resolved.path, namespace: "static-url" };
25
+ });
26
+
27
+ build.onLoad({ filter: /.*/, namespace: "static-url" }, (args) => {
28
+ if (!/\.(m?js|cjs)$/.test(args.path)) {
29
+ return {
30
+ errors: [
31
+ {
32
+ text: `${args.path}: only script files can be imported with ?url`,
33
+ },
34
+ ],
35
+ };
36
+ }
37
+ const bytes = fs.readFileSync(args.path);
38
+ const hash = crypto
39
+ .createHash("sha256")
40
+ .update(bytes)
41
+ .digest("hex")
42
+ .slice(0, 8);
43
+ const base = path.basename(args.path).replace(/\.(m?js|cjs)$/, "");
44
+ const name = `${base}-${hash}.js`;
45
+ fs.mkdirSync(path.join(outDir, "assets"), { recursive: true });
46
+ fs.writeFileSync(path.join(outDir, "assets", name), bytes);
47
+ return {
48
+ contents: `export default new URL("./assets/${name}", import.meta.url).href;`,
49
+ loader: "js",
50
+ };
51
+ });
52
+ },
53
+ };
54
+ }