From dcc17946ef8da850fff4e42edf8ff89761513287 Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Sun, 19 Apr 2026 01:03:58 +1000 Subject: [PATCH] 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. --- dist/PluginMarketplaceClient.d.ts | 53 +++++ dist/PluginMarketplaceClient.d.ts.map | 1 + dist/PluginMarketplaceClient.js | 94 +++++++++ dist/PluginMarketplaceClient.js.map | 1 + dist/PluginMarketplaceManager.d.ts | 88 +++++++++ dist/PluginMarketplaceManager.d.ts.map | 1 + dist/PluginMarketplaceManager.js | 169 ++++++++++++++++ dist/PluginMarketplaceManager.js.map | 1 + dist/index.browser.d.ts | 15 ++ dist/index.browser.d.ts.map | 1 + dist/index.browser.js | 8 + dist/index.browser.js.map | 1 + dist/index.d.ts | 4 + dist/index.d.ts.map | 2 +- dist/index.js | 2 + dist/index.js.map | 2 +- package.json | 1 + src/PluginMarketplaceClient.ts | 149 ++++++++++++++ src/PluginMarketplaceManager.ts | 262 +++++++++++++++++++++++++ src/index.browser.ts | 54 +++++ src/index.ts | 13 ++ test/electron/renderer.js | 140 +++++++++---- 22 files changed, 1018 insertions(+), 44 deletions(-) create mode 100644 dist/PluginMarketplaceClient.d.ts create mode 100644 dist/PluginMarketplaceClient.d.ts.map create mode 100644 dist/PluginMarketplaceClient.js create mode 100644 dist/PluginMarketplaceClient.js.map create mode 100644 dist/PluginMarketplaceManager.d.ts create mode 100644 dist/PluginMarketplaceManager.d.ts.map create mode 100644 dist/PluginMarketplaceManager.js create mode 100644 dist/PluginMarketplaceManager.js.map create mode 100644 dist/index.browser.d.ts create mode 100644 dist/index.browser.d.ts.map create mode 100644 dist/index.browser.js create mode 100644 dist/index.browser.js.map create mode 100644 src/PluginMarketplaceClient.ts create mode 100644 src/PluginMarketplaceManager.ts create mode 100644 src/index.browser.ts diff --git a/dist/PluginMarketplaceClient.d.ts b/dist/PluginMarketplaceClient.d.ts new file mode 100644 index 0000000..f87a0b1 --- /dev/null +++ b/dist/PluginMarketplaceClient.d.ts @@ -0,0 +1,53 @@ +import type { PluginIndex, PluginMetadata } from './types.js'; +/** + * Minimal response shape required by marketplace client. + */ +export interface PluginMarketplaceResponse { + ok: boolean; + status: number; + json(): Promise; +} +/** + * Fetch function used by marketplace client. + */ +export type PluginMarketplaceFetch = (input: string, init?: Record) => Promise; +/** + * Construction options for {@link PluginMarketplaceClient}. + */ +export interface PluginMarketplaceClientOptions { + /** URL of marketplace index JSON file. */ + indexUrl: string; + /** Base URL used to resolve `${pluginId}.json` metadata files. */ + baseUrl: string; + /** Optional fetch implementation. Falls back to global `fetch`. */ + fetchFn?: PluginMarketplaceFetch; +} +/** + * Host-agnostic client for reading marketplace metadata. + */ +export declare class PluginMarketplaceClient { + private readonly indexUrl; + private readonly baseUrl; + private readonly fetchFn?; + constructor(options: PluginMarketplaceClientOptions); + /** + * Fetches and validates marketplace index. + */ + fetchIndex(): Promise; + /** + * Fetches and validates single plugin metadata entry. + * + * @param pluginId - Identifier referenced by marketplace index. + */ + fetchPlugin(pluginId: string): Promise; + /** + * Fetches full marketplace catalog. + * + * Entries that fail fetch or validation get skipped so host can still render + * rest of catalog. + */ + fetchPlugins(): Promise; + private buildMetadataUrl; + private getFetchFn; +} +//# sourceMappingURL=PluginMarketplaceClient.d.ts.map \ No newline at end of file diff --git a/dist/PluginMarketplaceClient.d.ts.map b/dist/PluginMarketplaceClient.d.ts.map new file mode 100644 index 0000000..2c1b8ad --- /dev/null +++ b/dist/PluginMarketplaceClient.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"PluginMarketplaceClient.d.ts","sourceRoot":"","sources":["../src/PluginMarketplaceClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,CACnC,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC3B,OAAO,CAAC,yBAAyB,CAAC,CAAC;AAExC;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC7C,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IAEjB,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAEhB,mEAAmE;IACnE,OAAO,CAAC,EAAE,sBAAsB,CAAC;CAClC;AAED;;GAEG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAyB;gBAEtC,OAAO,EAAE,8BAA8B;IAMnD;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC;IAUxC;;;;OAIG;IACG,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAU5D;;;;;OAKG;IACG,YAAY,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAO/C,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,UAAU;CAWnB"} \ No newline at end of file diff --git a/dist/PluginMarketplaceClient.js b/dist/PluginMarketplaceClient.js new file mode 100644 index 0000000..c497cb8 --- /dev/null +++ b/dist/PluginMarketplaceClient.js @@ -0,0 +1,94 @@ +/** + * Host-agnostic client for reading marketplace metadata. + */ +export class PluginMarketplaceClient { + constructor(options) { + this.indexUrl = options.indexUrl; + this.baseUrl = options.baseUrl; + this.fetchFn = options.fetchFn; + } + /** + * Fetches and validates marketplace index. + */ + async fetchIndex() { + const response = await this.getFetchFn()(this.indexUrl); + if (!response.ok) { + throw new Error(`Failed to fetch plugin index: HTTP ${response.status}`); + } + return normalizePluginIndex(await response.json()); + } + /** + * Fetches and validates single plugin metadata entry. + * + * @param pluginId - Identifier referenced by marketplace index. + */ + async fetchPlugin(pluginId) { + const response = await this.getFetchFn()(this.buildMetadataUrl(pluginId)); + if (!response.ok) { + throw new Error(`Failed to fetch plugin ${pluginId}: HTTP ${response.status}`); + } + return normalizePluginMetadata(await response.json(), pluginId); + } + /** + * Fetches full marketplace catalog. + * + * Entries that fail fetch or validation get skipped so host can still render + * rest of catalog. + */ + async fetchPlugins() { + const index = await this.fetchIndex(); + const results = await Promise.allSettled(index.plugins.map((pluginId) => this.fetchPlugin(pluginId))); + return results.flatMap((result) => (result.status === 'fulfilled' ? [result.value] : [])); + } + buildMetadataUrl(pluginId) { + const trimmedBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl : `${this.baseUrl}/`; + return `${trimmedBaseUrl}${pluginId}.json`; + } + getFetchFn() { + if (this.fetchFn) { + return this.fetchFn; + } + if (typeof fetch !== 'function') { + throw new Error('No fetch implementation available for PluginMarketplaceClient'); + } + return fetch; + } +} +function normalizePluginIndex(value) { + const record = asRecord(value, 'Plugin index payload must be object'); + return { + version: asString(record.version), + updatedAt: asString(record.updatedAt), + plugins: Array.isArray(record.plugins) + ? record.plugins.filter((entry) => typeof entry === 'string') + : [], + }; +} +function normalizePluginMetadata(value, fallbackId) { + const record = asRecord(value, `Plugin metadata for ${fallbackId} must be object`); + return { + id: asString(record.id) || fallbackId, + name: asString(record.name), + version: asString(record.version), + description: asString(record.description), + author: asString(record.author), + repository: asString(record.repository), + thumbnail: asString(record.thumbnail), + downloadUrl: asString(record.downloadUrl), + homepage: asString(record.homepage), + tags: Array.isArray(record.tags) + ? record.tags.filter((entry) => typeof entry === 'string') + : [], + addedDate: asString(record.addedDate), + }; +} +function asRecord(value, message) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(message); + } + return value; +} +function asString(value) { + return typeof value === 'string' ? value : ''; +} +//# sourceMappingURL=PluginMarketplaceClient.js.map \ No newline at end of file diff --git a/dist/PluginMarketplaceClient.js.map b/dist/PluginMarketplaceClient.js.map new file mode 100644 index 0000000..8304ba0 --- /dev/null +++ b/dist/PluginMarketplaceClient.js.map @@ -0,0 +1 @@ +{"version":3,"file":"PluginMarketplaceClient.js","sourceRoot":"","sources":["../src/PluginMarketplaceClient.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/PluginMarketplaceManager.d.ts b/dist/PluginMarketplaceManager.d.ts new file mode 100644 index 0000000..aa98caa --- /dev/null +++ b/dist/PluginMarketplaceManager.d.ts @@ -0,0 +1,88 @@ +import { type IPluginStorage } from './interfaces.js'; +import type { PluginRegistry } from './PluginRegistry.js'; +import type { InstalledPlugin, PluginMetadata } from './types.js'; +import { PluginMarketplaceClient } from './PluginMarketplaceClient.js'; +/** + * Host-visible installed-plugin metadata. + * + * Hosts may only know filesystem facts such as `id`, `name`, or `installedDate`. + * Stored marketplace metadata fills remaining fields. + */ +export type PluginMarketplaceInstalledRecord = Partial & Pick; +/** + * Host callbacks used by {@link PluginMarketplaceManager}. + */ +export interface PluginMarketplaceHostAdapter { + /** + * Returns host-known installed plugins. + */ + listInstalledPlugins?(): Promise; + /** + * Performs host-specific install side effects. + */ + installPlugin?(plugin: PluginMetadata): Promise; + /** + * Performs host-specific uninstall side effects. + */ + uninstallPlugin?(pluginId: string): Promise; +} +/** + * Construction options for {@link PluginMarketplaceManager}. + */ +export interface PluginMarketplaceManagerOptions { + /** Shared marketplace client used to query remote metadata. */ + client: PluginMarketplaceClient; + /** Optional persistent storage for installed-plugin state. */ + storage?: IPluginStorage; + /** Optional host adapter for install and discovery side effects. */ + host?: PluginMarketplaceHostAdapter; + /** Optional registry used when disabling or uninstalling loaded plugins. */ + registry?: PluginRegistry; + /** Optional override for installed-plugin storage key. */ + installedPluginsStorageKey?: string; +} +/** + * Coordinates marketplace reads with host-specific install and enable state. + */ +export declare class PluginMarketplaceManager { + private readonly client; + private readonly storage; + private readonly host?; + private readonly registry?; + private readonly installedPluginsStorageKey; + constructor(options: PluginMarketplaceManagerOptions); + /** + * Fetches full marketplace catalog. + */ + fetchMarketplacePlugins(): Promise; + /** + * Lists installed plugins merged with persisted enabled state. + */ + listInstalledPlugins(): Promise; + /** + * Lists installed plugins currently enabled. + */ + listEnabledPlugins(): Promise; + /** + * Returns whether plugin is recorded as installed. + */ + isPluginInstalled(pluginId: string): Promise; + /** + * Installs marketplace plugin via host adapter and persists state. + */ + installPlugin(plugin: PluginMetadata): Promise; + /** + * Removes plugin from host and clears persisted install state. + */ + uninstallPlugin(pluginId: string): Promise; + /** + * Enables or disables installed plugin. + * + * Disabling triggers `registry.unregisterPlugin` when registry exists. + * Enabling only updates persisted state; host controls actual load timing. + */ + setPluginEnabled(pluginId: string, enabled: boolean): Promise; + private readStoredInstalledPlugins; + private writeStoredInstalledPlugins; +} +//# sourceMappingURL=PluginMarketplaceManager.d.ts.map \ No newline at end of file diff --git a/dist/PluginMarketplaceManager.d.ts.map b/dist/PluginMarketplaceManager.d.ts.map new file mode 100644 index 0000000..23a4bda --- /dev/null +++ b/dist/PluginMarketplaceManager.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"PluginMarketplaceManager.d.ts","sourceRoot":"","sources":["../src/PluginMarketplaceManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAIvE;;;;;GAKG;AACH,MAAM,MAAM,gCAAgC,GAAG,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;AAEtG;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C;;OAEG;IACH,oBAAoB,CAAC,IAAI,OAAO,CAAC,gCAAgC,EAAE,CAAC,CAAC;IAErE;;OAEG;IACH,aAAa,CAAC,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtD;;OAEG;IACH,eAAe,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD;AAED;;GAEG;AACH,MAAM,WAAW,+BAA+B;IAC9C,+DAA+D;IAC/D,MAAM,EAAE,uBAAuB,CAAC;IAEhC,8DAA8D;IAC9D,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB,oEAAoE;IACpE,IAAI,CAAC,EAAE,4BAA4B,CAAC;IAEpC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,cAAc,CAAC;IAE1B,0DAA0D;IAC1D,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED;;GAEG;AACH,qBAAa,wBAAwB;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAiB;IAC3C,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAAS;gBAExC,OAAO,EAAE,+BAA+B;IASpD;;OAEG;IACG,uBAAuB,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1D;;OAEG;IACG,oBAAoB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAaxD;;OAEG;IACG,kBAAkB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAItD;;OAEG;IACG,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI3D;;OAEG;IACG,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAerE;;OAEG;IACG,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWtD;;;;;OAKG;IACG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAiB3F,OAAO,CAAC,0BAA0B;IAsBlC,OAAO,CAAC,2BAA2B;CAGpC"} \ No newline at end of file diff --git a/dist/PluginMarketplaceManager.js b/dist/PluginMarketplaceManager.js new file mode 100644 index 0000000..113c0d8 --- /dev/null +++ b/dist/PluginMarketplaceManager.js @@ -0,0 +1,169 @@ +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 \ No newline at end of file diff --git a/dist/PluginMarketplaceManager.js.map b/dist/PluginMarketplaceManager.js.map new file mode 100644 index 0000000..f6b0d77 --- /dev/null +++ b/dist/PluginMarketplaceManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"PluginMarketplaceManager.js","sourceRoot":"","sources":["../src/PluginMarketplaceManager.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/index.browser.d.ts b/dist/index.browser.d.ts new file mode 100644 index 0000000..0c1a9ab --- /dev/null +++ b/dist/index.browser.d.ts @@ -0,0 +1,15 @@ +export type { IPluginStorage, IPluginEventClient, } from './interfaces.js'; +export { MemoryStorage } from './interfaces.js'; +export { PluginMarketplaceClient } from './PluginMarketplaceClient.js'; +export type { PluginMarketplaceClientOptions, PluginMarketplaceFetch, PluginMarketplaceResponse, } from './PluginMarketplaceClient.js'; +export { PluginMarketplaceManager } from './PluginMarketplaceManager.js'; +export type { PluginMarketplaceHostAdapter, PluginMarketplaceInstalledRecord, PluginMarketplaceManagerOptions, } from './PluginMarketplaceManager.js'; +export type { PluginMetadata, PluginIndex, InstalledPlugin, } from './types.js'; +export { PluginTab } from './types.js'; +export type { ThemeColorGroup, ThemePaletteGroup, PluginThemeColors, PluginTheme, CommandArg, PluginCommand, MessageInterceptor, MessageContext, CustomRenderer, SettingDefinition, SettingsSchema, NotificationOptions, PluginContext, PluginSettingsSection, UILocation, Plugin, PluginLogEntry, } from './PluginInterfaces.js'; +export { generateThemeCSS } from './theme-css.js'; +export { PluginRegistry } from './PluginRegistry.js'; +export type { PluginRegistryOptions } from './PluginRegistry.js'; +export { createPluginContext } from './PluginContext.js'; +export type { PluginContextOptions } from './PluginContext.js'; +//# sourceMappingURL=index.browser.d.ts.map \ No newline at end of file diff --git a/dist/index.browser.d.ts.map b/dist/index.browser.d.ts.map new file mode 100644 index 0000000..e9733f3 --- /dev/null +++ b/dist/index.browser.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.browser.d.ts","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,YAAY,EACV,8BAA8B,EAC9B,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,YAAY,EACV,4BAA4B,EAC5B,gCAAgC,EAChC,+BAA+B,GAChC,MAAM,+BAA+B,CAAC;AAEvC,YAAY,EACV,cAAc,EACd,WAAW,EACX,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,UAAU,EACV,MAAM,EACN,cAAc,GACf,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC"} \ No newline at end of file diff --git a/dist/index.browser.js b/dist/index.browser.js new file mode 100644 index 0000000..908ace2 --- /dev/null +++ b/dist/index.browser.js @@ -0,0 +1,8 @@ +export { MemoryStorage } from './interfaces.js'; +export { PluginMarketplaceClient } from './PluginMarketplaceClient.js'; +export { PluginMarketplaceManager } from './PluginMarketplaceManager.js'; +export { PluginTab } from './types.js'; +export { generateThemeCSS } from './theme-css.js'; +export { PluginRegistry } from './PluginRegistry.js'; +export { createPluginContext } from './PluginContext.js'; +//# sourceMappingURL=index.browser.js.map \ No newline at end of file diff --git a/dist/index.browser.js.map b/dist/index.browser.js.map new file mode 100644 index 0000000..08a4f2d --- /dev/null +++ b/dist/index.browser.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.browser.js","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAOvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAYzE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAsBvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts index b1c99cd..2e5e56b 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,5 +1,9 @@ export type { IPluginStorage, IPluginEventClient, } from './interfaces.js'; export { MemoryStorage } from './interfaces.js'; +export { PluginMarketplaceClient } from './PluginMarketplaceClient.js'; +export type { PluginMarketplaceClientOptions, PluginMarketplaceFetch, PluginMarketplaceResponse, } from './PluginMarketplaceClient.js'; +export { PluginMarketplaceManager } from './PluginMarketplaceManager.js'; +export type { PluginMarketplaceHostAdapter, PluginMarketplaceManagerOptions, } from './PluginMarketplaceManager.js'; export type { PluginMetadata, PluginIndex, InstalledPlugin, } from './types.js'; export { PluginTab } from './types.js'; export type { ThemeColorGroup, ThemePaletteGroup, PluginThemeColors, PluginTheme, CommandArg, PluginCommand, MessageInterceptor, MessageContext, CustomRenderer, SettingDefinition, SettingsSchema, NotificationOptions, PluginContext, PluginSettingsSection, UILocation, Plugin, PluginLogEntry, } from './PluginInterfaces.js'; diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map index 291e389..7d83b27 100644 --- a/dist/index.d.ts.map +++ b/dist/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,YAAY,EACV,cAAc,EACd,WAAW,EACX,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,UAAU,EACV,MAAM,EACN,cAAc,GACf,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,YAAY,EACV,8BAA8B,EAC9B,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,YAAY,EACV,4BAA4B,EAC5B,+BAA+B,GAChC,MAAM,+BAA+B,CAAC;AAEvC,YAAY,EACV,cAAc,EACd,WAAW,EACX,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,UAAU,EACV,MAAM,EACN,cAAc,GACf,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC"} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index f1f22bb..706ae32 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,4 +1,6 @@ export { MemoryStorage } from './interfaces.js'; +export { PluginMarketplaceClient } from './PluginMarketplaceClient.js'; +export { PluginMarketplaceManager } from './PluginMarketplaceManager.js'; export { PluginTab } from './types.js'; export { generateThemeCSS } from './theme-css.js'; export { PluginRegistry } from './PluginRegistry.js'; diff --git a/dist/index.js.map b/dist/index.js.map index 3172464..c44de3d 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAOhD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAsBvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAOvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAWzE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAsBvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"} \ No newline at end of file diff --git a/package.json b/package.json index 3a4cdee..b430ec9 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "types": "./dist/index.d.ts", "exports": { ".": { + "browser": "./dist/index.browser.js", "import": "./dist/index.js", "types": "./dist/index.d.ts" } diff --git a/src/PluginMarketplaceClient.ts b/src/PluginMarketplaceClient.ts new file mode 100644 index 0000000..18ca8e1 --- /dev/null +++ b/src/PluginMarketplaceClient.ts @@ -0,0 +1,149 @@ +import type { PluginIndex, PluginMetadata } from './types.js'; + +/** + * Minimal response shape required by marketplace client. + */ +export interface PluginMarketplaceResponse { + ok: boolean; + status: number; + json(): Promise; +} + +/** + * Fetch function used by marketplace client. + */ +export type PluginMarketplaceFetch = ( + input: string, + init?: Record +) => Promise; + +/** + * Construction options for {@link PluginMarketplaceClient}. + */ +export interface PluginMarketplaceClientOptions { + /** URL of marketplace index JSON file. */ + indexUrl: string; + + /** Base URL used to resolve `${pluginId}.json` metadata files. */ + baseUrl: string; + + /** Optional fetch implementation. Falls back to global `fetch`. */ + fetchFn?: PluginMarketplaceFetch; +} + +/** + * Host-agnostic client for reading marketplace metadata. + */ +export class PluginMarketplaceClient { + private readonly indexUrl: string; + private readonly baseUrl: string; + private readonly fetchFn?: PluginMarketplaceFetch; + + constructor(options: PluginMarketplaceClientOptions) { + this.indexUrl = options.indexUrl; + this.baseUrl = options.baseUrl; + this.fetchFn = options.fetchFn; + } + + /** + * Fetches and validates marketplace index. + */ + async fetchIndex(): Promise { + const response = await this.getFetchFn()(this.indexUrl); + + if (!response.ok) { + throw new Error(`Failed to fetch plugin index: HTTP ${response.status}`); + } + + return normalizePluginIndex(await response.json()); + } + + /** + * Fetches and validates single plugin metadata entry. + * + * @param pluginId - Identifier referenced by marketplace index. + */ + async fetchPlugin(pluginId: string): Promise { + const response = await this.getFetchFn()(this.buildMetadataUrl(pluginId)); + + if (!response.ok) { + throw new Error(`Failed to fetch plugin ${pluginId}: HTTP ${response.status}`); + } + + return normalizePluginMetadata(await response.json(), pluginId); + } + + /** + * Fetches full marketplace catalog. + * + * Entries that fail fetch or validation get skipped so host can still render + * rest of catalog. + */ + async fetchPlugins(): Promise { + const index = await this.fetchIndex(); + const results = await Promise.allSettled(index.plugins.map((pluginId) => this.fetchPlugin(pluginId))); + + return results.flatMap((result) => (result.status === 'fulfilled' ? [result.value] : [])); + } + + private buildMetadataUrl(pluginId: string): string { + const trimmedBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl : `${this.baseUrl}/`; + return `${trimmedBaseUrl}${pluginId}.json`; + } + + private getFetchFn(): PluginMarketplaceFetch { + if (this.fetchFn) { + return this.fetchFn; + } + + if (typeof fetch !== 'function') { + throw new Error('No fetch implementation available for PluginMarketplaceClient'); + } + + return fetch as unknown as PluginMarketplaceFetch; + } +} + +function normalizePluginIndex(value: unknown): PluginIndex { + const record = asRecord(value, 'Plugin index payload must be object'); + + return { + version: asString(record.version), + updatedAt: asString(record.updatedAt), + plugins: Array.isArray(record.plugins) + ? record.plugins.filter((entry): entry is string => typeof entry === 'string') + : [], + }; +} + +function normalizePluginMetadata(value: unknown, fallbackId: string): PluginMetadata { + const record = asRecord(value, `Plugin metadata for ${fallbackId} must be object`); + + return { + id: asString(record.id) || fallbackId, + name: asString(record.name), + version: asString(record.version), + description: asString(record.description), + author: asString(record.author), + repository: asString(record.repository), + thumbnail: asString(record.thumbnail), + downloadUrl: asString(record.downloadUrl), + homepage: asString(record.homepage), + tags: Array.isArray(record.tags) + ? record.tags.filter((entry): entry is string => typeof entry === 'string') + : [], + addedDate: asString(record.addedDate), + }; +} + +function asRecord(value: unknown, message: string): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(message); + } + + return value as Record; +} + +function asString(value: unknown): string { + return typeof value === 'string' ? value : ''; +} \ No newline at end of file diff --git a/src/PluginMarketplaceManager.ts b/src/PluginMarketplaceManager.ts new file mode 100644 index 0000000..d2808a9 --- /dev/null +++ b/src/PluginMarketplaceManager.ts @@ -0,0 +1,262 @@ +import { MemoryStorage, type IPluginStorage } from './interfaces.js'; +import type { PluginRegistry } from './PluginRegistry.js'; +import type { InstalledPlugin, PluginMetadata } from './types.js'; +import { PluginMarketplaceClient } from './PluginMarketplaceClient.js'; + +const DEFAULT_INSTALLED_PLUGINS_STORAGE_KEY = 'plugin-manager:installed-plugins'; + +/** + * Host-visible installed-plugin metadata. + * + * Hosts may only know filesystem facts such as `id`, `name`, or `installedDate`. + * Stored marketplace metadata fills remaining fields. + */ +export type PluginMarketplaceInstalledRecord = Partial & Pick; + +/** + * Host callbacks used by {@link PluginMarketplaceManager}. + */ +export interface PluginMarketplaceHostAdapter { + /** + * Returns host-known installed plugins. + */ + listInstalledPlugins?(): Promise; + + /** + * Performs host-specific install side effects. + */ + installPlugin?(plugin: PluginMetadata): Promise; + + /** + * Performs host-specific uninstall side effects. + */ + uninstallPlugin?(pluginId: string): Promise; +} + +/** + * Construction options for {@link PluginMarketplaceManager}. + */ +export interface PluginMarketplaceManagerOptions { + /** Shared marketplace client used to query remote metadata. */ + client: PluginMarketplaceClient; + + /** Optional persistent storage for installed-plugin state. */ + storage?: IPluginStorage; + + /** Optional host adapter for install and discovery side effects. */ + host?: PluginMarketplaceHostAdapter; + + /** Optional registry used when disabling or uninstalling loaded plugins. */ + registry?: PluginRegistry; + + /** Optional override for installed-plugin storage key. */ + installedPluginsStorageKey?: string; +} + +/** + * Coordinates marketplace reads with host-specific install and enable state. + */ +export class PluginMarketplaceManager { + private readonly client: PluginMarketplaceClient; + private readonly storage: IPluginStorage; + private readonly host?: PluginMarketplaceHostAdapter; + private readonly registry?: PluginRegistry; + private readonly installedPluginsStorageKey: string; + + constructor(options: PluginMarketplaceManagerOptions) { + 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(): Promise { + return this.client.fetchPlugins(); + } + + /** + * Lists installed plugins merged with persisted enabled state. + */ + async listInstalledPlugins(): Promise { + 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(): Promise { + return (await this.listInstalledPlugins()).filter((plugin) => plugin.enabled); + } + + /** + * Returns whether plugin is recorded as installed. + */ + async isPluginInstalled(pluginId: string): Promise { + return (await this.listInstalledPlugins()).some((plugin) => plugin.id === pluginId); + } + + /** + * Installs marketplace plugin via host adapter and persists state. + */ + async installPlugin(plugin: PluginMetadata): Promise { + await this.host?.installPlugin?.(plugin); + + const installedPlugin: 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: string): Promise { + 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: string, enabled: boolean): Promise { + 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: InstalledPlugin = { ...targetPlugin, enabled }; + this.writeStoredInstalledPlugins(upsertInstalledPlugin(installedPlugins, updatedPlugin)); + return updatedPlugin; + } + + private readStoredInstalledPlugins(): InstalledPlugin[] { + const rawValue = this.storage.getItem(this.installedPluginsStorageKey); + + if (!rawValue) { + return []; + } + + try { + const parsedValue = JSON.parse(rawValue) as unknown; + + if (!Array.isArray(parsedValue)) { + return []; + } + + return parsedValue + .map((entry) => normalizeInstalledPlugin(entry)) + .filter((entry): entry is InstalledPlugin => entry !== null); + } catch { + return []; + } + } + + private writeStoredInstalledPlugins(plugins: InstalledPlugin[]): void { + this.storage.setItem(this.installedPluginsStorageKey, JSON.stringify(plugins)); + } +} + +function mergeInstalledPlugins( + hostPlugins: PluginMarketplaceInstalledRecord[], + storedPlugins: InstalledPlugin[] +): InstalledPlugin[] { + 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): entry is string => 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: InstalledPlugin[], nextPlugin: InstalledPlugin): InstalledPlugin[] { + const filteredPlugins = plugins.filter((plugin) => plugin.id !== nextPlugin.id); + return [...filteredPlugins, nextPlugin]; +} + +function normalizeInstalledPlugin(value: unknown): InstalledPlugin | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return null; + } + + const record = value as Record; + 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): entry is string => 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: unknown, fallback = ''): string { + return typeof value === 'string' && value.length > 0 ? value : fallback; +} \ No newline at end of file diff --git a/src/index.browser.ts b/src/index.browser.ts new file mode 100644 index 0000000..0c9aaae --- /dev/null +++ b/src/index.browser.ts @@ -0,0 +1,54 @@ +export type { + IPluginStorage, + IPluginEventClient, +} from './interfaces.js'; +export { MemoryStorage } from './interfaces.js'; + +export { PluginMarketplaceClient } from './PluginMarketplaceClient.js'; +export type { + PluginMarketplaceClientOptions, + PluginMarketplaceFetch, + PluginMarketplaceResponse, +} from './PluginMarketplaceClient.js'; + +export { PluginMarketplaceManager } from './PluginMarketplaceManager.js'; +export type { + PluginMarketplaceHostAdapter, + PluginMarketplaceInstalledRecord, + PluginMarketplaceManagerOptions, +} from './PluginMarketplaceManager.js'; + +export type { + PluginMetadata, + PluginIndex, + InstalledPlugin, +} from './types.js'; +export { PluginTab } from './types.js'; + +export type { + ThemeColorGroup, + ThemePaletteGroup, + PluginThemeColors, + PluginTheme, + CommandArg, + PluginCommand, + MessageInterceptor, + MessageContext, + CustomRenderer, + SettingDefinition, + SettingsSchema, + NotificationOptions, + PluginContext, + PluginSettingsSection, + UILocation, + Plugin, + PluginLogEntry, +} from './PluginInterfaces.js'; + +export { generateThemeCSS } from './theme-css.js'; + +export { PluginRegistry } from './PluginRegistry.js'; +export type { PluginRegistryOptions } from './PluginRegistry.js'; + +export { createPluginContext } from './PluginContext.js'; +export type { PluginContextOptions } from './PluginContext.js'; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 205a4b1..2f379d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,19 @@ export type { } from './interfaces.js'; export { MemoryStorage } from './interfaces.js'; +export { PluginMarketplaceClient } from './PluginMarketplaceClient.js'; +export type { + PluginMarketplaceClientOptions, + PluginMarketplaceFetch, + PluginMarketplaceResponse, +} from './PluginMarketplaceClient.js'; + +export { PluginMarketplaceManager } from './PluginMarketplaceManager.js'; +export type { + PluginMarketplaceHostAdapter, + PluginMarketplaceManagerOptions, +} from './PluginMarketplaceManager.js'; + export type { PluginMetadata, PluginIndex, diff --git a/test/electron/renderer.js b/test/electron/renderer.js index 16a6e47..fbd9c7a 100644 --- a/test/electron/renderer.js +++ b/test/electron/renderer.js @@ -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} + * @type {Map} */ 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 => `${t}`).join('')} ${p.homepage ? `↗ Homepage` : ''} - @@ -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);