@archznn/xavva 1.6.5 → 1.8.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
@@ -1,75 +1,111 @@
1
- # XAVVA 🚀 (Windows Only) `v1.6.5`
1
+ # XAVVA 🚀 (Windows Only) `v1.8.0`
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
- > [!IMPORTANT]
6
- > **Compatibilidade:** Atualmente, o Xavva é exclusivo para **Windows**, utilizando integrações nativas com PowerShell e CMD para automação de browser e gerenciamento de processos.
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.
7
10
 
8
- ## 🛠️ Funcionalidades de Elite
11
+ ### Funcionalidades de Elite
9
12
 
10
- - **⚡ 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.
11
- - **🛠️ Modo Dev Inteligente**: O comando `xavva dev` ativa hot-reload, logs limpos, debugger (JPDA) e monitoramento de memória em um único fluxo.
12
- - **🌐 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.
13
- - **🔍 API Documentation (Swagger-like)**: O comando `xavva docs` mapeia estaticamente sua API, exibindo endpoints, métodos HTTP e parâmetros (Query, Path, Body) diretamente no terminal.
14
- - **📊 Real-time Log Filtering**: Filtra ruídos excessivos do Tomcat/Jersey/SLF4J, destacando erros Java com dicas de solução e tempo de startup.
15
- - **📈 JVM & Memory Monitor**: Exibe o consumo de RAM (Working Set) do processo do Tomcat em tempo real.
16
- - **🩺 Doctor Mode**: Diagnostica o ambiente (Java, Tomcat, Maven, Gradle) e corrige automaticamente problemas de **Encoding (UTF-8 BOM)** que podem causar falhas silenciosas no Java.
17
- - **🛡️ JAR Audit**: O comando `xavva audit` analisa todas as dependências (`.jar`) da sua aplicação e verifica vulnerabilidades conhecidas (CVEs) usando o banco de dados **OSV.dev**.
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.
21
+
22
+ ---
18
23
 
19
- ## 🚀 Instalação e Uso
24
+ ## 🚀 Começo Rápido
20
25
 
21
- Você pode instalar o Xavva globalmente usando o NPM (requer [Bun](https://bun.sh/) instalado no sistema):
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`.
22
30
 
23
- ```bash
24
- # Instalação global
31
+ ### Instalação
32
+ ```powershell
33
+ # Instalação global via NPM
25
34
  npm install -g @archznn/xavva
26
35
 
27
- # Ou rodar sem instalar via npx
36
+ # Ou use diretamente via npx
28
37
  npx @archznn/xavva dev
29
38
  ```
30
39
 
31
- ## ⚙️ Zero Config & Auto-Detection
40
+ ---
32
41
 
33
- O Xavva foi evoluído para um modelo **Zero Config**. Você não precisa mais de arquivos de configuração para começar.
42
+ ## 📖 Referência de Comandos
34
43
 
35
- - **Auto-Detecção:** O Xavva identifica automaticamente se seu projeto usa **Maven** (`pom.xml`) ou **Gradle** (`build.gradle`) ao ser executado na raiz.
36
- - **Ambiente Inteligente:** Ele utiliza as variáveis de ambiente `TOMCAT_HOME` ou `CATALINA_HOME` para localizar o servidor.
37
- - **Prioridade CLI:** Qualquer parâmetro passado via linha de comando (como `--path` ou `--port`) tem precedência total sobre o ambiente.
44
+ O Xavva utiliza uma arquitetura modular de comandos e serviços, garantindo alta performance e extensibilidade.
38
45
 
39
- ### Comandos Principais
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).
40
54
 
41
- ```bash
42
- # Inicia o modo de desenvolvimento completo (Auto-detecta Maven/Gradle)
43
- xavva dev
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.
44
59
 
45
- # Define o Tomcat e o Profile manualmente via CLI
46
- xavva dev -p C:\tomcat-9 -P production
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).
47
63
 
48
- # Exibe a documentação da API
49
- xavva docs
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.
50
70
 
51
- # Audita vulnerabilidades nas dependências JAR do app
52
- xavva audit
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.
53
73
 
54
- # Diagnostica o ambiente e limpa arquivos com BOM (UTF-8 signature)
55
- xavva doctor --fix
56
- ```
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
+ ---
57
78
 
58
- ### Opções Úteis
79
+ ## 🏗️ Arquitetura do Sistema
59
80
 
60
- - `-p, --path <path>`: Caminho customizado do Tomcat (Sobrescreve TOMCAT_HOME).
61
- - `-P, --profile <nome>`: Define o profile do Maven/Gradle (ex: dev, prod).
62
- - `-t, --tool <maven|gradle>`: Força o uso de uma ferramenta específica.
63
- - `-n, --name <nome>`: Define o nome do contexto da aplicação.
64
- - `-w, --watch`: Ativa o monitoramento de arquivos para hot-reload.
65
- - `-d, --debug`: Habilita o Java Debugger na porta 5005.
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
+ ---
66
102
 
67
- ## 📦 Stack Tecnológica
103
+ ## 🧩 Sincronização de Recursos
68
104
 
69
- - **Runtime:** [Bun](https://bun.sh/) (Engine de alta performance)
70
- - **Linguagem:** [TypeScript](https://www.typescriptlang.org/)
71
- - **Automação:** PowerShell & CMD (Integração nativa Windows)
72
- - **CI/CD:** GitHub Actions para geração de binários multi-plataforma (via Bun Compile)
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.
73
109
 
74
110
  ---
75
- *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.6.5",
3
+ "version": "1.8.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,22 +1,22 @@
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
- // 1. Tentar inferir do diretório atual se não foi passado via config
15
16
  if (!appName) {
16
17
  appName = this.inferFromArtifacts();
17
18
  }
18
19
 
19
- // 2. Se ainda não tem nome, tenta inferir do Tomcat
20
20
  if (!appName) {
21
21
  const webappsPath = path.join(config.tomcat.path, "webapps");
22
22
  if (fs.existsSync(webappsPath)) {
@@ -40,10 +40,8 @@ export class AuditCommand implements Command {
40
40
  return;
41
41
  }
42
42
 
43
- const auditService = new AuditService(config.tomcat);
44
-
45
43
  try {
46
- const results = await auditService.runAudit(appName);
44
+ const results = await this.auditService.runAudit(appName);
47
45
  const vulnerable = results.filter(r => r.vulnerabilities.length > 0);
48
46
 
49
47
  if (vulnerable.length === 0) {
@@ -68,14 +66,12 @@ export class AuditCommand implements Command {
68
66
  }
69
67
 
70
68
  private inferFromArtifacts(): string | undefined {
71
- // Busca .war no target (Maven) ou build/libs (Gradle)
72
69
  const paths = ["target", "build/libs"];
73
70
  for (const p of paths) {
74
71
  const fullPath = path.join(process.cwd(), p);
75
72
  if (fs.existsSync(fullPath)) {
76
73
  const wars = fs.readdirSync(fullPath).filter(f => f.endsWith(".war"));
77
74
  if (wars.length > 0) {
78
- // Retorna o nome do .war mais recente sem a extensão
79
75
  const latest = wars.map(name => ({
80
76
  name,
81
77
  time: fs.statSync(path.join(fullPath, name)).mtimeMs
@@ -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,159 +1,235 @@
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.section("Deploy Configuration");
36
- if (config.project.quiet) {
37
- Logger.info("App", `${config.project.appName} (${config.project.buildTool.toUpperCase()}${config.project.profile ? ` - ${config.project.profile}` : ""})`);
38
- Logger.info("Status", `Watch: ${isWatching ? "ON" : "OFF"} | Debug: ${config.project.debug ? "ON" : "OFF"}`);
39
- } else {
40
- Logger.info("Tool", config.project.buildTool.toUpperCase());
41
- Logger.info("App Name", config.project.appName);
42
- if (config.project.profile) Logger.info("Profile", config.project.profile);
43
- Logger.info("Watch Mode", isWatching ? "Active" : "Inactive");
44
- Logger.info("Debug Mode", config.project.debug ? "Active" : "Inactive");
45
- }
46
-
47
- const srcPath = path.join(process.cwd(), "src");
48
- if (fs.existsSync(srcPath)) {
49
- const contextPath = (config.project.appName || "").replace(".war", "");
50
- const endpoints = EndpointService.scan(srcPath, contextPath);
51
- if (endpoints.length > 0) {
52
- Logger.info("Endpoints", endpoints.length);
53
- }
54
- }
21
+ this.logConfiguration(config, isWatching);
55
22
  } else {
56
- console.log("");
57
- Logger.warn("Re-deploying detected changes...");
23
+ Logger.watcher("Change detected", "change");
58
24
  }
59
25
 
60
26
  try {
61
27
  const contextPath = (config.project.appName || "").replace(".war", "");
62
28
 
63
29
  if (!incremental) {
64
- await tomcat.killConflict();
65
- }
30
+ tomcat.clearWebapps(contextPath);
66
31
 
67
- if (!config.project.skipBuild) {
68
- await builder.runBuild(incremental);
32
+ const buildTask = config.project.skipBuild ? Promise.resolve() : builder.runBuild(incremental);
33
+ const killTask = tomcat.killConflict();
34
+
35
+ if (!config.project.skipBuild) {
36
+ Logger.watcher("Preparing environment and building in parallel", "start");
37
+ }
38
+
39
+ await Promise.all([buildTask, killTask]);
40
+
41
+ if (!config.project.skipBuild) {
42
+ Logger.build("Full project build and environment ready");
43
+ }
44
+ } else {
45
+ if (!config.project.skipBuild) {
46
+ Logger.watcher("Incremental compilation", "start");
47
+ await builder.runBuild(incremental);
48
+ }
69
49
  }
70
50
 
71
51
  if (incremental) {
72
- const appFolder = await builder.syncClasses();
73
- const actualContextPath = contextPath || appFolder || "";
74
- if (actualContextPath) {
75
- const actualAppUrl = `http://localhost:${config.tomcat.port}/${actualContextPath}`;
76
- await this.reloadBrowser(actualAppUrl);
77
- }
52
+ const actualAppFolder = await builder.syncClasses();
53
+ const actualContextPath = contextPath || actualAppFolder || "";
54
+ const actualAppUrl = `http://localhost:${config.tomcat.port}/${actualContextPath}`;
55
+ await BrowserService.reload(actualAppUrl);
56
+ Logger.watcher("Redeploy completed", "success");
78
57
  return;
79
58
  }
80
59
 
81
- Logger.step("Cleaning webapps and cache");
82
- tomcat.clearWebapps();
83
-
84
- Logger.step("Moving artifacts to webapps");
85
- const artifact = await builder.deployToWebapps();
86
-
87
- const finalContextPath = contextPath || artifact.replace(".war", "");
88
- const finalAppUrl = `http://localhost:${config.tomcat.port}/${finalContextPath}`;
60
+ Logger.build("Webapps cleaned");
61
+ const artifactInfo = await builder.deployToWebapps();
62
+ Logger.build("Artifacts generated");
89
63
 
90
- tomcat.onReady = async () => {
91
- Logger.step(`Checking health at ${finalAppUrl}`);
92
-
93
- try {
94
- await new Promise(r => setTimeout(r, 1500));
95
-
96
- const response = await fetch(finalAppUrl);
97
- if (response.status < 500) {
98
- const memory = await tomcat.getMemoryUsage();
99
- Logger.success(`App is UP! (Status: ${response.status} | RAM: ${memory})`);
100
-
101
- if (!config.project.quiet) {
102
- const endpoints = EndpointService.scan(path.join(process.cwd(), "src"), finalContextPath);
103
- if (endpoints.length > 0) {
104
- console.log(`\n ${"\x1b[36m"}◈ ENDPOINT MAP:${"\x1b[0m"}`);
105
- endpoints.forEach(e => console.log(` ${"\x1b[90m"}➜${"\x1b[0m"} http://localhost:${config.tomcat.port}${e.fullPath}`));
106
- console.log("");
107
- }
108
- }
109
-
110
- if (incremental) {
111
- await this.reloadBrowser(finalAppUrl);
112
- } else {
113
- if (process.platform === 'win32') {
114
- Bun.spawn(["cmd", "/c", "start", finalAppUrl]);
115
- } else {
116
- const start = process.platform === 'darwin' ? 'open' : 'xdg-open';
117
- Bun.spawn([start, finalAppUrl]);
64
+ const finalContextPath = contextPath || artifactInfo.finalName.replace(".war", "");
65
+ const appWebappPath = path.join(config.tomcat.path, "webapps", finalContextPath);
66
+
67
+ if (artifactInfo.isDirectory) {
68
+ await builder.syncClasses(artifactInfo.path);
69
+ Logger.build("Exploded directory synced");
70
+ } else {
71
+ if (!fs.existsSync(appWebappPath)) fs.mkdirSync(appWebappPath, { recursive: true });
72
+
73
+ const artifactStat = fs.statSync(artifactInfo.path);
74
+ const webappStat = fs.existsSync(appWebappPath) ? fs.statSync(appWebappPath) : null;
75
+
76
+ if (!webappStat || artifactStat.mtimeMs > webappStat.mtimeMs) {
77
+ try {
78
+ Bun.spawnSync(["jar", "xf", artifactInfo.path], { cwd: appWebappPath });
79
+ Logger.build("Artifacts deployed");
80
+ } catch (e) {
81
+ const extractCmd = `Expand-Archive -Path $env:ARTIFACT_PATH -DestinationPath $env:DEST_PATH -Force`;
82
+ Bun.spawnSync(["powershell", "-command", extractCmd], {
83
+ env: {
84
+ ...process.env,
85
+ ARTIFACT_PATH: artifactInfo.path,
86
+ DEST_PATH: appWebappPath
118
87
  }
119
- }
120
- } else {
121
- Logger.warn(`App is starting, but returned status ${response.status}. Check your logs.`);
88
+ });
89
+ Logger.build("Artifacts deployed (legacy mode)");
122
90
  }
123
- } catch (e) {
124
- Logger.error(`Health check failed: Could not connect to ${finalAppUrl}`);
91
+ } else {
92
+ Logger.build("Webapp already up to date, skipping extraction");
125
93
  }
94
+ }
95
+
96
+ this.injectHotswapProperties(appWebappPath);
97
+
98
+ const finalAppUrl = `http://localhost:${config.tomcat.port}/${finalContextPath}`;
99
+
100
+ tomcat.onReady = async () => {
101
+ await this.handleServerReady(config, finalAppUrl, finalContextPath, tomcat, incremental);
126
102
  };
127
103
 
128
- tomcat.start(config.project.cleanLogs, config.project.debug, config.project.skipScan, config.project.quiet);
104
+ tomcat.start(config, isWatching);
129
105
  } catch (error: any) {
130
106
  Logger.error(error.message);
131
107
  throw error;
132
108
  }
133
109
  }
134
110
 
111
+ private logConfiguration(config: AppConfig, isWatching: boolean) {
112
+ Logger.config("Runtime", config.project.buildTool.toUpperCase());
113
+ Logger.config("Watch Mode", isWatching ? "ON" : "OFF");
114
+ Logger.config("Debug", config.project.debug ? `ON (Port ${config.project.debugPort})` : "OFF");
115
+
116
+ let javaBin = "java";
117
+ if (process.env.JAVA_HOME) {
118
+ const homeBin = path.join(process.env.JAVA_HOME, "bin", "java.exe");
119
+ if (fs.existsSync(homeBin)) javaBin = homeBin;
120
+ }
121
+
122
+ const javaVer = Bun.spawnSync([javaBin, "-version"]);
123
+ const output = (javaVer.stderr.toString() + javaVer.stdout.toString()).toLowerCase();
124
+ const hasDcevm = ["dcevm", "jetbrains", "trava", "jbr"].some(v => output.includes(v));
125
+
126
+ if (!hasDcevm && isWatching) {
127
+ Logger.config("Hot Reload", "Standard (No structural changes)");
128
+ } else if (hasDcevm) {
129
+ Logger.config("Hot Reload", "Advanced (DCEVM Active)");
130
+ }
131
+
132
+ const srcPath = path.join(process.cwd(), "src");
133
+ if (fs.existsSync(srcPath)) {
134
+ const contextPath = (config.project.appName || "").replace(".war", "");
135
+ const endpoints = EndpointService.scan(srcPath, contextPath);
136
+ if (endpoints.length > 0) {
137
+ Logger.config("Endpoints", endpoints.length);
138
+ }
139
+ }
140
+ }
141
+
142
+ private injectHotswapProperties(appWebappPath: string) {
143
+ const webInfClassesDir = path.join(appWebappPath, "WEB-INF", "classes");
144
+ if (!fs.existsSync(webInfClassesDir)) fs.mkdirSync(webInfClassesDir, { recursive: true });
145
+
146
+ const xavvaProps = path.join(process.cwd(), ".xavva", "hotswap-agent.properties");
147
+ if (fs.existsSync(xavvaProps)) {
148
+ fs.copyFileSync(xavvaProps, path.join(webInfClassesDir, "hotswap-agent.properties"));
149
+ }
150
+ }
151
+
152
+ private async handleServerReady(config: AppConfig, url: string, context: string, tomcat: TomcatService, incremental: boolean) {
153
+ try {
154
+ await new Promise(r => setTimeout(r, 1500));
155
+ const response = await fetch(url);
156
+ if (response.status < 500) {
157
+ const memory = await tomcat.getMemoryUsage();
158
+ Logger.health(url, "success");
159
+ Logger.health(`Status ${response.status}`, "success");
160
+ Logger.health(`Memory ${memory}`, "success");
161
+
162
+ if (!config.project.quiet) {
163
+ this.showEndpointMap(config.tomcat.port, context);
164
+ }
165
+
166
+ if (incremental) {
167
+ await BrowserService.reload(url);
168
+ } else {
169
+ BrowserService.open(url);
170
+ }
171
+ } else {
172
+ Logger.health(`App returned status ${response.status}`, "warn");
173
+ }
174
+ } catch (e) {
175
+ Logger.health(`Could not connect to ${url}`, "error");
176
+ }
177
+ }
178
+
179
+ private showEndpointMap(port: number, context: string) {
180
+ const endpoints = EndpointService.scan(path.join(process.cwd(), "src"), context);
181
+ if (endpoints.length > 0) {
182
+ Logger.newline();
183
+ Logger.log(`${Logger.C.cyan}◈ ENDPOINT MAP:${Logger.C.reset}`);
184
+
185
+ const apis = endpoints.filter(e => e.className !== "JSP");
186
+ const jsps = endpoints.filter(e => e.className === "JSP");
187
+
188
+ if (apis.length > 0) {
189
+ const uniqueApiUrls = [...new Set(apis.map(e => `http://localhost:${port}${e.fullPath}`))];
190
+ uniqueApiUrls.forEach(url => Logger.log(`${Logger.C.dim}➜ ${Logger.C.reset}${url}`));
191
+ }
192
+
193
+ if (jsps.length > 0) {
194
+ Logger.log(`${Logger.C.dim}--- JSPs ---${Logger.C.reset}`);
195
+ const uniqueJspUrls = [...new Set(jsps.map(e => `http://localhost:${port}${e.fullPath}`))];
196
+ uniqueJspUrls.forEach(url => Logger.log(`${Logger.C.dim}📄 ${Logger.C.reset}${url}`));
197
+ }
198
+ }
199
+ }
200
+
135
201
  async syncResource(config: AppConfig, filename: string): Promise<void> {
136
- const appName = config.project.appName || "";
137
- const explodedPath = path.join(config.tomcat.path, "webapps", appName);
202
+ const contextPath = (config.project.appName || "").replace(".war", "");
203
+ const webappsPath = path.join(config.tomcat.path, "webapps");
204
+ let appFolder = contextPath;
138
205
 
139
- if (!fs.existsSync(explodedPath)) {
140
- return;
206
+ if (!appFolder && fs.existsSync(webappsPath)) {
207
+ const folders = fs.readdirSync(webappsPath, { withFileTypes: true })
208
+ .filter(dirent => dirent.isDirectory() && !["ROOT", "manager", "host-manager", "docs"].includes(dirent.name));
209
+ if (folders.length === 1) appFolder = folders[0].name;
141
210
  }
142
211
 
212
+ const explodedPath = path.join(webappsPath, appFolder);
213
+ if (!appFolder || !fs.existsSync(explodedPath)) return;
214
+
143
215
  const parts = filename.split(/[/\\]/);
144
216
  const webappIndex = parts.indexOf("webapp");
217
+ const webContentIndex = parts.indexOf("WebContent");
218
+ const rootIndex = webappIndex !== -1 ? webappIndex : webContentIndex;
145
219
 
146
- if (webappIndex !== -1) {
147
- const relPath = parts.slice(webappIndex + 1).join(path.sep);
220
+ if (rootIndex !== -1) {
221
+ const relPath = parts.slice(rootIndex + 1).join(path.sep);
148
222
  const targetPath = path.join(explodedPath, relPath);
149
223
 
150
224
  try {
225
+ const targetDir = path.dirname(targetPath);
226
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
227
+
151
228
  fs.copyFileSync(filename, targetPath);
152
229
  if (!config.project.quiet) Logger.success(`Synced ${path.basename(filename)} directly to Tomcat!`);
153
230
 
154
- const contextPath = config.project.appName || "";
155
- const appUrl = `http://localhost:${config.tomcat.port}/${contextPath}`;
156
- await this.reloadBrowser(appUrl);
231
+ const appUrl = `http://localhost:${config.tomcat.port}/${appFolder}`;
232
+ await BrowserService.reload(appUrl);
157
233
  } catch (e) {
158
234
  Logger.error(`Failed to sync resource: ${filename}`);
159
235
  }