Add categories and authors metadata to plugins

Introduces optional 'categories' and 'authors' fields to plugin manifests, updates plugin-manager.js to normalize and expose these fields, and enhances the settings UI to display plugin tags and authors. Also updates documentation and an example plugin manifest to demonstrate the new fields.
This commit is contained in:
2025-09-09 21:13:27 +12:00
parent 0b0bf27028
commit 0a26ecccd5
6 changed files with 48 additions and 8 deletions
+4
View File
@@ -22,6 +22,8 @@ Example:
"description": "What it does", "description": "What it does",
"main": "main.js", "main": "main.js",
"rendererPreload": "renderer-preload.js", "rendererPreload": "renderer-preload.js",
"categories": ["Search", "Productivity"],
"authors": ["Jane Doe", { "name": "Acme Labs", "email": "oss@acme.example" }],
"enabled": true "enabled": true
} }
``` ```
@@ -30,6 +32,8 @@ Fields:
- id: Unique id. Defaults to folder name if omitted. - id: Unique id. Defaults to folder name if omitted.
- main: Optional entry for main process integration. - main: Optional entry for main process integration.
- rendererPreload: Optional file injected into the preload. Use it to expose limited APIs. - rendererPreload: Optional file injected into the preload. Use it to expose limited APIs.
- categories: Optional string or array of strings used for organizing/filtering plugins in UI and APIs. Example: ["AI", "Utilities"].
- authors: Optional string or array of strings/objects describing authors. Objects support { name, email, url }. In APIs/UI, names are displayed.
- enabled: Defaults to true. - enabled: Defaults to true.
## Main process API (activate) ## Main process API (activate)
+1 -6
View File
@@ -1,8 +1,3 @@
[ [
{
"title": "Discord",
"url": "discord.gg",
"icon": "data:image/svg+xml;utf8,%3Csvg%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EDiscord%3C%2Ftitle%3E%3Cpath%20d%3D%22M20.317%204.3698a19.7913%2019.7913%200%2000-4.8851-1.5152.0741.0741%200%2000-.0785.0371c-.211.3753-.4447.8648-.6083%201.2495-1.8447-.2762-3.68-.2762-5.4868%200-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077%200%2000-.0785-.037%2019.7363%2019.7363%200%2000-4.8852%201.515.0699.0699%200%2000-.0321.0277C.5334%209.0458-.319%2013.5799.0992%2018.0578a.0824.0824%200%2000.0312.0561c2.0528%201.5076%204.0413%202.4228%205.9929%203.0294a.0777.0777%200%2000.0842-.0276c.4616-.6304.8731-1.2952%201.226-1.9942a.076.076%200%2000-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077%200%2001-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743%200%2001.0776-.0105c3.9278%201.7933%208.18%201.7933%2012.0614%200a.0739.0739%200%2001.0785.0095c.1202.099.246.1981.3728.2924a.077.077%200%2001-.0066.1276%2012.2986%2012.2986%200%2001-1.873.8914.0766.0766%200%2000-.0407.1067c.3604.698.7719%201.3628%201.225%201.9932a.076.076%200%2000.0842.0286c1.961-.6067%203.9495-1.5219%206.0023-3.0294a.077.077%200%2000.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061%200%2000-.0312-.0286zM8.02%2015.3312c-1.1825%200-2.1569-1.0857-2.1569-2.419%200-1.3332.9555-2.4189%202.157-2.4189%201.2108%200%202.1757%201.0952%202.1568%202.419%200%201.3332-.9555%202.4189-2.1569%202.4189zm7.9748%200c-1.1825%200-2.1569-1.0857-2.1569-2.419%200-1.3332.9554-2.4189%202.1569-2.4189%201.2108%200%202.1757%201.0952%202.1568%202.419%200%201.3332-.946%202.4189-2.1568%202.4189Z%22%2F%3E%3C%2Fsvg%3E",
"iconSet": "simple"
}
] ]
+27 -1
View File
@@ -42,7 +42,19 @@ class PluginManager {
const dir = path.join(root, ent.name); const dir = path.join(root, ent.name);
const manifestPath = path.join(dir, 'plugin.json'); const manifestPath = path.join(dir, 'plugin.json');
let manifest; let manifest;
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { continue; } try {
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
// Normalize optional fields
const cats = manifest.categories;
if (typeof cats === 'string') manifest.categories = [cats];
else if (Array.isArray(cats)) manifest.categories = cats.filter(x => typeof x === 'string');
else if (cats == null) manifest.categories = [];
const au = manifest.authors;
if (typeof au === 'string') manifest.authors = [au];
else if (Array.isArray(au)) manifest.authors = au.filter(x => (typeof x === 'string') || (x && typeof x === 'object' && typeof x.name === 'string'));
else if (au == null) manifest.authors = [];
} catch { continue; }
const enabled = manifest.enabled !== false; // default true const enabled = manifest.enabled !== false; // default true
const id = manifest.id || ent.name; const id = manifest.id || ent.name;
const record = { id, dir, manifest, enabled, mod: null, mainPath: null }; const record = { id, dir, manifest, enabled, mod: null, mainPath: null };
@@ -171,6 +183,10 @@ class PluginManager {
name: p.manifest.name || p.id, name: p.manifest.name || p.id,
version: p.manifest.version || '0.0.0', version: p.manifest.version || '0.0.0',
description: p.manifest.description || '', description: p.manifest.description || '',
categories: Array.isArray(p.manifest.categories) ? p.manifest.categories : [],
authors: Array.isArray(p.manifest.authors)
? p.manifest.authors.map(x => (typeof x === 'string' ? x : (x && x.name) || '')).filter(Boolean)
: [],
enabled: !!p.enabled, enabled: !!p.enabled,
hasMain: !!p.manifest.main, hasMain: !!p.manifest.main,
hasRendererPreload: !!p.manifest.rendererPreload, hasRendererPreload: !!p.manifest.rendererPreload,
@@ -190,11 +206,21 @@ class PluginManager {
const manifestPath = path.join(dir, 'plugin.json'); const manifestPath = path.join(dir, 'plugin.json');
try { try {
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const cats = manifest.categories;
const categories = typeof cats === 'string' ? [cats] : Array.isArray(cats) ? cats.filter(x => typeof x === 'string') : [];
const au = manifest.authors;
const authors = typeof au === 'string'
? [au]
: Array.isArray(au)
? au.map(x => (typeof x === 'string' ? x : (x && x.name) || null)).filter(Boolean)
: [];
out.push({ out.push({
id: manifest.id || ent.name, id: manifest.id || ent.name,
name: manifest.name || ent.name, name: manifest.name || ent.name,
version: manifest.version || '0.0.0', version: manifest.version || '0.0.0',
description: manifest.description || '', description: manifest.description || '',
categories,
authors,
enabled: manifest.enabled !== false, enabled: manifest.enabled !== false,
hasMain: !!manifest.main, hasMain: !!manifest.main,
hasRendererPreload: !!manifest.rendererPreload, hasRendererPreload: !!manifest.rendererPreload,
+6 -1
View File
@@ -1,9 +1,14 @@
{ {
"id": "ollama-chat", "id": "nebot-chat",
"name": "Nebot", "name": "Nebot",
"version": "0.1.0", "version": "0.1.0",
"description": "Nebot: a floating chat panel that talks to a local/remote Ollama server and saves chats in the plugin folder.", "description": "Nebot: a floating chat panel that talks to a local/remote Ollama server and saves chats in the plugin folder.",
"main": "main.js", "main": "main.js",
"rendererPreload": "renderer-preload.js", "rendererPreload": "renderer-preload.js",
"categories": ["AI", "Chat", "Utilities"],
"authors": [
{ "name": "Nebula Team", "email": "andrewzambazos@gmail.com" },
"Bobbybear007"
],
"enabled": true "enabled": true
} }
+4
View File
@@ -8,6 +8,10 @@
.plugin-desc { opacity:.8; font-size:.9em; } .plugin-desc { opacity:.8; font-size:.9em; }
.plugin-actions { display:flex; gap:8px; align-items:center; } .plugin-actions { display:flex; gap:8px; align-items:center; }
.plugin-actions .spacer { width:8px; } .plugin-actions .spacer { width:8px; }
.plugin-tags { display:flex; flex-wrap: wrap; gap:6px; margin-top: 4px; }
.plugin-tag { display:inline-flex; align-items:center; padding:2px 8px; border-radius:999px; font-size:.75em; opacity:.9; border:1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.06); }
.plugin-authors { margin-top: 4px; font-size:.85em; opacity:.85; }
.plugin-authors .muted { opacity:.7; margin-right: 6px; }
:root { :root {
--bg: #121418; --bg: #121418;
--dark-blue: #0B1C2B; --dark-blue: #0B1C2B;
+6
View File
@@ -355,6 +355,10 @@ async function loadPluginsUI() {
listEl.appendChild(empty); listEl.appendChild(empty);
} else { } else {
for (const p of items) { for (const p of items) {
const categories = Array.isArray(p.categories) ? p.categories.filter(x => x && typeof x === 'string') : [];
const authors = Array.isArray(p.authors) ? p.authors.filter(x => x && typeof x === 'string') : [];
const tagsHtml = categories.length ? `<div class="plugin-tags">${categories.map(c => `<span class=\"plugin-tag\">${escapeHtml(c)}</span>`).join('')}</div>` : '';
const authorsHtml = authors.length ? `<div class=\"plugin-authors\"><span class=\"muted\">Authors:</span> ${authors.map(a => `<span class=\"plugin-author\">${escapeHtml(a)}</span>`).join(', ')}</div>` : '';
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'plugin-item'; row.className = 'plugin-item';
row.setAttribute('role', 'listitem'); row.setAttribute('role', 'listitem');
@@ -362,6 +366,8 @@ async function loadPluginsUI() {
<div class="plugin-meta"> <div class="plugin-meta">
<div class="plugin-title">${escapeHtml(p.name)} <span style="opacity:.7;font-weight:400">v${escapeHtml(p.version)}</span></div> <div class="plugin-title">${escapeHtml(p.name)} <span style="opacity:.7;font-weight:400">v${escapeHtml(p.version)}</span></div>
<div class="plugin-desc">${escapeHtml(p.description || '')}</div> <div class="plugin-desc">${escapeHtml(p.description || '')}</div>
${tagsHtml}
${authorsHtml}
<div class="plugin-desc" style="opacity:.6; font-size:.85em;">${escapeHtml(p.dir)}</div> <div class="plugin-desc" style="opacity:.6; font-size:.85em;">${escapeHtml(p.dir)}</div>
</div> </div>
<div class="plugin-actions"> <div class="plugin-actions">