@archznn/xavva 1.7.0 → 1.8.1

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
@@ -1,41 +1,111 @@
1
- # XAVVA 🚀 (Windows Only) `v1.7.0`
1
+ # XAVVA 🚀 (Windows Only) `v1.8.1`
2
2
 
3
3
  Xavva é uma CLI de alto desempenho construída com **Bun** para automatizar o ciclo de desenvolvimento de aplicações Java (Maven/Gradle) rodando no Apache Tomcat. Ela foi desenhada especificamente para desenvolvedores que buscam a velocidade de ambientes modernos (como Node.js/Vite) dentro do ecossistema Java Enterprise.
4
4
 
5
- ## 🛠️ Funcionalidades de Elite
5
+ ---
6
+
7
+ ## 🛠️ Por que Xavva?
8
+
9
+ Desenvolver para Java/Tomcat tradicionalmente envolve ciclos lentos de `clean install`, `war deploy` e restarts de servidor. O Xavva quebra esse paradigma ao introduzir um fluxo de **Hot-Reload incremental**, onde apenas o que mudou é enviado ao servidor.
10
+
11
+ ### ⚡ Funcionalidades de Elite
6
12
 
7
- - **⚡ Ultra-Fast Hot Swap**: Compilação incremental e injeção direta de arquivos `.class` e recursos (JSP, HTML, CSS, JS) no Tomcat em execução sem necessidade de restart.
8
- - **📦 Multi-Module Support**: Detecção recursiva de diretórios de classes em projetos complexos, garantindo que o Hot-Reload funcione entre diferentes módulos.
9
- - **🛠️ Modo Dev Inteligente**: O comando `xavva dev` ativa hot-reload, logs limpos, debugger (JPDA) e monitoramento de memória em um único fluxo.
10
- - **🌐 Live Reload Automático**: Atualiza automaticamente as abas do Chrome ou Edge após o deploy ou sincronização de arquivos, mantendo o foco no código.
11
- - **🔍 API Documentation (Swagger-like)**: O comando `xavva docs` mapeia estaticamente sua API, exibindo endpoints, métodos HTTP e parâmetros diretamente no terminal.
12
- - **📊 Real-time Log Filtering**: Filtra ruídos excessivos do Tomcat/Jersey/SLF4J, destacando erros Java com dicas de solução.
13
- - **📈 JVM & Memory Monitor**: Exibe o consumo de RAM do processo do Tomcat em tempo real.
14
- - **🩺 Doctor Mode**: Diagnostica o ambiente e corrige automaticamente problemas de **Encoding (UTF-8 BOM)**.
15
- - **🛡️ JAR Audit**: Analisa todas as dependências (`.jar`) da sua aplicação em busca de vulnerabilidades (CVEs).
13
+ - **Ultra-Fast Hot Swap**: Compilação incremental e injeção direta de arquivos `.class` e recursos (JSP, HTML, CSS, JS) no Tomcat em execução sem restart.
14
+ - **Gradle & Maven Native**: Suporte robusto para ambos os ecossistemas, incluindo extração automática de classpath para execução de classes standalone (`run`/`debug`).
15
+ - **DCEVM Integration**: O Xavva pode baixar e configurar automaticamente uma JDK com DCEVM (JetBrains Runtime), permitindo mudanças estruturais em classes (novos métodos/campos) em tempo real.
16
+ - **API Documentation (Swagger-like)**: Mapeamento estático de endpoints, métodos HTTP e parâmetros diretamente no terminal via `xavva docs`.
17
+ - **Live Reload Automático**: Sincronização inteligente que atualiza o browser (Chrome/Edge) após mudanças em JSPs ou recursos estáticos.
18
+ - **Segurança & Robustez**: Auditoria de dependências (`.jar`) e execução protegida contra *Command Injection* no PowerShell.
19
+ - **Pathing JAR (Windows)**: Contorna limites de caracteres do Windows em classpaths gigantes através de geração dinâmica de Manifestos compatíveis com a especificação Java.
20
+ - **Auto-Healing**: Diagnóstico e reparo automático de problemas comuns de ambiente, como encoding UTF-8 com BOM.
16
21
 
17
- ## 🚀 Instalação e Uso
22
+ ---
23
+
24
+ ## 🚀 Começo Rápido
18
25
 
19
- ```bash
20
- # Instalação global
26
+ ### Pré-requisitos
27
+ - **Windows** (Otimizado para PowerShell Core e Windows PowerShell)
28
+ - **Bun** instalado (`powershell -c "irm bun.sh/install.ps1 | iex"`)
29
+ - **Tomcat** configurado via variável de ambiente `TOMCAT_HOME` ou `CATALINA_HOME`.
30
+
31
+ ### Instalação
32
+ ```powershell
33
+ # Instalação global via NPM
21
34
  npm install -g @archznn/xavva
22
35
 
23
- # Ou rodar sem instalar via npx
36
+ # Ou use diretamente via npx
24
37
  npx @archznn/xavva dev
25
38
  ```
26
39
 
27
- ## ⚙️ Zero Config & Auto-Detection
40
+ ---
28
41
 
29
- O Xavva identifica automaticamente se seu projeto usa **Maven** (`pom.xml`) ou **Gradle** (`build.gradle`) e localiza o Tomcat através das variáveis `TOMCAT_HOME` ou `CATALINA_HOME`.
42
+ ## 📖 Referência de Comandos
30
43
 
31
- ### Comandos Principais
44
+ O Xavva utiliza uma arquitetura modular de comandos e serviços, garantindo alta performance e extensibilidade.
32
45
 
33
- ```bash
34
- xavva dev # Modo desenvolvimento completo com Hot-Reload
35
- xavva docs # Documentação estática de endpoints
36
- xavva audit # Auditoria de segurança de dependências
37
- xavva doctor --fix # Diagnóstico e reparo de ambiente
38
- ```
46
+ ### 1. Modo Desenvolvimento (`xavva dev`)
47
+ O comando principal para o dia a dia. Ativa o monitoramento de arquivos e o Hot-Reload.
48
+ - **O que faz**: Compila Java, sincroniza recursos, limpa logs, inicia o Tomcat e monitora mudanças.
49
+ - **Flags úteis**:
50
+ - `--no-build`: Pula o build inicial.
51
+ - `--watch`: Ativa o modo de observação de arquivos (padrão em `dev`).
52
+ - `--port 8081`: Define uma porta específica para o Tomcat.
53
+ - `--dp 9000`: Altera a porta do Debugger (JPDA) (padrão 5005).
54
+
55
+ ### 2. Execução de Classes (`xavva run` / `xavva debug`)
56
+ Executa classes Java standalone (`public static void main`) com resolução automática de dependências Maven ou Gradle.
57
+ - **Inteligência de Classpath**: Gera automaticamente um `classpath.jar` temporário (Pathing JAR) para evitar o erro de "Command line too long" no Windows.
58
+ - **Busca por Grep**: Se você fornecer apenas parte do nome da classe, o Xavva a encontrará recursivamente no projeto.
59
+
60
+ ### 3. Documentação de API (`xavva docs`)
61
+ Gera uma documentação instantânea dos seus controladores Jersey/Spring no terminal.
62
+ - Mostra a URL completa, método HTTP e parâmetros (Path, Query, Body).
63
+
64
+ ### 4. Diagnóstico e Reparo (`xavva doctor`)
65
+ Verifica se o seu ambiente está saudável.
66
+ - **`xavva doctor --fix`**:
67
+ - Instala o **JetBrains Runtime (DCEVM)** se necessário.
68
+ - Remove automaticamente o **BOM (Byte Order Mark)** de arquivos Java que causam erros de compilação.
69
+ - Configura o `JAVA_HOME` do sistema.
70
+
71
+ ### 5. Auditoria de Segurança (`xavva audit`)
72
+ Analisa a pasta `WEB-INF/lib` em busca de JARs vulneráveis via integração com **OSV.dev**. Essencial para manter a integridade do projeto antes de deploys em produção.
73
+
74
+ ### 6. Logs em Tempo Real (`xavva logs`)
75
+ Exibe os logs do Tomcat filtrando ruídos excessivos e destacando StackTraces importantes. Use `--grep "NomeDaClasse"` para focar em logs específicos.
76
+
77
+ ---
78
+
79
+ ## 🏗️ Arquitetura do Sistema
80
+
81
+ O Xavva foi refatorado para uma arquitetura de **Injeção de Dependências** e **Serviços Centralizados**:
82
+
83
+ - **CommandRegistry**: Gerenciamento modular de comandos via Command Pattern.
84
+ - **ProjectService**: Inteligência centralizada para descoberta de diretórios de build, artefatos e classpaths Java.
85
+ - **AuditService**: Segurança aprimorada com execução isolada e protegida no PowerShell.
86
+ - **TomcatService**: Gerenciamento do ciclo de vida do servidor com suporte a hotswap dinâmico.
87
+
88
+ ---
89
+
90
+ ## ⚙️ Configuração (Zero Config)
91
+
92
+ O Xavva funciona sem arquivos de configuração externos, baseando-se no ambiente:
93
+
94
+ | Variável | Descrição |
95
+ |----------|-----------|
96
+ | `TOMCAT_HOME` | Caminho raiz do seu Apache Tomcat. |
97
+ | `JAVA_HOME` | JDK utilizada para compilação e execução. |
98
+
99
+ **Dica**: O Xavva cria automaticamente uma pasta `.xavva` no seu projeto para cache e artefatos temporários, e a adiciona ao seu `.gitignore`.
100
+
101
+ ---
102
+
103
+ ## 🧩 Sincronização de Recursos
104
+
105
+ Ao editar um arquivo, o Xavva decide a melhor estratégia:
106
+ - **`.java`**: Compila apenas a classe e injeta o bytecode via `fastSync`.
107
+ - **`.jsp` / `.html` / `.css`**: Sincroniza o arquivo diretamente na pasta de deploy do Tomcat.
108
+ - **`pom.xml` / `build.gradle`**: Identifica mudanças estruturais e sugere um rebuild completo com invalidação de cache inteligente.
39
109
 
40
110
  ---
41
- *Desenvolvido para transformar a experiência de desenvolvimento Java Legacy em algo ágil e produtivo.*
111
+ *Desenvolvido para transformar o legado em produtivo. 🚀*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archznn/xavva",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
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,15 +1,17 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
3
  import type { Command } from "./Command";
4
- import type { AppConfig } from "../types/config";
4
+ import type { AppConfig, CLIArguments } from "../types/config";
5
5
  import { AuditService, type JarAuditResult } from "../services/AuditService";
6
6
  import { Logger } from "../utils/ui";
7
7
 
8
8
  export class AuditCommand implements Command {
9
- async execute(config: AppConfig): Promise<void> {
9
+ constructor(private auditService: AuditService) {}
10
+
11
+ async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
10
12
  Logger.section("Vulnerability & JAR Audit");
11
13
 
12
- let appName = config.project.appName;
14
+ let appName = args?.name || config.project.appName;
13
15
 
14
16
  if (!appName) {
15
17
  appName = this.inferFromArtifacts();
@@ -38,10 +40,8 @@ export class AuditCommand implements Command {
38
40
  return;
39
41
  }
40
42
 
41
- const auditService = new AuditService(config.tomcat);
42
-
43
43
  try {
44
- const results = await auditService.runAudit(appName);
44
+ const results = await this.auditService.runAudit(appName);
45
45
  const vulnerable = results.filter(r => r.vulnerabilities.length > 0);
46
46
 
47
47
  if (vulnerable.length === 0) {
@@ -4,15 +4,15 @@ import { BuildService } from "../services/BuildService";
4
4
  import { Logger } from "../utils/ui";
5
5
 
6
6
  export class BuildCommand implements Command {
7
+ constructor(private buildService: BuildService) {}
8
+
7
9
  async execute(config: AppConfig): Promise<void> {
8
- const builder = new BuildService(config.project, config.tomcat);
9
-
10
10
  Logger.section("Build Only");
11
11
  Logger.info("Tool", config.project.buildTool.toUpperCase());
12
12
  if (config.project.profile) Logger.info("Profile", config.project.profile);
13
13
 
14
14
  try {
15
- await builder.runBuild();
15
+ await this.buildService.runBuild();
16
16
  Logger.success("Build completed successfully!");
17
17
  } catch (error: any) {
18
18
  Logger.error(error.message);
@@ -1,5 +1,5 @@
1
- import type { AppConfig } from "../types/config";
1
+ import type { AppConfig, CLIArguments } from "../types/config";
2
2
 
3
3
  export interface Command {
4
- execute(config: AppConfig): Promise<void>;
4
+ execute(config: AppConfig, args?: CLIArguments): Promise<void>;
5
5
  }
@@ -0,0 +1,36 @@
1
+ import type { AppConfig, CLIArguments } from "../types/config";
2
+ import type { Command } from "./Command";
3
+ import { Logger } from "../utils/ui";
4
+ import { HelpCommand } from "./HelpCommand";
5
+
6
+ export class CommandRegistry {
7
+ private commands = new Map<string, Command>();
8
+
9
+ register(name: string, command: Command) {
10
+ this.commands.set(name, command);
11
+ }
12
+
13
+ has(name: string): boolean {
14
+ return this.commands.has(name);
15
+ }
16
+
17
+ get(name: string): Command | undefined {
18
+ return this.commands.get(name);
19
+ }
20
+
21
+ async execute(name: string, config: AppConfig, args: CLIArguments) {
22
+ const command = this.commands.get(name);
23
+ if (!command) {
24
+ Logger.error(`Comando desconhecido: ${name}`);
25
+ await new HelpCommand().execute(config);
26
+ process.exit(1);
27
+ }
28
+
29
+ try {
30
+ await command.execute(config, args);
31
+ } catch (error: any) {
32
+ Logger.error(`Erro ao executar comando '${name}': ${error.message}`);
33
+ process.exit(1);
34
+ }
35
+ }
36
+ }
@@ -1,68 +1,24 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
3
  import type { Command } from "./Command";
4
- import type { AppConfig } from "../types/config";
4
+ import type { AppConfig, CLIArguments } from "../types/config";
5
5
  import { BuildService } from "../services/BuildService";
6
6
  import { TomcatService } from "../services/TomcatService";
7
7
  import { Logger } from "../utils/ui";
8
8
  import { EndpointService } from "../services/EndpointService";
9
+ import { BrowserService } from "../services/BrowserService";
9
10
 
10
11
  export class DeployCommand implements Command {
11
- constructor(private tomcat?: TomcatService, private builder?: BuildService) {}
12
+ constructor(private tomcat: TomcatService, private builder: BuildService) {}
12
13
 
13
- private async reloadBrowser(url: string) {
14
- if (process.platform !== 'win32') return;
15
-
16
- await new Promise(r => setTimeout(r, 800));
17
-
18
- const psCommand = `
19
- $shell = New-Object -ComObject WScript.Shell
20
- $process = Get-Process | Where-Object { $_.MainWindowTitle -match "Chrome" -or $_.MainWindowTitle -match "Edge" } | Select-Object -First 1
21
- if ($process) {
22
- $shell.AppActivate($process.Id)
23
- Sleep -m 100
24
- $shell.SendKeys("{F5}")
25
- }
26
- `;
27
- Bun.spawn(["powershell", "-command", psCommand]);
28
- }
29
-
30
- async execute(config: AppConfig, incremental = false, isWatching = false): Promise<void> {
31
- const tomcat = this.tomcat || new TomcatService(config.tomcat);
32
- const builder = this.builder || new BuildService(config.project, config.tomcat);
14
+ async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
15
+ const incremental = args?.watch && args?.incremental;
16
+ const isWatching = !!args?.watch;
17
+ const tomcat = this.tomcat;
18
+ const builder = this.builder;
33
19
 
34
20
  if (!incremental) {
35
- Logger.config("Runtime", config.project.buildTool.toUpperCase());
36
- Logger.config("Watch Mode", isWatching ? "ON" : "OFF");
37
- Logger.config("Debug", config.project.debug ? "ON (Port 5005)" : "OFF");
38
-
39
- let javaBin = "java";
40
- if (process.env.JAVA_HOME) {
41
- const homeBin = path.join(process.env.JAVA_HOME, "bin", "java.exe");
42
- if (fs.existsSync(homeBin)) javaBin = homeBin;
43
- }
44
-
45
- const javaVer = Bun.spawnSync([javaBin, "-version"]);
46
- const output = (javaVer.stderr.toString() + javaVer.stdout.toString()).toLowerCase();
47
- const hasDcevm = output.includes("dcevm") ||
48
- output.includes("jetbrains") ||
49
- output.includes("trava") ||
50
- output.includes("jbr");
51
-
52
- if (!hasDcevm && isWatching) {
53
- Logger.config("Hot Reload", "Standard (No structural changes)");
54
- } else if (hasDcevm) {
55
- Logger.config("Hot Reload", "Advanced (DCEVM Active)");
56
- }
57
-
58
- const srcPath = path.join(process.cwd(), "src");
59
- if (fs.existsSync(srcPath)) {
60
- const contextPath = (config.project.appName || "").replace(".war", "");
61
- const endpoints = EndpointService.scan(srcPath, contextPath);
62
- if (endpoints.length > 0) {
63
- Logger.config("Endpoints", endpoints.length);
64
- }
65
- }
21
+ this.logConfiguration(config, isWatching);
66
22
  } else {
67
23
  Logger.watcher("Change detected", "change");
68
24
  }
@@ -72,19 +28,28 @@ export class DeployCommand implements Command {
72
28
 
73
29
  if (!incremental) {
74
30
  await tomcat.killConflict();
75
- }
31
+ await tomcat.clearWebapps();
76
32
 
77
- if (!config.project.skipBuild) {
78
- if (incremental) Logger.watcher("Incremental compilation", "start");
79
- await builder.runBuild(incremental);
80
- if (!incremental) Logger.build("Full project build");
33
+ if (!config.project.skipBuild) {
34
+ Logger.watcher("Building project", "start");
35
+ await builder.runBuild(incremental);
36
+ }
37
+
38
+ if (!config.project.skipBuild) {
39
+ Logger.build("Full project build and environment ready");
40
+ }
41
+ } else {
42
+ if (!config.project.skipBuild) {
43
+ Logger.watcher("Incremental compilation", "start");
44
+ await builder.runBuild(incremental);
45
+ }
81
46
  }
82
47
 
83
48
  if (incremental) {
84
- const actualAppFolder = await builder.syncClasses(true);
49
+ const actualAppFolder = await builder.syncClasses();
85
50
  const actualContextPath = contextPath || actualAppFolder || "";
86
51
  const actualAppUrl = `http://localhost:${config.tomcat.port}/${actualContextPath}`;
87
- await this.reloadBrowser(actualAppUrl);
52
+ await BrowserService.reload(actualAppUrl);
88
53
  Logger.watcher("Redeploy completed", "success");
89
54
  return;
90
55
  }
@@ -96,62 +61,41 @@ export class DeployCommand implements Command {
96
61
  const finalContextPath = contextPath || artifactInfo.finalName.replace(".war", "");
97
62
  const appWebappPath = path.join(config.tomcat.path, "webapps", finalContextPath);
98
63
 
99
- if (!fs.existsSync(appWebappPath)) fs.mkdirSync(appWebappPath, { recursive: true });
100
-
101
- try {
102
- Bun.spawnSync(["jar", "xf", artifactInfo.path], { cwd: appWebappPath });
103
- Logger.build("Artifacts deployed");
104
- } catch (e: any) {
105
- const extractCmd = `powershell -command "Expand-Archive -Path '${artifactInfo.path}' -DestinationPath '${appWebappPath}' -Force"`;
106
- Bun.spawnSync(["powershell", "-command", extractCmd]);
107
- Logger.build("Artifacts deployed (legacy mode)");
64
+ if (artifactInfo.isDirectory) {
65
+ await builder.syncClasses(artifactInfo.path);
66
+ Logger.build("Exploded directory synced");
67
+ } else {
68
+ if (!fs.existsSync(appWebappPath)) fs.mkdirSync(appWebappPath, { recursive: true });
69
+
70
+ const artifactStat = fs.statSync(artifactInfo.path);
71
+ const webappStat = fs.existsSync(appWebappPath) ? fs.statSync(appWebappPath) : null;
72
+
73
+ if (!webappStat || artifactStat.mtimeMs > webappStat.mtimeMs) {
74
+ try {
75
+ Bun.spawnSync(["jar", "xf", artifactInfo.path], { cwd: appWebappPath });
76
+ Logger.build("Artifacts deployed");
77
+ } catch (e) {
78
+ const extractCmd = `Expand-Archive -Path $env:ARTIFACT_PATH -DestinationPath $env:DEST_PATH -Force`;
79
+ Bun.spawnSync(["powershell", "-command", extractCmd], {
80
+ env: {
81
+ ...process.env,
82
+ ARTIFACT_PATH: artifactInfo.path,
83
+ DEST_PATH: appWebappPath
84
+ }
85
+ });
86
+ Logger.build("Artifacts deployed (legacy mode)");
87
+ }
88
+ } else {
89
+ Logger.build("Webapp already up to date, skipping extraction");
90
+ }
108
91
  }
109
92
 
110
- const webInfClassesDir = path.join(appWebappPath, "WEB-INF", "classes");
111
- if (!fs.existsSync(webInfClassesDir)) fs.mkdirSync(webInfClassesDir, { recursive: true });
112
-
113
- const xavvaProps = path.join(process.cwd(), ".xavva", "hotswap-agent.properties");
114
- if (fs.existsSync(xavvaProps)) {
115
- fs.copyFileSync(xavvaProps, path.join(webInfClassesDir, "hotswap-agent.properties"));
116
- }
93
+ this.injectHotswapProperties(appWebappPath);
117
94
 
118
95
  const finalAppUrl = `http://localhost:${config.tomcat.port}/${finalContextPath}`;
119
96
 
120
97
  tomcat.onReady = async () => {
121
- try {
122
- await new Promise(r => setTimeout(r, 1500));
123
- const response = await fetch(finalAppUrl);
124
- if (response.status < 500) {
125
- const memory = await tomcat.getMemoryUsage();
126
- Logger.health(finalAppUrl, "success");
127
- Logger.health(`Status ${response.status}`, "success");
128
- Logger.health(`Memory ${memory}`, "success");
129
-
130
- if (!config.project.quiet) {
131
- const endpoints = EndpointService.scan(path.join(process.cwd(), "src"), finalContextPath);
132
- if (endpoints.length > 0) {
133
- Logger.newline();
134
- Logger.log(`${Logger.C.cyan}◈ ENDPOINT MAP:${Logger.C.reset}`);
135
- endpoints.forEach(e => Logger.log(`${Logger.C.dim}➜ ${Logger.C.reset}http://localhost:${config.tomcat.port}${e.fullPath}`));
136
- }
137
- }
138
-
139
- if (incremental) {
140
- await this.reloadBrowser(finalAppUrl);
141
- } else {
142
- if (process.platform === 'win32') {
143
- Bun.spawn(["cmd", "/c", "start", finalAppUrl]);
144
- } else {
145
- const start = process.platform === 'darwin' ? 'open' : 'xdg-open';
146
- Bun.spawn([start, finalAppUrl]);
147
- }
148
- }
149
- } else {
150
- Logger.health(`App returned status ${response.status}`, "warn");
151
- }
152
- } catch (e) {
153
- Logger.health(`Could not connect to ${finalAppUrl}`, "error");
154
- }
98
+ await this.handleServerReady(config, finalAppUrl, finalContextPath, tomcat, incremental);
155
99
  };
156
100
 
157
101
  tomcat.start(config, isWatching);
@@ -161,9 +105,98 @@ export class DeployCommand implements Command {
161
105
  }
162
106
  }
163
107
 
108
+ private logConfiguration(config: AppConfig, isWatching: boolean) {
109
+ Logger.config("Runtime", config.project.buildTool.toUpperCase());
110
+ Logger.config("Watch Mode", isWatching ? "ON" : "OFF");
111
+ Logger.config("Debug", config.project.debug ? `ON (Port ${config.project.debugPort})` : "OFF");
112
+
113
+ let javaBin = "java";
114
+ if (process.env.JAVA_HOME) {
115
+ const homeBin = path.join(process.env.JAVA_HOME, "bin", "java.exe");
116
+ if (fs.existsSync(homeBin)) javaBin = homeBin;
117
+ }
118
+
119
+ const javaVer = Bun.spawnSync([javaBin, "-version"]);
120
+ const output = (javaVer.stderr.toString() + javaVer.stdout.toString()).toLowerCase();
121
+ const hasDcevm = ["dcevm", "jetbrains", "trava", "jbr"].some(v => output.includes(v));
122
+
123
+ if (!hasDcevm && isWatching) {
124
+ Logger.config("Hot Reload", "Standard (No structural changes)");
125
+ } else if (hasDcevm) {
126
+ Logger.config("Hot Reload", "Advanced (DCEVM Active)");
127
+ }
128
+
129
+ const srcPath = path.join(process.cwd(), "src");
130
+ if (fs.existsSync(srcPath)) {
131
+ const contextPath = (config.project.appName || "").replace(".war", "");
132
+ const endpoints = EndpointService.scan(srcPath, contextPath);
133
+ if (endpoints.length > 0) {
134
+ Logger.config("Endpoints", endpoints.length);
135
+ }
136
+ }
137
+ }
138
+
139
+ private injectHotswapProperties(appWebappPath: string) {
140
+ const webInfClassesDir = path.join(appWebappPath, "WEB-INF", "classes");
141
+ if (!fs.existsSync(webInfClassesDir)) fs.mkdirSync(webInfClassesDir, { recursive: true });
142
+
143
+ const xavvaProps = path.join(process.cwd(), ".xavva", "hotswap-agent.properties");
144
+ if (fs.existsSync(xavvaProps)) {
145
+ fs.copyFileSync(xavvaProps, path.join(webInfClassesDir, "hotswap-agent.properties"));
146
+ }
147
+ }
148
+
149
+ private async handleServerReady(config: AppConfig, url: string, context: string, tomcat: TomcatService, incremental: boolean) {
150
+ try {
151
+ await new Promise(r => setTimeout(r, 1500));
152
+ const response = await fetch(url);
153
+ if (response.status < 500) {
154
+ const memory = await tomcat.getMemoryUsage();
155
+ Logger.health(url, "success");
156
+ Logger.health(`Status ${response.status}`, "success");
157
+ Logger.health(`Memory ${memory}`, "success");
158
+
159
+ if (!config.project.quiet) {
160
+ this.showEndpointMap(config.tomcat.port, context);
161
+ }
162
+
163
+ if (incremental) {
164
+ await BrowserService.reload(url);
165
+ } else {
166
+ BrowserService.open(url);
167
+ }
168
+ } else {
169
+ Logger.health(`App returned status ${response.status}`, "warn");
170
+ }
171
+ } catch (e) {
172
+ Logger.health(`Could not connect to ${url}`, "error");
173
+ }
174
+ }
175
+
176
+ private showEndpointMap(port: number, context: string) {
177
+ const endpoints = EndpointService.scan(path.join(process.cwd(), "src"), context);
178
+ if (endpoints.length > 0) {
179
+ Logger.newline();
180
+ Logger.log(`${Logger.C.cyan}◈ ENDPOINT MAP:${Logger.C.reset}`);
181
+
182
+ const apis = endpoints.filter(e => e.className !== "JSP");
183
+ const jsps = endpoints.filter(e => e.className === "JSP");
184
+
185
+ if (apis.length > 0) {
186
+ const uniqueApiUrls = [...new Set(apis.map(e => `http://localhost:${port}${e.fullPath}`))];
187
+ uniqueApiUrls.forEach(url => Logger.log(`${Logger.C.dim}➜ ${Logger.C.reset}${url}`));
188
+ }
189
+
190
+ if (jsps.length > 0) {
191
+ Logger.log(`${Logger.C.dim}--- JSPs ---${Logger.C.reset}`);
192
+ const uniqueJspUrls = [...new Set(jsps.map(e => `http://localhost:${port}${e.fullPath}`))];
193
+ uniqueJspUrls.forEach(url => Logger.log(`${Logger.C.dim}📄 ${Logger.C.reset}${url}`));
194
+ }
195
+ }
196
+ }
197
+
164
198
  async syncResource(config: AppConfig, filename: string): Promise<void> {
165
199
  const contextPath = (config.project.appName || "").replace(".war", "");
166
-
167
200
  const webappsPath = path.join(config.tomcat.path, "webapps");
168
201
  let appFolder = contextPath;
169
202
 
@@ -174,10 +207,7 @@ export class DeployCommand implements Command {
174
207
  }
175
208
 
176
209
  const explodedPath = path.join(webappsPath, appFolder);
177
-
178
- if (!appFolder || !fs.existsSync(explodedPath)) {
179
- return;
180
- }
210
+ if (!appFolder || !fs.existsSync(explodedPath)) return;
181
211
 
182
212
  const parts = filename.split(/[/\\]/);
183
213
  const webappIndex = parts.indexOf("webapp");
@@ -196,7 +226,7 @@ export class DeployCommand implements Command {
196
226
  if (!config.project.quiet) Logger.success(`Synced ${path.basename(filename)} directly to Tomcat!`);
197
227
 
198
228
  const appUrl = `http://localhost:${config.tomcat.port}/${appFolder}`;
199
- await this.reloadBrowser(appUrl);
229
+ await BrowserService.reload(appUrl);
200
230
  } catch (e) {
201
231
  Logger.error(`Failed to sync resource: ${filename}`);
202
232
  }