@archznn/xavva 2.5.0 → 2.6.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 +25 -4
- package/package.json +4 -2
- package/src/commands/DeployCommand.ts +27 -15
- package/src/commands/DoctorCommand.ts +129 -76
- package/src/commands/LogsCommand.ts +1 -1
- package/src/commands/RunCommand.ts +21 -11
- package/src/index.ts +1 -1
- package/src/services/BrowserService.ts +12 -9
- package/src/services/BuildService.ts +29 -2
- package/src/services/EmbeddedTomcatService.ts +45 -36
- package/src/services/LogAnalyzer.ts +4 -4
- package/src/services/TomcatService.ts +86 -13
- package/src/utils/platform.ts +323 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# XAVVA CLI 🚀
|
|
2
2
|
|
|
3
|
-
> Ultra-fast development toolkit for Java Enterprise (Tomcat) on Windows
|
|
3
|
+
> Ultra-fast development toolkit for Java Enterprise (Tomcat) on Windows, Linux & macOS
|
|
4
4
|
|
|
5
|
-
[](https://github.com/leorsousa05/Xavva)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
Xavva is a high-performance CLI built with **Bun** that transforms the Java/Tomcat development experience. It brings modern development workflows (like Node.js/Vite) to the Java Enterprise ecosystem with hot-reload, smart logging, and automated deployment.
|
|
@@ -25,7 +25,7 @@ Xavva is a high-performance CLI built with **Bun** that transforms the Java/Tomc
|
|
|
25
25
|
|
|
26
26
|
## 📦 Installation
|
|
27
27
|
|
|
28
|
-
```
|
|
28
|
+
```bash
|
|
29
29
|
# Via NPM
|
|
30
30
|
npm install -g @archznn/xavva
|
|
31
31
|
|
|
@@ -196,12 +196,14 @@ Create `xavva.json` in your project root:
|
|
|
196
196
|
"tui": false
|
|
197
197
|
},
|
|
198
198
|
"tomcat": {
|
|
199
|
-
"path": "
|
|
199
|
+
"path": "/home/user/apache-tomcat",
|
|
200
200
|
"port": 8080
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
+
> **Note:** On Windows use `"path": "C:/apache-tomcat"` format.
|
|
206
|
+
|
|
205
207
|
### CLI Options
|
|
206
208
|
|
|
207
209
|
| Option | Description |
|
|
@@ -224,6 +226,25 @@ Create `xavva.json` in your project root:
|
|
|
224
226
|
|
|
225
227
|
---
|
|
226
228
|
|
|
229
|
+
## 💻 Platform Support
|
|
230
|
+
|
|
231
|
+
Xavva works on all major platforms:
|
|
232
|
+
|
|
233
|
+
| Platform | Status | Notes |
|
|
234
|
+
|----------|--------|-------|
|
|
235
|
+
| Windows | ✅ Full | PowerShell for system integration |
|
|
236
|
+
| Linux | ✅ Full | Bash/Zsh auto-configuration |
|
|
237
|
+
| macOS | ✅ Full | Native terminal support |
|
|
238
|
+
|
|
239
|
+
### Requirements
|
|
240
|
+
|
|
241
|
+
- **Bun** runtime (latest version)
|
|
242
|
+
- **Java** 11 or higher (JDK)
|
|
243
|
+
- **Maven** 3.6+ or **Gradle** 7+
|
|
244
|
+
- **Git** (optional, for version info)
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
227
248
|
## 🏗️ Architecture
|
|
228
249
|
|
|
229
250
|
Xavva uses a modular service-oriented architecture:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@archznn/xavva",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Ultra-fast CLI tool for Java/Tomcat development
|
|
3
|
+
"version": "2.6.0",
|
|
4
|
+
"description": "Ultra-fast CLI tool for Java/Tomcat development with Hot-Reload and Zero Config. Supports Windows, Linux and macOS.",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"gradle",
|
|
20
20
|
"hot-reload",
|
|
21
21
|
"windows",
|
|
22
|
+
"linux",
|
|
23
|
+
"macos",
|
|
22
24
|
"cli",
|
|
23
25
|
"bun"
|
|
24
26
|
],
|
|
@@ -7,6 +7,11 @@ import { TomcatService } from "../services/TomcatService";
|
|
|
7
7
|
import { Logger } from "../utils/ui";
|
|
8
8
|
import { EndpointService } from "../services/EndpointService";
|
|
9
9
|
import { BrowserService } from "../services/BrowserService";
|
|
10
|
+
import {
|
|
11
|
+
getJavaPath,
|
|
12
|
+
getWarExtractCommand,
|
|
13
|
+
isWindows,
|
|
14
|
+
} from "../utils/platform";
|
|
10
15
|
|
|
11
16
|
export class DeployCommand implements Command {
|
|
12
17
|
constructor(private tomcat: TomcatService, private builder: BuildService) {}
|
|
@@ -78,15 +83,28 @@ export class DeployCommand implements Command {
|
|
|
78
83
|
Bun.spawnSync(["jar", "xf", artifactInfo.path], { cwd: appWebappPath });
|
|
79
84
|
Logger.server("extracted WAR");
|
|
80
85
|
} catch (e) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
env:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
// Fallback para extração com jar (funciona em todas as plataformas)
|
|
87
|
+
if (isWindows()) {
|
|
88
|
+
const extractCmd = `Expand-Archive -Path $env:ARTIFACT_PATH -DestinationPath $env:DEST_PATH -Force`;
|
|
89
|
+
Bun.spawnSync(["powershell", "-command", extractCmd], {
|
|
90
|
+
env: {
|
|
91
|
+
...process.env,
|
|
92
|
+
ARTIFACT_PATH: artifactInfo.path,
|
|
93
|
+
DEST_PATH: appWebappPath
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
// Linux/Mac: usa unzip ou jar
|
|
98
|
+
try {
|
|
99
|
+
Bun.spawnSync(["unzip", "-q", "-o", artifactInfo.path, "-d", appWebappPath]);
|
|
100
|
+
} catch {
|
|
101
|
+
// Fallback final para jar
|
|
102
|
+
Bun.spawnSync(getWarExtractCommand(artifactInfo.path, appWebappPath), {
|
|
103
|
+
cwd: appWebappPath
|
|
104
|
+
});
|
|
87
105
|
}
|
|
88
|
-
}
|
|
89
|
-
Logger.server("extracted WAR (
|
|
106
|
+
}
|
|
107
|
+
Logger.server("extracted WAR (fallback)");
|
|
90
108
|
}
|
|
91
109
|
} else {
|
|
92
110
|
Logger.server("webapp up to date");
|
|
@@ -117,13 +135,7 @@ export class DeployCommand implements Command {
|
|
|
117
135
|
Logger.config("watch", isWatching);
|
|
118
136
|
Logger.config("debug", config.project.debug ? `port ${config.project.debugPort}` : false);
|
|
119
137
|
|
|
120
|
-
|
|
121
|
-
if (process.env.JAVA_HOME) {
|
|
122
|
-
const homeBin = path.join(process.env.JAVA_HOME, "bin", "java.exe");
|
|
123
|
-
if (fs.existsSync(homeBin)) javaBin = homeBin;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const javaVer = Bun.spawnSync([javaBin, "-version"]);
|
|
138
|
+
const javaVer = Bun.spawnSync([getJavaPath(), "-version"]);
|
|
127
139
|
const output = (javaVer.stderr.toString() + javaVer.stdout.toString()).toLowerCase();
|
|
128
140
|
const hasDcevm = ["dcevm", "jetbrains", "trava", "jbr"].some(v => output.includes(v));
|
|
129
141
|
|
|
@@ -4,6 +4,17 @@ import { Logger } from "../utils/ui";
|
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import os from "os";
|
|
7
|
+
import {
|
|
8
|
+
getCatalinaScript,
|
|
9
|
+
getWhichCommand,
|
|
10
|
+
getJbrDownloadUrl,
|
|
11
|
+
getTarExtractCommand,
|
|
12
|
+
getJavaPath,
|
|
13
|
+
getJavaBinary,
|
|
14
|
+
isWindows,
|
|
15
|
+
isLinux,
|
|
16
|
+
isMacOS,
|
|
17
|
+
} from "../utils/platform";
|
|
7
18
|
|
|
8
19
|
export class DoctorCommand implements Command {
|
|
9
20
|
async execute(config: AppConfig, values: CLIArguments = {}): Promise<void> {
|
|
@@ -25,7 +36,7 @@ export class DoctorCommand implements Command {
|
|
|
25
36
|
|
|
26
37
|
if (!jvmInfo.dcevm) {
|
|
27
38
|
Logger.log(
|
|
28
|
-
` ${Logger.C.
|
|
39
|
+
` ${Logger.C.warning}💡 Dica: Sua JVM não suporta mudanças estruturais (novos métodos/campos).${Logger.C.reset}`,
|
|
29
40
|
);
|
|
30
41
|
if (values.fix) {
|
|
31
42
|
await this.installDCEVM();
|
|
@@ -41,12 +52,12 @@ export class DoctorCommand implements Command {
|
|
|
41
52
|
|
|
42
53
|
if (tomcatOk) {
|
|
43
54
|
const binOk = fs.existsSync(
|
|
44
|
-
path.join(config.tomcat.path, "bin",
|
|
55
|
+
path.join(config.tomcat.path, "bin", getCatalinaScript()),
|
|
45
56
|
);
|
|
46
57
|
this.check(
|
|
47
58
|
"Tomcat Bin",
|
|
48
59
|
binOk,
|
|
49
|
-
binOk ? "OK" :
|
|
60
|
+
binOk ? "OK" : `${getCatalinaScript()} não encontrado`,
|
|
50
61
|
);
|
|
51
62
|
}
|
|
52
63
|
|
|
@@ -211,10 +222,7 @@ export class DoctorCommand implements Command {
|
|
|
211
222
|
|
|
212
223
|
private checkBinary(name: string): boolean {
|
|
213
224
|
try {
|
|
214
|
-
const proc = Bun.spawnSync(
|
|
215
|
-
process.platform === "win32" ? "where" : "which",
|
|
216
|
-
name,
|
|
217
|
-
]);
|
|
225
|
+
const proc = Bun.spawnSync(getWhichCommand(name));
|
|
218
226
|
return proc.exitCode === 0;
|
|
219
227
|
} catch {
|
|
220
228
|
return false;
|
|
@@ -224,11 +232,7 @@ export class DoctorCommand implements Command {
|
|
|
224
232
|
private checkJVM(): { name: string, dcevm: boolean } {
|
|
225
233
|
try {
|
|
226
234
|
// Tentar primeiro o binário do JAVA_HOME para evitar cache do Path
|
|
227
|
-
let javaBin =
|
|
228
|
-
if (process.env.JAVA_HOME) {
|
|
229
|
-
const homeBin = path.join(process.env.JAVA_HOME, "bin", "java.exe");
|
|
230
|
-
if (fs.existsSync(homeBin)) javaBin = homeBin;
|
|
231
|
-
}
|
|
235
|
+
let javaBin = getJavaPath();
|
|
232
236
|
|
|
233
237
|
const proc = Bun.spawnSync([javaBin, "-version"]);
|
|
234
238
|
const output = (proc.stderr.toString() + proc.stdout.toString()).toLowerCase();
|
|
@@ -251,8 +255,8 @@ export class DoctorCommand implements Command {
|
|
|
251
255
|
Logger.section("Instalação do JetBrains Runtime (JBR 21)");
|
|
252
256
|
Logger.log("Baixando JDK moderna com DCEVM nativo (JBR 21 SDK)...");
|
|
253
257
|
|
|
254
|
-
// URL para o JetBrains Runtime 21 SDK
|
|
255
|
-
const url = "
|
|
258
|
+
// URL para o JetBrains Runtime 21 SDK (multiplataforma)
|
|
259
|
+
const url = getJbrDownloadUrl("21");
|
|
256
260
|
const installDir = path.join(os.homedir(), ".xavva", "jdk-dcevm");
|
|
257
261
|
|
|
258
262
|
// Limpar instalação anterior se existir
|
|
@@ -272,21 +276,15 @@ export class DoctorCommand implements Command {
|
|
|
272
276
|
|
|
273
277
|
Logger.success("Download concluído. Extraindo binários...");
|
|
274
278
|
|
|
275
|
-
//
|
|
276
|
-
const extractCmd =
|
|
277
|
-
Bun.spawnSync(
|
|
278
|
-
env: {
|
|
279
|
-
...process.env,
|
|
280
|
-
TAR_PATH: tarPath,
|
|
281
|
-
INSTALL_DIR: installDir
|
|
282
|
-
}
|
|
283
|
-
});
|
|
279
|
+
// Extrair .tar.gz usando comando apropriado para a plataforma
|
|
280
|
+
const extractCmd = getTarExtractCommand(tarPath, installDir);
|
|
281
|
+
Bun.spawnSync(extractCmd);
|
|
284
282
|
|
|
285
283
|
fs.rmSync(tarPath);
|
|
286
284
|
|
|
287
|
-
// Busca recursiva para encontrar onde está o bin/java
|
|
285
|
+
// Busca recursiva para encontrar onde está o bin/java
|
|
288
286
|
const findJdkRoot = (dir: string): string | null => {
|
|
289
|
-
if (fs.existsSync(path.join(dir, "bin",
|
|
287
|
+
if (fs.existsSync(path.join(dir, "bin", getJavaBinary()))) return dir;
|
|
290
288
|
const subdirs = fs.readdirSync(dir, { withFileTypes: true })
|
|
291
289
|
.filter(d => d.isDirectory())
|
|
292
290
|
.map(d => path.join(dir, d.name));
|
|
@@ -299,55 +297,110 @@ export class DoctorCommand implements Command {
|
|
|
299
297
|
|
|
300
298
|
const jdkPath = findJdkRoot(installDir) || installDir;
|
|
301
299
|
const binPath = path.join(jdkPath, "bin");
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
300
|
+
|
|
301
|
+
if (isWindows()) {
|
|
302
|
+
await this.configureWindowsEnv(jdkPath, binPath);
|
|
303
|
+
} else {
|
|
304
|
+
await this.configureUnixEnv(jdkPath, binPath);
|
|
305
|
+
}
|
|
306
|
+
} catch (e: any) {
|
|
307
|
+
Logger.error(`Falha na instalação: ${e.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private async configureWindowsEnv(jdkPath: string, binPath: string) {
|
|
312
|
+
Logger.process("Configurando variáveis de ambiente do SISTEMA...");
|
|
313
|
+
|
|
314
|
+
const setEnvCmd = `
|
|
315
|
+
$jdk = $env:JDK_PATH;
|
|
316
|
+
$bin = $env:BIN_PATH;
|
|
317
|
+
try {
|
|
318
|
+
[Environment]::SetEnvironmentVariable('JAVA_HOME', $jdk, 'Machine');
|
|
319
|
+
$pathVar = [Environment]::GetEnvironmentVariable('Path', 'Machine');
|
|
320
|
+
$paths = $pathVar -split ';' | Where-Object { $_ -ne '' };
|
|
321
|
+
$normalizedBin = $bin.TrimEnd('\\\\').ToLower();
|
|
322
|
+
|
|
323
|
+
$exists = $false;
|
|
324
|
+
foreach ($p in $paths) {
|
|
325
|
+
if ($p.TrimEnd('\\\\').ToLower() -eq $normalizedBin) { $exists = $true; break; }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (-not $exists) {
|
|
329
|
+
$newPath = "$bin;" + $pathVar;
|
|
330
|
+
[Environment]::SetEnvironmentVariable('Path', $newPath, 'Machine');
|
|
331
|
+
}
|
|
332
|
+
Write-Output "OK";
|
|
333
|
+
} catch {
|
|
334
|
+
Write-Error $_.Exception.Message;
|
|
335
|
+
}
|
|
336
|
+
`.replace(/\n/g, ' ');
|
|
337
|
+
|
|
338
|
+
const result = Bun.spawnSync(["powershell", "-command", setEnvCmd], {
|
|
339
|
+
env: {
|
|
340
|
+
...process.env,
|
|
341
|
+
JDK_PATH: jdkPath,
|
|
342
|
+
BIN_PATH: binPath
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
const output = result.stdout.toString() + result.stderr.toString();
|
|
346
|
+
|
|
347
|
+
if (output.includes("ACCESS_DENIED")) {
|
|
348
|
+
Logger.error("Falha ao configurar variáveis do SISTEMA (Acesso Negado).");
|
|
349
|
+
Logger.warn("Dica: Execute o terminal como ADMINISTRADOR para permitir esta alteração.");
|
|
350
|
+
Logger.info("JAVA_HOME manual", jdkPath);
|
|
351
|
+
} else {
|
|
352
|
+
Logger.success(`DCEVM configurado no SISTEMA com sucesso!`);
|
|
353
|
+
Logger.info("JAVA_HOME", jdkPath);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
Logger.newline();
|
|
357
|
+
Logger.warn("IMPORTANTE: Reinicie seu terminal (ou o VS Code) para as mudanças surtirem efeito.");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private async configureUnixEnv(jdkPath: string, binPath: string) {
|
|
361
|
+
Logger.process("Configurando variáveis de ambiente...");
|
|
362
|
+
|
|
363
|
+
const shell = process.env.SHELL || "/bin/bash";
|
|
364
|
+
let rcFile = path.join(os.homedir(), ".bashrc");
|
|
365
|
+
|
|
366
|
+
if (shell.includes("zsh")) {
|
|
367
|
+
rcFile = path.join(os.homedir(), ".zshrc");
|
|
368
|
+
} else if (shell.includes("fish")) {
|
|
369
|
+
rcFile = path.join(os.homedir(), ".config/fish/config.fish");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Verifica se já existe JAVA_HOME configurado
|
|
373
|
+
const exportLine = `export JAVA_HOME="${jdkPath}"`;
|
|
374
|
+
const pathLine = `export PATH="${binPath}:$PATH"`;
|
|
375
|
+
|
|
376
|
+
let rcContent = "";
|
|
377
|
+
if (fs.existsSync(rcFile)) {
|
|
378
|
+
rcContent = fs.readFileSync(rcFile, "utf8");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Remove linhas antigas do Xavva JBR
|
|
382
|
+
const lines = rcContent.split("\n");
|
|
383
|
+
const filteredLines = lines.filter(line =>
|
|
384
|
+
!line.includes("# Xavva JBR") &&
|
|
385
|
+
!line.includes("JAVA_HOME=") &&
|
|
386
|
+
!line.includes("# Added by Xavva")
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Adiciona novas configurações
|
|
390
|
+
filteredLines.push("# Added by Xavva - JetBrains Runtime (DCEVM)");
|
|
391
|
+
filteredLines.push(exportLine);
|
|
392
|
+
filteredLines.push(pathLine);
|
|
393
|
+
|
|
394
|
+
fs.writeFileSync(rcFile, filteredLines.join("\n") + "\n");
|
|
395
|
+
|
|
396
|
+
Logger.success(`DCEVM configurado em ${rcFile}`);
|
|
397
|
+
Logger.info("JAVA_HOME", jdkPath);
|
|
398
|
+
Logger.newline();
|
|
399
|
+
Logger.warn("IMPORTANTE: Execute 'source " + rcFile + "' ou reinicie seu terminal para aplicar permanentemente.");
|
|
400
|
+
Logger.info("Dica", "Para esta sessão, o JAVA_HOME já foi configurado temporariamente.");
|
|
401
|
+
|
|
402
|
+
// Configura JAVA_HOME temporariamente para o processo atual
|
|
403
|
+
process.env.JAVA_HOME = jdkPath;
|
|
404
|
+
process.env.PATH = `${binPath}:${process.env.PATH}`;
|
|
405
|
+
}
|
|
353
406
|
}
|
|
@@ -60,7 +60,7 @@ export class LogsCommand implements Command {
|
|
|
60
60
|
currentSize = newStats.size;
|
|
61
61
|
} else if (newStats.size < currentSize) {
|
|
62
62
|
currentSize = newStats.size;
|
|
63
|
-
dashboard.log(Logger.C.
|
|
63
|
+
dashboard.log(Logger.C.warning + "Arquivo de log foi resetado/rotacionado.");
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
});
|
|
@@ -5,6 +5,14 @@ import path from "path";
|
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import { glob } from "glob";
|
|
7
7
|
import readline from "readline";
|
|
8
|
+
import {
|
|
9
|
+
getJavaPath,
|
|
10
|
+
getMavenCommand,
|
|
11
|
+
getGradleCommand,
|
|
12
|
+
getClasspathSeparator,
|
|
13
|
+
normalizeClasspathPath,
|
|
14
|
+
isWindows,
|
|
15
|
+
} from "../utils/platform";
|
|
8
16
|
|
|
9
17
|
export class RunCommand implements Command {
|
|
10
18
|
async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
|
|
@@ -36,7 +44,8 @@ export class RunCommand implements Command {
|
|
|
36
44
|
const { localCp, dependencyCp } = await this.getClasspath(config);
|
|
37
45
|
const pathingJar = await this.createPathingJar(dependencyCp);
|
|
38
46
|
|
|
39
|
-
const
|
|
47
|
+
const sep = getClasspathSeparator();
|
|
48
|
+
const finalCp = `${localCp}${sep}${pathingJar}`;
|
|
40
49
|
|
|
41
50
|
const javaArgs = [
|
|
42
51
|
"-classpath", finalCp,
|
|
@@ -60,7 +69,7 @@ export class RunCommand implements Command {
|
|
|
60
69
|
Logger.warn(`🚀 Executando ${className}...`);
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
const bin =
|
|
72
|
+
const bin = getJavaPath();
|
|
64
73
|
|
|
65
74
|
const proc = Bun.spawn([bin, ...javaArgs], {
|
|
66
75
|
stdout: "inherit",
|
|
@@ -220,9 +229,10 @@ export class RunCommand implements Command {
|
|
|
220
229
|
const xavvaDir = path.join(process.cwd(), ".xavva");
|
|
221
230
|
const jarPath = path.join(xavvaDir, "classpath.jar");
|
|
222
231
|
|
|
223
|
-
const
|
|
232
|
+
const sep = getClasspathSeparator();
|
|
233
|
+
const paths = dependencyCp.split(sep).filter(p => p.trim());
|
|
224
234
|
const relativePaths = paths.map(p => {
|
|
225
|
-
let rel = path.relative(xavvaDir, p)
|
|
235
|
+
let rel = normalizeClasspathPath(path.relative(xavvaDir, p));
|
|
226
236
|
if (fs.existsSync(p) && fs.statSync(p).isDirectory() && !rel.endsWith("/")) rel += "/";
|
|
227
237
|
// Robust URL encoding for Class-Path as per Java Spec
|
|
228
238
|
return encodeURI(rel)
|
|
@@ -296,10 +306,9 @@ export class RunCommand implements Command {
|
|
|
296
306
|
const stopSpinner = Logger.spinner("Generating project classpath");
|
|
297
307
|
try {
|
|
298
308
|
if (config.project.buildTool === "maven") {
|
|
299
|
-
|
|
300
|
-
Bun.spawnSync([mvnCmd, "dependency:build-classpath", `-Dmdep.outputFile=${cpFile}`]);
|
|
309
|
+
Bun.spawnSync([getMavenCommand(), "dependency:build-classpath", `-Dmdep.outputFile=${cpFile}`]);
|
|
301
310
|
} else if (config.project.buildTool === "gradle") {
|
|
302
|
-
const gradleCmd =
|
|
311
|
+
const gradleCmd = getGradleCommand();
|
|
303
312
|
const initScriptPath = path.join(xavvaDir, "init-cp.gradle");
|
|
304
313
|
const normalizedCpFile = cpFile.replace(/\\/g, "/");
|
|
305
314
|
const initScriptContent = `
|
|
@@ -335,9 +344,10 @@ export class RunCommand implements Command {
|
|
|
335
344
|
|
|
336
345
|
let dependencyCp = fs.existsSync(cpFile) ? fs.readFileSync(cpFile, "utf8").trim() : "";
|
|
337
346
|
|
|
338
|
-
// Normalize platform specific separators
|
|
339
|
-
|
|
340
|
-
|
|
347
|
+
// Normalize platform specific separators para o separador consistente
|
|
348
|
+
const sep = getClasspathSeparator();
|
|
349
|
+
if (path.delimiter !== sep) {
|
|
350
|
+
dependencyCp = dependencyCp.split(path.delimiter).join(sep);
|
|
341
351
|
}
|
|
342
352
|
|
|
343
353
|
const localFolders = [
|
|
@@ -356,7 +366,7 @@ export class RunCommand implements Command {
|
|
|
356
366
|
const localCp = localFolders
|
|
357
367
|
.map(p => path.join(process.cwd(), p))
|
|
358
368
|
.filter(p => fs.existsSync(p))
|
|
359
|
-
.join(
|
|
369
|
+
.join(getClasspathSeparator());
|
|
360
370
|
|
|
361
371
|
return { localCp, dependencyCp };
|
|
362
372
|
}
|
package/src/index.ts
CHANGED
|
@@ -93,7 +93,7 @@ async function main() {
|
|
|
93
93
|
// Registrar ação de restart manual na TUI
|
|
94
94
|
if (dashboard.isTuiActive()) {
|
|
95
95
|
dashboard.onAction("r", () => {
|
|
96
|
-
dashboard.log(Logger.C.
|
|
96
|
+
dashboard.log(Logger.C.warning + "Restart manual solicitado via TUI...");
|
|
97
97
|
deployCmd.execute(config, false, true); // Executa deploy completo mas mantém o watch
|
|
98
98
|
});
|
|
99
99
|
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { Logger } from "../utils/ui";
|
|
2
|
+
import { isWindows, getOpenBrowserArgs } from "../utils/platform";
|
|
2
3
|
|
|
3
4
|
export class BrowserService {
|
|
4
5
|
/**
|
|
5
|
-
* Recarrega a aba ativa do browser (Chrome ou Edge)
|
|
6
|
+
* Recarrega a aba ativa do browser (Chrome ou Edge).
|
|
7
|
+
* Nota: No Linux/Mac, esta funcionalidade é limitada devido às restrições
|
|
8
|
+
* de automação de GUI. Recomenda-se usar extensões de Live Reload.
|
|
6
9
|
*/
|
|
7
10
|
public static async reload(url: string) {
|
|
8
|
-
if (process.platform !== 'win32') return;
|
|
9
|
-
|
|
10
11
|
// Pequeno delay para garantir que o Tomcat processou o novo contexto
|
|
11
12
|
await new Promise(r => setTimeout(r, 800));
|
|
12
13
|
|
|
14
|
+
if (!isWindows()) {
|
|
15
|
+
// No Linux/Mac, tenta notificar via Browser Sync ou similar se disponível
|
|
16
|
+
// Por enquanto, apenas loga (o usuário pode usar extensões de Live Reload)
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
const psCommand = `
|
|
14
21
|
$shell = New-Object -ComObject WScript.Shell
|
|
15
22
|
$process = Get-Process | Where-Object { $_.MainWindowTitle -match "Chrome" -or $_.MainWindowTitle -match "Edge" } | Select-Object -First 1
|
|
@@ -31,11 +38,7 @@ export class BrowserService {
|
|
|
31
38
|
* Abre a URL no browser padrão do sistema.
|
|
32
39
|
*/
|
|
33
40
|
public static open(url: string) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} else {
|
|
37
|
-
const start = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
38
|
-
Bun.spawn([start, url]);
|
|
39
|
-
}
|
|
41
|
+
const args = getOpenBrowserArgs(url);
|
|
42
|
+
Bun.spawn(args);
|
|
40
43
|
}
|
|
41
44
|
}
|
|
@@ -30,6 +30,11 @@ export class BuildService {
|
|
|
30
30
|
this.cache.clearCache();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Sempre limpa a pasta de build antes (target/ ou build/)
|
|
34
|
+
if (!incremental) {
|
|
35
|
+
await this.cleanBuildDirectory();
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
// Cache só é usado se --cache for passado ou em modo incremental
|
|
34
39
|
const useCache = this.projectConfig.cache || incremental;
|
|
35
40
|
|
|
@@ -54,7 +59,8 @@ export class BuildService {
|
|
|
54
59
|
if (incremental) {
|
|
55
60
|
command.push("compile");
|
|
56
61
|
} else {
|
|
57
|
-
|
|
62
|
+
// Sempre executa clean antes do build
|
|
63
|
+
command.push("clean");
|
|
58
64
|
// Use 'package' para gerar .war ou 'war:exploded' para pasta
|
|
59
65
|
if (this.projectConfig.war) {
|
|
60
66
|
command.push("package");
|
|
@@ -76,7 +82,8 @@ export class BuildService {
|
|
|
76
82
|
if (incremental) {
|
|
77
83
|
command.push("classes");
|
|
78
84
|
} else {
|
|
79
|
-
|
|
85
|
+
// Sempre executa clean antes do build
|
|
86
|
+
command.push("clean");
|
|
80
87
|
command.push("war");
|
|
81
88
|
command.push("--parallel", "--build-cache");
|
|
82
89
|
}
|
|
@@ -123,6 +130,26 @@ export class BuildService {
|
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Limpa fisicamente o diretório de build (target/ ou build/)
|
|
135
|
+
* Garante build limpo antes de cada execução
|
|
136
|
+
*/
|
|
137
|
+
private async cleanBuildDirectory(): Promise<void> {
|
|
138
|
+
const buildDir = this.projectConfig.buildTool === 'maven'
|
|
139
|
+
? path.join(process.cwd(), 'target')
|
|
140
|
+
: path.join(process.cwd(), 'build');
|
|
141
|
+
|
|
142
|
+
if (existsSync(buildDir)) {
|
|
143
|
+
try {
|
|
144
|
+
Logger.step(`Cleaning ${path.basename(buildDir)}/ directory...`);
|
|
145
|
+
await fs.rm(buildDir, { recursive: true, force: true });
|
|
146
|
+
Logger.debug(`Removed ${buildDir}`);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
Logger.warn(`Could not fully remove ${buildDir}, continuing...`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
async syncExploded(srcDir: string, destDir: string): Promise<void> {
|
|
127
154
|
if (!existsSync(srcDir)) return;
|
|
128
155
|
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
@@ -10,6 +10,17 @@ import {
|
|
|
10
10
|
import path from "path";
|
|
11
11
|
import os from "os";
|
|
12
12
|
import { spawn } from "child_process";
|
|
13
|
+
import {
|
|
14
|
+
getPlatform,
|
|
15
|
+
isWindows,
|
|
16
|
+
getTomcatArchiveName,
|
|
17
|
+
getTomcatDownloadUrl,
|
|
18
|
+
getTomcatArchiveUrl,
|
|
19
|
+
getExtractCommand,
|
|
20
|
+
getPortCheckCommand,
|
|
21
|
+
getCatalinaScript,
|
|
22
|
+
hasCatalinaScript,
|
|
23
|
+
} from "../utils/platform";
|
|
13
24
|
|
|
14
25
|
export interface EmbeddedTomcatOptions {
|
|
15
26
|
version?: string;
|
|
@@ -35,20 +46,18 @@ export class EmbeddedTomcatService {
|
|
|
35
46
|
private isInstalled: boolean = false;
|
|
36
47
|
|
|
37
48
|
// Versões estáveis do Tomcat (atualizadas: 2026-03-04)
|
|
49
|
+
// URLs são construídas dinamicamente baseadas na plataforma
|
|
38
50
|
private static readonly VERSIONS: Record<
|
|
39
51
|
string,
|
|
40
|
-
{
|
|
52
|
+
{ sha512: string }
|
|
41
53
|
> = {
|
|
42
54
|
"10.1.52": {
|
|
43
|
-
url: "https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.52/bin/apache-tomcat-10.1.52-windows-x64.zip",
|
|
44
55
|
sha512: "",
|
|
45
56
|
},
|
|
46
57
|
"9.0.115": {
|
|
47
|
-
url: "https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.115/bin/apache-tomcat-9.0.115-windows-x64.zip",
|
|
48
58
|
sha512: "",
|
|
49
59
|
},
|
|
50
60
|
"11.0.18": {
|
|
51
|
-
url: "https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.18/bin/apache-tomcat-11.0.18-windows-x64.zip",
|
|
52
61
|
sha512: "",
|
|
53
62
|
},
|
|
54
63
|
};
|
|
@@ -61,14 +70,14 @@ export class EmbeddedTomcatService {
|
|
|
61
70
|
this.baseDir = path.join(os.homedir(), ".xavva", "tomcat");
|
|
62
71
|
this.tomcatHome = path.join(this.baseDir, this.version);
|
|
63
72
|
|
|
64
|
-
//
|
|
73
|
+
// Constrói URL de download baseada na plataforma
|
|
65
74
|
const versionInfo = EmbeddedTomcatService.VERSIONS[this.version];
|
|
66
75
|
if (versionInfo) {
|
|
67
|
-
|
|
76
|
+
// Usa URL primária (CDN Apache)
|
|
77
|
+
this.downloadUrl = getTomcatDownloadUrl(this.version);
|
|
68
78
|
} else {
|
|
69
|
-
// Tenta inferir URL baseado no padrão Apache
|
|
70
|
-
|
|
71
|
-
this.downloadUrl = `https://archive.apache.org/dist/tomcat/tomcat-${majorVersion}/v${this.version}/bin/apache-tomcat-${this.version}-windows-x64.zip`;
|
|
79
|
+
// Tenta inferir URL baseado no padrão Apache (archive)
|
|
80
|
+
this.downloadUrl = getTomcatArchiveUrl(this.version);
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
83
|
|
|
@@ -76,8 +85,7 @@ export class EmbeddedTomcatService {
|
|
|
76
85
|
* Verifica se o Tomcat já está instalado
|
|
77
86
|
*/
|
|
78
87
|
checkInstallation(): boolean {
|
|
79
|
-
|
|
80
|
-
this.isInstalled = existsSync(catalinaBat);
|
|
88
|
+
this.isInstalled = hasCatalinaScript(this.tomcatHome);
|
|
81
89
|
return this.isInstalled;
|
|
82
90
|
}
|
|
83
91
|
|
|
@@ -100,13 +108,8 @@ export class EmbeddedTomcatService {
|
|
|
100
108
|
|
|
101
109
|
for (const entry of entries) {
|
|
102
110
|
if (entry.isDirectory()) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
entry.name,
|
|
106
|
-
"bin",
|
|
107
|
-
"catalina.bat",
|
|
108
|
-
);
|
|
109
|
-
if (existsSync(catalinaBat)) {
|
|
111
|
+
const tomcatPath = path.join(baseDir, entry.name);
|
|
112
|
+
if (hasCatalinaScript(tomcatPath)) {
|
|
110
113
|
versions.push(entry.name);
|
|
111
114
|
}
|
|
112
115
|
}
|
|
@@ -133,10 +136,8 @@ export class EmbeddedTomcatService {
|
|
|
133
136
|
mkdirSync(this.baseDir, { recursive: true });
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
`apache-tomcat-${this.version}.zip`,
|
|
139
|
-
);
|
|
139
|
+
const archiveName = getTomcatArchiveName(this.version);
|
|
140
|
+
const zipPath = path.join(this.baseDir, archiveName);
|
|
140
141
|
|
|
141
142
|
try {
|
|
142
143
|
// Download
|
|
@@ -300,21 +301,24 @@ export class EmbeddedTomcatService {
|
|
|
300
301
|
*/
|
|
301
302
|
async isPortAvailable(): Promise<boolean> {
|
|
302
303
|
return new Promise((resolve) => {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
`netstat -ano | findstr :${this.port}`,
|
|
306
|
-
]);
|
|
304
|
+
const cmd = getPortCheckCommand(this.port);
|
|
305
|
+
const checkProcess = spawn(cmd[0], cmd.slice(1));
|
|
307
306
|
let output = "";
|
|
308
307
|
|
|
309
|
-
|
|
308
|
+
checkProcess.stdout?.on("data", (data) => {
|
|
309
|
+
output += data.toString();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
checkProcess.stderr?.on("data", (data) => {
|
|
310
313
|
output += data.toString();
|
|
311
314
|
});
|
|
312
315
|
|
|
313
|
-
|
|
316
|
+
checkProcess.on("close", () => {
|
|
317
|
+
// Se houver output, a porta está em uso
|
|
314
318
|
resolve(output.trim().length === 0);
|
|
315
319
|
});
|
|
316
320
|
|
|
317
|
-
|
|
321
|
+
checkProcess.on("error", () => {
|
|
318
322
|
resolve(true); // Assume disponível se não conseguir verificar
|
|
319
323
|
});
|
|
320
324
|
});
|
|
@@ -382,18 +386,23 @@ export class EmbeddedTomcatService {
|
|
|
382
386
|
}
|
|
383
387
|
|
|
384
388
|
/**
|
|
385
|
-
* Extrai arquivo ZIP
|
|
389
|
+
* Extrai arquivo de arquivos (ZIP ou tar.gz)
|
|
386
390
|
*/
|
|
387
391
|
private async extractZip(zipPath: string, destDir: string): Promise<void> {
|
|
388
392
|
const spinner = Logger.spinner("Extraindo arquivos...");
|
|
389
393
|
|
|
390
394
|
return new Promise((resolve, reject) => {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
+
const cmd = getExtractCommand(zipPath, destDir);
|
|
396
|
+
|
|
397
|
+
if (!cmd) {
|
|
398
|
+
spinner(false);
|
|
399
|
+
reject(new Error(`Formato de arquivo não suportado: ${path.extname(zipPath)}`));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const extractProcess = spawn(cmd[0], cmd.slice(1));
|
|
395
404
|
|
|
396
|
-
|
|
405
|
+
extractProcess.on("close", (code) => {
|
|
397
406
|
if (code === 0) {
|
|
398
407
|
spinner(true);
|
|
399
408
|
resolve();
|
|
@@ -403,7 +412,7 @@ export class EmbeddedTomcatService {
|
|
|
403
412
|
}
|
|
404
413
|
});
|
|
405
414
|
|
|
406
|
-
|
|
415
|
+
extractProcess.on("error", (err) => {
|
|
407
416
|
spinner(false);
|
|
408
417
|
reject(err);
|
|
409
418
|
});
|
|
@@ -77,7 +77,7 @@ export class LogAnalyzer {
|
|
|
77
77
|
const isProject = this.projectPrefixes.some(p => trimmed.includes(p));
|
|
78
78
|
|
|
79
79
|
if (isProject) {
|
|
80
|
-
return ` ${Logger.C.bold}${Logger.C.
|
|
80
|
+
return ` ${Logger.C.bold}${Logger.C.warning}${trimmed}${Logger.C.reset}`;
|
|
81
81
|
} else {
|
|
82
82
|
return ` ${Logger.C.dim}${trimmed}${Logger.C.reset}`;
|
|
83
83
|
}
|
|
@@ -100,7 +100,7 @@ export class LogAnalyzer {
|
|
|
100
100
|
|
|
101
101
|
let color = Logger.C.primary;
|
|
102
102
|
let symbol = "●";
|
|
103
|
-
if (level === "WARN") { color = Logger.C.
|
|
103
|
+
if (level === "WARN") { color = Logger.C.warning; symbol = "▲"; }
|
|
104
104
|
else if (level === "ERROR") { color = Logger.C.red; symbol = "✖"; }
|
|
105
105
|
|
|
106
106
|
return `${color}${symbol} ${Logger.C.bold}Hotswap:${Logger.C.reset} ${msg}`;
|
|
@@ -113,7 +113,7 @@ export class LogAnalyzer {
|
|
|
113
113
|
|
|
114
114
|
let color = Logger.C.dim;
|
|
115
115
|
let symbol = "ℹ";
|
|
116
|
-
if (label === "WARNING") { color = Logger.C.
|
|
116
|
+
if (label === "WARNING") { color = Logger.C.warning; symbol = "▲"; }
|
|
117
117
|
else if (label === "SEVERE" || label === "ERROR") { color = Logger.C.red; symbol = "✖"; }
|
|
118
118
|
|
|
119
119
|
msg = msg.replace(/^(org\.apache|com\.sun|java\..*?|org\.glassfish)\.[a-zA-Z0-9.]+\s/, "").trim();
|
|
@@ -129,7 +129,7 @@ export class LogAnalyzer {
|
|
|
129
129
|
|
|
130
130
|
let color = Logger.C.dim;
|
|
131
131
|
let symbol = "ℹ";
|
|
132
|
-
if (label === "WARNING" || label === "WARN") { color = Logger.C.
|
|
132
|
+
if (label === "WARNING" || label === "WARN") { color = Logger.C.warning; symbol = "▲"; }
|
|
133
133
|
else if (label === "SEVERE" || label === "ERROR") { color = Logger.C.red; symbol = "✖"; }
|
|
134
134
|
|
|
135
135
|
msg = msg.replace(/^(org\.apache|com\.sun|java\..*?)\.[a-zA-Z0-9.]+\s/, "").trim();
|
|
@@ -5,6 +5,14 @@ import { ProjectService } from "./ProjectService";
|
|
|
5
5
|
import { existsSync, mkdirSync, writeFileSync, statSync, promises as fs } from "fs";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import os from "os";
|
|
8
|
+
import {
|
|
9
|
+
getCatalinaPath,
|
|
10
|
+
getMemoryCommand,
|
|
11
|
+
getKillCommand,
|
|
12
|
+
getPortCheckCommand,
|
|
13
|
+
getJavaPath,
|
|
14
|
+
isWindows,
|
|
15
|
+
} from "../utils/platform";
|
|
8
16
|
|
|
9
17
|
export class TomcatService {
|
|
10
18
|
private activeConfig: TomcatConfig;
|
|
@@ -25,23 +33,55 @@ export class TomcatService {
|
|
|
25
33
|
async getMemoryUsage(): Promise<string> {
|
|
26
34
|
if (!this.pid) return "0 MB";
|
|
27
35
|
try {
|
|
28
|
-
const
|
|
36
|
+
const cmd = getMemoryCommand(this.pid);
|
|
37
|
+
if (!cmd) return "N/A";
|
|
38
|
+
|
|
39
|
+
const { stdout } = Bun.spawnSync(cmd);
|
|
29
40
|
const mem = await new Response(stdout).text();
|
|
30
|
-
|
|
41
|
+
const memValue = parseFloat(mem.trim());
|
|
42
|
+
|
|
43
|
+
if (isNaN(memValue)) return "N/A";
|
|
44
|
+
|
|
45
|
+
// No Linux/macOS, ps retorna em KB, convertemos para MB
|
|
46
|
+
if (!isWindows()) {
|
|
47
|
+
return `${Math.round(memValue / 1024)} MB`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return `${Math.round(memValue)} MB`;
|
|
31
51
|
} catch (e) {
|
|
32
52
|
return "N/A";
|
|
33
53
|
}
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
async killConflict() {
|
|
37
|
-
const
|
|
57
|
+
const cmd = getPortCheckCommand(this.activeConfig.port);
|
|
58
|
+
const { stdout } = Bun.spawnSync(cmd);
|
|
38
59
|
const output = await new Response(stdout).text();
|
|
39
60
|
|
|
40
61
|
if (output) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
// Extrai PID do output
|
|
63
|
+
let pid: string | undefined;
|
|
64
|
+
|
|
65
|
+
if (isWindows()) {
|
|
66
|
+
// Windows: netstat output, última coluna é o PID
|
|
67
|
+
const lines = output.trim().split('\n');
|
|
68
|
+
pid = lines[0].trim().split(/\s+/).pop();
|
|
69
|
+
} else {
|
|
70
|
+
// Linux/Mac: tenta extrair PID de lsof, ss ou netstat
|
|
71
|
+
// lsof -i :port: formato tem PID na coluna 2
|
|
72
|
+
// ss -tlnp: tem pid=XXXX
|
|
73
|
+
// netstat -tlnp: tem /XXXX no final
|
|
74
|
+
const match = output.match(/\b(\d+)\b/) || // número isolado (lsof)
|
|
75
|
+
output.match(/pid=(\d+)/) || // ss format
|
|
76
|
+
output.match(/\/(\d+)/); // netstat format
|
|
77
|
+
if (match) pid = match[1];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (pid) {
|
|
81
|
+
Logger.step(`Freeing port ${this.activeConfig.port}`);
|
|
82
|
+
const killCmd = getKillCommand(pid);
|
|
83
|
+
Bun.spawnSync(killCmd);
|
|
84
|
+
}
|
|
45
85
|
}
|
|
46
86
|
}
|
|
47
87
|
|
|
@@ -105,21 +145,54 @@ export class TomcatService {
|
|
|
105
145
|
|
|
106
146
|
Logger.step("Downloading HotswapAgent v2.0.3 (Global)...");
|
|
107
147
|
const url = "https://github.com/HotswapProjects/HotswapAgent/releases/download/RELEASE-2.0.3/hotswap-agent-2.0.3.jar";
|
|
108
|
-
|
|
109
|
-
|
|
148
|
+
|
|
149
|
+
Logger.debug(`URL: ${url}`);
|
|
150
|
+
Logger.debug(`Destino: ${agentPath}`);
|
|
151
|
+
|
|
152
|
+
const response = await fetch(url, {
|
|
153
|
+
redirect: "follow",
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const contentLength = response.headers.get("content-length");
|
|
161
|
+
Logger.debug(`Content-Length: ${contentLength || "unknown"}`);
|
|
110
162
|
|
|
111
163
|
const buffer = await response.arrayBuffer();
|
|
164
|
+
Logger.debug(`Downloaded: ${buffer.byteLength} bytes`);
|
|
165
|
+
|
|
166
|
+
if (buffer.byteLength < 1000) {
|
|
167
|
+
throw new Error(`Arquivo muito pequeno (${buffer.byteLength} bytes)`);
|
|
168
|
+
}
|
|
169
|
+
|
|
112
170
|
writeFileSync(agentPath, Buffer.from(buffer));
|
|
171
|
+
|
|
172
|
+
// Verifica se foi escrito corretamente
|
|
173
|
+
const stats = statSync(agentPath);
|
|
174
|
+
Logger.debug(`Escrito: ${stats.size} bytes`);
|
|
175
|
+
|
|
113
176
|
Logger.success("HotswapAgent v2.0.3 installed globally!");
|
|
114
177
|
return agentPath;
|
|
115
|
-
} catch (e) {
|
|
116
|
-
Logger.warn(
|
|
178
|
+
} catch (e: any) {
|
|
179
|
+
Logger.warn(`Falha ao baixar HotswapAgent: ${e.message}`);
|
|
180
|
+
Logger.warn("Usando hot swap padrão da JVM.");
|
|
181
|
+
|
|
182
|
+
// Limpa arquivo parcial se existir
|
|
183
|
+
if (existsSync(agentPath)) {
|
|
184
|
+
try {
|
|
185
|
+
await fs.unlink(agentPath);
|
|
186
|
+
Logger.debug("Arquivo parcial removido");
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
|
|
117
190
|
return null;
|
|
118
191
|
}
|
|
119
192
|
}
|
|
120
193
|
|
|
121
194
|
async start(config: AppConfig, isWatching: boolean = false) {
|
|
122
|
-
const binPath =
|
|
195
|
+
const binPath = getCatalinaPath(this.activeConfig.path);
|
|
123
196
|
const args = (config.project.debug || isWatching) ? ["jpda", "run"] : ["run"];
|
|
124
197
|
|
|
125
198
|
const catalinaOpts = [process.env.CATALINA_OPTS || ""];
|
|
@@ -134,7 +207,7 @@ export class TomcatService {
|
|
|
134
207
|
javaBin = path.join(process.env.JAVA_HOME, "bin", "java.exe");
|
|
135
208
|
}
|
|
136
209
|
|
|
137
|
-
const javaVer = Bun.spawnSync([
|
|
210
|
+
const javaVer = Bun.spawnSync([getJavaPath(), "-version"]);
|
|
138
211
|
const output = (javaVer.stderr.toString() + javaVer.stdout.toString()).toLowerCase();
|
|
139
212
|
|
|
140
213
|
if (output.includes("dcevm") || output.includes("jbr") || output.includes("trava")) {
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform Utilities - Helpers para detecção e adaptação multiplataforma
|
|
3
|
+
*
|
|
4
|
+
* Centraliza toda a lógica de diferenciação entre Windows, Linux e macOS.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import os from "os";
|
|
8
|
+
import path from "path";
|
|
9
|
+
|
|
10
|
+
export type Platform = "win32" | "linux" | "darwin";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Retorna a plataforma atual normalizada
|
|
14
|
+
*/
|
|
15
|
+
export function getPlatform(): Platform {
|
|
16
|
+
const platform = process.platform;
|
|
17
|
+
if (platform === "win32") return "win32";
|
|
18
|
+
if (platform === "darwin") return "darwin";
|
|
19
|
+
return "linux";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Verifica se está rodando no Windows
|
|
24
|
+
*/
|
|
25
|
+
export function isWindows(): boolean {
|
|
26
|
+
return getPlatform() === "win32";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Verifica se está rodando no Linux
|
|
31
|
+
*/
|
|
32
|
+
export function isLinux(): boolean {
|
|
33
|
+
return getPlatform() === "linux";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Verifica se está rodando no macOS
|
|
38
|
+
*/
|
|
39
|
+
export function isMacOS(): boolean {
|
|
40
|
+
return getPlatform() === "darwin";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Retorna a extensão de script apropriada (.bat para Windows, .sh para Unix)
|
|
45
|
+
*/
|
|
46
|
+
export function getScriptExt(): string {
|
|
47
|
+
return isWindows() ? ".bat" : ".sh";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Retorna o nome do binário Java apropriado (java.exe para Windows, java para Unix)
|
|
52
|
+
*/
|
|
53
|
+
export function getJavaBinary(): string {
|
|
54
|
+
return isWindows() ? "java.exe" : "java";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Retorna o caminho completo para o binário Java se JAVA_HOME estiver definido
|
|
59
|
+
*/
|
|
60
|
+
export function getJavaPath(): string {
|
|
61
|
+
if (process.env.JAVA_HOME) {
|
|
62
|
+
const javaBin = path.join(process.env.JAVA_HOME, "bin", getJavaBinary());
|
|
63
|
+
return javaBin;
|
|
64
|
+
}
|
|
65
|
+
return getJavaBinary();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retorna o script catalina apropriado (catalina.bat ou catalina.sh)
|
|
70
|
+
*/
|
|
71
|
+
export function getCatalinaScript(): string {
|
|
72
|
+
return `catalina${getScriptExt()}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Retorna o script startup apropriado
|
|
77
|
+
*/
|
|
78
|
+
export function getStartupScript(): string {
|
|
79
|
+
return `startup${getScriptExt()}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Retorna o script shutdown apropriado
|
|
84
|
+
*/
|
|
85
|
+
export function getShutdownScript(): string {
|
|
86
|
+
return `shutdown${getScriptExt()}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Retorna o comando Maven apropriado
|
|
91
|
+
*/
|
|
92
|
+
export function getMavenCommand(): string {
|
|
93
|
+
return isWindows() ? "mvn.cmd" : "mvn";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Retorna o comando Gradle apropriado
|
|
98
|
+
*/
|
|
99
|
+
export function getGradleCommand(): string {
|
|
100
|
+
return isWindows() ? "gradle.bat" : "gradle";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Retorna a extensão de arquivo de download do Tomcat (.zip para Windows, .tar.gz para Unix)
|
|
105
|
+
*/
|
|
106
|
+
export function getTomcatArchiveExt(): string {
|
|
107
|
+
return isWindows() ? ".zip" : ".tar.gz";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Retorna o nome do arquivo do Tomcat para download
|
|
112
|
+
*/
|
|
113
|
+
export function getTomcatArchiveName(version: string): string {
|
|
114
|
+
const baseName = `apache-tomcat-${version}`;
|
|
115
|
+
if (isWindows()) {
|
|
116
|
+
return `${baseName}-windows-x64.zip`;
|
|
117
|
+
}
|
|
118
|
+
if (isMacOS()) {
|
|
119
|
+
// macOS usa a mesma versão do Linux (tar.gz)
|
|
120
|
+
return `${baseName}.tar.gz`;
|
|
121
|
+
}
|
|
122
|
+
return `${baseName}.tar.gz`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Retorna a URL de download do Tomcat baseada na versão e plataforma
|
|
127
|
+
*/
|
|
128
|
+
export function getTomcatDownloadUrl(version: string): string {
|
|
129
|
+
const majorVersion = version.split(".")[0];
|
|
130
|
+
const archiveName = getTomcatArchiveName(version);
|
|
131
|
+
|
|
132
|
+
// URL primária (CDN Apache)
|
|
133
|
+
return `https://dlcdn.apache.org/tomcat/tomcat-${majorVersion}/v${version}/bin/${archiveName}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Retorna a URL alternativa (archive) caso a primária falhe
|
|
138
|
+
*/
|
|
139
|
+
export function getTomcatArchiveUrl(version: string): string {
|
|
140
|
+
const majorVersion = version.split(".")[0];
|
|
141
|
+
const archiveName = getTomcatArchiveName(version);
|
|
142
|
+
|
|
143
|
+
// URL alternativa (archive Apache)
|
|
144
|
+
return `https://archive.apache.org/dist/tomcat/tomcat-${majorVersion}/v${version}/bin/${archiveName}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Retorna o comando para extrair um arquivo
|
|
149
|
+
* Retorna null se não for possível determinar
|
|
150
|
+
*/
|
|
151
|
+
export function getExtractCommand(archivePath: string, destDir: string): string[] | null {
|
|
152
|
+
if (isWindows()) {
|
|
153
|
+
// Windows: usa PowerShell para extrair
|
|
154
|
+
if (archivePath.endsWith(".zip")) {
|
|
155
|
+
return [
|
|
156
|
+
"powershell",
|
|
157
|
+
"-command",
|
|
158
|
+
`Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force`,
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
// Para .tar.gz no Windows (PowerShell 5.1+)
|
|
162
|
+
if (archivePath.endsWith(".tar.gz") || archivePath.endsWith(".tgz")) {
|
|
163
|
+
return [
|
|
164
|
+
"powershell",
|
|
165
|
+
"-command",
|
|
166
|
+
`tar -xzf '${archivePath}' -C '${destDir}'`,
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
// Linux/Mac: usa tar
|
|
171
|
+
if (archivePath.endsWith(".tar.gz") || archivePath.endsWith(".tgz")) {
|
|
172
|
+
return ["tar", "-xzf", archivePath, "-C", destDir];
|
|
173
|
+
}
|
|
174
|
+
// Para .zip no Linux/Mac
|
|
175
|
+
if (archivePath.endsWith(".zip")) {
|
|
176
|
+
// Tenta unzip primeiro, depois tar
|
|
177
|
+
return ["unzip", "-q", "-o", archivePath, "-d", destDir];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Retorna o comando para verificar se uma porta está em uso
|
|
185
|
+
*/
|
|
186
|
+
export function getPortCheckCommand(port: number): string[] {
|
|
187
|
+
if (isWindows()) {
|
|
188
|
+
return ["cmd", "/c", `netstat -ano | findstr :${port}`];
|
|
189
|
+
}
|
|
190
|
+
// Linux/Mac: tenta lsof, ss, ou netstat
|
|
191
|
+
// Preferência por lsof (mais comum)
|
|
192
|
+
return ["sh", "-c", `lsof -i :${port} 2>/dev/null || ss -tlnp 2>/dev/null | grep ':${port}' || netstat -tlnp 2>/dev/null | grep ':${port}'`];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Retorna o comando para matar um processo pelo PID
|
|
197
|
+
*/
|
|
198
|
+
export function getKillCommand(pid: number | string): string[] {
|
|
199
|
+
if (isWindows()) {
|
|
200
|
+
return ["taskkill", "/F", "/PID", String(pid)];
|
|
201
|
+
}
|
|
202
|
+
return ["kill", "-9", String(pid)];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Retorna o comando para obter uso de memória de um processo
|
|
207
|
+
*/
|
|
208
|
+
export function getMemoryCommand(pid: number): string[] | null {
|
|
209
|
+
if (isWindows()) {
|
|
210
|
+
return [
|
|
211
|
+
"powershell",
|
|
212
|
+
"-command",
|
|
213
|
+
`(Get-Process -Id ${pid}).WorkingSet64 / 1MB`,
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
// Linux: /proc/<pid>/status tem VmRSS em kB
|
|
217
|
+
if (isLinux()) {
|
|
218
|
+
return ["sh", "-c", `cat /proc/${pid}/status 2>/dev/null | grep VmRSS | awk '{print int($2/1024)}'`];
|
|
219
|
+
}
|
|
220
|
+
// macOS: usa ps
|
|
221
|
+
if (isMacOS()) {
|
|
222
|
+
return ["ps", "-o", "rss=", "-p", String(pid)];
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Retorna o comando para abrir uma URL no navegador padrão
|
|
229
|
+
*/
|
|
230
|
+
export function getOpenBrowserCommand(): string {
|
|
231
|
+
if (isWindows()) {
|
|
232
|
+
return "start";
|
|
233
|
+
}
|
|
234
|
+
if (isMacOS()) {
|
|
235
|
+
return "open";
|
|
236
|
+
}
|
|
237
|
+
return "xdg-open";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Retorna o comando para abrir uma URL no navegador (array para spawn)
|
|
242
|
+
*/
|
|
243
|
+
export function getOpenBrowserArgs(url: string): string[] {
|
|
244
|
+
const cmd = getOpenBrowserCommand();
|
|
245
|
+
if (isWindows()) {
|
|
246
|
+
// Windows: start "" "url" (o "" é necessário para títulos de janela)
|
|
247
|
+
return [cmd, "", url];
|
|
248
|
+
}
|
|
249
|
+
return [cmd, url];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Retorna o comando para encontrar um binário no PATH
|
|
254
|
+
*/
|
|
255
|
+
export function getWhichCommand(binary: string): string[] {
|
|
256
|
+
if (isWindows()) {
|
|
257
|
+
return ["where", binary];
|
|
258
|
+
}
|
|
259
|
+
return ["which", binary];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Retorna o separador de classpath apropriado
|
|
264
|
+
*/
|
|
265
|
+
export function getClasspathSeparator(): string {
|
|
266
|
+
return path.delimiter;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Normaliza um caminho para uso em classpath (converte backslash para forward slash)
|
|
271
|
+
*/
|
|
272
|
+
export function normalizeClasspathPath(p: string): string {
|
|
273
|
+
return p.replace(/\\/g, "/");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Retorna o comando para extrair um WAR/JAR (usando jar ou unzip)
|
|
278
|
+
*/
|
|
279
|
+
export function getWarExtractCommand(warPath: string, destDir: string): string[] {
|
|
280
|
+
// Tenta usar jar (disponível em qualquer JDK)
|
|
281
|
+
return ["jar", "xf", warPath];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Retorna a URL de download do JetBrains Runtime (JBR) com DCEVM
|
|
286
|
+
*/
|
|
287
|
+
export function getJbrDownloadUrl(version: string = "21"): string {
|
|
288
|
+
// JBR 21 é a versão recomendada
|
|
289
|
+
if (isWindows()) {
|
|
290
|
+
return `https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${version}.0.6-windows-x64-b895.97.tar.gz`;
|
|
291
|
+
}
|
|
292
|
+
if (isMacOS()) {
|
|
293
|
+
return `https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${version}.0.6-osx-x64-b895.97.tar.gz`;
|
|
294
|
+
}
|
|
295
|
+
// Linux x64
|
|
296
|
+
return `https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${version}.0.6-linux-x64-b895.97.tar.gz`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Retorna o comando para extrair um .tar.gz
|
|
301
|
+
*/
|
|
302
|
+
export function getTarExtractCommand(tarPath: string, destDir: string): string[] {
|
|
303
|
+
if (isWindows()) {
|
|
304
|
+
// Windows 10+ tem tar nativo via PowerShell
|
|
305
|
+
return ["powershell", "-command", `tar -xzf '${tarPath}' -C '${destDir}'`];
|
|
306
|
+
}
|
|
307
|
+
return ["tar", "-xzf", tarPath, "-C", destDir];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Constrói o caminho completo para o script catalina
|
|
312
|
+
*/
|
|
313
|
+
export function getCatalinaPath(tomcatHome: string): string {
|
|
314
|
+
return path.join(tomcatHome, "bin", getCatalinaScript());
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Verifica se o script catalina existe no diretório do Tomcat
|
|
319
|
+
*/
|
|
320
|
+
export function hasCatalinaScript(tomcatHome: string): boolean {
|
|
321
|
+
const { existsSync } = require("fs");
|
|
322
|
+
return existsSync(getCatalinaPath(tomcatHome));
|
|
323
|
+
}
|