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:
53
dist/PluginMarketplaceClient.d.ts
vendored
Normal file
53
dist/PluginMarketplaceClient.d.ts
vendored
Normal file
@@ -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<unknown>;
|
||||
}
|
||||
/**
|
||||
* Fetch function used by marketplace client.
|
||||
*/
|
||||
export type PluginMarketplaceFetch = (input: string, init?: Record<string, unknown>) => Promise<PluginMarketplaceResponse>;
|
||||
/**
|
||||
* 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<PluginIndex>;
|
||||
/**
|
||||
* Fetches and validates single plugin metadata entry.
|
||||
*
|
||||
* @param pluginId - Identifier referenced by marketplace index.
|
||||
*/
|
||||
fetchPlugin(pluginId: string): Promise<PluginMetadata>;
|
||||
/**
|
||||
* Fetches full marketplace catalog.
|
||||
*
|
||||
* Entries that fail fetch or validation get skipped so host can still render
|
||||
* rest of catalog.
|
||||
*/
|
||||
fetchPlugins(): Promise<PluginMetadata[]>;
|
||||
private buildMetadataUrl;
|
||||
private getFetchFn;
|
||||
}
|
||||
//# sourceMappingURL=PluginMarketplaceClient.d.ts.map
|
||||
1
dist/PluginMarketplaceClient.d.ts.map
vendored
Normal file
1
dist/PluginMarketplaceClient.d.ts.map
vendored
Normal file
@@ -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"}
|
||||
94
dist/PluginMarketplaceClient.js
vendored
Normal file
94
dist/PluginMarketplaceClient.js
vendored
Normal file
@@ -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
|
||||
1
dist/PluginMarketplaceClient.js.map
vendored
Normal file
1
dist/PluginMarketplaceClient.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"PluginMarketplaceClient.js","sourceRoot":"","sources":["../src/PluginMarketplaceClient.ts"],"names":[],"mappings":"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"}
|
||||
88
dist/PluginMarketplaceManager.d.ts
vendored
Normal file
88
dist/PluginMarketplaceManager.d.ts
vendored
Normal file
@@ -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<InstalledPlugin> & Pick<InstalledPlugin, 'id'>;
|
||||
/**
|
||||
* Host callbacks used by {@link PluginMarketplaceManager}.
|
||||
*/
|
||||
export interface PluginMarketplaceHostAdapter {
|
||||
/**
|
||||
* Returns host-known installed plugins.
|
||||
*/
|
||||
listInstalledPlugins?(): Promise<PluginMarketplaceInstalledRecord[]>;
|
||||
/**
|
||||
* Performs host-specific install side effects.
|
||||
*/
|
||||
installPlugin?(plugin: PluginMetadata): Promise<void>;
|
||||
/**
|
||||
* Performs host-specific uninstall side effects.
|
||||
*/
|
||||
uninstallPlugin?(pluginId: string): Promise<void>;
|
||||
}
|
||||
/**
|
||||
* 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<PluginMetadata[]>;
|
||||
/**
|
||||
* Lists installed plugins merged with persisted enabled state.
|
||||
*/
|
||||
listInstalledPlugins(): Promise<InstalledPlugin[]>;
|
||||
/**
|
||||
* Lists installed plugins currently enabled.
|
||||
*/
|
||||
listEnabledPlugins(): Promise<InstalledPlugin[]>;
|
||||
/**
|
||||
* Returns whether plugin is recorded as installed.
|
||||
*/
|
||||
isPluginInstalled(pluginId: string): Promise<boolean>;
|
||||
/**
|
||||
* Installs marketplace plugin via host adapter and persists state.
|
||||
*/
|
||||
installPlugin(plugin: PluginMetadata): Promise<InstalledPlugin>;
|
||||
/**
|
||||
* Removes plugin from host and clears persisted install state.
|
||||
*/
|
||||
uninstallPlugin(pluginId: string): Promise<void>;
|
||||
/**
|
||||
* 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<InstalledPlugin | null>;
|
||||
private readStoredInstalledPlugins;
|
||||
private writeStoredInstalledPlugins;
|
||||
}
|
||||
//# sourceMappingURL=PluginMarketplaceManager.d.ts.map
|
||||
1
dist/PluginMarketplaceManager.d.ts.map
vendored
Normal file
1
dist/PluginMarketplaceManager.d.ts.map
vendored
Normal file
@@ -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"}
|
||||
169
dist/PluginMarketplaceManager.js
vendored
Normal file
169
dist/PluginMarketplaceManager.js
vendored
Normal file
@@ -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
|
||||
1
dist/PluginMarketplaceManager.js.map
vendored
Normal file
1
dist/PluginMarketplaceManager.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
15
dist/index.browser.d.ts
vendored
Normal file
15
dist/index.browser.d.ts
vendored
Normal file
@@ -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
|
||||
1
dist/index.browser.d.ts.map
vendored
Normal file
1
dist/index.browser.d.ts.map
vendored
Normal file
@@ -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"}
|
||||
8
dist/index.browser.js
vendored
Normal file
8
dist/index.browser.js
vendored
Normal file
@@ -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
|
||||
1
dist/index.browser.js.map
vendored
Normal file
1
dist/index.browser.js.map
vendored
Normal file
@@ -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"}
|
||||
4
dist/index.d.ts
vendored
4
dist/index.d.ts
vendored
@@ -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';
|
||||
|
||||
2
dist/index.d.ts.map
vendored
2
dist/index.d.ts.map
vendored
@@ -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"}
|
||||
{"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"}
|
||||
2
dist/index.js
vendored
2
dist/index.js
vendored
@@ -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';
|
||||
|
||||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
@@ -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"}
|
||||
{"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"}
|
||||
@@ -7,6 +7,7 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"browser": "./dist/index.browser.js",
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
|
||||
149
src/PluginMarketplaceClient.ts
Normal file
149
src/PluginMarketplaceClient.ts
Normal file
@@ -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<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch function used by marketplace client.
|
||||
*/
|
||||
export type PluginMarketplaceFetch = (
|
||||
input: string,
|
||||
init?: Record<string, unknown>
|
||||
) => Promise<PluginMarketplaceResponse>;
|
||||
|
||||
/**
|
||||
* 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<PluginIndex> {
|
||||
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<PluginMetadata> {
|
||||
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<PluginMetadata[]> {
|
||||
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<string, unknown> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function asString(value: unknown): string {
|
||||
return typeof value === 'string' ? value : '';
|
||||
}
|
||||
262
src/PluginMarketplaceManager.ts
Normal file
262
src/PluginMarketplaceManager.ts
Normal file
@@ -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<InstalledPlugin> & Pick<InstalledPlugin, 'id'>;
|
||||
|
||||
/**
|
||||
* Host callbacks used by {@link PluginMarketplaceManager}.
|
||||
*/
|
||||
export interface PluginMarketplaceHostAdapter {
|
||||
/**
|
||||
* Returns host-known installed plugins.
|
||||
*/
|
||||
listInstalledPlugins?(): Promise<PluginMarketplaceInstalledRecord[]>;
|
||||
|
||||
/**
|
||||
* Performs host-specific install side effects.
|
||||
*/
|
||||
installPlugin?(plugin: PluginMetadata): Promise<void>;
|
||||
|
||||
/**
|
||||
* Performs host-specific uninstall side effects.
|
||||
*/
|
||||
uninstallPlugin?(pluginId: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<PluginMetadata[]> {
|
||||
return this.client.fetchPlugins();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists installed plugins merged with persisted enabled state.
|
||||
*/
|
||||
async listInstalledPlugins(): Promise<InstalledPlugin[]> {
|
||||
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<InstalledPlugin[]> {
|
||||
return (await this.listInstalledPlugins()).filter((plugin) => plugin.enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether plugin is recorded as installed.
|
||||
*/
|
||||
async isPluginInstalled(pluginId: string): Promise<boolean> {
|
||||
return (await this.listInstalledPlugins()).some((plugin) => plugin.id === pluginId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs marketplace plugin via host adapter and persists state.
|
||||
*/
|
||||
async installPlugin(plugin: PluginMetadata): Promise<InstalledPlugin> {
|
||||
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<void> {
|
||||
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<InstalledPlugin | null> {
|
||||
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<string, unknown>;
|
||||
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;
|
||||
}
|
||||
54
src/index.browser.ts
Normal file
54
src/index.browser.ts
Normal file
@@ -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';
|
||||
13
src/index.ts
13
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,
|
||||
|
||||
@@ -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