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 type { IPluginStorage, IPluginEventClient, } from './interfaces.js';
|
||||||
export { MemoryStorage } 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 type { PluginMetadata, PluginIndex, InstalledPlugin, } from './types.js';
|
||||||
export { PluginTab } 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 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 { MemoryStorage } from './interfaces.js';
|
||||||
|
export { PluginMarketplaceClient } from './PluginMarketplaceClient.js';
|
||||||
|
export { PluginMarketplaceManager } from './PluginMarketplaceManager.js';
|
||||||
export { PluginTab } from './types.js';
|
export { PluginTab } from './types.js';
|
||||||
export { generateThemeCSS } from './theme-css.js';
|
export { generateThemeCSS } from './theme-css.js';
|
||||||
export { PluginRegistry } from './PluginRegistry.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",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
|
"browser": "./dist/index.browser.js",
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts"
|
"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';
|
} from './interfaces.js';
|
||||||
export { MemoryStorage } 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 {
|
export type {
|
||||||
PluginMetadata,
|
PluginMetadata,
|
||||||
PluginIndex,
|
PluginIndex,
|
||||||
|
|||||||
@@ -14,7 +14,12 @@
|
|||||||
const pmEntry = path.join(
|
const pmEntry = path.join(
|
||||||
__dirname, 'node_modules', '@paarrot', 'plugin-manager', 'dist', 'index.js'
|
__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)
|
// 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)
|
// 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.
|
* Persistent record of every plugin we have ever seen, keyed by plugin id.
|
||||||
* Survives unload so the Installed list can show a Reload button.
|
* 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();
|
const knownPlugins = new Map();
|
||||||
|
|
||||||
@@ -189,7 +212,19 @@
|
|||||||
* registers and loads the plugin, then records it in knownPlugins.
|
* registers and loads the plugin, then records it in knownPlugins.
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {object} pluginModule
|
* @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 = {}) {
|
async function registerAndLoad(id, pluginModule, source = {}) {
|
||||||
const ctx = createPluginContext(
|
const ctx = createPluginContext(
|
||||||
@@ -211,13 +246,60 @@
|
|||||||
await pluginModule.onLoad(ctx);
|
await pluginModule.onLoad(ctx);
|
||||||
|
|
||||||
knownPlugins.set(id, {
|
knownPlugins.set(id, {
|
||||||
name: pluginModule.name ?? id,
|
name: source.name ?? pluginModule.name ?? id,
|
||||||
version: pluginModule.version ?? '?',
|
version: source.version ?? pluginModule.version ?? '?',
|
||||||
pluginModule,
|
pluginModule,
|
||||||
...source,
|
...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, {
|
await registerAndLoad('example-plugin', plugin, {
|
||||||
filePath: pluginPath,
|
filePath: pluginPath,
|
||||||
});
|
});
|
||||||
@@ -367,11 +449,6 @@
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Plugin Directory — fetch from remote index
|
// 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 = [];
|
let directoryPlugins = [];
|
||||||
const dirStatusEl = document.getElementById('dir-status');
|
const dirStatusEl = document.getElementById('dir-status');
|
||||||
const dirListEl = document.getElementById('dir-list');
|
const dirListEl = document.getElementById('dir-list');
|
||||||
@@ -415,11 +492,7 @@
|
|||||||
${(p.tags ?? []).map(t => `<span class="market-tag">${t}</span>`).join('')}
|
${(p.tags ?? []).map(t => `<span class="market-tag">${t}</span>`).join('')}
|
||||||
${p.homepage ? `<a href="${p.homepage}" target="_blank"
|
${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>` : ''}
|
style="font-size:10px;color:#7986cb;text-decoration:none;margin-left:auto;margin-right:6px;">↗ Homepage</a>` : ''}
|
||||||
<button class="market-install-btn"
|
<button class="market-install-btn" data-id="${p.id}" ${isLoaded ? 'disabled' : ''}>
|
||||||
data-url="${p.downloadUrl ?? ''}"
|
|
||||||
data-repo="${p.repository ?? ''}"
|
|
||||||
data-id="${p.id}"
|
|
||||||
${isLoaded ? 'disabled' : ''}>
|
|
||||||
${isLoaded ? 'Loaded' : 'Download'}
|
${isLoaded ? 'Loaded' : 'Download'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -428,24 +501,21 @@
|
|||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
dirListEl.querySelectorAll('.market-install-btn:not([disabled])').forEach(btn => {
|
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. */
|
/** Downloads a plugin's index.js from the marketplace and loads it into the registry. */
|
||||||
async function downloadAndLoadPlugin(pluginId, downloadUrl, repositoryUrl) {
|
async function downloadAndLoadPlugin(pluginId) {
|
||||||
if (!downloadUrl) {
|
const pluginMetadata = directoryPlugins.find((entry) => entry.id === pluginId);
|
||||||
addLog(`No download URL for plugin ${pluginId}`, 'error');
|
|
||||||
|
if (!pluginMetadata) {
|
||||||
|
addLog(`Plugin ${pluginId} is not present in directory cache`, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
addLog(`Downloading plugin ${pluginId}…`, 'info');
|
await marketplace.installPlugin(pluginMetadata);
|
||||||
const code = await fetchPluginCode(downloadUrl, repositoryUrl);
|
|
||||||
const raw = await evalPluginCode(code);
|
|
||||||
const p = normalisePlugin(raw);
|
|
||||||
|
|
||||||
await registerAndLoad(pluginId, p, { downloadUrl });
|
|
||||||
|
|
||||||
addLog(`Plugin installed from directory: ${pluginId}`, 'success');
|
addLog(`Plugin installed from directory: ${pluginId}`, 'success');
|
||||||
refreshPluginList();
|
refreshPluginList();
|
||||||
refreshCommandList();
|
refreshCommandList();
|
||||||
@@ -460,23 +530,9 @@
|
|||||||
async function fetchDirectory() {
|
async function fetchDirectory() {
|
||||||
dirStatusEl.textContent = 'Fetching plugin directory…';
|
dirStatusEl.textContent = 'Fetching plugin directory…';
|
||||||
dirListEl.innerHTML = '';
|
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 {
|
try {
|
||||||
const r = await fetch(`${PLUGIN_BASE_URL}${id}.json`);
|
directoryPlugins = await marketplace.fetchMarketplacePlugins();
|
||||||
return r.ok ? r.json() : null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
directoryPlugins = metas.filter(Boolean);
|
|
||||||
dirStatusEl.textContent = `${directoryPlugins.length} plugin${directoryPlugins.length !== 1 ? 's' : ''} in directory.`;
|
dirStatusEl.textContent = `${directoryPlugins.length} plugin${directoryPlugins.length !== 1 ? 's' : ''} in directory.`;
|
||||||
addLog(`Plugin directory fetched: ${directoryPlugins.length} entries`, 'success');
|
addLog(`Plugin directory fetched: ${directoryPlugins.length} entries`, 'success');
|
||||||
renderDirectoryList(dirSearchEl.value);
|
renderDirectoryList(dirSearchEl.value);
|
||||||
|
|||||||
Reference in New Issue
Block a user