Files
Termix/scripts/check-core-plugin-ids.cjs
a89ccc6b07 v26.10.3 (#1692)
* feat: shell controller, TabStage, layout and theme plugins with repair view

* feat: move the classic layout into its own bundled plugin

* feat: move color themes into a bundled themes plugin

* fix: plugin tests and type checks work with a linked sdk

* fix: home tab, layout switch and missing layout fallbacks for looks

* chore: add classic layout and themes to the readme

* chore: drop example plugin links from the plugin guide

* feat: plugin filters by type, paged browse, hide plugins needing newer termix

* feat: move desktop, status, import, audit and branding features into plugins

* feat: host lock api so a plugin can load hosts from a config file (#1581)

* chore: add hosts config to the plugin table

* feat: login policy hook for plugins and optional https redirect (#1584)

* chore: add login security to the plugin table

* feat: api key socket tickets so a master can open remote sessions (#1654)

* feat: sync moves to a plugin, desktop gets multiple accounts (#1580, #1654)

* chore: add sync to the plugin table

* feat: move the sync screens and wizard out of core into the plugin (#1580)

* fix: add a neutral linked desktop hook for plugins (#1580)

* fix: show the linked account's name and admin flag in the app (#1580)

* feat: ssh transport providers, core ssh relay and plugin folders api

* feat: support FIDO2 security keys through ssh agent

* feat: add companion panels beside host tabs

* feat: mTLS client certificates for desktop server links

* feat: native web view for plugins in the desktop app

* fix: detect the desktop link window by Electron's default user agent

Fixes #1675

* fix: changelog for desktop link fix

* fix: keep text primary keys in the plugin data export

* fix: ship sharp binaries for Windows arm64 builds

* feat: client cert on the sync link types (sdk 1.0.18)

* chore: update translations

* feat: themed title bar for the desktop app (#1690)

* chore: changelog for the themed title bar

* fix: draw the title bar in the sidebar color

* feat: per host layout for the terminal panel bar

* chore: sdk 1.0.19 for the panel bar layout

* chore: bundle all 43 plugins for 26.10.3

* chore: update translations

---------

Co-authored-by: SirHephaistos <60616553+SirHephaistos@users.noreply.github.com>
Co-authored-by: Giovanny Aranda <garanda21@gmail.com>
2026-10-20 16:03:29 -05:00

215 lines
6.2 KiB
JavaScript

#!/usr/bin/env node
/**
* Core knows no plugin by name.
*
* Fails when anything under src/ spells a plugin id: a bare literal
* ("docker"), a plugin route ("/plugin-api/docker/..."), or an action, slot
* or permission id that starts with one ("docker.open"). What core needs from
* a plugin comes through the registries instead. Regex literals are read too,
* so /^\/plugin-api\/docker/ counts the same as "/plugin-api/docker".
*
* Plugins live in their own repos, so the ids are the official ones below
* plus anything in docker/bundled-plugins.json. electron/ and vite.config.ts
* are read as well.
*
* src/backend/tests, src/ui/tests and src/ui/locales are exempt, and so is
* src/backend/upgrade/: the one-time
* 2.8 to 2.9 data moves have to name the plugin each piece of data moves to,
* the same way LEGACY_TABLE_OWNERS in the SDK names the plugin that adopts
* each legacy table.
*
* One plugin id is also an SSH term: "totp" is the one-time-code prompt kind
* in keyboard-interactive auth, which the connect pipeline in
* src/backend/hosts/connect/ classifies. It is not the login plugin.
*/
const fs = require("node:fs");
const path = require("node:path");
const SSH_TERMS = new Set(["totp"]);
const OFFICIAL_IDS = [
"acme-ssl",
"ai",
"alerts",
"audit-tools",
"automations",
"classic-layout",
"classic-themes",
"credential-tools",
"docker",
"file-manager",
"fleets",
"homepage",
"host-metrics",
"host-status",
"import-export",
"ldap",
"network-topology",
"opkssh",
"proxmox",
"remote-desktop",
"secret-sources",
"serial",
"session-recording",
"session-sharing",
"snippets",
"ssh-terminal",
"sso",
"step-ca",
"tailscale",
"telemetry",
"termix-identity",
"tmux-monitor",
"totp",
"tunnels",
"vault",
"wake-on-lan",
"warpgate",
"web-endpoint",
"webauthn",
"workspaces",
];
function paths(root) {
const src = path.join(root, "src");
return {
root,
src,
bundled: path.join(root, "docker", "bundled-plugins.json"),
extra: [path.join(root, "electron"), path.join(root, "vite.config.ts")],
exempt: [
path.join(src, "backend", "upgrade"),
path.join(src, "backend", "tests"),
path.join(src, "ui", "tests"),
path.join(src, "ui", "locales"),
],
sshConnect: path.join(src, "backend", "hosts", "connect"),
};
}
function pluginIds(bundledFile) {
const ids = new Set(OFFICIAL_IDS);
if (bundledFile && fs.existsSync(bundledFile)) {
for (const entry of JSON.parse(fs.readFileSync(bundledFile, "utf8"))
.plugins ?? []) {
if (typeof entry?.id === "string") ids.add(entry.id);
}
}
return ids;
}
function walk(dir, exempt, out) {
if (exempt.includes(dir) || !fs.existsSync(dir)) return out;
if (fs.statSync(dir).isFile()) {
out.push(dir);
return out;
}
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules") continue;
walk(full, exempt, out);
} else if (/\.(tsx?|mjs|cjs|jsx?)$/.test(entry.name)) {
out.push(full);
}
}
return out;
}
/** Every string literal in a file, template literals included. */
function literals(source) {
const out = [];
const pattern =
/"((?:[^"\\\n]|\\.)*)"|'((?:[^'\\\n]|\\.)*)'|`((?:[^`\\]|\\.)*)`/g;
for (const match of source.matchAll(pattern)) {
out.push(match[1] ?? match[2] ?? match[3] ?? "");
}
return out;
}
/**
* Every regex literal in a file, with escaped slashes undone, so a pattern
* reads the way the path it matches does.
*/
function regexLiterals(source) {
const out = [];
const pattern =
/(^|[=(,:[!&|?{};]|\breturn)\s*\/((?:[^/\\\n[]|\\.|\[(?:[^\]\\\n]|\\.)*\])+)\/[dgimsuyv]*/gm;
for (const match of source.matchAll(pattern)) {
const body = match[2];
// A comment, not a pattern.
if (body.startsWith("/") || body.startsWith("*")) continue;
out.push(body.replace(/\\(.)/g, "$1"));
}
return out;
}
function scan(root = path.resolve(__dirname, "..")) {
const { src, bundled, extra, exempt, sshConnect } = paths(root);
const ids = pluginIds(bundled);
const found = {};
const add = (file, what) => {
const key = path.relative(root, file).replaceAll("\\", "/");
(found[key] ??= new Set()).add(what);
};
const files = walk(src, exempt, []);
for (const target of extra) walk(target, exempt, files);
for (const file of files) {
// A line marked "plugin-id-ok" names a plugin on purpose, such as a key
// an older release stored; the marker has to say why.
const source = fs
.readFileSync(file, "utf8")
.split("\n")
.filter((line) => !/plugin-id-ok: \S/.test(line))
.join("\n");
const inConnect = file.startsWith(sshConnect + path.sep);
for (const text of literals(source)) {
const sshTerm = inConnect && SSH_TERMS.has(text);
if (ids.has(text) && !sshTerm) add(file, text);
for (const route of text.matchAll(
/\/plugin-(?:api|ws|assets)\/([a-z0-9-]+)/g,
)) {
if (ids.has(route[1])) add(file, route[0]);
}
const prefix = /^([a-z][a-z0-9-]*)\.[a-zA-Z]/.exec(text);
if (prefix && ids.has(prefix[1])) add(file, text);
}
for (const pattern of regexLiterals(source)) {
for (const route of pattern.matchAll(
/\/plugin-(?:api|ws|assets)\/([a-z0-9-]+)/g,
)) {
if (ids.has(route[1])) add(file, `/${pattern}/`);
}
const sshTerm = inConnect && SSH_TERMS.has(pattern);
if (ids.has(pattern.replace(/^\^|\$$/g, "")) && !sshTerm) {
add(file, `/${pattern}/`);
}
}
}
return Object.fromEntries(
Object.entries(found).map(([file, set]) => [file, [...set].sort()]),
);
}
function main() {
const found = scan();
const entries = Object.entries(found);
if (process.argv.includes("--list")) {
console.log(JSON.stringify(found, null, 2));
return;
}
if (entries.length > 0) {
console.error("Core names plugins it should reach through a registry:");
for (const [file, list] of entries) {
console.error(` ${file}: ${list.join(", ")}`);
}
process.exit(1);
}
}
if (require.main === module) main();
module.exports = { scan, regexLiterals };