@archznn/xavva 2.2.2 → 2.3.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.3.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,30 @@ 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
+ # Or install manually
106
+ xavva tomcat install
107
+
108
+ # Check Tomcat status
109
+ xavva tomcat status
110
+
111
+ # List available versions
112
+ xavva tomcat list
113
+
114
+ # Use specific version
115
+ xavva dev --tomcat-version 9.0.115
116
+ ```
85
117
 
86
118
  ---
87
119
 
@@ -176,6 +208,9 @@ Create `xavva.json` in your project root:
176
208
  | `-d, --debug` | Enable JPDA debugger |
177
209
  | `-c, --clean` | Clean logs before start |
178
210
  | `-s, --no-build` | Skip initial build |
211
+ | `-W, --war` | Generate .war file (vs exploded)|
212
+ | `--cache` | Use build cache (faster) |
213
+ | `-y, --yes` | Auto-install Tomcat (no prompt) |
179
214
  | `-V, --verbose` | Detailed output |
180
215
 
181
216
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archznn/xavva",
3
- "version": "2.2.2",
3
+ "version": "2.3.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",
@@ -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, 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,56 @@ 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 install
94
+ xavva tomcat status
95
+ xavva tomcat list
96
+
72
97
  ${this.c("yellow", "CONFIGURATION")}
73
98
  Settings are loaded from ${this.c("cyan", "xavva.json")} in the project root:
74
99
 
@@ -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,114 @@
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
+
7
+ export class TomcatCommand implements Command {
8
+ async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
9
+ const action = args?.["tomcat-action"] || "status";
10
+
11
+ switch (action) {
12
+ case "install":
13
+ await this.handleInstall(config, args);
14
+ break;
15
+ case "list":
16
+ this.handleList();
17
+ break;
18
+ case "uninstall":
19
+ await this.handleUninstall(config, args);
20
+ break;
21
+ case "status":
22
+ await this.handleStatus(config);
23
+ break;
24
+ default:
25
+ Logger.error(`Ação desconhecida: ${action}`);
26
+ Logger.info("Ações disponíveis", "install, list, uninstall, status");
27
+ }
28
+ }
29
+
30
+ private async handleInstall(config: AppConfig, args?: CLIArguments): Promise<void> {
31
+ const version = args?.["tomcat-version"] || config.tomcat.version || "10.1.52";
32
+
33
+ // Detectar webapp path
34
+ const webappPath = config.project.buildTool === "maven"
35
+ ? path.join(process.cwd(), "src", "main", "webapp")
36
+ : path.join(process.cwd(), "src", "main", "webapp");
37
+
38
+ const service = new EmbeddedTomcatService({
39
+ version,
40
+ port: config.tomcat.port,
41
+ webappPath
42
+ });
43
+
44
+ if (service.checkInstallation()) {
45
+ Logger.info("Tomcat", `Versão ${version} já está instalada`);
46
+ const info = service.getInfo();
47
+ Logger.config("Local", info.home);
48
+ return;
49
+ }
50
+
51
+ const installed = await service.install();
52
+ if (installed) {
53
+ Logger.success(`Tomcat ${version} instalado com sucesso!`);
54
+ } else {
55
+ Logger.error("Falha na instalação");
56
+ }
57
+ }
58
+
59
+ private handleList(): void {
60
+ Logger.section("Versões Disponíveis");
61
+ const versions = EmbeddedTomcatService.getAvailableVersions();
62
+
63
+ for (const version of versions) {
64
+ Logger.log(` ${Logger.C.primary}•${Logger.C.reset} ${version}`);
65
+ }
66
+
67
+ Logger.newline();
68
+ Logger.info("Versão padrão", "10.1.52");
69
+ }
70
+
71
+ private async handleUninstall(config: AppConfig, args?: CLIArguments): Promise<void> {
72
+ const version = args?.["tomcat-version"] || config.tomcat.version || "10.1.52";
73
+
74
+ const service = new EmbeddedTomcatService({
75
+ version,
76
+ port: config.tomcat.port,
77
+ webappPath: "."
78
+ });
79
+
80
+ if (!service.checkInstallation()) {
81
+ Logger.warn(`Tomcat ${version} não está instalado`);
82
+ return;
83
+ }
84
+
85
+ Logger.step(`Removendo Tomcat ${version}...`);
86
+ await service.uninstall();
87
+ Logger.success("Removido com sucesso!");
88
+ }
89
+
90
+ private async handleStatus(config: AppConfig): Promise<void> {
91
+ Logger.section("Status do Tomcat");
92
+
93
+ if (config.tomcat.embedded) {
94
+ Logger.config("Modo", "Embutido");
95
+ Logger.config("Versão", config.tomcat.version || "10.1.52");
96
+ Logger.config("Porta", String(config.tomcat.port));
97
+ Logger.config("Home", config.tomcat.path);
98
+ } else {
99
+ Logger.config("Modo", "Externo");
100
+ Logger.config("CATALINA_HOME", config.tomcat.path);
101
+ Logger.config("Porta", String(config.tomcat.port));
102
+ }
103
+
104
+ // Verificar se está rodando
105
+ const netstat = Bun.spawnSync(["cmd", "/c", `netstat -ano | findstr :${config.tomcat.port}`]);
106
+ const output = await new Response(netstat.stdout).text();
107
+
108
+ if (output.trim()) {
109
+ Logger.config("Status", `${Logger.C.success}Rodando${Logger.C.reset}`);
110
+ } else {
111
+ Logger.config("Status", `${Logger.C.warning}Parado${Logger.C.reset}`);
112
+ }
113
+ }
114
+ }
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
 
@@ -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,391 @@
1
+ import { Logger } from "../utils/ui";
2
+ import { existsSync, mkdirSync, createWriteStream, writeFileSync, promises as fs } from "fs";
3
+ import path from "path";
4
+ import os from "os";
5
+ import { spawn } from "child_process";
6
+
7
+ export interface EmbeddedTomcatOptions {
8
+ version?: string;
9
+ port?: number;
10
+ webappPath: string;
11
+ contextPath?: string;
12
+ }
13
+
14
+ interface DownloadProgress {
15
+ downloaded: number;
16
+ total: number;
17
+ percent: number;
18
+ }
19
+
20
+ 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"?>
243
+ <Context
244
+ docBase="${this.webappPath.replace(/\\/g, "/")}"
245
+ reloadable="true"
246
+ crossContext="true"
247
+ antiResourceLocking="false"
248
+ antiJARLocking="false">
249
+ </Context>`;
250
+
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
+ }
391
+ }
@@ -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,77 @@ 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
+ let embeddedVersion = String(cliValues["tomcat-version"] || xavvaJson.version || "10.1.52");
83
+
84
+ // Se não há Tomcat configurado ou não existe no path, usar embutido
85
+ if (!tomcatPath || (!fs.existsSync(path.join(tomcatPath, "bin", "catalina.bat")) && isStart)) {
86
+ useEmbedded = true;
87
+ const embeddedService = new EmbeddedTomcatService({
88
+ version: embeddedVersion,
89
+ port: parseInt(String(cliValues.port || xavvaJson.port || String(DEFAULT_TOMCAT_PORT))),
90
+ webappPath: webappPath
91
+ });
92
+
93
+ // Instala se necessário
94
+ if (!embeddedService.checkInstallation()) {
95
+ Logger.newline();
96
+ Logger.warn("Tomcat não encontrado!");
97
+ Logger.info("Versão solicitada", embeddedVersion);
98
+ Logger.newline();
99
+ Logger.log(`${Logger.C.primary}?${Logger.C.reset} Deseja instalar o Tomcat ${embeddedVersion} automaticamente?`);
100
+ Logger.log(`${Logger.C.dim} O download é de ~16MB e será salvo em:~/.xavva/tomcat/${embeddedVersion}${Logger.C.reset}`);
101
+ Logger.newline();
102
+
103
+ // Garante que não há output pendente antes da pergunta
104
+ await new Promise(resolve => setTimeout(resolve, 50));
105
+ process.stdout.write('\r\x1b[K'); // Limpa linha atual
106
+
107
+ const autoYes = !!cliValues.yes;
108
+ const shouldInstall = autoYes || await this.askYesNo("Instalar");
109
+
110
+ if (!shouldInstall) {
111
+ Logger.newline();
112
+ Logger.info("Opções disponíveis", "");
113
+ Logger.log(` ${Logger.C.primary}1.${Logger.C.reset} Defina TOMCAT_HOME ou CATALINA_HOME`);
114
+ Logger.log(` ${Logger.C.primary}2.${Logger.C.reset} Use --path para especificar o Tomcat`);
115
+ Logger.log(` ${Logger.C.primary}3.${Logger.C.reset} Use --tomcat-version para outra versão`);
116
+ Logger.newline();
117
+ process.exit(0);
118
+ }
119
+
120
+ Logger.newline();
121
+ const installed = await embeddedService.install();
122
+ if (!installed) {
123
+ Logger.error("Falha ao instalar Tomcat embutido.");
124
+ Logger.info("Dica", "Instale o Tomcat manualmente ou defina TOMCAT_HOME");
125
+ process.exit(1);
126
+ }
127
+ } else {
128
+ Logger.info("Tomcat", `Usando versão embutida ${embeddedVersion}`);
129
+ }
130
+
131
+ // Configura contexto da aplicação
132
+ await embeddedService.createAppContext();
133
+
134
+ tomcatPath = embeddedService.getTomcatHome();
135
+ }
136
+
67
137
  const config: AppConfig = {
68
138
  tomcat: {
69
- path: String(cliValues.path || xavvaJson.path || envTomcatPath),
139
+ path: tomcatPath,
70
140
  port: parseInt(String(cliValues.port || xavvaJson.port || String(DEFAULT_TOMCAT_PORT))),
71
141
  webapps: "webapps",
72
142
  grep: cliValues.grep || xavvaJson.grep ? String(cliValues.grep || xavvaJson.grep) : "",
143
+ embedded: useEmbedded,
144
+ version: embeddedVersion,
73
145
  },
74
146
  project: {
75
147
  appName: cliValues.name || xavvaJson.name ? String(cliValues.name || xavvaJson.name) : "",
@@ -86,6 +158,8 @@ export class ConfigManager {
86
158
  grep: runClass || (cliValues.grep || xavvaJson.grep ? String(cliValues.grep || xavvaJson.grep) : ""),
87
159
  tui: !!(cliValues.tui ?? xavvaJson.tui),
88
160
  encoding: cliValues.encoding || xavvaJson.encoding || "",
161
+ war: !!(cliValues.war ?? xavvaJson.war),
162
+ cache: !!(cliValues.cache ?? xavvaJson.cache),
89
163
  }
90
164
  };
91
165
 
@@ -106,6 +180,50 @@ export class ConfigManager {
106
180
  return "maven";
107
181
  }
108
182
 
183
+ private static async askYesNo(question: string): Promise<boolean> {
184
+ // Pequeno delay para garantir que o output anterior foi processado
185
+ await new Promise(resolve => setTimeout(resolve, 100));
186
+
187
+ // Limpa qualquer coisa pendente no stdout
188
+ process.stdout.write('\x1b[0m');
189
+
190
+ return new Promise((resolve) => {
191
+ const chunks: Buffer[] = [];
192
+
193
+ const cleanup = () => {
194
+ process.stdin.removeListener('data', onData);
195
+ process.stdin.removeListener('end', onEnd);
196
+ process.stdin.pause();
197
+ };
198
+
199
+ const onData = (data: Buffer) => {
200
+ chunks.push(data);
201
+ const str = Buffer.concat(chunks).toString();
202
+
203
+ // Procura por enter no input
204
+ if (str.includes('\n') || str.includes('\r')) {
205
+ cleanup();
206
+ const answer = str.replace(/\r?\n/g, '').trim().toLowerCase();
207
+ process.stdout.write('\n');
208
+ resolve(answer === '' || answer === 'y' || answer === 'yes');
209
+ }
210
+ };
211
+
212
+ const onEnd = () => {
213
+ cleanup();
214
+ const answer = Buffer.concat(chunks).toString().trim().toLowerCase();
215
+ resolve(answer === '' || answer === 'y' || answer === 'yes');
216
+ };
217
+
218
+ // Mostra a pergunta
219
+ process.stdout.write(`${question} [Y/n]: `);
220
+
221
+ process.stdin.resume();
222
+ process.stdin.on('data', onData);
223
+ process.stdin.on('end', onEnd);
224
+ });
225
+ }
226
+
109
227
  private static ensureGitIgnore() {
110
228
  const gitignorePath = path.join(process.cwd(), ".gitignore");
111
229