/** * Renderer process entry point for the plugin-manager Electron test harness. * * Uses a dynamic import() to load the ESM @paarrot/plugin-manager dist, * then loads the example plugin from disk and demonstrates every API surface. */ (async () => { const { pathToFileURL } = require('url'); const path = require('path'); const fs = require('fs'); // Resolve the plugin-manager dist entry using a file:// URL so Electron can // import an ESM module located inside node_modules. const pmEntry = path.join( __dirname, 'node_modules', '@paarrot', 'plugin-manager', 'dist', 'index.js' ); const { PluginRegistry, createPluginContext } = await import(pathToFileURL(pmEntry).href); // --------------------------------------------------------------------------- // Logging helper — writes to the on-screen log panel // --------------------------------------------------------------------------- const logEl = document.getElementById('log'); /** @param {'log'|'info'|'success'|'error'|'warn'} type */ function addLog(message, type = 'log') { const entry = document.createElement('div'); entry.className = `log-entry log-${type}`; entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; logEl.appendChild(entry); logEl.scrollTop = logEl.scrollHeight; } // --------------------------------------------------------------------------- // Registry // --------------------------------------------------------------------------- const registry = new PluginRegistry({ storage: localStorage, onThemeRegistered: (themeId, _className, css) => { const el = document.createElement('style'); el.id = `plugin-theme-${themeId}`; el.textContent = css; document.head.appendChild(el); addLog(`Theme registered: ${themeId}`, 'info'); }, onThemeUnregistered: (themeId) => { document.getElementById(`plugin-theme-${themeId}`)?.remove(); addLog(`Theme unregistered: ${themeId}`, 'info'); }, }); // --------------------------------------------------------------------------- // Load the example plugin from disk (CJS eval — same pattern as cinny) // --------------------------------------------------------------------------- const pluginPath = path.join(__dirname, 'plugins', 'example-plugin', 'index.js'); const pluginCode = fs.readFileSync(pluginPath, 'utf-8'); const pluginExports = {}; const mod = { exports: pluginExports }; // eslint-disable-next-line no-new-func new Function('module', 'exports', pluginCode)(mod, pluginExports); const plugin = mod.exports; // --------------------------------------------------------------------------- // Simple in-process event emitter (stands in for a real client like Matrix) // --------------------------------------------------------------------------- const eventEmitter = { _handlers: {}, on(event, handler) { (this._handlers[event] ??= []).push(handler); }, off(event, handler) { this._handlers[event] = (this._handlers[event] ?? []).filter(h => h !== handler); }, emit(event, data) { (this._handlers[event] ?? []).forEach(h => h(data)); }, }; // --------------------------------------------------------------------------- // Plugin context // --------------------------------------------------------------------------- const context = createPluginContext( { pluginId: 'example-plugin', eventClient: eventEmitter, onNotify: (opts) => addLog(`[${opts.type?.toUpperCase() ?? 'NOTIFY'}] ${opts.title}: ${opts.body}`, 'info'), }, registry ); // Intercept context logging so it appears in the UI panel context.log = (...args) => { console.log('[Plugin example-plugin]', ...args); registry.addLog('example-plugin', 'log', args); addLog(args.map(String).join(' '), 'log'); }; // --------------------------------------------------------------------------- // Register + load // --------------------------------------------------------------------------- registry.registerPlugin('example-plugin', plugin, context); await plugin.onLoad(context); document.getElementById('status-badge').textContent = 'example-plugin loaded'; document.getElementById('status-badge').classList.add('loaded'); addLog('example-plugin loaded successfully', 'success'); // --------------------------------------------------------------------------- // Plugin Manager panel // --------------------------------------------------------------------------- /** Renders the plugin directory card list. */ function refreshPluginList() { const listEl = document.getElementById('plugin-list'); const plugins = registry.getRegisteredPlugins(); if (!plugins.length) { listEl.innerHTML = '
No plugins loaded
'; return; } listEl.innerHTML = plugins.map(p => `
${p.name} v${p.version}
${p.id}
loaded ${p.commandCount ? `${p.commandCount} cmd${p.commandCount !== 1 ? 's' : ''}` : ''} ${p.interceptorCount ? `${p.interceptorCount} interceptor${p.interceptorCount !== 1 ? 's' : ''}` : ''} ${p.themeCount ? `${p.themeCount} theme${p.themeCount !== 1 ? 's' : ''}` : ''} ${p.hasSettings ? `has settings` : ''}
`).join(''); listEl.querySelectorAll('.plugin-unload-btn').forEach(btn => { btn.addEventListener('click', async () => { const id = btn.dataset.id; await registry.unregisterPlugin(id); addLog(`Plugin unloaded: ${id}`, 'warn'); refreshPluginList(); refreshCommandList(); renderSettings(); }); }); } /** * Loads a plugin from an arbitrary file path entered in the Load input. * Supports CJS modules (same eval pattern as cinny-desktop). * @param {string} filePath */ async function loadPluginFromPath(filePath) { try { const code = fs.readFileSync(filePath, 'utf-8'); const m = { exports: {} }; // eslint-disable-next-line no-new-func new Function('module', 'exports', code)(m, m.exports); const p = m.exports; if (!p || typeof p.onLoad !== 'function') { throw new Error('File does not export a valid plugin (needs an onLoad function)'); } const id = p.id ?? path.basename(path.dirname(filePath)); if (registry.getRegisteredPlugins().some(r => r.id === id)) { throw new Error(`Plugin with id "${id}" is already loaded`); } const ctx = createPluginContext( { pluginId: id, eventClient: eventEmitter, onNotify: (opts) => addLog(`[${opts.type?.toUpperCase() ?? 'NOTIFY'}] ${opts.title}: ${opts.body}`, 'info'), }, registry ); ctx.log = (...args) => { console.log(`[Plugin ${id}]`, ...args); registry.addLog(id, 'log', args); addLog(args.map(String).join(' '), 'log'); }; registry.registerPlugin(id, p, ctx); await p.onLoad(ctx); addLog(`Plugin loaded from path: ${id}`, 'success'); refreshPluginList(); refreshCommandList(); renderSettings(); } catch (err) { addLog(`Failed to load plugin: ${err.message}`, 'error'); } } document.getElementById('load-plugin-btn').addEventListener('click', () => { const filePath = document.getElementById('load-plugin-path').value.trim(); if (!filePath) { addLog('Enter a path to a plugin index.js', 'error'); return; } loadPluginFromPath(filePath); document.getElementById('load-plugin-path').value = ''; }); document.getElementById('load-plugin-path').addEventListener('keydown', (e) => { if (e.key === 'Enter') document.getElementById('load-plugin-btn').click(); }); refreshPluginList(); // Populate registered command list function refreshCommandList() { const cmdListEl = document.getElementById('cmd-list'); cmdListEl.innerHTML = registry .getCommands() .map(c => `
  • /${c.name} — ${c.command.description ?? ''}
  • `) .join('') || '
  • No commands registered
  • '; } const cmdListEl = document.getElementById('cmd-list'); cmdListEl.innerHTML = registry .getCommands() .map(c => `
  • /${c.name} — ${c.command.description ?? ''}
  • `) .join(''); // Render settings form renderSettings(); // --------------------------------------------------------------------------- // UI — Commands // --------------------------------------------------------------------------- const cmdInput = document.getElementById('cmd-input'); const cmdResult = document.getElementById('cmd-result'); document.getElementById('cmd-run').addEventListener('click', async () => { const raw = cmdInput.value.trim(); if (!raw.startsWith('/')) { addLog('Commands must start with /', 'error'); return; } const [name, ...rest] = raw.slice(1).split(' '); try { const result = await registry.executeCommand(name, rest.join(' ')); cmdResult.textContent = result ?? '(no output)'; addLog(`/${name}: ${result ?? '(no output)'}`, 'success'); } catch (err) { cmdResult.textContent = err.message; addLog(err.message, 'error'); } cmdInput.value = ''; }); cmdInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') document.getElementById('cmd-run').click(); }); // --------------------------------------------------------------------------- // UI — Message Interceptor // --------------------------------------------------------------------------- document.getElementById('msg-send').addEventListener('click', async () => { const content = document.getElementById('msg-input').value; const msg = { content, roomId: 'test-room', eventType: 'm.room.message' }; const processed = await registry.processBeforeSend(msg); document.getElementById('msg-result').textContent = `Original: "${content}"\nProcessed: "${processed.content}"`; addLog(`Intercepted: "${content}" → "${processed.content}"`, 'log'); }); // --------------------------------------------------------------------------- // UI — Theme // --------------------------------------------------------------------------- document.getElementById('theme-apply').addEventListener('click', () => { const themes = registry.getPluginThemes(); if (!themes.length) { addLog('No plugin themes registered', 'error'); return; } const theme = themes[0]; document.body.className = theme.className; addLog(`Applied theme: ${theme.name}`, 'success'); }); document.getElementById('theme-reset').addEventListener('click', () => { document.body.className = ''; addLog('Theme reset', 'log'); }); // --------------------------------------------------------------------------- // UI — Custom Events // --------------------------------------------------------------------------- document.getElementById('event-fire').addEventListener('click', () => { const payload = { from: 'UI button', timestamp: Date.now() }; eventEmitter.emit('test-event', payload); addLog(`Fired test-event with payload: ${JSON.stringify(payload)}`, 'log'); }); // --------------------------------------------------------------------------- // Settings renderer // --------------------------------------------------------------------------- function renderSettings() { const schema = registry.getPluginSettingsSchema('example-plugin'); if (!schema) return; const container = document.getElementById('settings-container'); container.innerHTML = ''; for (const [key, def] of Object.entries(schema)) { const value = registry.getPluginSetting('example-plugin', key); const row = document.createElement('div'); row.className = 'setting-row'; const label = document.createElement('label'); label.textContent = def.label ?? key; row.appendChild(label); let input; if (def.type === 'boolean') { input = document.createElement('input'); input.type = 'checkbox'; input.checked = !!value; input.addEventListener('change', () => { registry.setPluginSetting('example-plugin', key, input.checked); addLog(`Setting "${key}" = ${input.checked}`, 'log'); }); } else if (def.type === 'select') { input = document.createElement('select'); (def.options ?? []).forEach((opt) => { const o = document.createElement('option'); o.value = opt.value; o.textContent = opt.label; if (opt.value === value) o.selected = true; input.appendChild(o); }); input.addEventListener('change', () => { registry.setPluginSetting('example-plugin', key, input.value); addLog(`Setting "${key}" = ${input.value}`, 'log'); }); } else { input = document.createElement('input'); input.type = 'text'; input.value = value ?? ''; input.addEventListener('change', () => { registry.setPluginSetting('example-plugin', key, input.value); addLog(`Setting "${key}" = "${input.value}"`, 'log'); }); } row.appendChild(input); container.appendChild(row); } } })();