chore: rework plugin readmes, json changelogs and standalone repo setup

This commit is contained in:
LukeGus committed 2026-10-04 21:25:16 -05:00
1 parent a8f383d2b3
commit 29a946d02e
500 files changed
+6729 -1250

No files matched your search

+56 -12
View File
@@ -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
+126
View File
@@ -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;
}
+1 -1
View File
@@ -9,7 +9,7 @@ export const TMXPLUG_CONTENTS = [
"locales",
"migrations",
"README.md",
"CHANGELOG.md",
"CHANGELOG.json",
"icon.svg",
];
+2 -1
View File
@@ -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 }
}
}
}
}