@termix-ssh/plugin-sdk 1.0.11 → 1.0.12

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 (117) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/PLUGIN-GUIDE.md +95 -3
  3. package/cli/commands/build.mjs +36 -22
  4. package/cli/commands/pack.mjs +12 -2
  5. package/cli/commands/validate.mjs +63 -3
  6. package/cli/lib/plugin-dir.mjs +11 -0
  7. package/cli/lib/tmxplug.mjs +9 -0
  8. package/dist/capabilities.js +2 -0
  9. package/dist/frontend.d.ts +152 -0
  10. package/dist/frontend.js +7 -0
  11. package/dist/host/PluginsTab-EV4MSGY6.js +2933 -0
  12. package/dist/host/{chunk-RLQBKJRK.js → chunk-AWR6FYDO.js} +8873 -11163
  13. package/dist/host/{chunk-7RRTYD6B.js → chunk-RUPLD6RF.js} +1497 -15
  14. package/dist/host/{chunk-LSWB3ZZV.js → chunk-VEWOYAMZ.js} +6 -1
  15. package/dist/host/chunk-X4QZKO5G.js +3120 -0
  16. package/dist/host/chunk-XKA3DBB2.js +10375 -0
  17. package/dist/host/chunk-YGYBM7SQ.js +58 -0
  18. package/dist/host/core-classes.json +1 -1
  19. package/dist/host/{linked-server-UCW74T2V.js → linked-server-B4NEG4MA.js} +2 -2
  20. package/dist/host/{rbac-api-6UYMFIFV.js → rbac-api-OUO3LUOC.js} +2 -2
  21. package/dist/host/testing-host.js +123 -57
  22. package/dist/host/types/src/types/ui-types.d.ts +2 -1
  23. package/dist/host/types/src/ui/api/audit-log-api.d.ts +46 -0
  24. package/dist/host/types/src/ui/api/host-defaults-api.d.ts +42 -0
  25. package/dist/host/types/src/ui/api/host-tags-api.d.ts +2 -0
  26. package/dist/host/types/src/ui/api/look-api.d.ts +40 -0
  27. package/dist/host/types/src/ui/api/open-tabs-api.d.ts +4 -0
  28. package/dist/host/types/src/ui/api/plugins-api.d.ts +26 -0
  29. package/dist/host/types/src/ui/api/private-endpoints-api.d.ts +6 -0
  30. package/dist/host/types/src/ui/api/tls-api.d.ts +25 -0
  31. package/dist/host/types/src/ui/auth/ElectronLoginForm.d.ts +9 -0
  32. package/dist/host/types/src/ui/components/command.d.ts +78 -0
  33. package/dist/host/types/src/ui/components/folder-style.d.ts +16 -0
  34. package/dist/host/types/src/ui/components/theme-provider.d.ts +2 -1
  35. package/dist/host/types/src/ui/components/version-badge.d.ts +7 -0
  36. package/dist/host/types/src/ui/hooks/use-page-visible-interval.d.ts +10 -0
  37. package/dist/host/types/src/ui/lib/activity-types.d.ts +15 -0
  38. package/dist/host/types/src/ui/lib/app-version.d.ts +2 -0
  39. package/dist/host/types/src/ui/lib/apply-ui-preset.d.ts +19 -0
  40. package/dist/host/types/src/ui/lib/create-id.d.ts +2 -0
  41. package/dist/host/types/src/ui/lib/database-transfer-url.d.ts +8 -0
  42. package/dist/host/types/src/ui/lib/desktop-updates.d.ts +29 -0
  43. package/dist/host/types/src/ui/lib/host-connection-tabs.d.ts +14 -0
  44. package/dist/host/types/src/ui/lib/host-to-ssh-host.d.ts +4 -0
  45. package/dist/host/types/src/ui/lib/issue-url.d.ts +21 -0
  46. package/dist/host/types/src/ui/lib/local-adaptive-preferences.d.ts +9 -0
  47. package/dist/host/types/src/ui/lib/pane-nav-hotkey.d.ts +7 -0
  48. package/dist/host/types/src/ui/lib/plugin-i18n.d.ts +4 -3
  49. package/dist/host/types/src/ui/lib/role-label.d.ts +3 -0
  50. package/dist/host/types/src/ui/look/LookSettings.d.ts +8 -0
  51. package/dist/host/types/src/ui/look/layout-registry.d.ts +34 -0
  52. package/dist/host/types/src/ui/look/look-store.d.ts +29 -0
  53. package/dist/host/types/src/ui/look/resolve-look.d.ts +18 -0
  54. package/dist/host/types/src/ui/look/theme-css.d.ts +7 -0
  55. package/dist/host/types/src/ui/look/theme-registry.d.ts +41 -0
  56. package/dist/host/types/src/ui/look/use-look-theme.d.ts +12 -0
  57. package/dist/host/types/src/ui/manage/manage-requests.d.ts +34 -0
  58. package/dist/host/types/src/ui/plugin-host/loader.d.ts +2 -0
  59. package/dist/host/types/src/ui/plugin-host/scope.d.ts +6 -0
  60. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +102 -0
  61. package/dist/host/types/src/ui/plugin-host/view-ownership.d.ts +1 -1
  62. package/dist/host/types/src/ui/plugins/PluginConsentPrompt.d.ts +20 -0
  63. package/dist/host/types/src/ui/plugins/PluginDetail.d.ts +7 -0
  64. package/dist/host/types/src/ui/plugins/PluginReleaseNotes.d.ts +14 -0
  65. package/dist/host/types/src/ui/plugins/PluginsTab.d.ts +1 -0
  66. package/dist/host/types/src/ui/plugins/RegistriesSection.d.ts +4 -0
  67. package/dist/host/types/src/ui/plugins/plugin-bits.d.ts +23 -0
  68. package/dist/host/types/src/ui/plugins/plugin-model.d.ts +116 -0
  69. package/dist/host/types/src/ui/plugins/use-plugins-manager.d.ts +75 -0
  70. package/dist/host/types/src/ui/settings/appearance/appearance-controls.d.ts +53 -0
  71. package/dist/host/types/src/ui/settings/sync/LinkServerDialog.d.ts +12 -0
  72. package/dist/host/types/src/ui/settings/sync/ProxySettingsFields.d.ts +15 -0
  73. package/dist/host/types/src/ui/settings/sync/saved-servers.d.ts +3 -0
  74. package/dist/host/types/src/ui/shell/controller/ShellControllerProvider.d.ts +20 -0
  75. package/dist/host/types/src/ui/shell/controller/plugin-shell.d.ts +7 -0
  76. package/dist/host/types/src/ui/shell/controller/shell-store.d.ts +4 -0
  77. package/dist/host/types/src/ui/shell/controller/types.d.ts +156 -0
  78. package/dist/host/types/src/ui/shell/controller/use-hosts.d.ts +10 -0
  79. package/dist/host/types/src/ui/shell/controller/use-regions.d.ts +5 -0
  80. package/dist/host/types/src/ui/shell/controller/use-shell-keybindings.d.ts +12 -0
  81. package/dist/host/types/src/ui/shell/controller/use-shell-lifecycle.d.ts +17 -0
  82. package/dist/host/types/src/ui/shell/controller/use-split.d.ts +21 -0
  83. package/dist/host/types/src/ui/shell/controller/use-tabs.d.ts +70 -0
  84. package/dist/host/types/src/ui/shell/controller/use-workspace-layout.d.ts +4 -0
  85. package/dist/host/types/src/ui/shell/reconnect-tabs.d.ts +6 -0
  86. package/dist/host/types/src/ui/shell/shell-layout.d.ts +46 -0
  87. package/dist/host/types/src/ui/shell/split/EmptyPanePicker.d.ts +32 -0
  88. package/dist/host/types/src/ui/shell/split/SplitDropOverlay.d.ts +9 -0
  89. package/dist/host/types/src/ui/shell/split/SplitLayoutMenu.d.ts +16 -0
  90. package/dist/host/types/src/ui/shell/split/SplitView.d.ts +31 -0
  91. package/dist/host/types/src/ui/shell/split/split-drag.d.ts +58 -0
  92. package/dist/host/types/src/ui/shell/split/split-presets.d.ts +28 -0
  93. package/dist/host/types/src/ui/shell/split/split-tabs.d.ts +95 -0
  94. package/dist/host/types/src/ui/shell/split/split-targets.d.ts +30 -0
  95. package/dist/host/types/src/ui/shell/split/split-tree.d.ts +89 -0
  96. package/dist/host/types/src/ui/shell/stage/TabStage.d.ts +9 -0
  97. package/dist/host/types/src/ui/shell/stage/tab-stage-store.d.ts +24 -0
  98. package/dist/host/types/src/ui/shell/tab-close-guards.d.ts +9 -0
  99. package/dist/host/types/src/ui/shell/tab-handles.d.ts +8 -0
  100. package/dist/host/types/src/ui/shell/tabUtils.d.ts +15 -0
  101. package/dist/host/types/src/ui/sidebar/AuthEnrollmentSections.d.ts +5 -0
  102. package/dist/host/types/src/ui/sidebar/HostManagerData.d.ts +4 -0
  103. package/dist/host/types/src/ui/sidebar/build-host-tree.d.ts +25 -0
  104. package/dist/host/types/src/ui/sidebar/host-export-payload.d.ts +30 -0
  105. package/dist/host/ui.js +6236 -4055
  106. package/dist/host/{use-permissions-GYCBY2AU.js → use-permissions-6VSHHUMB.js} +2 -2
  107. package/dist/manifest.d.ts +41 -1
  108. package/dist/manifest.js +89 -1
  109. package/dist/testing/setup.js +1 -0
  110. package/dist/theme-css.d.ts +11 -0
  111. package/dist/theme-css.js +142 -0
  112. package/dist/vitest-preset.d.ts +18 -0
  113. package/dist/vitest-preset.js +14 -3
  114. package/package.json +1 -1
  115. package/schema/manifest.schema.json +111 -8
  116. package/tsconfig.plugin.json +1 -0
  117. package/dist/host/chunk-YC2MAKSP.js +0 -14
@@ -7,8 +7,8 @@ import {
7
7
  resetPermissionsCache,
8
8
  setPermissionsForTesting,
9
9
  usePermissions
10
- } from "./chunk-LSWB3ZZV.js";
11
- import "./chunk-YC2MAKSP.js";
10
+ } from "./chunk-VEWOYAMZ.js";
11
+ import "./chunk-YGYBM7SQ.js";
12
12
  export {
13
13
  hasPermission,
14
14
  loadPermissionChecker,
@@ -10,6 +10,7 @@
10
10
  * made a manifest look like it declared something it did not.
11
11
  */
12
12
  import { type PluginEnvVar } from "./docs.js";
13
+ export { validateThemeCss } from "./theme-css.js";
13
14
  export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDocsUrl, pluginEnvVars, type PluginEnvVar, } from "./docs.js";
14
15
  /**
15
16
  * The plugin API this build implements. A minor bump adds to the API and
@@ -19,7 +20,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
19
20
  * "^1.2" needs 1.2 or later). engine.termix is checked against the core
20
21
  * version by the server.
21
22
  */
22
- export declare const PLUGIN_API_VERSION = "1.3.0";
23
+ export declare const PLUGIN_API_VERSION = "1.4.0";
23
24
  /** The API major, the default engine.api for a new plugin. */
24
25
  export declare const SUPPORTED_PLUGIN_API_VERSION: string;
25
26
  /** Whether this build's plugin API satisfies a manifest's engine.api. */
@@ -265,6 +266,41 @@ export interface PluginAuthContribution {
265
266
  /** Keyboard-interactive handler ids, e.g. "warpgate". */
266
267
  keyboardInteractive?: string[];
267
268
  }
269
+ /** A layout id or theme id: short, lowercase, used as a CSS class for themes. */
270
+ export declare const LOOK_ID_PATTERN: RegExp;
271
+ /**
272
+ * A whole-app layout the frontend registers with app.registerLayout. Needs
273
+ * the ui:layout capability. Declared here so core can list and choose it
274
+ * before any plugin code runs.
275
+ */
276
+ export interface PluginLayoutContribution {
277
+ id: string;
278
+ /** i18n key in the plugin's namespace, or plain text. */
279
+ label: string;
280
+ description?: string;
281
+ /** Theme picked with this layout when nobody chose one. */
282
+ defaultTheme?: string;
283
+ /** The layout a default-look plugin starts with. */
284
+ default?: boolean;
285
+ }
286
+ /**
287
+ * A theme: one CSS file of token rules scoped under `.<id>`, applied as a
288
+ * class on <html>. Needs the ui:theme capability. No code runs for a theme.
289
+ */
290
+ export interface PluginThemeContribution {
291
+ id: string;
292
+ /** i18n key in the plugin's namespace, or plain text. */
293
+ label: string;
294
+ mode: "light" | "dark";
295
+ /** Path of the CSS file inside the plugin. */
296
+ css: string;
297
+ /** Colors the theme picker previews it with. */
298
+ swatch?: {
299
+ background: string;
300
+ panel: string;
301
+ text: string;
302
+ };
303
+ }
268
304
  export interface PluginContributions {
269
305
  tabs?: PluginTabContribution[];
270
306
  panels?: PluginViewContribution[];
@@ -323,6 +359,10 @@ export interface PluginContributions {
323
359
  * ctx.credentials.resolveHostProtocol.
324
360
  */
325
361
  protocols?: PluginProtocolContribution[];
362
+ /** Whole-app layouts. Needs the ui:layout capability. */
363
+ layouts?: PluginLayoutContribution[];
364
+ /** Themes. Needs the ui:theme capability. */
365
+ themes?: PluginThemeContribution[];
326
366
  }
327
367
  /** A field a protocol login carries besides username and password. */
328
368
  export interface PluginProtocolCredentialField {
package/dist/manifest.js CHANGED
@@ -12,6 +12,7 @@
12
12
  import semver from "semver";
13
13
  import { isKnownCapability } from "./capabilities.js";
14
14
  import { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsUrl, } from "./docs.js";
15
+ export { validateThemeCss } from "./theme-css.js";
15
16
  export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDocsUrl, pluginEnvVars, } from "./docs.js";
16
17
  /**
17
18
  * The plugin API this build implements. A minor bump adds to the API and
@@ -21,7 +22,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
21
22
  * "^1.2" needs 1.2 or later). engine.termix is checked against the core
22
23
  * version by the server.
23
24
  */
24
- export const PLUGIN_API_VERSION = "1.3.0";
25
+ export const PLUGIN_API_VERSION = "1.4.0";
25
26
  /** The API major, the default engine.api for a new plugin. */
26
27
  export const SUPPORTED_PLUGIN_API_VERSION = String(semver.major(PLUGIN_API_VERSION));
27
28
  /** Whether this build's plugin API satisfies a manifest's engine.api. */
@@ -168,6 +169,8 @@ export const SETTINGS_FIELD_TYPES = [
168
169
  "custom",
169
170
  ];
170
171
  export const SETTINGS_SCOPES = ["admin", "user", "host"];
172
+ /** A layout id or theme id: short, lowercase, used as a CSS class for themes. */
173
+ export const LOOK_ID_PATTERN = /^[a-z][a-z0-9-]{1,39}$/;
171
174
  /** Protocol ids core keeps for itself. */
172
175
  export const RESERVED_PROTOCOL_IDS = ["ssh"];
173
176
  /** Login keys a declared credential field may not reuse. */
@@ -243,6 +246,8 @@ const ALLOWED_CONTRIBUTES = new Set([
243
246
  "uses",
244
247
  "keybindingActions",
245
248
  "protocols",
249
+ "layouts",
250
+ "themes",
246
251
  ]);
247
252
  const ALLOWED_SETTINGS_FIELD = [
248
253
  "key",
@@ -734,6 +739,81 @@ function validateContributes(contributes, pluginId, errors) {
734
739
  validateUiPresets(contributes.uiPresets, errors);
735
740
  validateKeybindingActions(contributes.keybindingActions, errors);
736
741
  validateProtocols(contributes.protocols, errors);
742
+ validateLayouts(contributes.layouts, errors);
743
+ validateThemes(contributes.themes, errors);
744
+ }
745
+ /** An array of objects with unique look ids; calls `each` for every valid entry. */
746
+ function validateLookList(value, field, allowed, errors, each) {
747
+ if (value === undefined)
748
+ return;
749
+ if (!Array.isArray(value)) {
750
+ errors.push(`Field "contributes.${field}" must be an array`);
751
+ return;
752
+ }
753
+ const seen = new Set();
754
+ value.forEach((raw, index) => {
755
+ const where = `contributes.${field}[${index}]`;
756
+ if (!isPlainObject(raw)) {
757
+ errors.push(`${where} must be an object`);
758
+ return;
759
+ }
760
+ rejectUnknown(raw, allowed, where, errors);
761
+ if (typeof raw.id !== "string" || !LOOK_ID_PATTERN.test(raw.id)) {
762
+ errors.push(`${where}.id must match ${LOOK_ID_PATTERN} (lowercase letters, digits, dashes)`);
763
+ }
764
+ else {
765
+ if (seen.has(raw.id))
766
+ errors.push(`${where}.id duplicates "${raw.id}"`);
767
+ seen.add(raw.id);
768
+ }
769
+ requireString(raw.label, `${where}.label`, errors);
770
+ each(raw, where);
771
+ });
772
+ }
773
+ function validateLayouts(value, errors) {
774
+ validateLookList(value, "layouts", ["id", "label", "description", "defaultTheme", "default"], errors, (raw, where) => {
775
+ if (raw.description !== undefined)
776
+ requireString(raw.description, `${where}.description`, errors);
777
+ if (raw.defaultTheme !== undefined &&
778
+ (typeof raw.defaultTheme !== "string" ||
779
+ !LOOK_ID_PATTERN.test(raw.defaultTheme))) {
780
+ errors.push(`${where}.defaultTheme must be a theme id`);
781
+ }
782
+ if (raw.default !== undefined && typeof raw.default !== "boolean") {
783
+ errors.push(`${where}.default must be a boolean`);
784
+ }
785
+ });
786
+ if (Array.isArray(value) &&
787
+ value.filter((raw) => isPlainObject(raw) && raw.default === true).length > 1) {
788
+ errors.push('Only one entry in "contributes.layouts" may set default');
789
+ }
790
+ }
791
+ const HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;
792
+ function validateThemes(value, errors) {
793
+ validateLookList(value, "themes", ["id", "label", "mode", "css", "swatch"], errors, (raw, where) => {
794
+ if (raw.mode !== "light" && raw.mode !== "dark") {
795
+ errors.push(`${where}.mode must be "light" or "dark"`);
796
+ }
797
+ if (requireString(raw.css, `${where}.css`, errors)) {
798
+ requireRelativePath(raw.css, `${where}.css`, errors);
799
+ if (!raw.css.endsWith(".css")) {
800
+ errors.push(`${where}.css must be a .css file`);
801
+ }
802
+ }
803
+ if (raw.swatch !== undefined) {
804
+ if (!isPlainObject(raw.swatch)) {
805
+ errors.push(`${where}.swatch must be an object`);
806
+ return;
807
+ }
808
+ rejectUnknown(raw.swatch, ["background", "panel", "text"], `${where}.swatch`, errors);
809
+ for (const key of ["background", "panel", "text"]) {
810
+ const color = raw.swatch[key];
811
+ if (typeof color !== "string" || !HEX_COLOR.test(color)) {
812
+ errors.push(`${where}.swatch.${key} must be a hex color`);
813
+ }
814
+ }
815
+ }
816
+ });
737
817
  }
738
818
  export const PROTOCOL_ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
739
819
  export const PROTOCOL_FIELD_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9]{0,31}$/;
@@ -1595,6 +1675,14 @@ export function parseManifest(raw) {
1595
1675
  if (contributesAuth && !manifest.capabilities.includes("auth:provide")) {
1596
1676
  errors.push('contributes.auth needs the "auth:provide" capability in the capabilities array');
1597
1677
  }
1678
+ if ((manifest.contributes?.layouts?.length ?? 0) > 0 &&
1679
+ !manifest.capabilities.includes("ui:layout")) {
1680
+ errors.push('contributes.layouts needs the "ui:layout" capability in the capabilities array');
1681
+ }
1682
+ if ((manifest.contributes?.themes?.length ?? 0) > 0 &&
1683
+ !manifest.capabilities.includes("ui:theme")) {
1684
+ errors.push('contributes.themes needs the "ui:theme" capability in the capabilities array');
1685
+ }
1598
1686
  for (const secret of manifest.requiresSecret ?? []) {
1599
1687
  if (secret.plugin === manifest.id) {
1600
1688
  errors.push(`requiresSecret["${secret.key}"] points at this plugin; use ctx.secrets.get instead`);
@@ -89,6 +89,7 @@ globalThis.__termixTestPluginHost = {
89
89
  useHostStatus: () => null,
90
90
  useCurrentUser: () => null,
91
91
  useTheme: () => ({ theme: "dark" }),
92
+ useShell: () => null,
92
93
  toast: { success: noop, error: noop, info: noop, warning: noop },
93
94
  getApi: () => ({
94
95
  get: offline,
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Checks a theme's CSS before core serves it.
3
+ *
4
+ * A theme is colors and styles only, so its file may hold nothing but rules
5
+ * whose every selector starts with the theme's own class (`.nord`,
6
+ * `:root.nord`, `html.nord ...`), optionally inside @media or @supports.
7
+ * Anything that could load from elsewhere or run code is refused: @import,
8
+ * url() to another host, other at-rules, expression() and javascript:.
9
+ */
10
+ /** Every problem with a theme's CSS, empty when it is safe to serve. */
11
+ export declare function validateThemeCss(css: string, themeId: string): string[];
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Checks a theme's CSS before core serves it.
3
+ *
4
+ * A theme is colors and styles only, so its file may hold nothing but rules
5
+ * whose every selector starts with the theme's own class (`.nord`,
6
+ * `:root.nord`, `html.nord ...`), optionally inside @media or @supports.
7
+ * Anything that could load from elsewhere or run code is refused: @import,
8
+ * url() to another host, other at-rules, expression() and javascript:.
9
+ */
10
+ const ALLOWED_BLOCK_AT_RULES = new Set(["media", "supports"]);
11
+ function stripComments(css) {
12
+ return css.replace(/\/\*[\s\S]*?\*\//g, " ");
13
+ }
14
+ /** Splits on commas that are not inside parentheses or brackets. */
15
+ function splitSelectors(prelude) {
16
+ const parts = [];
17
+ let depth = 0;
18
+ let current = "";
19
+ for (const ch of prelude) {
20
+ if (ch === "(" || ch === "[")
21
+ depth += 1;
22
+ if (ch === ")" || ch === "]")
23
+ depth -= 1;
24
+ if (ch === "," && depth === 0) {
25
+ parts.push(current);
26
+ current = "";
27
+ continue;
28
+ }
29
+ current += ch;
30
+ }
31
+ parts.push(current);
32
+ return parts.map((part) => part.trim());
33
+ }
34
+ function withoutStrings(css) {
35
+ return css.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""');
36
+ }
37
+ function escapeRegExp(value) {
38
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
39
+ }
40
+ /** Index of the brace closing the one at `open`, skipping strings. */
41
+ function matchingBrace(css, open) {
42
+ let depth = 0;
43
+ let quote = null;
44
+ for (let i = open; i < css.length; i += 1) {
45
+ const ch = css[i];
46
+ if (quote) {
47
+ if (ch === "\\")
48
+ i += 1;
49
+ else if (ch === quote)
50
+ quote = null;
51
+ continue;
52
+ }
53
+ if (ch === '"' || ch === "'")
54
+ quote = ch;
55
+ else if (ch === "{")
56
+ depth += 1;
57
+ else if (ch === "}") {
58
+ depth -= 1;
59
+ if (depth === 0)
60
+ return i;
61
+ }
62
+ }
63
+ return -1;
64
+ }
65
+ function checkUrls(css, errors) {
66
+ const urls = css.matchAll(/url\(\s*(['"]?)([^'")]*)\1\s*\)/gi);
67
+ for (const match of urls) {
68
+ const target = match[2].trim();
69
+ if (target.startsWith("data:"))
70
+ continue;
71
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//")) {
72
+ errors.push(`url(${target}) points outside the plugin`);
73
+ }
74
+ }
75
+ }
76
+ function checkRules(css, scope, themeId, errors) {
77
+ let index = 0;
78
+ while (index < css.length) {
79
+ const rest = css.slice(index);
80
+ if (rest.trim() === "")
81
+ return;
82
+ const open = css.indexOf("{", index);
83
+ const semicolon = css.indexOf(";", index);
84
+ if (semicolon !== -1 && (open === -1 || semicolon < open)) {
85
+ const statement = css.slice(index, semicolon).trim();
86
+ errors.push(statement.startsWith("@")
87
+ ? `${statement.split(/\s/)[0]} is not allowed in a theme`
88
+ : `Unexpected "${statement}" outside a rule`);
89
+ index = semicolon + 1;
90
+ continue;
91
+ }
92
+ if (open === -1) {
93
+ errors.push(`Unexpected "${rest.trim()}" at the end`);
94
+ return;
95
+ }
96
+ const close = matchingBrace(css, open);
97
+ if (close === -1) {
98
+ errors.push("A rule is missing its closing brace");
99
+ return;
100
+ }
101
+ const prelude = css.slice(index, open).trim();
102
+ const body = css.slice(open + 1, close);
103
+ if (prelude.startsWith("@")) {
104
+ const name = prelude.slice(1).split(/[\s(]/)[0].toLowerCase();
105
+ if (ALLOWED_BLOCK_AT_RULES.has(name)) {
106
+ checkRules(body, scope, themeId, errors);
107
+ }
108
+ else {
109
+ errors.push(`@${name} is not allowed in a theme`);
110
+ }
111
+ }
112
+ else {
113
+ for (const selector of splitSelectors(prelude)) {
114
+ if (!scope.test(selector)) {
115
+ errors.push(`Selector "${selector}" must start with .${themeId}, :root.${themeId} or html.${themeId}`);
116
+ }
117
+ }
118
+ if (withoutStrings(body).includes("{")) {
119
+ errors.push(`Nested rules are not allowed in "${prelude}"`);
120
+ }
121
+ }
122
+ index = close + 1;
123
+ }
124
+ }
125
+ /** Every problem with a theme's CSS, empty when it is safe to serve. */
126
+ export function validateThemeCss(css, themeId) {
127
+ const errors = [];
128
+ const clean = stripComments(css);
129
+ if (/@import\b/i.test(clean))
130
+ errors.push("@import is not allowed in a theme");
131
+ if (/expression\s*\(/i.test(clean))
132
+ errors.push("expression() is not allowed in a theme");
133
+ if (/javascript:/i.test(clean))
134
+ errors.push("javascript: is not allowed in a theme");
135
+ if (/-moz-binding|behavior\s*:/i.test(clean))
136
+ errors.push("Script bindings are not allowed in a theme");
137
+ checkUrls(clean, errors);
138
+ const id = escapeRegExp(themeId);
139
+ const scope = new RegExp(`^(?::root|html)?\\.${id}(?![\\w-])`);
140
+ checkRules(clean, scope, themeId, errors);
141
+ return [...new Set(errors)];
142
+ }
@@ -27,8 +27,14 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
27
27
  enforce: "pre";
28
28
  load(id: string): string;
29
29
  }[];
30
+ server: {
31
+ fs: {
32
+ allow: string[];
33
+ };
34
+ };
30
35
  resolve: {
31
36
  alias: Record<string, string>;
37
+ dedupe: string[];
32
38
  };
33
39
  test: {
34
40
  globals: boolean;
@@ -40,8 +46,14 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
40
46
  enforce: "pre";
41
47
  load(id: string): string;
42
48
  }[];
49
+ server: {
50
+ fs: {
51
+ allow: string[];
52
+ };
53
+ };
43
54
  resolve: {
44
55
  alias: Record<string, string>;
56
+ dedupe: string[];
45
57
  };
46
58
  test: {
47
59
  name: string;
@@ -59,8 +71,14 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
59
71
  enforce: "pre";
60
72
  load(id: string): string;
61
73
  }[];
74
+ server: {
75
+ fs: {
76
+ allow: string[];
77
+ };
78
+ };
62
79
  resolve: {
63
80
  alias: Record<string, string>;
81
+ dedupe: string[];
64
82
  };
65
83
  test: {
66
84
  name: string;
@@ -80,9 +80,18 @@ export function pluginVitestConfig(configUrl, options = {}) {
80
80
  // unexpectedly". Threads don't.
81
81
  const pool = "threads";
82
82
  const plugins = [wasmBinaryPlugin()];
83
+ // A local SDK linked into node_modules sits outside the plugin folder, so
84
+ // Vite must be allowed to read it, and React must come from the plugin.
85
+ const sdkDir = fs.realpathSync(fileURLToPath(new URL("..", import.meta.url)));
86
+ const server = { fs: { allow: [pluginDir, sdkDir] } };
87
+ const resolve = {
88
+ alias,
89
+ dedupe: ["react", "react-dom", "react-i18next", "i18next"],
90
+ };
83
91
  return {
84
92
  plugins,
85
- resolve: { alias },
93
+ server,
94
+ resolve,
86
95
  test: {
87
96
  globals: true,
88
97
  setupFiles: [setupFile],
@@ -90,7 +99,8 @@ export function pluginVitestConfig(configUrl, options = {}) {
90
99
  projects: [
91
100
  {
92
101
  plugins,
93
- resolve: { alias },
102
+ server,
103
+ resolve,
94
104
  test: {
95
105
  name: `${pluginId}:backend`,
96
106
  root: pluginDir,
@@ -103,7 +113,8 @@ export function pluginVitestConfig(configUrl, options = {}) {
103
113
  },
104
114
  {
105
115
  plugins,
106
- resolve: { alias },
116
+ server,
117
+ resolve,
107
118
  test: {
108
119
  name: `${pluginId}:frontend`,
109
120
  root: pluginDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termix-ssh/plugin-sdk",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -82,7 +82,11 @@
82
82
  "features": {
83
83
  "type": "array",
84
84
  "maxItems": 20,
85
- "items": { "type": "string", "minLength": 1, "maxLength": 160 },
85
+ "items": {
86
+ "type": "string",
87
+ "minLength": 1,
88
+ "maxLength": 160
89
+ },
86
90
  "description": "Short lines listing what the plugin does, shown on its page."
87
91
  },
88
92
  "docs": {
@@ -98,11 +102,24 @@
98
102
  "additionalProperties": false,
99
103
  "required": ["name", "description"],
100
104
  "properties": {
101
- "name": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]{0,63}$" },
102
- "description": { "type": "string", "minLength": 1, "maxLength": 300 },
103
- "default": { "type": "string" },
104
- "required": { "type": "boolean" },
105
- "secret": { "type": "boolean" }
105
+ "name": {
106
+ "type": "string",
107
+ "pattern": "^[A-Z][A-Z0-9_]{0,63}$"
108
+ },
109
+ "description": {
110
+ "type": "string",
111
+ "minLength": 1,
112
+ "maxLength": 300
113
+ },
114
+ "default": {
115
+ "type": "string"
116
+ },
117
+ "required": {
118
+ "type": "boolean"
119
+ },
120
+ "secret": {
121
+ "type": "boolean"
122
+ }
106
123
  }
107
124
  }
108
125
  },
@@ -155,7 +172,11 @@
155
172
  "files:own",
156
173
  "secrets:own",
157
174
  "settings:read-core",
158
- "ui:surface"
175
+ "ui:surface",
176
+ "notify:hub",
177
+ "plugins:read",
178
+ "ui:layout",
179
+ "ui:theme"
159
180
  ]
160
181
  }
161
182
  },
@@ -944,7 +965,9 @@
944
965
  "defaultLevels": {
945
966
  "type": "array",
946
967
  "minItems": 1,
947
- "items": { "enum": ["admin", "user", "folder"] },
968
+ "items": {
969
+ "enum": ["admin", "user", "folder"]
970
+ },
948
971
  "description": "Levels that may set a default for the field. All three by default."
949
972
  },
950
973
  "personal": {
@@ -1127,6 +1150,86 @@
1127
1150
  "type": "string",
1128
1151
  "pattern": "^[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)+$"
1129
1152
  }
1153
+ },
1154
+ "layouts": {
1155
+ "type": "array",
1156
+ "description": "Whole-app layouts the frontend registers with app.registerLayout. Needs the ui:layout capability.",
1157
+ "items": {
1158
+ "type": "object",
1159
+ "additionalProperties": false,
1160
+ "required": ["id", "label"],
1161
+ "properties": {
1162
+ "id": {
1163
+ "type": "string",
1164
+ "pattern": "^[a-z][a-z0-9-]{1,39}$"
1165
+ },
1166
+ "label": {
1167
+ "type": "string",
1168
+ "minLength": 1,
1169
+ "description": "i18n key in the plugin namespace, or plain text."
1170
+ },
1171
+ "description": {
1172
+ "type": "string",
1173
+ "minLength": 1
1174
+ },
1175
+ "defaultTheme": {
1176
+ "type": "string",
1177
+ "pattern": "^[a-z][a-z0-9-]{1,39}$",
1178
+ "description": "Theme picked with this layout when nobody chose one."
1179
+ },
1180
+ "default": {
1181
+ "type": "boolean",
1182
+ "description": "The layout a default-look plugin starts with. At most one."
1183
+ }
1184
+ }
1185
+ }
1186
+ },
1187
+ "themes": {
1188
+ "type": "array",
1189
+ "description": "Themes: one CSS file each with token rules scoped under .<id>. Needs the ui:theme capability.",
1190
+ "items": {
1191
+ "type": "object",
1192
+ "additionalProperties": false,
1193
+ "required": ["id", "label", "mode", "css"],
1194
+ "properties": {
1195
+ "id": {
1196
+ "type": "string",
1197
+ "pattern": "^[a-z][a-z0-9-]{1,39}$"
1198
+ },
1199
+ "label": {
1200
+ "type": "string",
1201
+ "minLength": 1
1202
+ },
1203
+ "mode": {
1204
+ "type": "string",
1205
+ "enum": ["light", "dark"]
1206
+ },
1207
+ "css": {
1208
+ "type": "string",
1209
+ "pattern": ".css$",
1210
+ "description": "Path of the CSS file inside the plugin."
1211
+ },
1212
+ "swatch": {
1213
+ "type": "object",
1214
+ "additionalProperties": false,
1215
+ "required": ["background", "panel", "text"],
1216
+ "properties": {
1217
+ "background": {
1218
+ "type": "string",
1219
+ "pattern": "^#[0-9a-fA-F]{3,8}$"
1220
+ },
1221
+ "panel": {
1222
+ "type": "string",
1223
+ "pattern": "^#[0-9a-fA-F]{3,8}$"
1224
+ },
1225
+ "text": {
1226
+ "type": "string",
1227
+ "pattern": "^#[0-9a-fA-F]{3,8}$"
1228
+ }
1229
+ }
1230
+ }
1231
+ }
1232
+ }
1130
1233
  }
1131
1234
  }
1132
1235
  },
@@ -12,6 +12,7 @@
12
12
  "moduleDetection": "force",
13
13
  "esModuleInterop": true,
14
14
  "resolveJsonModule": true,
15
+ "preserveSymlinks": true,
15
16
  "noEmit": true,
16
17
  "strict": false,
17
18
  "noUnusedLocals": false,
@@ -1,14 +0,0 @@
1
- const __termixNoGlob = () => ({});
2
- var __defProp = Object.defineProperty;
3
- var __export = (target, all) => {
4
- for (var name in all)
5
- __defProp(target, name, { get: all[name], enumerable: true });
6
- };
7
-
8
- // <define:import.meta.env>
9
- var define_import_meta_env_default = { DEV: false, PROD: true, MODE: "production", SSR: false, BASE_URL: "/" };
10
-
11
- export {
12
- __export,
13
- define_import_meta_env_default
14
- };