Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
336 lines
8.2 KiB
JavaScript
336 lines
8.2 KiB
JavaScript
/**
|
|
* Plugin Service - Manages frontend plugins for DSS Admin UI
|
|
*
|
|
* Supports loading, enabling/disabling, and configuring plugins.
|
|
* Plugins can extend UI, add commands, and integrate with services.
|
|
*/
|
|
|
|
import logger from '../core/logger.js';
|
|
|
|
class PluginService {
|
|
constructor() {
|
|
this.plugins = new Map();
|
|
this.hooks = new Map();
|
|
this.loadedPlugins = [];
|
|
this.storageKey = 'dss_plugins_config';
|
|
|
|
// Load saved plugin states
|
|
this.config = this._loadConfig();
|
|
}
|
|
|
|
/**
|
|
* Load plugin configuration from localStorage
|
|
*/
|
|
_loadConfig() {
|
|
try {
|
|
const saved = localStorage.getItem(this.storageKey);
|
|
return saved ? JSON.parse(saved) : { enabled: {}, settings: {} };
|
|
} catch (e) {
|
|
logger.warn('PluginService', 'Failed to load plugin config', e);
|
|
return { enabled: {}, settings: {} };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save plugin configuration to localStorage
|
|
*/
|
|
_saveConfig() {
|
|
try {
|
|
localStorage.setItem(this.storageKey, JSON.stringify(this.config));
|
|
} catch (e) {
|
|
logger.warn('PluginService', 'Failed to save plugin config', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a plugin
|
|
* @param {Object} plugin - Plugin definition
|
|
*/
|
|
register(plugin) {
|
|
if (!plugin.id || !plugin.name) {
|
|
throw new Error('Plugin must have id and name');
|
|
}
|
|
|
|
if (this.plugins.has(plugin.id)) {
|
|
logger.warn('PluginService', `Plugin ${plugin.id} already registered`);
|
|
return;
|
|
}
|
|
|
|
const pluginDef = {
|
|
id: plugin.id,
|
|
name: plugin.name,
|
|
version: plugin.version || '1.0.0',
|
|
description: plugin.description || '',
|
|
author: plugin.author || 'Unknown',
|
|
icon: plugin.icon || '🔌',
|
|
category: plugin.category || 'general',
|
|
|
|
// Lifecycle hooks
|
|
onInit: plugin.onInit || (() => {}),
|
|
onEnable: plugin.onEnable || (() => {}),
|
|
onDisable: plugin.onDisable || (() => {}),
|
|
onDestroy: plugin.onDestroy || (() => {}),
|
|
|
|
// Extension points
|
|
commands: plugin.commands || [],
|
|
panels: plugin.panels || [],
|
|
settings: plugin.settings || [],
|
|
|
|
// State
|
|
enabled: this.config.enabled[plugin.id] ?? plugin.enabledByDefault ?? false,
|
|
initialized: false
|
|
};
|
|
|
|
this.plugins.set(plugin.id, pluginDef);
|
|
logger.info('PluginService', `Registered plugin: ${plugin.name} v${pluginDef.version}`);
|
|
|
|
// Auto-init if enabled
|
|
if (pluginDef.enabled) {
|
|
this._initPlugin(pluginDef);
|
|
}
|
|
|
|
return pluginDef;
|
|
}
|
|
|
|
/**
|
|
* Initialize a plugin
|
|
*/
|
|
async _initPlugin(plugin) {
|
|
if (plugin.initialized) return;
|
|
|
|
try {
|
|
await plugin.onInit(this._createPluginContext(plugin));
|
|
plugin.initialized = true;
|
|
this.loadedPlugins.push(plugin.id);
|
|
logger.info('PluginService', `Initialized plugin: ${plugin.name}`);
|
|
} catch (e) {
|
|
logger.error('PluginService', `Failed to init plugin ${plugin.name}`, e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create context object passed to plugin hooks
|
|
*/
|
|
_createPluginContext(plugin) {
|
|
return {
|
|
pluginId: plugin.id,
|
|
settings: this.config.settings[plugin.id] || {},
|
|
|
|
// API for plugins
|
|
registerHook: (hookName, callback) => this.registerHook(plugin.id, hookName, callback),
|
|
unregisterHook: (hookName) => this.unregisterHook(plugin.id, hookName),
|
|
getSettings: () => this.config.settings[plugin.id] || {},
|
|
setSetting: (key, value) => this.setPluginSetting(plugin.id, key, value),
|
|
|
|
// Access to app APIs
|
|
emit: (event, data) => this._emitEvent(plugin.id, event, data),
|
|
log: (msg, data) => logger.info(`Plugin:${plugin.name}`, msg, data)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Enable a plugin
|
|
*/
|
|
async enable(pluginId) {
|
|
const plugin = this.plugins.get(pluginId);
|
|
if (!plugin) {
|
|
throw new Error(`Plugin ${pluginId} not found`);
|
|
}
|
|
|
|
if (plugin.enabled) return;
|
|
|
|
plugin.enabled = true;
|
|
this.config.enabled[pluginId] = true;
|
|
this._saveConfig();
|
|
|
|
if (!plugin.initialized) {
|
|
await this._initPlugin(plugin);
|
|
}
|
|
|
|
try {
|
|
await plugin.onEnable(this._createPluginContext(plugin));
|
|
logger.info('PluginService', `Enabled plugin: ${plugin.name}`);
|
|
this._emitEvent('system', 'plugin:enabled', { pluginId });
|
|
} catch (e) {
|
|
logger.error('PluginService', `Failed to enable plugin ${plugin.name}`, e);
|
|
plugin.enabled = false;
|
|
this.config.enabled[pluginId] = false;
|
|
this._saveConfig();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disable a plugin
|
|
*/
|
|
async disable(pluginId) {
|
|
const plugin = this.plugins.get(pluginId);
|
|
if (!plugin) {
|
|
throw new Error(`Plugin ${pluginId} not found`);
|
|
}
|
|
|
|
if (!plugin.enabled) return;
|
|
|
|
try {
|
|
await plugin.onDisable(this._createPluginContext(plugin));
|
|
plugin.enabled = false;
|
|
this.config.enabled[pluginId] = false;
|
|
this._saveConfig();
|
|
logger.info('PluginService', `Disabled plugin: ${plugin.name}`);
|
|
this._emitEvent('system', 'plugin:disabled', { pluginId });
|
|
} catch (e) {
|
|
logger.error('PluginService', `Failed to disable plugin ${plugin.name}`, e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle plugin enabled state
|
|
*/
|
|
async toggle(pluginId) {
|
|
const plugin = this.plugins.get(pluginId);
|
|
if (!plugin) {
|
|
throw new Error(`Plugin ${pluginId} not found`);
|
|
}
|
|
|
|
if (plugin.enabled) {
|
|
await this.disable(pluginId);
|
|
} else {
|
|
await this.enable(pluginId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a hook callback
|
|
*/
|
|
registerHook(pluginId, hookName, callback) {
|
|
const key = `${pluginId}:${hookName}`;
|
|
if (!this.hooks.has(hookName)) {
|
|
this.hooks.set(hookName, new Map());
|
|
}
|
|
this.hooks.get(hookName).set(pluginId, callback);
|
|
logger.debug('PluginService', `Registered hook ${hookName} for ${pluginId}`);
|
|
}
|
|
|
|
/**
|
|
* Unregister a hook callback
|
|
*/
|
|
unregisterHook(pluginId, hookName) {
|
|
const hookMap = this.hooks.get(hookName);
|
|
if (hookMap) {
|
|
hookMap.delete(pluginId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute all callbacks for a hook
|
|
*/
|
|
async executeHook(hookName, data = {}) {
|
|
const hookMap = this.hooks.get(hookName);
|
|
if (!hookMap) return [];
|
|
|
|
const results = [];
|
|
for (const [pluginId, callback] of hookMap) {
|
|
const plugin = this.plugins.get(pluginId);
|
|
if (plugin?.enabled) {
|
|
try {
|
|
const result = await callback(data);
|
|
results.push({ pluginId, result });
|
|
} catch (e) {
|
|
logger.error('PluginService', `Hook ${hookName} failed for ${pluginId}`, e);
|
|
}
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Set a plugin setting
|
|
*/
|
|
setPluginSetting(pluginId, key, value) {
|
|
if (!this.config.settings[pluginId]) {
|
|
this.config.settings[pluginId] = {};
|
|
}
|
|
this.config.settings[pluginId][key] = value;
|
|
this._saveConfig();
|
|
this._emitEvent(pluginId, 'settings:changed', { key, value });
|
|
}
|
|
|
|
/**
|
|
* Get plugin settings
|
|
*/
|
|
getPluginSettings(pluginId) {
|
|
return this.config.settings[pluginId] || {};
|
|
}
|
|
|
|
/**
|
|
* Emit event to listeners
|
|
*/
|
|
_emitEvent(source, event, data) {
|
|
window.dispatchEvent(new CustomEvent('dss:plugin:event', {
|
|
detail: { source, event, data }
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Get all registered plugins
|
|
*/
|
|
getAll() {
|
|
return Array.from(this.plugins.values());
|
|
}
|
|
|
|
/**
|
|
* Get enabled plugins
|
|
*/
|
|
getEnabled() {
|
|
return this.getAll().filter(p => p.enabled);
|
|
}
|
|
|
|
/**
|
|
* Get plugin by ID
|
|
*/
|
|
get(pluginId) {
|
|
return this.plugins.get(pluginId);
|
|
}
|
|
|
|
/**
|
|
* Get all commands from enabled plugins
|
|
*/
|
|
getAllCommands() {
|
|
const commands = [];
|
|
for (const plugin of this.getEnabled()) {
|
|
for (const cmd of plugin.commands) {
|
|
commands.push({
|
|
...cmd,
|
|
pluginId: plugin.id,
|
|
pluginName: plugin.name
|
|
});
|
|
}
|
|
}
|
|
return commands;
|
|
}
|
|
|
|
/**
|
|
* Get all panels from enabled plugins
|
|
*/
|
|
getAllPanels() {
|
|
const panels = [];
|
|
for (const plugin of this.getEnabled()) {
|
|
for (const panel of plugin.panels) {
|
|
panels.push({
|
|
...panel,
|
|
pluginId: plugin.id,
|
|
pluginName: plugin.name
|
|
});
|
|
}
|
|
}
|
|
return panels;
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
const pluginService = new PluginService();
|
|
|
|
export default pluginService;
|
|
export { PluginService };
|