'use strict' const fs = require('fs') const path = require('path') const { execute, queryOne, query } = require('../database/postgres') const PluginConfigStore = require('./config') // ─── Tabelas já criadas no schema.sql ─────────────────────────────────────── // plugins e plugin_configs existem — sem necessidade de CREATE aqui. // ─── PluginManager ─────────────────────────────────────────────────────────── class PluginManager { constructor(app, dbAccess, httpServer = null) { this.app = app this.db = dbAccess // { query, queryOne, execute } do postgres.js this.httpServer = httpServer // http.Server nativo — para proxy WS this.plugins = new Map() // id → { manifest, instance, active } } // Carrega todos os plugins encontrados em plugins/*/ async loadAll() { const dir = __dirname const entries = fs.readdirSync(dir, { withFileTypes: true }) for (const entry of entries) { if (!entry.isDirectory()) continue const pluginDir = path.join(dir, entry.name) const manifestPath = path.join(pluginDir, 'manifest.json') if (!fs.existsSync(manifestPath)) continue try { const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) await this._register(manifest, pluginDir) } catch (err) { console.error(`[plugins] Erro ao carregar "${entry.name}":`, err.message) } } } // Registra um plugin: garante linha na tabela e ativa se estava ativo async _register(manifest, pluginDir) { const { id, name, version, active_by_default = false } = manifest // Garante registro na tabela (upsert) await execute( `INSERT INTO plugins (id, active) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING`, [id, active_by_default ? 1 : 0] ) const record = await queryOne('SELECT active FROM plugins WHERE id = $1', [id]) const instance = require(path.join(pluginDir, 'index.js')) this.plugins.set(id, { manifest, instance, active: false }) if (record && record.active) { await this.activate(id) } const status = (record && record.active) ? '✓ ativo' : '○ inativo' console.log(`[plugins] ${name} v${version} — ${status}`) } // Ativa um plugin async activate(id) { const plugin = this.plugins.get(id) if (!plugin) throw new Error(`Plugin "${id}" não encontrado`) if (plugin.active) return const ctx = this._buildContext(id) await plugin.instance.activate(ctx) plugin.active = true await execute('UPDATE plugins SET active = 1 WHERE id = $1', [id]) } // Desativa um plugin async deactivate(id) { const plugin = this.plugins.get(id) if (!plugin) throw new Error(`Plugin "${id}" não encontrado`) if (!plugin.active) return const ctx = this._buildContext(id) if (typeof plugin.instance.deactivate === 'function') { await plugin.instance.deactivate(ctx) } plugin.active = false await execute('UPDATE plugins SET active = 0 WHERE id = $1', [id]) } // Contexto injetado nos plugins durante activate/deactivate _buildContext(id) { return { app: this.app, db: this.db, httpServer: this.httpServer, // http.Server nativo (pode ser null) config: new PluginConfigStore(id), logger: { info: (msg) => console.log(`[${id}] ${msg}`), warn: (msg) => console.warn(`[${id}] WARN: ${msg}`), error: (msg) => console.error(`[${id}] ERR: ${msg}`), }, } } // Lista todos os plugins async list() { const records = await query('SELECT id, active, installed_at FROM plugins') return records.map((r) => { const p = this.plugins.get(r.id) return { id: r.id, active: !!r.active, installed_at: r.installed_at, name: p?.manifest?.name ?? r.id, version: p?.manifest?.version ?? '?', description: p?.manifest?.description ?? '', config_schema: p?.manifest?.config_schema ?? [], features: p?.manifest?.features ?? [], ui_path: p?.manifest?.ui_path ?? null, } }) } // Retorna configuração pública de um plugin async getConfig(id) { const store = new PluginConfigStore(id) return store.getAll() } // Salva múltiplos pares key/value de uma vez async setConfig(id, values) { const store = new PluginConfigStore(id) for (const [k, v] of Object.entries(values)) { await store.set(k, v) } } } module.exports = PluginManager