@archznn/xavva 2.6.0 → 2.8.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 +55 -0
- package/package.json +4 -2
- package/src/commands/CompletionCommand.ts +212 -0
- package/src/commands/ConfigCommand.ts +184 -0
- package/src/commands/DeployCommand.ts +2 -1
- package/src/commands/EncodingCommand.ts +351 -0
- package/src/commands/HealthCommand.ts +302 -0
- package/src/commands/HelpCommand.ts +42 -0
- package/src/commands/HistoryCommand.ts +49 -0
- package/src/commands/InitCommand.ts +148 -0
- package/src/commands/RedoCommand.ts +36 -0
- package/src/config/versions.ts +63 -0
- package/src/di/container.ts +249 -0
- package/src/errors/ErrorHandler.ts +249 -0
- package/src/errors/XavvaError.ts +273 -0
- package/src/index.ts +136 -96
- package/src/services/AuditService.ts +3 -2
- package/src/services/BrowserService.ts +127 -16
- package/src/services/DeployWatcher.ts +183 -0
- package/src/services/EmbeddedTomcatService.ts +67 -37
- package/src/services/EncodingService.ts +548 -0
- package/src/services/FileWatcher.ts +243 -0
- package/src/services/HistoryService.ts +73 -0
- package/src/services/NotificationService.ts +145 -0
- package/src/services/TomcatService.ts +59 -26
- package/src/types/args.ts +151 -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/ProgressBar.ts +182 -0
- package/src/utils/config.ts +6 -0
- package/src/utils/parsers/JavaParser.ts +413 -0
- package/src/utils/platform.ts +2 -2
- 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
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
|
|
6
|
+
export interface HistoryEntry {
|
|
7
|
+
command: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
timestamp: string;
|
|
10
|
+
success: boolean;
|
|
11
|
+
duration?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const HISTORY_FILE = join(homedir(), ".xavva", "history.json");
|
|
15
|
+
const MAX_HISTORY_SIZE = 100;
|
|
16
|
+
|
|
17
|
+
export class HistoryService {
|
|
18
|
+
private async ensureDir(): Promise<void> {
|
|
19
|
+
const dir = join(homedir(), ".xavva");
|
|
20
|
+
if (!existsSync(dir)) {
|
|
21
|
+
await mkdir(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private async load(): Promise<HistoryEntry[]> {
|
|
26
|
+
try {
|
|
27
|
+
await this.ensureDir();
|
|
28
|
+
const data = await readFile(HISTORY_FILE, "utf-8");
|
|
29
|
+
return JSON.parse(data);
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async save(entries: HistoryEntry[]): Promise<void> {
|
|
36
|
+
await this.ensureDir();
|
|
37
|
+
// Keep only last MAX_HISTORY_SIZE entries
|
|
38
|
+
const trimmed = entries.slice(-MAX_HISTORY_SIZE);
|
|
39
|
+
await writeFile(HISTORY_FILE, JSON.stringify(trimmed, null, 2));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async add(entry: Omit<HistoryEntry, "timestamp">): Promise<void> {
|
|
43
|
+
const entries = await this.load();
|
|
44
|
+
entries.push({
|
|
45
|
+
...entry,
|
|
46
|
+
timestamp: new Date().toISOString()
|
|
47
|
+
});
|
|
48
|
+
await this.save(entries);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getRecent(limit: number = 10): Promise<HistoryEntry[]> {
|
|
52
|
+
const entries = await this.load();
|
|
53
|
+
return entries.slice(-limit).reverse();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getLast(): Promise<HistoryEntry | null> {
|
|
57
|
+
const entries = await this.load();
|
|
58
|
+
return entries.length > 0 ? entries[entries.length - 1] : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async clear(): Promise<void> {
|
|
62
|
+
await this.save([]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async getStats(): Promise<{ total: number; successful: number; failed: number }> {
|
|
66
|
+
const entries = await this.load();
|
|
67
|
+
return {
|
|
68
|
+
total: entries.length,
|
|
69
|
+
successful: entries.filter(e => e.success).length,
|
|
70
|
+
failed: entries.filter(e => !e.success).length
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { platform } from "os";
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
|
|
7
|
+
export interface NotificationOptions {
|
|
8
|
+
title: string;
|
|
9
|
+
message: string;
|
|
10
|
+
type?: "info" | "success" | "warning" | "error";
|
|
11
|
+
sound?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class NotificationService {
|
|
15
|
+
private static enabled = true;
|
|
16
|
+
|
|
17
|
+
static disable(): void {
|
|
18
|
+
this.enabled = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static enable(): void {
|
|
22
|
+
this.enabled = true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static async notify(options: NotificationOptions): Promise<void> {
|
|
26
|
+
if (!this.enabled) return;
|
|
27
|
+
|
|
28
|
+
const { title, message, type = "info", sound = false } = options;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
if (platform() === "win32") {
|
|
32
|
+
await this.notifyWindows(title, message, type, sound);
|
|
33
|
+
} else if (platform() === "darwin") {
|
|
34
|
+
await this.notifyMacOS(title, message, type, sound);
|
|
35
|
+
} else {
|
|
36
|
+
await this.notifyLinux(title, message, type, sound);
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Silently fail - notifications are not critical
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private static async notifyWindows(
|
|
44
|
+
title: string,
|
|
45
|
+
message: string,
|
|
46
|
+
type: string,
|
|
47
|
+
sound: boolean
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
// Use PowerShell notification
|
|
50
|
+
const iconMap: Record<string, string> = {
|
|
51
|
+
info: "Information",
|
|
52
|
+
success: "Information",
|
|
53
|
+
warning: "Warning",
|
|
54
|
+
error: "Error"
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const psScript = `
|
|
58
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
59
|
+
$notify = New-Object System.Windows.Forms.NotifyIcon
|
|
60
|
+
$notify.Icon = [System.Drawing.SystemIcons]::${iconMap[type]}
|
|
61
|
+
$notify.BalloonTipTitle = "${title.replace(/"/g, '""')}"
|
|
62
|
+
$notify.BalloonTipText = "${message.replace(/"/g, '""')}"
|
|
63
|
+
$notify.BalloonTipIcon = "${iconMap[type]}"
|
|
64
|
+
$notify.Visible = $true
|
|
65
|
+
$notify.ShowBalloonTip(5000)
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
await execAsync(`powershell -Command "${psScript}"`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private static async notifyMacOS(
|
|
72
|
+
title: string,
|
|
73
|
+
message: string,
|
|
74
|
+
_type: string,
|
|
75
|
+
sound: boolean
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const soundFlag = sound ? '"\\"\\""' : "";
|
|
78
|
+
await execAsync(`osascript -e 'display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"${soundFlag}'`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private static async notifyLinux(
|
|
82
|
+
title: string,
|
|
83
|
+
message: string,
|
|
84
|
+
type: string,
|
|
85
|
+
_sound: boolean
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
const urgencyMap: Record<string, string> = {
|
|
88
|
+
info: "normal",
|
|
89
|
+
success: "normal",
|
|
90
|
+
warning: "normal",
|
|
91
|
+
error: "critical"
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const iconMap: Record<string, string> = {
|
|
95
|
+
info: "dialog-information",
|
|
96
|
+
success: "dialog-information",
|
|
97
|
+
warning: "dialog-warning",
|
|
98
|
+
error: "dialog-error"
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await execAsync(`notify-send -u ${urgencyMap[type]} -i ${iconMap[type]} "${title}" "${message}"`);
|
|
103
|
+
} catch {
|
|
104
|
+
// Fallback: try zenity
|
|
105
|
+
try {
|
|
106
|
+
await execAsync(`zenity --info --title="${title}" --text="${message}" --timeout=5`);
|
|
107
|
+
} catch {
|
|
108
|
+
// No notification available
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Convenience methods
|
|
114
|
+
static buildSuccess(duration: number): Promise<void> {
|
|
115
|
+
return this.notify({
|
|
116
|
+
title: "Xavva - Build Completo",
|
|
117
|
+
message: `Build finalizado com sucesso em ${duration.toFixed(1)}s`,
|
|
118
|
+
type: "success"
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static buildFailed(error: string): Promise<void> {
|
|
123
|
+
return this.notify({
|
|
124
|
+
title: "Xavva - Build Falhou",
|
|
125
|
+
message: error.slice(0, 100),
|
|
126
|
+
type: "error"
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static deployComplete(appName: string): Promise<void> {
|
|
131
|
+
return this.notify({
|
|
132
|
+
title: "Xavva - Deploy Completo",
|
|
133
|
+
message: `${appName} implantado com sucesso`,
|
|
134
|
+
type: "success"
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static watchReady(): Promise<void> {
|
|
139
|
+
return this.notify({
|
|
140
|
+
title: "Xavva - Watch Mode",
|
|
141
|
+
message: "Monitorando alterações nos arquivos...",
|
|
142
|
+
type: "info"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
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";
|
|
5
|
+
import { ProgressBar, ThemedSpinner } from "../utils/ProgressBar";
|
|
3
6
|
import type { Subprocess } from "bun";
|
|
4
7
|
import { ProjectService } from "./ProjectService";
|
|
5
8
|
import { existsSync, mkdirSync, writeFileSync, statSync, promises as fs } from "fs";
|
|
@@ -21,6 +24,7 @@ export class TomcatService {
|
|
|
21
24
|
public onReady?: () => void;
|
|
22
25
|
private pid: number | null = null;
|
|
23
26
|
private projectService: ProjectService | null = null;
|
|
27
|
+
private hasReadyBeenCalled: boolean = false;
|
|
24
28
|
|
|
25
29
|
constructor(customConfig: TomcatConfig) {
|
|
26
30
|
this.activeConfig = customConfig;
|
|
@@ -136,54 +140,82 @@ export class TomcatService {
|
|
|
136
140
|
|
|
137
141
|
private async ensureHotswapAgent(): Promise<string | null> {
|
|
138
142
|
const agentDir = path.join(os.homedir(), ".xavva", "agents");
|
|
139
|
-
const agentPath = path.join(agentDir,
|
|
143
|
+
const agentPath = path.join(agentDir, `hotswap-agent-${VERSIONS.HOTSWAP_AGENT.VERSION}.jar`);
|
|
140
144
|
|
|
141
145
|
if (existsSync(agentPath) && statSync(agentPath).size > 1000) return agentPath;
|
|
142
146
|
|
|
143
147
|
try {
|
|
144
148
|
if (!existsSync(agentDir)) mkdirSync(agentDir, { recursive: true });
|
|
145
149
|
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
Logger.debug(`URL: ${url}`);
|
|
150
|
-
Logger.debug(`Destino: ${agentPath}`);
|
|
151
|
-
|
|
152
|
-
const response = await fetch(url, {
|
|
153
|
-
redirect: "follow",
|
|
154
|
-
});
|
|
150
|
+
const url = getHotswapAgentUrl();
|
|
151
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
155
152
|
|
|
156
153
|
if (!response.ok) {
|
|
157
154
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
158
155
|
}
|
|
159
156
|
|
|
160
|
-
const
|
|
161
|
-
Logger.debug(`Content-Length: ${contentLength || "unknown"}`);
|
|
157
|
+
const totalSize = parseInt(response.headers.get("content-length") || "0");
|
|
162
158
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
159
|
+
if (totalSize > 0) {
|
|
160
|
+
// Usar progress bar
|
|
161
|
+
const progress = new ProgressBar({
|
|
162
|
+
title: `Baixando HotswapAgent v${VERSIONS.HOTSWAP_AGENT.VERSION}`,
|
|
163
|
+
total: totalSize,
|
|
164
|
+
width: 25
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const reader = response.body?.getReader();
|
|
168
|
+
if (!reader) throw new Error("Response body não disponível");
|
|
169
|
+
|
|
170
|
+
const chunks: Uint8Array[] = [];
|
|
171
|
+
let received = 0;
|
|
172
|
+
|
|
173
|
+
while (true) {
|
|
174
|
+
const { done, value } = await reader.read();
|
|
175
|
+
if (done) break;
|
|
176
|
+
|
|
177
|
+
chunks.push(value);
|
|
178
|
+
received += value.length;
|
|
179
|
+
progress.update(received);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
progress.complete();
|
|
183
|
+
|
|
184
|
+
// Concatena e salva
|
|
185
|
+
const allChunks = new Uint8Array(received);
|
|
186
|
+
let position = 0;
|
|
187
|
+
for (const chunk of chunks) {
|
|
188
|
+
allChunks.set(chunk, position);
|
|
189
|
+
position += chunk.length;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await fs.writeFile(agentPath, allChunks);
|
|
193
|
+
} else {
|
|
194
|
+
// Sem content-length, usar spinner
|
|
195
|
+
const spinner = new ThemedSpinner();
|
|
196
|
+
const stop = spinner.start(`Baixando HotswapAgent v${VERSIONS.HOTSWAP_AGENT.VERSION}`, "dots", "download");
|
|
197
|
+
|
|
198
|
+
const buffer = await response.arrayBuffer();
|
|
199
|
+
writeFileSync(agentPath, Buffer.from(buffer));
|
|
200
|
+
|
|
201
|
+
stop(true);
|
|
168
202
|
}
|
|
169
203
|
|
|
170
|
-
writeFileSync(agentPath, Buffer.from(buffer));
|
|
171
|
-
|
|
172
204
|
// Verifica se foi escrito corretamente
|
|
173
205
|
const stats = statSync(agentPath);
|
|
174
|
-
|
|
206
|
+
if (stats.size < 1000) {
|
|
207
|
+
throw new Error(`Arquivo muito pequeno (${stats.size} bytes)`);
|
|
208
|
+
}
|
|
175
209
|
|
|
176
|
-
Logger.success(
|
|
210
|
+
Logger.success(`HotswapAgent v${VERSIONS.HOTSWAP_AGENT.VERSION} instalado!`);
|
|
177
211
|
return agentPath;
|
|
178
212
|
} catch (e: any) {
|
|
179
|
-
Logger.warn(
|
|
180
|
-
Logger.warn("Usando hot swap padrão da JVM.");
|
|
213
|
+
Logger.warn("Falha ao baixar HotswapAgent. Usando hot swap padrão da JVM.");
|
|
181
214
|
|
|
182
215
|
// Limpa arquivo parcial se existir
|
|
183
216
|
if (existsSync(agentPath)) {
|
|
184
217
|
try {
|
|
185
218
|
await fs.unlink(agentPath);
|
|
186
|
-
Logger.debug("Arquivo parcial removido");
|
|
187
219
|
} catch {}
|
|
188
220
|
}
|
|
189
221
|
|
|
@@ -331,7 +363,8 @@ export class TomcatService {
|
|
|
331
363
|
this.stopStartupSpinner(isSuccess);
|
|
332
364
|
this.stopStartupSpinner = undefined;
|
|
333
365
|
}
|
|
334
|
-
if (isSuccess && this.onReady) {
|
|
366
|
+
if (isSuccess && this.onReady && !this.hasReadyBeenCalled) {
|
|
367
|
+
this.hasReadyBeenCalled = true;
|
|
335
368
|
this.onReady();
|
|
336
369
|
}
|
|
337
370
|
}
|