@archznn/xavva 2.2.1 β 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 +244 -2
- package/package.json +1 -1
- package/src/commands/DepsCommand.ts +4 -3
- package/src/commands/DoctorCommand.ts +1 -1
- package/src/commands/HelpCommand.ts +32 -7
- package/src/commands/ProfilesCommand.ts +1 -1
- package/src/commands/RunCommand.ts +1 -1
- package/src/commands/TomcatCommand.ts +114 -0
- package/src/index.ts +6 -1
- package/src/services/BuildService.ts +12 -4
- package/src/services/DependencyAnalyzerService.ts +26 -2
- package/src/services/EmbeddedTomcatService.ts +391 -0
- package/src/services/LogAnalyzer.ts +1 -1
- package/src/services/ProjectService.ts +6 -1
- package/src/types/config.ts +11 -0
- package/src/utils/config.ts +120 -2
package/README.md
CHANGED
|
@@ -1,3 +1,245 @@
|
|
|
1
|
-
# XAVVA CLI
|
|
1
|
+
# XAVVA CLI π
|
|
2
|
+
|
|
3
|
+
> Ultra-fast development toolkit for Java Enterprise (Tomcat) on Windows
|
|
4
|
+
|
|
5
|
+
[](https://github.com/leorsousa05/Xavva)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
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.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## β¨ Features
|
|
13
|
+
|
|
14
|
+
- β‘ **Hot Reload** β Incremental compilation and class injection without server restart
|
|
15
|
+
- π **Interactive Dashboard** β Real-time TUI with system metrics and shortcuts
|
|
16
|
+
- π§ **Smart Log Analyzer** β Stack trace folding and root cause highlighting
|
|
17
|
+
- π **Security Audit** β Automated vulnerability scanning via OSV.dev
|
|
18
|
+
- π¦ **Dependency Analysis** β Detect conflicts and outdated dependencies
|
|
19
|
+
- π― **Maven & Gradle** β Native support for both build tools
|
|
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
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## π¦ Installation
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
# Via NPM
|
|
30
|
+
npm install -g @archznn/xavva
|
|
31
|
+
|
|
32
|
+
# Or run directly with Bun
|
|
33
|
+
bunx @archznn/xavva dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## π Quick Start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Start development mode with dashboard
|
|
42
|
+
xavva dev --tui
|
|
43
|
+
|
|
44
|
+
# Deploy to Tomcat
|
|
45
|
+
xavva deploy
|
|
46
|
+
|
|
47
|
+
# Build and deploy as .war file
|
|
48
|
+
xavva deploy --war
|
|
49
|
+
|
|
50
|
+
# Analyze dependencies for issues
|
|
51
|
+
xavva deps
|
|
52
|
+
|
|
2
53
|
# Update safe dependencies (non-breaking)
|
|
3
|
-
xavva deps --update-safe
|
|
54
|
+
xavva deps --update-safe
|
|
55
|
+
|
|
56
|
+
# Check for security vulnerabilities
|
|
57
|
+
xavva audit
|
|
58
|
+
|
|
59
|
+
# Use embedded Tomcat (auto-install)
|
|
60
|
+
xavva dev --yes
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## π Commands
|
|
66
|
+
|
|
67
|
+
### Core Development
|
|
68
|
+
|
|
69
|
+
| Command | Description |
|
|
70
|
+
| -------------- | ------------------------------------------------------ |
|
|
71
|
+
| `xavva dev` | Full development mode (build + deploy + watch + debug) |
|
|
72
|
+
| `xavva deploy` | Build and deploy application to Tomcat |
|
|
73
|
+
| `xavva build` | Compile project only |
|
|
74
|
+
| `xavva start` | Start Tomcat server only |
|
|
75
|
+
|
|
76
|
+
### Code Execution
|
|
77
|
+
|
|
78
|
+
| Command | Description |
|
|
79
|
+
| --------------------- | --------------------------------------------- |
|
|
80
|
+
| `xavva run <class>` | Execute a Java class with automatic classpath |
|
|
81
|
+
| `xavva debug <class>` | Debug a Java class (port 5005) |
|
|
82
|
+
|
|
83
|
+
### Analysis & Monitoring
|
|
84
|
+
|
|
85
|
+
| Command | Description |
|
|
86
|
+
| ---------------- | --------------------------------------------------------- |
|
|
87
|
+
| `xavva logs` | Stream and analyze Tomcat logs in real-time |
|
|
88
|
+
| `xavva deps` | **Analyze dependencies** β detect conflicts, find updates |
|
|
89
|
+
| `xavva audit` | Security audit of JAR files via OSV.dev |
|
|
90
|
+
| `xavva doctor` | Diagnose environment issues (JAVA_HOME, DCEVM) |
|
|
91
|
+
| `xavva profiles` | List available Maven/Gradle profiles |
|
|
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
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## π Dependency Analysis
|
|
121
|
+
|
|
122
|
+
The `xavva deps` command provides comprehensive dependency analysis:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Basic analysis
|
|
126
|
+
xavva deps
|
|
127
|
+
|
|
128
|
+
# With verbose output for debugging
|
|
129
|
+
xavva deps --verbose
|
|
130
|
+
|
|
131
|
+
# Update safe dependencies only (non-breaking)
|
|
132
|
+
xavva deps --update-safe
|
|
133
|
+
|
|
134
|
+
# Show fix suggestions for conflicts
|
|
135
|
+
xavva deps --fix
|
|
136
|
+
|
|
137
|
+
# Export report as JSON
|
|
138
|
+
xavva deps --output report.json
|
|
139
|
+
|
|
140
|
+
# Fail on critical conflicts (useful in CI/CD)
|
|
141
|
+
xavva deps --strict
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### What it detects:
|
|
145
|
+
|
|
146
|
+
- β οΈ **Version Conflicts** β Same dependency with different versions
|
|
147
|
+
- β¬οΈ **Available Updates** β Newer versions in Maven Central
|
|
148
|
+
- π΄ **Major Updates** β Breaking changes that need attention
|
|
149
|
+
- π **Statistics** β Direct vs transitive dependencies
|
|
150
|
+
|
|
151
|
+
### Sample output:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
155
|
+
π DEPENDENCY ANALYSIS
|
|
156
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
157
|
+
|
|
158
|
+
Statistics:
|
|
159
|
+
Total: 183 dependencies
|
|
160
|
+
Direct: 45 | Transitivas: 138
|
|
161
|
+
|
|
162
|
+
β οΈ VERSION CONFLICTS (2)
|
|
163
|
+
β com.fasterxml.jackson.core:jackson-databind
|
|
164
|
+
Versions: 2.13.0, 2.12.6
|
|
165
|
+
|
|
166
|
+
β¬οΈ UPDATES AVAILABLE (5)
|
|
167
|
+
β org.postgresql:postgresql
|
|
168
|
+
42.2.5 β 42.7.1
|
|
169
|
+
|
|
170
|
+
β οΈ MAJOR UPDATES (1)
|
|
171
|
+
! org.springframework.boot:spring-boot-starter
|
|
172
|
+
2.5.0 β 3.1.0
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## βοΈ Configuration
|
|
178
|
+
|
|
179
|
+
Create `xavva.json` in your project root:
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"project": {
|
|
184
|
+
"appName": "my-application",
|
|
185
|
+
"buildTool": "maven",
|
|
186
|
+
"profile": "dev",
|
|
187
|
+
"tui": false
|
|
188
|
+
},
|
|
189
|
+
"tomcat": {
|
|
190
|
+
"path": "C:/apache-tomcat",
|
|
191
|
+
"port": 8080
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### CLI Options
|
|
197
|
+
|
|
198
|
+
| Option | Description |
|
|
199
|
+
| ---------------------- | ------------------------------- |
|
|
200
|
+
| `-p, --path <path>` | Tomcat installation path |
|
|
201
|
+
| `-t, --tool <tool>` | Build tool: `maven` or `gradle` |
|
|
202
|
+
| `-n, --name <name>` | Application name (WAR context) |
|
|
203
|
+
| `--port <port>` | Tomcat port (default: 8080) |
|
|
204
|
+
| `-P, --profile <prof>` | Maven/Gradle profile |
|
|
205
|
+
| `-e, --encoding <enc>` | Source encoding (utf8, cp1252) |
|
|
206
|
+
| `-w, --watch` | Enable file watching |
|
|
207
|
+
| `--tui` | Interactive dashboard mode |
|
|
208
|
+
| `-d, --debug` | Enable JPDA debugger |
|
|
209
|
+
| `-c, --clean` | Clean logs before start |
|
|
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) |
|
|
214
|
+
| `-V, --verbose` | Detailed output |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## ποΈ Architecture
|
|
219
|
+
|
|
220
|
+
Xavva uses a modular service-oriented architecture:
|
|
221
|
+
|
|
222
|
+
- **DashboardService** β TUI management and interactivity
|
|
223
|
+
- **LogAnalyzer** β Intelligent log processing
|
|
224
|
+
- **DependencyAnalyzerService** β Dependency conflict detection
|
|
225
|
+
- **ProjectService** β Project structure discovery
|
|
226
|
+
- **BuildService** β Maven/Gradle integration
|
|
227
|
+
- **TomcatService** β Server lifecycle management
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## π€ Contributing
|
|
232
|
+
|
|
233
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## π License
|
|
238
|
+
|
|
239
|
+
MIT License β see [LICENSE](LICENSE) for details.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
<p align="center">
|
|
244
|
+
<sub>Built with β€οΈ for Java developers who miss modern tooling</sub>
|
|
245
|
+
</p>
|
package/package.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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:");
|
|
@@ -142,7 +142,8 @@ export class DepsCommand implements Command {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
Logger.newline();
|
|
145
|
-
Logger.log(`${Logger.C.warning}β οΈ Execute '
|
|
145
|
+
Logger.log(`${Logger.C.warning}β οΈ Execute 'xavva build' para compilar e aplicar as mudanΓ§as${Logger.C.reset}`);
|
|
146
|
+
Logger.log(`${Logger.C.primary}π‘ Dica:${Logger.C.reset} Execute 'xavva audit' para verificar vulnerabilidades nas novas versΓ΅es`);
|
|
146
147
|
} else {
|
|
147
148
|
Logger.warn("Nenhuma dependΓͺncia foi atualizada");
|
|
148
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.
|
|
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
|
|
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", "#
|
|
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", "#
|
|
64
|
-
xavva
|
|
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", "#
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
33
|
+
// Cache sΓ³ Γ© 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:
|
|
47
|
-
if (!this.cache.shouldRebuild('maven', this.projectService)) {
|
|
49
|
+
// Smart Offline: sΓ³ 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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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];
|
package/src/types/config.ts
CHANGED
|
@@ -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 {
|
package/src/utils/config.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|