feat: Implement Plugin Marketplace Manager and Client
- Added PluginMarketplaceManager to coordinate marketplace reads and manage installed plugins. - Introduced PluginMarketplaceClient for fetching marketplace metadata. - Implemented storage management for installed plugins using MemoryStorage. - Added methods for installing, uninstalling, and enabling/disabling plugins. - Created type definitions for PluginMarketplaceManager options and installed plugin records. - Added support for fetching full marketplace catalog and individual plugin metadata. - Included error handling for fetch operations and validation of plugin data. - Updated index.browser.ts to export new modules and types.
This commit is contained in:
@@ -14,7 +14,12 @@
|
||||
const pmEntry = path.join(
|
||||
__dirname, 'node_modules', '@paarrot', 'plugin-manager', 'dist', 'index.js'
|
||||
);
|
||||
const { PluginRegistry, createPluginContext } = await import(pathToFileURL(pmEntry).href);
|
||||
const {
|
||||
PluginMarketplaceClient,
|
||||
PluginMarketplaceManager,
|
||||
PluginRegistry,
|
||||
createPluginContext,
|
||||
} = await import(pathToFileURL(pmEntry).href);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module loader — supports both CJS (module.exports) and ESM (export default)
|
||||
@@ -149,6 +154,11 @@
|
||||
},
|
||||
});
|
||||
|
||||
const marketplaceClient = new PluginMarketplaceClient({
|
||||
indexUrl: 'https://raw.githubusercontent.com/Paarrot/Plugin-Directory/refs/heads/main/plugins/index.json',
|
||||
baseUrl: 'https://raw.githubusercontent.com/Paarrot/Plugin-Directory/refs/heads/main/plugins/',
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Load the example plugin from disk (CJS eval — same pattern as cinny)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -180,7 +190,20 @@
|
||||
* Persistent record of every plugin we have ever seen, keyed by plugin id.
|
||||
* Survives unload so the Installed list can show a Reload button.
|
||||
*
|
||||
* @type {Map<string, { name: string, version: string, pluginModule: object, filePath?: string, downloadUrl?: string }>}
|
||||
* @type {Map<string, {
|
||||
* name: string,
|
||||
* version: string,
|
||||
* pluginModule: object,
|
||||
* filePath?: string,
|
||||
* downloadUrl?: string,
|
||||
* description?: string,
|
||||
* author?: string,
|
||||
* repository?: string,
|
||||
* thumbnail?: string,
|
||||
* homepage?: string,
|
||||
* tags?: string[],
|
||||
* addedDate?: string
|
||||
* }>}
|
||||
*/
|
||||
const knownPlugins = new Map();
|
||||
|
||||
@@ -189,7 +212,19 @@
|
||||
* registers and loads the plugin, then records it in knownPlugins.
|
||||
* @param {string} id
|
||||
* @param {object} pluginModule
|
||||
* @param {{ filePath?: string, downloadUrl?: string }} [source]
|
||||
* @param {{
|
||||
* filePath?: string,
|
||||
* downloadUrl?: string,
|
||||
* name?: string,
|
||||
* version?: string,
|
||||
* description?: string,
|
||||
* author?: string,
|
||||
* repository?: string,
|
||||
* thumbnail?: string,
|
||||
* homepage?: string,
|
||||
* tags?: string[],
|
||||
* addedDate?: string,
|
||||
* }} [source]
|
||||
*/
|
||||
async function registerAndLoad(id, pluginModule, source = {}) {
|
||||
const ctx = createPluginContext(
|
||||
@@ -211,13 +246,60 @@
|
||||
await pluginModule.onLoad(ctx);
|
||||
|
||||
knownPlugins.set(id, {
|
||||
name: pluginModule.name ?? id,
|
||||
version: pluginModule.version ?? '?',
|
||||
name: source.name ?? pluginModule.name ?? id,
|
||||
version: source.version ?? pluginModule.version ?? '?',
|
||||
pluginModule,
|
||||
...source,
|
||||
});
|
||||
}
|
||||
|
||||
const marketplace = new PluginMarketplaceManager({
|
||||
client: marketplaceClient,
|
||||
storage: localStorage,
|
||||
registry,
|
||||
host: {
|
||||
listInstalledPlugins: async () => Array.from(knownPlugins.entries()).map(([id, known]) => ({
|
||||
id,
|
||||
name: known.name ?? id,
|
||||
version: known.version ?? '',
|
||||
description: known.description ?? '',
|
||||
author: known.author ?? '',
|
||||
repository: known.repository ?? '',
|
||||
thumbnail: known.thumbnail ?? '',
|
||||
downloadUrl: known.downloadUrl ?? '',
|
||||
homepage: known.homepage ?? '',
|
||||
tags: Array.isArray(known.tags) ? known.tags : [],
|
||||
addedDate: known.addedDate ?? '',
|
||||
})),
|
||||
installPlugin: async (pluginMetadata) => {
|
||||
if (!pluginMetadata.downloadUrl) {
|
||||
throw new Error(`No download URL for plugin ${pluginMetadata.id}`);
|
||||
}
|
||||
|
||||
addLog(`Downloading plugin ${pluginMetadata.id}…`, 'info');
|
||||
const code = await fetchPluginCode(pluginMetadata.downloadUrl, pluginMetadata.repository);
|
||||
const raw = await evalPluginCode(code);
|
||||
const pluginModule = normalisePlugin(raw);
|
||||
|
||||
await registerAndLoad(pluginMetadata.id, pluginModule, {
|
||||
downloadUrl: pluginMetadata.downloadUrl,
|
||||
name: pluginMetadata.name,
|
||||
version: pluginMetadata.version,
|
||||
description: pluginMetadata.description,
|
||||
author: pluginMetadata.author,
|
||||
repository: pluginMetadata.repository,
|
||||
thumbnail: pluginMetadata.thumbnail,
|
||||
homepage: pluginMetadata.homepage,
|
||||
tags: pluginMetadata.tags,
|
||||
addedDate: pluginMetadata.addedDate,
|
||||
});
|
||||
},
|
||||
uninstallPlugin: async (pluginId) => {
|
||||
knownPlugins.delete(pluginId);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await registerAndLoad('example-plugin', plugin, {
|
||||
filePath: pluginPath,
|
||||
});
|
||||
@@ -367,11 +449,6 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin Directory — fetch from remote index
|
||||
// ---------------------------------------------------------------------------
|
||||
const PLUGIN_INDEX_URL =
|
||||
'https://raw.githubusercontent.com/Paarrot/Plugin-Directory/refs/heads/main/plugins/index.json';
|
||||
const PLUGIN_BASE_URL =
|
||||
'https://raw.githubusercontent.com/Paarrot/Plugin-Directory/refs/heads/main/plugins/';
|
||||
|
||||
let directoryPlugins = [];
|
||||
const dirStatusEl = document.getElementById('dir-status');
|
||||
const dirListEl = document.getElementById('dir-list');
|
||||
@@ -415,11 +492,7 @@
|
||||
${(p.tags ?? []).map(t => `<span class="market-tag">${t}</span>`).join('')}
|
||||
${p.homepage ? `<a href="${p.homepage}" target="_blank"
|
||||
style="font-size:10px;color:#7986cb;text-decoration:none;margin-left:auto;margin-right:6px;">↗ Homepage</a>` : ''}
|
||||
<button class="market-install-btn"
|
||||
data-url="${p.downloadUrl ?? ''}"
|
||||
data-repo="${p.repository ?? ''}"
|
||||
data-id="${p.id}"
|
||||
${isLoaded ? 'disabled' : ''}>
|
||||
<button class="market-install-btn" data-id="${p.id}" ${isLoaded ? 'disabled' : ''}>
|
||||
${isLoaded ? 'Loaded' : 'Download'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -428,24 +501,21 @@
|
||||
}).join('');
|
||||
|
||||
dirListEl.querySelectorAll('.market-install-btn:not([disabled])').forEach(btn => {
|
||||
btn.addEventListener('click', () => downloadAndLoadPlugin(btn.dataset.id, btn.dataset.url, btn.dataset.repo));
|
||||
btn.addEventListener('click', () => downloadAndLoadPlugin(btn.dataset.id));
|
||||
});
|
||||
}
|
||||
|
||||
/** Downloads a plugin's index.js from the marketplace and loads it into the registry. */
|
||||
async function downloadAndLoadPlugin(pluginId, downloadUrl, repositoryUrl) {
|
||||
if (!downloadUrl) {
|
||||
addLog(`No download URL for plugin ${pluginId}`, 'error');
|
||||
async function downloadAndLoadPlugin(pluginId) {
|
||||
const pluginMetadata = directoryPlugins.find((entry) => entry.id === pluginId);
|
||||
|
||||
if (!pluginMetadata) {
|
||||
addLog(`Plugin ${pluginId} is not present in directory cache`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
addLog(`Downloading plugin ${pluginId}…`, 'info');
|
||||
const code = await fetchPluginCode(downloadUrl, repositoryUrl);
|
||||
const raw = await evalPluginCode(code);
|
||||
const p = normalisePlugin(raw);
|
||||
|
||||
await registerAndLoad(pluginId, p, { downloadUrl });
|
||||
|
||||
await marketplace.installPlugin(pluginMetadata);
|
||||
addLog(`Plugin installed from directory: ${pluginId}`, 'success');
|
||||
refreshPluginList();
|
||||
refreshCommandList();
|
||||
@@ -460,23 +530,9 @@
|
||||
async function fetchDirectory() {
|
||||
dirStatusEl.textContent = 'Fetching plugin directory…';
|
||||
dirListEl.innerHTML = '';
|
||||
|
||||
try {
|
||||
const indexRes = await fetch(PLUGIN_INDEX_URL);
|
||||
if (!indexRes.ok) throw new Error(`HTTP ${indexRes.status}`);
|
||||
const index = await indexRes.json();
|
||||
|
||||
const metas = await Promise.all(
|
||||
index.plugins.map(async (id) => {
|
||||
try {
|
||||
const r = await fetch(`${PLUGIN_BASE_URL}${id}.json`);
|
||||
return r.ok ? r.json() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
directoryPlugins = metas.filter(Boolean);
|
||||
directoryPlugins = await marketplace.fetchMarketplacePlugins();
|
||||
dirStatusEl.textContent = `${directoryPlugins.length} plugin${directoryPlugins.length !== 1 ? 's' : ''} in directory.`;
|
||||
addLog(`Plugin directory fetched: ${directoryPlugins.length} entries`, 'success');
|
||||
renderDirectoryList(dirSearchEl.value);
|
||||
|
||||
Reference in New Issue
Block a user