bro-framework 2.2.1 → 2.3.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.
package/src/server.js CHANGED
@@ -1,243 +1,277 @@
1
- import express from 'express';
2
- import cors from 'cors';
3
- import http from 'node:http';
4
- import { Server } from 'socket.io';
5
- import rateLimit from 'express-rate-limit';
6
- import multer from 'multer';
7
- import { apiReference } from '@scalar/express-api-reference';
8
- import { verifyJwt, signJwt } from './auth.js';
9
- import { loadRoutes } from './router.js';
10
-
11
- /**
12
- * Creates and configures the core Express server.
13
- * @param {Object} globalConfig - User's bro.config.js configurations.
14
- * @param {string} routesDir - Path to the target routes directory.
15
- * @param {any} db - Initialized database instance.
16
- * @returns {Promise<{ app: import('express').Application, server: http.Server, routes: Array, reload: Function, io: import('socket.io').Server }>}
17
- */
18
- export async function createServer(globalConfig, routesDir, db) {
19
- if (process.env.NODE_ENV === 'production' && ['dev_secret_please_change', 'bro_default_secret_key'].includes(globalConfig.jwtSecret)) {
20
- throw new Error('CRITICAL SECURITY ERROR: You are running in production with a default JWT secret! Please set auth.jwtSecret in bro.config.js or via JWT_SECRET environment variable.');
21
- }
22
-
23
- const app = express();
24
- const server = http.createServer(app);
25
-
26
- const corsConfig = globalConfig.server?.cors !== undefined ? globalConfig.server.cors : true;
27
-
28
- if (corsConfig !== false) {
29
- app.use(cors(typeof corsConfig === 'object' ? corsConfig : {}));
30
- }
31
-
32
- app.use(express.json());
33
-
34
- if (globalConfig.rateLimit) {
35
- app.use(rateLimit(globalConfig.rateLimit));
36
- }
37
-
38
- const io = new Server(server, { cors: typeof corsConfig === 'object' ? corsConfig : undefined });
39
-
40
- if (globalConfig.sockets) {
41
- await globalConfig.sockets(io, db);
42
- }
43
-
44
- const createHandler = (routeConfig) => {
45
- const middlewares = [];
46
- const bodySchema = routeConfig.schema?.body || routeConfig.body;
47
- const paramsSchema = routeConfig.schema?.params || routeConfig.params;
48
- const querySchema = routeConfig.schema?.query || routeConfig.query;
49
-
50
- if (routeConfig.rateLimit) {
51
- middlewares.push(rateLimit(routeConfig.rateLimit));
52
- }
53
-
54
- if (routeConfig.upload) {
55
- const routeMulterConfig = {
56
- limits: globalConfig.upload?.limits || {
57
- fileSize: 10 * 1024 * 1024,
58
- files: 5,
59
- fields: 20
60
- }
61
- };
62
- if (typeof routeConfig.upload === 'object') {
63
- if (routeConfig.upload.limits) {
64
- routeMulterConfig.limits = { ...routeMulterConfig.limits, ...routeConfig.upload.limits };
65
- }
66
- if (routeConfig.upload.fileFilter) routeMulterConfig.fileFilter = routeConfig.upload.fileFilter;
67
- if (routeConfig.upload.storage) routeMulterConfig.storage = routeConfig.upload.storage;
68
- }
69
-
70
- const uploadParser = multer(routeMulterConfig);
71
-
72
- if (typeof routeConfig.upload === 'object' && routeConfig.upload.fields) {
73
- middlewares.push(uploadParser.fields(routeConfig.upload.fields));
74
- } else if (typeof routeConfig.upload === 'object' && routeConfig.upload.single) {
75
- middlewares.push(uploadParser.single(routeConfig.upload.single));
76
- } else if (typeof routeConfig.upload === 'object' && routeConfig.upload.array) {
77
- middlewares.push(uploadParser.array(routeConfig.upload.array));
78
- } else {
79
- middlewares.push(uploadParser.any());
80
- }
81
- }
82
-
83
- middlewares.push(async (req, res) => {
84
- try {
85
- const ctx = {
86
- env: globalConfig.envData || process.env,
87
- db,
88
- io,
89
- body: req.body,
90
- params: req.params,
91
- query: req.query,
92
- files: req.files || req.file,
93
- user: null,
94
- jwt: { sign: (payload, opts) => signJwt(payload, globalConfig.jwtSecret, opts || { expiresIn: globalConfig.auth?.expiresIn || '1d' }) },
95
- error: (status, message) => {
96
- const err = new Error(message);
97
- err.status = status;
98
- throw err;
99
- }
100
- };
101
-
102
- if (routeConfig.auth) {
103
- const authHeader = req.headers.authorization;
104
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
105
- return res.status(401).json({ error: 'Unauthorized', details: 'Missing or invalid Bearer token' });
106
- }
107
-
108
- const token = authHeader.split(' ')[1];
109
- const authResult = verifyJwt(token, globalConfig.jwtSecret);
110
-
111
- if (!authResult.valid) {
112
- return res.status(401).json({ error: 'Unauthorized', details: authResult.error });
113
- }
114
-
115
- ctx.user = authResult.payload;
116
- }
117
-
118
- if (paramsSchema) {
119
- const result = paramsSchema.safeParse(req.params);
120
- if (!result.success) {
121
- return res.status(400).json({ error: 'Invalid URL Parameters', details: result.error.flatten() });
122
- }
123
- ctx.params = result.data;
124
- }
125
-
126
- if (bodySchema) {
127
- const result = bodySchema.safeParse(req.body);
128
- if (!result.success) {
129
- return res.status(400).json({ error: 'Invalid Request Body', details: result.error.flatten() });
130
- }
131
- ctx.body = result.data;
132
- }
133
-
134
- if (querySchema) {
135
- const result = querySchema.safeParse(req.query);
136
- if (!result.success) {
137
- return res.status(400).json({ error: 'Invalid Query Parameters', details: result.error.flatten() });
138
- }
139
- ctx.query = result.data;
140
- }
141
-
142
- if (typeof routeConfig.handler !== 'function') {
143
- throw new Error('Route "handler" is missing or is not a function');
144
- }
145
-
146
- const responseData = await routeConfig.handler(ctx);
147
-
148
- if (!res.headersSent) {
149
- res.status(200).json(responseData);
150
- }
151
-
152
- } catch (err) {
153
- const status = err.status || 500;
154
- const message = status === 500 ? 'Internal Server Error' : err.message;
155
-
156
- if (status === 500) {
157
- console.error(`[bro.js] Execution Error in route:`);
158
- console.error(err.stack);
159
- }
160
-
161
- if (!res.headersSent) {
162
- res.status(status).json({
163
- error: message,
164
- ...(status !== 500 && err.details ? { details: err.details } : {})
165
- });
166
- }
167
- }
168
- });
169
-
170
- return middlewares;
171
- };
172
-
173
- let openApiSpec = {
174
- openapi: '3.0.0',
175
- info: { title: 'bro.js API', version: '1.0.0' },
176
- components: {
177
- securitySchemes: {
178
- bearerAuth: {
179
- type: 'http',
180
- scheme: 'bearer',
181
- bearerFormat: 'JWT'
182
- }
183
- },
184
- responses: {
185
- BadRequest: { description: 'Bad Request', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
186
- Unauthorized: { description: 'Unauthorized', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
187
- NotFound: { description: 'Not Found', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
188
- ServerError: { description: 'Internal Server Error', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } }
189
- }
190
- },
191
- paths: {}
192
- };
193
-
194
- const shouldMountDocs = globalConfig.docs !== false && (globalConfig.docs === true || typeof globalConfig.docs === 'object' || process.env.NODE_ENV !== 'production');
195
-
196
- if (shouldMountDocs) {
197
- const docsAuthMiddleware = (req, res, next) => {
198
- if (typeof globalConfig.docs === 'object' && globalConfig.docs.auth) {
199
- const b64auth = (req.headers.authorization || '').split(' ')[1] || '';
200
- const [user, pass] = Buffer.from(b64auth, 'base64').toString().split(':');
201
-
202
- if (user === globalConfig.docs.auth.user && pass === globalConfig.docs.auth.pass) {
203
- return next();
204
- }
205
-
206
- res.set('WWW-Authenticate', 'Basic realm="bro.js API Docs"');
207
- return res.status(401).send('Authentication required.');
208
- }
209
- next();
210
- };
211
-
212
- app.get('/docs/json', docsAuthMiddleware, (req, res) => res.json(openApiSpec));
213
- app.use('/docs', docsAuthMiddleware, apiReference({ spec: { url: '/docs/json' } }));
214
- }
215
-
216
- let routeStack = express.Router();
217
-
218
- app.use((req, res, next) => {
219
- routeStack(req, res, next);
220
- });
221
-
222
- app.use((req, res) => {
223
- res.status(404).json({ error: 'Not Found' });
224
- });
225
-
226
- app.use((err, req, res, next) => {
227
- console.error(`[bro.js] Uncaught Error:`, err);
228
- res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
229
- });
230
-
231
- const reload = async () => {
232
- const newRouter = express.Router();
233
- const tempSpec = { paths: {} };
234
- const routes = await loadRoutes(newRouter, routesDir, createHandler, tempSpec);
235
- openApiSpec.paths = tempSpec.paths;
236
- routeStack = newRouter;
237
- return routes;
238
- };
239
-
240
- const initialRoutes = await reload();
241
-
242
- return { app, server, routes: initialRoutes, reload, io };
243
- }
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import http from 'node:http';
4
+ import path from 'node:path';
5
+ import { Server } from 'socket.io';
6
+ import rateLimit from 'express-rate-limit';
7
+ import multer from 'multer';
8
+ import { apiReference } from '@scalar/express-api-reference';
9
+ import { verifyJwt, signJwt } from './auth.js';
10
+ import { loadLocale } from './locale.js';
11
+ import { loadRoutes } from './router.js';
12
+ import { scanTasks } from './tasks.js';
13
+
14
+ /**
15
+ * Creates and configures the core Express server.
16
+ * @param {Object} globalConfig - User's bro.config.js configurations.
17
+ * @param {string} routesDir - Path to the target routes directory.
18
+ * @param {any} db - Initialized database instance.
19
+ * @returns {Promise<{ app: import('express').Application, server: http.Server, routes: Array, reload: Function, reloadLocale: Function, io: import('socket.io').Server, shutdown: Function }>}
20
+ */
21
+ export async function createServer(globalConfig, routesDir, db) {
22
+ if (process.env.NODE_ENV === 'production' && ['dev_secret_please_change', 'bro_default_secret_key'].includes(globalConfig.jwtSecret)) {
23
+ throw new Error('CRITICAL SECURITY ERROR: You are running in production with a default JWT secret! Please set auth.jwtSecret in bro.config.js or via JWT_SECRET environment variable.');
24
+ }
25
+
26
+ const app = express();
27
+ const server = http.createServer(app);
28
+ const localeDirectory = globalConfig.locale?.directory || path.join(process.cwd(), 'locale');
29
+ let locale = await loadLocale(localeDirectory, globalConfig.locale);
30
+
31
+ const corsConfig = globalConfig.server?.cors !== undefined ? globalConfig.server.cors : true;
32
+
33
+ if (corsConfig !== false) {
34
+ app.use(cors(typeof corsConfig === 'object' ? corsConfig : {}));
35
+ }
36
+
37
+ app.use(express.json());
38
+
39
+ if (globalConfig.rateLimit) {
40
+ app.use(rateLimit(globalConfig.rateLimit));
41
+ }
42
+
43
+ const io = new Server(server, { cors: typeof corsConfig === 'object' ? corsConfig : undefined });
44
+
45
+ if (globalConfig.sockets) {
46
+ await globalConfig.sockets(io, db);
47
+ }
48
+ const createHandler = (routeConfig) => {
49
+ const middlewares = [];
50
+ if (routeConfig.schema) {
51
+ throw new Error("Nested 'schema' object is no longer supported in bro.js v2.3.0+. Please use flat, top-level properties (body, query, params) instead.");
52
+ }
53
+ const bodySchema = routeConfig.body;
54
+ const paramsSchema = routeConfig.params;
55
+ const querySchema = routeConfig.query;
56
+
57
+ if (routeConfig.rateLimit) {
58
+ middlewares.push(rateLimit(routeConfig.rateLimit));
59
+ }
60
+
61
+ if (routeConfig.upload) {
62
+ const defaultLimits = { fileSize: 10 * 1024 * 1024, files: 5, fields: 20, parts: 25, fieldSize: 1024 * 1024 };
63
+ const routeMulterConfig = {
64
+ limits: { ...defaultLimits, ...(globalConfig.upload?.limits || {}) }
65
+ };
66
+ if (typeof routeConfig.upload === 'object') {
67
+ if (routeConfig.upload.limits) {
68
+ routeMulterConfig.limits = { ...routeMulterConfig.limits, ...routeConfig.upload.limits };
69
+ }
70
+ if (routeConfig.upload.fileFilter) routeMulterConfig.fileFilter = routeConfig.upload.fileFilter;
71
+ if (routeConfig.upload.storage) routeMulterConfig.storage = routeConfig.upload.storage;
72
+ }
73
+
74
+ const uploadParser = multer(routeMulterConfig);
75
+
76
+ if (typeof routeConfig.upload === 'object' && routeConfig.upload.fields) {
77
+ middlewares.push(uploadParser.fields(routeConfig.upload.fields));
78
+ } else if (typeof routeConfig.upload === 'object' && routeConfig.upload.single) {
79
+ middlewares.push(uploadParser.single(routeConfig.upload.single));
80
+ } else if (typeof routeConfig.upload === 'object' && routeConfig.upload.array) {
81
+ middlewares.push(uploadParser.array(routeConfig.upload.array));
82
+ } else {
83
+ middlewares.push(uploadParser.any());
84
+ }
85
+ }
86
+
87
+ middlewares.push(async (req, res) => {
88
+ try {
89
+ const requestLocale = locale.resolveLocale(req);
90
+ const ctx = {
91
+ env: globalConfig.envData || process.env,
92
+ db,
93
+ io,
94
+ body: req.body,
95
+ params: req.params,
96
+ query: req.query,
97
+ files: req.files || req.file,
98
+ locale: requestLocale,
99
+ t: (key, values) => locale.translate(requestLocale, key, values),
100
+ user: null,
101
+ jwt: { sign: (payload, opts) => signJwt(payload, globalConfig.jwtSecret, opts || { expiresIn: globalConfig.auth?.expiresIn || '1d' }) },
102
+ error: (status, message) => {
103
+ const err = new Error(message);
104
+ err.status = status;
105
+ throw err;
106
+ }
107
+ };
108
+
109
+ if (routeConfig.auth) {
110
+ const authHeader = req.headers.authorization;
111
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
112
+ return res.status(401).json({ error: 'Unauthorized', details: 'Missing or invalid Bearer token' });
113
+ }
114
+
115
+ const token = authHeader.split(' ')[1];
116
+ const authResult = verifyJwt(token, globalConfig.jwtSecret);
117
+
118
+ if (!authResult.valid) {
119
+ return res.status(401).json({ error: 'Unauthorized', details: authResult.error });
120
+ }
121
+
122
+ ctx.user = authResult.payload;
123
+ }
124
+
125
+ if (paramsSchema) {
126
+ const result = paramsSchema.safeParse(req.params);
127
+ if (!result.success) {
128
+ return res.status(400).json({ error: 'Invalid URL Parameters', details: result.error.flatten() });
129
+ }
130
+ ctx.params = result.data;
131
+ }
132
+
133
+ if (bodySchema) {
134
+ const result = bodySchema.safeParse(req.body);
135
+ if (!result.success) {
136
+ return res.status(400).json({ error: 'Invalid Request Body', details: result.error.flatten() });
137
+ }
138
+ ctx.body = result.data;
139
+ }
140
+
141
+ if (querySchema) {
142
+ const result = querySchema.safeParse(req.query);
143
+ if (!result.success) {
144
+ return res.status(400).json({ error: 'Invalid Query Parameters', details: result.error.flatten() });
145
+ }
146
+ ctx.query = result.data;
147
+ }
148
+
149
+ if (typeof routeConfig.handler !== 'function') {
150
+ throw new Error('Route "handler" is missing or is not a function');
151
+ }
152
+
153
+ const responseData = await routeConfig.handler(ctx);
154
+
155
+ if (!res.headersSent) {
156
+ res.status(200).json(responseData);
157
+ }
158
+
159
+ } catch (err) {
160
+ const status = err.status || 500;
161
+ const message = status === 500 ? 'Internal Server Error' : err.message;
162
+
163
+ if (status === 500) {
164
+ console.error(`[bro.js] Execution Error in route:`);
165
+ console.error(err.stack);
166
+ }
167
+
168
+ if (!res.headersSent) {
169
+ res.status(status).json({
170
+ error: message,
171
+ ...(status !== 500 && err.details ? { details: err.details } : {})
172
+ });
173
+ }
174
+ }
175
+ });
176
+
177
+ return middlewares;
178
+ };
179
+
180
+ let openApiSpec = {
181
+ openapi: '3.0.0',
182
+ info: { title: 'bro.js API', version: '1.0.0' },
183
+ components: {
184
+ securitySchemes: {
185
+ bearerAuth: {
186
+ type: 'http',
187
+ scheme: 'bearer',
188
+ bearerFormat: 'JWT'
189
+ }
190
+ },
191
+ responses: {
192
+ BadRequest: { description: 'Bad Request', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
193
+ Unauthorized: { description: 'Unauthorized', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
194
+ NotFound: { description: 'Not Found', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
195
+ ServerError: { description: 'Internal Server Error', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } }
196
+ }
197
+ },
198
+ paths: {}
199
+ };
200
+
201
+ const shouldMountDocs = globalConfig.docs !== false && (globalConfig.docs === true || typeof globalConfig.docs === 'object' || process.env.NODE_ENV !== 'production');
202
+
203
+ if (shouldMountDocs) {
204
+ const docsAuthMiddleware = (req, res, next) => {
205
+ if (typeof globalConfig.docs === 'object' && globalConfig.docs.auth) {
206
+ const b64auth = (req.headers.authorization || '').split(' ')[1] || '';
207
+ const [user, pass] = Buffer.from(b64auth, 'base64').toString().split(':');
208
+
209
+ if (user === globalConfig.docs.auth.user && pass === globalConfig.docs.auth.pass) {
210
+ return next();
211
+ }
212
+
213
+ res.set('WWW-Authenticate', 'Basic realm="bro.js API Docs"');
214
+ return res.status(401).send('Authentication required.');
215
+ }
216
+ next();
217
+ };
218
+
219
+ app.get('/docs/json', docsAuthMiddleware, (req, res) => res.json(openApiSpec));
220
+ app.use('/docs', docsAuthMiddleware, apiReference({ spec: { url: '/docs/json' } }));
221
+ }
222
+
223
+ let routeStack = express.Router();
224
+
225
+ app.use((req, res, next) => {
226
+ routeStack(req, res, next);
227
+ });
228
+
229
+ app.use((req, res) => {
230
+ res.status(404).json({ error: 'Not Found' });
231
+ });
232
+
233
+ app.use((err, req, res, next) => {
234
+ console.error(`[bro.js] Uncaught Error:`, err);
235
+ res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
236
+ });
237
+
238
+ const reload = async () => {
239
+ const newRouter = express.Router();
240
+ const tempSpec = { paths: {} };
241
+ const routes = await loadRoutes(newRouter, routesDir, createHandler, tempSpec);
242
+ openApiSpec.paths = tempSpec.paths;
243
+ routeStack = newRouter;
244
+ return routes;
245
+ };
246
+
247
+ const reloadLocale = async () => {
248
+ locale = await loadLocale(localeDirectory, globalConfig.locale);
249
+ return locale;
250
+ };
251
+
252
+ const initialRoutes = await reload();
253
+
254
+ const taskManager = await scanTasks({ db, io });
255
+
256
+ let isShuttingDown = false;
257
+ const shutdown = async () => {
258
+ if (isShuttingDown) return;
259
+ isShuttingDown = true;
260
+ if (taskManager) taskManager.stopAll();
261
+ if (io) io.close();
262
+
263
+ if (typeof globalConfig.onShutdown === 'function') {
264
+ try {
265
+ await globalConfig.onShutdown(db);
266
+ } catch (err) {
267
+ console.error('[bro.js] Error during database teardown hook:', err);
268
+ }
269
+ }
270
+
271
+ return new Promise((resolve) => {
272
+ server.close(() => resolve());
273
+ });
274
+ };
275
+
276
+ return { app, server, routes: initialRoutes, reload, reloadLocale, io, shutdown };
277
+ }
package/src/tasks.js CHANGED
@@ -1,54 +1,54 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { pathToFileURL } from 'url';
4
- import cron from 'node-cron';
5
- import { colors } from './logger.js';
6
- import { scanDir } from './router.js';
7
-
8
- export class TaskManager {
9
- constructor() {
10
- this.taskHandles = [];
11
- }
12
-
13
- stopAll() {
14
- this.taskHandles.forEach(t => t.stop());
15
- this.taskHandles = [];
16
- }
17
- }
18
-
19
- export async function scanTasks(ctx) {
20
- const manager = new TaskManager();
21
- const tasksDir = path.join(process.cwd(), 'tasks');
22
- if (!fs.existsSync(tasksDir)) return manager;
23
-
24
- const files = scanDir(tasksDir);
25
- if (files.length === 0) return manager;
26
-
27
- let count = 0;
28
- for (const file of files) {
29
- try {
30
- const moduleUrl = pathToFileURL(file).href;
31
- const taskModule = await import(moduleUrl);
32
-
33
- if (taskModule.cron && typeof taskModule.handler === 'function') {
34
- const task = cron.schedule(taskModule.cron, async () => {
35
- try {
36
- await taskModule.handler(ctx);
37
- } catch (err) {
38
- console.error(`\n ${colors.red}❌ Task Error (${file}):${colors.reset}`, err);
39
- }
40
- });
41
- manager.taskHandles.push(task);
42
- count++;
43
- }
44
- } catch (err) {
45
- console.error(`\n ${colors.red}❌ Failed to load task ${file}:${colors.reset}`, err);
46
- }
47
- }
48
-
49
- if (count > 0) {
50
- console.log(` ${colors.dim}├──${colors.reset} ${colors.cyan}Scheduled ${count} background task(s)${colors.reset}`);
51
- }
52
-
53
- return manager;
54
- }
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { pathToFileURL } from 'url';
4
+ import cron from 'node-cron';
5
+ import { colors } from './logger.js';
6
+ import { scanDir } from './router.js';
7
+
8
+ export class TaskManager {
9
+ constructor() {
10
+ this.taskHandles = [];
11
+ }
12
+
13
+ stopAll() {
14
+ this.taskHandles.forEach(t => t.stop());
15
+ this.taskHandles = [];
16
+ }
17
+ }
18
+
19
+ export async function scanTasks(ctx) {
20
+ const manager = new TaskManager();
21
+ const tasksDir = path.join(process.cwd(), 'tasks');
22
+ if (!fs.existsSync(tasksDir)) return manager;
23
+
24
+ const files = scanDir(tasksDir);
25
+ if (files.length === 0) return manager;
26
+
27
+ let count = 0;
28
+ for (const file of files) {
29
+ try {
30
+ const moduleUrl = pathToFileURL(file).href;
31
+ const taskModule = await import(moduleUrl);
32
+
33
+ if (taskModule.cron && typeof taskModule.handler === 'function') {
34
+ const task = cron.schedule(taskModule.cron, async () => {
35
+ try {
36
+ await taskModule.handler(ctx);
37
+ } catch (err) {
38
+ console.error(`\n ${colors.red}❌ Task Error (${file}):${colors.reset}`, err);
39
+ }
40
+ });
41
+ manager.taskHandles.push(task);
42
+ count++;
43
+ }
44
+ } catch (err) {
45
+ console.error(`\n ${colors.red}❌ Failed to load task ${file}:${colors.reset}`, err);
46
+ }
47
+ }
48
+
49
+ if (count > 0) {
50
+ console.log(` ${colors.dim}├──${colors.reset} ${colors.cyan}Scheduled ${count} background task(s)${colors.reset}`);
51
+ }
52
+
53
+ return manager;
54
+ }