@archznn/xavva 2.5.0 → 2.7.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.
@@ -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
+ }
@@ -30,6 +30,7 @@ export class HelpCommand implements Command {
30
30
  ${this.c("green", "profiles")} List available Maven/Gradle profiles
31
31
  ${this.c("green", "tomcat")} Manage embedded Tomcat (install, list, installed, use, status)
32
32
  ${this.c("green", "docs")} Generate endpoint documentation
33
+ ${this.c("green", "encoding")} Convert file encoding (detect, convert, fix, list)
33
34
 
34
35
  ${this.c("yellow", "GENERAL OPTIONS")}
35
36
  ${this.c("cyan", "-p, --path")} <path> Tomcat installation path
@@ -65,6 +66,13 @@ export class HelpCommand implements Command {
65
66
  ${this.c("cyan", "--strict")} Fail on critical conflicts (for CI/CD)
66
67
  ${this.c("cyan", "-o, --output")} <file> Export report as JSON
67
68
 
69
+ ${this.c("yellow", "ENCODING OPTIONS")} ${this.c("dim", "(for xavva encoding)")}
70
+ ${this.c("cyan", "--from")} <encoding> Source encoding (auto-detect if not specified)
71
+ ${this.c("cyan", "--to")} <encoding> Target encoding (default: from xavva.json or UTF-8)
72
+ ${this.c("cyan", "--backup")} Create backup before conversion
73
+ ${this.c("cyan", "--dry-run")} Simulate without modifying files
74
+ ${this.c("cyan", "--src")} <path> Source directory (default: src/)
75
+
68
76
  ${this.c("yellow", "EXAMPLES")}
69
77
  ${this.c("dim", "# Development with hot reload and dashboard")}
70
78
  xavva dev --tui --watch
@@ -97,6 +105,13 @@ export class HelpCommand implements Command {
97
105
  xavva tomcat status
98
106
  xavva tomcat uninstall 9.0.115
99
107
 
108
+ ${this.c("dim", "# Convert file encoding")}
109
+ xavva encoding detect src/main/java/MinhaClasse.java
110
+ xavva encoding convert --from utf-8 --to cp1252 src/main/java/
111
+ xavva encoding convert --to cp1252 --backup src/main/java/MinhaClasse.java
112
+ xavva encoding fix src/main/java/MinhaClasse.java # Fix mojibake
113
+ xavva encoding list # List all file encodings
114
+
100
115
  ${this.c("yellow", "CONFIGURATION")}
101
116
  Settings are loaded from ${this.c("cyan", "xavva.json")} in the project root:
102
117
 
@@ -60,7 +60,7 @@ export class LogsCommand implements Command {
60
60
  currentSize = newStats.size;
61
61
  } else if (newStats.size < currentSize) {
62
62
  currentSize = newStats.size;
63
- dashboard.log(Logger.C.yellow + "Arquivo de log foi resetado/rotacionado.");
63
+ dashboard.log(Logger.C.warning + "Arquivo de log foi resetado/rotacionado.");
64
64
  }
65
65
  }
66
66
  });
@@ -5,6 +5,14 @@ import path from "path";
5
5
  import fs from "fs";
6
6
  import { glob } from "glob";
7
7
  import readline from "readline";
8
+ import {
9
+ getJavaPath,
10
+ getMavenCommand,
11
+ getGradleCommand,
12
+ getClasspathSeparator,
13
+ normalizeClasspathPath,
14
+ isWindows,
15
+ } from "../utils/platform";
8
16
 
9
17
  export class RunCommand implements Command {
10
18
  async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
@@ -36,7 +44,8 @@ export class RunCommand implements Command {
36
44
  const { localCp, dependencyCp } = await this.getClasspath(config);
37
45
  const pathingJar = await this.createPathingJar(dependencyCp);
38
46
 
39
- const finalCp = `${localCp};${pathingJar}`;
47
+ const sep = getClasspathSeparator();
48
+ const finalCp = `${localCp}${sep}${pathingJar}`;
40
49
 
41
50
  const javaArgs = [
42
51
  "-classpath", finalCp,
@@ -60,7 +69,7 @@ export class RunCommand implements Command {
60
69
  Logger.warn(`🚀 Executando ${className}...`);
61
70
  }
62
71
 
63
- const bin = process.env.JAVA_HOME ? path.join(process.env.JAVA_HOME, "bin", "java.exe") : "java";
72
+ const bin = getJavaPath();
64
73
 
65
74
  const proc = Bun.spawn([bin, ...javaArgs], {
66
75
  stdout: "inherit",
@@ -220,9 +229,10 @@ export class RunCommand implements Command {
220
229
  const xavvaDir = path.join(process.cwd(), ".xavva");
221
230
  const jarPath = path.join(xavvaDir, "classpath.jar");
222
231
 
223
- const paths = dependencyCp.split(";").filter(p => p.trim());
232
+ const sep = getClasspathSeparator();
233
+ const paths = dependencyCp.split(sep).filter(p => p.trim());
224
234
  const relativePaths = paths.map(p => {
225
- let rel = path.relative(xavvaDir, p).replace(/\\/g, "/");
235
+ let rel = normalizeClasspathPath(path.relative(xavvaDir, p));
226
236
  if (fs.existsSync(p) && fs.statSync(p).isDirectory() && !rel.endsWith("/")) rel += "/";
227
237
  // Robust URL encoding for Class-Path as per Java Spec
228
238
  return encodeURI(rel)
@@ -296,10 +306,9 @@ export class RunCommand implements Command {
296
306
  const stopSpinner = Logger.spinner("Generating project classpath");
297
307
  try {
298
308
  if (config.project.buildTool === "maven") {
299
- const mvnCmd = process.platform === "win32" ? "mvn.cmd" : "mvn";
300
- Bun.spawnSync([mvnCmd, "dependency:build-classpath", `-Dmdep.outputFile=${cpFile}`]);
309
+ Bun.spawnSync([getMavenCommand(), "dependency:build-classpath", `-Dmdep.outputFile=${cpFile}`]);
301
310
  } else if (config.project.buildTool === "gradle") {
302
- const gradleCmd = process.platform === "win32" ? "gradle.bat" : "gradle";
311
+ const gradleCmd = getGradleCommand();
303
312
  const initScriptPath = path.join(xavvaDir, "init-cp.gradle");
304
313
  const normalizedCpFile = cpFile.replace(/\\/g, "/");
305
314
  const initScriptContent = `
@@ -335,9 +344,10 @@ export class RunCommand implements Command {
335
344
 
336
345
  let dependencyCp = fs.existsSync(cpFile) ? fs.readFileSync(cpFile, "utf8").trim() : "";
337
346
 
338
- // Normalize platform specific separators to semicolon for consistency
339
- if (path.delimiter !== ";") {
340
- dependencyCp = dependencyCp.split(path.delimiter).join(";");
347
+ // Normalize platform specific separators para o separador consistente
348
+ const sep = getClasspathSeparator();
349
+ if (path.delimiter !== sep) {
350
+ dependencyCp = dependencyCp.split(path.delimiter).join(sep);
341
351
  }
342
352
 
343
353
  const localFolders = [
@@ -356,7 +366,7 @@ export class RunCommand implements Command {
356
366
  const localCp = localFolders
357
367
  .map(p => path.join(process.cwd(), p))
358
368
  .filter(p => fs.existsSync(p))
359
- .join(";");
369
+ .join(getClasspathSeparator());
360
370
 
361
371
  return { localCp, dependencyCp };
362
372
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Configurações centralizadas de versões
3
+ * Evita hardcoding espalhado pelo código
4
+ */
5
+
6
+ export const VERSIONS = {
7
+ // Versões padrão do Tomcat
8
+ TOMCAT: {
9
+ DEFAULT: "10.1.52",
10
+ AVAILABLE: {
11
+ "10.1.52": { sha512: "" },
12
+ "9.0.115": { sha512: "" },
13
+ "11.0.18": { sha512: "" },
14
+ },
15
+ },
16
+
17
+ // HotswapAgent
18
+ HOTSWAP_AGENT: {
19
+ VERSION: "2.0.3",
20
+ URL: "https://github.com/HotswapProjects/HotswapAgent/releases/download/RELEASE-{version}/hotswap-agent-{version}.jar",
21
+ },
22
+
23
+ // Configurações padrão
24
+ DEFAULTS: {
25
+ TOMCAT_PORT: 8080,
26
+ DEBUG_PORT: 5005,
27
+ DEBOUNCE_MS: 300,
28
+ COOLING_MS: 1000,
29
+ },
30
+ } as const;
31
+
32
+ // Type helpers
33
+ export type TomcatVersion = keyof typeof VERSIONS.TOMCAT.AVAILABLE;
34
+
35
+ /**
36
+ * Obtém URL de download do HotswapAgent
37
+ */
38
+ export function getHotswapAgentUrl(version: string = VERSIONS.HOTSWAP_AGENT.VERSION): string {
39
+ return VERSIONS.HOTSWAP_AGENT.URL
40
+ .replace(/{version}/g, version);
41
+ }
42
+
43
+ /**
44
+ * Verifica se versão do Tomcat é suportada
45
+ */
46
+ export function isSupportedTomcatVersion(version: string): version is TomcatVersion {
47
+ return version in VERSIONS.TOMCAT.AVAILABLE;
48
+ }
49
+
50
+ /**
51
+ * Obtém versões disponíveis do Tomcat
52
+ */
53
+ export function getAvailableTomcatVersions(): string[] {
54
+ return Object.keys(VERSIONS.TOMCAT.AVAILABLE);
55
+ }
56
+
57
+ /**
58
+ * Obtém SHA512 de uma versão do Tomcat
59
+ */
60
+ export function getTomcatSha512(version: string): string | undefined {
61
+ const info = VERSIONS.TOMCAT.AVAILABLE[version as TomcatVersion];
62
+ return info?.sha512;
63
+ }