@archznn/xavva 2.5.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -4
- package/package.json +4 -2
- package/src/commands/DeployCommand.ts +29 -16
- package/src/commands/DoctorCommand.ts +129 -76
- package/src/commands/EncodingCommand.ts +351 -0
- package/src/commands/HelpCommand.ts +15 -0
- package/src/commands/LogsCommand.ts +1 -1
- package/src/commands/RunCommand.ts +21 -11
- package/src/config/versions.ts +63 -0
- package/src/di/container.ts +226 -0
- package/src/errors/ErrorHandler.ts +249 -0
- package/src/errors/XavvaError.ts +273 -0
- package/src/index.ts +98 -96
- package/src/services/AuditService.ts +3 -2
- package/src/services/BrowserService.ts +131 -17
- package/src/services/BuildService.ts +29 -2
- package/src/services/DeployWatcher.ts +183 -0
- package/src/services/EmbeddedTomcatService.ts +48 -53
- package/src/services/EncodingService.ts +548 -0
- package/src/services/FileWatcher.ts +243 -0
- package/src/services/LogAnalyzer.ts +4 -4
- package/src/services/TomcatService.ts +94 -17
- package/src/types/args.ts +136 -0
- package/src/types/config.ts +6 -0
- package/src/types/index.ts +7 -0
- package/src/utils/PathUtils.ts +221 -0
- package/src/utils/config.ts +6 -0
- package/src/utils/parsers/JavaParser.ts +413 -0
- package/src/utils/platform.ts +323 -0
- package/src/services/WatcherService.ts +0 -117
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileWatcher genérico
|
|
3
|
+
* Responsabilidade única: observar mudanças em arquivos
|
|
4
|
+
* Sem acoplamento com lógica de deploy
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { watch, type FSWatcher } from "fs";
|
|
8
|
+
import { Logger } from "../utils/ui";
|
|
9
|
+
|
|
10
|
+
export interface FileWatcherOptions {
|
|
11
|
+
recursive?: boolean;
|
|
12
|
+
debounceMs?: number;
|
|
13
|
+
coolingMs?: number;
|
|
14
|
+
ignoredPatterns?: RegExp[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FileChangeEvent {
|
|
18
|
+
eventType: "rename" | "change";
|
|
19
|
+
filename: string | null;
|
|
20
|
+
fullPath: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type FileChangeHandler = (event: FileChangeEvent) => void | Promise<void>;
|
|
24
|
+
|
|
25
|
+
export class FileWatcher {
|
|
26
|
+
private watcher: FSWatcher | null = null;
|
|
27
|
+
private options: Required<FileWatcherOptions>;
|
|
28
|
+
private handlers: Map<string, FileChangeHandler> = new Map();
|
|
29
|
+
private debounceTimers: Map<string, Timer> = new Map();
|
|
30
|
+
private coolingFiles: Set<string> = new Set();
|
|
31
|
+
private isWatching = false;
|
|
32
|
+
|
|
33
|
+
private static readonly DEFAULT_OPTIONS: Required<FileWatcherOptions> = {
|
|
34
|
+
recursive: true,
|
|
35
|
+
debounceMs: 300,
|
|
36
|
+
coolingMs: 1000,
|
|
37
|
+
ignoredPatterns: [
|
|
38
|
+
/node_modules/,
|
|
39
|
+
/\.git/,
|
|
40
|
+
/target/,
|
|
41
|
+
/build/,
|
|
42
|
+
/\.xavva/,
|
|
43
|
+
/\.idea/,
|
|
44
|
+
/\.vscode/,
|
|
45
|
+
/dist/,
|
|
46
|
+
/out/,
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
constructor(options: FileWatcherOptions = {}) {
|
|
51
|
+
this.options = { ...FileWatcher.DEFAULT_OPTIONS, ...options };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Registra um handler para um padrão específico
|
|
56
|
+
*/
|
|
57
|
+
on(pattern: string | RegExp, handler: FileChangeHandler): () => void {
|
|
58
|
+
const key = pattern.toString();
|
|
59
|
+
this.handlers.set(key, handler);
|
|
60
|
+
|
|
61
|
+
// Retorna função para remover handler
|
|
62
|
+
return () => this.handlers.delete(key);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Registra handler para qualquer mudança
|
|
67
|
+
*/
|
|
68
|
+
onAny(handler: FileChangeHandler): () => void {
|
|
69
|
+
return this.on("*", handler);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Inicia o watching
|
|
74
|
+
*/
|
|
75
|
+
start(rootPath: string = process.cwd()): void {
|
|
76
|
+
if (this.isWatching) {
|
|
77
|
+
Logger.debug("FileWatcher já está rodando");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.watcher = watch(
|
|
82
|
+
rootPath,
|
|
83
|
+
{ recursive: this.options.recursive },
|
|
84
|
+
(eventType, filename) => this.handleWatchEvent(eventType, filename)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
this.isWatching = true;
|
|
88
|
+
Logger.debug(`FileWatcher iniciado em ${rootPath}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Para o watching
|
|
93
|
+
*/
|
|
94
|
+
stop(): void {
|
|
95
|
+
if (this.watcher) {
|
|
96
|
+
this.watcher.close();
|
|
97
|
+
this.watcher = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Limpa timers pendentes
|
|
101
|
+
this.debounceTimers.forEach(timer => clearTimeout(timer));
|
|
102
|
+
this.debounceTimers.clear();
|
|
103
|
+
|
|
104
|
+
this.isWatching = false;
|
|
105
|
+
Logger.debug("FileWatcher parado");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Verifica se está observando
|
|
110
|
+
*/
|
|
111
|
+
isActive(): boolean {
|
|
112
|
+
return this.isWatching;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Processa evento do fs.watch
|
|
117
|
+
*/
|
|
118
|
+
private handleWatchEvent(
|
|
119
|
+
eventType: "rename" | "change",
|
|
120
|
+
filename: string | null
|
|
121
|
+
): void {
|
|
122
|
+
if (!filename) return;
|
|
123
|
+
|
|
124
|
+
// Ignora arquivos em cooling
|
|
125
|
+
if (this.coolingFiles.has(filename)) return;
|
|
126
|
+
this.addToCooling(filename);
|
|
127
|
+
|
|
128
|
+
// Ignora patterns definidos
|
|
129
|
+
if (this.isIgnored(filename)) return;
|
|
130
|
+
|
|
131
|
+
const fullPath = this.resolvePath(filename);
|
|
132
|
+
const event: FileChangeEvent = { eventType, filename, fullPath };
|
|
133
|
+
|
|
134
|
+
// Aplica debounce
|
|
135
|
+
this.debounce(filename, () => {
|
|
136
|
+
this.notifyHandlers(event);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Adiciona arquivo ao período de cooling
|
|
142
|
+
*/
|
|
143
|
+
private addToCooling(filename: string): void {
|
|
144
|
+
this.coolingFiles.add(filename);
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
this.coolingFiles.delete(filename);
|
|
147
|
+
}, this.options.coolingMs);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Verifica se arquivo deve ser ignorado
|
|
152
|
+
*/
|
|
153
|
+
private isIgnored(filename: string): boolean {
|
|
154
|
+
return this.options.ignoredPatterns.some(pattern => pattern.test(filename));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Resolve caminho completo
|
|
159
|
+
*/
|
|
160
|
+
private resolvePath(filename: string): string {
|
|
161
|
+
// Normaliza separadores de path
|
|
162
|
+
return filename.replace(/\\/g, "/");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Aplica debounce no handler
|
|
167
|
+
*/
|
|
168
|
+
private debounce(key: string, fn: () => void): void {
|
|
169
|
+
const existing = this.debounceTimers.get(key);
|
|
170
|
+
if (existing) {
|
|
171
|
+
clearTimeout(existing);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const timer = setTimeout(() => {
|
|
175
|
+
this.debounceTimers.delete(key);
|
|
176
|
+
fn();
|
|
177
|
+
}, this.options.debounceMs);
|
|
178
|
+
|
|
179
|
+
this.debounceTimers.set(key, timer);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Notifica todos os handlers relevantes
|
|
184
|
+
*/
|
|
185
|
+
private notifyHandlers(event: FileChangeEvent): void {
|
|
186
|
+
// Notifica handlers específicos
|
|
187
|
+
for (const [pattern, handler] of this.handlers) {
|
|
188
|
+
if (this.matchesPattern(event.filename, pattern)) {
|
|
189
|
+
try {
|
|
190
|
+
const result = handler(event);
|
|
191
|
+
if (result instanceof Promise) {
|
|
192
|
+
result.catch(err => {
|
|
193
|
+
Logger.debug(`Erro em handler de watch: ${err.message}`);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
Logger.debug(`Erro em handler de watch: ${(err as Error).message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Verifica se filename match com pattern
|
|
205
|
+
*/
|
|
206
|
+
private matchesPattern(filename: string | null, pattern: string): boolean {
|
|
207
|
+
if (!filename) return false;
|
|
208
|
+
if (pattern === "*") return true;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const regex = new RegExp(pattern);
|
|
212
|
+
return regex.test(filename);
|
|
213
|
+
} catch {
|
|
214
|
+
// Se não for regex válido, trata como string simples
|
|
215
|
+
return filename.includes(pattern);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Helper para criar watcher com configuração padrão de projetos Java
|
|
222
|
+
*/
|
|
223
|
+
export function createJavaFileWatcher(): FileWatcher {
|
|
224
|
+
return new FileWatcher({
|
|
225
|
+
recursive: true,
|
|
226
|
+
debounceMs: 300,
|
|
227
|
+
coolingMs: 1000,
|
|
228
|
+
ignoredPatterns: [
|
|
229
|
+
/node_modules/,
|
|
230
|
+
/\.git/,
|
|
231
|
+
/target/,
|
|
232
|
+
/build/,
|
|
233
|
+
/\.xavva/,
|
|
234
|
+
/\.idea/,
|
|
235
|
+
/\.vscode/,
|
|
236
|
+
/dist/,
|
|
237
|
+
/out/,
|
|
238
|
+
/\.class$/,
|
|
239
|
+
/\.jar$/,
|
|
240
|
+
/\.war$/,
|
|
241
|
+
],
|
|
242
|
+
});
|
|
243
|
+
}
|
|
@@ -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();
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import type { TomcatConfig, AppConfig } from "../types
|
|
1
|
+
import type { TomcatConfig, AppConfig } from "../types";
|
|
2
|
+
import { getHotswapAgentUrl, VERSIONS } from "../config/versions";
|
|
3
|
+
import { NetworkError } from "../errors/XavvaError";
|
|
2
4
|
import { Logger } from "../utils/ui";
|
|
3
5
|
import type { Subprocess } from "bun";
|
|
4
6
|
import { ProjectService } from "./ProjectService";
|
|
5
7
|
import { existsSync, mkdirSync, writeFileSync, statSync, promises as fs } from "fs";
|
|
6
8
|
import path from "path";
|
|
7
9
|
import os from "os";
|
|
10
|
+
import {
|
|
11
|
+
getCatalinaPath,
|
|
12
|
+
getMemoryCommand,
|
|
13
|
+
getKillCommand,
|
|
14
|
+
getPortCheckCommand,
|
|
15
|
+
getJavaPath,
|
|
16
|
+
isWindows,
|
|
17
|
+
} from "../utils/platform";
|
|
8
18
|
|
|
9
19
|
export class TomcatService {
|
|
10
20
|
private activeConfig: TomcatConfig;
|
|
@@ -13,6 +23,7 @@ export class TomcatService {
|
|
|
13
23
|
public onReady?: () => void;
|
|
14
24
|
private pid: number | null = null;
|
|
15
25
|
private projectService: ProjectService | null = null;
|
|
26
|
+
private hasReadyBeenCalled: boolean = false;
|
|
16
27
|
|
|
17
28
|
constructor(customConfig: TomcatConfig) {
|
|
18
29
|
this.activeConfig = customConfig;
|
|
@@ -25,23 +36,55 @@ export class TomcatService {
|
|
|
25
36
|
async getMemoryUsage(): Promise<string> {
|
|
26
37
|
if (!this.pid) return "0 MB";
|
|
27
38
|
try {
|
|
28
|
-
const
|
|
39
|
+
const cmd = getMemoryCommand(this.pid);
|
|
40
|
+
if (!cmd) return "N/A";
|
|
41
|
+
|
|
42
|
+
const { stdout } = Bun.spawnSync(cmd);
|
|
29
43
|
const mem = await new Response(stdout).text();
|
|
30
|
-
|
|
44
|
+
const memValue = parseFloat(mem.trim());
|
|
45
|
+
|
|
46
|
+
if (isNaN(memValue)) return "N/A";
|
|
47
|
+
|
|
48
|
+
// No Linux/macOS, ps retorna em KB, convertemos para MB
|
|
49
|
+
if (!isWindows()) {
|
|
50
|
+
return `${Math.round(memValue / 1024)} MB`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return `${Math.round(memValue)} MB`;
|
|
31
54
|
} catch (e) {
|
|
32
55
|
return "N/A";
|
|
33
56
|
}
|
|
34
57
|
}
|
|
35
58
|
|
|
36
59
|
async killConflict() {
|
|
37
|
-
const
|
|
60
|
+
const cmd = getPortCheckCommand(this.activeConfig.port);
|
|
61
|
+
const { stdout } = Bun.spawnSync(cmd);
|
|
38
62
|
const output = await new Response(stdout).text();
|
|
39
63
|
|
|
40
64
|
if (output) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
// Extrai PID do output
|
|
66
|
+
let pid: string | undefined;
|
|
67
|
+
|
|
68
|
+
if (isWindows()) {
|
|
69
|
+
// Windows: netstat output, última coluna é o PID
|
|
70
|
+
const lines = output.trim().split('\n');
|
|
71
|
+
pid = lines[0].trim().split(/\s+/).pop();
|
|
72
|
+
} else {
|
|
73
|
+
// Linux/Mac: tenta extrair PID de lsof, ss ou netstat
|
|
74
|
+
// lsof -i :port: formato tem PID na coluna 2
|
|
75
|
+
// ss -tlnp: tem pid=XXXX
|
|
76
|
+
// netstat -tlnp: tem /XXXX no final
|
|
77
|
+
const match = output.match(/\b(\d+)\b/) || // número isolado (lsof)
|
|
78
|
+
output.match(/pid=(\d+)/) || // ss format
|
|
79
|
+
output.match(/\/(\d+)/); // netstat format
|
|
80
|
+
if (match) pid = match[1];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (pid) {
|
|
84
|
+
Logger.step(`Freeing port ${this.activeConfig.port}`);
|
|
85
|
+
const killCmd = getKillCommand(pid);
|
|
86
|
+
Bun.spawnSync(killCmd);
|
|
87
|
+
}
|
|
45
88
|
}
|
|
46
89
|
}
|
|
47
90
|
|
|
@@ -96,7 +139,7 @@ export class TomcatService {
|
|
|
96
139
|
|
|
97
140
|
private async ensureHotswapAgent(): Promise<string | null> {
|
|
98
141
|
const agentDir = path.join(os.homedir(), ".xavva", "agents");
|
|
99
|
-
const agentPath = path.join(agentDir,
|
|
142
|
+
const agentPath = path.join(agentDir, `hotswap-agent-${VERSIONS.HOTSWAP_AGENT.VERSION}.jar`);
|
|
100
143
|
|
|
101
144
|
if (existsSync(agentPath) && statSync(agentPath).size > 1000) return agentPath;
|
|
102
145
|
|
|
@@ -105,21 +148,54 @@ export class TomcatService {
|
|
|
105
148
|
|
|
106
149
|
Logger.step("Downloading HotswapAgent v2.0.3 (Global)...");
|
|
107
150
|
const url = "https://github.com/HotswapProjects/HotswapAgent/releases/download/RELEASE-2.0.3/hotswap-agent-2.0.3.jar";
|
|
108
|
-
|
|
109
|
-
|
|
151
|
+
|
|
152
|
+
Logger.debug(`URL: ${url}`);
|
|
153
|
+
Logger.debug(`Destino: ${agentPath}`);
|
|
154
|
+
|
|
155
|
+
const response = await fetch(url, {
|
|
156
|
+
redirect: "follow",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const contentLength = response.headers.get("content-length");
|
|
164
|
+
Logger.debug(`Content-Length: ${contentLength || "unknown"}`);
|
|
110
165
|
|
|
111
166
|
const buffer = await response.arrayBuffer();
|
|
167
|
+
Logger.debug(`Downloaded: ${buffer.byteLength} bytes`);
|
|
168
|
+
|
|
169
|
+
if (buffer.byteLength < 1000) {
|
|
170
|
+
throw new Error(`Arquivo muito pequeno (${buffer.byteLength} bytes)`);
|
|
171
|
+
}
|
|
172
|
+
|
|
112
173
|
writeFileSync(agentPath, Buffer.from(buffer));
|
|
113
|
-
|
|
174
|
+
|
|
175
|
+
// Verifica se foi escrito corretamente
|
|
176
|
+
const stats = statSync(agentPath);
|
|
177
|
+
Logger.debug(`Escrito: ${stats.size} bytes`);
|
|
178
|
+
|
|
179
|
+
Logger.success(`HotswapAgent v${VERSIONS.HOTSWAP_AGENT.VERSION} installed globally!`);
|
|
114
180
|
return agentPath;
|
|
115
|
-
} catch (e) {
|
|
116
|
-
|
|
181
|
+
} catch (e: any) {
|
|
182
|
+
throw new NetworkError(url, e);
|
|
183
|
+
Logger.warn("Usando hot swap padrão da JVM.");
|
|
184
|
+
|
|
185
|
+
// Limpa arquivo parcial se existir
|
|
186
|
+
if (existsSync(agentPath)) {
|
|
187
|
+
try {
|
|
188
|
+
await fs.unlink(agentPath);
|
|
189
|
+
Logger.debug("Arquivo parcial removido");
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
|
|
117
193
|
return null;
|
|
118
194
|
}
|
|
119
195
|
}
|
|
120
196
|
|
|
121
197
|
async start(config: AppConfig, isWatching: boolean = false) {
|
|
122
|
-
const binPath =
|
|
198
|
+
const binPath = getCatalinaPath(this.activeConfig.path);
|
|
123
199
|
const args = (config.project.debug || isWatching) ? ["jpda", "run"] : ["run"];
|
|
124
200
|
|
|
125
201
|
const catalinaOpts = [process.env.CATALINA_OPTS || ""];
|
|
@@ -134,7 +210,7 @@ export class TomcatService {
|
|
|
134
210
|
javaBin = path.join(process.env.JAVA_HOME, "bin", "java.exe");
|
|
135
211
|
}
|
|
136
212
|
|
|
137
|
-
const javaVer = Bun.spawnSync([
|
|
213
|
+
const javaVer = Bun.spawnSync([getJavaPath(), "-version"]);
|
|
138
214
|
const output = (javaVer.stderr.toString() + javaVer.stdout.toString()).toLowerCase();
|
|
139
215
|
|
|
140
216
|
if (output.includes("dcevm") || output.includes("jbr") || output.includes("trava")) {
|
|
@@ -258,7 +334,8 @@ export class TomcatService {
|
|
|
258
334
|
this.stopStartupSpinner(isSuccess);
|
|
259
335
|
this.stopStartupSpinner = undefined;
|
|
260
336
|
}
|
|
261
|
-
if (isSuccess && this.onReady) {
|
|
337
|
+
if (isSuccess && this.onReady && !this.hasReadyBeenCalled) {
|
|
338
|
+
this.hasReadyBeenCalled = true;
|
|
262
339
|
this.onReady();
|
|
263
340
|
}
|
|
264
341
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argumentos de linha de comando tipados por contexto
|
|
3
|
+
* Substitui CLIArguments monolítico
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ===== Args Base (comuns a todos os comandos) =====
|
|
7
|
+
export interface BaseArgs {
|
|
8
|
+
help?: boolean;
|
|
9
|
+
version?: boolean;
|
|
10
|
+
verbose?: boolean;
|
|
11
|
+
quiet?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ===== Args de Configuração de Projeto =====
|
|
15
|
+
export interface ProjectArgs {
|
|
16
|
+
tool?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
profile?: string;
|
|
19
|
+
encoding?: string;
|
|
20
|
+
"no-build"?: boolean;
|
|
21
|
+
clean?: boolean;
|
|
22
|
+
war?: boolean;
|
|
23
|
+
cache?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ===== Args de Configuração do Tomcat =====
|
|
27
|
+
export interface TomcatArgs {
|
|
28
|
+
path?: string;
|
|
29
|
+
port?: string;
|
|
30
|
+
"tomcat-version"?: string;
|
|
31
|
+
yes?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ===== Args de Debug/Desenvolvimento =====
|
|
35
|
+
export interface DebugArgs {
|
|
36
|
+
debug?: boolean;
|
|
37
|
+
watch?: boolean;
|
|
38
|
+
tui?: boolean;
|
|
39
|
+
dp?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ===== Args de Análise =====
|
|
43
|
+
export interface AnalysisArgs {
|
|
44
|
+
grep?: string;
|
|
45
|
+
scan?: boolean;
|
|
46
|
+
fix?: boolean;
|
|
47
|
+
output?: string;
|
|
48
|
+
strict?: boolean;
|
|
49
|
+
"update-safe"?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ===== Args de Encoding =====
|
|
53
|
+
export interface EncodingArgs {
|
|
54
|
+
from?: string;
|
|
55
|
+
to?: string;
|
|
56
|
+
backup?: boolean;
|
|
57
|
+
"dry-run"?: boolean;
|
|
58
|
+
force?: boolean;
|
|
59
|
+
src?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ===== Args Específicas de Comandos =====
|
|
63
|
+
export interface DeployArgs extends BaseArgs, ProjectArgs, TomcatArgs, DebugArgs {
|
|
64
|
+
incremental?: boolean;
|
|
65
|
+
changedFiles?: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BuildArgs extends BaseArgs, ProjectArgs {
|
|
69
|
+
// Herda de ProjectArgs
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface StartArgs extends BaseArgs, TomcatArgs, DebugArgs {
|
|
73
|
+
// Herda de TomcatArgs e DebugArgs
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RunArgs extends BaseArgs, DebugArgs {
|
|
77
|
+
// Positional: className
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface LogsArgs extends BaseArgs {
|
|
81
|
+
grep?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface AuditArgs extends BaseArgs, AnalysisArgs {
|
|
85
|
+
// Herda de AnalysisArgs
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface DepsArgs extends BaseArgs, AnalysisArgs {
|
|
89
|
+
// Herda de AnalysisArgs
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface DocsArgs extends BaseArgs {
|
|
93
|
+
output?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ProfilesArgs extends BaseArgs {
|
|
97
|
+
// Sem args específicas
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface DoctorArgs extends BaseArgs {
|
|
101
|
+
fix?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface TomcatCommandArgs extends BaseArgs, TomcatArgs {
|
|
105
|
+
"tomcat-action"?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface EncodingCommandArgs extends BaseArgs, EncodingArgs {
|
|
109
|
+
// Herda de EncodingArgs
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ===== CLIArguments Legado (para compatibilidade) =====
|
|
113
|
+
// Será gradualmente removido
|
|
114
|
+
export interface CLIArguments extends
|
|
115
|
+
BaseArgs,
|
|
116
|
+
ProjectArgs,
|
|
117
|
+
TomcatArgs,
|
|
118
|
+
DebugArgs,
|
|
119
|
+
AnalysisArgs,
|
|
120
|
+
EncodingArgs {
|
|
121
|
+
// Campos adicionais para compatibilidade
|
|
122
|
+
[key: string]: unknown;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ===== Type Guards =====
|
|
126
|
+
export function isDeployArgs(args: CLIArguments): args is DeployArgs {
|
|
127
|
+
return "incremental" in args || "watch" in args;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function isBuildArgs(args: CLIArguments): args is BuildArgs {
|
|
131
|
+
return "tool" in args || "clean" in args;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isEncodingArgs(args: CLIArguments): args is EncodingCommandArgs {
|
|
135
|
+
return "from" in args || "to" in args;
|
|
136
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -62,6 +62,12 @@ export interface CLIArguments {
|
|
|
62
62
|
war?: boolean;
|
|
63
63
|
cache?: boolean;
|
|
64
64
|
changedFiles?: string[];
|
|
65
|
+
from?: string;
|
|
66
|
+
to?: string;
|
|
67
|
+
backup?: boolean;
|
|
68
|
+
"dry-run"?: boolean;
|
|
69
|
+
force?: boolean;
|
|
70
|
+
src?: string;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
export interface CommandContext {
|