141 lines
4.7 KiB
JavaScript
141 lines
4.7 KiB
JavaScript
'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
|