@archznn/xavva 3.1.2 → 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.
- package/README.md +221 -12
- package/package.json +3 -2
- package/src/commands/AuditCommand.ts +12 -10
- package/src/commands/BuildCommand.ts +9 -7
- package/src/commands/ChangelogCommand.ts +5 -5
- package/src/commands/CleanCommand.ts +242 -0
- package/src/commands/CompletionCommand.ts +7 -7
- package/src/commands/DbCommand.ts +43 -14
- package/src/commands/DeployCommand.ts +252 -229
- package/src/commands/DepsCommand.ts +174 -174
- package/src/commands/DockerCommand.ts +35 -4
- package/src/commands/DoctorCommand.ts +252 -239
- package/src/commands/EncodingCommand.ts +26 -19
- package/src/commands/HealthCommand.ts +7 -7
- package/src/commands/HelpCommand.ts +34 -14
- package/src/commands/HistoryCommand.ts +5 -5
- package/src/commands/HttpCommand.ts +27 -1
- package/src/commands/IdeCommand.ts +313 -0
- package/src/commands/InitCommand.ts +26 -25
- package/src/commands/LogsCommand.ts +8 -6
- package/src/commands/ProfilesCommand.ts +6 -6
- package/src/commands/RedoCommand.ts +2 -2
- package/src/commands/RunCommand.ts +64 -24
- package/src/commands/StartCommand.ts +9 -7
- package/src/commands/TestCommand.ts +25 -1
- package/src/commands/TomcatCommand.ts +232 -88
- package/src/config/versions.ts +111 -9
- package/src/di/container.ts +239 -105
- package/src/errors/ErrorHandler.ts +23 -19
- package/src/errors/errorMessages.ts +235 -0
- package/src/index.ts +20 -6
- package/src/logging/FileLogger.ts +235 -0
- package/src/logging/Logger.ts +545 -0
- package/src/logging/OperationLogger.ts +296 -0
- package/src/logging/ProgressLogger.ts +187 -0
- package/src/logging/TableLogger.ts +246 -0
- package/src/logging/colors.ts +167 -0
- package/src/logging/constants.ts +176 -0
- package/src/logging/formatters.ts +337 -0
- package/src/logging/index.ts +93 -0
- package/src/logging/types.ts +64 -0
- package/src/plugins/PluginManager.ts +325 -0
- package/src/plugins/types.ts +82 -0
- package/src/services/AuditService.ts +5 -3
- package/src/services/BuildService.ts +15 -17
- package/src/services/DashboardService.ts +14 -3
- package/src/services/DbService.ts +35 -34
- package/src/services/DependencyAnalyzerService.ts +18 -18
- package/src/services/DependencyCacheService.ts +303 -0
- package/src/services/DeployWatcher.ts +127 -23
- package/src/services/DockerService.ts +3 -3
- package/src/services/EmbeddedTomcatService.ts +13 -12
- package/src/services/FileWatcher.ts +15 -7
- package/src/services/HttpService.ts +5 -5
- package/src/services/LogAnalyzer.ts +26 -22
- package/src/services/PerformanceProfiler.ts +267 -0
- package/src/services/ProjectService.ts +3 -0
- package/src/services/TestService.ts +3 -3
- package/src/services/TomcatService.ts +46 -25
- package/src/services/tomcat/TomcatBackupManager.ts +330 -0
- package/src/services/tomcat/TomcatChecksumVerifier.ts +211 -0
- package/src/services/tomcat/TomcatCompatibilityChecker.ts +298 -0
- package/src/services/tomcat/TomcatDownloadCache.ts +250 -0
- package/src/services/tomcat/TomcatDownloadService.ts +335 -0
- package/src/services/tomcat/TomcatInstallerService.ts +474 -0
- package/src/services/tomcat/TomcatMirrorManager.ts +181 -0
- package/src/services/tomcat/index.ts +36 -0
- package/src/services/tomcat/types.ts +120 -0
- package/src/types/args.ts +68 -1
- package/src/types/configSchema.ts +174 -0
- package/src/utils/ChangelogGenerator.ts +11 -11
- package/src/utils/LoggerLevel.ts +44 -20
- package/src/utils/ProgressBar.ts +87 -46
- package/src/utils/argsParser.ts +260 -0
- package/src/utils/config.ts +340 -189
- package/src/utils/constants.ts +87 -9
- package/src/utils/dryRun.ts +192 -0
- package/src/utils/processManager.ts +23 -7
- package/src/utils/security.ts +293 -0
- 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();
|