import fs from 'fs'; import path from 'path'; import { Express, Router } from 'express'; import db from '../config/database'; import logger from '../utils/logger'; export interface PluginManifest { name: string; slug: string; version: string; description: string; author: string; entryPoint: string; dependencies?: string[]; hooks?: string[]; } export interface PluginInstance { manifest: PluginManifest; router?: Router; hooks: Record; activate: () => Promise; deactivate: () => Promise; } const loadedPlugins: Map = new Map(); const hookRegistry: Record = {}; const PLUGINS_DIR = path.resolve(__dirname, '../../../plugins'); export const pluginService = { async loadAll(app: Express) { if (!fs.existsSync(PLUGINS_DIR)) { fs.mkdirSync(PLUGINS_DIR, { recursive: true }); return; } const activePlugins = await db('plugins').where('active', true); for (const pluginRecord of activePlugins) { try { await this.load(app, pluginRecord.slug); } catch (err) { logger.error(`Failed to load plugin: ${pluginRecord.slug}`, err); } } logger.info(`Loaded ${loadedPlugins.size} plugins`); }, async load(app: Express, slug: string) { const pluginDir = path.join(PLUGINS_DIR, slug); const manifestPath = path.join(pluginDir, 'manifest.json'); if (!fs.existsSync(manifestPath)) { throw new Error(`Plugin manifest not found: ${manifestPath}`); } const manifest: PluginManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); const entryPath = path.join(pluginDir, manifest.entryPoint || 'index.js'); if (!fs.existsSync(entryPath)) { throw new Error(`Plugin entry point not found: ${entryPath}`); } const pluginModule = require(entryPath); const instance: PluginInstance = { manifest, router: pluginModule.router, hooks: pluginModule.hooks || {}, activate: pluginModule.activate || (async () => { }), deactivate: pluginModule.deactivate || (async () => { }), }; // Register routes if (instance.router) { app.use(`/api/plugins/${slug}`, instance.router); } // Register hooks for (const [hookName, handlers] of Object.entries(instance.hooks)) { if (!hookRegistry[hookName]) hookRegistry[hookName] = []; if (Array.isArray(handlers)) { hookRegistry[hookName].push(...handlers); } } await instance.activate(); loadedPlugins.set(slug, instance); logger.info(`Plugin loaded: ${manifest.name} v${manifest.version}`); }, async unload(slug: string) { const instance = loadedPlugins.get(slug); if (instance) { await instance.deactivate(); loadedPlugins.delete(slug); logger.info(`Plugin unloaded: ${slug}`); } }, async install(slug: string) { const pluginDir = path.join(PLUGINS_DIR, slug); const manifestPath = path.join(pluginDir, 'manifest.json'); if (!fs.existsSync(manifestPath)) { throw new Error('Manifest not found'); } const manifest: PluginManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); const existing = await db('plugins').where('slug', slug).first(); if (existing) { await db('plugins').where('slug', slug).update({ version: manifest.version, description: manifest.description, author: manifest.author, entry_point: manifest.entryPoint, dependencies: JSON.stringify(manifest.dependencies || []), hooks: JSON.stringify(manifest.hooks || []), }); } else { await db('plugins').insert({ name: manifest.name, slug: manifest.slug, version: manifest.version, description: manifest.description, author: manifest.author, entry_point: manifest.entryPoint, active: false, dependencies: JSON.stringify(manifest.dependencies || []), hooks: JSON.stringify(manifest.hooks || []), directory: pluginDir, }); } return manifest; }, async activate(app: Express, slug: string) { await db('plugins').where('slug', slug).update({ active: true }); await this.load(app, slug); }, async deactivate(slug: string) { await db('plugins').where('slug', slug).update({ active: false }); await this.unload(slug); }, async getAll() { return db('plugins').orderBy('name'); }, async remove(slug: string) { await this.unload(slug); await db('plugins').where('slug', slug).delete(); // Note: does not delete plugin files for safety }, async triggerHook(hookName: string, data: any) { const handlers = hookRegistry[hookName] || []; for (const handler of handlers) { try { await handler(data); } catch (err) { logger.error(`Hook error [${hookName}]:`, err); } } }, getLoadedPlugins() { return Array.from(loadedPlugins.entries()).map(([slug, instance]) => ({ slug, name: instance.manifest.name, version: instance.manifest.version, })); }, };