mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 14:37:26 +00:00
Scaffold LogicSRC and CommandBoard plugins
This commit is contained in:
parent
59927e140c
commit
5c9ea821f6
67 changed files with 5958 additions and 0 deletions
18
packages/plugin-core/package.json
Normal file
18
packages/plugin-core/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
33
packages/plugin-core/src/index.test.ts
Normal file
33
packages/plugin-core/src/index.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
104
packages/plugin-core/src/index.ts
Normal file
104
packages/plugin-core/src/index.ts
Normal 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";
|
||||
60
packages/plugin-core/src/types.ts
Normal file
60
packages/plugin-core/src/types.ts
Normal 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[]>;
|
||||
}
|
||||
8
packages/plugin-core/tsconfig.json
Normal file
8
packages/plugin-core/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue