@archznn/xavva 3.1.3 → 3.2.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 (80) hide show
  1. package/README.md +221 -12
  2. package/package.json +3 -2
  3. package/src/commands/AuditCommand.ts +12 -10
  4. package/src/commands/BuildCommand.ts +9 -7
  5. package/src/commands/ChangelogCommand.ts +5 -5
  6. package/src/commands/CleanCommand.ts +242 -0
  7. package/src/commands/CompletionCommand.ts +7 -7
  8. package/src/commands/DbCommand.ts +33 -31
  9. package/src/commands/DeployCommand.ts +252 -229
  10. package/src/commands/DepsCommand.ts +174 -174
  11. package/src/commands/DockerCommand.ts +14 -14
  12. package/src/commands/DoctorCommand.ts +252 -239
  13. package/src/commands/EncodingCommand.ts +19 -19
  14. package/src/commands/HealthCommand.ts +7 -7
  15. package/src/commands/HelpCommand.ts +34 -14
  16. package/src/commands/HistoryCommand.ts +5 -5
  17. package/src/commands/HttpCommand.ts +6 -6
  18. package/src/commands/IdeCommand.ts +313 -0
  19. package/src/commands/InitCommand.ts +26 -25
  20. package/src/commands/LogsCommand.ts +8 -6
  21. package/src/commands/ProfilesCommand.ts +6 -6
  22. package/src/commands/RedoCommand.ts +2 -2
  23. package/src/commands/RunCommand.ts +64 -24
  24. package/src/commands/StartCommand.ts +9 -7
  25. package/src/commands/TestCommand.ts +4 -4
  26. package/src/commands/TomcatCommand.ts +219 -100
  27. package/src/config/versions.ts +111 -9
  28. package/src/di/container.ts +239 -105
  29. package/src/errors/ErrorHandler.ts +23 -19
  30. package/src/errors/errorMessages.ts +235 -0
  31. package/src/index.ts +11 -3
  32. package/src/logging/FileLogger.ts +235 -0
  33. package/src/logging/Logger.ts +545 -0
  34. package/src/logging/OperationLogger.ts +296 -0
  35. package/src/logging/ProgressLogger.ts +187 -0
  36. package/src/logging/TableLogger.ts +246 -0
  37. package/src/logging/colors.ts +167 -0
  38. package/src/logging/constants.ts +176 -0
  39. package/src/logging/formatters.ts +337 -0
  40. package/src/logging/index.ts +93 -0
  41. package/src/logging/types.ts +64 -0
  42. package/src/plugins/PluginManager.ts +325 -0
  43. package/src/plugins/types.ts +82 -0
  44. package/src/services/AuditService.ts +5 -3
  45. package/src/services/BuildService.ts +15 -17
  46. package/src/services/DashboardService.ts +14 -3
  47. package/src/services/DbService.ts +35 -34
  48. package/src/services/DependencyAnalyzerService.ts +18 -18
  49. package/src/services/DependencyCacheService.ts +303 -0
  50. package/src/services/DeployWatcher.ts +127 -23
  51. package/src/services/DockerService.ts +3 -3
  52. package/src/services/EmbeddedTomcatService.ts +13 -12
  53. package/src/services/FileWatcher.ts +15 -7
  54. package/src/services/HttpService.ts +5 -5
  55. package/src/services/LogAnalyzer.ts +26 -22
  56. package/src/services/PerformanceProfiler.ts +267 -0
  57. package/src/services/ProjectService.ts +3 -0
  58. package/src/services/TestService.ts +3 -3
  59. package/src/services/TomcatService.ts +46 -25
  60. package/src/services/tomcat/TomcatBackupManager.ts +330 -0
  61. package/src/services/tomcat/TomcatChecksumVerifier.ts +211 -0
  62. package/src/services/tomcat/TomcatCompatibilityChecker.ts +298 -0
  63. package/src/services/tomcat/TomcatDownloadCache.ts +250 -0
  64. package/src/services/tomcat/TomcatDownloadService.ts +335 -0
  65. package/src/services/tomcat/TomcatInstallerService.ts +474 -0
  66. package/src/services/tomcat/TomcatMirrorManager.ts +181 -0
  67. package/src/services/tomcat/index.ts +36 -0
  68. package/src/services/tomcat/types.ts +120 -0
  69. package/src/types/args.ts +68 -1
  70. package/src/types/configSchema.ts +174 -0
  71. package/src/utils/ChangelogGenerator.ts +11 -11
  72. package/src/utils/LoggerLevel.ts +44 -20
  73. package/src/utils/ProgressBar.ts +87 -46
  74. package/src/utils/argsParser.ts +260 -0
  75. package/src/utils/config.ts +340 -189
  76. package/src/utils/constants.ts +87 -9
  77. package/src/utils/dryRun.ts +192 -0
  78. package/src/utils/processManager.ts +23 -7
  79. package/src/utils/security.ts +293 -0
  80. package/src/utils/ui.ts +299 -428
@@ -0,0 +1,545 @@
1
+ /**
2
+ * Logger principal do XAVVA CLI
3
+ * Sistema unificado de logging com suporte a hierarquia, rate limiting e múltiplos formatos
4
+ */
5
+
6
+ import type { LogLevel, LogEntry, LoggerConfig, LogContext, RateLimitEntry } from './types';
7
+ import { Colors, colorize, Icons, getIcon, supportsColor, visualWidth, padText, stripAnsi } from './colors';
8
+ import {
9
+ DEFAULT_CONFIG,
10
+ LOG_LEVELS,
11
+ LEVEL_COLORS,
12
+ LEVEL_ICONS,
13
+ LAYOUT,
14
+ NOISE_PATTERNS,
15
+ ESSENTIAL_PATTERNS,
16
+ SPINNER_FRAMES,
17
+ SPINNER_INTERVAL
18
+ } from './constants';
19
+ import { formatEntry, formatStatusLine, formatSection, formatConfig, formatUrl, formatFile } from './formatters';
20
+
21
+ export class Logger {
22
+ private static instance: Logger;
23
+ private loggerConfig: LoggerConfig;
24
+ private rateLimitMap: Map<string, RateLimitEntry> = new Map();
25
+ private groupStack: string[] = [];
26
+ private spinners: Map<string, { timer: Timer; stop: () => void }> = new Map();
27
+
28
+ // Cache de contexto para reutilização
29
+ private currentTraceId: string | null = null;
30
+
31
+ private constructor(config: Partial<LoggerConfig> = {}) {
32
+ this.loggerConfig = { ...DEFAULT_CONFIG, ...config };
33
+
34
+ // Detecta suporte a cores automaticamente
35
+ if (!supportsColor()) {
36
+ this.loggerConfig.colors = false;
37
+ }
38
+
39
+ // Limpa rate limit periodicamente
40
+ setInterval(() => this.cleanupRateLimit(), 60000);
41
+ }
42
+
43
+ /**
44
+ * Obtém instância singleton do Logger
45
+ */
46
+ static getInstance(config?: Partial<LoggerConfig>): Logger {
47
+ if (!Logger.instance) {
48
+ Logger.instance = new Logger(config);
49
+ }
50
+ return Logger.instance;
51
+ }
52
+
53
+ /**
54
+ * Reinicia a instância (útil para testes)
55
+ */
56
+ static reset(): void {
57
+ Logger.instance = new Logger();
58
+ }
59
+
60
+ /**
61
+ * Configura o logger
62
+ */
63
+ configure(config: Partial<LoggerConfig>): void {
64
+ this.loggerConfig = { ...this.loggerConfig, ...config };
65
+ }
66
+
67
+ /**
68
+ * Obtém configuração atual
69
+ */
70
+ getConfig(): LoggerConfig {
71
+ return { ...this.loggerConfig };
72
+ }
73
+
74
+ /**
75
+ * Define nível de log
76
+ */
77
+ setLevel(level: LogLevel): void {
78
+ this.loggerConfig.level = level;
79
+ }
80
+
81
+ /**
82
+ * Define modo de saída
83
+ */
84
+ setMode(mode: LoggerConfig['mode']): void {
85
+ this.loggerConfig.mode = mode;
86
+ }
87
+
88
+ /**
89
+ * Verifica se um nível deve ser logado
90
+ */
91
+ private shouldLog(level: LogLevel): boolean {
92
+ return LOG_LEVELS[level] <= LOG_LEVELS[this.loggerConfig.level];
93
+ }
94
+
95
+ /**
96
+ * Verifica rate limiting
97
+ */
98
+ private checkRateLimit(message: string): boolean {
99
+ const now = Date.now();
100
+ const plainMessage = stripAnsi(message);
101
+ const existing = this.rateLimitMap.get(plainMessage);
102
+
103
+ if (existing) {
104
+ // Verifica se está dentro da janela de rate limit
105
+ if (now - existing.firstSeen < this.loggerConfig.rateLimitWindowMs) {
106
+ existing.count++;
107
+ existing.lastSeen = now;
108
+
109
+ // Se excedeu o limite, suprime o log
110
+ if (existing.count > this.loggerConfig.maxDuplicateLogs) {
111
+ return false;
112
+ }
113
+ } else {
114
+ // Reseta o contador se passou da janela
115
+ existing.count = 1;
116
+ existing.firstSeen = now;
117
+ existing.lastSeen = now;
118
+ }
119
+ } else {
120
+ // Primeira ocorrência
121
+ this.rateLimitMap.set(plainMessage, {
122
+ message: plainMessage,
123
+ count: 1,
124
+ firstSeen: now,
125
+ lastSeen: now,
126
+ });
127
+ }
128
+
129
+ return true;
130
+ }
131
+
132
+ /**
133
+ * Limpa entradas antigas do rate limit
134
+ */
135
+ private cleanupRateLimit(): void {
136
+ const now = Date.now();
137
+ for (const [key, entry] of this.rateLimitMap) {
138
+ if (now - entry.lastSeen > this.loggerConfig.rateLimitWindowMs * 2) {
139
+ // Se foi suprimido, mostra resumo
140
+ if (entry.count > this.loggerConfig.maxDuplicateLogs) {
141
+ const suppressed = entry.count - this.loggerConfig.maxDuplicateLogs;
142
+ this.write({
143
+ level: 'debug',
144
+ message: `${Colors.gray}(mensagem repetida ${suppressed}x e suprimida)${Colors.reset}`,
145
+ timestamp: new Date(),
146
+ });
147
+ }
148
+ this.rateLimitMap.delete(key);
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Gera um trace ID único
155
+ */
156
+ generateTraceId(): string {
157
+ const id = Math.random().toString(36).substring(2, 8);
158
+ this.currentTraceId = id;
159
+ return id;
160
+ }
161
+
162
+ /**
163
+ * Define trace ID atual
164
+ */
165
+ setTraceId(id: string | null): void {
166
+ this.currentTraceId = id;
167
+ }
168
+
169
+ /**
170
+ * Obtém trace ID atual
171
+ */
172
+ getTraceId(): string | null {
173
+ return this.currentTraceId;
174
+ }
175
+
176
+ /**
177
+ * Cria contexto de log
178
+ */
179
+ private createContext(extra?: Partial<LogContext>): LogContext {
180
+ return {
181
+ traceId: this.currentTraceId || undefined,
182
+ indent: this.groupStack.length,
183
+ ...extra,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Escreve uma entrada de log
189
+ */
190
+ private write(entry: LogEntry): void {
191
+ if (this.loggerConfig.mode === 'silent') return;
192
+
193
+ const formatted = formatEntry(entry, this.loggerConfig);
194
+ if (!formatted) return;
195
+
196
+ // Usa console.log ou console.error conforme o nível
197
+ if (entry.level === 'error') {
198
+ console.error(formatted);
199
+ } else {
200
+ console.log(formatted);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Log genérico
206
+ */
207
+ log(level: LogLevel, message: string, metadata?: Record<string, unknown>): void {
208
+ if (!this.shouldLog(level)) return;
209
+ if (!this.checkRateLimit(message)) return;
210
+
211
+ this.write({
212
+ level,
213
+ message,
214
+ timestamp: new Date(),
215
+ context: this.createContext(),
216
+ metadata,
217
+ });
218
+ }
219
+
220
+ // ========== Métodos de conveniência por nível ==========
221
+
222
+ error(message: string, metadata?: Record<string, unknown>): void {
223
+ const icon = this.loggerConfig.icons ? `${Icons.error} ` : '';
224
+ const color = this.loggerConfig.colors ? Colors.error : '';
225
+ const reset = this.loggerConfig.colors ? Colors.reset : '';
226
+ this.log('error', `${color}${icon}${message}${reset}`, metadata);
227
+ }
228
+
229
+ warn(message: string, metadata?: Record<string, unknown>): void {
230
+ const icon = this.loggerConfig.icons ? `${Icons.warning} ` : '';
231
+ const color = this.loggerConfig.colors ? Colors.warning : '';
232
+ const reset = this.loggerConfig.colors ? Colors.reset : '';
233
+ this.log('warn', `${color}${icon}${message}${reset}`, metadata);
234
+ }
235
+
236
+ info(message: string, metadata?: Record<string, unknown>): void {
237
+ const icon = this.loggerConfig.icons ? `${Icons.info} ` : '';
238
+ const color = this.loggerConfig.colors ? Colors.info : '';
239
+ const reset = this.loggerConfig.colors ? Colors.reset : '';
240
+ this.log('info', `${color}${icon}${message}${reset}`, metadata);
241
+ }
242
+
243
+ success(message: string, metadata?: Record<string, unknown>): void {
244
+ const icon = this.loggerConfig.icons ? `${Icons.success} ` : '';
245
+ const color = this.loggerConfig.colors ? Colors.success : '';
246
+ const reset = this.loggerConfig.colors ? Colors.reset : '';
247
+ this.log('success', `${color}${icon}${message}${reset}`, metadata);
248
+ }
249
+
250
+ debug(message: string, metadata?: Record<string, unknown>): void {
251
+ const icon = this.loggerConfig.icons ? `${Icons.bullet} ` : '';
252
+ const color = this.loggerConfig.colors ? Colors.gray : '';
253
+ const reset = this.loggerConfig.colors ? Colors.reset : '';
254
+ this.log('debug', `${color}${icon}${message}${reset}`, metadata);
255
+ }
256
+
257
+ trace(message: string, metadata?: Record<string, unknown>): void {
258
+ const icon = this.loggerConfig.icons ? `${Icons.circle} ` : '';
259
+ const color = this.loggerConfig.colors ? Colors.darkGray : '';
260
+ const reset = this.loggerConfig.colors ? Colors.reset : '';
261
+ this.log('trace', `${color}${icon}${message}${reset}`, metadata);
262
+ }
263
+
264
+ silly(message: string, metadata?: Record<string, unknown>): void {
265
+ const color = this.loggerConfig.colors ? Colors.darkGray : '';
266
+ const reset = this.loggerConfig.colors ? Colors.reset : '';
267
+ this.log('silly', `${color}${message}${reset}`, metadata);
268
+ }
269
+
270
+ // ========== Métodos de formatação específica ==========
271
+
272
+ /**
273
+ * Log de status (nome, status, info)
274
+ */
275
+ status(
276
+ name: string,
277
+ status: 'pending' | 'running' | 'success' | 'error' | 'warning',
278
+ info?: string
279
+ ): void {
280
+ if (!this.shouldLog('info')) return;
281
+
282
+ const line = formatStatusLine(name, status, info, this.loggerConfig);
283
+ this.write({
284
+ level: status === 'error' ? 'error' : 'info',
285
+ message: line,
286
+ timestamp: new Date(),
287
+ context: this.createContext(),
288
+ });
289
+ }
290
+
291
+ /**
292
+ * Log de seção
293
+ */
294
+ section(title: string): void {
295
+ if (!this.shouldLog('info')) return;
296
+
297
+ const formatted = formatSection(title, this.loggerConfig);
298
+ this.write({
299
+ level: 'info',
300
+ message: formatted,
301
+ timestamp: new Date(),
302
+ context: this.createContext(),
303
+ });
304
+ }
305
+
306
+ /**
307
+ * Log de configuração
308
+ */
309
+ config(key: string, value: string | number | boolean): void {
310
+ if (!this.shouldLog('info')) return;
311
+
312
+ const formatted = formatConfig(key, value, this.loggerConfig);
313
+ this.write({
314
+ level: 'info',
315
+ message: formatted,
316
+ timestamp: new Date(),
317
+ context: this.createContext(),
318
+ });
319
+ }
320
+
321
+ /**
322
+ * Log de URL
323
+ */
324
+ url(label: string, url: string): void {
325
+ if (!this.shouldLog('info')) return;
326
+
327
+ const formatted = formatUrl(label, url, this.loggerConfig);
328
+ this.write({
329
+ level: 'info',
330
+ message: formatted,
331
+ timestamp: new Date(),
332
+ context: this.createContext(),
333
+ });
334
+ }
335
+
336
+ /**
337
+ * Log de arquivo
338
+ */
339
+ file(name: string, action: 'changed' | 'compiled' | 'synced' | 'error', path?: string): void {
340
+ if (!this.shouldLog('info')) return;
341
+
342
+ const formatted = formatFile(name, action, path, this.loggerConfig);
343
+ this.write({
344
+ level: action === 'error' ? 'error' : 'info',
345
+ message: formatted,
346
+ timestamp: new Date(),
347
+ context: this.createContext(),
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Log de pronto/concluído
353
+ */
354
+ ready(message: string): void {
355
+ const icon = this.loggerConfig.icons ? `${Icons.success} ` : '';
356
+ const color = this.loggerConfig.colors ? Colors.success : '';
357
+ const bold = this.loggerConfig.colors ? Colors.bold : '';
358
+ const reset = this.loggerConfig.colors ? Colors.reset : '';
359
+ this.success(`${color}${icon}${bold}${message}${reset}`);
360
+ }
361
+
362
+ /**
363
+ * Log de passo/etapa
364
+ */
365
+ step(message: string): void {
366
+ const icon = this.loggerConfig.icons ? `${Icons.arrow} ` : '';
367
+ const color = this.loggerConfig.colors ? Colors.gray : '';
368
+ const reset = this.loggerConfig.colors ? Colors.reset : '';
369
+ this.info(`${color}${icon}${message}${reset}`);
370
+ }
371
+
372
+ /**
373
+ * Log de divisão
374
+ */
375
+ divider(): void {
376
+ const indent = ' '.repeat(LAYOUT.indentSize);
377
+ const line = this.loggerConfig.colors
378
+ ? `${Colors.darkGray}${LAYOUT.borders.horizontal.repeat(60)}${Colors.reset}`
379
+ : '-'.repeat(60);
380
+ this.write({
381
+ level: 'info',
382
+ message: `${indent}${line}`,
383
+ timestamp: new Date(),
384
+ context: this.createContext(),
385
+ });
386
+ }
387
+
388
+ /**
389
+ * Nova linha
390
+ */
391
+ newline(): void {
392
+ console.log();
393
+ }
394
+
395
+ // ========== Grupos hierárquicos ==========
396
+
397
+ /**
398
+ * Inicia um grupo de logs
399
+ */
400
+ group(label: string): void {
401
+ if (!this.shouldLog('info')) return;
402
+
403
+ const indent = this.groupStack.length * LAYOUT.indentSize;
404
+ const icon = this.loggerConfig.icons ? '▶ ' : '';
405
+ const color = this.loggerConfig.colors ? Colors.bold : '';
406
+ const reset = this.loggerConfig.colors ? Colors.reset : '';
407
+
408
+ this.write({
409
+ level: 'info',
410
+ message: `${' '.repeat(indent)}${color}${icon}${label}${reset}`,
411
+ timestamp: new Date(),
412
+ context: this.createContext(),
413
+ });
414
+
415
+ this.groupStack.push(label);
416
+ }
417
+
418
+ /**
419
+ * Encerra um grupo
420
+ */
421
+ groupEnd(): void {
422
+ this.groupStack.pop();
423
+ }
424
+
425
+ /**
426
+ * Executa função dentro de um grupo
427
+ */
428
+ async withGroup<T>(label: string, fn: () => Promise<T>): Promise<T> {
429
+ this.group(label);
430
+ try {
431
+ return await fn();
432
+ } finally {
433
+ this.groupEnd();
434
+ }
435
+ }
436
+
437
+ // ========== Spinner / Loading ==========
438
+
439
+ /**
440
+ * Cria um spinner
441
+ */
442
+ spinner(message: string): { stop: (success?: boolean) => void; update: (msg: string) => void } {
443
+ if (this.loggerConfig.mode === 'silent' || this.loggerConfig.mode === 'minimal') {
444
+ return {
445
+ stop: () => {},
446
+ update: () => {}
447
+ };
448
+ }
449
+
450
+ const id = Math.random().toString(36).substring(2, 8);
451
+ const frames = SPINNER_FRAMES.default;
452
+ let frameIndex = 0;
453
+
454
+ // Para cursor
455
+ process.stdout.write('\x1B[?25l');
456
+
457
+ const render = (msg: string) => {
458
+ const frame = frames[frameIndex];
459
+ const color = this.loggerConfig.colors ? Colors.primary : '';
460
+ const dim = this.loggerConfig.colors ? Colors.dim : '';
461
+ const reset = this.loggerConfig.colors ? Colors.reset : '';
462
+ const indent = this.groupStack.length * LAYOUT.indentSize;
463
+
464
+ const line = `${' '.repeat(indent)}${color}${frame}${reset} ${dim}${msg}${reset}`;
465
+ process.stdout.write(`\r${line}`);
466
+
467
+ frameIndex = (frameIndex + 1) % frames.length;
468
+ };
469
+
470
+ render(message);
471
+
472
+ const timer = setInterval(() => render(message), SPINNER_INTERVAL);
473
+
474
+ const stop = (success = true) => {
475
+ clearInterval(timer);
476
+ this.spinners.delete(id);
477
+
478
+ const icon = success ? Icons.success : Icons.error;
479
+ const color = success
480
+ ? (this.loggerConfig.colors ? Colors.success : '')
481
+ : (this.loggerConfig.colors ? Colors.error : '');
482
+ const reset = this.loggerConfig.colors ? Colors.reset : '';
483
+ const indent = this.groupStack.length * LAYOUT.indentSize;
484
+
485
+ // Limpa a linha e escreve o resultado
486
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
487
+ console.log(`${' '.repeat(indent)}${color}${icon}${reset} ${message}`);
488
+
489
+ // Restaura cursor
490
+ process.stdout.write('\x1B[?25h');
491
+ };
492
+
493
+ const update = (msg: string) => {
494
+ message = msg;
495
+ };
496
+
497
+ this.spinners.set(id, { timer, stop });
498
+
499
+ return { stop, update };
500
+ }
501
+
502
+ /**
503
+ * Para todos os spinners ativos
504
+ */
505
+ stopAllSpinners(success = false): void {
506
+ for (const [, spinner] of this.spinners) {
507
+ spinner.stop();
508
+ }
509
+ this.spinners.clear();
510
+ process.stdout.write('\x1B[?25h');
511
+ }
512
+
513
+ // ========== Métodos estáticos para conveniência ==========
514
+
515
+ static error(message: string, metadata?: Record<string, unknown>): void {
516
+ Logger.getInstance().error(message, metadata);
517
+ }
518
+
519
+ static warn(message: string, metadata?: Record<string, unknown>): void {
520
+ Logger.getInstance().warn(message, metadata);
521
+ }
522
+
523
+ static info(message: string, metadata?: Record<string, unknown>): void {
524
+ Logger.getInstance().info(message, metadata);
525
+ }
526
+
527
+ static success(message: string, metadata?: Record<string, unknown>): void {
528
+ Logger.getInstance().success(message, metadata);
529
+ }
530
+
531
+ static debug(message: string, metadata?: Record<string, unknown>): void {
532
+ Logger.getInstance().debug(message, metadata);
533
+ }
534
+
535
+ static trace(message: string, metadata?: Record<string, unknown>): void {
536
+ Logger.getInstance().trace(message, metadata);
537
+ }
538
+
539
+ static silly(message: string, metadata?: Record<string, unknown>): void {
540
+ Logger.getInstance().silly(message, metadata);
541
+ }
542
+ }
543
+
544
+ // Exporta instância padrão
545
+ export const logger = Logger.getInstance();