@archznn/xavva 2.6.0 → 2.8.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 +55 -0
- package/package.json +4 -2
- package/src/commands/CompletionCommand.ts +212 -0
- package/src/commands/ConfigCommand.ts +184 -0
- package/src/commands/DeployCommand.ts +2 -1
- package/src/commands/EncodingCommand.ts +351 -0
- package/src/commands/HealthCommand.ts +302 -0
- package/src/commands/HelpCommand.ts +42 -0
- package/src/commands/HistoryCommand.ts +49 -0
- package/src/commands/InitCommand.ts +148 -0
- package/src/commands/RedoCommand.ts +36 -0
- package/src/config/versions.ts +63 -0
- package/src/di/container.ts +249 -0
- package/src/errors/ErrorHandler.ts +249 -0
- package/src/errors/XavvaError.ts +273 -0
- package/src/index.ts +136 -96
- package/src/services/AuditService.ts +3 -2
- package/src/services/BrowserService.ts +127 -16
- package/src/services/DeployWatcher.ts +183 -0
- package/src/services/EmbeddedTomcatService.ts +67 -37
- package/src/services/EncodingService.ts +548 -0
- package/src/services/FileWatcher.ts +243 -0
- package/src/services/HistoryService.ts +73 -0
- package/src/services/NotificationService.ts +145 -0
- package/src/services/TomcatService.ts +59 -26
- package/src/types/args.ts +151 -0
- package/src/types/config.ts +6 -0
- package/src/types/index.ts +7 -0
- package/src/utils/PathUtils.ts +221 -0
- package/src/utils/ProgressBar.ts +182 -0
- package/src/utils/config.ts +6 -0
- package/src/utils/parsers/JavaParser.ts +413 -0
- package/src/utils/platform.ts +2 -2
- package/src/services/WatcherService.ts +0 -117
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import type { Command } from "./Command";
|
|
2
|
+
import type { AppConfig, CLIArguments } from "../types/config";
|
|
3
|
+
import { EncodingService } from "../services/EncodingService";
|
|
4
|
+
import { Logger } from "../utils/ui";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { ProcessManager } from "../utils/processManager";
|
|
8
|
+
|
|
9
|
+
export class EncodingCommand implements Command {
|
|
10
|
+
private encodingService: EncodingService;
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
this.encodingService = new EncodingService();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async execute(config: AppConfig, args?: CLIArguments, positionals?: string[]): Promise<void> {
|
|
17
|
+
const processManager = ProcessManager.getInstance();
|
|
18
|
+
|
|
19
|
+
// Parse subcomando
|
|
20
|
+
const subcommand = positionals?.[1] || "help";
|
|
21
|
+
const fileArg = positionals?.[2]; // Arquivo específico (opcional)
|
|
22
|
+
|
|
23
|
+
switch (subcommand) {
|
|
24
|
+
case "detect":
|
|
25
|
+
await this.handleDetect(fileArg);
|
|
26
|
+
break;
|
|
27
|
+
case "convert":
|
|
28
|
+
await this.handleConvert(config, args, fileArg);
|
|
29
|
+
break;
|
|
30
|
+
case "fix":
|
|
31
|
+
await this.handleFix(fileArg, args);
|
|
32
|
+
break;
|
|
33
|
+
case "list":
|
|
34
|
+
await this.handleList(config);
|
|
35
|
+
break;
|
|
36
|
+
case "help":
|
|
37
|
+
default:
|
|
38
|
+
this.showHelp();
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await processManager.shutdown(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private showHelp() {
|
|
46
|
+
Logger.section("Encoding Command");
|
|
47
|
+
Logger.log("Gerencia conversão de encoding de arquivos de texto");
|
|
48
|
+
Logger.newline();
|
|
49
|
+
|
|
50
|
+
Logger.log(`${Logger.C.primary}Uso:${Logger.C.reset}`);
|
|
51
|
+
Logger.log(" xavva encoding <subcomando> [arquivo] [opções]");
|
|
52
|
+
Logger.newline();
|
|
53
|
+
|
|
54
|
+
Logger.log(`${Logger.C.primary}Subcomandos:${Logger.C.reset}`);
|
|
55
|
+
Logger.log(` ${Logger.C.secondary}detect${Logger.C.reset} [arquivo] Detecta encoding de um arquivo ou diretório`);
|
|
56
|
+
Logger.log(` ${Logger.C.secondary}convert${Logger.C.reset} [arquivo] Converte arquivo(s) para outro encoding`);
|
|
57
|
+
Logger.log(` ${Logger.C.secondary}fix${Logger.C.reset} [arquivo] Tenta corrigir mojibake automaticamente`);
|
|
58
|
+
Logger.log(` ${Logger.C.secondary}list${Logger.C.reset} Lista encodings de todos os arquivos do projeto`);
|
|
59
|
+
Logger.log(` ${Logger.C.secondary}help${Logger.C.reset} Mostra esta ajuda`);
|
|
60
|
+
Logger.newline();
|
|
61
|
+
|
|
62
|
+
Logger.log(`${Logger.C.primary}Opções:${Logger.C.reset}`);
|
|
63
|
+
Logger.log(` --from <encoding> Encoding de origem (padrão: auto-detect)`);
|
|
64
|
+
Logger.log(` --to <encoding> Encoding de destino (padrão: do xavva.json ou UTF-8)`);
|
|
65
|
+
Logger.log(` --backup Cria backup antes de converter`);
|
|
66
|
+
Logger.log(` --dry-run Simula sem modificar arquivos`);
|
|
67
|
+
Logger.log(` --force Força correção mesmo se detectado como UTF-8`);
|
|
68
|
+
Logger.log(` --src <path> Diretório fonte (padrão: src/)`);
|
|
69
|
+
Logger.newline();
|
|
70
|
+
|
|
71
|
+
Logger.log(`${Logger.C.primary}Encodings suportados:${Logger.C.reset}`);
|
|
72
|
+
Logger.log(` utf-8, utf8 UTF-8 (padrão)`);
|
|
73
|
+
Logger.log(` windows-1252, cp1252 Windows CP1252 (ANSI)`);
|
|
74
|
+
Logger.log(` iso-8859-1, latin1 ISO-8859-1 (Latin-1)`);
|
|
75
|
+
Logger.newline();
|
|
76
|
+
|
|
77
|
+
Logger.log(`${Logger.C.primary}Exemplos:${Logger.C.reset}`);
|
|
78
|
+
Logger.log(` xavva encoding detect src/main/java/MinhaClasse.java`);
|
|
79
|
+
Logger.log(` xavva encoding convert --from utf-8 --to cp1252 src/main/java/`);
|
|
80
|
+
Logger.log(` xavva encoding convert --to cp1252 --backup src/main/java/MinhaClasse.java`);
|
|
81
|
+
Logger.log(` xavva encoding fix src/main/java/MinhaClasse.java`);
|
|
82
|
+
Logger.log(` xavva encoding fix --force src/main/java/MinhaClasse.java # Força correção`);
|
|
83
|
+
Logger.log(` xavva encoding list`);
|
|
84
|
+
Logger.endSection();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async handleDetect(fileArg?: string) {
|
|
88
|
+
const target = fileArg || path.join(process.cwd(), "src");
|
|
89
|
+
|
|
90
|
+
if (!existsSync(target)) {
|
|
91
|
+
Logger.error(`Caminho não encontrado: ${target}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Logger.section("Detecção de Encoding");
|
|
96
|
+
Logger.info("Alvo", target);
|
|
97
|
+
Logger.newline();
|
|
98
|
+
|
|
99
|
+
const stat = await Bun.file(target).stat();
|
|
100
|
+
|
|
101
|
+
if (stat.isFile()) {
|
|
102
|
+
// Detectar arquivo único
|
|
103
|
+
const detection = await this.encodingService.detectFileEncoding(target);
|
|
104
|
+
if (detection) {
|
|
105
|
+
Logger.info("Arquivo", path.basename(target));
|
|
106
|
+
Logger.config("Encoding detectado", detection.encoding);
|
|
107
|
+
Logger.config("Confiança", `${Math.round(detection.confidence * 100)}%`);
|
|
108
|
+
Logger.config("Tem BOM", detection.hasBOM ? "Sim" : "Não");
|
|
109
|
+
} else {
|
|
110
|
+
Logger.error("Não foi possível detectar o encoding");
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Detectar diretório
|
|
114
|
+
const detections = await this.encodingService.detectDirectoryEncodings(target);
|
|
115
|
+
|
|
116
|
+
if (detections.size === 0) {
|
|
117
|
+
Logger.warn("Nenhum arquivo de texto encontrado");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Agrupa por encoding
|
|
122
|
+
const byEncoding = new Map<string, number>();
|
|
123
|
+
for (const [file, detection] of detections) {
|
|
124
|
+
const count = byEncoding.get(detection.encoding) || 0;
|
|
125
|
+
byEncoding.set(detection.encoding, count + 1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Logger.info("Arquivos analisados", String(detections.size));
|
|
129
|
+
Logger.newline();
|
|
130
|
+
|
|
131
|
+
Logger.log(`${Logger.C.primary}Distribuição por encoding:${Logger.C.reset}`);
|
|
132
|
+
for (const [encoding, count] of byEncoding) {
|
|
133
|
+
Logger.config(encoding, `${count} arquivo(s)`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
Logger.newline();
|
|
137
|
+
Logger.log(`${Logger.C.primary}Arquivos com baixa confiança:${Logger.C.reset}`);
|
|
138
|
+
let lowConfidenceFound = false;
|
|
139
|
+
for (const [file, detection] of detections) {
|
|
140
|
+
if (detection.confidence < 0.8) {
|
|
141
|
+
Logger.warn(`${path.relative(target, file)} (${Math.round(detection.confidence * 100)}%)`);
|
|
142
|
+
lowConfidenceFound = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!lowConfidenceFound) {
|
|
146
|
+
Logger.success("Todos os arquivos têm confiança alta na detecção");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
Logger.endSection();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async handleConvert(config: AppConfig, args?: CLIArguments, fileArg?: string) {
|
|
154
|
+
const fromEncoding = args?.["from"] as string || "auto";
|
|
155
|
+
const toEncoding = args?.["to"] as string || config.project.encoding || "utf-8";
|
|
156
|
+
const backup = args?.["backup"] as boolean || false;
|
|
157
|
+
const dryRun = args?.["dry-run"] as boolean || false;
|
|
158
|
+
const srcDir = args?.["src"] as string || path.join(process.cwd(), "src");
|
|
159
|
+
|
|
160
|
+
const target = fileArg || srcDir;
|
|
161
|
+
|
|
162
|
+
if (!existsSync(target)) {
|
|
163
|
+
Logger.error(`Caminho não encontrado: ${target}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Valida encoding de destino
|
|
168
|
+
if (!this.encodingService.isValidEncoding(toEncoding)) {
|
|
169
|
+
Logger.error(`Encoding não suportado: "${toEncoding}"`);
|
|
170
|
+
Logger.info("Encodings suportados", this.encodingService.getSupportedEncodings().join(", "));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Valida encoding de origem (se não for auto)
|
|
175
|
+
if (fromEncoding !== "auto" && !this.encodingService.isValidEncoding(fromEncoding)) {
|
|
176
|
+
Logger.error(`Encoding não suportado: "${fromEncoding}"`);
|
|
177
|
+
Logger.info("Encodings suportados", this.encodingService.getSupportedEncodings().join(", "));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
Logger.section("Conversão de Encoding");
|
|
182
|
+
Logger.info("De", fromEncoding === "auto" ? "auto-detect" : fromEncoding);
|
|
183
|
+
Logger.info("Para", toEncoding);
|
|
184
|
+
Logger.info("Alvo", target);
|
|
185
|
+
if (backup) Logger.config("Backup", "Sim");
|
|
186
|
+
if (dryRun) Logger.config("Modo", "DRY RUN (simulação)");
|
|
187
|
+
Logger.newline();
|
|
188
|
+
|
|
189
|
+
const stat = await Bun.file(target).stat();
|
|
190
|
+
|
|
191
|
+
if (stat.isFile()) {
|
|
192
|
+
// Converter arquivo único
|
|
193
|
+
let actualFrom = fromEncoding;
|
|
194
|
+
|
|
195
|
+
if (fromEncoding === "auto") {
|
|
196
|
+
const detection = await this.encodingService.detectFileEncoding(target);
|
|
197
|
+
if (detection) {
|
|
198
|
+
actualFrom = detection.encoding;
|
|
199
|
+
Logger.info("Detectado", `${actualFrom} (${Math.round(detection.confidence * 100)}%)`);
|
|
200
|
+
} else {
|
|
201
|
+
Logger.error("Não foi possível detectar encoding. Use --from para especificar.");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const result = await this.encodingService.convertFile(target, actualFrom, toEncoding, {
|
|
207
|
+
backup,
|
|
208
|
+
dryRun
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (result.success) {
|
|
212
|
+
Logger.success(result.message);
|
|
213
|
+
if (result.unsupportedChars && result.unsupportedChars.length > 0) {
|
|
214
|
+
const unique = [...new Set(result.unsupportedChars)].slice(0, 5);
|
|
215
|
+
const codes = unique.map(c => `U+${c.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')}`);
|
|
216
|
+
Logger.warn(`${result.unsupportedChars.length} caractere(s) não suportado(s): ${codes.join(", ")}`);
|
|
217
|
+
Logger.info("Dica", "Caracteres não suportados foram substituídos por '?'");
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
Logger.error(result.message);
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
// Converter diretório
|
|
224
|
+
let actualFrom = fromEncoding;
|
|
225
|
+
|
|
226
|
+
if (fromEncoding === "auto") {
|
|
227
|
+
Logger.warn("Modo auto-detect em diretórios assume UTF-8 ou detecta individualmente");
|
|
228
|
+
actualFrom = "utf-8"; // Default para diretórios em auto
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const result = await this.encodingService.convertDirectory(target, actualFrom, toEncoding, {
|
|
232
|
+
backup,
|
|
233
|
+
dryRun
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
Logger.newline();
|
|
237
|
+
Logger.info("Total", String(result.total));
|
|
238
|
+
Logger.success(`Sucesso: ${result.success}`);
|
|
239
|
+
if (result.failed > 0) {
|
|
240
|
+
Logger.warn(`Falhas: ${result.failed}`);
|
|
241
|
+
}
|
|
242
|
+
if (result.totalUnsupported > 0) {
|
|
243
|
+
Logger.warn(`${result.totalUnsupported} caractere(s) substituído(s) por "?"`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
Logger.endSection();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private async handleFix(fileArg?: string, args?: CLIArguments) {
|
|
251
|
+
const backup = args?.["backup"] as boolean || false;
|
|
252
|
+
const dryRun = args?.["dry-run"] as boolean || false;
|
|
253
|
+
const force = args?.["force"] as boolean || false;
|
|
254
|
+
const target = fileArg || path.join(process.cwd(), "src");
|
|
255
|
+
|
|
256
|
+
if (!existsSync(target)) {
|
|
257
|
+
Logger.error(`Caminho não encontrado: ${target}`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
Logger.section("Correção de Mojibake");
|
|
262
|
+
Logger.info("Alvo", target);
|
|
263
|
+
if (backup) Logger.config("Backup", "Sim");
|
|
264
|
+
if (dryRun) Logger.config("Modo", "DRY RUN (simulação)");
|
|
265
|
+
Logger.newline();
|
|
266
|
+
|
|
267
|
+
const stat = await Bun.file(target).stat();
|
|
268
|
+
|
|
269
|
+
if (stat.isFile()) {
|
|
270
|
+
const result = await this.encodingService.fixMojibake(target, { backup, dryRun, force });
|
|
271
|
+
|
|
272
|
+
if (result.success) {
|
|
273
|
+
Logger.success(result.message);
|
|
274
|
+
} else {
|
|
275
|
+
Logger.warn(result.message);
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
// Fix em diretório
|
|
279
|
+
const files = await this.encodingService.findTextFiles(target);
|
|
280
|
+
let fixed = 0;
|
|
281
|
+
let skipped = 0;
|
|
282
|
+
let failed = 0;
|
|
283
|
+
|
|
284
|
+
Logger.info("Arquivos encontrados", String(files.length));
|
|
285
|
+
Logger.newline();
|
|
286
|
+
|
|
287
|
+
for (const file of files) {
|
|
288
|
+
const result = await this.encodingService.fixMojibake(file, { backup, dryRun, force });
|
|
289
|
+
const relativePath = path.relative(target, file);
|
|
290
|
+
|
|
291
|
+
if (result.success && result.detectedFrom !== "utf-8") {
|
|
292
|
+
Logger.success(`${relativePath}: ${result.message}`);
|
|
293
|
+
fixed++;
|
|
294
|
+
} else if (result.detectedFrom === "utf-8") {
|
|
295
|
+
// Já está em UTF-8, não mostra nada em modo silencioso
|
|
296
|
+
skipped++;
|
|
297
|
+
} else {
|
|
298
|
+
Logger.warn(`${relativePath}: ${result.message}`);
|
|
299
|
+
failed++;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
Logger.newline();
|
|
304
|
+
Logger.info("Corrigidos", String(fixed));
|
|
305
|
+
Logger.info("Já em UTF-8", String(skipped));
|
|
306
|
+
if (failed > 0) Logger.warn(`Não corrigidos: ${failed}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
Logger.endSection();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private async handleList(config: AppConfig) {
|
|
313
|
+
const srcDir = path.join(process.cwd(), "src");
|
|
314
|
+
|
|
315
|
+
if (!existsSync(srcDir)) {
|
|
316
|
+
Logger.error(`Diretório src/ não encontrado`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
Logger.section("Lista de Encodings");
|
|
321
|
+
Logger.info("Diretório", srcDir);
|
|
322
|
+
Logger.info("Encoding padrão", config.project.encoding || "utf-8 (padrão)");
|
|
323
|
+
Logger.newline();
|
|
324
|
+
|
|
325
|
+
const detections = await this.encodingService.detectDirectoryEncodings(srcDir);
|
|
326
|
+
|
|
327
|
+
if (detections.size === 0) {
|
|
328
|
+
Logger.warn("Nenhum arquivo de texto encontrado");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Ordena arquivos
|
|
333
|
+
const sortedFiles = Array.from(detections.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
334
|
+
|
|
335
|
+
Logger.log(`${Logger.C.primary}Arquivos:${Logger.C.reset}`);
|
|
336
|
+
for (const [file, detection] of sortedFiles) {
|
|
337
|
+
const relativePath = path.relative(srcDir, file);
|
|
338
|
+
const confidenceStr = detection.confidence >= 0.9 ? "" :
|
|
339
|
+
` ${Logger.C.gray}(${Math.round(detection.confidence * 100)}%)${Logger.C.reset}`;
|
|
340
|
+
const bomStr = detection.hasBOM ? ` ${Logger.C.warning}[BOM]${Logger.C.reset}` : "";
|
|
341
|
+
|
|
342
|
+
const encodingColor = detection.encoding === (config.project.encoding || "utf-8")
|
|
343
|
+
? Logger.C.success
|
|
344
|
+
: Logger.C.warning;
|
|
345
|
+
|
|
346
|
+
Logger.log(` ${encodingColor}${detection.encoding.padEnd(12)}${Logger.C.reset} ${relativePath}${confidenceStr}${bomStr}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
Logger.endSection();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { platform, totalmem, freemem, arch } from "os";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import type { Command } from "./Command";
|
|
6
|
+
import type { AppConfig, CLIArguments } from "../types/config";
|
|
7
|
+
import { Logger } from "../utils/ui";
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
interface HealthCheck {
|
|
12
|
+
name: string;
|
|
13
|
+
status: "ok" | "warning" | "error";
|
|
14
|
+
message: string;
|
|
15
|
+
details?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class HealthCommand implements Command {
|
|
19
|
+
async execute(config: AppConfig, _args?: CLIArguments): Promise<void> {
|
|
20
|
+
Logger.banner("health");
|
|
21
|
+
Logger.section("Verificando saúde do ambiente");
|
|
22
|
+
|
|
23
|
+
const checks: HealthCheck[] = [];
|
|
24
|
+
|
|
25
|
+
// Java
|
|
26
|
+
checks.push(await this.checkJava());
|
|
27
|
+
|
|
28
|
+
// Maven/Gradle
|
|
29
|
+
checks.push(await this.checkBuildTool(config.project.buildTool));
|
|
30
|
+
|
|
31
|
+
// Tomcat
|
|
32
|
+
checks.push(await this.checkTomcat(config));
|
|
33
|
+
|
|
34
|
+
// Portas
|
|
35
|
+
checks.push(await this.checkPorts(config.tomcat.port));
|
|
36
|
+
|
|
37
|
+
// Memória
|
|
38
|
+
checks.push(this.checkMemory());
|
|
39
|
+
|
|
40
|
+
// Disco
|
|
41
|
+
checks.push(await this.checkDisk());
|
|
42
|
+
|
|
43
|
+
// Git
|
|
44
|
+
checks.push(this.checkGit());
|
|
45
|
+
|
|
46
|
+
// Exibir resultados
|
|
47
|
+
Logger.newline();
|
|
48
|
+
let errors = 0;
|
|
49
|
+
let warnings = 0;
|
|
50
|
+
|
|
51
|
+
for (const check of checks) {
|
|
52
|
+
const icon = check.status === "ok"
|
|
53
|
+
? `${Logger.C.success}✓${Logger.C.reset}`
|
|
54
|
+
: check.status === "warning"
|
|
55
|
+
? `${Logger.C.warning}⚠${Logger.C.reset}`
|
|
56
|
+
: `${Logger.C.error}✗${Logger.C.reset}`;
|
|
57
|
+
|
|
58
|
+
Logger.log(`${Logger.C.gray}│${Logger.C.reset} ${icon} ${Logger.C.bold}${check.name}${Logger.C.reset}`);
|
|
59
|
+
Logger.log(`${Logger.C.gray}│${Logger.C.reset} ${check.message}`);
|
|
60
|
+
|
|
61
|
+
if (check.details) {
|
|
62
|
+
Logger.log(`${Logger.C.gray}│${Logger.C.reset} ${Logger.C.dim}${check.details}${Logger.C.reset}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (check.status === "error") errors++;
|
|
66
|
+
if (check.status === "warning") warnings++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Logger.endSection();
|
|
70
|
+
|
|
71
|
+
// Summary
|
|
72
|
+
if (errors === 0 && warnings === 0) {
|
|
73
|
+
Logger.ready("Ambiente saudável! ✓");
|
|
74
|
+
} else if (errors === 0) {
|
|
75
|
+
Logger.warn(`${warnings} aviso(s) encontrado(s)`);
|
|
76
|
+
} else {
|
|
77
|
+
Logger.error(`${errors} erro(s) encontrado(s)`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async checkJava(): Promise<HealthCheck> {
|
|
82
|
+
try {
|
|
83
|
+
const { stdout, stderr } = await execAsync("java -version");
|
|
84
|
+
const output = stderr || stdout;
|
|
85
|
+
const versionMatch = output.match(/version "?(\d+\.?\d*)/);
|
|
86
|
+
const version = versionMatch ? versionMatch[1] : "unknown";
|
|
87
|
+
const isDCEVM = output.toLowerCase().includes("dcevm") || output.toLowerCase().includes("jbr");
|
|
88
|
+
|
|
89
|
+
const majorVersion = parseInt(version.split(".")[0]);
|
|
90
|
+
const status = majorVersion >= 11 ? "ok" : "warning";
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
name: "Java",
|
|
94
|
+
status,
|
|
95
|
+
message: `v${version}${isDCEVM ? " + DCEVM" : ""}`,
|
|
96
|
+
details: isDCEVM ? "Hot-reload disponível" : "Considere instalar DCEVM para hot-reload"
|
|
97
|
+
};
|
|
98
|
+
} catch {
|
|
99
|
+
return {
|
|
100
|
+
name: "Java",
|
|
101
|
+
status: "error",
|
|
102
|
+
message: "Java não encontrado",
|
|
103
|
+
details: "Instale o JDK 11+ e configure JAVA_HOME"
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async checkBuildTool(tool: string): Promise<HealthCheck> {
|
|
109
|
+
try {
|
|
110
|
+
if (tool === "maven") {
|
|
111
|
+
const { stdout } = await execAsync("mvn -version");
|
|
112
|
+
const version = stdout.match(/Apache Maven (\d+\.\d+\.\d+)/)?.[1] || "unknown";
|
|
113
|
+
return {
|
|
114
|
+
name: "Maven",
|
|
115
|
+
status: "ok",
|
|
116
|
+
message: `v${version}`
|
|
117
|
+
};
|
|
118
|
+
} else {
|
|
119
|
+
const { stdout } = await execAsync("gradle --version");
|
|
120
|
+
const version = stdout.match(/Gradle (\d+\.\d+\.\d+)/)?.[1] || "unknown";
|
|
121
|
+
return {
|
|
122
|
+
name: "Gradle",
|
|
123
|
+
status: "ok",
|
|
124
|
+
message: `v${version}`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
return {
|
|
129
|
+
name: tool === "maven" ? "Maven" : "Gradle",
|
|
130
|
+
status: "error",
|
|
131
|
+
message: `${tool === "maven" ? "mvn" : "gradle"} não encontrado`,
|
|
132
|
+
details: `Instale ${tool === "maven" ? "Maven" : "Gradle"} e adicione ao PATH`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async checkTomcat(config: AppConfig): Promise<HealthCheck> {
|
|
138
|
+
if (config.tomcat.embedded) {
|
|
139
|
+
return {
|
|
140
|
+
name: "Tomcat",
|
|
141
|
+
status: "ok",
|
|
142
|
+
message: `Embutido v${config.tomcat.version || "10.1.52"}`,
|
|
143
|
+
details: "Auto-download habilitado"
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (existsSync(config.tomcat.path)) {
|
|
148
|
+
const versionFile = `${config.tomcat.path}/bin/version.sh`;
|
|
149
|
+
const versionBat = `${config.tomcat.path}/bin/version.bat`;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const cmd = existsSync(versionBat) ? versionBat : versionFile;
|
|
153
|
+
const { stdout } = await execAsync(cmd);
|
|
154
|
+
const version = stdout.match(/Server version: Apache Tomcat\/(\d+\.\d+\.\d+)/)?.[1] || "unknown";
|
|
155
|
+
return {
|
|
156
|
+
name: "Tomcat",
|
|
157
|
+
status: "ok",
|
|
158
|
+
message: `v${version}`,
|
|
159
|
+
details: config.tomcat.path
|
|
160
|
+
};
|
|
161
|
+
} catch {
|
|
162
|
+
return {
|
|
163
|
+
name: "Tomcat",
|
|
164
|
+
status: "warning",
|
|
165
|
+
message: "Caminho existe mas não foi possível verificar versão",
|
|
166
|
+
details: config.tomcat.path
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
name: "Tomcat",
|
|
173
|
+
status: "error",
|
|
174
|
+
message: "Caminho não encontrado",
|
|
175
|
+
details: `Configure CATALINA_HOME ou use Tomcat embutido`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async checkPorts(port: number): Promise<HealthCheck> {
|
|
180
|
+
try {
|
|
181
|
+
let cmd: string;
|
|
182
|
+
if (platform() === "win32") {
|
|
183
|
+
cmd = `netstat -an | findstr :${port}`;
|
|
184
|
+
} else if (platform() === "darwin") {
|
|
185
|
+
cmd = `lsof -i :${port}`;
|
|
186
|
+
} else {
|
|
187
|
+
cmd = `ss -tuln | grep :${port}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { stdout } = await execAsync(cmd);
|
|
191
|
+
const isInUse = stdout.trim().length > 0;
|
|
192
|
+
|
|
193
|
+
if (isInUse) {
|
|
194
|
+
return {
|
|
195
|
+
name: "Portas",
|
|
196
|
+
status: "warning",
|
|
197
|
+
message: `Porta ${port} em uso`,
|
|
198
|
+
details: "Outro processo pode estar usando a porta"
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
name: "Portas",
|
|
204
|
+
status: "ok",
|
|
205
|
+
message: `Porta ${port} disponível`
|
|
206
|
+
};
|
|
207
|
+
} catch {
|
|
208
|
+
// Comando falhou, assume que porta está livre
|
|
209
|
+
return {
|
|
210
|
+
name: "Portas",
|
|
211
|
+
status: "ok",
|
|
212
|
+
message: `Porta ${port} parece disponível`
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private checkMemory(): HealthCheck {
|
|
218
|
+
const total = totalmem();
|
|
219
|
+
const free = freemem();
|
|
220
|
+
const used = total - free;
|
|
221
|
+
const percentUsed = Math.round((used / total) * 100);
|
|
222
|
+
const freeGB = (free / 1024 / 1024 / 1024).toFixed(1);
|
|
223
|
+
const totalGB = (total / 1024 / 1024 / 1024).toFixed(1);
|
|
224
|
+
|
|
225
|
+
const status = percentUsed > 90 ? "warning" : "ok";
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
name: "Memória",
|
|
229
|
+
status,
|
|
230
|
+
message: `${freeGB}GB livre de ${totalGB}GB`,
|
|
231
|
+
details: `${percentUsed}% em uso`
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async checkDisk(): Promise<HealthCheck> {
|
|
236
|
+
try {
|
|
237
|
+
let cmd: string;
|
|
238
|
+
if (platform() === "win32") {
|
|
239
|
+
cmd = "wmic logicaldisk get size,freespace,caption";
|
|
240
|
+
} else {
|
|
241
|
+
cmd = "df -h .";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const { stdout } = await execAsync(cmd);
|
|
245
|
+
|
|
246
|
+
if (platform() === "win32") {
|
|
247
|
+
const lines = stdout.trim().split("\n").slice(1);
|
|
248
|
+
const mainDisk = lines.find(l => l.includes(":")) || "";
|
|
249
|
+
const parts = mainDisk.trim().split(/\s+/);
|
|
250
|
+
if (parts.length >= 3) {
|
|
251
|
+
const free = parseInt(parts[0]) / 1024 / 1024 / 1024;
|
|
252
|
+
const total = parseInt(parts[1]) / 1024 / 1024 / 1024;
|
|
253
|
+
const percentFree = Math.round((free / total) * 100);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
name: "Disco",
|
|
257
|
+
status: percentFree < 10 ? "warning" : "ok",
|
|
258
|
+
message: `${free.toFixed(1)}GB livre`,
|
|
259
|
+
details: `${percentFree}% disponível`
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
const match = stdout.match(/(\d+)%/);
|
|
264
|
+
if (match) {
|
|
265
|
+
const used = parseInt(match[1]);
|
|
266
|
+
return {
|
|
267
|
+
name: "Disco",
|
|
268
|
+
status: used > 90 ? "warning" : "ok",
|
|
269
|
+
message: `${100 - used}% disponível`,
|
|
270
|
+
details: stdout.split("\n")[1]?.split(/\s+/).pop() || ""
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
throw new Error("Could not parse disk info");
|
|
276
|
+
} catch {
|
|
277
|
+
return {
|
|
278
|
+
name: "Disco",
|
|
279
|
+
status: "warning",
|
|
280
|
+
message: "Não foi possível verificar",
|
|
281
|
+
details: "Verifique manualmente"
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private checkGit(): HealthCheck {
|
|
287
|
+
if (existsSync(".git")) {
|
|
288
|
+
return {
|
|
289
|
+
name: "Git",
|
|
290
|
+
status: "ok",
|
|
291
|
+
message: "Repositório Git detectado"
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
name: "Git",
|
|
297
|
+
status: "warning",
|
|
298
|
+
message: "Sem repositório Git",
|
|
299
|
+
details: "Execute 'git init' para versionamento"
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|