From 905fdc25b867cfa01ff088b194c9f312bc396674 Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Sat, 18 Apr 2026 20:13:12 +1000 Subject: [PATCH] Add usage guide --- USAGE.md | 249 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 USAGE.md diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..fcb08f2 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,249 @@ +# @paarrot/plugin-manager + +Framework-agnostic, event-client-agnostic plugin system. + +--- + +## Installation + +```bash +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 + +```ts +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 + +```ts +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 + +```ts +await registry.unregisterPlugin('my-plugin'); + +// Or tear everything down at once (e.g. on page unmount): +registry.clear(); +``` + +--- + +## Writing a Plugin + +```js +// 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` +```ts +ctx.commands.register(command: PluginCommand): void +ctx.commands.unregister(name: string): void +ctx.commands.execute(name: string, args: Record): Promise +``` + +### `messages` +```ts +ctx.messages.onBeforeSend(interceptor: MessageInterceptor): void +ctx.messages.onReceive(interceptor: MessageInterceptor): void +``` + +### `settings` +```ts +ctx.settings.define(schema: SettingsSchema): void +ctx.settings.get(key: string): T | undefined +ctx.settings.set(key: string, value: unknown): void +``` +Settings are persisted to the storage adapter automatically. + +### `themes` +```ts +ctx.themes.register(theme: PluginTheme): void +ctx.themes.unregister(themeId: string): void +``` + +### `events` +```ts +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` +```ts +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` +```ts +ctx.ui.registerRenderer(type: string, renderer: CustomRenderer): void +ctx.ui.unregisterRenderer(type: string): void +``` + +### `notify` +```ts +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` +```ts +const api = ctx.require('other-plugin-id'); +``` +Returns `exports` from another loaded plugin. + +### Logging +```ts +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 + +```ts +registry.processBeforeSend(msg: MessageContext): Promise +registry.processReceive(msg: MessageContext): Promise +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: + +```ts +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: + +```ts +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.