@archznn/xavva 2.3.0 → 2.4.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Ultra-fast development toolkit for Java Enterprise (Tomcat) on Windows
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.3.0-blue.svg)](https://github.com/leorsousa05/Xavva)
5
+ [![Version](https://img.shields.io/badge/version-2.4.0-blue.svg)](https://github.com/leorsousa05/Xavva)
6
6
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7
7
 
8
8
  Xavva is a high-performance CLI built with **Bun** that transforms the Java/Tomcat development experience. It brings modern development workflows (like Node.js/Vite) to the Java Enterprise ecosystem with hot-reload, smart logging, and automated deployment.
@@ -102,16 +102,25 @@ Xavva can automatically download and manage a Tomcat installation for you:
102
102
  # First time usage - auto-install Tomcat
103
103
  xavva dev --yes
104
104
 
105
- # Or install manually
106
- xavva tomcat install
105
+ # List available versions to download
106
+ xavva tomcat list
107
+
108
+ # List already installed versions
109
+ xavva tomcat installed
110
+
111
+ # Install a specific version
112
+ xavva tomcat install 9.0.115
113
+
114
+ # Switch to a version for this project
115
+ xavva tomcat use 9.0.115
107
116
 
108
117
  # Check Tomcat status
109
118
  xavva tomcat status
110
119
 
111
- # List available versions
112
- xavva tomcat list
120
+ # Remove a version
121
+ xavva tomcat uninstall 9.0.115
113
122
 
114
- # Use specific version
123
+ # Or use with flags
115
124
  xavva dev --tomcat-version 9.0.115
116
125
  ```
117
126
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archznn/xavva",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Ultra-fast CLI tool for Java/Tomcat development on Windows with Hot-Reload and Zero Config.",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  import type { AppConfig, CLIArguments } from "../types/config";
2
2
 
3
3
  export interface Command {
4
- execute(config: AppConfig, args?: CLIArguments): Promise<void>;
4
+ execute(config: AppConfig, args?: CLIArguments, positionals?: string[]): Promise<void>;
5
5
  }
@@ -19,18 +19,18 @@ export class CommandRegistry {
19
19
  return this.commands.get(name);
20
20
  }
21
21
 
22
- async execute(name: string, config: AppConfig, args: CLIArguments): Promise<void> {
22
+ async execute(name: string, config: AppConfig, args: CLIArguments, positionals?: string[]): Promise<void> {
23
23
  const command = this.commands.get(name);
24
24
  const processManager = ProcessManager.getInstance();
25
25
 
26
26
  if (!command) {
27
27
  Logger.error(`Comando desconhecido: ${name}`);
28
- await new HelpCommand().execute(config);
28
+ await new HelpCommand().execute(config, args, positionals);
29
29
  await processManager.shutdown(2);
30
30
  }
31
31
 
32
32
  try {
33
- await command.execute(config, args);
33
+ await command.execute(config, args, positionals);
34
34
  } catch (error) {
35
35
  const message = error instanceof Error ? error.message : String(error);
36
36
  Logger.error(`Erro ao executar comando '${name}': ${message}`);
@@ -28,7 +28,7 @@ export class HelpCommand implements Command {
28
28
  ${this.c("green", "audit")} Security audit of JAR files
29
29
  ${this.c("green", "doctor")} Diagnose and fix environment issues
30
30
  ${this.c("green", "profiles")} List available Maven/Gradle profiles
31
- ${this.c("green", "tomcat")} Manage embedded Tomcat (install, list, status)
31
+ ${this.c("green", "tomcat")} Manage embedded Tomcat (install, list, installed, use, status)
32
32
  ${this.c("green", "docs")} Generate endpoint documentation
33
33
 
34
34
  ${this.c("yellow", "GENERAL OPTIONS")}
@@ -90,9 +90,12 @@ export class HelpCommand implements Command {
90
90
  xavva audit --fix
91
91
 
92
92
  ${this.c("dim", "# Manage embedded Tomcat")}
93
- xavva tomcat install
93
+ xavva tomcat list # List available versions
94
+ xavva tomcat installed # List installed versions
95
+ xavva tomcat install 9.0.115 # Install specific version
96
+ xavva tomcat use 9.0.115 # Switch to version for this project
94
97
  xavva tomcat status
95
- xavva tomcat list
98
+ xavva tomcat uninstall 9.0.115
96
99
 
97
100
  ${this.c("yellow", "CONFIGURATION")}
98
101
  Settings are loaded from ${this.c("cyan", "xavva.json")} in the project root:
@@ -3,32 +3,47 @@ import type { AppConfig, CLIArguments } from "../types/config";
3
3
  import { EmbeddedTomcatService } from "../services/EmbeddedTomcatService";
4
4
  import { Logger } from "../utils/ui";
5
5
  import path from "path";
6
+ import fs from "fs";
6
7
 
7
8
  export class TomcatCommand implements Command {
8
- async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
9
- const action = args?.["tomcat-action"] || "status";
9
+ async execute(config: AppConfig, args?: CLIArguments, positionals?: string[]): Promise<void> {
10
+ // A ação vem como positional após "tomcat" (ex: xavva tomcat list)
11
+ const tomcatIndex = positionals?.indexOf("tomcat") ?? -1;
12
+ const action = positionals && tomcatIndex >= 0 && positionals[tomcatIndex + 1]
13
+ ? positionals[tomcatIndex + 1]
14
+ : "status";
15
+
16
+ // Argumentos extras após a ação (ex: xavva tomcat install 9.0.115)
17
+ const extraArgs = positionals && tomcatIndex >= 0 ? positionals.slice(tomcatIndex + 2) : [];
10
18
 
11
19
  switch (action) {
12
20
  case "install":
13
- await this.handleInstall(config, args);
21
+ await this.handleInstall(config, args, extraArgs);
14
22
  break;
15
23
  case "list":
16
24
  this.handleList();
17
25
  break;
26
+ case "installed":
27
+ this.handleInstalled();
28
+ break;
29
+ case "use":
30
+ await this.handleUse(config, args, extraArgs);
31
+ break;
18
32
  case "uninstall":
19
- await this.handleUninstall(config, args);
33
+ await this.handleUninstall(config, args, extraArgs);
20
34
  break;
21
35
  case "status":
22
36
  await this.handleStatus(config);
23
37
  break;
24
38
  default:
25
39
  Logger.error(`Ação desconhecida: ${action}`);
26
- Logger.info("Ações disponíveis", "install, list, uninstall, status");
40
+ Logger.info("Ações disponíveis", "install, list, installed, use, uninstall, status");
27
41
  }
28
42
  }
29
43
 
30
- private async handleInstall(config: AppConfig, args?: CLIArguments): Promise<void> {
31
- const version = args?.["tomcat-version"] || config.tomcat.version || "10.1.52";
44
+ private async handleInstall(config: AppConfig, args?: CLIArguments, extraArgs: string[] = []): Promise<void> {
45
+ // Versão pode vir de: flag --tomcat-version, argumento posicional, config, ou padrão
46
+ const version = args?.["tomcat-version"] || extraArgs[0] || config.tomcat.version || "10.1.52";
32
47
 
33
48
  // Detectar webapp path
34
49
  const webappPath = config.project.buildTool === "maven"
@@ -57,7 +72,7 @@ export class TomcatCommand implements Command {
57
72
  }
58
73
 
59
74
  private handleList(): void {
60
- Logger.section("Versões Disponíveis");
75
+ Logger.section("Versões Disponíveis para Download");
61
76
  const versions = EmbeddedTomcatService.getAvailableVersions();
62
77
 
63
78
  for (const version of versions) {
@@ -66,10 +81,90 @@ export class TomcatCommand implements Command {
66
81
 
67
82
  Logger.newline();
68
83
  Logger.info("Versão padrão", "10.1.52");
84
+ Logger.newline();
85
+ Logger.info("Dica", "Use 'xavva tomcat installed' para ver versões já instaladas");
86
+ }
87
+
88
+ private handleInstalled(): void {
89
+ const installed = EmbeddedTomcatService.listInstalledVersions();
90
+
91
+ Logger.section("Versões Instaladas");
92
+
93
+ if (installed.length === 0) {
94
+ Logger.warn("Nenhuma versão instalada");
95
+ Logger.info("Dica", "Use 'xavva tomcat install <version>' para instalar");
96
+ return;
97
+ }
98
+
99
+ for (const version of installed) {
100
+ Logger.log(` ${Logger.C.success}✓${Logger.C.reset} ${version}`);
101
+ }
102
+
103
+ Logger.newline();
104
+ Logger.info("Para usar uma versão", "xavva tomcat use <version>");
105
+ }
106
+
107
+ private async handleUse(config: AppConfig, args?: CLIArguments, extraArgs: string[] = []): Promise<void> {
108
+ const version = extraArgs[0] || args?.["tomcat-version"];
109
+
110
+ if (!version) {
111
+ Logger.error("Versão não especificada");
112
+ Logger.info("Uso", "xavva tomcat use <version>");
113
+ Logger.info("Exemplo", "xavva tomcat use 9.0.115");
114
+ Logger.newline();
115
+ Logger.info("Versões instaladas", "");
116
+ this.handleInstalled();
117
+ return;
118
+ }
119
+
120
+ // Verifica se a versão está instalada
121
+ const service = new EmbeddedTomcatService({
122
+ version,
123
+ port: config.tomcat.port,
124
+ webappPath: "."
125
+ });
126
+
127
+ if (!service.checkInstallation()) {
128
+ Logger.warn(`Tomcat ${version} não está instalado`);
129
+ Logger.newline();
130
+ Logger.info("Opções", "");
131
+ Logger.log(` ${Logger.C.primary}1.${Logger.C.reset} Instalar agora: xavva tomcat install ${version}`);
132
+ Logger.log(` ${Logger.C.primary}2.${Logger.C.reset} Ver instaladas: xavva tomcat installed`);
133
+ return;
134
+ }
135
+
136
+ // Salva a versão no xavva.json do projeto
137
+ await this.saveTomcatVersion(version);
138
+
139
+ Logger.success(`Tomcat ${version} configurado para este projeto!`);
140
+ Logger.newline();
141
+ Logger.info("Próximos comandos", "");
142
+ Logger.log(` ${Logger.C.primary}•${Logger.C.reset} xavva dev # Iniciar desenvolvimento`);
143
+ Logger.log(` ${Logger.C.primary}•${Logger.C.reset} xavva deploy # Fazer deploy`);
144
+ }
145
+
146
+ private async saveTomcatVersion(version: string): Promise<void> {
147
+ const xavvaJsonPath = path.join(process.cwd(), "xavva.json");
148
+ let config: any = {};
149
+
150
+ if (fs.existsSync(xavvaJsonPath)) {
151
+ try {
152
+ config = JSON.parse(fs.readFileSync(xavvaJsonPath, "utf-8"));
153
+ } catch (e) {
154
+ // Arquivo existe mas é inválido, começa do zero
155
+ }
156
+ }
157
+
158
+ if (!config.tomcat) config.tomcat = {};
159
+ config.tomcat.version = version;
160
+ config.tomcat.embedded = true;
161
+
162
+ fs.writeFileSync(xavvaJsonPath, JSON.stringify(config, null, 2));
69
163
  }
70
164
 
71
- private async handleUninstall(config: AppConfig, args?: CLIArguments): Promise<void> {
72
- const version = args?.["tomcat-version"] || config.tomcat.version || "10.1.52";
165
+ private async handleUninstall(config: AppConfig, args?: CLIArguments, extraArgs: string[] = []): Promise<void> {
166
+ // Versão pode vir de: flag --tomcat-version, argumento posicional, config, ou padrão
167
+ const version = args?.["tomcat-version"] || extraArgs[0] || config.tomcat.version || "10.1.52";
73
168
 
74
169
  const service = new EmbeddedTomcatService({
75
170
  version,
package/src/index.ts CHANGED
@@ -105,7 +105,7 @@ async function main() {
105
105
  if (commandName === "debug") values.debug = true;
106
106
  if (commandName === "run") values.debug = false;
107
107
 
108
- await registry.execute(commandName, config, values);
108
+ await registry.execute(commandName, config, values, positionals);
109
109
  }
110
110
  }
111
111
 
@@ -1,245 +1,288 @@
1
1
  import { Logger } from "../utils/ui";
2
- import { existsSync, mkdirSync, createWriteStream, writeFileSync, promises as fs } from "fs";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ createWriteStream,
6
+ writeFileSync,
7
+ readdirSync,
8
+ promises as fsPromises,
9
+ } from "fs";
3
10
  import path from "path";
4
11
  import os from "os";
5
12
  import { spawn } from "child_process";
6
13
 
7
14
  export interface EmbeddedTomcatOptions {
8
- version?: string;
9
- port?: number;
10
- webappPath: string;
11
- contextPath?: string;
15
+ version?: string;
16
+ port?: number;
17
+ webappPath: string;
18
+ contextPath?: string;
12
19
  }
13
20
 
14
21
  interface DownloadProgress {
15
- downloaded: number;
16
- total: number;
17
- percent: number;
22
+ downloaded: number;
23
+ total: number;
24
+ percent: number;
18
25
  }
19
26
 
20
27
  export class EmbeddedTomcatService {
21
- private readonly baseDir: string;
22
- private readonly version: string;
23
- private port: number;
24
- private webappPath: string;
25
- private contextPath: string;
26
- private tomcatHome: string;
27
- private downloadUrl: string;
28
- private isInstalled: boolean = false;
29
-
30
- // Versões estáveis do Tomcat (atualizadas: 2026-03-04)
31
- private static readonly VERSIONS: Record<string, { url: string; sha512: string }> = {
32
- "10.1.52": {
33
- url: "https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.52/bin/apache-tomcat-10.1.52-windows-x64.zip",
34
- sha512: ""
35
- },
36
- "9.0.115": {
37
- url: "https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.115/bin/apache-tomcat-9.0.115-windows-x64.zip",
38
- sha512: ""
39
- },
40
- "11.0.18": {
41
- url: "https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.18/bin/apache-tomcat-11.0.18-windows-x64.zip",
42
- sha512: ""
43
- }
44
- };
45
-
46
- constructor(options: EmbeddedTomcatOptions) {
47
- this.version = options.version || "10.1.52";
48
- this.port = options.port || 8080;
49
- this.webappPath = path.resolve(options.webappPath);
50
- this.contextPath = options.contextPath || "/";
51
- this.baseDir = path.join(os.homedir(), ".xavva", "tomcat");
52
- this.tomcatHome = path.join(this.baseDir, this.version);
53
-
54
- // Se a versão não está na lista, usa URL padrão
55
- const versionInfo = EmbeddedTomcatService.VERSIONS[this.version];
56
- if (versionInfo) {
57
- this.downloadUrl = versionInfo.url;
58
- } else {
59
- // Tenta inferir URL baseado no padrão Apache
60
- const majorVersion = this.version.split(".")[0];
61
- this.downloadUrl = `https://archive.apache.org/dist/tomcat/tomcat-${majorVersion}/v${this.version}/bin/apache-tomcat-${this.version}-windows-x64.zip`;
62
- }
63
- }
64
-
65
- /**
66
- * Verifica se o Tomcat já está instalado
67
- */
68
- checkInstallation(): boolean {
69
- const catalinaBat = path.join(this.tomcatHome, "bin", "catalina.bat");
70
- this.isInstalled = existsSync(catalinaBat);
71
- return this.isInstalled;
72
- }
73
-
74
- /**
75
- * Retorna o caminho do Tomcat (instalado ou para instalar)
76
- */
77
- getTomcatHome(): string {
78
- return this.tomcatHome;
79
- }
80
-
81
- /**
82
- * Baixa e instala o Tomcat
83
- */
84
- async install(): Promise<boolean> {
85
- if (this.checkInstallation()) {
86
- Logger.info("Tomcat", `Versão ${this.version} já instalada`);
87
- return true;
88
- }
89
-
90
- Logger.section("Instalando Tomcat Embutido");
91
- Logger.info("Versão", this.version);
92
- Logger.info("Destino", this.tomcatHome);
93
-
94
- // Cria diretório base
95
- if (!existsSync(this.baseDir)) {
96
- mkdirSync(this.baseDir, { recursive: true });
97
- }
98
-
99
- const zipPath = path.join(this.baseDir, `apache-tomcat-${this.version}.zip`);
100
-
101
- try {
102
- // Download
103
- await this.downloadFile(this.downloadUrl, zipPath);
104
-
105
- // Extração
106
- await this.extractZip(zipPath, this.baseDir);
107
-
108
- // Renomeia diretório extraído para versão padronizada
109
- const extractedDir = path.join(this.baseDir, `apache-tomcat-${this.version}`);
110
- if (existsSync(extractedDir) && extractedDir !== this.tomcatHome) {
111
- await fs.rename(extractedDir, this.tomcatHome);
112
- }
113
-
114
- // Limpa arquivo zip
115
- await fs.unlink(zipPath).catch(() => {});
116
-
117
- // Configura server.xml
118
- await this.configureServerXml();
119
-
120
- // Configura context.xml para hot-reload
121
- await this.configureContextXml();
122
-
123
- this.isInstalled = true;
124
- Logger.success(`Tomcat ${this.version} instalado com sucesso!`);
125
- return true;
126
-
127
- } catch (error) {
128
- Logger.error(`Falha ao instalar Tomcat: ${error}`);
129
- // Limpa arquivos parciais
130
- if (existsSync(this.tomcatHome)) {
131
- await fs.rm(this.tomcatHome, { recursive: true, force: true });
132
- }
133
- return false;
134
- }
135
- }
136
-
137
- /**
138
- * Configura server.xml com porta personalizada
139
- */
140
- private async configureServerXml(): Promise<void> {
141
- const serverXmlPath = path.join(this.tomcatHome, "conf", "server.xml");
142
-
143
- if (!existsSync(serverXmlPath)) {
144
- throw new Error("server.xml não encontrado após extração");
145
- }
146
-
147
- let content = await fs.readFile(serverXmlPath, "utf-8");
148
-
149
- // Atualiza porta HTTP
150
- content = content.replace(
151
- /<Connector port="8080"/,
152
- `<Connector port="${this.port}"`
153
- );
154
-
155
- // Atualiza porta de shutdown
156
- const shutdownPort = this.port + 1000;
157
- content = content.replace(
158
- /<Server port="8005"/,
159
- `<Server port="${shutdownPort}"`
160
- );
161
-
162
- // Atualiza porta AJP (se existir)
163
- content = content.replace(
164
- /<Connector port="8009"/,
165
- `<Connector port="${this.port + 1001}"`
166
- );
167
-
168
- // Desabilita manager e host-manager em embedded (opcional)
169
- // Remove context do manager para segurança
170
- content = content.replace(
171
- /<Context docBase="manager"[^>]*\/>/g,
172
- "<!-- <Context docBase=\"manager\" ... /> -->"
173
- );
174
-
175
- await fs.writeFile(serverXmlPath, content, "utf-8");
176
- Logger.debug(`server.xml configurado na porta ${this.port}`);
177
- }
178
-
179
- /**
180
- * Configura context.xml para hot-reload
181
- */
182
- private async configureContextXml(): Promise<void> {
183
- const contextXmlPath = path.join(this.tomcatHome, "conf", "context.xml");
184
-
185
- if (!existsSync(contextXmlPath)) return;
186
-
187
- let content = await fs.readFile(contextXmlPath, "utf-8");
188
-
189
- // Adiciona atributos para hot-reload se não existirem
190
- if (!content.includes("reloadable")) {
191
- content = content.replace(
192
- /<Context>/,
193
- '<Context reloadable="true" autoDeploy="true" deployOnStartup="true">'
194
- );
195
- }
196
-
197
- await fs.writeFile(contextXmlPath, content, "utf-8");
198
- }
199
-
200
- /**
201
- * Cria contexto para a aplicação
202
- */
203
- async createAppContext(): Promise<void> {
204
- const webappsDir = path.join(this.tomcatHome, "webapps");
205
-
206
- // Limpa webapps padrão
207
- const defaultApps = ["docs", "examples", "host-manager", "manager", "ROOT"];
208
- for (const app of defaultApps) {
209
- const appPath = path.join(webappsDir, app);
210
- if (existsSync(appPath)) {
211
- await fs.rm(appPath, { recursive: true, force: true });
212
- }
213
- }
214
-
215
- // Cria diretório para a aplicação
216
- const appName = this.contextPath === "/" ? "ROOT" : this.contextPath.replace(/^\//, "");
217
- const appDir = path.join(webappsDir, appName);
218
-
219
- if (existsSync(appDir)) {
220
- await fs.rm(appDir, { recursive: true, force: true });
221
- }
222
-
223
- // Se webappPath é um diretório, cria link/simula deploy
224
- if (existsSync(this.webappPath)) {
225
- // Em Windows, vamos copiar inicialmente (symlink requer privilégios)
226
- // Ou criar um context XML apontando para o diretório
227
- await this.createContextXml(appName);
228
- }
229
- }
230
-
231
- /**
232
- * Cria arquivo context XML para apontar para diretório externo
233
- */
234
- private async createContextXml(appName: string): Promise<void> {
235
- const confDir = path.join(this.tomcatHome, "conf", "Catalina", "localhost");
236
-
237
- if (!existsSync(confDir)) {
238
- mkdirSync(confDir, { recursive: true });
239
- }
240
-
241
- const contextFile = path.join(confDir, `${appName}.xml`);
242
- const content = `<?xml version="1.0" encoding="UTF-8"?>
28
+ private readonly baseDir: string;
29
+ private readonly version: string;
30
+ private port: number;
31
+ private webappPath: string;
32
+ private contextPath: string;
33
+ private tomcatHome: string;
34
+ private downloadUrl: string;
35
+ private isInstalled: boolean = false;
36
+
37
+ // Versões estáveis do Tomcat (atualizadas: 2026-03-04)
38
+ private static readonly VERSIONS: Record<
39
+ string,
40
+ { url: string; sha512: string }
41
+ > = {
42
+ "10.1.52": {
43
+ url: "https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.52/bin/apache-tomcat-10.1.52-windows-x64.zip",
44
+ sha512: "",
45
+ },
46
+ "9.0.115": {
47
+ url: "https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.115/bin/apache-tomcat-9.0.115-windows-x64.zip",
48
+ sha512: "",
49
+ },
50
+ "11.0.18": {
51
+ url: "https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.18/bin/apache-tomcat-11.0.18-windows-x64.zip",
52
+ sha512: "",
53
+ },
54
+ };
55
+
56
+ constructor(options: EmbeddedTomcatOptions) {
57
+ this.version = options.version || "10.1.52";
58
+ this.port = options.port || 8080;
59
+ this.webappPath = path.resolve(options.webappPath);
60
+ this.contextPath = options.contextPath || "/";
61
+ this.baseDir = path.join(os.homedir(), ".xavva", "tomcat");
62
+ this.tomcatHome = path.join(this.baseDir, this.version);
63
+
64
+ // Se a versão não está na lista, usa URL padrão
65
+ const versionInfo = EmbeddedTomcatService.VERSIONS[this.version];
66
+ if (versionInfo) {
67
+ this.downloadUrl = versionInfo.url;
68
+ } else {
69
+ // Tenta inferir URL baseado no padrão Apache
70
+ const majorVersion = this.version.split(".")[0];
71
+ this.downloadUrl = `https://archive.apache.org/dist/tomcat/tomcat-${majorVersion}/v${this.version}/bin/apache-tomcat-${this.version}-windows-x64.zip`;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Verifica se o Tomcat já está instalado
77
+ */
78
+ checkInstallation(): boolean {
79
+ const catalinaBat = path.join(this.tomcatHome, "bin", "catalina.bat");
80
+ this.isInstalled = existsSync(catalinaBat);
81
+ return this.isInstalled;
82
+ }
83
+
84
+ /**
85
+ * Retorna o caminho do Tomcat (instalado ou para instalar)
86
+ */
87
+ getTomcatHome(): string {
88
+ return this.tomcatHome;
89
+ }
90
+
91
+ /**
92
+ * Lista todas as versões instaladas
93
+ */
94
+ static listInstalledVersions(): string[] {
95
+ const baseDir = path.join(os.homedir(), ".xavva", "tomcat");
96
+ if (!existsSync(baseDir)) return [];
97
+
98
+ const versions: string[] = [];
99
+ const entries = readdirSync(baseDir, { withFileTypes: true });
100
+
101
+ for (const entry of entries) {
102
+ if (entry.isDirectory()) {
103
+ const catalinaBat = path.join(
104
+ baseDir,
105
+ entry.name,
106
+ "bin",
107
+ "catalina.bat",
108
+ );
109
+ if (existsSync(catalinaBat)) {
110
+ versions.push(entry.name);
111
+ }
112
+ }
113
+ }
114
+
115
+ return versions.sort();
116
+ }
117
+
118
+ /**
119
+ * Baixa e instala o Tomcat
120
+ */
121
+ async install(): Promise<boolean> {
122
+ if (this.checkInstallation()) {
123
+ Logger.info("Tomcat", `Versão ${this.version} já instalada`);
124
+ return true;
125
+ }
126
+
127
+ Logger.section("Instalando Tomcat Embutido");
128
+ Logger.info("Versão", this.version);
129
+ Logger.info("Destino", this.tomcatHome);
130
+
131
+ // Cria diretório base
132
+ if (!existsSync(this.baseDir)) {
133
+ mkdirSync(this.baseDir, { recursive: true });
134
+ }
135
+
136
+ const zipPath = path.join(
137
+ this.baseDir,
138
+ `apache-tomcat-${this.version}.zip`,
139
+ );
140
+
141
+ try {
142
+ // Download
143
+ await this.downloadFile(this.downloadUrl, zipPath);
144
+
145
+ // Extração
146
+ await this.extractZip(zipPath, this.baseDir);
147
+
148
+ // Renomeia diretório extraído para versão padronizada
149
+ const extractedDir = path.join(
150
+ this.baseDir,
151
+ `apache-tomcat-${this.version}`,
152
+ );
153
+ if (existsSync(extractedDir) && extractedDir !== this.tomcatHome) {
154
+ await fsPromises.rename(extractedDir, this.tomcatHome);
155
+ }
156
+
157
+ // Limpa arquivo zip
158
+ await fsPromises.unlink(zipPath).catch(() => { });
159
+
160
+ // Configura server.xml
161
+ await this.configureServerXml();
162
+
163
+ // Configura context.xml para hot-reload
164
+ await this.configureContextXml();
165
+
166
+ this.isInstalled = true;
167
+ Logger.success(`Tomcat ${this.version} instalado com sucesso!`);
168
+ return true;
169
+ } catch (error) {
170
+ Logger.error(`Falha ao instalar Tomcat: ${error}`);
171
+ // Limpa arquivos parciais
172
+ if (existsSync(this.tomcatHome)) {
173
+ await fsPromises.rm(this.tomcatHome, { recursive: true, force: true });
174
+ }
175
+ return false;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Configura server.xml com porta personalizada
181
+ */
182
+ private async configureServerXml(): Promise<void> {
183
+ const serverXmlPath = path.join(this.tomcatHome, "conf", "server.xml");
184
+
185
+ if (!existsSync(serverXmlPath)) {
186
+ throw new Error("server.xml não encontrado após extração");
187
+ }
188
+
189
+ let content = await fsPromises.readFile(serverXmlPath, "utf-8");
190
+
191
+ // Atualiza porta HTTP
192
+ content = content.replace(
193
+ /<Connector port="8080"/,
194
+ `<Connector port="${this.port}"`,
195
+ );
196
+
197
+ // Atualiza porta de shutdown
198
+ const shutdownPort = this.port + 1000;
199
+ content = content.replace(
200
+ /<Server port="8005"/,
201
+ `<Server port="${shutdownPort}"`,
202
+ );
203
+
204
+ // Atualiza porta AJP (se existir)
205
+ content = content.replace(
206
+ /<Connector port="8009"/,
207
+ `<Connector port="${this.port + 1001}"`,
208
+ );
209
+
210
+ // Desabilita manager e host-manager em embedded (opcional)
211
+ // Remove context do manager para segurança
212
+ content = content.replace(
213
+ /<Context docBase="manager"[^>]*\/>/g,
214
+ '<!-- <Context docBase="manager" ... /> -->',
215
+ );
216
+
217
+ await fsPromises.writeFile(serverXmlPath, content, "utf-8");
218
+ Logger.debug(`server.xml configurado na porta ${this.port}`);
219
+ }
220
+
221
+ /**
222
+ * Configura context.xml para hot-reload
223
+ */
224
+ private async configureContextXml(): Promise<void> {
225
+ const contextXmlPath = path.join(this.tomcatHome, "conf", "context.xml");
226
+
227
+ if (!existsSync(contextXmlPath)) return;
228
+
229
+ let content = await fsPromises.readFile(contextXmlPath, "utf-8");
230
+
231
+ // Adiciona atributos para hot-reload se não existirem
232
+ if (!content.includes("reloadable")) {
233
+ content = content.replace(
234
+ /<Context>/,
235
+ '<Context reloadable="true" autoDeploy="true" deployOnStartup="true">',
236
+ );
237
+ }
238
+
239
+ await fsPromises.writeFile(contextXmlPath, content, "utf-8");
240
+ }
241
+
242
+ /**
243
+ * Cria contexto para a aplicação
244
+ */
245
+ async createAppContext(): Promise<void> {
246
+ const webappsDir = path.join(this.tomcatHome, "webapps");
247
+
248
+ // Limpa webapps padrão
249
+ const defaultApps = ["docs", "examples", "host-manager", "manager", "ROOT"];
250
+ for (const app of defaultApps) {
251
+ const appPath = path.join(webappsDir, app);
252
+ if (existsSync(appPath)) {
253
+ await fsPromises.rm(appPath, { recursive: true, force: true });
254
+ }
255
+ }
256
+
257
+ // Cria diretório para a aplicação
258
+ const appName =
259
+ this.contextPath === "/" ? "ROOT" : this.contextPath.replace(/^\//, "");
260
+ const appDir = path.join(webappsDir, appName);
261
+
262
+ if (existsSync(appDir)) {
263
+ await fsPromises.rm(appDir, { recursive: true, force: true });
264
+ }
265
+
266
+ // Se webappPath é um diretório, cria link/simula deploy
267
+ if (existsSync(this.webappPath)) {
268
+ // Em Windows, vamos copiar inicialmente (symlink requer privilégios)
269
+ // Ou criar um context XML apontando para o diretório
270
+ await this.createContextXml(appName);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Cria arquivo context XML para apontar para diretório externo
276
+ */
277
+ private async createContextXml(appName: string): Promise<void> {
278
+ const confDir = path.join(this.tomcatHome, "conf", "Catalina", "localhost");
279
+
280
+ if (!existsSync(confDir)) {
281
+ mkdirSync(confDir, { recursive: true });
282
+ }
283
+
284
+ const contextFile = path.join(confDir, `${appName}.xml`);
285
+ const content = `<?xml version="1.0" encoding="UTF-8"?>
243
286
  <Context
244
287
  docBase="${this.webappPath.replace(/\\/g, "/")}"
245
288
  reloadable="true"
@@ -248,144 +291,146 @@ export class EmbeddedTomcatService {
248
291
  antiJARLocking="false">
249
292
  </Context>`;
250
293
 
251
- writeFileSync(contextFile, content);
252
- Logger.debug(`Context criado: ${contextFile}`);
253
- }
254
-
255
- /**
256
- * Verifica se porta está disponível
257
- */
258
- async isPortAvailable(): Promise<boolean> {
259
- return new Promise((resolve) => {
260
- const netstat = spawn("cmd", ["/c", `netstat -ano | findstr :${this.port}`]);
261
- let output = "";
262
-
263
- netstat.stdout?.on("data", (data) => {
264
- output += data.toString();
265
- });
266
-
267
- netstat.on("close", () => {
268
- resolve(output.trim().length === 0);
269
- });
270
-
271
- netstat.on("error", () => {
272
- resolve(true); // Assume disponível se não conseguir verificar
273
- });
274
- });
275
- }
276
-
277
- /**
278
- * Encontra próxima porta disponível
279
- */
280
- async findAvailablePort(startPort: number = 8080): Promise<number> {
281
- let port = startPort;
282
- while (!(await this.isPortAvailable())) {
283
- port++;
284
- if (port > 65535) {
285
- throw new Error("Nenhuma porta disponível encontrada");
286
- }
287
- }
288
- this.port = port;
289
- return port;
290
- }
291
-
292
- /**
293
- * Retorna variáveis de ambiente para o Tomcat
294
- */
295
- getEnvironment(): Record<string, string> {
296
- return {
297
- CATALINA_HOME: this.tomcatHome,
298
- CATALINA_BASE: this.tomcatHome,
299
- CATALINA_OPTS: process.env.CATALINA_OPTS || ""
300
- };
301
- }
302
-
303
- /**
304
- * Lista versões disponíveis
305
- */
306
- static getAvailableVersions(): string[] {
307
- return Object.keys(EmbeddedTomcatService.VERSIONS);
308
- }
309
-
310
- /**
311
- * Download com progresso
312
- */
313
- private async downloadFile(url: string, destPath: string): Promise<void> {
314
- const spinner = Logger.spinner(`Baixando Tomcat ${this.version}...`);
315
-
316
- try {
317
- const response = await fetch(url);
318
-
319
- if (!response.ok) {
320
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
321
- }
322
-
323
- const totalSize = parseInt(response.headers.get("content-length") || "0");
324
- const buffer = await response.arrayBuffer();
325
-
326
- writeFileSync(destPath, Buffer.from(buffer));
327
-
328
- spinner(true);
329
-
330
- const sizeMB = (buffer.byteLength / 1024 / 1024).toFixed(1);
331
- Logger.info("Download", `${sizeMB} MB baixados`);
332
-
333
- } catch (error) {
334
- spinner(false);
335
- throw error;
336
- }
337
- }
338
-
339
- /**
340
- * Extrai arquivo ZIP usando PowerShell
341
- */
342
- private async extractZip(zipPath: string, destDir: string): Promise<void> {
343
- const spinner = Logger.spinner("Extraindo arquivos...");
344
-
345
- return new Promise((resolve, reject) => {
346
- const ps = spawn("powershell", [
347
- "-command",
348
- `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`
349
- ]);
350
-
351
- ps.on("close", (code) => {
352
- if (code === 0) {
353
- spinner(true);
354
- resolve();
355
- } else {
356
- spinner(false);
357
- reject(new Error(`Falha ao extrair (código ${code})`));
358
- }
359
- });
360
-
361
- ps.on("error", (err) => {
362
- spinner(false);
363
- reject(err);
364
- });
365
- });
366
- }
367
-
368
- /**
369
- * Remove instalação
370
- */
371
- async uninstall(): Promise<void> {
372
- if (existsSync(this.tomcatHome)) {
373
- await fs.rm(this.tomcatHome, { recursive: true, force: true });
374
- Logger.info("Tomcat", `Versão ${this.version} removida`);
375
- }
376
- }
377
-
378
- /**
379
- * Retorna informações da instalação
380
- */
381
- getInfo(): Record<string, string> {
382
- return {
383
- version: this.version,
384
- home: this.tomcatHome,
385
- port: String(this.port),
386
- installed: this.isInstalled ? "sim" : "não",
387
- webapp: this.webappPath,
388
- context: this.contextPath
389
- };
390
- }
294
+ writeFileSync(contextFile, content);
295
+ Logger.debug(`Context criado: ${contextFile}`);
296
+ }
297
+
298
+ /**
299
+ * Verifica se porta está disponível
300
+ */
301
+ async isPortAvailable(): Promise<boolean> {
302
+ return new Promise((resolve) => {
303
+ const netstat = spawn("cmd", [
304
+ "/c",
305
+ `netstat -ano | findstr :${this.port}`,
306
+ ]);
307
+ let output = "";
308
+
309
+ netstat.stdout?.on("data", (data) => {
310
+ output += data.toString();
311
+ });
312
+
313
+ netstat.on("close", () => {
314
+ resolve(output.trim().length === 0);
315
+ });
316
+
317
+ netstat.on("error", () => {
318
+ resolve(true); // Assume disponível se não conseguir verificar
319
+ });
320
+ });
321
+ }
322
+
323
+ /**
324
+ * Encontra próxima porta disponível
325
+ */
326
+ async findAvailablePort(startPort: number = 8080): Promise<number> {
327
+ let port = startPort;
328
+ while (!(await this.isPortAvailable())) {
329
+ port++;
330
+ if (port > 65535) {
331
+ throw new Error("Nenhuma porta disponível encontrada");
332
+ }
333
+ }
334
+ this.port = port;
335
+ return port;
336
+ }
337
+
338
+ /**
339
+ * Retorna variáveis de ambiente para o Tomcat
340
+ */
341
+ getEnvironment(): Record<string, string> {
342
+ return {
343
+ CATALINA_HOME: this.tomcatHome,
344
+ CATALINA_BASE: this.tomcatHome,
345
+ CATALINA_OPTS: process.env.CATALINA_OPTS || "",
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Lista versões disponíveis
351
+ */
352
+ static getAvailableVersions(): string[] {
353
+ return Object.keys(EmbeddedTomcatService.VERSIONS);
354
+ }
355
+
356
+ /**
357
+ * Download com progresso
358
+ */
359
+ private async downloadFile(url: string, destPath: string): Promise<void> {
360
+ const spinner = Logger.spinner(`Baixando Tomcat ${this.version}...`);
361
+
362
+ try {
363
+ const response = await fetch(url);
364
+
365
+ if (!response.ok) {
366
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
367
+ }
368
+
369
+ const totalSize = parseInt(response.headers.get("content-length") || "0");
370
+ const buffer = await response.arrayBuffer();
371
+
372
+ writeFileSync(destPath, Buffer.from(buffer));
373
+
374
+ spinner(true);
375
+
376
+ const sizeMB = (buffer.byteLength / 1024 / 1024).toFixed(1);
377
+ Logger.info("Download", `${sizeMB} MB baixados`);
378
+ } catch (error) {
379
+ spinner(false);
380
+ throw error;
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Extrai arquivo ZIP usando PowerShell
386
+ */
387
+ private async extractZip(zipPath: string, destDir: string): Promise<void> {
388
+ const spinner = Logger.spinner("Extraindo arquivos...");
389
+
390
+ return new Promise((resolve, reject) => {
391
+ const ps = spawn("powershell", [
392
+ "-command",
393
+ `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`,
394
+ ]);
395
+
396
+ ps.on("close", (code) => {
397
+ if (code === 0) {
398
+ spinner(true);
399
+ resolve();
400
+ } else {
401
+ spinner(false);
402
+ reject(new Error(`Falha ao extrair (código ${code})`));
403
+ }
404
+ });
405
+
406
+ ps.on("error", (err) => {
407
+ spinner(false);
408
+ reject(err);
409
+ });
410
+ });
411
+ }
412
+
413
+ /**
414
+ * Remove instalação
415
+ */
416
+ async uninstall(): Promise<void> {
417
+ if (existsSync(this.tomcatHome)) {
418
+ await fsPromises.rm(this.tomcatHome, { recursive: true, force: true });
419
+ Logger.info("Tomcat", `Versão ${this.version} removida`);
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Retorna informações da instalação
425
+ */
426
+ getInfo(): Record<string, string> {
427
+ return {
428
+ version: this.version,
429
+ home: this.tomcatHome,
430
+ port: String(this.port),
431
+ installed: this.isInstalled ? "sim" : "não",
432
+ webapp: this.webappPath,
433
+ context: this.contextPath,
434
+ };
435
+ }
391
436
  }
@@ -79,7 +79,9 @@ export class ConfigManager {
79
79
  // Verificar se usar Tomcat embutido
80
80
  let tomcatPath = String(cliValues.path || xavvaJson.path || envTomcatPath || "");
81
81
  let useEmbedded = false;
82
- let embeddedVersion = String(cliValues["tomcat-version"] || xavvaJson.version || "10.1.52");
82
+ // Versão pode vir de: CLI flag > xavva.json tomcat.version > xavva.json version (legado) > padrão
83
+ const xavvaTomcatVersion = (xavvaJson as any).tomcat?.version;
84
+ let embeddedVersion = String(cliValues["tomcat-version"] || xavvaTomcatVersion || xavvaJson.version || "10.1.52");
83
85
 
84
86
  // Se não há Tomcat configurado ou não existe no path, usar embutido
85
87
  if (!tomcatPath || (!fs.existsSync(path.join(tomcatPath, "bin", "catalina.bat")) && isStart)) {