@archznn/xavva 2.4.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 +31 -18
- 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 +134 -5
- package/src/services/EmbeddedTomcatService.ts +45 -36
- package/src/services/LogAnalyzer.ts +4 -4
- package/src/services/TomcatService.ts +86 -13
- package/src/services/WatcherService.ts +41 -3
- package/src/types/config.ts +1 -0
- package/src/utils/constants.ts +2 -2
- package/src/utils/platform.ts +323 -0
|
@@ -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,25 +130,147 @@ 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 });
|
|
129
156
|
await this.fastSync(srcDir, destDir);
|
|
130
157
|
}
|
|
131
158
|
|
|
132
|
-
async syncClasses(
|
|
159
|
+
async syncClasses(changedFiles?: string[]): Promise<string | null> {
|
|
133
160
|
const appFolder = this.projectService.getInferredAppName();
|
|
134
161
|
const webappPath = path.join(this.tomcatConfig.path, "webapps", appFolder);
|
|
135
162
|
const targetLib = path.join(webappPath, "WEB-INF", "classes");
|
|
136
|
-
const sourceDir =
|
|
163
|
+
const sourceDir = this.projectService.getClassesDir();
|
|
137
164
|
|
|
138
165
|
if (!existsSync(sourceDir)) return null;
|
|
139
166
|
if (!existsSync(targetLib)) mkdirSync(targetLib, { recursive: true });
|
|
140
167
|
|
|
141
|
-
|
|
168
|
+
// Se temos uma lista específica de arquivos modificados, sincroniza apenas eles
|
|
169
|
+
if (changedFiles && changedFiles.length > 0) {
|
|
170
|
+
await this.syncSpecificFiles(changedFiles, sourceDir, targetLib);
|
|
171
|
+
} else {
|
|
172
|
+
// Caso contrário, sincroniza tudo (comportamento padrão)
|
|
173
|
+
await this.fastSync(sourceDir, targetLib);
|
|
174
|
+
}
|
|
175
|
+
|
|
142
176
|
return appFolder;
|
|
143
177
|
}
|
|
144
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Sincroniza apenas arquivos específicos baseado nos arquivos .java modificados.
|
|
181
|
+
* Converte .java para .class e sincroniza apenas os arquivos realmente modificados.
|
|
182
|
+
*/
|
|
183
|
+
private async syncSpecificFiles(changedFiles: string[], sourceDir: string, targetLib: string): Promise<void> {
|
|
184
|
+
const tasks: Promise<void>[] = [];
|
|
185
|
+
const syncedCount = { value: 0 };
|
|
186
|
+
|
|
187
|
+
for (const javaFile of changedFiles) {
|
|
188
|
+
// Converte caminho do .java para caminho do .class
|
|
189
|
+
// Ex: src/main/java/com/example/Foo.java -> target/classes/com/example/Foo.class
|
|
190
|
+
const relativePath = this.javaToClassPath(javaFile);
|
|
191
|
+
if (!relativePath) continue;
|
|
192
|
+
|
|
193
|
+
const sourcePath = path.join(sourceDir, relativePath);
|
|
194
|
+
const targetPath = path.join(targetLib, relativePath);
|
|
195
|
+
|
|
196
|
+
if (!existsSync(sourcePath)) {
|
|
197
|
+
// Se o .class não existe, talvez seja um arquivo excluído ou inner class
|
|
198
|
+
// Neste caso, faz sync completo como fallback
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
tasks.push((async () => {
|
|
203
|
+
const targetDir = path.dirname(targetPath);
|
|
204
|
+
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
205
|
+
|
|
206
|
+
const srcStat = statSync(sourcePath);
|
|
207
|
+
const destStat = existsSync(targetPath) ? statSync(targetPath) : null;
|
|
208
|
+
|
|
209
|
+
if (!destStat || srcStat.mtimeMs > destStat.mtimeMs) {
|
|
210
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
211
|
+
syncedCount.value++;
|
|
212
|
+
}
|
|
213
|
+
})());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await Promise.all(tasks);
|
|
217
|
+
|
|
218
|
+
// Se não conseguimos sincronizar nenhum arquivo específico, faz sync completo
|
|
219
|
+
if (syncedCount.value === 0) {
|
|
220
|
+
await this.fastSync(sourceDir, targetLib);
|
|
221
|
+
} else if (!this.projectConfig.quiet) {
|
|
222
|
+
Logger.info("sync", `${syncedCount.value} classe(s) sincronizada(s)`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Converte caminho de arquivo .java para caminho relativo de .class
|
|
228
|
+
*/
|
|
229
|
+
private javaToClassPath(javaFile: string): string | null {
|
|
230
|
+
// Remove prefixos comuns de diretórios source
|
|
231
|
+
const parts = javaFile.split(/[/\\]/);
|
|
232
|
+
|
|
233
|
+
// Encontra o índice após "java" ou "src/main/java" ou "src"
|
|
234
|
+
let startIndex = -1;
|
|
235
|
+
|
|
236
|
+
for (let i = 0; i < parts.length; i++) {
|
|
237
|
+
if (parts[i] === "java" && i > 0 && (parts[i-1] === "main" || parts[i-1] === "test")) {
|
|
238
|
+
startIndex = i + 1;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Se não encontrou padrão maven, tenta achar "src"
|
|
244
|
+
if (startIndex === -1) {
|
|
245
|
+
const srcIndex = parts.indexOf("src");
|
|
246
|
+
if (srcIndex !== -1 && srcIndex < parts.length - 1) {
|
|
247
|
+
// Pula "src" e possível "main/java"
|
|
248
|
+
if (parts[srcIndex + 1] === "main" && parts[srcIndex + 2] === "java") {
|
|
249
|
+
startIndex = srcIndex + 3;
|
|
250
|
+
} else {
|
|
251
|
+
startIndex = srcIndex + 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Se ainda não encontrou, assume que o caminho já é relativo ao package
|
|
257
|
+
if (startIndex === -1) {
|
|
258
|
+
startIndex = 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Pega o caminho relativo
|
|
262
|
+
const relativeParts = parts.slice(startIndex);
|
|
263
|
+
if (relativeParts.length === 0) return null;
|
|
264
|
+
|
|
265
|
+
// Substitui extensão .java por .class
|
|
266
|
+
const fileName = relativeParts[relativeParts.length - 1];
|
|
267
|
+
if (!fileName || !fileName.endsWith(".java")) return null;
|
|
268
|
+
|
|
269
|
+
relativeParts[relativeParts.length - 1] = fileName.replace(".java", ".class");
|
|
270
|
+
|
|
271
|
+
return path.join(...relativeParts);
|
|
272
|
+
}
|
|
273
|
+
|
|
145
274
|
private async fastSync(src: string, dest: string) {
|
|
146
275
|
const entries = readdirSync(src, { withFileTypes: true });
|
|
147
276
|
|
|
@@ -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")) {
|
|
@@ -9,6 +9,11 @@ export class WatcherService {
|
|
|
9
9
|
private pendingFullBuild = false;
|
|
10
10
|
private coolingFiles = new Set<string>();
|
|
11
11
|
private debounceTimer?: Timer;
|
|
12
|
+
|
|
13
|
+
// Rastreamento de arquivos modificados para build incremental inteligente
|
|
14
|
+
private modifiedFiles = new Set<string>();
|
|
15
|
+
private pendingFiles = new Set<string>(); // Arquivos modificados durante compilação
|
|
16
|
+
private hasPendingChanges = false;
|
|
12
17
|
|
|
13
18
|
constructor(private config: AppConfig, private deployCmd: DeployCommand) {}
|
|
14
19
|
|
|
@@ -43,24 +48,57 @@ export class WatcherService {
|
|
|
43
48
|
if (!isJava) return;
|
|
44
49
|
|
|
45
50
|
Logger.watcher(filename, 'watch');
|
|
51
|
+
|
|
52
|
+
// Se estiver compilando, acumula na fila de pendentes
|
|
53
|
+
if (this.isDeploying) {
|
|
54
|
+
this.pendingFiles.add(filename);
|
|
55
|
+
this.hasPendingChanges = true;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Acumula arquivos modificados para o próximo build
|
|
60
|
+
this.modifiedFiles.add(filename);
|
|
61
|
+
|
|
46
62
|
clearTimeout(this.debounceTimer);
|
|
47
63
|
|
|
48
64
|
this.debounceTimer = setTimeout(() => {
|
|
49
|
-
|
|
65
|
+
const filesToCompile = [...this.modifiedFiles];
|
|
66
|
+
this.modifiedFiles.clear();
|
|
67
|
+
this.run(this.pendingFullBuild ? false : true, filesToCompile);
|
|
50
68
|
this.pendingFullBuild = false;
|
|
51
69
|
}, WATCHER_DEBOUNCE_MS);
|
|
52
70
|
});
|
|
53
71
|
}
|
|
54
72
|
|
|
55
|
-
private async run(incremental = false) {
|
|
73
|
+
private async run(incremental = false, changedFiles?: string[]) {
|
|
56
74
|
if (this.isDeploying) return;
|
|
57
75
|
this.isDeploying = true;
|
|
76
|
+
|
|
58
77
|
try {
|
|
59
|
-
|
|
78
|
+
// Passa os arquivos específicos que foram modificados
|
|
79
|
+
await this.deployCmd.execute(this.config, {
|
|
80
|
+
watch: true,
|
|
81
|
+
incremental,
|
|
82
|
+
changedFiles
|
|
83
|
+
});
|
|
60
84
|
} catch (e) {
|
|
61
85
|
// Error handled by command
|
|
62
86
|
} finally {
|
|
63
87
|
this.isDeploying = false;
|
|
88
|
+
|
|
89
|
+
// Se houve mudanças durante a compilação, processa imediatamente
|
|
90
|
+
if (this.hasPendingChanges) {
|
|
91
|
+
const pending = [...this.pendingFiles];
|
|
92
|
+
this.pendingFiles.clear();
|
|
93
|
+
this.hasPendingChanges = false;
|
|
94
|
+
|
|
95
|
+
Logger.watcher(`Processing ${pending.length} pending change(s)...`, 'warn');
|
|
96
|
+
|
|
97
|
+
// Pequeno delay para garantir que os arquivos foram salvos completamente
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
this.run(true, pending);
|
|
100
|
+
}, 100);
|
|
101
|
+
}
|
|
64
102
|
}
|
|
65
103
|
}
|
|
66
104
|
|
package/src/types/config.ts
CHANGED
package/src/utils/constants.ts
CHANGED
|
@@ -10,8 +10,8 @@ export const DEFAULT_DEBUG_PORT = 5005;
|
|
|
10
10
|
|
|
11
11
|
// Timeouts (em milissegundos)
|
|
12
12
|
export const TIMEOUT_SHUTDOWN_MS = 5000;
|
|
13
|
-
export const WATCHER_DEBOUNCE_MS =
|
|
14
|
-
export const WATCHER_COOLING_MS =
|
|
13
|
+
export const WATCHER_DEBOUNCE_MS = 1500;
|
|
14
|
+
export const WATCHER_COOLING_MS = 1000;
|
|
15
15
|
export const BROWSER_OPEN_DELAY_MS = 800;
|
|
16
16
|
export const DEPLOY_HEALTH_CHECK_DELAY_MS = 1500;
|
|
17
17
|
export const HOTSWAP_DELAY_MS = 500;
|