mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
chore: rework plugin readmes, json changelogs and standalone repo setup
This commit is contained in:
1 parent
a8f383d2b3
commit
29a946d02e
500 files changed
+6729
-1250
No files matched your search
@@ -1,18 +1,62 @@
|
||||
# @termix/plugin-sdk
|
||||
<div align="center">
|
||||
|
||||
Everything a [Termix](https://github.com/Termix-SSH/Termix) plugin builds
|
||||
against: the types for `activate(ctx)` and `activate(app)`, table
|
||||
definitions, the manifest schema, test helpers, a vitest preset and the
|
||||
`termix-plugin` CLI.
|
||||
<img src="https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.svg" width="120" height="120" alt="Termix Logo" />
|
||||
|
||||
<h1>Termix Plugin SDK</h1>
|
||||
|
||||
<p>Everything you need to build a Termix plugin</p>
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
## Overview
|
||||
|
||||
`@termix/plugin-sdk` is what every [Termix](https://github.com/Termix-SSH/Termix) plugin builds against, including the ones that ship with Termix. It has the types for a plugin's backend and frontend, table definitions, the manifest schema, test helpers, and the `termix-plugin` CLI. The easiest way to start is the [Termix Plugin Template](https://github.com/Termix-SSH/Termix-Plugin-Template).
|
||||
|
||||
<br />
|
||||
|
||||
## What's Inside
|
||||
|
||||
- **Backend:** the types for `activate(ctx)`, database and table helpers, host commands and SSH certificates
|
||||
- **Frontend:** the types for `activate(app)`, plus `@termix/plugin-sdk/ui`, the components and theme Termix serves to plugins
|
||||
- **Manifest:** the manifest schema, the capability catalog and settings types
|
||||
- **Testing:** `createMockCtx()`, `createTestDb()`, `renderWithApp()` and a Vitest preset
|
||||
- **CLI:** `termix-plugin`, which builds, tests, validates, packs and signs a plugin and writes its migrations
|
||||
|
||||
<br />
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install --save-dev @termix/plugin-sdk
|
||||
npx termix-plugin build # bundle dist/backend.js and dist/frontend.js
|
||||
npx termix-plugin test # run the plugin's vitest suite
|
||||
npx termix-plugin validate # check manifest.json and the files it names
|
||||
npx termix-plugin pack # write <id>-<version>.tmxplug
|
||||
npx termix-plugin sign <file.tmxplug> # needs TERMIX_PLUGIN_SIGNING_KEY
|
||||
```
|
||||
|
||||
Start from
|
||||
[Termix-Plugin-Template](https://github.com/Termix-SSH/Termix-Plugin-Template).
|
||||
<br />
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
npx termix-plugin build # bundle dist/backend.js and dist/frontend.js
|
||||
npx termix-plugin test # run the plugin's Vitest suite
|
||||
npx termix-plugin validate # check manifest.json and the files it names
|
||||
npx termix-plugin migrations # write migrations from the table definitions
|
||||
npx termix-plugin pack # write <id>-<version>.tmxplug
|
||||
npx termix-plugin sign <file.tmxplug> # sign it, needs TERMIX_PLUGIN_SIGNING_KEY
|
||||
npx termix-plugin verify <file.tmxplug> --key <base64> # check a signature
|
||||
npx termix-plugin keygen --out <dir> # make a new signing key pair
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Support
|
||||
|
||||
To report a bug or request a feature, open a [support ticket](https://github.com/Termix-SSH/Support/issues/new/choose). You need to be logged in to GitHub. Please be as detailed as possible, preferably in English.
|
||||
|
||||
For discussions and questions, join the [Discord](https://discord.gg/jVQGdvHDrf) server.
|
||||
|
||||
<br />
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the Apache License Version 2.0. See [LICENSE](https://github.com/Termix-SSH/Termix/blob/main/LICENSE) for more information.
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { readManifest } from "../lib/plugin-dir.mjs";
|
||||
import { validateChangelog } from "../lib/changelog.mjs";
|
||||
|
||||
/**
|
||||
* The manifest rules live in src/manifest.ts, which the server uses too, so
|
||||
@@ -40,6 +41,7 @@ export async function validate({ cwd }) {
|
||||
);
|
||||
problems.push(...validateNativeDependencies(cwd, raw));
|
||||
problems.push(...validatePackage(cwd, raw));
|
||||
problems.push(...validateChangelogFile(cwd, raw));
|
||||
|
||||
if (problems.length > 0) {
|
||||
for (const problem of problems) console.error(` ${problem}`);
|
||||
@@ -79,6 +81,30 @@ function validatePackage(cwd, raw) {
|
||||
return problems;
|
||||
}
|
||||
|
||||
/**
|
||||
* CHANGELOG.json is optional, but when it is there it has to parse and its
|
||||
* newest release has to match the manifest version.
|
||||
*/
|
||||
function validateChangelogFile(cwd, raw) {
|
||||
const jsonPath = path.join(cwd, "CHANGELOG.json");
|
||||
if (!fs.existsSync(jsonPath)) {
|
||||
return fs.existsSync(path.join(cwd, "CHANGELOG.md"))
|
||||
? [
|
||||
"CHANGELOG.md is no longer shipped; move the release notes to CHANGELOG.json",
|
||||
]
|
||||
: [];
|
||||
}
|
||||
let changelog;
|
||||
try {
|
||||
changelog = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
} catch (error) {
|
||||
return [`CHANGELOG.json is not valid JSON: ${error.message}`];
|
||||
}
|
||||
return validateChangelog(changelog, raw.version).map(
|
||||
(problem) => `CHANGELOG.json: ${problem}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A native dependency has to actually be a real dependency of the plugin, or
|
||||
* the build's external declaration points at nothing node_modules can
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
export const CHANGE_TYPES = [
|
||||
"added",
|
||||
"changed",
|
||||
"fixed",
|
||||
"removed",
|
||||
"deprecated",
|
||||
"security",
|
||||
];
|
||||
|
||||
const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
||||
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const TOP_KEYS = ["$schema", "unreleased", "releases"];
|
||||
const RELEASE_KEYS = ["version", "date", "notes", "changes"];
|
||||
const CHANGE_KEYS = ["type", "text"];
|
||||
|
||||
function compareVersions(a, b) {
|
||||
const pa = SEMVER.exec(a);
|
||||
const pb = SEMVER.exec(b);
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const diff = Number(pa[i]) - Number(pb[i]);
|
||||
if (diff !== 0) return diff;
|
||||
}
|
||||
if (pa[4] === pb[4]) return 0;
|
||||
if (!pa[4]) return 1;
|
||||
if (!pb[4]) return -1;
|
||||
return pa[4] < pb[4] ? -1 : 1;
|
||||
}
|
||||
|
||||
function unknownKeys(value, allowed, where, problems) {
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!allowed.includes(key))
|
||||
problems.push(`${where} has unknown key "${key}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function checkChanges(changes, where, problems) {
|
||||
if (!Array.isArray(changes)) {
|
||||
problems.push(`${where} must be an array`);
|
||||
return;
|
||||
}
|
||||
changes.forEach((change, i) => {
|
||||
const at = `${where}[${i}]`;
|
||||
if (!change || typeof change !== "object" || Array.isArray(change)) {
|
||||
problems.push(`${at} must be an object`);
|
||||
return;
|
||||
}
|
||||
unknownKeys(change, CHANGE_KEYS, at, problems);
|
||||
if (!CHANGE_TYPES.includes(change.type)) {
|
||||
problems.push(`${at}.type must be one of ${CHANGE_TYPES.join(", ")}`);
|
||||
}
|
||||
if (typeof change.text !== "string" || change.text.trim() === "") {
|
||||
problems.push(`${at}.text must be a non-empty string`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a parsed CHANGELOG.json. The newest release has to be the version
|
||||
* in the manifest, so a release never ships without its notes.
|
||||
*/
|
||||
export function validateChangelog(changelog, manifestVersion) {
|
||||
const problems = [];
|
||||
if (!changelog || typeof changelog !== "object" || Array.isArray(changelog)) {
|
||||
return ["CHANGELOG.json must be an object"];
|
||||
}
|
||||
unknownKeys(changelog, TOP_KEYS, "CHANGELOG.json", problems);
|
||||
|
||||
if (changelog.unreleased !== undefined) {
|
||||
checkChanges(changelog.unreleased, "unreleased", problems);
|
||||
}
|
||||
|
||||
const releases = changelog.releases;
|
||||
if (!Array.isArray(releases) || releases.length === 0) {
|
||||
problems.push("releases must be a non-empty array");
|
||||
return problems;
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
releases.forEach((release, i) => {
|
||||
const at = `releases[${i}]`;
|
||||
if (!release || typeof release !== "object" || Array.isArray(release)) {
|
||||
problems.push(`${at} must be an object`);
|
||||
return;
|
||||
}
|
||||
unknownKeys(release, RELEASE_KEYS, at, problems);
|
||||
if (typeof release.version !== "string" || !SEMVER.test(release.version)) {
|
||||
problems.push(`${at}.version must be a version like 1.2.3`);
|
||||
} else if (seen.has(release.version)) {
|
||||
problems.push(`${at}.version ${release.version} is listed twice`);
|
||||
} else {
|
||||
seen.add(release.version);
|
||||
}
|
||||
if (
|
||||
release.date !== undefined &&
|
||||
(typeof release.date !== "string" || !DATE.test(release.date))
|
||||
) {
|
||||
problems.push(`${at}.date must be a date like 2026-10-01`);
|
||||
}
|
||||
if (release.notes !== undefined && typeof release.notes !== "string") {
|
||||
problems.push(`${at}.notes must be a string`);
|
||||
}
|
||||
checkChanges(release.changes, `${at}.changes`, problems);
|
||||
if (Array.isArray(release.changes) && release.changes.length === 0) {
|
||||
problems.push(`${at}.changes must list at least one change`);
|
||||
}
|
||||
});
|
||||
|
||||
const versions = releases
|
||||
.map((release) => release?.version)
|
||||
.filter((v) => typeof v === "string" && SEMVER.test(v));
|
||||
for (let i = 1; i < versions.length; i++) {
|
||||
if (compareVersions(versions[i - 1], versions[i]) < 0) {
|
||||
problems.push("releases must be listed newest first");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const newest = releases[0]?.version;
|
||||
if (manifestVersion && newest !== manifestVersion) {
|
||||
problems.push(
|
||||
`the newest release is ${newest}, but manifest version is ${manifestVersion}`,
|
||||
);
|
||||
}
|
||||
|
||||
return problems;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export const TMXPLUG_CONTENTS = [
|
||||
"locales",
|
||||
"migrations",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"CHANGELOG.json",
|
||||
"icon.svg",
|
||||
];
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@termix/plugin-sdk",
|
||||
"version": "1.0.0",
|
||||
"description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
|
||||
"license": "MIT",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Termix-SSH/Termix.git",
|
||||
@@ -75,6 +75,7 @@
|
||||
},
|
||||
"./tsconfig.plugin.json": "./tsconfig.plugin.json",
|
||||
"./schema/manifest.schema.json": "./schema/manifest.schema.json",
|
||||
"./schema/changelog.schema.json": "./schema/changelog.schema.json",
|
||||
"./ui": {
|
||||
"types": "./dist/host/ui.d.ts",
|
||||
"default": "./dist/host/ui.js"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://raw.githubusercontent.com/Termix-SSH/Termix/main/packages/plugin-sdk/schema/changelog.schema.json",
|
||||
"title": "Termix plugin changelog",
|
||||
"description": "Every release of a plugin, newest first. The newest release must match the manifest version.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["releases"],
|
||||
"properties": {
|
||||
"$schema": { "type": "string" },
|
||||
"unreleased": {
|
||||
"description": "Changes that are merged but not released yet.",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/change" }
|
||||
},
|
||||
"releases": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/release" }
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"release": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["version", "changes"],
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$"
|
||||
},
|
||||
"date": {
|
||||
"description": "When the version was released. Leave it out until it ships.",
|
||||
"type": "string",
|
||||
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||
},
|
||||
"notes": {
|
||||
"description": "A short line shown above the list of changes.",
|
||||
"type": "string"
|
||||
},
|
||||
"changes": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/change" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"change": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "text"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"added",
|
||||
"changed",
|
||||
"fixed",
|
||||
"removed",
|
||||
"deprecated",
|
||||
"security"
|
||||
]
|
||||
},
|
||||
"text": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user