@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.
@@ -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.yellow}${trimmed}${Logger.C.reset}`;
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.yellow; symbol = "▲"; }
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.yellow; symbol = "▲"; }
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.yellow; symbol = "▲"; }
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/config";
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 { stdout } = Bun.spawnSync(["powershell", "-command", `(Get-Process -Id ${this.pid}).WorkingSet64 / 1MB`]);
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
- return `${Math.round(parseFloat(mem))} MB`;
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 { stdout } = Bun.spawnSync(["cmd", "/c", `netstat -ano | findstr :${this.activeConfig.port}`]);
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
- const lines = output.trim().split('\n');
42
- const pid = lines[0].trim().split(/\s+/).pop();
43
- Logger.step(`Freeing port ${this.activeConfig.port}`);
44
- Bun.spawnSync(["taskkill", "/F", "/PID", pid]);
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, "hotswap-agent-2.0.3.jar");
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
- const response = await fetch(url);
109
- if (!response.ok) throw new Error(`Status: ${response.status}`);
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
- Logger.success("HotswapAgent v2.0.3 installed globally!");
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
- Logger.warn("Falha ao baixar HotswapAgent. Usando hot swap padrão da JVM.");
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 = `${this.activeConfig.path}\\bin\\catalina.bat`;
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([javaBin, "-version"]);
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
+ }
@@ -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 {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Barrel export para todos os tipos
3
+ */
4
+
5
+ export * from "./config";
6
+ export * from "./endpoint";
7
+ export * from "./args";