Initial commit: Clean DSS implementation
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
This commit is contained in:
335
admin-ui/js/services/plugin-service.js
Normal file
335
admin-ui/js/services/plugin-service.js
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user