@termix-ssh/plugin-sdk 1.0.0 → 1.0.2
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/CHANGELOG.md +54 -0
- package/README.md +8 -1
- 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 +62 -0
- package/dist/changelog.js +187 -0
- package/dist/frontend.d.ts +67 -1
- package/dist/host/{chunk-F3SDZDDG.js → chunk-LKYAVM4T.js} +17 -2
- package/dist/host/{chunk-XH5NXL6Y.js → chunk-SS43S6VI.js} +7816 -7344
- package/dist/host/{chunk-XGWE7BTJ.js → chunk-XKMRKQFN.js} +6 -1
- package/dist/host/{linked-server-FZGWQW32.js → linked-server-DW5P3NLZ.js} +1 -1
- package/dist/host/{rbac-api-6KDMBVDZ.js → rbac-api-JKISXZEP.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 +211 -0
- package/dist/host/types/src/ui/api/system-status-api.d.ts +24 -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-installed-plugins.d.ts +6 -0
- 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/settings/settings-fields-util.d.ts +10 -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 +1097 -787
- package/dist/host/{use-permissions-NULZL43J.js → use-permissions-5CZEA3P4.js} +3 -1
- package/dist/manifest.d.ts +19 -2
- package/dist/manifest.js +82 -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 +7 -3
- package/schema/manifest.schema.json +12 -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-XKMRKQFN.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,18 @@ 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
|
+
export declare const MAX_FEATURES = 20;
|
|
31
|
+
export declare const MAX_FEATURE_LENGTH = 160;
|
|
32
|
+
/**
|
|
33
|
+
* The usable feature lines of a manifest or registry entry. Anything that is
|
|
34
|
+
* not a short non-empty string is dropped, so an index cannot flood the page.
|
|
35
|
+
*/
|
|
36
|
+
export declare function pluginFeatures(value: unknown): string[];
|
|
37
|
+
/**
|
|
38
|
+
* The video id of a YouTube link, or null for anything else. Only the id is
|
|
39
|
+
* ever used, so a manifest can never point the embed at another site.
|
|
40
|
+
*/
|
|
41
|
+
export declare function youtubeVideoId(value: unknown): string | null;
|
|
30
42
|
export declare const PLUGIN_CATEGORIES: readonly ["Terminal", "Files & Transfer", "Infrastructure", "Monitoring", "Networking", "Access & Security", "Productivity"];
|
|
31
43
|
export declare const PLUGIN_PLATFORMS: readonly ["linux", "win32", "darwin"];
|
|
32
44
|
export declare const ACTION_CONTRIBUTION_KINDS: readonly ["button", "component"];
|
|
@@ -327,7 +339,8 @@ export interface PluginProtocolContribution {
|
|
|
327
339
|
defaultPort?: number;
|
|
328
340
|
/**
|
|
329
341
|
* What the owner's login falls back to from the host's own SSH login
|
|
330
|
-
* when the protocol login leaves it empty.
|
|
342
|
+
* when the protocol login leaves it empty. The password only falls back
|
|
343
|
+
* while the host signs in to SSH with a password.
|
|
331
344
|
*/
|
|
332
345
|
hostLoginFallback?: Array<"username" | "password">;
|
|
333
346
|
}
|
|
@@ -408,6 +421,10 @@ export interface PluginManifest {
|
|
|
408
421
|
repository?: string;
|
|
409
422
|
category: string;
|
|
410
423
|
icon?: string;
|
|
424
|
+
/** A YouTube link shown at the top of the plugin's page. */
|
|
425
|
+
video?: string;
|
|
426
|
+
/** Short lines listing what the plugin does, shown on its page. */
|
|
427
|
+
features?: string[];
|
|
411
428
|
engine: PluginEngine;
|
|
412
429
|
/** Catalog capability ids. */
|
|
413
430
|
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,62 @@ 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
|
+
export const MAX_FEATURES = 20;
|
|
51
|
+
export const MAX_FEATURE_LENGTH = 160;
|
|
52
|
+
/**
|
|
53
|
+
* The usable feature lines of a manifest or registry entry. Anything that is
|
|
54
|
+
* not a short non-empty string is dropped, so an index cannot flood the page.
|
|
55
|
+
*/
|
|
56
|
+
export function pluginFeatures(value) {
|
|
57
|
+
if (!Array.isArray(value))
|
|
58
|
+
return [];
|
|
59
|
+
return value
|
|
60
|
+
.filter((f) => typeof f === "string" &&
|
|
61
|
+
f.trim().length > 0 &&
|
|
62
|
+
f.length <= MAX_FEATURE_LENGTH)
|
|
63
|
+
.map((f) => f.trim())
|
|
64
|
+
.slice(0, MAX_FEATURES);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The video id of a YouTube link, or null for anything else. Only the id is
|
|
68
|
+
* ever used, so a manifest can never point the embed at another site.
|
|
69
|
+
*/
|
|
70
|
+
export function youtubeVideoId(value) {
|
|
71
|
+
if (typeof value !== "string")
|
|
72
|
+
return null;
|
|
73
|
+
let url;
|
|
74
|
+
try {
|
|
75
|
+
url = new URL(value.trim());
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (url.protocol !== "https:" || url.username || url.password)
|
|
81
|
+
return null;
|
|
82
|
+
const host = url.hostname.toLowerCase();
|
|
83
|
+
if (!YOUTUBE_HOSTS.has(host))
|
|
84
|
+
return null;
|
|
85
|
+
let id;
|
|
86
|
+
if (host === "youtu.be") {
|
|
87
|
+
id = url.pathname.slice(1);
|
|
88
|
+
}
|
|
89
|
+
else if (url.pathname === "/watch") {
|
|
90
|
+
id = url.searchParams.get("v");
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
id = /^\/(?:embed|shorts|live)\/([^/]+)\/?$/.exec(url.pathname)?.[1];
|
|
94
|
+
}
|
|
95
|
+
return id && YOUTUBE_ID.test(id) ? id : null;
|
|
96
|
+
}
|
|
41
97
|
export const PLUGIN_CATEGORIES = [
|
|
42
98
|
"Terminal",
|
|
43
99
|
"Files & Transfer",
|
|
@@ -64,6 +120,7 @@ export const RESERVED_PLUGIN_IDS = [
|
|
|
64
120
|
"termix",
|
|
65
121
|
"plugin",
|
|
66
122
|
"plugins",
|
|
123
|
+
"onboarding",
|
|
67
124
|
"users",
|
|
68
125
|
"system",
|
|
69
126
|
"sdk",
|
|
@@ -148,6 +205,8 @@ const ALLOWED_TOP_LEVEL = new Set([
|
|
|
148
205
|
...REQUIRED_TOP_LEVEL,
|
|
149
206
|
"repository",
|
|
150
207
|
"icon",
|
|
208
|
+
"video",
|
|
209
|
+
"features",
|
|
151
210
|
"dependencies",
|
|
152
211
|
"optionalDependencies",
|
|
153
212
|
"provides",
|
|
@@ -274,6 +333,10 @@ export function validateManifest(manifest) {
|
|
|
274
333
|
requireString(m.repository, "repository", errors);
|
|
275
334
|
if ("icon" in m)
|
|
276
335
|
requireString(m.icon, "icon", errors);
|
|
336
|
+
if ("video" in m && youtubeVideoId(m.video) === null) {
|
|
337
|
+
errors.push(`Field "video" must be a YouTube link such as https://www.youtube.com/watch?v=..., got: ${JSON.stringify(m.video)}`);
|
|
338
|
+
}
|
|
339
|
+
validateFeatures(m.features, errors);
|
|
277
340
|
for (const field of ["backend", "frontend", "locales"]) {
|
|
278
341
|
if (field in m && requireString(m[field], field, errors)) {
|
|
279
342
|
requireRelativePath(m[field], field, errors);
|
|
@@ -298,6 +361,24 @@ export function validateManifest(manifest) {
|
|
|
298
361
|
validateContributes(m.contributes, typeof m.id === "string" ? m.id : undefined, errors);
|
|
299
362
|
return errors;
|
|
300
363
|
}
|
|
364
|
+
function validateFeatures(features, errors) {
|
|
365
|
+
if (features === undefined)
|
|
366
|
+
return;
|
|
367
|
+
if (!Array.isArray(features)) {
|
|
368
|
+
errors.push('Field "features" must be an array of strings');
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (features.length > MAX_FEATURES) {
|
|
372
|
+
errors.push(`Field "features" may list at most ${MAX_FEATURES} items`);
|
|
373
|
+
}
|
|
374
|
+
features.forEach((feature, i) => {
|
|
375
|
+
if (typeof feature !== "string" ||
|
|
376
|
+
feature.trim().length === 0 ||
|
|
377
|
+
feature.length > MAX_FEATURE_LENGTH) {
|
|
378
|
+
errors.push(`Field "features[${i}]" must be a non-empty string of at most ${MAX_FEATURE_LENGTH} characters`);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
301
382
|
function validateAuthor(author, errors) {
|
|
302
383
|
if (author === undefined)
|
|
303
384
|
return;
|
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.2",
|
|
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": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"node": ">=22"
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
|
-
"termix-plugin": "
|
|
17
|
+
"termix-plugin": "cli/index.mjs"
|
|
18
18
|
},
|
|
19
19
|
"exports": {
|
|
20
20
|
"./backend": {
|
|
@@ -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"
|
|
@@ -91,6 +94,7 @@
|
|
|
91
94
|
"schema",
|
|
92
95
|
"tsconfig.plugin.json",
|
|
93
96
|
"README.md",
|
|
97
|
+
"CHANGELOG.md",
|
|
94
98
|
"client.d.ts"
|
|
95
99
|
],
|
|
96
100
|
"scripts": {
|
|
@@ -72,7 +72,18 @@
|
|
|
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."
|
|
81
|
+
},
|
|
82
|
+
"features": {
|
|
83
|
+
"type": "array",
|
|
84
|
+
"maxItems": 20,
|
|
85
|
+
"items": { "type": "string", "minLength": 1, "maxLength": 160 },
|
|
86
|
+
"description": "Short lines listing what the plugin does, shown on its page."
|
|
76
87
|
},
|
|
77
88
|
"engine": {
|
|
78
89
|
"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
|
-
}
|