5.9 KiB
@paarrot/plugin-manager
Framework-agnostic, event-client-agnostic plugin system.
Installation
npm install git+http://synbox.ruv.wtf:8418/litruv/plugin-manager.git
Core Concepts
| Class / Function | Purpose |
|---|---|
PluginRegistry |
Central store — manages plugins, commands, settings, themes, etc. |
createPluginContext |
Factory that wires a PluginContext to a registry instance. |
PluginContext |
The object passed to every plugin's onLoad. |
Quick Start
1. Create the registry
import { PluginRegistry } from '@paarrot/plugin-manager';
export const registry = new PluginRegistry({
// Use localStorage in the browser, or MemoryStorage for Node/tests
storage: localStorage,
// Called when a plugin registers a theme — inject the CSS however you want
onThemeRegistered: (themeId, className, css) => {
const el = document.createElement('style');
el.id = `theme-${themeId}`;
el.textContent = css;
document.head.appendChild(el);
},
onThemeUnregistered: (themeId) => {
document.getElementById(`theme-${themeId}`)?.remove();
},
});
2. Load a plugin
import { createPluginContext } from '@paarrot/plugin-manager';
// Any object with on()/off() works as the event client —
// Node EventEmitter, matrix-js-sdk MatrixClient, socket.io, etc.
const context = createPluginContext(
{
pluginId: 'my-plugin',
eventClient: myEmitter,
onNotify: (opts) => console.log(`[${opts.title}]`, opts.body),
},
registry
);
registry.registerPlugin('my-plugin', plugin, context);
await plugin.onLoad(context);
3. Unload / cleanup
await registry.unregisterPlugin('my-plugin');
// Or tear everything down at once (e.g. on page unmount):
registry.clear();
Writing a Plugin
// my-plugin/index.js
module.exports = {
name: 'My Plugin',
version: '1.0.0',
onLoad(ctx) {
// Register a slash command
ctx.commands.register({
name: 'hello',
description: 'Say hello',
run: (args, ctx) => {
ctx.log('Hello!');
return 'Hello, world!';
},
});
// Define persistent settings
ctx.settings.define({
enabled: { type: 'boolean', label: 'Enable feature', default: true },
prefix: { type: 'string', label: 'Prefix', default: '!' },
});
// Listen to events from the event client
ctx.events.on('Room.timeline', (event) => {
ctx.log('Received event:', event.getType());
});
// Intercept outgoing messages
ctx.messages.onBeforeSend((msg) => {
if (msg.content.startsWith('brb')) {
msg.content = msg.content.replace('brb', 'be right back');
}
});
// Send a notification on load
ctx.notify({ title: 'My Plugin', body: 'Loaded!', type: 'success' });
},
onUnload() {
// cleanup if needed
},
// Other plugins can call ctx.require('my-plugin').greet()
exports: {
greet: (name) => `Hello, ${name}!`,
},
};
PluginContext API Reference
commands
ctx.commands.register(command: PluginCommand): void
ctx.commands.unregister(name: string): void
ctx.commands.execute(name: string, args: Record<string, unknown>): Promise<string | void>
messages
ctx.messages.onBeforeSend(interceptor: MessageInterceptor): void
ctx.messages.onReceive(interceptor: MessageInterceptor): void
settings
ctx.settings.define(schema: SettingsSchema): void
ctx.settings.get<T>(key: string): T | undefined
ctx.settings.set(key: string, value: unknown): void
Settings are persisted to the storage adapter automatically.
themes
ctx.themes.register(theme: PluginTheme): void
ctx.themes.unregister(themeId: string): void
events
ctx.events.on(eventType: string, handler: (...args: any[]) => void): void
ctx.events.off(eventType: string, handler: (...args: any[]) => void): void
Wires to whatever eventClient you pass into createPluginContext. All handlers are cleaned up automatically on unregisterPlugin.
timers
ctx.timers.setInterval(callback, ms): number
ctx.timers.setTimeout(callback, ms): number
ctx.timers.clearInterval(id): void
ctx.timers.clearTimeout(id): void
All timers are cleared automatically on unregisterPlugin.
ui
ctx.ui.registerRenderer(type: string, renderer: CustomRenderer): void
ctx.ui.unregisterRenderer(type: string): void
notify
ctx.notify('Simple message')
ctx.notify({ title: 'Title', body: 'Body', type: 'success' | 'error' | 'warning' | 'info' })
Delegates to the onNotify callback you provide to createPluginContext.
require
const api = ctx.require('other-plugin-id');
Returns exports from another loaded plugin.
Logging
ctx.log(...args)
ctx.warn(...args)
ctx.error(...args)
Forwarded to console and stored in the registry log buffer (accessible via registry.getLogs(pluginId)).
Registry Methods
registry.processBeforeSend(msg: MessageContext): Promise<MessageContext>
registry.processReceive(msg: MessageContext): Promise<MessageContext>
registry.getCommands(): Array<{ name, pluginId, command }>
registry.getRenderer(type: string): CustomRenderer | undefined
registry.getPluginThemes(): Array<{ id, name, kind, className }>
registry.getLogs(pluginId?: string): PluginLogEntry[]
registry.clearLogs(pluginId?: string): void
Storage Adapter
The default is MemoryStorage (in-memory, not persisted). Pass any object implementing:
interface IPluginStorage {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
}
localStorage works directly in browsers.
Event Client Adapter
Pass any object implementing:
interface IPluginEventClient {
on(event: string, handler: (...args: any[]) => void): void;
off(event: string, handler: (...args: any[]) => void): void;
}
Compatible with: Node.js EventEmitter, matrix-js-sdk MatrixClient, socket.io sockets, and similar.