kukuy 1.6.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "Verificador de Salud Periódica",
3
+ "version": "1.0.0",
4
+ "description": "Plugin para verificar periódicamente la salud de los servidores backend",
5
+ "author": "Sistema Kukuy",
6
+ "main": "index.js",
7
+ "kukuyVersion": "^1.6.0",
8
+ "hooks": [
9
+ "onHealthCheckStart",
10
+ "onHealthCheckComplete",
11
+ "onHealthCheckError",
12
+ "onHealthCheckTimeout",
13
+ "onHealthCheckException"
14
+ ],
15
+ "enabled": true
16
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Plugin de Monitor de Salud de Servidores para Kukuy
3
+ * Monitorea eventos de verificación de salud de servidores backend
4
+ */
5
+
6
+ async function init(balancer) {
7
+ console.log('Inicializando plugin de monitor de salud de servidores...');
8
+
9
+ const extension = balancer.getPostStartupExtension();
10
+
11
+ // Hook que se ejecuta cuando comienza la verificación de salud de un servidor
12
+ extension.registerHook('onHealthCheckStart', async ({ server, checkTime }) => {
13
+ console.log(`[HEALTH-MONITOR] Iniciando verificación de salud para servidor: ${server.url} (${checkTime})`);
14
+ }, 5);
15
+
16
+ // Hook que se ejecuta cuando se completa la verificación de salud
17
+ extension.registerHook('onHealthCheckComplete', async ({ server, isHealthy, statusCode, checkTime }) => {
18
+ const status = isHealthy ? 'SALUDABLE' : 'NO SALUDABLE';
19
+ console.log(`[HEALTH-MONITOR] Verificación completada para ${server.url} - Estado: ${status} - Código: ${statusCode} (${checkTime})`);
20
+
21
+ // Aquí podrías agregar lógica adicional como alertas o métricas personalizadas
22
+ if (!isHealthy) {
23
+ console.warn(`[HEALTH-MONITOR] ¡Alerta! Servidor ${server.url} no está saludable`);
24
+ }
25
+ }, 5);
26
+
27
+ // Hook que se ejecuta cuando ocurre un error durante la verificación de salud
28
+ extension.registerHook('onHealthCheckError', async ({ server, error, checkTime }) => {
29
+ console.error(`[HEALTH-MONITOR] Error en verificación de salud para ${server.url}: ${error.message} (${checkTime})`);
30
+
31
+ // Lógica para manejar errores de verificación
32
+ console.warn(`[HEALTH-MONITOR] ¡Error crítico! Servidor ${server.url} no pudo ser verificado`);
33
+ }, 10); // Prioridad alta para errores
34
+
35
+ // Hook que se ejecuta cuando ocurre un timeout durante la verificación de salud
36
+ extension.registerHook('onHealthCheckTimeout', async ({ server, checkTime }) => {
37
+ console.warn(`[HEALTH-MONITOR] Timeout en verificación de salud para ${server.url} (${checkTime})`);
38
+
39
+ // Lógica para manejar timeouts
40
+ console.warn(`[HEALTH-MONITOR] ¡Timeout! Servidor ${server.url} no respondió a tiempo`);
41
+ }, 10); // Prioridad alta para timeouts
42
+
43
+ // Hook que se ejecuta cuando ocurre una excepción durante la verificación de salud
44
+ extension.registerHook('onHealthCheckException', async ({ server, error, checkTime }) => {
45
+ console.error(`[HEALTH-MONITOR] Excepción en verificación de salud para ${server.url}: ${error.message} (${checkTime})`);
46
+
47
+ // Lógica para manejar excepciones
48
+ console.error(`[HEALTH-MONITOR] ¡Excepción crítica! Error inesperado verificando ${server.url}`);
49
+ }, 10); // Prioridad alta para excepciones
50
+
51
+ console.log('Plugin de monitor de salud de servidores inicializado correctamente');
52
+ }
53
+
54
+ async function deinit(balancer) {
55
+ console.log('Desactivando plugin de monitor de salud de servidores...');
56
+ }
57
+
58
+ module.exports = { init, deinit };
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "Monitor de Salud de Servidores",
3
+ "version": "1.0.0",
4
+ "description": "Plugin para monitorear eventos de verificación de salud de servidores backend",
5
+ "author": "Sistema Kukuy",
6
+ "main": "index.js",
7
+ "kukuyVersion": "^1.6.0",
8
+ "hooks": [
9
+ "onHealthCheckStart",
10
+ "onHealthCheckComplete",
11
+ "onHealthCheckError",
12
+ "onHealthCheckTimeout",
13
+ "onHealthCheckException"
14
+ ],
15
+ "enabled": true
16
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Plugin de Redirección para Kukuy
3
+ * Permite implementar reglas de redirección personalizadas
4
+ */
5
+
6
+ // Reglas de redirección
7
+ let redirectRules = [
8
+ // Ejemplo: redirigir /old-path a /new-path permanentemente
9
+ // { pattern: /^\/old-path$/, target: '/new-path', type: 'permanent' },
10
+ ];
11
+
12
+ /**
13
+ * Inicializa el plugin de redirección
14
+ * @param {Object} balancer - Instancia del balanceador
15
+ */
16
+ async function init(balancer) {
17
+ console.log('Inicializando plugin de redirección...');
18
+
19
+ const extension = balancer.getPostStartupExtension();
20
+
21
+ // Registrar filtro para procesar solicitudes
22
+ extension.registerFilter('request_processing', requestProcessingFilter, 2);
23
+
24
+ // Registrar hook para cuando se recibe una solicitud
25
+ extension.registerHook('onRequestReceived', onRequestReceivedHook, 2);
26
+
27
+ // Cargar reglas de redirección desde el entorno
28
+ loadRedirectRulesFromEnv();
29
+
30
+ console.log('Plugin de redirección inicializado correctamente');
31
+ }
32
+
33
+ /**
34
+ * Carga reglas de redirección desde variables de entorno
35
+ */
36
+ function loadRedirectRulesFromEnv() {
37
+ if (process.env.REDIRECT_RULES) {
38
+ try {
39
+ const rules = JSON.parse(process.env.REDIRECT_RULES);
40
+ redirectRules = [...redirectRules, ...rules];
41
+ console.log(`[REDIRECT-PLUGIN] Cargadas ${rules.length} reglas de redirección desde entorno`);
42
+ } catch (error) {
43
+ console.error('[REDIRECT-PLUGIN] Error al parsear reglas de redirección desde entorno:', error.message);
44
+ }
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Filtro para procesar solicitudes
50
+ * @param {Object} data - Datos de la solicitud
51
+ * @returns {Object} - Resultado del procesamiento
52
+ */
53
+ async function requestProcessingFilter(data) {
54
+ const { req, res } = data;
55
+
56
+ // Buscar una regla de redirección que coincida
57
+ for (const rule of redirectRules) {
58
+ if (rule.pattern instanceof RegExp) {
59
+ // Si el patrón es una expresión regular
60
+ if (rule.pattern.test(req.url)) {
61
+ // Encontramos una coincidencia, realizar la redirección
62
+ const target = typeof rule.target === 'function'
63
+ ? rule.target(req.url)
64
+ : rule.target;
65
+
66
+ const statusCode = rule.type === 'permanent' ? 301 : 302;
67
+
68
+ res.writeHead(statusCode, {
69
+ 'Location': target,
70
+ 'Content-Type': 'text/html'
71
+ });
72
+
73
+ res.end(`<html><body>Redirigido a <a href="${target}">${target}</a></body></html>`);
74
+
75
+ console.log(`[REDIRECT-PLUGIN] Redirección ${statusCode} de ${req.url} a ${target}`);
76
+
77
+ return {
78
+ allowed: false, // Indicar que no debe continuar con el procesamiento normal
79
+ redirected: true,
80
+ redirectUrl: target,
81
+ statusCode
82
+ };
83
+ }
84
+ } else if (typeof rule.pattern === 'string') {
85
+ // Si el patrón es una cadena, verificar coincidencia exacta o parcial
86
+ if (req.url === rule.pattern || (rule.partialMatch && req.url.startsWith(rule.pattern))) {
87
+ // Encontramos una coincidencia, realizar la redirección
88
+ const target = typeof rule.target === 'function'
89
+ ? rule.target(req.url)
90
+ : rule.target;
91
+
92
+ const statusCode = rule.type === 'permanent' ? 301 : 302;
93
+
94
+ res.writeHead(statusCode, {
95
+ 'Location': target,
96
+ 'Content-Type': 'text/html'
97
+ });
98
+
99
+ res.end(`<html><body>Redirigido a <a href="${target}">${target}</a></body></html>`);
100
+
101
+ console.log(`[REDIRECT-PLUGIN] Redirección ${statusCode} de ${req.url} a ${target}`);
102
+
103
+ return {
104
+ allowed: false, // Indicar que no debe continuar con el procesamiento normal
105
+ redirected: true,
106
+ redirectUrl: target,
107
+ statusCode
108
+ };
109
+ }
110
+ }
111
+ }
112
+
113
+ // No se encontró ninguna regla de redirección, continuar normalmente
114
+ return { allowed: true, redirected: false };
115
+ }
116
+
117
+ /**
118
+ * Hook para cuando se recibe una solicitud
119
+ * @param {Object} params - Parámetros del hook
120
+ */
121
+ async function onRequestReceivedHook({ req, res }) {
122
+ console.log(`[REDIRECT-PLUGIN] Solicitud recibida: ${req.method} ${req.url}`);
123
+
124
+ // Podríamos hacer alguna lógica aquí si es necesario
125
+ }
126
+
127
+ /**
128
+ * Agrega una nueva regla de redirección
129
+ * @param {Object} rule - Regla de redirección
130
+ */
131
+ function addRedirectRule(rule) {
132
+ redirectRules.push(rule);
133
+ console.log(`[REDIRECT-PLUGIN] Añadida nueva regla de redirección: ${rule.pattern} -> ${rule.target}`);
134
+ }
135
+
136
+ /**
137
+ * Obtiene todas las reglas de redirección
138
+ * @returns {Array} - Array de reglas de redirección
139
+ */
140
+ function getRedirectRules() {
141
+ return redirectRules;
142
+ }
143
+
144
+ /**
145
+ * Limpia todas las reglas de redirección
146
+ */
147
+ function clearRedirectRules() {
148
+ redirectRules = [];
149
+ console.log('[REDIRECT-PLUGIN] Reglas de redirección limpiadas');
150
+ }
151
+
152
+ /**
153
+ * Desinicializa el plugin
154
+ * @param {Object} balancer - Instancia del balanceador
155
+ */
156
+ async function deinit(balancer) {
157
+ console.log('Desactivando plugin de redirección...');
158
+
159
+ // Limpiar recursos si es necesario
160
+ redirectRules = [];
161
+
162
+ console.log('Plugin de redirección desactivado correctamente');
163
+ }
164
+
165
+ // Exportar funciones
166
+ module.exports = {
167
+ init,
168
+ deinit,
169
+ addRedirectRule,
170
+ getRedirectRules,
171
+ clearRedirectRules
172
+ };
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "Plugin de Redirección",
3
+ "version": "1.0.0",
4
+ "description": "Plugin para implementar redirecciones basadas en reglas personalizadas",
5
+ "author": "Sistema Kukuy",
6
+ "main": "index.js",
7
+ "kukuyVersion": "^1.6.0",
8
+ "hooks": [
9
+ "onRequestReceived"
10
+ ],
11
+ "filters": [
12
+ "request_processing"
13
+ ],
14
+ "enabled": false
15
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kukuy",
3
- "version": "1.6.0",
4
- "description": "Balanceador de carga Backend",
3
+ "version": "1.9.0",
4
+ "description": "Balanceador de carga con soporte para hooks, filtros y cache robusto",
5
5
  "main": "kukuy.js",
6
6
  "scripts": {
7
7
  "start": "node kukuy.js",
@@ -9,13 +9,16 @@
9
9
  "test": "echo \"Error: no test specified\" && exit 1"
10
10
  },
11
11
  "keywords": [
12
- "backend",
13
12
  "load-balancer",
14
13
  "round-robin",
15
14
  "ip-hash",
16
15
  "proxy",
17
16
  "http",
18
- "https"
17
+ "https",
18
+ "hooks",
19
+ "filters",
20
+ "cache",
21
+ "plugins"
19
22
  ],
20
23
  "author": "Benjamín Sánchez Cárdenas",
21
24
  "license": "GPL-3.0-or-later",
@@ -39,7 +39,7 @@ class Balancer {
39
39
  this.config = ConfigManager.getInstance();
40
40
  this.hookManager = new ExtendedHookManager();
41
41
  this.filterChain = new ExtendedFilterChain();
42
- this.serverPool = new ServerPool();
42
+ this.serverPool = new ServerPool(this);
43
43
  this.algorithmManager = new AlgorithmManager();
44
44
  this.routeLoader = new RouteLoader();
45
45
  this.logger = new Logger();
@@ -175,13 +175,16 @@ class Balancer {
175
175
 
176
176
  // Cargar plugins después de iniciar componentes principales
177
177
  await this.pluginManager.loadPlugins();
178
+
179
+ // Imprimir información sobre los plugins disponibles y activos
180
+ this.pluginManager.printPluginsInfo();
178
181
  }
179
182
 
180
183
  async handleRequest(clientReq, clientRes) {
181
184
  try {
182
185
  // Aplicar filters antes de procesar la solicitud
183
186
  const filterData = { req: clientReq, res: clientRes, allowed: true, statusCode: 200, message: '' };
184
- const filteredResult = await this.filterChain.applyFilters('request', clientReq, clientRes);
187
+ const filteredResult = await this.filterChain.applyFilters('request_processing', clientReq, clientRes);
185
188
 
186
189
  if (!filteredResult || !filteredResult.allowed) {
187
190
  const statusCode = filteredResult?.statusCode || 403;
@@ -191,27 +194,16 @@ class Balancer {
191
194
  return;
192
195
  }
193
196
 
194
- // Emitir hook cuando se recibe una solicitud
195
- await this.hookManager.executeHooks('onRequestReceived', { req: clientReq, res: clientRes });
196
-
197
- // Determinar la ruta objetivo
198
- const targetServer = await this.getTargetServer(clientReq.url, clientReq.method, clientReq);
199
-
200
- if (!targetServer) {
201
- clientRes.writeHead(404);
202
- clientRes.end('No available servers');
197
+ // Verificar si el filtro indicó que la respuesta se sirvió desde caché
198
+ if (filteredResult?.cached && filteredResult?.cacheHit) {
199
+ // La respuesta ya fue enviada desde el filtro de caché, terminamos aquí
200
+ // No debemos continuar con ningún otro procesamiento
203
201
  return;
204
202
  }
205
203
 
206
- // Emitir hook después de seleccionar servidor
207
- await this.hookManager.executeHooks('onServerSelected', {
208
- req: clientReq,
209
- res: clientRes,
210
- server: targetServer
211
- });
212
-
213
- // Reenviar solicitud al servidor backend
214
- this.proxyRequest(clientReq, clientRes, targetServer);
204
+ // Si llegamos aquí, no hubo cache hit, continuar con el flujo normal
205
+ // y permitir que se ejecuten los hooks y se contacte al servidor backend
206
+ await this.processRequestNormally(clientReq, clientRes);
215
207
 
216
208
  } catch (error) {
217
209
  this.logger.error(`Error manejando solicitud: ${error.message}`);
@@ -228,6 +220,36 @@ class Balancer {
228
220
  }
229
221
  }
230
222
 
223
+ async processRequestNormally(clientReq, clientRes) {
224
+ // Emitir hook cuando se recibe una solicitud
225
+ await this.hookManager.executeHooks('onRequestReceived', { req: clientReq, res: clientRes });
226
+
227
+ // Determinar la ruta objetivo
228
+ const targetServer = await this.getTargetServer(clientReq.url, clientReq.method, clientReq);
229
+
230
+ if (!targetServer) {
231
+ clientRes.writeHead(404);
232
+ clientRes.end('No available servers');
233
+ return;
234
+ }
235
+
236
+ // Emitir hook después de seleccionar servidor
237
+ await this.hookManager.executeHooks('onServerSelected', {
238
+ req: clientReq,
239
+ res: clientRes,
240
+ server: targetServer
241
+ });
242
+
243
+ // Verificar si después de los hooks la respuesta ya fue enviada
244
+ // (esto cubre el caso de onServerError u otros hooks que puedan enviar respuesta desde la caché)
245
+ if (clientRes.headersSent) {
246
+ return;
247
+ }
248
+
249
+ // Reenviar solicitud al servidor backend
250
+ this.proxyRequest(clientReq, clientRes, targetServer);
251
+ }
252
+
231
253
  async getTargetServer(requestUrl, method, clientReq = null) {
232
254
  // Usar RouteLoader para encontrar la ruta coincidente
233
255
  const matchedRoute = this.routeLoader.findMatchingRoute(requestUrl, method);
@@ -283,12 +305,6 @@ class Balancer {
283
305
  clientReq.pipe(proxyReq);
284
306
 
285
307
  proxyReq.on('response', (serverRes) => {
286
- // Copiar headers de la respuesta
287
- clientRes.writeHead(serverRes.statusCode, serverRes.headers);
288
-
289
- // Enviar cuerpo de la respuesta al cliente
290
- serverRes.pipe(clientRes);
291
-
292
308
  // Calcular tiempo de respuesta
293
309
  const responseTime = Date.now() - startTime;
294
310
 
@@ -313,16 +329,66 @@ class Balancer {
313
329
  // Actualizar estado del servidor en las métricas
314
330
  this.metricsCollector.updateServerStatus(server.id, 'online');
315
331
 
316
- // Emitir hook cuando se envía la respuesta
317
- this.hookManager.executeHooks('onResponseSent', {
318
- req: clientReq,
319
- res: clientRes,
320
- serverRes,
321
- server,
322
- responseTime
332
+ // Capturar el cuerpo de la respuesta para posibles plugins
333
+ let responseData = [];
334
+
335
+ serverRes.on('data', (chunk) => {
336
+ responseData.push(chunk);
323
337
  });
324
338
 
325
- resolve(); // Resolver la promesa al completar la solicitud
339
+ serverRes.on('end', () => {
340
+ const responseBody = Buffer.concat(responseData);
341
+
342
+ // Emitir hook antes de enviar la respuesta al cliente
343
+ // Esto permite a los plugins interceptar la respuesta
344
+ this.hookManager.executeHooks('onResponseReady', {
345
+ req: clientReq,
346
+ res: clientRes,
347
+ serverRes,
348
+ responseBody,
349
+ server,
350
+ responseTime
351
+ }).then(() => {
352
+ // Si algún plugin ha manejado la respuesta, no la enviamos aquí
353
+ if (!clientRes.headersSent) {
354
+ // Copiar headers de la respuesta
355
+ clientRes.writeHead(serverRes.statusCode, serverRes.headers);
356
+
357
+ // Enviar cuerpo de la respuesta al cliente
358
+ clientRes.end(responseBody);
359
+ }
360
+
361
+ // Emitir hook cuando se envía la respuesta
362
+ this.hookManager.executeHooks('onResponseSent', {
363
+ req: clientReq,
364
+ res: clientRes,
365
+ serverRes,
366
+ server,
367
+ responseTime
368
+ });
369
+
370
+ resolve(); // Resolver la promesa al completar la solicitud
371
+ }).catch(err => {
372
+ console.error('Error ejecutando hook onResponseReady:', err);
373
+
374
+ // En caso de error, enviar la respuesta de todas formas
375
+ if (!clientRes.headersSent) {
376
+ clientRes.writeHead(serverRes.statusCode, serverRes.headers);
377
+ clientRes.end(responseBody);
378
+ }
379
+
380
+ // Emitir hook cuando se envía la respuesta
381
+ this.hookManager.executeHooks('onResponseSent', {
382
+ req: clientReq,
383
+ res: clientRes,
384
+ serverRes,
385
+ server,
386
+ responseTime
387
+ });
388
+
389
+ resolve(); // Resolver la promesa al completar la solicitud
390
+ });
391
+ });
326
392
  });
327
393
 
328
394
  proxyReq.on('error', async (err) => {
@@ -1,12 +1,12 @@
1
1
  const { HealthChecker } = require('../utils/HealthChecker');
2
2
 
3
3
  class ServerPool {
4
- constructor() {
4
+ constructor(balancer = null) {
5
5
  this.servers = [];
6
6
  this.healthyServers = []; // Caché de servidores saludables
7
7
  this.serversById = new Map(); // Acceso rápido por ID
8
8
  this.serversByTag = new Map(); // Caché de servidores por tag
9
- this.healthChecker = new HealthChecker();
9
+ this.healthChecker = new HealthChecker(balancer);
10
10
  this.nextId = 1;
11
11
  this.cacheValid = false; // Indicador de validez de caché
12
12
  }
@@ -35,14 +35,6 @@ class FilterChain {
35
35
  }
36
36
 
37
37
  async applyFilters(type, req, res, additionalData = {}) {
38
- // Verificar si la solicitud puede ser atendida desde caché
39
- const cacheResult = await this.cachingFilter.apply(req, res, additionalData);
40
-
41
- // Si fue un hit de caché, retornar inmediatamente
42
- if (cacheResult.cached && cacheResult.cacheHit) {
43
- return cacheResult;
44
- }
45
-
46
38
  // Usar el sistema de filtros de la librería para otros filtros
47
39
  // Creamos un objeto combinado para pasar al filtro
48
40
  const filterData = { req, res, ...additionalData, allowed: true, statusCode: 200, message: '' };
@@ -51,7 +43,8 @@ class FilterChain {
51
43
  const processedData = this.hooks.applyFilters('request_processing', filterData);
52
44
 
53
45
  // Devolver el resultado procesado
54
- return { ...processedData, ...cacheResult };
46
+ // Nota: El manejo de caché ahora se delega a los plugins registrados
47
+ return processedData;
55
48
  }
56
49
 
57
50
  // Filtro de autenticación básico
@@ -11,6 +11,7 @@ class HookManager {
11
11
  const hookNames = [
12
12
  'onRequestReceived',
13
13
  'onServerSelected',
14
+ 'onResponseReady',
14
15
  'onResponseSent',
15
16
  'onServerError'
16
17
  ];
@@ -178,6 +178,54 @@ class PluginManager {
178
178
  isActive: plugin.isActive
179
179
  }));
180
180
  }
181
+
182
+ /**
183
+ * Print information about available and loaded plugins to stdout
184
+ */
185
+ printPluginsInfo() {
186
+ console.log('\n========== INFORMACIÓN DE PLUGINS ==========');
187
+
188
+ if (!fs.existsSync(this.pluginPath)) {
189
+ console.log(`Directorio de plugins no encontrado: ${this.pluginPath}`);
190
+ console.log('=============================================\n');
191
+ return;
192
+ }
193
+
194
+ const allItems = fs.readdirSync(this.pluginPath);
195
+ const pluginDirs = allItems.filter(item => this.isDirectory(path.join(this.pluginPath, item)));
196
+
197
+ console.log(`Plugins encontrados en ${this.pluginPath}: ${pluginDirs.length}`);
198
+
199
+ for (const pluginDir of pluginDirs) {
200
+ const pluginPath = path.join(this.pluginPath, pluginDir);
201
+ const manifestPath = path.join(pluginPath, 'manifest.json');
202
+
203
+ if (fs.existsSync(manifestPath)) {
204
+ try {
205
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
206
+
207
+ if (this.plugins.has(pluginDir)) {
208
+ const plugin = this.plugins.get(pluginDir);
209
+ console.log(`✓ ACTIVO - ${pluginDir} (v${manifest.version}): ${manifest.description}`);
210
+ } else {
211
+ if (manifest.enabled !== false) {
212
+ console.log(`○ DISPONIBLE - ${pluginDir} (v${manifest.version}): ${manifest.description}`);
213
+ } else {
214
+ console.log(`○ DESHABILITADO - ${pluginDir} (v${manifest.version}): ${manifest.description} (manifest: enabled=false)`);
215
+ }
216
+ }
217
+ } catch (error) {
218
+ console.log(`✗ ERROR - ${pluginDir}: Error leyendo manifest.json`);
219
+ }
220
+ } else {
221
+ console.log(`✗ SIN MANIFEST - ${pluginDir}: No tiene archivo manifest.json`);
222
+ }
223
+ }
224
+
225
+ const loadedCount = this.plugins.size;
226
+ console.log(`\nTotal de plugins activos: ${loadedCount}`);
227
+ console.log('=============================================\n');
228
+ }
181
229
  }
182
230
 
183
231
  module.exports = { PluginManager };