# Plugin Development Guide This guide walks through creating a SlopSmith plugin from scratch. Plugins live in the user plugin directory and are discovered automatically at server start. --- ## Directory Structure A plugin is a folder containing at minimum a `plugin.json` manifest: ``` my-plugin/ ├── plugin.json # required — manifest ├── server.ts # optional — backend module ├── component.vue # optional — frontend Vue SFC ├── screen.html # optional — fallback HTML view └── settings.html # optional — settings page HTML ``` Place the folder in the configured user plugins directory (default: `~/.slopsmith/plugins/`). --- ## The Manifest (`plugin.json`) ```jsonc { "id": "my-plugin", // unique, kebab-case "name": "My Plugin", "version": "1.0.0", "type": "visualization", // visualization | provider | (omit for generic) // Backend "script": "./server.ts", // backend module with setup() export "routes": [], // declared for documentation; registered inside setup() // Frontend "component": "./component.vue", // Vue SFC path "screen": "./screen.html", // fallback if no component "settings": "./settings.html", // settings page // Navigation "nav": { "label": "My Plugin", "icon": "puzzle-piece", // any Heroicon name "order": 50 }, // Dependency ordering "dependsOn": ["other-plugin-id"], // Flags "bundled": false, // true for built-in plugins "private": false // true to hide from UI listings } ``` Only `id`, `name`, and `version` are required. Add only the fields your plugin uses. --- ## Backend Module Export `setup` and optionally `teardown`. The runtime calls these during lifecycle transitions. ```typescript // server.ts import type { PluginModule } from '@slopsmith/plugin-types' export const plugin: PluginModule = { async setup(ctx) { // Register an HTTP route ctx.routes.register('GET', '/hello', async (req, reply) => { return { message: 'Hello from my-plugin!' } }) // Subscribe to a hook ctx.hooks.on('song:play', async ({ songId }) => { ctx.logger.info(`Song started: ${songId}`) }) // Persist some data await ctx.db.set('initialized', true) }, async teardown(ctx) { // Clean up resources — hooks and routes are removed automatically ctx.logger.info('my-plugin unloaded') } } ``` ### PluginContext at a glance | Property | Type | Purpose | |---|---|---| | `ctx.pluginId` | `string` | This plugin's ID | | `ctx.pluginDir` | `string` | Absolute path to the plugin folder | | `ctx.config` | `AppConfig` | Global app configuration | | `ctx.hooks.on(event, handler)` | — | Subscribe to a lifecycle event | | `ctx.hooks.once(event, handler)` | — | Subscribe once | | `ctx.hooks.off(event, handler)` | — | Unsubscribe | | `ctx.routes.register(method, path, handler)` | — | Register HTTP route (scoped to `/api/plugins/{id}/`) | | `ctx.routes.ws(path, handler)` | — | Register WebSocket endpoint | | `ctx.providers.register(type, name, impl)` | — | Register a provider implementation | | `ctx.providers.get(type)` | — | Get the active provider | | `ctx.db.get/set/delete/list(key)` | `Promise` | Scoped key-value storage | | `ctx.permissions.define(name, description)` | — | Declare a custom permission | | `ctx.logger` | `Logger` | Structured logger | --- ## Frontend Module The Vue SFC can export a `setup` function for programmatic initialization: ```vue ``` If `setup` is not needed, just ship a plain Vue SFC — the loader will import and render it without calling setup. ### FrontendPluginContext at a glance | Property | Purpose | |---|---| | `ctx.pluginId` | This plugin's ID | | `ctx.events.on(event, handler)` | Subscribe to frontend events | | `ctx.events.once(event, handler)` | Subscribe once | | `ctx.events.emit(event, detail)` | Emit a custom event | | `ctx.slots.register(slot, component, opts?)` | Inject a component into a named UI slot | | `ctx.api.get/post/patch/delete(path, body?)` | Call this plugin's backend routes | --- ## Registering a Provider Providers let a plugin offer a swappable implementation of a backend service. Example: a cloud storage backend. ```typescript // server.ts import type { IStorageProvider } from '@slopsmith/core' export const plugin: PluginModule = { async setup(ctx) { const s3Storage: IStorageProvider = { async read(path) { /* ... */ }, async write(path, data) { /* ... */ }, async delete(path) { /* ... */ }, } ctx.providers.register('storage', 's3', s3Storage) } } ``` Users switch the active provider via **Settings → Providers** or `PUT /api/plugins/providers/storage/active`. --- ## Defining Custom Permissions ```typescript ctx.permissions.define('manage_settings', 'Allow user to modify plugin configuration') ``` The permission name is then referenced in route registration: ```typescript ctx.routes.register('POST', '/config', handler, { permission: 'manage_settings' }) ``` --- ## Frontend-Only Plugin (Script) For simple browser-only plugins, skip the backend module entirely and point `script` at a plain JS/TS file: ```json { "id": "my-script", "name": "My Script Plugin", "version": "1.0.0", "script": "./index.js" } ``` ```javascript // index.js — loaded as a