diff --git a/src/PluginHost.js b/src/PluginHost.js index 367def1..5e52878 100644 --- a/src/PluginHost.js +++ b/src/PluginHost.js @@ -4,6 +4,11 @@ import { join, resolve, dirname } from 'path'; import { existsSync } from 'fs'; import { fileURLToPath } from 'url'; import fetch from 'node-fetch'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import readline from 'readline'; + +const execAsync = promisify(exec); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -291,6 +296,99 @@ export class PluginHost extends EventEmitter { return Array.from(this.pluginRegistry.values()); } + /** + * Install a plugin from its repository + */ + async installPlugin(pluginId) { + const pluginDef = this.pluginRegistry.get(pluginId); + if (!pluginDef) { + throw new Error(`Plugin ${pluginId} not found in registry`); + } + + const pluginPath = join(this.pluginsDir, pluginId); + if (existsSync(pluginPath)) { + console.log(`⚠ Plugin ${pluginId} already installed`); + return false; + } + + console.log(`šŸ“„ Installing ${pluginDef.name} v${pluginDef.version}...`); + + try { + await execAsync(`git clone ${pluginDef.repository} "${pluginPath}"`, { + cwd: this.pluginsDir + }); + + console.log(`āœ… Installed ${pluginDef.name}`); + this.emit('plugin-installed', pluginDef); + return true; + } catch (error) { + console.error(`āŒ Failed to install ${pluginId}:`, error.message); + return false; + } + } + + /** + * Interactive plugin installation + */ + async interactiveInstall() { + const available = this.getRegistry(); + const loadedIds = new Set(this.getLoadedPlugins().map(p => p.id)); + const installedPluginDirs = existsSync(this.pluginsDir) + ? await readdir(this.pluginsDir, { withFileTypes: true }).then(entries => + entries.filter(e => e.isDirectory()).map(e => e.name) + ) + : []; + const installedIds = new Set(installedPluginDirs); + + const installable = available.filter(p => !installedIds.has(p.id)); + + if (installable.length === 0) { + console.log('\n✨ All available plugins are already installed!'); + return; + } + + console.log('\nšŸ“¦ Available Plugins to Install:'); + console.log('================================'); + installable.forEach((p, i) => { + console.log(`${i + 1}. ${p.name} v${p.version} by ${p.author}`); + console.log(` ${p.description || 'No description'}`); + }); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + const question = (query) => new Promise(resolve => rl.question(query, resolve)); + + console.log('\nEnter plugin numbers to install (comma-separated), or "all" for all plugins:'); + const answer = await question('> '); + + rl.close(); + + let toInstall = []; + + if (answer.toLowerCase().trim() === 'all') { + toInstall = installable; + } else { + const indices = answer.split(',').map(s => parseInt(s.trim()) - 1).filter(i => !isNaN(i)); + toInstall = indices.map(i => installable[i]).filter(Boolean); + } + + if (toInstall.length === 0) { + console.log('\nāŒ No valid plugins selected'); + return; + } + + console.log(`\nšŸ“„ Installing ${toInstall.length} plugin(s)...`); + + for (const plugin of toInstall) { + await this.installPlugin(plugin.id); + } + + console.log('\nāœ… Installation complete!'); + } + /** * Shutdown the plugin host */ diff --git a/src/index.js b/src/index.js index 18e958b..c7edf0d 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,20 @@ const host = new PluginHost({ console.log('šŸ”Œ Initializing Plugin Host...\n'); await host.initialize(); + +// Check if user wants to install plugins interactively +const availableCount = host.getRegistry().length; +const alreadyInstalled = (await import('fs/promises')).readdir(host.pluginsDir, { withFileTypes: true }) + .then(entries => entries.filter(e => e.isDirectory()).length) + .catch(() => 0); + +const installedCount = await alreadyInstalled; + +if (availableCount > installedCount) { + await host.interactiveInstall(); + console.log(''); +} + await host.loadPlugins(); console.log('\nšŸ“Š Plugin Host Status:');