@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.
- 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 +33 -31
- package/src/commands/DeployCommand.ts +252 -229
- package/src/commands/DepsCommand.ts +174 -174
- package/src/commands/DockerCommand.ts +14 -14
- package/src/commands/DoctorCommand.ts +252 -239
- package/src/commands/EncodingCommand.ts +19 -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 +6 -6
- 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 +4 -4
- package/src/commands/TomcatCommand.ts +219 -100
- 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 +11 -3
- 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,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serviço de download avançado do Tomcat
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Retry automático com backoff exponencial
|
|
6
|
+
* - Resume de download (HTTP Range)
|
|
7
|
+
* - Progresso detalhado (MB/s, ETA)
|
|
8
|
+
* - Suporte a proxy
|
|
9
|
+
* - Modo headless/silencioso
|
|
10
|
+
*/
|
|
11
|
+
import { Logger } from "../../logging";
|
|
12
|
+
import { ProgressBar, ThemedSpinner } from "../../utils/ProgressBar";
|
|
13
|
+
import { existsSync, createWriteStream, promises as fsPromises } from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import type { TomcatProxyConfig } from "./types";
|
|
16
|
+
|
|
17
|
+
export interface DownloadOptions {
|
|
18
|
+
url: string;
|
|
19
|
+
destPath: string;
|
|
20
|
+
/** Tamanho total esperado (para validação) */
|
|
21
|
+
expectedSize?: number;
|
|
22
|
+
/** Checksum esperado (SHA512) */
|
|
23
|
+
expectedChecksum?: string;
|
|
24
|
+
/** Retry em caso de falha */
|
|
25
|
+
retries?: number;
|
|
26
|
+
/** Timeout em ms */
|
|
27
|
+
timeout?: number;
|
|
28
|
+
/** Configuração de proxy */
|
|
29
|
+
proxy?: TomcatProxyConfig;
|
|
30
|
+
/** Continuar download parcial */
|
|
31
|
+
resume?: boolean;
|
|
32
|
+
/** Modo silencioso (para CI/CD) */
|
|
33
|
+
silent?: boolean;
|
|
34
|
+
/** Título para progresso */
|
|
35
|
+
title?: string;
|
|
36
|
+
/** Callback de progresso */
|
|
37
|
+
onProgress?: (downloaded: number, total: number, speed: number, eta: number) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DownloadResult {
|
|
41
|
+
success: boolean;
|
|
42
|
+
destPath: string;
|
|
43
|
+
size: number;
|
|
44
|
+
duration: number;
|
|
45
|
+
speed: number; // MB/s
|
|
46
|
+
resumed: boolean;
|
|
47
|
+
attempts: number;
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class TomcatDownloadService {
|
|
52
|
+
private logger = Logger.getInstance();
|
|
53
|
+
private abortController?: AbortController;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Download com retry automático e resume
|
|
57
|
+
*/
|
|
58
|
+
async download(options: DownloadOptions): Promise<DownloadResult> {
|
|
59
|
+
const {
|
|
60
|
+
url,
|
|
61
|
+
destPath,
|
|
62
|
+
retries = 3,
|
|
63
|
+
timeout = 300000, // 5 minutos
|
|
64
|
+
resume = true,
|
|
65
|
+
silent = false,
|
|
66
|
+
title = "Download",
|
|
67
|
+
proxy,
|
|
68
|
+
onProgress
|
|
69
|
+
} = options;
|
|
70
|
+
|
|
71
|
+
const startTime = Date.now();
|
|
72
|
+
let lastError: Error | undefined;
|
|
73
|
+
let attempts = 0;
|
|
74
|
+
let resumed = false;
|
|
75
|
+
|
|
76
|
+
// Verifica se pode resumir
|
|
77
|
+
let startByte = 0;
|
|
78
|
+
if (resume && existsSync(destPath)) {
|
|
79
|
+
const stats = await fsPromises.stat(destPath).catch(() => null);
|
|
80
|
+
if (stats && stats.size > 0) {
|
|
81
|
+
startByte = stats.size;
|
|
82
|
+
resumed = true;
|
|
83
|
+
if (!silent) {
|
|
84
|
+
this.logger.info(`Continuando download de ${this.formatBytes(startByte)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
90
|
+
attempts = attempt;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
if (!silent && attempt > 1) {
|
|
94
|
+
this.logger.warn(`Tentativa ${attempt}/${retries}...`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result = await this.doDownload({
|
|
98
|
+
url,
|
|
99
|
+
destPath,
|
|
100
|
+
startByte,
|
|
101
|
+
timeout,
|
|
102
|
+
proxy,
|
|
103
|
+
silent,
|
|
104
|
+
title,
|
|
105
|
+
onProgress
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
109
|
+
const sizeMB = result.size / 1024 / 1024;
|
|
110
|
+
const speed = duration > 0 ? sizeMB / duration : 0;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
destPath,
|
|
115
|
+
size: result.size,
|
|
116
|
+
duration,
|
|
117
|
+
speed,
|
|
118
|
+
resumed,
|
|
119
|
+
attempts
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
} catch (error) {
|
|
123
|
+
lastError = error as Error;
|
|
124
|
+
|
|
125
|
+
if (!silent) {
|
|
126
|
+
this.logger.error(`Falha na tentativa ${attempt}: ${lastError.message}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (attempt < retries) {
|
|
130
|
+
// Backoff exponencial: 1s, 2s, 4s
|
|
131
|
+
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
|
|
132
|
+
if (!silent) {
|
|
133
|
+
this.logger.info(`Aguardando ${delay / 1000}s antes de retry...`);
|
|
134
|
+
}
|
|
135
|
+
await this.sleep(delay);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Todas as tentativas falharam
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
destPath,
|
|
144
|
+
size: 0,
|
|
145
|
+
duration: (Date.now() - startTime) / 1000,
|
|
146
|
+
speed: 0,
|
|
147
|
+
resumed,
|
|
148
|
+
attempts,
|
|
149
|
+
error: lastError?.message || "Download falhou após todas as tentativas"
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Download único com suporte a resume
|
|
155
|
+
*/
|
|
156
|
+
private async doDownload(params: {
|
|
157
|
+
url: string;
|
|
158
|
+
destPath: string;
|
|
159
|
+
startByte: number;
|
|
160
|
+
timeout: number;
|
|
161
|
+
proxy?: TomcatProxyConfig;
|
|
162
|
+
silent: boolean;
|
|
163
|
+
title: string;
|
|
164
|
+
onProgress?: (downloaded: number, total: number, speed: number, eta: number) => void;
|
|
165
|
+
}): Promise<{ size: number }> {
|
|
166
|
+
const { url, destPath, startByte, timeout, proxy, silent, title, onProgress } = params;
|
|
167
|
+
|
|
168
|
+
// Prepara headers
|
|
169
|
+
const headers: Record<string, string> = {};
|
|
170
|
+
if (startByte > 0) {
|
|
171
|
+
headers["Range"] = `bytes=${startByte}-`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Configura proxy se necessário
|
|
175
|
+
let fetchUrl = url;
|
|
176
|
+
const fetchOptions: RequestInit = {
|
|
177
|
+
headers,
|
|
178
|
+
signal: this.createTimeoutSignal(timeout)
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Para proxy, usamos variáveis de ambiente ou config explícita
|
|
182
|
+
if (proxy?.http || proxy?.https) {
|
|
183
|
+
// Bun respeita HTTP_PROXY/HTTPS_PROXY automaticamente
|
|
184
|
+
if (proxy.http) {
|
|
185
|
+
process.env.HTTP_PROXY = proxy.http;
|
|
186
|
+
}
|
|
187
|
+
if (proxy.https) {
|
|
188
|
+
process.env.HTTPS_PROXY = proxy.https;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const response = await fetch(fetchUrl, fetchOptions);
|
|
193
|
+
|
|
194
|
+
if (!response.ok && response.status !== 206) { // 206 = Partial Content
|
|
195
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const contentLength = response.headers.get("content-length");
|
|
199
|
+
const totalSize = contentLength ? parseInt(contentLength) + startByte : startByte;
|
|
200
|
+
const isResumed = response.status === 206;
|
|
201
|
+
|
|
202
|
+
// Determina modo de escrita
|
|
203
|
+
const flags = isResumed || startByte > 0 ? "a" : "w";
|
|
204
|
+
const fileStream = createWriteStream(destPath, { flags });
|
|
205
|
+
|
|
206
|
+
const reader = response.body?.getReader();
|
|
207
|
+
if (!reader) {
|
|
208
|
+
throw new Error("Response body não disponível");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Progress tracking
|
|
212
|
+
let downloaded = startByte;
|
|
213
|
+
const startTime = Date.now();
|
|
214
|
+
let lastProgressTime = startTime;
|
|
215
|
+
let lastDownloaded = startByte;
|
|
216
|
+
|
|
217
|
+
// Progress bar ou spinner
|
|
218
|
+
let progressBar: ProgressBar | null = null;
|
|
219
|
+
let spinnerStop: ((success: boolean) => void) | null = null;
|
|
220
|
+
|
|
221
|
+
if (!silent) {
|
|
222
|
+
if (totalSize > 0) {
|
|
223
|
+
progressBar = new ProgressBar({
|
|
224
|
+
title,
|
|
225
|
+
total: totalSize,
|
|
226
|
+
width: 30,
|
|
227
|
+
showSpeed: true,
|
|
228
|
+
showEta: true
|
|
229
|
+
});
|
|
230
|
+
} else {
|
|
231
|
+
const spinner = new ThemedSpinner();
|
|
232
|
+
spinnerStop = spinner.start(title, "dots", "download");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
while (true) {
|
|
238
|
+
const { done, value } = await reader.read();
|
|
239
|
+
if (done) break;
|
|
240
|
+
|
|
241
|
+
// Escreve chunk
|
|
242
|
+
fileStream.write(Buffer.from(value));
|
|
243
|
+
downloaded += value.length;
|
|
244
|
+
|
|
245
|
+
// Calcula velocidade e ETA
|
|
246
|
+
const now = Date.now();
|
|
247
|
+
const elapsed = (now - startTime) / 1000;
|
|
248
|
+
const speed = elapsed > 0 ? (downloaded / 1024 / 1024) / elapsed : 0;
|
|
249
|
+
|
|
250
|
+
// ETA
|
|
251
|
+
let eta = 0;
|
|
252
|
+
if (totalSize > 0 && speed > 0) {
|
|
253
|
+
const remaining = (totalSize - downloaded) / 1024 / 1024;
|
|
254
|
+
eta = remaining / speed;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Atualiza progresso a cada 200ms ou quando completo
|
|
258
|
+
if (now - lastProgressTime > 200 || done) {
|
|
259
|
+
if (progressBar) {
|
|
260
|
+
progressBar.update(downloaded, speed, eta);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (onProgress) {
|
|
264
|
+
onProgress(downloaded, totalSize, speed, eta);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
lastProgressTime = now;
|
|
268
|
+
lastDownloaded = downloaded;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fileStream.end();
|
|
273
|
+
|
|
274
|
+
// Finaliza progresso
|
|
275
|
+
if (progressBar) {
|
|
276
|
+
progressBar.complete();
|
|
277
|
+
} else if (spinnerStop) {
|
|
278
|
+
spinnerStop(true);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Verifica tamanho final
|
|
282
|
+
const stats = await fsPromises.stat(destPath);
|
|
283
|
+
|
|
284
|
+
return { size: stats.size };
|
|
285
|
+
|
|
286
|
+
} catch (error) {
|
|
287
|
+
fileStream.destroy();
|
|
288
|
+
if (progressBar) {
|
|
289
|
+
// não precisa fazer nada
|
|
290
|
+
} else if (spinnerStop) {
|
|
291
|
+
spinnerStop(false);
|
|
292
|
+
}
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Aborta download em andamento
|
|
299
|
+
*/
|
|
300
|
+
abort(): void {
|
|
301
|
+
this.abortController?.abort();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Cria signal com timeout
|
|
306
|
+
*/
|
|
307
|
+
private createTimeoutSignal(ms: number): AbortSignal {
|
|
308
|
+
this.abortController = new AbortController();
|
|
309
|
+
const timeoutId = setTimeout(() => this.abortController?.abort(), ms);
|
|
310
|
+
|
|
311
|
+
// Limpa timeout se signal for usado antes
|
|
312
|
+
const signal = this.abortController.signal;
|
|
313
|
+
signal.addEventListener("abort", () => clearTimeout(timeoutId));
|
|
314
|
+
|
|
315
|
+
return signal;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Formata bytes para string legível
|
|
320
|
+
*/
|
|
321
|
+
private formatBytes(bytes: number): string {
|
|
322
|
+
if (bytes === 0) return "0 B";
|
|
323
|
+
const k = 1024;
|
|
324
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
325
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
326
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Sleep utilitário
|
|
331
|
+
*/
|
|
332
|
+
private sleep(ms: number): Promise<void> {
|
|
333
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
334
|
+
}
|
|
335
|
+
}
|