voltjs-framework 1.0.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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1265 -0
  3. package/bin/volt.js +139 -0
  4. package/package.json +56 -0
  5. package/src/api/graphql.js +399 -0
  6. package/src/api/rest.js +204 -0
  7. package/src/api/websocket.js +285 -0
  8. package/src/cli/build.js +111 -0
  9. package/src/cli/create.js +371 -0
  10. package/src/cli/db.js +106 -0
  11. package/src/cli/dev.js +114 -0
  12. package/src/cli/generate.js +278 -0
  13. package/src/cli/lint.js +172 -0
  14. package/src/cli/routes.js +118 -0
  15. package/src/cli/start.js +42 -0
  16. package/src/cli/test.js +138 -0
  17. package/src/core/app.js +701 -0
  18. package/src/core/config.js +232 -0
  19. package/src/core/middleware.js +133 -0
  20. package/src/core/plugins.js +88 -0
  21. package/src/core/react-renderer.js +244 -0
  22. package/src/core/renderer.js +337 -0
  23. package/src/core/router.js +183 -0
  24. package/src/database/index.js +461 -0
  25. package/src/database/migration.js +192 -0
  26. package/src/database/model.js +285 -0
  27. package/src/database/query.js +394 -0
  28. package/src/database/seeder.js +89 -0
  29. package/src/index.js +156 -0
  30. package/src/security/auth.js +425 -0
  31. package/src/security/cors.js +80 -0
  32. package/src/security/csrf.js +125 -0
  33. package/src/security/encryption.js +110 -0
  34. package/src/security/helmet.js +103 -0
  35. package/src/security/index.js +75 -0
  36. package/src/security/rateLimit.js +119 -0
  37. package/src/security/sanitizer.js +113 -0
  38. package/src/security/xss.js +110 -0
  39. package/src/ui/component.js +224 -0
  40. package/src/ui/reactive.js +503 -0
  41. package/src/ui/template.js +448 -0
  42. package/src/utils/cache.js +216 -0
  43. package/src/utils/collection.js +772 -0
  44. package/src/utils/cron.js +213 -0
  45. package/src/utils/date.js +223 -0
  46. package/src/utils/events.js +181 -0
  47. package/src/utils/excel.js +482 -0
  48. package/src/utils/form.js +547 -0
  49. package/src/utils/hash.js +121 -0
  50. package/src/utils/http.js +461 -0
  51. package/src/utils/logger.js +186 -0
  52. package/src/utils/mail.js +347 -0
  53. package/src/utils/paginator.js +179 -0
  54. package/src/utils/pdf.js +417 -0
  55. package/src/utils/queue.js +199 -0
  56. package/src/utils/schema.js +985 -0
  57. package/src/utils/sms.js +243 -0
  58. package/src/utils/storage.js +348 -0
  59. package/src/utils/string.js +236 -0
  60. package/src/utils/validation.js +318 -0
@@ -0,0 +1,461 @@
1
+ /**
2
+ * VoltJS HttpClient
3
+ *
4
+ * Zero-dependency HTTP client for external API calls.
5
+ *
6
+ * @example
7
+ * const { HttpClient } = require('voltjs');
8
+ *
9
+ * const api = new HttpClient({ baseURL: 'https://api.example.com' });
10
+ * api.setHeader('Authorization', 'Bearer token');
11
+ *
12
+ * const users = await api.get('/users');
13
+ * const created = await api.post('/users', { name: 'John' });
14
+ *
15
+ * // Quick usage
16
+ * const data = await HttpClient.get('https://api.example.com/data');
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const https = require('https');
22
+ const http = require('http');
23
+ const { URL } = require('url');
24
+ const zlib = require('zlib');
25
+
26
+ class HttpClient {
27
+ constructor(options = {}) {
28
+ this.baseURL = options.baseURL || '';
29
+ this.headers = {
30
+ 'Content-Type': 'application/json',
31
+ 'Accept': 'application/json',
32
+ 'User-Agent': 'VoltJS/1.0',
33
+ ...options.headers,
34
+ };
35
+ this.timeout = options.timeout || 30000;
36
+ this.retries = options.retries || 0;
37
+ this.retryDelay = options.retryDelay || 1000;
38
+
39
+ this._interceptors = { request: [], response: [] };
40
+ }
41
+
42
+ /** Set a default header */
43
+ setHeader(key, value) {
44
+ this.headers[key] = value;
45
+ return this;
46
+ }
47
+
48
+ /** Set Bearer token */
49
+ setBearerToken(token) {
50
+ this.headers.Authorization = `Bearer ${token}`;
51
+ return this;
52
+ }
53
+
54
+ /** Set Basic auth */
55
+ setBasicAuth(username, password) {
56
+ const cred = Buffer.from(`${username}:${password}`).toString('base64');
57
+ this.headers.Authorization = `Basic ${cred}`;
58
+ return this;
59
+ }
60
+
61
+ /** Add request interceptor */
62
+ onRequest(fn) { this._interceptors.request.push(fn); return this; }
63
+
64
+ /** Add response interceptor */
65
+ onResponse(fn) { this._interceptors.response.push(fn); return this; }
66
+
67
+ // HTTP Methods
68
+ async get(url, options = {}) { return this.request('GET', url, null, options); }
69
+ async post(url, data, options = {}) { return this.request('POST', url, data, options); }
70
+ async put(url, data, options = {}) { return this.request('PUT', url, data, options); }
71
+ async patch(url, data, options = {}) { return this.request('PATCH', url, data, options); }
72
+ async delete(url, options = {}) { return this.request('DELETE', url, null, options); }
73
+ async head(url, options = {}) { return this.request('HEAD', url, null, options); }
74
+
75
+ /** Core request method */
76
+ async request(method, url, data = null, options = {}) {
77
+ const fullURL = url.startsWith('http') ? url : this.baseURL + url;
78
+ const parsed = new URL(fullURL);
79
+
80
+ // Query params
81
+ if (options.params) {
82
+ for (const [key, value] of Object.entries(options.params)) {
83
+ parsed.searchParams.set(key, value);
84
+ }
85
+ }
86
+
87
+ let reqConfig = {
88
+ method,
89
+ url: parsed.toString(),
90
+ headers: { ...this.headers, ...options.headers },
91
+ data,
92
+ };
93
+
94
+ // Run request interceptors
95
+ for (const fn of this._interceptors.request) {
96
+ reqConfig = await fn(reqConfig) || reqConfig;
97
+ }
98
+
99
+ // Serialize body
100
+ let body = null;
101
+ if (reqConfig.data !== null && reqConfig.data !== undefined) {
102
+ if (typeof reqConfig.data === 'string' || Buffer.isBuffer(reqConfig.data)) {
103
+ body = reqConfig.data;
104
+ } else if (reqConfig.data instanceof URLSearchParams) {
105
+ body = reqConfig.data.toString();
106
+ reqConfig.headers['Content-Type'] = 'application/x-www-form-urlencoded';
107
+ } else {
108
+ body = JSON.stringify(reqConfig.data);
109
+ }
110
+ }
111
+
112
+ // Retry logic
113
+ let lastError;
114
+ const maxAttempts = (options.retries ?? this.retries) + 1;
115
+
116
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
117
+ try {
118
+ const response = await this._doRequest(parsed, method, reqConfig.headers, body, options);
119
+
120
+ // Run response interceptors
121
+ let result = response;
122
+ for (const fn of this._interceptors.response) {
123
+ result = await fn(result) || result;
124
+ }
125
+
126
+ return result;
127
+ } catch (err) {
128
+ lastError = err;
129
+ if (attempt < maxAttempts) {
130
+ await new Promise(r => setTimeout(r, this.retryDelay * attempt));
131
+ }
132
+ }
133
+ }
134
+
135
+ throw lastError;
136
+ }
137
+
138
+ /** Download a file */
139
+ async download(url, filePath) {
140
+ const fs = require('fs');
141
+ const path = require('path');
142
+ const dir = path.dirname(filePath);
143
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
144
+
145
+ return new Promise((resolve, reject) => {
146
+ const fullURL = url.startsWith('http') ? url : this.baseURL + url;
147
+ const parsed = new URL(fullURL);
148
+ const transport = parsed.protocol === 'https:' ? https : http;
149
+
150
+ transport.get(fullURL, { headers: this.headers }, (response) => {
151
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
152
+ // Follow redirects
153
+ return this.download(response.headers.location, filePath).then(resolve).catch(reject);
154
+ }
155
+
156
+ const file = fs.createWriteStream(filePath);
157
+ response.pipe(file);
158
+ file.on('finish', () => {
159
+ file.close();
160
+ resolve({ path: filePath, size: fs.statSync(filePath).size });
161
+ });
162
+ }).on('error', reject);
163
+ });
164
+ }
165
+
166
+ // ===== STATIC SHORTCUTS =====
167
+
168
+ static async get(url, options = {}) { return new HttpClient(options).get(url, options); }
169
+ static async post(url, data, options = {}) { return new HttpClient(options).post(url, data, options); }
170
+ static async put(url, data, options = {}) { return new HttpClient(options).put(url, data, options); }
171
+ static async patch(url, data, options = {}) { return new HttpClient(options).patch(url, data, options); }
172
+ static async delete(url, options = {}) { return new HttpClient(options).delete(url, options); }
173
+
174
+ // ===== SWR: STALE-WHILE-REVALIDATE =====
175
+
176
+ /**
177
+ * SWR-like data fetching with cache + background revalidation
178
+ *
179
+ * @example
180
+ * const fetcher = HttpClient.swr({ ttl: 30, revalidate: 60 });
181
+ * const data = await fetcher.get('/api/users'); // Cached + background refresh
182
+ * fetcher.invalidate('/api/users');
183
+ */
184
+ static swr(options = {}) {
185
+ const client = new HttpClient(options);
186
+ const cache = new Map();
187
+ const ttl = (options.ttl || 60) * 1000; // stale after N seconds
188
+ const revalidateTTL = (options.revalidate || 300) * 1000; // refetch after N seconds
189
+ const dedupeInterval = (options.dedupe || 2) * 1000;
190
+ const inflight = new Map();
191
+
192
+ const getCacheKey = (method, url, params) =>
193
+ `${method}:${url}:${JSON.stringify(params || {})}`;
194
+
195
+ const fetcher = {
196
+ async get(url, params = {}) {
197
+ const key = getCacheKey('GET', url, params);
198
+ const now = Date.now();
199
+ const entry = cache.get(key);
200
+
201
+ // Return cached data if fresh
202
+ if (entry && (now - entry.timestamp) < ttl) {
203
+ return entry.data;
204
+ }
205
+
206
+ // Return stale data + revalidate in background
207
+ if (entry && (now - entry.timestamp) < revalidateTTL) {
208
+ // Dedupe: don't fire multiple revalidations
209
+ if (!inflight.has(key)) {
210
+ const revalidate = client.get(url, { params }).then(res => {
211
+ cache.set(key, { data: res, timestamp: Date.now() });
212
+ return res;
213
+ }).finally(() => {
214
+ setTimeout(() => inflight.delete(key), dedupeInterval);
215
+ });
216
+ inflight.set(key, revalidate);
217
+ }
218
+ return entry.data; // Return stale immediately
219
+ }
220
+
221
+ // No cache or expired: fetch fresh
222
+ if (inflight.has(key)) return inflight.get(key); // Dedupe
223
+
224
+ const promise = client.get(url, { params }).then(res => {
225
+ cache.set(key, { data: res, timestamp: Date.now() });
226
+ return res;
227
+ }).finally(() => {
228
+ setTimeout(() => inflight.delete(key), dedupeInterval);
229
+ });
230
+
231
+ inflight.set(key, promise);
232
+ return promise;
233
+ },
234
+
235
+ /** Invalidate cache for a URL */
236
+ invalidate(url) {
237
+ for (const key of cache.keys()) {
238
+ if (key.includes(url)) cache.delete(key);
239
+ }
240
+ },
241
+
242
+ /** Invalidate all cache */
243
+ invalidateAll() { cache.clear(); },
244
+
245
+ /** Mutate cached data directly (optimistic update) */
246
+ mutate(url, data) {
247
+ for (const [key, entry] of cache.entries()) {
248
+ if (key.includes(url)) {
249
+ entry.data = typeof data === 'function' ? data(entry.data) : data;
250
+ entry.timestamp = Date.now();
251
+ }
252
+ }
253
+ },
254
+
255
+ /** Prefetch data into cache */
256
+ async prefetch(url, params = {}) {
257
+ const key = getCacheKey('GET', url, params);
258
+ const res = await client.get(url, { params });
259
+ cache.set(key, { data: res, timestamp: Date.now() });
260
+ return res;
261
+ },
262
+
263
+ /** Get cache stats */
264
+ stats() {
265
+ return { size: cache.size, inflight: inflight.size };
266
+ },
267
+ };
268
+
269
+ return fetcher;
270
+ }
271
+
272
+ // ===== ABORT / CANCEL =====
273
+
274
+ /**
275
+ * Create an abortable request
276
+ *
277
+ * @example
278
+ * const { promise, abort } = HttpClient.abortable('https://api.example.com/data');
279
+ * setTimeout(() => abort(), 5000); // Cancel after 5s
280
+ * const data = await promise;
281
+ */
282
+ static abortable(url, options = {}) {
283
+ let aborted = false;
284
+ let rejectFn;
285
+
286
+ const promise = new Promise((resolve, reject) => {
287
+ rejectFn = reject;
288
+ if (aborted) return reject(new Error('Request aborted'));
289
+
290
+ const client = new HttpClient(options);
291
+ client.get(url, { ...options, allowError: true }).then(resolve).catch(reject);
292
+ });
293
+
294
+ return {
295
+ promise,
296
+ abort() {
297
+ aborted = true;
298
+ if (rejectFn) rejectFn(new Error('Request aborted'));
299
+ },
300
+ };
301
+ }
302
+
303
+ // ===== POLLING =====
304
+
305
+ /**
306
+ * Poll a URL at intervals
307
+ *
308
+ * @example
309
+ * const poller = HttpClient.poll('https://api.example.com/status', {
310
+ * interval: 5000,
311
+ * onData: (data) => console.log(data),
312
+ * until: (data) => data.status === 'complete',
313
+ * });
314
+ *
315
+ * poller.stop(); // Stop polling
316
+ */
317
+ static poll(url, options = {}) {
318
+ const {
319
+ interval = 5000,
320
+ onData,
321
+ onError,
322
+ until,
323
+ maxAttempts = Infinity,
324
+ immediate = true,
325
+ } = options;
326
+
327
+ const client = new HttpClient(options);
328
+ let timer = null;
329
+ let attempts = 0;
330
+ let stopped = false;
331
+
332
+ const tick = async () => {
333
+ if (stopped) return;
334
+ attempts++;
335
+ try {
336
+ const res = await client.get(url);
337
+ if (onData) onData(res.data || res, attempts);
338
+ if (until && until(res.data || res)) {
339
+ stopped = true;
340
+ return;
341
+ }
342
+ } catch (err) {
343
+ if (onError) onError(err, attempts);
344
+ }
345
+ if (!stopped && attempts < maxAttempts) {
346
+ timer = setTimeout(tick, interval);
347
+ }
348
+ };
349
+
350
+ if (immediate) tick();
351
+ else timer = setTimeout(tick, interval);
352
+
353
+ return {
354
+ stop() { stopped = true; if (timer) clearTimeout(timer); },
355
+ get attempts() { return attempts; },
356
+ get running() { return !stopped; },
357
+ };
358
+ }
359
+
360
+ // ===== CONCURRENT REQUESTS =====
361
+
362
+ /**
363
+ * Execute multiple requests in parallel
364
+ *
365
+ * @example
366
+ * const [users, posts] = await HttpClient.all([
367
+ * HttpClient.get('/api/users'),
368
+ * HttpClient.get('/api/posts'),
369
+ * ]);
370
+ */
371
+ static all(requests) {
372
+ return Promise.all(requests);
373
+ }
374
+
375
+ /** Like Promise.allSettled — returns all results even if some fail */
376
+ static allSettled(requests) {
377
+ return Promise.allSettled(requests);
378
+ }
379
+
380
+ /** Race: return first to resolve */
381
+ static race(requests) {
382
+ return Promise.race(requests);
383
+ }
384
+
385
+ // ===== INTERNAL =====
386
+
387
+ _doRequest(parsed, method, headers, body, options) {
388
+ return new Promise((resolve, reject) => {
389
+ const transport = parsed.protocol === 'https:' ? https : http;
390
+
391
+ const reqOptions = {
392
+ hostname: parsed.hostname,
393
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
394
+ path: parsed.pathname + parsed.search,
395
+ method,
396
+ headers: { ...headers },
397
+ timeout: options.timeout || this.timeout,
398
+ };
399
+
400
+ if (body) {
401
+ reqOptions.headers['Content-Length'] = Buffer.byteLength(body);
402
+ }
403
+
404
+ // Accept gzip
405
+ reqOptions.headers['Accept-Encoding'] = 'gzip, deflate';
406
+
407
+ const req = transport.request(reqOptions, (res) => {
408
+ const chunks = [];
409
+
410
+ // Handle gzip
411
+ let stream = res;
412
+ const encoding = res.headers['content-encoding'];
413
+ if (encoding === 'gzip') {
414
+ stream = res.pipe(zlib.createGunzip());
415
+ } else if (encoding === 'deflate') {
416
+ stream = res.pipe(zlib.createInflate());
417
+ }
418
+
419
+ stream.on('data', chunk => chunks.push(chunk));
420
+ stream.on('end', () => {
421
+ const rawBody = Buffer.concat(chunks).toString('utf-8');
422
+
423
+ let parsedBody = rawBody;
424
+ const contentType = res.headers['content-type'] || '';
425
+ if (contentType.includes('application/json')) {
426
+ try { parsedBody = JSON.parse(rawBody); } catch { }
427
+ }
428
+
429
+ const response = {
430
+ status: res.statusCode,
431
+ statusText: res.statusMessage,
432
+ headers: res.headers,
433
+ data: parsedBody,
434
+ raw: rawBody,
435
+ ok: res.statusCode >= 200 && res.statusCode < 300,
436
+ };
437
+
438
+ if (!response.ok && !options.allowError) {
439
+ const error = new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`);
440
+ error.response = response;
441
+ reject(error);
442
+ } else {
443
+ resolve(response);
444
+ }
445
+ });
446
+ stream.on('error', reject);
447
+ });
448
+
449
+ req.on('error', reject);
450
+ req.on('timeout', () => {
451
+ req.destroy();
452
+ reject(new Error(`Request timed out after ${reqOptions.timeout}ms`));
453
+ });
454
+
455
+ if (body) req.write(body);
456
+ req.end();
457
+ });
458
+ }
459
+ }
460
+
461
+ module.exports = { HttpClient };
@@ -0,0 +1,186 @@
1
+ /**
2
+ * VoltJS Logger
3
+ *
4
+ * Structured, colorful logging with levels, file output, and request logging.
5
+ *
6
+ * @example
7
+ * const { Logger } = require('voltjs');
8
+ *
9
+ * const log = new Logger({ level: 'debug', file: 'logs/app.log' });
10
+ * log.info('Server started', { port: 3000 });
11
+ * log.error('Failed', { error: err.message });
12
+ *
13
+ * // Request logging middleware
14
+ * app.use(Logger.requestLogger());
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const LEVELS = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, fatal: 5, silent: 6 };
23
+
24
+ const COLORS = {
25
+ trace: '\x1b[90m', // gray
26
+ debug: '\x1b[36m', // cyan
27
+ info: '\x1b[32m', // green
28
+ warn: '\x1b[33m', // yellow
29
+ error: '\x1b[31m', // red
30
+ fatal: '\x1b[35m', // magenta
31
+ reset: '\x1b[0m',
32
+ dim: '\x1b[2m',
33
+ bold: '\x1b[1m',
34
+ };
35
+
36
+ const ICONS = { trace: '·', debug: '◆', info: '✓', warn: '⚠', error: '✗', fatal: '☠' };
37
+
38
+ class Logger {
39
+ constructor(options = {}) {
40
+ this.level = options.level || process.env.LOG_LEVEL || 'info';
41
+ this.colorize = options.colorize !== false;
42
+ this.timestamp = options.timestamp !== false;
43
+ this.file = options.file || null;
44
+ this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024; // 10MB
45
+ this.prefix = options.prefix || '';
46
+
47
+ if (this.file) {
48
+ const dir = path.dirname(this.file);
49
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
50
+ }
51
+ }
52
+
53
+ trace(message, meta) { this._log('trace', message, meta); }
54
+ debug(message, meta) { this._log('debug', message, meta); }
55
+ info(message, meta) { this._log('info', message, meta); }
56
+ warn(message, meta) { this._log('warn', message, meta); }
57
+ error(message, meta) { this._log('error', message, meta); }
58
+ fatal(message, meta) { this._log('fatal', message, meta); }
59
+
60
+ /** Create a child logger with prefix */
61
+ child(prefix) {
62
+ return new Logger({
63
+ level: this.level,
64
+ colorize: this.colorize,
65
+ timestamp: this.timestamp,
66
+ file: this.file,
67
+ prefix: this.prefix ? `${this.prefix}:${prefix}` : prefix,
68
+ });
69
+ }
70
+
71
+ /** Time an operation */
72
+ time(label) {
73
+ const start = performance.now();
74
+ return {
75
+ end: (meta) => {
76
+ const duration = (performance.now() - start).toFixed(2);
77
+ this.debug(`${label} completed in ${duration}ms`, meta);
78
+ },
79
+ };
80
+ }
81
+
82
+ /** Create table from data */
83
+ table(data) {
84
+ if (!Array.isArray(data) || data.length === 0) return;
85
+ console.table(data);
86
+ }
87
+
88
+ /** Request logging middleware */
89
+ static requestLogger(options = {}) {
90
+ const logger = new Logger(options);
91
+
92
+ return (req, res) => {
93
+ const start = Date.now();
94
+
95
+ const originalEnd = res.end.bind(res);
96
+ res.end = (...args) => {
97
+ const duration = Date.now() - start;
98
+ const status = res.statusCode;
99
+ const color = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info';
100
+
101
+ logger._log(color, `${req.method} ${req.url} ${status} ${duration}ms`, {
102
+ method: req.method,
103
+ url: req.url,
104
+ status,
105
+ duration: `${duration}ms`,
106
+ ip: req.headers['x-forwarded-for'] || req.socket?.remoteAddress,
107
+ userAgent: req.headers['user-agent'],
108
+ });
109
+
110
+ originalEnd(...args);
111
+ };
112
+ };
113
+ }
114
+
115
+ /** Log to JSON format (for production/log aggregators) */
116
+ static json(options = {}) {
117
+ const logger = new Logger({ ...options, colorize: false });
118
+ logger._formatJSON = true;
119
+ return logger;
120
+ }
121
+
122
+ // ===== INTERNAL =====
123
+
124
+ _log(level, message, meta) {
125
+ if (LEVELS[level] < LEVELS[this.level]) return;
126
+
127
+ const ts = this.timestamp ? new Date().toISOString() : '';
128
+ const prefix = this.prefix ? `[${this.prefix}]` : '';
129
+
130
+ // JSON format
131
+ if (this._formatJSON) {
132
+ const entry = {
133
+ timestamp: ts,
134
+ level,
135
+ message,
136
+ ...(this.prefix ? { context: this.prefix } : {}),
137
+ ...(meta || {}),
138
+ };
139
+ const line = JSON.stringify(entry);
140
+ process.stdout.write(line + '\n');
141
+ if (this.file) this._writeFile(line);
142
+ return;
143
+ }
144
+
145
+ // Pretty format
146
+ const metaStr = meta ? ' ' + JSON.stringify(meta) : '';
147
+
148
+ if (this.colorize) {
149
+ const c = COLORS[level] || '';
150
+ const icon = ICONS[level] || '';
151
+ const tsStr = ts ? `${COLORS.dim}${ts}${COLORS.reset} ` : '';
152
+ const prefixStr = prefix ? `${COLORS.dim}${prefix}${COLORS.reset} ` : '';
153
+ const levelStr = `${c}${COLORS.bold}${level.toUpperCase().padEnd(5)}${COLORS.reset}`;
154
+ const metaColored = meta ? ` ${COLORS.dim}${JSON.stringify(meta)}${COLORS.reset}` : '';
155
+
156
+ process.stdout.write(`${tsStr}${c}${icon}${COLORS.reset} ${levelStr} ${prefixStr}${message}${metaColored}\n`);
157
+ } else {
158
+ const line = `${ts} ${level.toUpperCase().padEnd(5)} ${prefix} ${message}${metaStr}`.trim();
159
+ process.stdout.write(line + '\n');
160
+ }
161
+
162
+ // File output
163
+ if (this.file) {
164
+ const fileLine = `${ts} ${level.toUpperCase().padEnd(5)} ${prefix} ${message}${metaStr}`.trim();
165
+ this._writeFile(fileLine);
166
+ }
167
+ }
168
+
169
+ _writeFile(line) {
170
+ try {
171
+ // Rotate if too large
172
+ if (fs.existsSync(this.file)) {
173
+ const stat = fs.statSync(this.file);
174
+ if (stat.size > this.maxFileSize) {
175
+ const rotated = this.file + '.' + Date.now();
176
+ fs.renameSync(this.file, rotated);
177
+ }
178
+ }
179
+ fs.appendFileSync(this.file, line + '\n');
180
+ } catch (e) {
181
+ // Silently fail file logging
182
+ }
183
+ }
184
+ }
185
+
186
+ module.exports = { Logger };