Files
plugin-manager/USAGE.md
2026-04-18 20:13:12 +10:00

250 lines
5.9 KiB
Markdown

# @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<string, unknown>): Promise<string | void>
```
### `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<T>(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<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:
```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.