@archznn/xavva 2.2.2 → 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.2.2-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.
@@ -18,6 +18,8 @@ Xavva is a high-performance CLI built with **Bun** that transforms the Java/Tomc
18
18
  - 📦 **Dependency Analysis** — Detect conflicts and outdated dependencies
19
19
  - 🎯 **Maven & Gradle** — Native support for both build tools
20
20
  - 🔧 **Auto-Healing** — Automatic diagnosis and repair of common issues
21
+ - 🐱 **Embedded Tomcat** — Auto-install Tomcat, no manual setup needed
22
+ - 📦 **WAR Generation** — Build as .war file or exploded directory
21
23
 
22
24
  ---
23
25
 
@@ -42,6 +44,9 @@ xavva dev --tui
42
44
  # Deploy to Tomcat
43
45
  xavva deploy
44
46
 
47
+ # Build and deploy as .war file
48
+ xavva deploy --war
49
+
45
50
  # Analyze dependencies for issues
46
51
  xavva deps
47
52
 
@@ -50,6 +55,9 @@ xavva deps --update-safe
50
55
 
51
56
  # Check for security vulnerabilities
52
57
  xavva audit
58
+
59
+ # Use embedded Tomcat (auto-install)
60
+ xavva dev --yes
53
61
  ```
54
62
 
55
63
  ---
@@ -82,6 +90,39 @@ xavva audit
82
90
  | `xavva doctor` | Diagnose environment issues (JAVA_HOME, DCEVM) |
83
91
  | `xavva profiles` | List available Maven/Gradle profiles |
84
92
  | `xavva docs` | Generate endpoint documentation |
93
+ | `xavva tomcat` | Manage embedded Tomcat installations |
94
+
95
+ ---
96
+
97
+ ## 🐱 Embedded Tomcat
98
+
99
+ Xavva can automatically download and manage a Tomcat installation for you:
100
+
101
+ ```bash
102
+ # First time usage - auto-install Tomcat
103
+ xavva dev --yes
104
+
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
116
+
117
+ # Check Tomcat status
118
+ xavva tomcat status
119
+
120
+ # Remove a version
121
+ xavva tomcat uninstall 9.0.115
122
+
123
+ # Or use with flags
124
+ xavva dev --tomcat-version 9.0.115
125
+ ```
85
126
 
86
127
  ---
87
128
 
@@ -176,6 +217,9 @@ Create `xavva.json` in your project root:
176
217
  | `-d, --debug` | Enable JPDA debugger |
177
218
  | `-c, --clean` | Clean logs before start |
178
219
  | `-s, --no-build` | Skip initial build |
220
+ | `-W, --war` | Generate .war file (vs exploded)|
221
+ | `--cache` | Use build cache (faster) |
222
+ | `-y, --yes` | Auto-install Tomcat (no prompt) |
179
223
  | `-V, --verbose` | Detailed output |
180
224
 
181
225
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archznn/xavva",
3
- "version": "2.2.2",
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}`);
@@ -34,7 +34,7 @@ export class DepsCommand implements Command {
34
34
  Logger.log(" • Arquivo pom.xml/build.gradle não encontrado");
35
35
  Logger.log(" • Erro de parsing no arquivo de configuração");
36
36
  Logger.newline();
37
- Logger.log(`${Logger.C.cyan}Dica:${Logger.C.reset} Execute com --verbose para mais detalhes`);
37
+ Logger.log(`${Logger.C.primary}Dica:${Logger.C.reset} Execute com --verbose para mais detalhes`);
38
38
  return;
39
39
  }
40
40
 
@@ -87,7 +87,7 @@ export class DepsCommand implements Command {
87
87
  Logger.section("Sugestões de Correção");
88
88
 
89
89
  for (const conflict of result.conflicts) {
90
- Logger.log(`\n${Logger.C.cyan}${conflict.groupId}:${conflict.artifactId}${Logger.C.reset}`);
90
+ Logger.log(`\n${Logger.C.primary}${conflict.groupId}:${conflict.artifactId}${Logger.C.reset}`);
91
91
 
92
92
  if (buildTool === "maven") {
93
93
  Logger.log(" Adicione ao pom.xml:");
@@ -143,7 +143,7 @@ export class DepsCommand implements Command {
143
143
 
144
144
  Logger.newline();
145
145
  Logger.log(`${Logger.C.warning}⚠️ Execute 'xavva build' para compilar e aplicar as mudanças${Logger.C.reset}`);
146
- Logger.log(`${Logger.C.cyan}💡 Dica:${Logger.C.reset} Execute 'xavva audit' para verificar vulnerabilidades nas novas versões`);
146
+ Logger.log(`${Logger.C.primary}💡 Dica:${Logger.C.reset} Execute 'xavva audit' para verificar vulnerabilidades nas novas versões`);
147
147
  } else {
148
148
  Logger.warn("Nenhuma dependência foi atualizada");
149
149
  }
@@ -31,7 +31,7 @@ export class DoctorCommand implements Command {
31
31
  await this.installDCEVM();
32
32
  } else {
33
33
  Logger.log(
34
- ` ${Logger.C.cyan}Use 'xavva doctor --fix' para baixar uma JDK com DCEVM integrado.${Logger.C.reset}`,
34
+ ` ${Logger.C.primary}Use 'xavva doctor --fix' para baixar uma JDK com DCEVM integrado.${Logger.C.reset}`,
35
35
  );
36
36
  }
37
37
  }
@@ -28,9 +28,10 @@ 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, installed, use, status)
31
32
  ${this.c("green", "docs")} Generate endpoint documentation
32
33
 
33
- ${this.c("yellow", "OPTIONS")}
34
+ ${this.c("yellow", "GENERAL OPTIONS")}
34
35
  ${this.c("cyan", "-p, --path")} <path> Tomcat installation path
35
36
  ${this.c("cyan", "-t, --tool")} <tool> Build tool: maven | gradle
36
37
  ${this.c("cyan", "-n, --name")} <name> Application name (WAR context)
@@ -43,32 +44,59 @@ export class HelpCommand implements Command {
43
44
  ${this.c("cyan", "-d, --debug")} Enable JPDA debugger
44
45
  ${this.c("cyan", "--dp")} <port> Debugger port (default: 5005)
45
46
 
46
- ${this.c("cyan", "-c, --clean")} Clean logs before start
47
+ ${this.c("cyan", "-c, --clean")} Clean before build
47
48
  ${this.c("cyan", "-s, --no-build")} Skip initial build
48
49
  ${this.c("cyan", "-q, --quiet")} Minimal output
49
50
  ${this.c("cyan", "-V, --verbose")} Detailed output
50
51
  ${this.c("cyan", "-h, --help")} Show this help
51
52
  ${this.c("cyan", "-v, --version")} Show version
52
53
 
54
+ ${this.c("yellow", "BUILD OPTIONS")} ${this.c("dim", "(for deploy, dev, build)")}
55
+ ${this.c("cyan", "-W, --war")} Generate .war file instead of exploded directory
56
+ ${this.c("cyan", "--cache")} Use build cache (skip if no changes)
57
+
58
+ ${this.c("yellow", "TOMCAT OPTIONS")} ${this.c("dim", "(for embedded Tomcat)")}
59
+ ${this.c("cyan", "--tomcat-version")} <v> Tomcat version to install (default: 10.1.52)
60
+ ${this.c("cyan", "-y, --yes")} Auto-install without confirmation
61
+
62
+ ${this.c("yellow", "DEPS OPTIONS")} ${this.c("dim", "(for xavva deps)")}
63
+ ${this.c("cyan", "--update-safe")} Update only non-breaking dependencies
64
+ ${this.c("cyan", "--fix")} Show fix suggestions for conflicts
65
+ ${this.c("cyan", "--strict")} Fail on critical conflicts (for CI/CD)
66
+ ${this.c("cyan", "-o, --output")} <file> Export report as JSON
67
+
53
68
  ${this.c("yellow", "EXAMPLES")}
54
69
  ${this.c("dim", "# Development with hot reload and dashboard")}
55
70
  xavva dev --tui --watch
56
71
 
57
- ${this.c("dim", "# Quick deploy to specific Tomcat")}
72
+ ${this.c("dim", "# Deploy to specific Tomcat installation")}
58
73
  xavva deploy -p /opt/tomcat --port 8081
59
74
 
75
+ ${this.c("dim", "# Build and deploy as .war file")}
76
+ xavva deploy --war
77
+
60
78
  ${this.c("dim", "# Run a class with debugging")}
61
79
  xavva debug com.example.MyClass
62
80
 
63
- ${this.c("dim", "# Analyze dependencies for conflicts")}
64
- xavva deps --verbose
81
+ ${this.c("dim", "# Use embedded Tomcat (auto-install)")}
82
+ xavva dev --yes
83
+ xavva dev --tomcat-version 9.0.115
65
84
 
66
- ${this.c("dim", "# Update safe dependencies (non-breaking only)")}
85
+ ${this.c("dim", "# Analyze and update dependencies")}
86
+ xavva deps --verbose
67
87
  xavva deps --update-safe
68
88
 
69
- ${this.c("dim", "# Security audit with auto-fix suggestions")}
89
+ ${this.c("dim", "# Security audit with auto-fix")}
70
90
  xavva audit --fix
71
91
 
92
+ ${this.c("dim", "# Manage embedded Tomcat")}
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
97
+ xavva tomcat status
98
+ xavva tomcat uninstall 9.0.115
99
+
72
100
  ${this.c("yellow", "CONFIGURATION")}
73
101
  Settings are loaded from ${this.c("cyan", "xavva.json")} in the project root:
74
102
 
@@ -20,7 +20,7 @@ export class ProfilesCommand implements Command {
20
20
  }
21
21
 
22
22
  Logger.log(`
23
- ${Logger.C.cyan}Perfis detectados:${Logger.C.reset}`);
23
+ ${Logger.C.primary}Perfis detectados:${Logger.C.reset}`);
24
24
  profiles.forEach(p => {
25
25
  const active = config.project.profile === p ? ` ${Logger.C.green}(Ativo)${Logger.C.reset}` : "";
26
26
  Logger.log(` ${Logger.C.bold}➜${Logger.C.reset} ${p}${active}`);
@@ -54,7 +54,7 @@ export class RunCommand implements Command {
54
54
 
55
55
  if (isDebug) {
56
56
  Logger.warn(`🚀 Aguardando debugger na porta 5005 para ${className}...`);
57
- Logger.log(`${Logger.C.cyan}Dica:${Logger.C.reset} No VS Code ou IntelliJ, use 'Attach to Remote JVM' na porta 5005.`);
57
+ Logger.log(`${Logger.C.primary}Dica:${Logger.C.reset} No VS Code ou IntelliJ, use 'Attach to Remote JVM' na porta 5005.`);
58
58
  Logger.newline();
59
59
  } else {
60
60
  Logger.warn(`🚀 Executando ${className}...`);
@@ -0,0 +1,209 @@
1
+ import type { Command } from "./Command";
2
+ import type { AppConfig, CLIArguments } from "../types/config";
3
+ import { EmbeddedTomcatService } from "../services/EmbeddedTomcatService";
4
+ import { Logger } from "../utils/ui";
5
+ import path from "path";
6
+ import fs from "fs";
7
+
8
+ export class TomcatCommand implements Command {
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) : [];
18
+
19
+ switch (action) {
20
+ case "install":
21
+ await this.handleInstall(config, args, extraArgs);
22
+ break;
23
+ case "list":
24
+ this.handleList();
25
+ break;
26
+ case "installed":
27
+ this.handleInstalled();
28
+ break;
29
+ case "use":
30
+ await this.handleUse(config, args, extraArgs);
31
+ break;
32
+ case "uninstall":
33
+ await this.handleUninstall(config, args, extraArgs);
34
+ break;
35
+ case "status":
36
+ await this.handleStatus(config);
37
+ break;
38
+ default:
39
+ Logger.error(`Ação desconhecida: ${action}`);
40
+ Logger.info("Ações disponíveis", "install, list, installed, use, uninstall, status");
41
+ }
42
+ }
43
+
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";
47
+
48
+ // Detectar webapp path
49
+ const webappPath = config.project.buildTool === "maven"
50
+ ? path.join(process.cwd(), "src", "main", "webapp")
51
+ : path.join(process.cwd(), "src", "main", "webapp");
52
+
53
+ const service = new EmbeddedTomcatService({
54
+ version,
55
+ port: config.tomcat.port,
56
+ webappPath
57
+ });
58
+
59
+ if (service.checkInstallation()) {
60
+ Logger.info("Tomcat", `Versão ${version} já está instalada`);
61
+ const info = service.getInfo();
62
+ Logger.config("Local", info.home);
63
+ return;
64
+ }
65
+
66
+ const installed = await service.install();
67
+ if (installed) {
68
+ Logger.success(`Tomcat ${version} instalado com sucesso!`);
69
+ } else {
70
+ Logger.error("Falha na instalação");
71
+ }
72
+ }
73
+
74
+ private handleList(): void {
75
+ Logger.section("Versões Disponíveis para Download");
76
+ const versions = EmbeddedTomcatService.getAvailableVersions();
77
+
78
+ for (const version of versions) {
79
+ Logger.log(` ${Logger.C.primary}•${Logger.C.reset} ${version}`);
80
+ }
81
+
82
+ Logger.newline();
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));
163
+ }
164
+
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";
168
+
169
+ const service = new EmbeddedTomcatService({
170
+ version,
171
+ port: config.tomcat.port,
172
+ webappPath: "."
173
+ });
174
+
175
+ if (!service.checkInstallation()) {
176
+ Logger.warn(`Tomcat ${version} não está instalado`);
177
+ return;
178
+ }
179
+
180
+ Logger.step(`Removendo Tomcat ${version}...`);
181
+ await service.uninstall();
182
+ Logger.success("Removido com sucesso!");
183
+ }
184
+
185
+ private async handleStatus(config: AppConfig): Promise<void> {
186
+ Logger.section("Status do Tomcat");
187
+
188
+ if (config.tomcat.embedded) {
189
+ Logger.config("Modo", "Embutido");
190
+ Logger.config("Versão", config.tomcat.version || "10.1.52");
191
+ Logger.config("Porta", String(config.tomcat.port));
192
+ Logger.config("Home", config.tomcat.path);
193
+ } else {
194
+ Logger.config("Modo", "Externo");
195
+ Logger.config("CATALINA_HOME", config.tomcat.path);
196
+ Logger.config("Porta", String(config.tomcat.port));
197
+ }
198
+
199
+ // Verificar se está rodando
200
+ const netstat = Bun.spawnSync(["cmd", "/c", `netstat -ano | findstr :${config.tomcat.port}`]);
201
+ const output = await new Response(netstat.stdout).text();
202
+
203
+ if (output.trim()) {
204
+ Logger.config("Status", `${Logger.C.success}Rodando${Logger.C.reset}`);
205
+ } else {
206
+ Logger.config("Status", `${Logger.C.warning}Parado${Logger.C.reset}`);
207
+ }
208
+ }
209
+ }
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import { DocsCommand } from "./commands/DocsCommand";
12
12
  import { AuditCommand } from "./commands/AuditCommand";
13
13
  import { ProfilesCommand } from "./commands/ProfilesCommand";
14
14
  import { DepsCommand } from "./commands/DepsCommand";
15
+ import { TomcatCommand } from "./commands/TomcatCommand";
15
16
 
16
17
  import { ProjectService } from "./services/ProjectService";
17
18
  import { TomcatService } from "./services/TomcatService";
@@ -36,7 +37,7 @@ async function main() {
36
37
  await processManager.shutdown(0);
37
38
  }
38
39
 
39
- const commandNames = ["deploy", "build", "start", "dev", "doctor", "run", "debug", "logs", "docs", "audit", "profiles", "deps"];
40
+ const commandNames = ["deploy", "build", "start", "dev", "doctor", "run", "debug", "logs", "docs", "audit", "profiles", "deps", "tomcat"];
40
41
  const commandName = positionals.find(p => commandNames.includes(p)) || "deploy";
41
42
 
42
43
  if (!values.help && !values.tui) {
@@ -44,6 +45,9 @@ async function main() {
44
45
  if (config.project.encoding) {
45
46
  Logger.config("Encoding", config.project.encoding);
46
47
  }
48
+ if (config.tomcat.embedded) {
49
+ Logger.config("Tomcat", `Embutido ${config.tomcat.version}`);
50
+ }
47
51
  }
48
52
 
49
53
  if (values.help) {
@@ -80,6 +84,7 @@ async function main() {
80
84
  registry.register("audit", new AuditCommand(auditService));
81
85
  registry.register("profiles", new ProfilesCommand(projectService));
82
86
  registry.register("deps", new DepsCommand());
87
+ registry.register("tomcat", new TomcatCommand());
83
88
  registry.register("deploy", deployCmd);
84
89
  registry.register("dev", deployCmd);
85
90
 
@@ -100,7 +105,7 @@ async function main() {
100
105
  if (commandName === "debug") values.debug = true;
101
106
  if (commandName === "run") values.debug = false;
102
107
 
103
- await registry.execute(commandName, config, values);
108
+ await registry.execute(commandName, config, values, positionals);
104
109
  }
105
110
  }
106
111
 
@@ -30,7 +30,10 @@ export class BuildService {
30
30
  this.cache.clearCache();
31
31
  }
32
32
 
33
- if (!incremental && !this.projectConfig.skipBuild) {
33
+ // Cache é usado se --cache for passado ou em modo incremental
34
+ const useCache = this.projectConfig.cache || incremental;
35
+
36
+ if (useCache && !incremental && !this.projectConfig.skipBuild) {
34
37
  if (!this.projectConfig.clean && !this.cache.shouldRebuild(this.projectConfig.buildTool, this.projectService)) {
35
38
  Logger.success("Build cache hit! Skipping full build.");
36
39
  return;
@@ -43,8 +46,8 @@ export class BuildService {
43
46
  if (this.projectConfig.buildTool === 'maven') {
44
47
  command.push(process.platform === "win32" ? "mvn.cmd" : "mvn");
45
48
 
46
- // Smart Offline: Se o pom.xml não mudou e é incremental ou rebuild forçado (mas cache existe), usa -o
47
- if (!this.cache.shouldRebuild('maven', this.projectService)) {
49
+ // Smart Offline: usa -o se cache estiver habilitado e pom não mudou
50
+ if (useCache && !this.cache.shouldRebuild('maven', this.projectService)) {
48
51
  command.push("-o");
49
52
  }
50
53
 
@@ -52,7 +55,12 @@ export class BuildService {
52
55
  command.push("compile");
53
56
  } else {
54
57
  if (this.projectConfig.clean) command.push("clean");
55
- command.push("compile", "war:exploded");
58
+ // Use 'package' para gerar .war ou 'war:exploded' para pasta
59
+ if (this.projectConfig.war) {
60
+ command.push("package");
61
+ } else {
62
+ command.push("compile", "war:exploded");
63
+ }
56
64
  command.push("-T", "1C");
57
65
  }
58
66
  command.push("-Dmaven.test.skip=true", "-Dmaven.javadoc.skip=true");
@@ -417,6 +417,14 @@ export class DependencyAnalyzerService {
417
417
 
418
418
  private async checkSingleUpdate(dep: Dependency): Promise<DependencyUpdate | null> {
419
419
  try {
420
+ // Primeiro verifica se a versão atual existe no Maven Central
421
+ // Se não existir, é uma dependência local - não sugerir atualização
422
+ const currentExists = await this.checkVersionExists(dep.groupId, dep.artifactId, dep.version);
423
+ if (!currentExists) {
424
+ Logger.debug(`Dependência local detectada (não no Maven Central): ${dep.groupId}:${dep.artifactId}:${dep.version}`);
425
+ return null;
426
+ }
427
+
420
428
  const latest = await this.fetchLatestVersion(dep.groupId, dep.artifactId);
421
429
  if (latest && this.isNewer(latest, dep.version)) {
422
430
  return {
@@ -433,6 +441,18 @@ export class DependencyAnalyzerService {
433
441
  return null;
434
442
  }
435
443
 
444
+ private async checkVersionExists(groupId: string, artifactId: string, version: string): Promise<boolean> {
445
+ try {
446
+ const url = `https://search.maven.org/solrsearch/select?q=g:"${groupId}"+AND+a:"${artifactId}"+AND+v:"${version}"&rows=1&wt=json`;
447
+ const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
448
+ const data = await response.json();
449
+ return data.response?.numFound > 0;
450
+ } catch (e) {
451
+ // Se não conseguir verificar, assume que existe para não bloquear
452
+ return true;
453
+ }
454
+ }
455
+
436
456
  private async fetchLatestVersion(groupId: string, artifactId: string): Promise<string | null> {
437
457
  // Usar Maven Central API
438
458
  try {
@@ -505,13 +525,17 @@ export class DependencyAnalyzerService {
505
525
  const currentVersion = update.currentVersion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
506
526
 
507
527
  // Regex para encontrar <version> dentro do bloco da dependência
528
+ // Captura: grupo 1 = tudo antes da versão incluindo <version>
529
+ // grupo 2 = a versão atual
530
+ // grupo 3 = </version> e resto
508
531
  const depPattern = new RegExp(
509
- `(<dependency>\\s*<groupId>${groupId}</groupId>\\s*<artifactId>${artifactId}</artifactId>(?:\\s*<version>)${currentVersion}(</version>))`,
532
+ `(<dependency>\\s*<groupId>${groupId}</groupId>\\s*<artifactId>${artifactId}</artifactId>\\s*<version>)(${currentVersion})(</version>)`,
510
533
  'g'
511
534
  );
512
535
 
513
536
  if (depPattern.test(content)) {
514
- content = content.replace(depPattern, `$1${update.latestVersion}$2`);
537
+ // Substitui apenas o grupo 2 (a versão), mantendo as tags
538
+ content = content.replace(depPattern, `$1${update.latestVersion}$3`);
515
539
  result.updated++;
516
540
  modified = true;
517
541
  } else {
@@ -0,0 +1,436 @@
1
+ import { Logger } from "../utils/ui";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ createWriteStream,
6
+ writeFileSync,
7
+ readdirSync,
8
+ promises as fsPromises,
9
+ } from "fs";
10
+ import path from "path";
11
+ import os from "os";
12
+ import { spawn } from "child_process";
13
+
14
+ export interface EmbeddedTomcatOptions {
15
+ version?: string;
16
+ port?: number;
17
+ webappPath: string;
18
+ contextPath?: string;
19
+ }
20
+
21
+ interface DownloadProgress {
22
+ downloaded: number;
23
+ total: number;
24
+ percent: number;
25
+ }
26
+
27
+ export class EmbeddedTomcatService {
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"?>
286
+ <Context
287
+ docBase="${this.webappPath.replace(/\\/g, "/")}"
288
+ reloadable="true"
289
+ crossContext="true"
290
+ antiResourceLocking="false"
291
+ antiJARLocking="false">
292
+ </Context>`;
293
+
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
+ }
436
+ }
@@ -98,7 +98,7 @@ export class LogAnalyzer {
98
98
  return `${Logger.C.magenta}👀 ${Logger.C.bold}Hotswap:${Logger.C.reset} ${msg.replace(/Class '.*?'/, (m) => Logger.C.bold + m + Logger.C.reset)}`;
99
99
  }
100
100
 
101
- let color = Logger.C.cyan;
101
+ let color = Logger.C.primary;
102
102
  let symbol = "●";
103
103
  if (level === "WARN") { color = Logger.C.yellow; symbol = "▲"; }
104
104
  else if (level === "ERROR") { color = Logger.C.red; symbol = "✖"; }
@@ -28,7 +28,12 @@ export class ProjectService {
28
28
  const artifacts = this.searchArtifacts(buildDir).sort((a, b) => b.time - a.time);
29
29
 
30
30
  if (artifacts.length === 0) {
31
- throw new Error(`Nenhum artefato (.war ou pasta exploded) encontrado em ${buildDir}!`);
31
+ // Debug: listar o que existe no diretório target
32
+ let debugInfo = `\nDiretório ${buildDir} existe: ${existsSync(buildDir)}`;
33
+ if (existsSync(buildDir)) {
34
+ debugInfo += `\nConteúdo: ${readdirSync(buildDir).join(', ')}`;
35
+ }
36
+ throw new Error(`Nenhum artefato (.war ou pasta exploded) encontrado em ${buildDir}!${debugInfo}`);
32
37
  }
33
38
 
34
39
  const artifact = artifacts[0];
@@ -3,6 +3,8 @@ export interface TomcatConfig {
3
3
  port: number;
4
4
  webapps: string;
5
5
  grep?: string;
6
+ embedded?: boolean;
7
+ version?: string;
6
8
  }
7
9
 
8
10
  export interface ProjectConfig {
@@ -20,6 +22,8 @@ export interface ProjectConfig {
20
22
  grep?: string;
21
23
  tui: boolean;
22
24
  encoding?: string;
25
+ war?: boolean;
26
+ cache?: boolean;
23
27
  }
24
28
 
25
29
  export interface AppConfig {
@@ -50,6 +54,13 @@ export interface CLIArguments {
50
54
  tui?: boolean;
51
55
  output?: string;
52
56
  strict?: boolean;
57
+ "tomcat-version"?: string;
58
+ "tomcat-action"?: string;
59
+ "update-safe"?: boolean;
60
+ "updateSafe"?: boolean;
61
+ yes?: boolean;
62
+ war?: boolean;
63
+ cache?: boolean;
53
64
  }
54
65
 
55
66
  export interface CommandContext {
@@ -3,6 +3,8 @@ import path from "path";
3
3
  import fs from "fs";
4
4
  import { DEFAULT_TOMCAT_PORT, DEFAULT_DEBUG_PORT } from "./constants";
5
5
  import type { AppConfig, CLIArguments, CommandContext } from "../types/config";
6
+ import { EmbeddedTomcatService } from "../services/EmbeddedTomcatService";
7
+ import { Logger } from "./ui";
6
8
 
7
9
  export class ConfigManager {
8
10
  static async load(): Promise<CommandContext> {
@@ -32,6 +34,10 @@ export class ConfigManager {
32
34
  tui: { type: "boolean" },
33
35
  output: { type: "string", short: "o" },
34
36
  strict: { type: "boolean" },
37
+ "tomcat-version": { type: "string" },
38
+ yes: { type: "boolean", short: "y" },
39
+ war: { type: "boolean", short: "W" },
40
+ cache: { type: "boolean" },
35
41
  },
36
42
  strict: false,
37
43
  allowPositionals: true,
@@ -52,8 +58,9 @@ export class ConfigManager {
52
58
 
53
59
  const isDev = positionals.includes("dev");
54
60
  const isRun = positionals.includes("run") || positionals.includes("debug");
61
+ const isStart = positionals.includes("start") || positionals.includes("deploy") || isDev;
55
62
 
56
- const envTomcatPath = process.env.TOMCAT_HOME || process.env.CATALINA_HOME || "C:\\apache-tomcat";
63
+ const envTomcatPath = process.env.TOMCAT_HOME || process.env.CATALINA_HOME;
57
64
  const detectedTool = this.detectBuildTool();
58
65
 
59
66
  let runClass = "";
@@ -64,12 +71,79 @@ export class ConfigManager {
64
71
  runClass = positionals[idx + 1] || "";
65
72
  }
66
73
 
74
+ // Detectar webapp path baseado no build tool
75
+ const webappPath = detectedTool === "maven"
76
+ ? path.join(process.cwd(), "src", "main", "webapp")
77
+ : path.join(process.cwd(), "src", "main", "webapp");
78
+
79
+ // Verificar se usar Tomcat embutido
80
+ let tomcatPath = String(cliValues.path || xavvaJson.path || envTomcatPath || "");
81
+ let useEmbedded = false;
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");
85
+
86
+ // Se não há Tomcat configurado ou não existe no path, usar embutido
87
+ if (!tomcatPath || (!fs.existsSync(path.join(tomcatPath, "bin", "catalina.bat")) && isStart)) {
88
+ useEmbedded = true;
89
+ const embeddedService = new EmbeddedTomcatService({
90
+ version: embeddedVersion,
91
+ port: parseInt(String(cliValues.port || xavvaJson.port || String(DEFAULT_TOMCAT_PORT))),
92
+ webappPath: webappPath
93
+ });
94
+
95
+ // Instala se necessário
96
+ if (!embeddedService.checkInstallation()) {
97
+ Logger.newline();
98
+ Logger.warn("Tomcat não encontrado!");
99
+ Logger.info("Versão solicitada", embeddedVersion);
100
+ Logger.newline();
101
+ Logger.log(`${Logger.C.primary}?${Logger.C.reset} Deseja instalar o Tomcat ${embeddedVersion} automaticamente?`);
102
+ Logger.log(`${Logger.C.dim} O download é de ~16MB e será salvo em:~/.xavva/tomcat/${embeddedVersion}${Logger.C.reset}`);
103
+ Logger.newline();
104
+
105
+ // Garante que não há output pendente antes da pergunta
106
+ await new Promise(resolve => setTimeout(resolve, 50));
107
+ process.stdout.write('\r\x1b[K'); // Limpa linha atual
108
+
109
+ const autoYes = !!cliValues.yes;
110
+ const shouldInstall = autoYes || await this.askYesNo("Instalar");
111
+
112
+ if (!shouldInstall) {
113
+ Logger.newline();
114
+ Logger.info("Opções disponíveis", "");
115
+ Logger.log(` ${Logger.C.primary}1.${Logger.C.reset} Defina TOMCAT_HOME ou CATALINA_HOME`);
116
+ Logger.log(` ${Logger.C.primary}2.${Logger.C.reset} Use --path para especificar o Tomcat`);
117
+ Logger.log(` ${Logger.C.primary}3.${Logger.C.reset} Use --tomcat-version para outra versão`);
118
+ Logger.newline();
119
+ process.exit(0);
120
+ }
121
+
122
+ Logger.newline();
123
+ const installed = await embeddedService.install();
124
+ if (!installed) {
125
+ Logger.error("Falha ao instalar Tomcat embutido.");
126
+ Logger.info("Dica", "Instale o Tomcat manualmente ou defina TOMCAT_HOME");
127
+ process.exit(1);
128
+ }
129
+ } else {
130
+ Logger.info("Tomcat", `Usando versão embutida ${embeddedVersion}`);
131
+ }
132
+
133
+ // Configura contexto da aplicação
134
+ await embeddedService.createAppContext();
135
+
136
+ tomcatPath = embeddedService.getTomcatHome();
137
+ }
138
+
67
139
  const config: AppConfig = {
68
140
  tomcat: {
69
- path: String(cliValues.path || xavvaJson.path || envTomcatPath),
141
+ path: tomcatPath,
70
142
  port: parseInt(String(cliValues.port || xavvaJson.port || String(DEFAULT_TOMCAT_PORT))),
71
143
  webapps: "webapps",
72
144
  grep: cliValues.grep || xavvaJson.grep ? String(cliValues.grep || xavvaJson.grep) : "",
145
+ embedded: useEmbedded,
146
+ version: embeddedVersion,
73
147
  },
74
148
  project: {
75
149
  appName: cliValues.name || xavvaJson.name ? String(cliValues.name || xavvaJson.name) : "",
@@ -86,6 +160,8 @@ export class ConfigManager {
86
160
  grep: runClass || (cliValues.grep || xavvaJson.grep ? String(cliValues.grep || xavvaJson.grep) : ""),
87
161
  tui: !!(cliValues.tui ?? xavvaJson.tui),
88
162
  encoding: cliValues.encoding || xavvaJson.encoding || "",
163
+ war: !!(cliValues.war ?? xavvaJson.war),
164
+ cache: !!(cliValues.cache ?? xavvaJson.cache),
89
165
  }
90
166
  };
91
167
 
@@ -106,6 +182,50 @@ export class ConfigManager {
106
182
  return "maven";
107
183
  }
108
184
 
185
+ private static async askYesNo(question: string): Promise<boolean> {
186
+ // Pequeno delay para garantir que o output anterior foi processado
187
+ await new Promise(resolve => setTimeout(resolve, 100));
188
+
189
+ // Limpa qualquer coisa pendente no stdout
190
+ process.stdout.write('\x1b[0m');
191
+
192
+ return new Promise((resolve) => {
193
+ const chunks: Buffer[] = [];
194
+
195
+ const cleanup = () => {
196
+ process.stdin.removeListener('data', onData);
197
+ process.stdin.removeListener('end', onEnd);
198
+ process.stdin.pause();
199
+ };
200
+
201
+ const onData = (data: Buffer) => {
202
+ chunks.push(data);
203
+ const str = Buffer.concat(chunks).toString();
204
+
205
+ // Procura por enter no input
206
+ if (str.includes('\n') || str.includes('\r')) {
207
+ cleanup();
208
+ const answer = str.replace(/\r?\n/g, '').trim().toLowerCase();
209
+ process.stdout.write('\n');
210
+ resolve(answer === '' || answer === 'y' || answer === 'yes');
211
+ }
212
+ };
213
+
214
+ const onEnd = () => {
215
+ cleanup();
216
+ const answer = Buffer.concat(chunks).toString().trim().toLowerCase();
217
+ resolve(answer === '' || answer === 'y' || answer === 'yes');
218
+ };
219
+
220
+ // Mostra a pergunta
221
+ process.stdout.write(`${question} [Y/n]: `);
222
+
223
+ process.stdin.resume();
224
+ process.stdin.on('data', onData);
225
+ process.stdin.on('end', onEnd);
226
+ });
227
+ }
228
+
109
229
  private static ensureGitIgnore() {
110
230
  const gitignorePath = path.join(process.cwd(), ".gitignore");
111
231