import { MemoryStorage } from './interfaces.js'; const DEFAULT_INSTALLED_PLUGINS_STORAGE_KEY = 'plugin-manager:installed-plugins'; /** * Coordinates marketplace reads with host-specific install and enable state. */ export class PluginMarketplaceManager { constructor(options) { this.client = options.client; this.storage = options.storage ?? new MemoryStorage(); this.host = options.host; this.registry = options.registry; this.installedPluginsStorageKey = options.installedPluginsStorageKey ?? DEFAULT_INSTALLED_PLUGINS_STORAGE_KEY; } /** * Fetches full marketplace catalog. */ async fetchMarketplacePlugins() { return this.client.fetchPlugins(); } /** * Lists installed plugins merged with persisted enabled state. */ async listInstalledPlugins() { const storedPlugins = this.readStoredInstalledPlugins(); if (!this.host?.listInstalledPlugins) { return storedPlugins; } const hostPlugins = await this.host.listInstalledPlugins(); const mergedPlugins = mergeInstalledPlugins(hostPlugins, storedPlugins); this.writeStoredInstalledPlugins(mergedPlugins); return mergedPlugins; } /** * Lists installed plugins currently enabled. */ async listEnabledPlugins() { return (await this.listInstalledPlugins()).filter((plugin) => plugin.enabled); } /** * Returns whether plugin is recorded as installed. */ async isPluginInstalled(pluginId) { return (await this.listInstalledPlugins()).some((plugin) => plugin.id === pluginId); } /** * Installs marketplace plugin via host adapter and persists state. */ async installPlugin(plugin) { await this.host?.installPlugin?.(plugin); const installedPlugin = { ...plugin, installedDate: new Date().toISOString(), enabled: true, }; const updatedPlugins = upsertInstalledPlugin(this.readStoredInstalledPlugins(), installedPlugin); this.writeStoredInstalledPlugins(updatedPlugins); return updatedPlugins.find((entry) => entry.id === plugin.id) ?? installedPlugin; } /** * Removes plugin from host and clears persisted install state. */ async uninstallPlugin(pluginId) { if (this.registry) { await this.registry.unregisterPlugin(pluginId); } await this.host?.uninstallPlugin?.(pluginId); this.writeStoredInstalledPlugins(this.readStoredInstalledPlugins().filter((plugin) => plugin.id !== pluginId)); } /** * Enables or disables installed plugin. * * Disabling triggers `registry.unregisterPlugin` when registry exists. * Enabling only updates persisted state; host controls actual load timing. */ async setPluginEnabled(pluginId, enabled) { if (!enabled && this.registry) { await this.registry.unregisterPlugin(pluginId); } const installedPlugins = await this.listInstalledPlugins(); const targetPlugin = installedPlugins.find((plugin) => plugin.id === pluginId); if (!targetPlugin) { return null; } const updatedPlugin = { ...targetPlugin, enabled }; this.writeStoredInstalledPlugins(upsertInstalledPlugin(installedPlugins, updatedPlugin)); return updatedPlugin; } readStoredInstalledPlugins() { const rawValue = this.storage.getItem(this.installedPluginsStorageKey); if (!rawValue) { return []; } try { const parsedValue = JSON.parse(rawValue); if (!Array.isArray(parsedValue)) { return []; } return parsedValue .map((entry) => normalizeInstalledPlugin(entry)) .filter((entry) => entry !== null); } catch { return []; } } writeStoredInstalledPlugins(plugins) { this.storage.setItem(this.installedPluginsStorageKey, JSON.stringify(plugins)); } } function mergeInstalledPlugins(hostPlugins, storedPlugins) { const storedPluginsById = new Map(storedPlugins.map((plugin) => [plugin.id, plugin])); const discoveredAt = new Date().toISOString(); return hostPlugins.map((plugin) => { const storedPlugin = storedPluginsById.get(plugin.id); return { id: plugin.id, name: pickString(plugin.name, storedPlugin?.name), version: pickString(plugin.version, storedPlugin?.version), description: pickString(plugin.description, storedPlugin?.description), author: pickString(plugin.author, storedPlugin?.author), repository: pickString(plugin.repository, storedPlugin?.repository), thumbnail: pickString(plugin.thumbnail, storedPlugin?.thumbnail), downloadUrl: pickString(plugin.downloadUrl, storedPlugin?.downloadUrl), homepage: pickString(plugin.homepage, storedPlugin?.homepage), tags: Array.isArray(plugin.tags) ? plugin.tags.filter((entry) => typeof entry === 'string') : storedPlugin?.tags ?? [], addedDate: pickString(plugin.addedDate, storedPlugin?.addedDate), installedDate: pickString(plugin.installedDate, storedPlugin?.installedDate) || discoveredAt, enabled: typeof plugin.enabled === 'boolean' ? plugin.enabled : storedPlugin?.enabled ?? true, }; }); } function upsertInstalledPlugin(plugins, nextPlugin) { const filteredPlugins = plugins.filter((plugin) => plugin.id !== nextPlugin.id); return [...filteredPlugins, nextPlugin]; } function normalizeInstalledPlugin(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) { return null; } const record = value; const id = typeof record.id === 'string' ? record.id : ''; if (!id) { return null; } return { id, name: typeof record.name === 'string' ? record.name : '', version: typeof record.version === 'string' ? record.version : '', description: typeof record.description === 'string' ? record.description : '', author: typeof record.author === 'string' ? record.author : '', repository: typeof record.repository === 'string' ? record.repository : '', thumbnail: typeof record.thumbnail === 'string' ? record.thumbnail : '', downloadUrl: typeof record.downloadUrl === 'string' ? record.downloadUrl : '', homepage: typeof record.homepage === 'string' ? record.homepage : '', tags: Array.isArray(record.tags) ? record.tags.filter((entry) => typeof entry === 'string') : [], addedDate: typeof record.addedDate === 'string' ? record.addedDate : '', installedDate: typeof record.installedDate === 'string' ? record.installedDate : '', enabled: typeof record.enabled === 'boolean' ? record.enabled : true, }; } function pickString(value, fallback = '') { return typeof value === 'string' && value.length > 0 ? value : fallback; } //# sourceMappingURL=PluginMarketplaceManager.js.map