Scaffold LogicSRC and CommandBoard plugins

This commit is contained in:
Anthony Ettinger 2026-06-06 11:24:02 +00:00
parent 59927e140c
commit 5c9ea821f6
67 changed files with 5958 additions and 0 deletions

View file

@ -0,0 +1,18 @@
{
"name": "@logicsrc/plugin-core",
"version": "0.1.0",
"description": "LogicSRC plugin runtime primitives.",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "vitest run src"
},
"dependencies": {
"@logicsrc/validators": "file:../validators"
},
"devDependencies": {
"vitest": "^4.0.8"
}
}

View file

@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { createPluginRegistry } from "./index.js";
import type { PluginDefinition } from "./types.js";
const examplePlugin: PluginDefinition = {
manifest: {
id: "example",
name: "Example",
version: "1.0.0",
type: ["testing"],
default: false,
capabilities: ["tests.run"],
commands: ["test"],
env: ["EXAMPLE_TOKEN"]
}
};
describe("PluginRegistry", () => {
it("indexes enabled plugins by capability", () => {
const registry = createPluginRegistry([examplePlugin]);
expect(registry.byCapability("tests.run")).toHaveLength(1);
expect(registry.snapshot().capabilities["tests.run"]).toEqual(["example"]);
});
it("honors disabled config", () => {
const registry = createPluginRegistry([examplePlugin], {
example: { enabled: false }
});
expect(registry.enabled()).toHaveLength(0);
});
});

View file

@ -0,0 +1,104 @@
import { validate } from "@logicsrc/validators";
import type { LoadedPlugin, PluginConfig, PluginDefinition, PluginManifest, PluginRegistrySnapshot } from "./types.js";
export class PluginManifestError extends Error {
constructor(id: string, message: string) {
super(`Invalid plugin manifest for ${id}: ${message}`);
}
}
export class PluginRegistry {
private readonly plugins = new Map<string, LoadedPlugin>();
register(definition: PluginDefinition, config: PluginConfig = {}) {
validateManifest(definition.manifest);
const id = definition.manifest.id;
if (this.plugins.has(id)) {
throw new Error(`Plugin "${id}" is already registered`);
}
const mergedConfig = {
...definition.configDefaults,
...config
};
this.plugins.set(id, {
definition,
config: mergedConfig,
enabled: mergedConfig.enabled !== false
});
return this;
}
get(id: string) {
return this.plugins.get(id);
}
list() {
return [...this.plugins.values()];
}
enabled() {
return this.list().filter((plugin) => plugin.enabled);
}
byCapability(capability: string) {
return this.enabled().filter((plugin) => plugin.definition.manifest.capabilities.includes(capability));
}
snapshot(): PluginRegistrySnapshot {
const capabilities: Record<string, string[]> = {};
for (const plugin of this.enabled()) {
for (const capability of plugin.definition.manifest.capabilities) {
capabilities[capability] ??= [];
capabilities[capability].push(plugin.definition.manifest.id);
}
}
return {
plugins: this.list().map((plugin) => ({
id: plugin.definition.manifest.id,
name: plugin.definition.manifest.name,
version: plugin.definition.manifest.version,
enabled: plugin.enabled,
default: plugin.definition.manifest.default,
type: plugin.definition.manifest.type,
capabilities: plugin.definition.manifest.capabilities,
commands: plugin.definition.manifest.commands
})),
capabilities
};
}
}
export function validateManifest(manifest: PluginManifest) {
const result = validate("plugin", manifest);
if (!result.ok) {
const message = result.errors.map((error: { instancePath?: string; message?: string }) => `${error.instancePath || "/"} ${error.message}`).join("; ");
throw new PluginManifestError(manifest.id ?? "unknown", message);
}
}
export function createPluginRegistry(definitions: PluginDefinition[], config: Record<string, PluginConfig> = {}) {
const registry = new PluginRegistry();
for (const definition of definitions) {
registry.register(definition, config[definition.manifest.id] ?? {});
}
return registry;
}
export type {
LoadedPlugin,
PluginConfig,
PluginDefinition,
PluginEventHandler,
PluginManifest,
PluginPanel,
PluginRegistrySnapshot,
PluginRoute
} from "./types.js";

View file

@ -0,0 +1,60 @@
export interface PluginManifest {
id: string;
name: string;
version: string;
type: string[];
default: boolean;
capabilities: string[];
commands: string[];
env: string[];
}
export interface PluginConfig {
enabled?: boolean;
[key: string]: unknown;
}
export interface PluginDefinition {
manifest: PluginManifest;
configDefaults?: PluginConfig;
routes?: PluginRoute[];
events?: PluginEventHandler[];
permissions?: string[];
tuiPanels?: PluginPanel[];
}
export interface PluginRoute {
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
path: string;
capability: string;
}
export interface PluginEventHandler {
event: string;
capability: string;
}
export interface PluginPanel {
id: string;
title: string;
}
export interface LoadedPlugin {
definition: PluginDefinition;
config: PluginConfig;
enabled: boolean;
}
export interface PluginRegistrySnapshot {
plugins: Array<{
id: string;
name: string;
version: string;
enabled: boolean;
default: boolean;
type: string[];
capabilities: string[];
commands: string[];
}>;
capabilities: Record<string, string[]>;
}

View file

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}