@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.
- package/README.md +7 -0
- package/cli/commands/changelog.mjs +25 -0
- package/cli/commands/dev.mjs +158 -0
- package/cli/commands/validate.mjs +16 -24
- package/cli/index.mjs +8 -0
- package/cli/lib/tmxplug.mjs +1 -1
- package/dist/backend.d.ts +6 -2
- package/dist/changelog.d.ts +67 -0
- package/dist/changelog.js +195 -0
- package/dist/frontend.d.ts +67 -1
- package/dist/host/{chunk-XGWE7BTJ.js → chunk-3KG44G55.js} +6 -1
- package/dist/host/{chunk-XH5NXL6Y.js → chunk-7GL25GFS.js} +8013 -7606
- package/dist/host/{chunk-F3SDZDDG.js → chunk-COGRSHKO.js} +17 -2
- package/dist/host/{linked-server-FZGWQW32.js → linked-server-BD573BEG.js} +1 -1
- package/dist/host/{rbac-api-6KDMBVDZ.js → rbac-api-7GHDJLU4.js} +1 -1
- package/dist/host/testing-host.js +38 -5
- package/dist/host/types/src/types/credential-sidebar-preferences.d.ts +12 -1
- package/dist/host/types/src/types/host-sidebar-preferences.d.ts +18 -1
- package/dist/host/types/src/types/onboarding.d.ts +29 -0
- package/dist/host/types/src/types/plugin-onboarding.d.ts +39 -0
- package/dist/host/types/src/types/ui-preferences.d.ts +13 -4
- package/dist/host/types/src/types/ui-types.d.ts +1 -1
- package/dist/host/types/src/ui/api/plugins-api.d.ts +194 -0
- package/dist/host/types/src/ui/components/connection/ConnectionLogPanel.d.ts +2 -1
- package/dist/host/types/src/ui/components/connection/ConnectionScreen.d.ts +5 -5
- package/dist/host/types/src/ui/components/list-kit.d.ts +102 -0
- package/dist/host/types/src/ui/components/panel-layout.d.ts +6 -1
- package/dist/host/types/src/ui/components/surface/surface-scope.d.ts +5 -1
- package/dist/host/types/src/ui/contexts/UiPreferencesContext.d.ts +15 -2
- package/dist/host/types/src/ui/hooks/use-permissions.d.ts +2 -0
- package/dist/host/types/src/ui/lib/embedded-frame.d.ts +14 -0
- package/dist/host/types/src/ui/onboarding/onboarding-registry.d.ts +49 -0
- package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +1 -0
- package/dist/host/types/src/ui/plugins/open-plugins.d.ts +5 -0
- package/dist/host/types/src/ui/sidebar/host-contributions.d.ts +2 -0
- package/dist/host/types/src/ui/sidebar/rail-items.d.ts +10 -0
- package/dist/host/ui.js +982 -698
- package/dist/host/{use-permissions-NULZL43J.js → use-permissions-SKWXHWDJ.js} +3 -1
- package/dist/manifest.d.ts +10 -2
- package/dist/manifest.js +46 -1
- package/dist/testing/setup.js +8 -0
- package/dist/testing.d.ts +12 -0
- package/dist/vitest-preset.d.ts +3 -0
- package/dist/vitest-preset.js +6 -0
- package/package.json +5 -2
- package/schema/manifest.schema.json +6 -1
- package/cli/lib/changelog.mjs +0 -126
- 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-
|
|
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,
|
package/dist/manifest.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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);
|
package/dist/testing/setup.js
CHANGED
|
@@ -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;
|
package/dist/vitest-preset.d.ts
CHANGED
|
@@ -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: {
|
package/dist/vitest-preset.js
CHANGED
|
@@ -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.
|
|
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,
|
|
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",
|
package/cli/lib/changelog.mjs
DELETED
|
@@ -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
|
-
}
|