Skip to content

Commit f2da6ad

Browse files
Merge pull request #51 from kaifcoder/feature/plugin-hook-execution-pipeline
feat: implement robust plugin hook execution pipeline
2 parents 49859f3 + 6d9805b commit f2da6ad

File tree

10 files changed

+2410
-23
lines changed

10 files changed

+2410
-23
lines changed

bin/index.js

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,255 @@ program
199199
process.exit(1);
200200
}
201201
});
202+
203+
// Plugin management commands
204+
const pluginCmd = program
205+
.command('plugin')
206+
.description('Manage plugins');
207+
208+
pluginCmd
209+
.command('list')
210+
.description('List all plugins in the current workspace')
211+
.option('--json', 'Output as JSON')
212+
.option('--enabled-only', 'Show only enabled plugins')
213+
.action(async (opts) => {
214+
try {
215+
const { pluginSystem } = await import('./lib/plugin-system.js');
216+
const cwd = process.cwd();
217+
218+
await pluginSystem.initialize(cwd);
219+
const plugins = pluginSystem.getAllPlugins();
220+
221+
let filteredPlugins = plugins;
222+
if (opts.enabledOnly) {
223+
filteredPlugins = plugins.filter(p => p.enabled);
224+
}
225+
226+
if (opts.json) {
227+
console.log(JSON.stringify(filteredPlugins, null, 2));
228+
} else {
229+
if (filteredPlugins.length === 0) {
230+
console.log(chalk.yellow('No plugins found.'));
231+
return;
232+
}
233+
234+
console.log(chalk.blue(`\n📦 Found ${filteredPlugins.length} plugin(s):\n`));
235+
for (const plugin of filteredPlugins) {
236+
const status = plugin.enabled ? chalk.green('enabled') : chalk.red('disabled');
237+
const type = plugin.type === 'local' ? chalk.cyan('local') : chalk.magenta('external');
238+
console.log(` ${chalk.bold(plugin.name)} [${status}] (${type})`);
239+
if (plugin.plugin?.description) {
240+
console.log(` ${chalk.gray(plugin.plugin.description)}`);
241+
}
242+
if (plugin.plugin?.version) {
243+
console.log(` ${chalk.gray('v' + plugin.plugin.version)}`);
244+
}
245+
console.log();
246+
}
247+
}
248+
} catch (e) {
249+
console.error(chalk.red('Failed to list plugins:'), e.message);
250+
process.exit(1);
251+
}
252+
});
253+
254+
pluginCmd
255+
.command('enable')
256+
.description('Enable a plugin')
257+
.argument('<name>', 'Plugin name')
258+
.action(async (name) => {
259+
try {
260+
const { pluginSystem } = await import('./lib/plugin-system.js');
261+
const cwd = process.cwd();
262+
263+
await pluginSystem.initialize(cwd);
264+
await pluginSystem.enablePlugin(name);
265+
266+
console.log(chalk.green(`✅ Plugin '${name}' enabled successfully`));
267+
} catch (e) {
268+
console.error(chalk.red('Failed to enable plugin:'), e.message);
269+
process.exit(1);
270+
}
271+
});
272+
273+
pluginCmd
274+
.command('disable')
275+
.description('Disable a plugin')
276+
.argument('<name>', 'Plugin name')
277+
.action(async (name) => {
278+
try {
279+
const { pluginSystem } = await import('./lib/plugin-system.js');
280+
const cwd = process.cwd();
281+
282+
await pluginSystem.initialize(cwd);
283+
await pluginSystem.disablePlugin(name);
284+
285+
console.log(chalk.green(`✅ Plugin '${name}' disabled successfully`));
286+
} catch (e) {
287+
console.error(chalk.red('Failed to disable plugin:'), e.message);
288+
process.exit(1);
289+
}
290+
});
291+
292+
pluginCmd
293+
.command('info')
294+
.description('Show detailed information about a plugin')
295+
.argument('<name>', 'Plugin name')
296+
.option('--json', 'Output as JSON')
297+
.action(async (name, opts) => {
298+
try {
299+
const { pluginSystem, HOOK_POINTS } = await import('./lib/plugin-system.js');
300+
const cwd = process.cwd();
301+
302+
await pluginSystem.initialize(cwd);
303+
const plugin = pluginSystem.getPlugin(name);
304+
305+
if (!plugin) {
306+
console.log(chalk.yellow(`Plugin '${name}' not found.`));
307+
process.exit(1);
308+
}
309+
310+
if (opts.json) {
311+
const info = {
312+
name: plugin.name,
313+
type: plugin.type,
314+
enabled: plugin.enabled,
315+
loadedAt: plugin.loadedAt,
316+
plugin: plugin.plugin,
317+
config: plugin.config
318+
};
319+
console.log(JSON.stringify(info, null, 2));
320+
} else {
321+
console.log(chalk.blue(`\n📦 Plugin: ${chalk.bold(plugin.name)}\n`));
322+
console.log(`Type: ${plugin.type === 'local' ? chalk.cyan('Local') : chalk.magenta('External')}`);
323+
console.log(`Status: ${plugin.enabled ? chalk.green('Enabled') : chalk.red('Disabled')}`);
324+
console.log(`Loaded: ${plugin.loadedAt ? new Date(plugin.loadedAt).toLocaleString() : 'Not loaded'}`);
325+
326+
if (plugin.plugin) {
327+
if (plugin.plugin.version) {
328+
console.log(`Version: ${plugin.plugin.version}`);
329+
}
330+
if (plugin.plugin.description) {
331+
console.log(`Description: ${plugin.plugin.description}`);
332+
}
333+
334+
if (plugin.plugin.hooks) {
335+
console.log(`\nHooks (${Object.keys(plugin.plugin.hooks).length}):`);
336+
for (const [hookName, handler] of Object.entries(plugin.plugin.hooks)) {
337+
const description = HOOK_POINTS[hookName] || 'Custom hook';
338+
console.log(` ${chalk.cyan(hookName)} - ${chalk.gray(description)}`);
339+
}
340+
}
341+
342+
if (plugin.plugin.methods) {
343+
console.log(`\nMethods (${Object.keys(plugin.plugin.methods).length}):`);
344+
for (const methodName of Object.keys(plugin.plugin.methods)) {
345+
console.log(` ${chalk.cyan(methodName)}`);
346+
}
347+
}
348+
}
349+
350+
if (plugin.config) {
351+
console.log(`\nConfiguration:`);
352+
console.log(JSON.stringify(plugin.config, null, 2));
353+
}
354+
console.log();
355+
}
356+
} catch (e) {
357+
console.error(chalk.red('Failed to get plugin info:'), e.message);
358+
process.exit(1);
359+
}
360+
});
361+
362+
pluginCmd
363+
.command('configure')
364+
.description('Configure a plugin')
365+
.argument('<name>', 'Plugin name')
366+
.option('--config <json>', 'Configuration as JSON string')
367+
.option('--priority <number>', 'Plugin loading priority (higher = loads first)')
368+
.option('--external <path>', 'Set external plugin path or npm package')
369+
.action(async (name, opts) => {
370+
try {
371+
const { pluginSystem } = await import('./lib/plugin-system.js');
372+
const cwd = process.cwd();
373+
374+
await pluginSystem.initialize(cwd);
375+
376+
const config = {};
377+
378+
if (opts.config) {
379+
try {
380+
Object.assign(config, JSON.parse(opts.config));
381+
} catch (e) {
382+
console.error(chalk.red('Invalid JSON in --config option'));
383+
process.exit(1);
384+
}
385+
}
386+
387+
if (opts.priority !== undefined) {
388+
config.priority = parseInt(opts.priority);
389+
}
390+
391+
if (opts.external) {
392+
config.external = opts.external;
393+
}
394+
395+
if (Object.keys(config).length === 0) {
396+
console.log(chalk.yellow('No configuration options provided. Use --config, --priority, or --external.'));
397+
process.exit(1);
398+
}
399+
400+
await pluginSystem.configurePlugin(name, config);
401+
402+
console.log(chalk.green(`✅ Plugin '${name}' configured successfully`));
403+
console.log('New configuration:', JSON.stringify(config, null, 2));
404+
} catch (e) {
405+
console.error(chalk.red('Failed to configure plugin:'), e.message);
406+
process.exit(1);
407+
}
408+
});
409+
410+
pluginCmd
411+
.command('stats')
412+
.description('Show plugin system statistics')
413+
.option('--json', 'Output as JSON')
414+
.action(async (opts) => {
415+
try {
416+
const { pluginSystem } = await import('./lib/plugin-system.js');
417+
const cwd = process.cwd();
418+
419+
await pluginSystem.initialize(cwd);
420+
const stats = pluginSystem.getStats();
421+
422+
if (opts.json) {
423+
console.log(JSON.stringify(stats, null, 2));
424+
} else {
425+
console.log(chalk.blue('\n📊 Plugin System Statistics\n'));
426+
console.log(`Initialized: ${stats.initialized ? chalk.green('Yes') : chalk.red('No')}`);
427+
console.log(`Project Directory: ${stats.projectDir || 'N/A'}`);
428+
console.log(`Total Plugins: ${stats.totalPlugins}`);
429+
console.log(`Enabled Plugins: ${stats.enabledPlugins}`);
430+
console.log(`Available Hook Points: ${stats.hookPoints}`);
431+
432+
console.log('\nRegistered Hooks:');
433+
for (const [hookName, count] of Object.entries(stats.registeredHooks)) {
434+
console.log(` ${chalk.cyan(hookName)}: ${count} handler(s)`);
435+
}
436+
437+
if (Object.keys(stats.config).length > 0) {
438+
console.log('\nPlugin Configuration:');
439+
for (const [pluginName, config] of Object.entries(stats.config)) {
440+
console.log(` ${chalk.bold(pluginName)}:`);
441+
console.log(` ${JSON.stringify(config, null, 4).replace(/^/gm, ' ')}`);
442+
}
443+
}
444+
console.log();
445+
}
446+
} catch (e) {
447+
console.error(chalk.red('Failed to get plugin stats:'), e.message);
448+
process.exit(1);
449+
}
450+
});
202451

203452
program.parse();
204453

bin/lib/admin.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { spawn } from 'node:child_process';
66
import { WebSocketServer, WebSocket } from 'ws';
77
import { getLogsForAPI, LogFileWatcher } from './logs.js';
88
import { startService, stopService, restartService, getServiceStatus, getAllServiceStatuses, validateServiceCanRun } from './service-manager.js';
9+
import { initializePlugins, callHook } from './plugin-system.js';
910

1011
// ws helper
1112
function sendWebSocketMessage(ws, message) {
@@ -886,6 +887,15 @@ export async function startAdminDashboard(options = {}) {
886887
process.exit(1);
887888
}
888889

890+
// Initialize plugins
891+
await initializePlugins(cwd);
892+
893+
// Call before:admin:start hook
894+
await callHook('before:admin:start', {
895+
projectDir: cwd,
896+
options
897+
});
898+
889899
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
890900
const port = options.port || 8080;
891901
const refreshInterval = options.refresh || 5000;
@@ -1064,9 +1074,18 @@ export async function startAdminDashboard(options = {}) {
10641074
res.end(html);
10651075
});
10661076

1067-
server.listen(port, () => {
1077+
server.listen(port, async () => {
10681078
console.log(chalk.green(`✅ Admin Dashboard running at http://localhost:${port}`));
10691079

1080+
// Call after:admin:start hook
1081+
await callHook('after:admin:start', {
1082+
projectDir: cwd,
1083+
port,
1084+
dashboardUrl: `http://localhost:${port}`,
1085+
options,
1086+
services: cfg.services
1087+
});
1088+
10701089
// Auto-open browser if requested
10711090
if (options.open !== false) {
10721091
const url = `http://localhost:${port}`;

bin/lib/dev.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import chalk from 'chalk';
44
import { spawn } from 'node:child_process';
55
import http from 'http';
66
import { initializeServiceLogs } from './logs.js';
7+
import { initializePlugins, callHook } from './plugin-system.js';
78
import { fileURLToPath } from 'url';
89

910
const __filename = fileURLToPath(import.meta.url);
@@ -70,6 +71,17 @@ export async function runDev({ docker=false } = {}) {
7071
console.error(chalk.red('polyglot.json not found. Run inside a generated workspace.'));
7172
process.exit(1);
7273
}
74+
75+
// Initialize plugins
76+
await initializePlugins(cwd);
77+
78+
// Call before:dev:start hook
79+
await callHook('before:dev:start', {
80+
projectDir: cwd,
81+
docker,
82+
mode: docker ? 'docker' : 'local'
83+
});
84+
7385
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
7486
const servicesDir = path.join(cwd, 'services');
7587
if (!fs.existsSync(servicesDir)) {
@@ -164,9 +176,31 @@ const args = ['run', useScript];
164176
}
165177
await Promise.all(healthPromises);
166178

179+
// Call after:dev:start hook
180+
await callHook('after:dev:start', {
181+
projectDir: cwd,
182+
docker,
183+
mode: docker ? 'docker' : 'local',
184+
processes: procs.length,
185+
services: procs.map(p => p.serviceName).filter(Boolean)
186+
});
187+
167188
if (procs.length > 0) {
168189
console.log(chalk.blue('Watching services. Press Ctrl+C to exit.'));
169-
process.on('SIGINT', () => { procs.forEach(p => p.kill('SIGINT')); process.exit(0); });
190+
process.on('SIGINT', async () => {
191+
await callHook('before:dev:stop', {
192+
projectDir: cwd,
193+
docker,
194+
mode: docker ? 'docker' : 'local'
195+
});
196+
procs.forEach(p => p.kill('SIGINT'));
197+
await callHook('after:dev:stop', {
198+
projectDir: cwd,
199+
docker,
200+
mode: docker ? 'docker' : 'local'
201+
});
202+
process.exit(0);
203+
});
170204
}
171205
}
172206

0 commit comments

Comments
 (0)