@archznn/xavva 1.6.5 → 1.7.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 +12 -46
- package/package.json +1 -1
- package/src/commands/AuditCommand.ts +0 -4
- package/src/commands/DeployCommand.ts +84 -41
- package/src/commands/DocsCommand.ts +11 -11
- package/src/commands/DoctorCommand.ts +231 -68
- package/src/commands/RunCommand.ts +4 -3
- package/src/commands/StartCommand.ts +1 -1
- package/src/index.ts +29 -10
- package/src/services/AuditService.ts +0 -5
- package/src/services/BuildService.ts +7 -11
- package/src/services/EndpointService.ts +0 -3
- package/src/services/TomcatService.ts +138 -12
- package/src/utils/config.ts +1 -1
- package/src/utils/ui.ts +196 -102
package/README.md
CHANGED
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
# XAVVA 🚀 (Windows Only) `v1.
|
|
1
|
+
# XAVVA 🚀 (Windows Only) `v1.7.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.
|
|
7
|
-
|
|
8
5
|
## 🛠️ Funcionalidades de Elite
|
|
9
6
|
|
|
10
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.
|
|
11
9
|
- **🛠️ Modo Dev Inteligente**: O comando `xavva dev` ativa hot-reload, logs limpos, debugger (JPDA) e monitoramento de memória em um único fluxo.
|
|
12
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.
|
|
13
|
-
- **🔍 API Documentation (Swagger-like)**: O comando `xavva docs` mapeia estaticamente sua API, exibindo endpoints, métodos HTTP e parâmetros
|
|
14
|
-
- **📊 Real-time Log Filtering**: Filtra ruídos excessivos do Tomcat/Jersey/SLF4J, destacando erros Java com dicas de solução
|
|
15
|
-
- **📈 JVM & Memory Monitor**: Exibe o consumo de RAM
|
|
16
|
-
- **🩺 Doctor Mode**: Diagnostica o ambiente
|
|
17
|
-
- **🛡️ JAR Audit**:
|
|
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).
|
|
18
16
|
|
|
19
17
|
## 🚀 Instalação e Uso
|
|
20
18
|
|
|
21
|
-
Você pode instalar o Xavva globalmente usando o NPM (requer [Bun](https://bun.sh/) instalado no sistema):
|
|
22
|
-
|
|
23
19
|
```bash
|
|
24
20
|
# Instalação global
|
|
25
21
|
npm install -g @archznn/xavva
|
|
@@ -30,46 +26,16 @@ npx @archznn/xavva dev
|
|
|
30
26
|
|
|
31
27
|
## ⚙️ Zero Config & Auto-Detection
|
|
32
28
|
|
|
33
|
-
O Xavva
|
|
34
|
-
|
|
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.
|
|
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`.
|
|
38
30
|
|
|
39
31
|
### Comandos Principais
|
|
40
32
|
|
|
41
33
|
```bash
|
|
42
|
-
#
|
|
43
|
-
xavva
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
xavva dev -p C:\tomcat-9 -P production
|
|
47
|
-
|
|
48
|
-
# Exibe a documentação da API
|
|
49
|
-
xavva docs
|
|
50
|
-
|
|
51
|
-
# Audita vulnerabilidades nas dependências JAR do app
|
|
52
|
-
xavva audit
|
|
53
|
-
|
|
54
|
-
# Diagnostica o ambiente e limpa arquivos com BOM (UTF-8 signature)
|
|
55
|
-
xavva doctor --fix
|
|
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
|
|
56
38
|
```
|
|
57
39
|
|
|
58
|
-
### Opções Úteis
|
|
59
|
-
|
|
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.
|
|
66
|
-
|
|
67
|
-
## 📦 Stack Tecnológica
|
|
68
|
-
|
|
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)
|
|
73
|
-
|
|
74
40
|
---
|
|
75
41
|
*Desenvolvido para transformar a experiência de desenvolvimento Java Legacy em algo ágil e produtivo.*
|
package/package.json
CHANGED
|
@@ -11,12 +11,10 @@ export class AuditCommand implements Command {
|
|
|
11
11
|
|
|
12
12
|
let appName = config.project.appName;
|
|
13
13
|
|
|
14
|
-
// 1. Tentar inferir do diretório atual se não foi passado via config
|
|
15
14
|
if (!appName) {
|
|
16
15
|
appName = this.inferFromArtifacts();
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
// 2. Se ainda não tem nome, tenta inferir do Tomcat
|
|
20
18
|
if (!appName) {
|
|
21
19
|
const webappsPath = path.join(config.tomcat.path, "webapps");
|
|
22
20
|
if (fs.existsSync(webappsPath)) {
|
|
@@ -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
|
|
@@ -32,16 +32,27 @@ export class DeployCommand implements Command {
|
|
|
32
32
|
const builder = this.builder || new BuildService(config.project, config.tomcat);
|
|
33
33
|
|
|
34
34
|
if (!incremental) {
|
|
35
|
-
Logger.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
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)");
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
const srcPath = path.join(process.cwd(), "src");
|
|
@@ -49,12 +60,11 @@ export class DeployCommand implements Command {
|
|
|
49
60
|
const contextPath = (config.project.appName || "").replace(".war", "");
|
|
50
61
|
const endpoints = EndpointService.scan(srcPath, contextPath);
|
|
51
62
|
if (endpoints.length > 0) {
|
|
52
|
-
Logger.
|
|
63
|
+
Logger.config("Endpoints", endpoints.length);
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
} else {
|
|
56
|
-
|
|
57
|
-
Logger.warn("Re-deploying detected changes...");
|
|
67
|
+
Logger.watcher("Change detected", "change");
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
try {
|
|
@@ -65,45 +75,64 @@ export class DeployCommand implements Command {
|
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
if (!config.project.skipBuild) {
|
|
78
|
+
if (incremental) Logger.watcher("Incremental compilation", "start");
|
|
68
79
|
await builder.runBuild(incremental);
|
|
80
|
+
if (!incremental) Logger.build("Full project build");
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
if (incremental) {
|
|
72
|
-
const
|
|
73
|
-
const actualContextPath = contextPath ||
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
84
|
+
const actualAppFolder = await builder.syncClasses(true);
|
|
85
|
+
const actualContextPath = contextPath || actualAppFolder || "";
|
|
86
|
+
const actualAppUrl = `http://localhost:${config.tomcat.port}/${actualContextPath}`;
|
|
87
|
+
await this.reloadBrowser(actualAppUrl);
|
|
88
|
+
Logger.watcher("Redeploy completed", "success");
|
|
78
89
|
return;
|
|
79
90
|
}
|
|
80
91
|
|
|
81
|
-
Logger.
|
|
82
|
-
|
|
92
|
+
Logger.build("Webapps cleaned");
|
|
93
|
+
const artifactInfo = await builder.deployToWebapps();
|
|
94
|
+
Logger.build("Artifacts generated");
|
|
95
|
+
|
|
96
|
+
const finalContextPath = contextPath || artifactInfo.finalName.replace(".war", "");
|
|
97
|
+
const appWebappPath = path.join(config.tomcat.path, "webapps", finalContextPath);
|
|
98
|
+
|
|
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)");
|
|
108
|
+
}
|
|
83
109
|
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
const webInfClassesDir = path.join(appWebappPath, "WEB-INF", "classes");
|
|
111
|
+
if (!fs.existsSync(webInfClassesDir)) fs.mkdirSync(webInfClassesDir, { recursive: true });
|
|
86
112
|
|
|
87
|
-
const
|
|
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
|
+
}
|
|
117
|
+
|
|
88
118
|
const finalAppUrl = `http://localhost:${config.tomcat.port}/${finalContextPath}`;
|
|
89
119
|
|
|
90
120
|
tomcat.onReady = async () => {
|
|
91
|
-
Logger.step(`Checking health at ${finalAppUrl}`);
|
|
92
|
-
|
|
93
121
|
try {
|
|
94
122
|
await new Promise(r => setTimeout(r, 1500));
|
|
95
|
-
|
|
96
123
|
const response = await fetch(finalAppUrl);
|
|
97
124
|
if (response.status < 500) {
|
|
98
125
|
const memory = await tomcat.getMemoryUsage();
|
|
99
|
-
Logger.
|
|
126
|
+
Logger.health(finalAppUrl, "success");
|
|
127
|
+
Logger.health(`Status ${response.status}`, "success");
|
|
128
|
+
Logger.health(`Memory ${memory}`, "success");
|
|
100
129
|
|
|
101
130
|
if (!config.project.quiet) {
|
|
102
131
|
const endpoints = EndpointService.scan(path.join(process.cwd(), "src"), finalContextPath);
|
|
103
132
|
if (endpoints.length > 0) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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}`));
|
|
107
136
|
}
|
|
108
137
|
}
|
|
109
138
|
|
|
@@ -118,14 +147,14 @@ export class DeployCommand implements Command {
|
|
|
118
147
|
}
|
|
119
148
|
}
|
|
120
149
|
} else {
|
|
121
|
-
Logger.
|
|
150
|
+
Logger.health(`App returned status ${response.status}`, "warn");
|
|
122
151
|
}
|
|
123
152
|
} catch (e) {
|
|
124
|
-
Logger.
|
|
153
|
+
Logger.health(`Could not connect to ${finalAppUrl}`, "error");
|
|
125
154
|
}
|
|
126
155
|
};
|
|
127
156
|
|
|
128
|
-
tomcat.start(config
|
|
157
|
+
tomcat.start(config, isWatching);
|
|
129
158
|
} catch (error: any) {
|
|
130
159
|
Logger.error(error.message);
|
|
131
160
|
throw error;
|
|
@@ -133,26 +162,40 @@ export class DeployCommand implements Command {
|
|
|
133
162
|
}
|
|
134
163
|
|
|
135
164
|
async syncResource(config: AppConfig, filename: string): Promise<void> {
|
|
136
|
-
const
|
|
137
|
-
|
|
165
|
+
const contextPath = (config.project.appName || "").replace(".war", "");
|
|
166
|
+
|
|
167
|
+
const webappsPath = path.join(config.tomcat.path, "webapps");
|
|
168
|
+
let appFolder = contextPath;
|
|
138
169
|
|
|
139
|
-
if (!fs.existsSync(
|
|
170
|
+
if (!appFolder && fs.existsSync(webappsPath)) {
|
|
171
|
+
const folders = fs.readdirSync(webappsPath, { withFileTypes: true })
|
|
172
|
+
.filter(dirent => dirent.isDirectory() && !["ROOT", "manager", "host-manager", "docs"].includes(dirent.name));
|
|
173
|
+
if (folders.length === 1) appFolder = folders[0].name;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const explodedPath = path.join(webappsPath, appFolder);
|
|
177
|
+
|
|
178
|
+
if (!appFolder || !fs.existsSync(explodedPath)) {
|
|
140
179
|
return;
|
|
141
180
|
}
|
|
142
181
|
|
|
143
182
|
const parts = filename.split(/[/\\]/);
|
|
144
183
|
const webappIndex = parts.indexOf("webapp");
|
|
184
|
+
const webContentIndex = parts.indexOf("WebContent");
|
|
185
|
+
const rootIndex = webappIndex !== -1 ? webappIndex : webContentIndex;
|
|
145
186
|
|
|
146
|
-
if (
|
|
147
|
-
const relPath = parts.slice(
|
|
187
|
+
if (rootIndex !== -1) {
|
|
188
|
+
const relPath = parts.slice(rootIndex + 1).join(path.sep);
|
|
148
189
|
const targetPath = path.join(explodedPath, relPath);
|
|
149
190
|
|
|
150
191
|
try {
|
|
192
|
+
const targetDir = path.dirname(targetPath);
|
|
193
|
+
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
194
|
+
|
|
151
195
|
fs.copyFileSync(filename, targetPath);
|
|
152
196
|
if (!config.project.quiet) Logger.success(`Synced ${path.basename(filename)} directly to Tomcat!`);
|
|
153
197
|
|
|
154
|
-
const
|
|
155
|
-
const appUrl = `http://localhost:${config.tomcat.port}/${contextPath}`;
|
|
198
|
+
const appUrl = `http://localhost:${config.tomcat.port}/${appFolder}`;
|
|
156
199
|
await this.reloadBrowser(appUrl);
|
|
157
200
|
} catch (e) {
|
|
158
201
|
Logger.error(`Failed to sync resource: ${filename}`);
|
|
@@ -49,12 +49,12 @@ export class DocsCommand implements Command {
|
|
|
49
49
|
|
|
50
50
|
private renderEndpoint(ep: ApiEndpoint, port: number) {
|
|
51
51
|
const methodColors: Record<string, string> = {
|
|
52
|
-
GET: "\x1b[32m",
|
|
53
|
-
POST: "\x1b[33m",
|
|
54
|
-
PUT: "\x1b[34m",
|
|
55
|
-
DELETE: "\x1b[31m",
|
|
56
|
-
PATCH: "\x1b[35m",
|
|
57
|
-
ALL: "\x1b[37m"
|
|
52
|
+
GET: "\x1b[32m",
|
|
53
|
+
POST: "\x1b[33m",
|
|
54
|
+
PUT: "\x1b[34m",
|
|
55
|
+
DELETE: "\x1b[31m",
|
|
56
|
+
PATCH: "\x1b[35m",
|
|
57
|
+
ALL: "\x1b[37m"
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
const color = methodColors[ep.method] || "\x1b[37m";
|
|
@@ -75,11 +75,11 @@ export class DocsCommand implements Command {
|
|
|
75
75
|
|
|
76
76
|
private renderParameter(param: ApiParam) {
|
|
77
77
|
const sourceColors: Record<string, string> = {
|
|
78
|
-
PATH: "\x1b[35m",
|
|
79
|
-
QUERY: "\x1b[36m",
|
|
80
|
-
BODY: "\x1b[33m",
|
|
81
|
-
HEADER: "\x1b[32m",
|
|
82
|
-
FORM: "\x1b[34m"
|
|
78
|
+
PATH: "\x1b[35m",
|
|
79
|
+
QUERY: "\x1b[36m",
|
|
80
|
+
BODY: "\x1b[33m",
|
|
81
|
+
HEADER: "\x1b[32m",
|
|
82
|
+
FORM: "\x1b[34m"
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
const color = sourceColors[param.source] || "\x1b[37m";
|