@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,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OperationLogger - Logger especializado para operações complexas com múltiplas etapas
|
|
3
|
+
*
|
|
4
|
+
* Uso:
|
|
5
|
+
* const op = new OperationLogger('deploy');
|
|
6
|
+
* op.start('Iniciando deploy');
|
|
7
|
+
*
|
|
8
|
+
* const buildStep = op.step('build', 'Compilando projeto...');
|
|
9
|
+
* await buildService.build();
|
|
10
|
+
* buildStep.success('Build concluído em 3.2s');
|
|
11
|
+
*
|
|
12
|
+
* op.complete('Deploy finalizado com sucesso');
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { OperationStep } from './types';
|
|
16
|
+
import { Logger } from './Logger';
|
|
17
|
+
import { Colors, Icons } from './colors';
|
|
18
|
+
import { LAYOUT } from './constants';
|
|
19
|
+
|
|
20
|
+
export class OperationStepLogger {
|
|
21
|
+
private operation: OperationLogger;
|
|
22
|
+
private step: OperationStep;
|
|
23
|
+
|
|
24
|
+
constructor(operation: OperationLogger, step: OperationStep) {
|
|
25
|
+
this.operation = operation;
|
|
26
|
+
this.step = step;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Marca o passo como em execução
|
|
31
|
+
*/
|
|
32
|
+
start(message?: string): void {
|
|
33
|
+
this.step.status = 'running';
|
|
34
|
+
this.step.startTime = new Date();
|
|
35
|
+
if (message) {
|
|
36
|
+
this.step.message = message;
|
|
37
|
+
}
|
|
38
|
+
this.operation.logStep(this.step);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Atualiza mensagem do passo
|
|
43
|
+
*/
|
|
44
|
+
update(message: string): void {
|
|
45
|
+
this.step.message = message;
|
|
46
|
+
this.operation.logStep(this.step);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Marca o passo como concluído com sucesso
|
|
51
|
+
*/
|
|
52
|
+
success(message?: string): void {
|
|
53
|
+
this.step.status = 'success';
|
|
54
|
+
this.step.endTime = new Date();
|
|
55
|
+
if (message) {
|
|
56
|
+
this.step.message = message;
|
|
57
|
+
}
|
|
58
|
+
this.operation.logStep(this.step);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Marca o passo como falho
|
|
63
|
+
*/
|
|
64
|
+
fail(error: Error | string): void {
|
|
65
|
+
this.step.status = 'failed';
|
|
66
|
+
this.step.endTime = new Date();
|
|
67
|
+
this.step.error = error instanceof Error ? error : new Error(error);
|
|
68
|
+
this.step.message = error instanceof Error ? error.message : error;
|
|
69
|
+
this.operation.logStep(this.step);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Obtém dados do passo
|
|
74
|
+
*/
|
|
75
|
+
getData(): OperationStep {
|
|
76
|
+
return { ...this.step };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class OperationLogger {
|
|
81
|
+
private name: string;
|
|
82
|
+
private steps: OperationStep[] = [];
|
|
83
|
+
private startTime: Date | null = null;
|
|
84
|
+
private endTime: Date | null = null;
|
|
85
|
+
private status: 'pending' | 'running' | 'success' | 'failed' = 'pending';
|
|
86
|
+
private traceId: string;
|
|
87
|
+
private logger: Logger;
|
|
88
|
+
|
|
89
|
+
constructor(name: string, logger?: Logger) {
|
|
90
|
+
this.name = name;
|
|
91
|
+
this.logger = logger || Logger.getInstance();
|
|
92
|
+
this.traceId = this.logger.generateTraceId() || '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Inicia a operação
|
|
97
|
+
*/
|
|
98
|
+
start(message?: string): void {
|
|
99
|
+
this.startTime = new Date();
|
|
100
|
+
this.status = 'running';
|
|
101
|
+
|
|
102
|
+
const icon = Icons.arrow;
|
|
103
|
+
const color = Colors.primary;
|
|
104
|
+
const bold = Colors.bold;
|
|
105
|
+
const reset = Colors.reset;
|
|
106
|
+
|
|
107
|
+
// Usa log direto sem ícone duplicado
|
|
108
|
+
this.logger.log('info', `${color}${icon}${reset} ${bold}${this.name}${reset}${message ? ': ' + message : ''}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Cria um novo passo na operação
|
|
113
|
+
*/
|
|
114
|
+
step(id: string, message?: string): OperationStepLogger {
|
|
115
|
+
const stepData: OperationStep = {
|
|
116
|
+
id,
|
|
117
|
+
name: id,
|
|
118
|
+
status: 'pending',
|
|
119
|
+
message,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
this.steps.push(stepData);
|
|
123
|
+
const stepLogger = new OperationStepLogger(this, stepData);
|
|
124
|
+
stepLogger.start(message);
|
|
125
|
+
return stepLogger;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Loga um passo
|
|
130
|
+
*/
|
|
131
|
+
logStep(step: OperationStep): void {
|
|
132
|
+
const indent = ' '.repeat(LAYOUT.indentSize * 2);
|
|
133
|
+
|
|
134
|
+
let icon: string;
|
|
135
|
+
let color: string;
|
|
136
|
+
|
|
137
|
+
switch (step.status) {
|
|
138
|
+
case 'running':
|
|
139
|
+
icon = Icons.spinner;
|
|
140
|
+
color = Colors.primary;
|
|
141
|
+
break;
|
|
142
|
+
case 'success':
|
|
143
|
+
icon = Icons.success;
|
|
144
|
+
color = Colors.success;
|
|
145
|
+
break;
|
|
146
|
+
case 'failed':
|
|
147
|
+
icon = Icons.error;
|
|
148
|
+
color = Colors.error;
|
|
149
|
+
break;
|
|
150
|
+
default:
|
|
151
|
+
icon = Icons.pending;
|
|
152
|
+
color = Colors.gray;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const reset = Colors.reset;
|
|
156
|
+
const dim = Colors.dim;
|
|
157
|
+
|
|
158
|
+
let message = step.message || step.name;
|
|
159
|
+
|
|
160
|
+
// Adiciona tempo se concluído
|
|
161
|
+
if ((step.status === 'success' || step.status === 'failed') && step.startTime && step.endTime) {
|
|
162
|
+
const duration = ((step.endTime.getTime() - step.startTime.getTime()) / 1000).toFixed(1);
|
|
163
|
+
message += ` ${dim}(${duration}s)${reset}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Usa log direto sem ícone duplicado do logger.info()
|
|
167
|
+
this.logger.log('info', `${indent}${color}${icon}${reset} ${message}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Completa a operação com sucesso
|
|
172
|
+
*/
|
|
173
|
+
complete(message?: string): void {
|
|
174
|
+
this.endTime = new Date();
|
|
175
|
+
this.status = 'success';
|
|
176
|
+
|
|
177
|
+
const icon = Icons.success;
|
|
178
|
+
const color = Colors.success;
|
|
179
|
+
const bold = Colors.bold;
|
|
180
|
+
const reset = Colors.reset;
|
|
181
|
+
const dim = Colors.dim;
|
|
182
|
+
|
|
183
|
+
let finalMessage = message || `${this.name} concluído`;
|
|
184
|
+
|
|
185
|
+
// Adiciona tempo total
|
|
186
|
+
if (this.startTime) {
|
|
187
|
+
const duration = ((this.endTime.getTime() - this.startTime.getTime()) / 1000).toFixed(1);
|
|
188
|
+
finalMessage += ` ${dim}(${duration}s)${reset}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Usa log direto sem ícone duplicado
|
|
192
|
+
this.logger.log('info', `${color}${icon}${reset} ${bold}${finalMessage}${reset}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Falha na operação
|
|
197
|
+
*/
|
|
198
|
+
fail(message?: string, error?: Error): void {
|
|
199
|
+
this.endTime = new Date();
|
|
200
|
+
this.status = 'failed';
|
|
201
|
+
|
|
202
|
+
const icon = Icons.error;
|
|
203
|
+
const color = Colors.error;
|
|
204
|
+
const bold = Colors.bold;
|
|
205
|
+
const reset = Colors.reset;
|
|
206
|
+
const dim = Colors.dim;
|
|
207
|
+
|
|
208
|
+
let finalMessage = message || `${this.name} falhou`;
|
|
209
|
+
|
|
210
|
+
// Adiciona tempo total
|
|
211
|
+
if (this.startTime) {
|
|
212
|
+
const duration = ((this.endTime.getTime() - this.startTime.getTime()) / 1000).toFixed(1);
|
|
213
|
+
finalMessage += ` ${dim}(${duration}s)${reset}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Usa log direto para erro
|
|
217
|
+
this.logger.log('error', `${color}${icon}${reset} ${bold}${finalMessage}${reset}`);
|
|
218
|
+
|
|
219
|
+
// Loga detalhes do erro
|
|
220
|
+
if (error) {
|
|
221
|
+
this.logger.group('Detalhes do erro');
|
|
222
|
+
this.logger.error(error.message);
|
|
223
|
+
if (error.stack) {
|
|
224
|
+
const lines = error.stack.split('\n').slice(1, 5);
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
this.logger.debug(line.trim());
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
this.logger.groupEnd();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Loga passos que falharam
|
|
233
|
+
const failedSteps = this.steps.filter(s => s.status === 'failed');
|
|
234
|
+
if (failedSteps.length > 0) {
|
|
235
|
+
this.logger.group('Passos com falha');
|
|
236
|
+
for (const step of failedSteps) {
|
|
237
|
+
this.logger.error(`${step.name}: ${step.error?.message || 'Falha desconhecida'}`);
|
|
238
|
+
}
|
|
239
|
+
this.logger.groupEnd();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Obtém estatísticas da operação
|
|
245
|
+
*/
|
|
246
|
+
getStats(): {
|
|
247
|
+
name: string;
|
|
248
|
+
status: string;
|
|
249
|
+
duration: number;
|
|
250
|
+
steps: { total: number; success: number; failed: number; pending: number };
|
|
251
|
+
} {
|
|
252
|
+
const duration = this.startTime && this.endTime
|
|
253
|
+
? (this.endTime.getTime() - this.startTime.getTime()) / 1000
|
|
254
|
+
: 0;
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
name: this.name,
|
|
258
|
+
status: this.status,
|
|
259
|
+
duration,
|
|
260
|
+
steps: {
|
|
261
|
+
total: this.steps.length,
|
|
262
|
+
success: this.steps.filter(s => s.status === 'success').length,
|
|
263
|
+
failed: this.steps.filter(s => s.status === 'failed').length,
|
|
264
|
+
pending: this.steps.filter(s => s.status === 'pending').length,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Verifica se a operação teve sucesso
|
|
271
|
+
*/
|
|
272
|
+
isSuccess(): boolean {
|
|
273
|
+
return this.status === 'success';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Verifica se a operação falhou
|
|
278
|
+
*/
|
|
279
|
+
isFailed(): boolean {
|
|
280
|
+
return this.status === 'failed';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Obtém trace ID da operação
|
|
285
|
+
*/
|
|
286
|
+
getTraceId(): string {
|
|
287
|
+
return this.traceId;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Cria uma nova operação (função helper)
|
|
293
|
+
*/
|
|
294
|
+
export function createOperation(name: string, logger?: Logger): OperationLogger {
|
|
295
|
+
return new OperationLogger(name, logger);
|
|
296
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressLogger - Logger especializado para operações com progresso
|
|
3
|
+
*
|
|
4
|
+
* Uso:
|
|
5
|
+
* const progress = new ProgressLogger('Download', { total: 100, unit: 'MB' });
|
|
6
|
+
* progress.update(50);
|
|
7
|
+
* progress.complete();
|
|
8
|
+
*
|
|
9
|
+
* Ou:
|
|
10
|
+
* const progress = new ProgressLogger('Processando', { total: files.length, unit: 'arquivos' });
|
|
11
|
+
* for (let i = 0; i < files.length; i++) {
|
|
12
|
+
* await process(files[i]);
|
|
13
|
+
* progress.increment();
|
|
14
|
+
* }
|
|
15
|
+
* progress.complete();
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Logger } from './Logger';
|
|
19
|
+
import { Colors } from './colors';
|
|
20
|
+
|
|
21
|
+
export interface ProgressOptions {
|
|
22
|
+
total: number;
|
|
23
|
+
unit?: string;
|
|
24
|
+
width?: number;
|
|
25
|
+
showPercentage?: boolean;
|
|
26
|
+
showEta?: boolean;
|
|
27
|
+
showCount?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ProgressLogger {
|
|
31
|
+
private name: string;
|
|
32
|
+
private options: Required<ProgressOptions>;
|
|
33
|
+
private current = 0;
|
|
34
|
+
private startTime: number;
|
|
35
|
+
private logger: Logger;
|
|
36
|
+
private lastUpdate = 0;
|
|
37
|
+
private updateInterval = 100; // ms - limita updates para não flickerar
|
|
38
|
+
|
|
39
|
+
constructor(name: string, options: ProgressOptions, logger?: Logger) {
|
|
40
|
+
this.name = name;
|
|
41
|
+
this.logger = logger || Logger.getInstance();
|
|
42
|
+
this.startTime = Date.now();
|
|
43
|
+
|
|
44
|
+
this.options = {
|
|
45
|
+
total: options.total,
|
|
46
|
+
unit: options.unit || '',
|
|
47
|
+
width: options.width || 30,
|
|
48
|
+
showPercentage: options.showPercentage !== false,
|
|
49
|
+
showEta: options.showEta !== false,
|
|
50
|
+
showCount: options.showCount !== false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Atualiza o progresso para um valor específico
|
|
56
|
+
*/
|
|
57
|
+
update(value: number): void {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
if (now - this.lastUpdate < this.updateInterval && value < this.options.total) {
|
|
60
|
+
return; // Rate limiting de updates visuais
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.current = Math.min(value, this.options.total);
|
|
64
|
+
this.lastUpdate = now;
|
|
65
|
+
this.render();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Incrementa o progresso
|
|
70
|
+
*/
|
|
71
|
+
increment(amount = 1): void {
|
|
72
|
+
this.update(this.current + amount);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Marca como completo
|
|
77
|
+
*/
|
|
78
|
+
complete(message?: string): void {
|
|
79
|
+
this.current = this.options.total;
|
|
80
|
+
this.render();
|
|
81
|
+
|
|
82
|
+
// Limpa a linha atual
|
|
83
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
84
|
+
|
|
85
|
+
const duration = ((Date.now() - this.startTime) / 1000).toFixed(1);
|
|
86
|
+
const finalMessage = message || `${this.name} concluído`;
|
|
87
|
+
|
|
88
|
+
this.logger.success(`${finalMessage} (${duration}s)`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Falha no progresso
|
|
93
|
+
*/
|
|
94
|
+
fail(message?: string): void {
|
|
95
|
+
// Limpa a linha atual
|
|
96
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
97
|
+
|
|
98
|
+
const finalMessage = message || `${this.name} falhou`;
|
|
99
|
+
this.logger.error(finalMessage);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Renderiza a barra de progresso
|
|
104
|
+
*/
|
|
105
|
+
private render(): void {
|
|
106
|
+
const { total, width, showPercentage, showEta, showCount, unit } = this.options;
|
|
107
|
+
const ratio = this.current / total;
|
|
108
|
+
const filled = Math.floor(ratio * width);
|
|
109
|
+
const empty = width - filled;
|
|
110
|
+
|
|
111
|
+
// Caracteres da barra
|
|
112
|
+
const barFilled = '█'.repeat(filled);
|
|
113
|
+
const barEmpty = '░'.repeat(empty);
|
|
114
|
+
|
|
115
|
+
const primaryColor = Colors.primary;
|
|
116
|
+
const dimColor = Colors.dim;
|
|
117
|
+
const whiteColor = Colors.white;
|
|
118
|
+
const reset = Colors.reset;
|
|
119
|
+
|
|
120
|
+
let line = '\r ';
|
|
121
|
+
|
|
122
|
+
// Nome da operação
|
|
123
|
+
line += `${primaryColor}▸${reset} ${dimColor}${this.name}${reset} `;
|
|
124
|
+
|
|
125
|
+
// Barra
|
|
126
|
+
line += `${primaryColor}[${barFilled}${barEmpty}]${reset}`;
|
|
127
|
+
|
|
128
|
+
// Porcentagem
|
|
129
|
+
if (showPercentage) {
|
|
130
|
+
const percentage = Math.floor(ratio * 100);
|
|
131
|
+
line += ` ${whiteColor}${percentage}%${reset}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Contagem
|
|
135
|
+
if (showCount) {
|
|
136
|
+
line += ` ${dimColor}(${this.current}/${total}${unit ? ' ' + unit : ''})${reset}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ETA
|
|
140
|
+
if (showEta && this.current > 0 && this.current < total) {
|
|
141
|
+
const elapsed = Date.now() - this.startTime;
|
|
142
|
+
const rate = this.current / elapsed;
|
|
143
|
+
const remaining = (total - this.current) / rate;
|
|
144
|
+
const eta = Math.ceil(remaining / 1000);
|
|
145
|
+
|
|
146
|
+
line += ` ${dimColor}(faltam ~${eta}s)${reset}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Limpa o resto da linha e escreve
|
|
150
|
+
process.stdout.write(line + '\x1b[K');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Obtém porcentagem atual
|
|
155
|
+
*/
|
|
156
|
+
getPercentage(): number {
|
|
157
|
+
return Math.floor((this.current / this.options.total) * 100);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Obtém valor atual
|
|
162
|
+
*/
|
|
163
|
+
getCurrent(): number {
|
|
164
|
+
return this.current;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Obtém total
|
|
169
|
+
*/
|
|
170
|
+
getTotal(): number {
|
|
171
|
+
return this.options.total;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Verifica se está completo
|
|
176
|
+
*/
|
|
177
|
+
isComplete(): boolean {
|
|
178
|
+
return this.current >= this.options.total;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Cria um progress logger (função helper)
|
|
184
|
+
*/
|
|
185
|
+
export function createProgress(name: string, options: ProgressOptions, logger?: Logger): ProgressLogger {
|
|
186
|
+
return new ProgressLogger(name, options, logger);
|
|
187
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TableLogger - Logger especializado para exibição de dados tabulares
|
|
3
|
+
*
|
|
4
|
+
* Uso:
|
|
5
|
+
* const table = new TableLogger(['Nome', 'Versão', 'Status']);
|
|
6
|
+
* table.add(['Spring', '5.3.0', '✓']);
|
|
7
|
+
* table.add(['Hibernate', '6.0.0', '⚠']);
|
|
8
|
+
* table.print();
|
|
9
|
+
*
|
|
10
|
+
* Ou com opções de alinhamento:
|
|
11
|
+
* const table = new TableLogger([
|
|
12
|
+
* { header: 'ID', align: 'center', width: 10 },
|
|
13
|
+
* { header: 'Nome', align: 'left' },
|
|
14
|
+
* { header: 'Valor', align: 'right', width: 15 }
|
|
15
|
+
* ]);
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { TableColumn, TableRow } from './types';
|
|
19
|
+
import { Logger } from './Logger';
|
|
20
|
+
import { Colors, stripAnsi, visualWidth, padText } from './colors';
|
|
21
|
+
|
|
22
|
+
export interface TableOptions {
|
|
23
|
+
headerStyle?: boolean;
|
|
24
|
+
border?: boolean;
|
|
25
|
+
compact?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class TableLogger {
|
|
29
|
+
private columns: TableColumn[];
|
|
30
|
+
private rows: TableRow[] = [];
|
|
31
|
+
private options: Required<TableOptions>;
|
|
32
|
+
private logger: Logger;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
columns: (string | TableColumn)[],
|
|
36
|
+
options: TableOptions = {},
|
|
37
|
+
logger?: Logger
|
|
38
|
+
) {
|
|
39
|
+
this.logger = logger || Logger.getInstance();
|
|
40
|
+
|
|
41
|
+
// Normaliza colunas
|
|
42
|
+
this.columns = columns.map(col => {
|
|
43
|
+
if (typeof col === 'string') {
|
|
44
|
+
return { header: col, align: 'left' };
|
|
45
|
+
}
|
|
46
|
+
return { align: 'left', ...col };
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.options = {
|
|
50
|
+
headerStyle: true,
|
|
51
|
+
border: false,
|
|
52
|
+
compact: false,
|
|
53
|
+
...options,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Adiciona uma linha à tabela
|
|
59
|
+
*/
|
|
60
|
+
add(cells: (string | number)[], styles?: string[]): this {
|
|
61
|
+
this.rows.push({
|
|
62
|
+
cells: cells.map(c => String(c)),
|
|
63
|
+
styles,
|
|
64
|
+
});
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Adiciona múltiplas linhas
|
|
70
|
+
*/
|
|
71
|
+
addMany(rows: (string | number)[][]): this {
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
this.add(row);
|
|
74
|
+
}
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Limpa todas as linhas
|
|
80
|
+
*/
|
|
81
|
+
clear(): this {
|
|
82
|
+
this.rows = [];
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Define larguras das colunas baseado no conteúdo
|
|
88
|
+
*/
|
|
89
|
+
private calculateWidths(): number[] {
|
|
90
|
+
const widths: number[] = [];
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < this.columns.length; i++) {
|
|
93
|
+
const col = this.columns[i];
|
|
94
|
+
|
|
95
|
+
// Se já tem largura definida, usa ela
|
|
96
|
+
if (col.width) {
|
|
97
|
+
widths[i] = col.width;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Calcula largura máxima baseada no header e conteúdo
|
|
102
|
+
let maxWidth = visualWidth(col.header);
|
|
103
|
+
|
|
104
|
+
for (const row of this.rows) {
|
|
105
|
+
const cell = row.cells[i] || '';
|
|
106
|
+
maxWidth = Math.max(maxWidth, visualWidth(cell));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Adiciona padding
|
|
110
|
+
widths[i] = maxWidth + 2;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return widths;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Renderiza a tabela
|
|
118
|
+
*/
|
|
119
|
+
render(): string {
|
|
120
|
+
if (this.rows.length === 0 && this.options.compact) {
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const widths = this.calculateWidths();
|
|
125
|
+
const lines: string[] = [];
|
|
126
|
+
|
|
127
|
+
// Linha de borda superior
|
|
128
|
+
if (this.options.border) {
|
|
129
|
+
let border = ' ┌';
|
|
130
|
+
for (let i = 0; i < widths.length; i++) {
|
|
131
|
+
border += '─'.repeat(widths[i] + 2);
|
|
132
|
+
if (i < widths.length - 1) border += '┬';
|
|
133
|
+
}
|
|
134
|
+
border += '┐';
|
|
135
|
+
lines.push(Colors.darkGray + border + Colors.reset);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Header
|
|
139
|
+
const headerCells = this.columns.map((col, i) => {
|
|
140
|
+
const width = widths[i];
|
|
141
|
+
const padded = padText(col.header, width, col.align);
|
|
142
|
+
return this.options.headerStyle
|
|
143
|
+
? `${Colors.bold}${padded}${Colors.reset}`
|
|
144
|
+
: padded;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const headerLine = this.options.border
|
|
148
|
+
? ` │ ${headerCells.join(' │ ')} │`
|
|
149
|
+
: ' ' + headerCells.join('');
|
|
150
|
+
lines.push(headerLine);
|
|
151
|
+
|
|
152
|
+
// Linha separadora
|
|
153
|
+
if (this.options.border) {
|
|
154
|
+
let sep = ' ├';
|
|
155
|
+
for (let i = 0; i < widths.length; i++) {
|
|
156
|
+
sep += '─'.repeat(widths[i] + 2);
|
|
157
|
+
if (i < widths.length - 1) sep += '┼';
|
|
158
|
+
}
|
|
159
|
+
sep += '┤';
|
|
160
|
+
lines.push(Colors.darkGray + sep + Colors.reset);
|
|
161
|
+
} else if (!this.options.compact) {
|
|
162
|
+
let sep = ' ';
|
|
163
|
+
for (let i = 0; i < widths.length; i++) {
|
|
164
|
+
sep += '─'.repeat(widths[i]);
|
|
165
|
+
}
|
|
166
|
+
lines.push(Colors.darkGray + sep + Colors.reset);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Linhas de dados
|
|
170
|
+
for (const row of this.rows) {
|
|
171
|
+
const cells = row.cells.map((cell, i) => {
|
|
172
|
+
const width = widths[i];
|
|
173
|
+
const col = this.columns[i];
|
|
174
|
+
const style = row.styles?.[i] || '';
|
|
175
|
+
const reset = style ? Colors.reset : '';
|
|
176
|
+
return style + padText(cell, width, col.align) + reset;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const line = this.options.border
|
|
180
|
+
? ` │ ${cells.join(' │ ')} │`
|
|
181
|
+
: ' ' + cells.join('');
|
|
182
|
+
lines.push(line);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Linha de borda inferior
|
|
186
|
+
if (this.options.border) {
|
|
187
|
+
let border = ' └';
|
|
188
|
+
for (let i = 0; i < widths.length; i++) {
|
|
189
|
+
border += '─'.repeat(widths[i] + 2);
|
|
190
|
+
if (i < widths.length - 1) border += '┴';
|
|
191
|
+
}
|
|
192
|
+
border += '┘';
|
|
193
|
+
lines.push(Colors.darkGray + border + Colors.reset);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return lines.join('\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Imprime a tabela
|
|
201
|
+
*/
|
|
202
|
+
print(): void {
|
|
203
|
+
const output = this.render();
|
|
204
|
+
if (output) {
|
|
205
|
+
this.logger.info(output);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Obtém número de linhas
|
|
211
|
+
*/
|
|
212
|
+
getRowCount(): number {
|
|
213
|
+
return this.rows.length;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Obtém número de colunas
|
|
218
|
+
*/
|
|
219
|
+
getColumnCount(): number {
|
|
220
|
+
return this.columns.length;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Cria uma tabela (função helper)
|
|
226
|
+
*/
|
|
227
|
+
export function createTable(
|
|
228
|
+
columns: (string | TableColumn)[],
|
|
229
|
+
options?: TableOptions,
|
|
230
|
+
logger?: Logger
|
|
231
|
+
): TableLogger {
|
|
232
|
+
return new TableLogger(columns, options, logger);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Imprime uma tabela simples rapidamente
|
|
237
|
+
*/
|
|
238
|
+
export function printTable(
|
|
239
|
+
headers: string[],
|
|
240
|
+
rows: (string | number)[][],
|
|
241
|
+
logger?: Logger
|
|
242
|
+
): void {
|
|
243
|
+
const table = new TableLogger(headers, {}, logger);
|
|
244
|
+
table.addMany(rows);
|
|
245
|
+
table.print();
|
|
246
|
+
}
|