@termix-ssh/plugin-sdk 1.0.0 → 1.0.1

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 (48) hide show
  1. package/README.md +7 -0
  2. package/cli/commands/changelog.mjs +25 -0
  3. package/cli/commands/dev.mjs +158 -0
  4. package/cli/commands/validate.mjs +16 -24
  5. package/cli/index.mjs +8 -0
  6. package/cli/lib/tmxplug.mjs +1 -1
  7. package/dist/backend.d.ts +6 -2
  8. package/dist/changelog.d.ts +67 -0
  9. package/dist/changelog.js +195 -0
  10. package/dist/frontend.d.ts +67 -1
  11. package/dist/host/{chunk-XGWE7BTJ.js → chunk-3KG44G55.js} +6 -1
  12. package/dist/host/{chunk-XH5NXL6Y.js → chunk-7GL25GFS.js} +8013 -7606
  13. package/dist/host/{chunk-F3SDZDDG.js → chunk-COGRSHKO.js} +17 -2
  14. package/dist/host/{linked-server-FZGWQW32.js → linked-server-BD573BEG.js} +1 -1
  15. package/dist/host/{rbac-api-6KDMBVDZ.js → rbac-api-7GHDJLU4.js} +1 -1
  16. package/dist/host/testing-host.js +38 -5
  17. package/dist/host/types/src/types/credential-sidebar-preferences.d.ts +12 -1
  18. package/dist/host/types/src/types/host-sidebar-preferences.d.ts +18 -1
  19. package/dist/host/types/src/types/onboarding.d.ts +29 -0
  20. package/dist/host/types/src/types/plugin-onboarding.d.ts +39 -0
  21. package/dist/host/types/src/types/ui-preferences.d.ts +13 -4
  22. package/dist/host/types/src/types/ui-types.d.ts +1 -1
  23. package/dist/host/types/src/ui/api/plugins-api.d.ts +194 -0
  24. package/dist/host/types/src/ui/components/connection/ConnectionLogPanel.d.ts +2 -1
  25. package/dist/host/types/src/ui/components/connection/ConnectionScreen.d.ts +5 -5
  26. package/dist/host/types/src/ui/components/list-kit.d.ts +102 -0
  27. package/dist/host/types/src/ui/components/panel-layout.d.ts +6 -1
  28. package/dist/host/types/src/ui/components/surface/surface-scope.d.ts +5 -1
  29. package/dist/host/types/src/ui/contexts/UiPreferencesContext.d.ts +15 -2
  30. package/dist/host/types/src/ui/hooks/use-permissions.d.ts +2 -0
  31. package/dist/host/types/src/ui/lib/embedded-frame.d.ts +14 -0
  32. package/dist/host/types/src/ui/onboarding/onboarding-registry.d.ts +49 -0
  33. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +1 -0
  34. package/dist/host/types/src/ui/plugins/open-plugins.d.ts +5 -0
  35. package/dist/host/types/src/ui/sidebar/host-contributions.d.ts +2 -0
  36. package/dist/host/types/src/ui/sidebar/rail-items.d.ts +10 -0
  37. package/dist/host/ui.js +982 -698
  38. package/dist/host/{use-permissions-NULZL43J.js → use-permissions-SKWXHWDJ.js} +3 -1
  39. package/dist/manifest.d.ts +10 -2
  40. package/dist/manifest.js +46 -1
  41. package/dist/testing/setup.js +8 -0
  42. package/dist/testing.d.ts +12 -0
  43. package/dist/vitest-preset.d.ts +3 -0
  44. package/dist/vitest-preset.js +6 -0
  45. package/package.json +5 -2
  46. package/schema/manifest.schema.json +6 -1
  47. package/cli/lib/changelog.mjs +0 -126
  48. package/schema/changelog.schema.json +0 -67
@@ -1,15 +1,17 @@
1
1
  const __termixNoGlob = () => ({});
2
2
  import {
3
3
  hasPermission,
4
+ loadPermissionChecker,
4
5
  notifyPermissionsChanged,
5
6
  preloadPermissions,
6
7
  resetPermissionsCache,
7
8
  setPermissionsForTesting,
8
9
  usePermissions
9
- } from "./chunk-XGWE7BTJ.js";
10
+ } from "./chunk-3KG44G55.js";
10
11
  import "./chunk-YC2MAKSP.js";
11
12
  export {
12
13
  hasPermission,
14
+ loadPermissionChecker,
13
15
  notifyPermissionsChanged,
14
16
  preloadPermissions,
15
17
  resetPermissionsCache,
@@ -17,7 +17,7 @@
17
17
  * "^1.2" needs 1.2 or later). engine.termix is checked against the core
18
18
  * version by the server.
19
19
  */
20
- export declare const PLUGIN_API_VERSION = "1.0.0";
20
+ export declare const PLUGIN_API_VERSION = "1.1.0";
21
21
  /** The API major, the default engine.api for a new plugin. */
22
22
  export declare const SUPPORTED_PLUGIN_API_VERSION: string;
23
23
  /** Whether this build's plugin API satisfies a manifest's engine.api. */
@@ -27,6 +27,11 @@ export declare function isApiCompatible(range: string): boolean;
27
27
  * core (2.9.0-beta.1) counts as that release.
28
28
  */
29
29
  export declare function isTermixCompatible(range: string, coreVersion: string | null): boolean;
30
+ /**
31
+ * The video id of a YouTube link, or null for anything else. Only the id is
32
+ * ever used, so a manifest can never point the embed at another site.
33
+ */
34
+ export declare function youtubeVideoId(value: unknown): string | null;
30
35
  export declare const PLUGIN_CATEGORIES: readonly ["Terminal", "Files & Transfer", "Infrastructure", "Monitoring", "Networking", "Access & Security", "Productivity"];
31
36
  export declare const PLUGIN_PLATFORMS: readonly ["linux", "win32", "darwin"];
32
37
  export declare const ACTION_CONTRIBUTION_KINDS: readonly ["button", "component"];
@@ -327,7 +332,8 @@ export interface PluginProtocolContribution {
327
332
  defaultPort?: number;
328
333
  /**
329
334
  * What the owner's login falls back to from the host's own SSH login
330
- * when the protocol login leaves it empty.
335
+ * when the protocol login leaves it empty. The password only falls back
336
+ * while the host signs in to SSH with a password.
331
337
  */
332
338
  hostLoginFallback?: Array<"username" | "password">;
333
339
  }
@@ -408,6 +414,8 @@ export interface PluginManifest {
408
414
  repository?: string;
409
415
  category: string;
410
416
  icon?: string;
417
+ /** A YouTube link shown at the top of the plugin's page. */
418
+ video?: string;
411
419
  engine: PluginEngine;
412
420
  /** Catalog capability ids. */
413
421
  capabilities: string[];
package/dist/manifest.js CHANGED
@@ -19,7 +19,7 @@ import { isKnownCapability } from "./capabilities.js";
19
19
  * "^1.2" needs 1.2 or later). engine.termix is checked against the core
20
20
  * version by the server.
21
21
  */
22
- export const PLUGIN_API_VERSION = "1.0.0";
22
+ export const PLUGIN_API_VERSION = "1.1.0";
23
23
  /** The API major, the default engine.api for a new plugin. */
24
24
  export const SUPPORTED_PLUGIN_API_VERSION = String(semver.major(PLUGIN_API_VERSION));
25
25
  /** Whether this build's plugin API satisfies a manifest's engine.api. */
@@ -38,6 +38,46 @@ export function isTermixCompatible(range, coreVersion) {
38
38
  return false;
39
39
  return semver.satisfies(semver.coerce(coreVersion).version, range);
40
40
  }
41
+ const YOUTUBE_ID = /^[A-Za-z0-9_-]{11}$/;
42
+ const YOUTUBE_HOSTS = new Set([
43
+ "youtube.com",
44
+ "www.youtube.com",
45
+ "m.youtube.com",
46
+ "youtu.be",
47
+ "youtube-nocookie.com",
48
+ "www.youtube-nocookie.com",
49
+ ]);
50
+ /**
51
+ * The video id of a YouTube link, or null for anything else. Only the id is
52
+ * ever used, so a manifest can never point the embed at another site.
53
+ */
54
+ export function youtubeVideoId(value) {
55
+ if (typeof value !== "string")
56
+ return null;
57
+ let url;
58
+ try {
59
+ url = new URL(value.trim());
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ if (url.protocol !== "https:" || url.username || url.password)
65
+ return null;
66
+ const host = url.hostname.toLowerCase();
67
+ if (!YOUTUBE_HOSTS.has(host))
68
+ return null;
69
+ let id;
70
+ if (host === "youtu.be") {
71
+ id = url.pathname.slice(1);
72
+ }
73
+ else if (url.pathname === "/watch") {
74
+ id = url.searchParams.get("v");
75
+ }
76
+ else {
77
+ id = /^\/(?:embed|shorts|live)\/([^/]+)\/?$/.exec(url.pathname)?.[1];
78
+ }
79
+ return id && YOUTUBE_ID.test(id) ? id : null;
80
+ }
41
81
  export const PLUGIN_CATEGORIES = [
42
82
  "Terminal",
43
83
  "Files & Transfer",
@@ -64,6 +104,7 @@ export const RESERVED_PLUGIN_IDS = [
64
104
  "termix",
65
105
  "plugin",
66
106
  "plugins",
107
+ "onboarding",
67
108
  "users",
68
109
  "system",
69
110
  "sdk",
@@ -148,6 +189,7 @@ const ALLOWED_TOP_LEVEL = new Set([
148
189
  ...REQUIRED_TOP_LEVEL,
149
190
  "repository",
150
191
  "icon",
192
+ "video",
151
193
  "dependencies",
152
194
  "optionalDependencies",
153
195
  "provides",
@@ -274,6 +316,9 @@ export function validateManifest(manifest) {
274
316
  requireString(m.repository, "repository", errors);
275
317
  if ("icon" in m)
276
318
  requireString(m.icon, "icon", errors);
319
+ if ("video" in m && youtubeVideoId(m.video) === null) {
320
+ errors.push(`Field "video" must be a YouTube link such as https://www.youtube.com/watch?v=..., got: ${JSON.stringify(m.video)}`);
321
+ }
277
322
  for (const field of ["backend", "frontend", "locales"]) {
278
323
  if (field in m && requireString(m[field], field, errors)) {
279
324
  requireRelativePath(m[field], field, errors);
@@ -7,6 +7,14 @@
7
7
  import { afterEach, vi } from "vitest";
8
8
  import { installSafePorts } from "./safe-ports.js";
9
9
  installSafePorts();
10
+ // Radix measures some controls (Checkbox, Switch) and jsdom has no observer.
11
+ if (typeof globalThis.ResizeObserver === "undefined") {
12
+ globalThis.ResizeObserver = class {
13
+ observe() { }
14
+ unobserve() { }
15
+ disconnect() { }
16
+ };
17
+ }
10
18
  if (typeof window !== "undefined" && !window.matchMedia) {
11
19
  window.matchMedia = vi.fn().mockImplementation((query) => ({
12
20
  matches: false,
package/dist/testing.d.ts CHANGED
@@ -492,6 +492,8 @@ export interface RenderedPluginApp {
492
492
  paletteGroups: () => string[];
493
493
  keybindingActions: () => string[];
494
494
  keybindingDefaults: () => string[];
495
+ /** Onboarding step ids, without the plugin prefix. */
496
+ onboardingSteps: () => string[];
495
497
  };
496
498
  /** A registered palette group's items, as the palette loads them on open. */
497
499
  loadPaletteGroup: (id: string) => Promise<import("./frontend.js").PaletteItem[]>;
@@ -515,6 +517,16 @@ export interface RenderedPluginApp {
515
517
  renderTab: (type: string, props?: Record<string, unknown>) => HTMLElement;
516
518
  renderPanel: (id: string, props?: Record<string, unknown>) => HTMLElement;
517
519
  renderDashboardCard: (id: string) => HTMLElement;
520
+ /**
521
+ * Renders an onboarding step with OnboardingStepProps for a first run,
522
+ * overridable. Read what the step set with the returned `canContinue()`
523
+ * and `next()` (runs its beforeNext, true when it has none).
524
+ */
525
+ renderOnboardingStep: (id: string, props?: Record<string, unknown>) => {
526
+ element: HTMLElement;
527
+ canContinue: () => boolean;
528
+ next: () => Promise<boolean>;
529
+ };
518
530
  /** Renders one component of a contributed extension, "view" by default. */
519
531
  renderExtension: (pointId: string, id: string, component?: string, props?: Record<string, unknown>) => HTMLElement;
520
532
  renderHostEditorSection: (id: string, props?: Record<string, unknown>) => HTMLElement;
@@ -19,6 +19,7 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
19
19
  test: {
20
20
  globals: boolean;
21
21
  setupFiles: string[];
22
+ pool: "threads";
22
23
  projects: ({
23
24
  resolve: {
24
25
  alias: Record<string, string>;
@@ -27,6 +28,7 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
27
28
  name: string;
28
29
  root: string;
29
30
  environment: string;
31
+ pool: "threads";
30
32
  globals: boolean;
31
33
  setupFiles: string[];
32
34
  include: string[];
@@ -40,6 +42,7 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
40
42
  name: string;
41
43
  root: string;
42
44
  environment: string;
45
+ pool: "threads";
43
46
  globals: boolean;
44
47
  setupFiles: string[];
45
48
  server: {
@@ -58,11 +58,15 @@ export function pluginVitestConfig(configUrl, options = {}) {
58
58
  }),
59
59
  ...options.alias,
60
60
  };
61
+ // Forks workers on Windows abort now and then with "Worker exited
62
+ // unexpectedly". Threads don't.
63
+ const pool = "threads";
61
64
  return {
62
65
  resolve: { alias },
63
66
  test: {
64
67
  globals: true,
65
68
  setupFiles: [setupFile],
69
+ pool,
66
70
  projects: [
67
71
  {
68
72
  resolve: { alias },
@@ -70,6 +74,7 @@ export function pluginVitestConfig(configUrl, options = {}) {
70
74
  name: `${pluginId}:backend`,
71
75
  root: pluginDir,
72
76
  environment: "node",
77
+ pool,
73
78
  globals: true,
74
79
  setupFiles: [setupFile],
75
80
  include: ["tests/backend/**/*.test.ts"],
@@ -81,6 +86,7 @@ export function pluginVitestConfig(configUrl, options = {}) {
81
86
  name: `${pluginId}:frontend`,
82
87
  root: pluginDir,
83
88
  environment: "jsdom",
89
+ pool,
84
90
  globals: true,
85
91
  setupFiles: [setupFile],
86
92
  // Run the SDK through Vite rather than plain Node, so its import
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termix-ssh/plugin-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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": {
@@ -49,6 +49,10 @@
49
49
  "types": "./dist/capabilities.d.ts",
50
50
  "default": "./dist/capabilities.js"
51
51
  },
52
+ "./changelog": {
53
+ "types": "./dist/changelog.d.ts",
54
+ "default": "./dist/changelog.js"
55
+ },
52
56
  "./settings": {
53
57
  "types": "./dist/settings.d.ts",
54
58
  "default": "./dist/settings.js"
@@ -75,7 +79,6 @@
75
79
  },
76
80
  "./tsconfig.plugin.json": "./tsconfig.plugin.json",
77
81
  "./schema/manifest.schema.json": "./schema/manifest.schema.json",
78
- "./schema/changelog.schema.json": "./schema/changelog.schema.json",
79
82
  "./ui": {
80
83
  "types": "./dist/host/ui.d.ts",
81
84
  "default": "./dist/host/ui.js"
@@ -72,7 +72,12 @@
72
72
  "icon": {
73
73
  "type": "string",
74
74
  "minLength": 1,
75
- "description": "A Lucide icon name, or a relative path to an icon.svg in the plugin root."
75
+ "description": "A Lucide icon name, like \"Play\"."
76
+ },
77
+ "video": {
78
+ "type": "string",
79
+ "pattern": "^https://(www\\.|m\\.)?(youtube\\.com|youtu\\.be|youtube-nocookie\\.com)/",
80
+ "description": "A YouTube link shown at the top of the plugin's page."
76
81
  },
77
82
  "engine": {
78
83
  "type": "object",
@@ -1,126 +0,0 @@
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
- }
@@ -1,67 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://raw.githubusercontent.com/Termix-SSH/Termix/main/packages/plugin-sdk/schema/changelog.schema.json",
4
- "title": "Termix plugin changelog",
5
- "description": "Every release of a plugin, newest first. The newest release must match the manifest version.",
6
- "type": "object",
7
- "additionalProperties": false,
8
- "required": ["releases"],
9
- "properties": {
10
- "$schema": { "type": "string" },
11
- "unreleased": {
12
- "description": "Changes that are merged but not released yet.",
13
- "type": "array",
14
- "items": { "$ref": "#/$defs/change" }
15
- },
16
- "releases": {
17
- "type": "array",
18
- "minItems": 1,
19
- "items": { "$ref": "#/$defs/release" }
20
- }
21
- },
22
- "$defs": {
23
- "release": {
24
- "type": "object",
25
- "additionalProperties": false,
26
- "required": ["version", "changes"],
27
- "properties": {
28
- "version": {
29
- "type": "string",
30
- "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$"
31
- },
32
- "date": {
33
- "description": "When the version was released. Leave it out until it ships.",
34
- "type": "string",
35
- "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
36
- },
37
- "notes": {
38
- "description": "A short line shown above the list of changes.",
39
- "type": "string"
40
- },
41
- "changes": {
42
- "type": "array",
43
- "minItems": 1,
44
- "items": { "$ref": "#/$defs/change" }
45
- }
46
- }
47
- },
48
- "change": {
49
- "type": "object",
50
- "additionalProperties": false,
51
- "required": ["type", "text"],
52
- "properties": {
53
- "type": {
54
- "enum": [
55
- "added",
56
- "changed",
57
- "fixed",
58
- "removed",
59
- "deprecated",
60
- "security"
61
- ]
62
- },
63
- "text": { "type": "string", "minLength": 1 }
64
- }
65
- }
66
- }
67
- }