termcode
Version:
Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative
713 lines (712 loc) • 27.3 kB
JavaScript
import { promises as fs } from "node:fs";
import path from "node:path";
import { log } from "../util/logging.js";
import { hookManager } from "../hooks/manager.js";
import { workspaceManager } from "../workspace/manager.js";
/**
* Advanced Plugin System for TermCoder
* Provides extensibility beyond Claude Code's capabilities
*/
export class PluginSystem {
plugins = new Map();
loadedPlugins = new Map();
pluginConfigs = new Map();
eventEmitter = this.createEventEmitter();
pluginDir;
marketplaceUrl = 'https://registry.termcode.dev';
constructor(baseDir = path.join(process.env.HOME || "~", ".termcode")) {
this.pluginDir = path.join(baseDir, "plugins");
}
/**
* Initialize plugin system
*/
async initialize() {
try {
await fs.mkdir(this.pluginDir, { recursive: true });
await this.loadInstalledPlugins();
await this.registerBuiltinPlugins();
log.info(`Plugin system initialized with ${this.plugins.size} plugins`);
}
catch (error) {
log.error("Failed to initialize plugin system:", error);
throw error;
}
}
/**
* Install plugin from marketplace or local path
*/
async installPlugin(source, options = {}) {
log.info(`Installing plugin: ${source}`);
try {
let manifest;
let pluginPath;
if (source.startsWith('http://') || source.startsWith('https://')) {
// Install from URL
const result = await this.installFromUrl(source, options);
manifest = result.manifest;
pluginPath = result.path;
}
else if (source.includes('/') || source.includes('\\')) {
// Install from local path
const result = await this.installFromPath(source);
manifest = result.manifest;
pluginPath = result.path;
}
else {
// Install from marketplace
const result = await this.installFromMarketplace(source, options);
manifest = result.manifest;
pluginPath = result.path;
}
// Validate plugin
const validation = this.validatePlugin(manifest);
if (!validation.valid) {
throw new Error(`Plugin validation failed: ${validation.errors.join(', ')}`);
}
// Check dependencies
await this.resolveDependencies(manifest);
// Create plugin info
const pluginInfo = {
manifest,
installed: true,
enabled: true,
version: manifest.version,
installPath: pluginPath,
config: manifest.defaultConfig || {},
dependencies: [],
lastUpdated: Date.now(),
usage: {
lastUsed: 0,
usageCount: 0,
errorCount: 0
}
};
this.plugins.set(manifest.id, pluginInfo);
// Run installation lifecycle hook
if (manifest.lifecycle?.install) {
await this.runLifecycleHook(manifest.id, 'install');
}
// Activate plugin
await this.activatePlugin(manifest.id);
log.success(`Plugin installed: ${manifest.name} v${manifest.version}`);
return true;
}
catch (error) {
log.error(`Failed to install plugin ${source}:`, error);
return false;
}
}
/**
* Uninstall plugin
*/
async uninstallPlugin(pluginId) {
const plugin = this.plugins.get(pluginId);
if (!plugin) {
log.error(`Plugin not found: ${pluginId}`);
return false;
}
log.info(`Uninstalling plugin: ${plugin.manifest.name}`);
try {
// Deactivate plugin first
await this.deactivatePlugin(pluginId);
// Run uninstallation lifecycle hook
if (plugin.manifest.lifecycle?.uninstall) {
await this.runLifecycleHook(pluginId, 'uninstall');
}
// Remove plugin files
await fs.rm(plugin.installPath, { recursive: true, force: true });
// Remove from registry
this.plugins.delete(pluginId);
this.loadedPlugins.delete(pluginId);
this.pluginConfigs.delete(pluginId);
log.success(`Plugin uninstalled: ${plugin.manifest.name}`);
return true;
}
catch (error) {
log.error(`Failed to uninstall plugin ${pluginId}:`, error);
return false;
}
}
/**
* Activate plugin
*/
async activatePlugin(pluginId) {
const plugin = this.plugins.get(pluginId);
if (!plugin) {
log.error(`Plugin not found: ${pluginId}`);
return false;
}
if (plugin.enabled) {
log.warn(`Plugin already activated: ${pluginId}`);
return true;
}
log.info(`Activating plugin: ${plugin.manifest.name}`);
try {
// Load plugin code
const pluginModule = await this.loadPluginModule(plugin);
this.loadedPlugins.set(pluginId, pluginModule);
// Register hooks
if (plugin.manifest.hooks) {
for (const hookDef of plugin.manifest.hooks) {
await this.registerPluginHook(plugin, hookDef, pluginModule);
}
}
// Register commands
if (plugin.manifest.commands) {
for (const commandDef of plugin.manifest.commands) {
await this.registerPluginCommand(plugin, commandDef, pluginModule);
}
}
// Register providers
if (plugin.manifest.providers) {
for (const providerDef of plugin.manifest.providers) {
await this.registerPluginProvider(plugin, providerDef, pluginModule);
}
}
// Run activation lifecycle hook
if (plugin.manifest.lifecycle?.activate) {
await this.runLifecycleHook(pluginId, 'activate');
}
plugin.enabled = true;
log.success(`Plugin activated: ${plugin.manifest.name}`);
this.eventEmitter.emit('plugin:activated', { pluginId, plugin });
return true;
}
catch (error) {
log.error(`Failed to activate plugin ${pluginId}:`, error);
return false;
}
}
/**
* Deactivate plugin
*/
async deactivatePlugin(pluginId) {
const plugin = this.plugins.get(pluginId);
if (!plugin || !plugin.enabled) {
return true;
}
log.info(`Deactivating plugin: ${plugin.manifest.name}`);
try {
// Run deactivation lifecycle hook
if (plugin.manifest.lifecycle?.deactivate) {
await this.runLifecycleHook(pluginId, 'deactivate');
}
// Unregister hooks, commands, providers
await this.unregisterPluginIntegrations(pluginId);
// Unload plugin module
this.loadedPlugins.delete(pluginId);
plugin.enabled = false;
log.success(`Plugin deactivated: ${plugin.manifest.name}`);
this.eventEmitter.emit('plugin:deactivated', { pluginId, plugin });
return true;
}
catch (error) {
log.error(`Failed to deactivate plugin ${pluginId}:`, error);
return false;
}
}
/**
* Update plugin
*/
async updatePlugin(pluginId, version) {
const plugin = this.plugins.get(pluginId);
if (!plugin) {
log.error(`Plugin not found: ${pluginId}`);
return false;
}
log.info(`Updating plugin: ${plugin.manifest.name}`);
try {
// Check for updates
const updateInfo = await this.checkForUpdates(pluginId);
if (!updateInfo.hasUpdate && !version) {
log.info(`Plugin is up to date: ${plugin.manifest.name}`);
return true;
}
const targetVersion = version || updateInfo.latestVersion;
// Deactivate current version
await this.deactivatePlugin(pluginId);
// Install new version
const success = await this.installPlugin(plugin.manifest.id, {
version: targetVersion,
force: true
});
if (success) {
// Run update lifecycle hook
if (plugin.manifest.lifecycle?.update) {
await this.runLifecycleHook(pluginId, 'update');
}
log.success(`Plugin updated: ${plugin.manifest.name} to v${targetVersion}`);
this.eventEmitter.emit('plugin:updated', { pluginId, plugin, oldVersion: plugin.version, newVersion: targetVersion });
}
return success;
}
catch (error) {
log.error(`Failed to update plugin ${pluginId}:`, error);
return false;
}
}
/**
* List installed plugins
*/
listPlugins(filter) {
let plugins = Array.from(this.plugins.values());
if (filter) {
if (filter.type) {
plugins = plugins.filter(p => p.manifest.type === filter.type);
}
if (filter.category) {
plugins = plugins.filter(p => p.manifest.category === filter.category);
}
if (filter.enabled !== undefined) {
plugins = plugins.filter(p => p.enabled === filter.enabled);
}
if (filter.keyword) {
plugins = plugins.filter(p => p.manifest.name.toLowerCase().includes(filter.keyword.toLowerCase()) ||
p.manifest.description.toLowerCase().includes(filter.keyword.toLowerCase()) ||
p.manifest.keywords?.some(k => k.toLowerCase().includes(filter.keyword.toLowerCase())));
}
}
return plugins;
}
/**
* Search marketplace for plugins
*/
async searchMarketplace(query, options) {
try {
const searchUrl = new URL('/search', this.marketplaceUrl);
searchUrl.searchParams.set('q', query);
if (options?.type)
searchUrl.searchParams.set('type', options.type);
if (options?.category)
searchUrl.searchParams.set('category', options.category);
if (options?.sort)
searchUrl.searchParams.set('sort', options.sort);
if (options?.limit)
searchUrl.searchParams.set('limit', options.limit.toString());
const response = await fetch(searchUrl.toString());
if (!response.ok) {
throw new Error(`Marketplace search failed: ${response.statusText}`);
}
return await response.json();
}
catch (error) {
log.error('Marketplace search failed:', error);
return [];
}
}
/**
* Get plugin information
*/
getPlugin(pluginId) {
return this.plugins.get(pluginId);
}
/**
* Configure plugin
*/
async configurePlugin(pluginId, config) {
const plugin = this.plugins.get(pluginId);
if (!plugin) {
log.error(`Plugin not found: ${pluginId}`);
return false;
}
try {
// Validate configuration against schema
if (plugin.manifest.configSchema) {
const validation = this.validateConfig(config, plugin.manifest.configSchema);
if (!validation.valid) {
throw new Error(`Configuration validation failed: ${validation.errors.join(', ')}`);
}
}
// Merge with existing config
plugin.config = { ...plugin.config, ...config };
this.pluginConfigs.set(pluginId, plugin.config);
// Save to disk
await this.savePluginConfig(pluginId, plugin.config);
// Notify plugin of config change
const pluginModule = this.loadedPlugins.get(pluginId);
if (pluginModule && pluginModule.onConfigChange) {
await pluginModule.onConfigChange(plugin.config);
}
log.info(`Plugin configured: ${plugin.manifest.name}`);
this.eventEmitter.emit('plugin:configured', { pluginId, config });
return true;
}
catch (error) {
log.error(`Failed to configure plugin ${pluginId}:`, error);
return false;
}
}
/**
* Execute plugin command
*/
async executePluginCommand(pluginId, commandName, args = []) {
const plugin = this.plugins.get(pluginId);
if (!plugin || !plugin.enabled) {
throw new Error(`Plugin not found or not enabled: ${pluginId}`);
}
const pluginModule = this.loadedPlugins.get(pluginId);
if (!pluginModule) {
throw new Error(`Plugin module not loaded: ${pluginId}`);
}
const commandDef = plugin.manifest.commands?.find(c => c.name === commandName);
if (!commandDef) {
throw new Error(`Command not found: ${commandName} in plugin ${pluginId}`);
}
try {
// Update usage statistics
plugin.usage.lastUsed = Date.now();
plugin.usage.usageCount++;
// Create plugin context
const context = this.createPluginContext(plugin);
// Execute command
const handler = pluginModule[commandDef.handler];
if (!handler || typeof handler !== 'function') {
throw new Error(`Command handler not found: ${commandDef.handler}`);
}
const result = await handler.call(pluginModule, context, ...args);
this.eventEmitter.emit('plugin:command', { pluginId, commandName, args, result });
return result;
}
catch (error) {
plugin.usage.errorCount++;
log.error(`Plugin command failed: ${pluginId}.${commandName}:`, error);
throw error;
}
}
/**
* Private implementation methods
*/
async loadInstalledPlugins() {
try {
const entries = await fs.readdir(this.pluginDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
try {
const manifestPath = path.join(this.pluginDir, entry.name, 'plugin.json');
const manifestContent = await fs.readFile(manifestPath, 'utf8');
const manifest = JSON.parse(manifestContent);
const pluginInfo = {
manifest,
installed: true,
enabled: false, // Will be enabled based on config
version: manifest.version,
installPath: path.join(this.pluginDir, entry.name),
config: manifest.defaultConfig || {},
dependencies: [],
lastUpdated: Date.now(),
usage: {
lastUsed: 0,
usageCount: 0,
errorCount: 0
}
};
this.plugins.set(manifest.id, pluginInfo);
}
catch (error) {
log.warn(`Failed to load plugin in ${entry.name}:`, error);
}
}
}
}
catch (error) {
// Plugin directory doesn't exist yet
log.debug('Plugin directory not found, will be created');
}
}
async registerBuiltinPlugins() {
// Register built-in plugins
const builtinPlugins = [
{
id: 'termcode-git-enhanced',
name: 'Enhanced Git Integration',
type: 'integration',
description: 'Advanced Git workflow management'
},
{
id: 'termcode-ai-suggestions',
name: 'AI Code Suggestions',
type: 'tool',
description: 'Real-time AI-powered code suggestions'
},
{
id: 'termcode-security-scanner',
name: 'Security Scanner',
type: 'tool',
description: 'Advanced security vulnerability scanning'
}
];
// These would be implemented as actual plugin modules
log.debug(`${builtinPlugins.length} built-in plugins available`);
}
async installFromMarketplace(pluginId, options) {
// Implement marketplace installation
throw new Error('Marketplace installation not implemented yet');
}
async installFromUrl(url, options) {
// Implement URL installation
throw new Error('URL installation not implemented yet');
}
async installFromPath(sourcePath) {
const manifestPath = path.join(sourcePath, 'plugin.json');
const manifestContent = await fs.readFile(manifestPath, 'utf8');
const manifest = JSON.parse(manifestContent);
const targetPath = path.join(this.pluginDir, manifest.id);
// Copy plugin files
await fs.cp(sourcePath, targetPath, { recursive: true });
return { manifest, path: targetPath };
}
validatePlugin(manifest) {
const errors = [];
if (!manifest.id)
errors.push('Plugin ID is required');
if (!manifest.name)
errors.push('Plugin name is required');
if (!manifest.version)
errors.push('Plugin version is required');
if (!manifest.entry)
errors.push('Plugin entry point is required');
if (!manifest.type)
errors.push('Plugin type is required');
// Check version compatibility
if (manifest.termcodeVersion && !this.isVersionCompatible(manifest.termcodeVersion)) {
errors.push(`Plugin requires TermCode version ${manifest.termcodeVersion}`);
}
return { valid: errors.length === 0, errors };
}
async resolveDependencies(manifest) {
if (!manifest.dependencies)
return;
for (const [depId, version] of Object.entries(manifest.dependencies)) {
const existingPlugin = this.plugins.get(depId);
if (!existingPlugin) {
// Try to install dependency
log.info(`Installing dependency: ${depId}@${version}`);
const success = await this.installPlugin(depId, { version });
if (!success) {
throw new Error(`Failed to install dependency: ${depId}@${version}`);
}
}
}
}
async loadPluginModule(plugin) {
const entryPath = path.join(plugin.installPath, plugin.manifest.entry);
// Dynamic import of plugin module
const module = await import(entryPath);
return module.default || module;
}
createPluginContext(plugin) {
return {
repoPath: process.cwd(),
workspaceManager,
hookManager,
log,
config: plugin.config,
api: this.createPluginAPI(plugin)
};
}
createPluginAPI(plugin) {
return {
executeCommand: async (command) => {
// Implement secure command execution
throw new Error('Not implemented');
},
readFile: async (filePath) => {
return await fs.readFile(filePath, 'utf8');
},
writeFile: async (filePath, content) => {
await fs.writeFile(filePath, content, 'utf8');
},
getCurrentWorkspace: () => {
return workspaceManager.getCurrentWorkspace();
},
updateWorkspace: async (updates) => {
// Implement workspace updates
},
addHook: async (hook) => {
await hookManager.addHook(hook);
},
removeHook: async (hookId) => {
await hookManager.removeHook(hookId);
},
getProvider: (providerId) => {
// Implement provider access
return null;
},
registerProvider: async (provider) => {
// Implement provider registration
},
showMessage: (message, type = 'info') => {
switch (type) {
case 'info':
log.info(message);
break;
case 'warn':
log.warn(message);
break;
case 'error':
log.error(message);
break;
default:
log.info(message);
}
},
showProgress: (message, progress) => {
log.raw(`${message} ${Math.round(progress)}%`);
},
getConfig: (key) => {
return plugin.config[key];
},
setConfig: async (key, value) => {
plugin.config[key] = value;
await this.savePluginConfig(plugin.manifest.id, plugin.config);
},
emit: (event, data) => {
this.eventEmitter.emit(event, data);
},
on: (event, handler) => {
this.eventEmitter.on(event, handler);
},
off: (event, handler) => {
if (handler) {
this.eventEmitter.off(event, handler);
}
else {
this.eventEmitter.removeAllListeners(event);
}
}
};
}
async registerPluginHook(plugin, hookDef, pluginModule) {
// Register hook with hook manager
const hook = {
id: `${plugin.manifest.id}_${hookDef.type}`,
name: `${plugin.manifest.name} ${hookDef.type}`,
description: `Hook from plugin: ${plugin.manifest.name}`,
type: hookDef.type,
matcher: {},
handler: {
type: 'javascript',
function: hookDef.handler
},
priority: hookDef.priority || 100,
enabled: true,
timeout: 30000,
retries: 0
};
await hookManager.addHook(hook);
}
async registerPluginCommand(plugin, commandDef, pluginModule) {
// Commands are registered in the plugin info and executed via executePluginCommand
log.debug(`Registered command: ${commandDef.name} from plugin ${plugin.manifest.name}`);
}
async registerPluginProvider(plugin, providerDef, pluginModule) {
// Register AI provider
log.debug(`Registered provider: ${providerDef.id} from plugin ${plugin.manifest.name}`);
}
async unregisterPluginIntegrations(pluginId) {
// Remove hooks, commands, providers registered by this plugin
const plugin = this.plugins.get(pluginId);
if (!plugin)
return;
// Remove hooks
if (plugin.manifest.hooks) {
for (const hookDef of plugin.manifest.hooks) {
const hookId = `${pluginId}_${hookDef.type}`;
await hookManager.removeHook(hookId);
}
}
}
async runLifecycleHook(pluginId, lifecycle) {
const plugin = this.plugins.get(pluginId);
if (!plugin)
return;
const pluginModule = this.loadedPlugins.get(pluginId);
if (!pluginModule)
return;
const lifecycleHandler = plugin.manifest.lifecycle?.[lifecycle];
if (!lifecycleHandler)
return;
const handler = pluginModule[lifecycleHandler];
if (handler && typeof handler === 'function') {
const context = this.createPluginContext(plugin);
await handler.call(pluginModule, context);
}
}
async checkForUpdates(pluginId) {
// Check marketplace for updates
return { hasUpdate: false };
}
validateConfig(config, schema) {
// Implement JSON schema validation
return { valid: true, errors: [] };
}
async savePluginConfig(pluginId, config) {
const configPath = path.join(this.pluginDir, pluginId, 'config.json');
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
}
isVersionCompatible(requiredVersion) {
// Implement version compatibility checking
return true;
}
createEventEmitter() {
// Simple event emitter implementation
const events = new Map();
return {
emit: (event, data) => {
const handlers = events.get(event) || [];
handlers.forEach(handler => {
try {
handler(data);
}
catch (error) {
log.error(`Event handler error for ${event}:`, error);
}
});
},
on: (event, handler) => {
if (!events.has(event)) {
events.set(event, []);
}
events.get(event).push(handler);
},
off: (event, handler) => {
const handlers = events.get(event) || [];
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
},
removeAllListeners: (event) => {
events.delete(event);
}
};
}
/**
* Get plugin statistics
*/
getPluginStats() {
const plugins = Array.from(this.plugins.values());
const enabled = plugins.filter(p => p.enabled);
const byType = {};
plugins.forEach(p => {
byType[p.manifest.type] = (byType[p.manifest.type] || 0) + 1;
});
const mostUsed = plugins
.sort((a, b) => b.usage.usageCount - a.usage.usageCount)
.slice(0, 5)
.map(p => ({
id: p.manifest.id,
name: p.manifest.name,
usage: p.usage.usageCount
}));
return {
totalPlugins: plugins.length,
enabledPlugins: enabled.length,
pluginsByType: byType,
mostUsedPlugins: mostUsed,
recentErrors: []
};
}
}
// Export singleton instance
export const pluginSystem = new PluginSystem();