Add getRegisteredPlugins, plugin manager panel in test app
This commit is contained in:
@@ -40,13 +40,19 @@
|
||||
main {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
/* left: plugin manager, center: commands+interceptor, right: settings+log */
|
||||
grid-template-columns: 280px 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
gap: 1px;
|
||||
background: #0f3460;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Plugin manager spans both rows */
|
||||
#panel-manager {
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #1a1a2e;
|
||||
display: flex;
|
||||
@@ -155,6 +161,55 @@
|
||||
|
||||
/* Theme section */
|
||||
.theme-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
/* Plugin manager cards */
|
||||
.plugin-card {
|
||||
background: #0f3460;
|
||||
border: 1px solid #1e3a6e;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.plugin-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
}
|
||||
.plugin-name { font-weight: 700; font-size: 13px; color: #e0e0e0; }
|
||||
.plugin-version { font-size: 10px; color: #7986cb; background: #162250; padding: 1px 6px; border-radius: 8px; }
|
||||
.plugin-badges { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 2px; }
|
||||
.badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
background: #162250;
|
||||
color: #90caf9;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.badge.green { background: #1a4a2e; color: #4caf50; }
|
||||
.badge.red { background: #4a1a1a; color: #e94560; }
|
||||
.badge.amber { background: #4a3a00; color: #ffb300; }
|
||||
|
||||
.plugin-unload-btn {
|
||||
font-size: 10px;
|
||||
padding: 2px 8px;
|
||||
background: #3a0f20;
|
||||
color: #e94560;
|
||||
border-radius: 4px;
|
||||
margin-top: 2px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.plugin-unload-btn:hover { background: #5a1a30; }
|
||||
|
||||
/* Load-plugin section */
|
||||
#load-plugin-row { display: flex; gap: 6px; }
|
||||
#load-plugin-row input { flex: 1; font-size: 11px; }
|
||||
#load-plugin-row button { white-space: nowrap; font-size: 11px; padding: 4px 10px; }
|
||||
|
||||
#no-plugins-msg { color: #555; font-size: 12px; text-align: center; padding: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -164,7 +219,21 @@
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Top-left: Commands -->
|
||||
<!-- Left: Plugin Manager (spans both rows) -->
|
||||
<div class="panel" id="panel-manager">
|
||||
<div class="panel-header">Plugin Manager</div>
|
||||
<div class="panel-body">
|
||||
<div style="font-size:11px;color:#7986cb;margin-bottom:2px;">Load plugin from file:</div>
|
||||
<div id="load-plugin-row">
|
||||
<input type="text" id="load-plugin-path" placeholder="/path/to/plugin/index.js" />
|
||||
<button class="primary" id="load-plugin-btn">Load</button>
|
||||
</div>
|
||||
<div style="margin-top:10px;font-size:11px;color:#7986cb;margin-bottom:4px;">Registered plugins:</div>
|
||||
<div id="plugin-list"><div id="no-plugins-msg">No plugins loaded</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top-center: Commands -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">Commands</div>
|
||||
<div class="panel-body">
|
||||
@@ -195,7 +264,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom-left: Settings + Theme -->
|
||||
<!-- Bottom-center: Settings + Theme -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">Settings & Theme</div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -105,7 +105,121 @@
|
||||
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 = '<div id="no-plugins-msg" style="color:#555;font-size:12px;text-align:center;padding:20px 0;">No plugins loaded</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = plugins.map(p => `
|
||||
<div class="plugin-card" data-id="${p.id}">
|
||||
<div class="plugin-card-header">
|
||||
<span class="plugin-name">${p.name}</span>
|
||||
<span class="plugin-version">v${p.version}</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#7986cb;font-family:monospace;">${p.id}</div>
|
||||
<div class="plugin-badges">
|
||||
<span class="badge green">loaded</span>
|
||||
${p.commandCount ? `<span class="badge">${p.commandCount} cmd${p.commandCount !== 1 ? 's' : ''}</span>` : ''}
|
||||
${p.interceptorCount ? `<span class="badge">${p.interceptorCount} interceptor${p.interceptorCount !== 1 ? 's' : ''}</span>` : ''}
|
||||
${p.themeCount ? `<span class="badge amber">${p.themeCount} theme${p.themeCount !== 1 ? 's' : ''}</span>` : ''}
|
||||
${p.hasSettings ? `<span class="badge">has settings</span>` : ''}
|
||||
</div>
|
||||
<button class="plugin-unload-btn" data-id="${p.id}">Unload</button>
|
||||
</div>
|
||||
`).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 => `<li><code>/${c.name}</code> — ${c.command.description ?? ''}</li>`)
|
||||
.join('') || '<li style="color:#555">No commands registered</li>';
|
||||
}
|
||||
|
||||
const cmdListEl = document.getElementById('cmd-list');
|
||||
cmdListEl.innerHTML = registry
|
||||
.getCommands()
|
||||
|
||||
Reference in New Issue
Block a user