@archznn/xavva 2.1.0 → 2.2.1

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.
@@ -1,142 +1,187 @@
1
1
  import { Logger } from "../utils/ui";
2
2
  import { ProcessManager } from "../utils/processManager";
3
3
  import {
4
- MAX_LOG_SCROLLBUFFER,
5
- DASHBOARD_REFRESH_INTERVAL_MS,
6
- DASHBOARD_LOG_SLICE_LINES
4
+ MAX_LOG_SCROLLBUFFER,
5
+ DASHBOARD_REFRESH_INTERVAL_MS,
6
+ DASHBOARD_LOG_SLICE_LINES
7
7
  } from "../utils/constants";
8
8
  import type { AppConfig } from "../types/config";
9
9
  import os from "os";
10
10
 
11
+ const C = Logger.C;
12
+
11
13
  export class DashboardService {
12
- private isTui: boolean;
13
- private logLines: string[] = [];
14
- private maxLogLines: number = 0;
15
- private status: string = "IDLE";
16
- private statusColor: string = Logger.C.dim;
17
- private gitContext: { branch: string; commit: string } | null = null;
18
- private actions: Map<string, () => void> = new Map();
19
-
20
- constructor(private config: AppConfig) {
21
- this.isTui = config.project.tui;
22
- if (this.isTui) {
23
- this.gitContext = Logger.getGitContext();
24
- this.maxLogLines = process.stdout.rows - 6;
25
- this.setupTui();
26
- this.registerShutdownHandlers();
27
- }
28
- }
29
-
30
- private registerShutdownHandlers() {
31
- const processManager = ProcessManager.getInstance();
32
- processManager.onShutdown(() => {
33
- this.restoreTerminal();
34
- });
35
- }
36
-
37
- private restoreTerminal() {
38
- if (this.isTui) {
39
- process.stdout.write("\x1B[?1049l"); // Restore buffer
40
- process.stdout.write("\x1B[?25h"); // Show cursor
41
- process.stdin.setRawMode(false);
42
- process.stdin.pause();
43
- }
44
- }
45
-
46
- public isTuiActive(): boolean {
47
- return this.isTui;
48
- }
49
-
50
- public onAction(key: string, callback: () => void) {
51
- this.actions.set(key.toLowerCase(), callback);
52
- }
53
-
54
- private setupTui() {
55
- process.stdout.write("\x1B[?1049h"); // Switch to alternate buffer
56
- process.stdout.write("\x1B[2J"); // Clear screen
57
- process.stdout.write("\x1B[?25l"); // Hide cursor
58
-
59
- process.stdin.setRawMode(true);
60
- process.stdin.resume();
61
- process.stdin.setEncoding("utf8");
62
-
63
- process.stdin.on("data", (key: string) => {
64
- const input = key.toLowerCase();
65
- // Ctrl+C ou Q para sair
66
- if (key === "\u0003" || input === "q") {
67
- this.exit();
68
- }
69
- if (input === "l") {
70
- this.logLines = [];
71
- this.render();
72
- return;
73
- }
74
-
75
- const action = this.actions.get(input);
76
- if (action) action();
77
- });
78
-
79
- process.on("SIGINT", () => this.exit());
80
- process.on("exit", () => this.exit());
81
-
82
- // Atualiza o dashboard periodicamente para memória/status
83
- setInterval(() => this.render(), 1000);
84
- }
85
-
86
- public setStatus(status: string, color: string = Logger.C.cyan) {
87
- this.status = status;
88
- this.statusColor = color;
89
- this.render();
90
- }
91
-
92
- public log(message: string) {
93
- if (!message) return;
94
-
95
- if (this.isTui) {
96
- const lines = message.split("\n");
97
- this.logLines.push(...lines);
98
- if (this.logLines.length > MAX_LOG_SCROLLBUFFER) {
99
- this.logLines = this.logLines.slice(-DASHBOARD_LOG_SLICE_LINES);
100
- }
101
- this.render();
102
- } else {
103
- console.log(message);
104
- }
105
- }
106
-
107
- private render() {
108
- if (!this.isTui) return;
109
-
110
- this.maxLogLines = process.stdout.rows - 6;
111
-
112
- let output = "\x1B[H"; // Move to 0,0
113
-
114
- // Header
115
- const name = (process.cwd().split(/[/\\]/).pop() || "PROJECT").toUpperCase();
116
- const mem = Math.round((os.totalmem() - os.freemem()) / 1024 / 1024 / 1024 * 10) / 10;
117
- const totalMem = Math.round(os.totalmem() / 1024 / 1024 / 1024);
118
- const profile = this.config.project.profile ? ` ${Logger.C.dim}•${Logger.C.reset} ${Logger.C.yellow}♦ ${this.config.project.profile.toUpperCase()}${Logger.C.reset}` : "";
119
-
120
- output += `${Logger.C.bold}${Logger.C.cyan} X A V V A 2.0 ${Logger.C.reset} ${Logger.C.dim}│${Logger.C.reset} ${Logger.C.white}${Logger.C.bold}${name}${Logger.C.reset}${profile}\x1B[K\n`;
121
- output += `${Logger.C.dim} STATUS: ${this.statusColor}${this.status.padEnd(10)}${Logger.C.reset} ${Logger.C.dim}│ MEM: ${Logger.C.yellow}${mem}G/${totalMem}G${Logger.C.reset} ${Logger.C.dim}│ BRANCH: ${Logger.C.magenta}${this.gitContext?.branch || "unknown"}${Logger.C.reset}\x1B[K\n`;
122
- output += `${Logger.C.dim}──────────────────────────────────────────────────────────────────────────${Logger.C.reset}\x1B[K\n`;
123
-
124
- // Logs
125
- const visibleLogs = this.logLines.slice(-this.maxLogLines);
126
- for (let i = 0; i < this.maxLogLines; i++) {
127
- const line = visibleLogs[i] || "";
128
- output += line.substring(0, process.stdout.columns) + "\x1B[K\n";
129
- }
130
-
131
- // Footer
132
- output += `\x1B[${process.stdout.rows};1H`; // Move to last row
133
- output += ` ${Logger.C.bold}${Logger.C.white}R${Logger.C.reset} Restart ${Logger.C.bold}${Logger.C.white}L${Logger.C.reset} Clear ${Logger.C.bold}${Logger.C.white}Q${Logger.C.reset} Quit ${Logger.C.dim} (Xavva 2.0 TUI Mode)${Logger.C.reset}\x1B[K`;
134
-
135
- process.stdout.write(output);
136
- }
137
-
138
- private async exit() {
139
- this.restoreTerminal();
140
- await ProcessManager.getInstance().shutdown(0);
141
- }
14
+ private isTui: boolean;
15
+ private logLines: string[] = [];
16
+ private maxLogLines: number = 0;
17
+ private status: string = "idle";
18
+ private statusColor: string = C.gray;
19
+ private gitContext: { branch: string; hash: string } | null = null;
20
+ private actions: Map<string, () => void> = new Map();
21
+ private startTime = Date.now();
22
+
23
+ constructor(private config: AppConfig) {
24
+ this.isTui = config.project.tui;
25
+ if (this.isTui) {
26
+ this.gitContext = Logger.getGitContext();
27
+ this.maxLogLines = process.stdout.rows - 8;
28
+ this.setupTui();
29
+ this.registerShutdownHandlers();
30
+ }
31
+ }
32
+
33
+ private registerShutdownHandlers() {
34
+ const processManager = ProcessManager.getInstance();
35
+ processManager.onShutdown(() => {
36
+ this.restoreTerminal();
37
+ });
38
+ }
39
+
40
+ private restoreTerminal() {
41
+ if (this.isTui) {
42
+ process.stdout.write("\x1B[?1049l");
43
+ process.stdout.write("\x1B[?25h");
44
+ process.stdin.setRawMode(false);
45
+ process.stdin.pause();
46
+ }
47
+ }
48
+
49
+ public isTuiActive(): boolean {
50
+ return this.isTui;
51
+ }
52
+
53
+ public onAction(key: string, callback: () => void) {
54
+ this.actions.set(key.toLowerCase(), callback);
55
+ }
56
+
57
+ private setupTui() {
58
+ process.stdout.write("\x1B[?1049h");
59
+ process.stdout.write("\x1B[2J");
60
+ process.stdout.write("\x1B[?25l");
61
+
62
+ process.stdin.setRawMode(true);
63
+ process.stdin.resume();
64
+ process.stdin.setEncoding("utf8");
65
+
66
+ process.stdin.on("data", (key: string) => {
67
+ const input = key.toLowerCase();
68
+ if (key === "\u0003" || input === "q") {
69
+ this.exit();
70
+ }
71
+ if (input === "l") {
72
+ this.logLines = [];
73
+ this.render();
74
+ }
75
+
76
+ const action = this.actions.get(input);
77
+ if (action) action();
78
+ });
79
+
80
+ process.on("SIGINT", () => this.exit());
81
+ process.on("exit", () => this.exit());
82
+
83
+ setInterval(() => this.render(), DASHBOARD_REFRESH_INTERVAL_MS);
84
+ }
85
+
86
+ public setStatus(status: string, type: 'idle' | 'building' | 'ready' | 'error' = 'idle') {
87
+ this.status = status;
88
+ switch (type) {
89
+ case 'building':
90
+ this.statusColor = C.primary;
91
+ break;
92
+ case 'ready':
93
+ this.statusColor = C.success;
94
+ break;
95
+ case 'error':
96
+ this.statusColor = C.error;
97
+ break;
98
+ default:
99
+ this.statusColor = C.gray;
100
+ }
101
+ this.render();
102
+ }
103
+
104
+ public spinner(msg: string) {
105
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
106
+ let i = 0;
107
+
108
+ this.setStatus(msg, 'building');
109
+
110
+ const timer = setInterval(() => {
111
+ this.status = `${frames[i]} ${msg}`;
112
+ i = (i + 1) % frames.length;
113
+ this.render();
114
+ }, 80);
115
+
116
+ return (success = true) => {
117
+ clearInterval(timer);
118
+ if (success) {
119
+ this.setStatus('ready', 'ready');
120
+ } else {
121
+ this.setStatus('error', 'error');
122
+ }
123
+ };
124
+ }
125
+
126
+ public log(message: string) {
127
+ if (!message) return;
128
+
129
+ if (this.isTui) {
130
+ const lines = message.split("\n");
131
+ this.logLines.push(...lines);
132
+ if (this.logLines.length > MAX_LOG_SCROLLBUFFER) {
133
+ this.logLines = this.logLines.slice(-DASHBOARD_LOG_SLICE_LINES);
134
+ }
135
+ this.render();
136
+ } else {
137
+ console.log(message);
138
+ }
139
+ }
140
+
141
+ private render() {
142
+ if (!this.isTui) return;
143
+
144
+ this.maxLogLines = process.stdout.rows - 8;
145
+
146
+ const projectName = (process.cwd().split(/[/\\]/).pop() || "project").toLowerCase();
147
+ const uptime = Math.floor((Date.now() - this.startTime) / 1000);
148
+ const uptimeStr = uptime < 60 ? `${uptime}s` : `${Math.floor(uptime / 60)}m ${uptime % 60}s`;
149
+
150
+ const mem = Math.round((os.totalmem() - os.freemem()) / 1024 / 1024 / 1024 * 10) / 10;
151
+ const totalMem = Math.round(os.totalmem() / 1024 / 1024 / 1024);
152
+
153
+ // Header minimalista
154
+ let output = "\x1B[H";
155
+ output += `${C.gray}┌─ ${C.primary}${C.bold}XAVVA${C.reset}${C.gray}.${C.reset}${projectName}${C.reset}`;
156
+ output += ` ${C.gray}│${C.reset} ${this.statusColor}${this.status}${C.reset}\x1B[K\n`;
157
+
158
+ // Info bar
159
+ const infos: string[] = [];
160
+ if (this.config.project.profile) infos.push(`${C.warning}${this.config.project.profile}${C.reset}`);
161
+ if (this.gitContext?.branch) infos.push(`${C.secondary}git:${this.gitContext.branch}${C.reset}`);
162
+ infos.push(`${C.gray}mem:${mem}/${totalMem}GB${C.reset}`);
163
+ infos.push(`${C.gray}up:${uptimeStr}${C.reset}`);
164
+
165
+ output += `${C.gray}│${C.reset} ${infos.join(` ${C.gray}•${C.reset} `)}\x1B[K\n`;
166
+ output += `${C.gray}├────────────────────────────────────────────────────────┤${C.reset}\x1B[K\n`;
167
+
168
+ // Logs
169
+ const visibleLogs = this.logLines.slice(-this.maxLogLines);
170
+ for (let i = 0; i < this.maxLogLines; i++) {
171
+ const line = visibleLogs[i] || "";
172
+ output += `${C.gray}│${C.reset} ${line.substring(0, process.stdout.columns - 3)}\x1B[K\n`;
173
+ }
174
+
175
+ // Footer minimalista
176
+ output += `${C.gray}├────────────────────────────────────────────────────────┤${C.reset}\x1B[K\n`;
177
+ output += `${C.gray}│${C.reset} ${C.white}[r]${C.reset}${C.gray}estart ${C.white}[l]${C.reset}${C.gray}og clear ${C.white}[q]${C.reset}${C.gray}uit${C.reset}\x1B[K\n`;
178
+ output += `${C.gray}└────────────────────────────────────────────────────────┘${C.reset}\x1B[K`;
179
+
180
+ process.stdout.write(output);
181
+ }
182
+
183
+ private async exit() {
184
+ this.restoreTerminal();
185
+ await ProcessManager.getInstance().shutdown(0);
186
+ }
142
187
  }
@@ -471,12 +471,120 @@ export class DependencyAnalyzerService {
471
471
  return 0;
472
472
  }
473
473
 
474
+ async updateSafe(updates: DependencyUpdate[]): Promise<{ updated: number; skipped: number; errors: string[] }> {
475
+ const safeUpdates = updates.filter(u => !u.isMajor);
476
+ const result = { updated: 0, skipped: 0, errors: [] as string[] };
477
+
478
+ if (safeUpdates.length === 0) {
479
+ return result;
480
+ }
481
+
482
+ if (this.projectConfig.buildTool === "maven") {
483
+ return this.updateMavenSafe(safeUpdates);
484
+ } else {
485
+ return this.updateGradleSafe(safeUpdates);
486
+ }
487
+ }
488
+
489
+ private async updateMavenSafe(updates: DependencyUpdate[]): Promise<{ updated: number; skipped: number; errors: string[] }> {
490
+ const result = { updated: 0, skipped: 0, errors: [] as string[] };
491
+ const pomPath = path.join(process.cwd(), "pom.xml");
492
+
493
+ if (!fs.existsSync(pomPath)) {
494
+ result.errors.push("pom.xml não encontrado");
495
+ return result;
496
+ }
497
+
498
+ let content = fs.readFileSync(pomPath, "utf-8");
499
+ let modified = false;
500
+
501
+ for (const update of updates) {
502
+ // Pattern para encontrar a versão específica desta dependência
503
+ const groupId = update.groupId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
504
+ const artifactId = update.artifactId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
505
+ const currentVersion = update.currentVersion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
506
+
507
+ // Regex para encontrar <version> dentro do bloco da dependência
508
+ const depPattern = new RegExp(
509
+ `(<dependency>\\s*<groupId>${groupId}</groupId>\\s*<artifactId>${artifactId}</artifactId>(?:\\s*<version>)${currentVersion}(</version>))`,
510
+ 'g'
511
+ );
512
+
513
+ if (depPattern.test(content)) {
514
+ content = content.replace(depPattern, `$1${update.latestVersion}$2`);
515
+ result.updated++;
516
+ modified = true;
517
+ } else {
518
+ // Pode ser definida via property
519
+ result.skipped++;
520
+ }
521
+ }
522
+
523
+ if (modified) {
524
+ // Backup do pom.xml
525
+ fs.writeFileSync(`${pomPath}.backup`, fs.readFileSync(pomPath));
526
+ fs.writeFileSync(pomPath, content);
527
+ }
528
+
529
+ return result;
530
+ }
531
+
532
+ private async updateGradleSafe(updates: DependencyUpdate[]): Promise<{ updated: number; skipped: number; errors: string[] }> {
533
+ const result = { updated: 0, skipped: 0, errors: [] as string[] };
534
+ const gradlePath = path.join(process.cwd(), "build.gradle");
535
+
536
+ if (!fs.existsSync(gradlePath)) {
537
+ result.errors.push("build.gradle não encontrado");
538
+ return result;
539
+ }
540
+
541
+ let content = fs.readFileSync(gradlePath, "utf-8");
542
+ let modified = false;
543
+
544
+ for (const update of updates) {
545
+ const groupId = update.groupId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
546
+ const artifactId = update.artifactId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
547
+ const currentVersion = update.currentVersion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
548
+
549
+ // Pattern: implementation("group:artifact:version") ou implementation 'group:artifact:version'
550
+ const patterns = [
551
+ new RegExp(`(implementation\\s*\\(\\s*["']${groupId}:${artifactId}:)${currentVersion}(["']\\s*\\))`, 'g'),
552
+ new RegExp(`(implementation\\s+["']${groupId}:${artifactId}:)${currentVersion}(["'])`, 'g'),
553
+ new RegExp(`(compile\\s*\\(\\s*["']${groupId}:${artifactId}:)${currentVersion}(["']\\s*\\))`, 'g'),
554
+ new RegExp(`(compile\\s+["']${groupId}:${artifactId}:)${currentVersion}(["'])`, 'g'),
555
+ ];
556
+
557
+ let updated = false;
558
+ for (const pattern of patterns) {
559
+ if (pattern.test(content)) {
560
+ content = content.replace(pattern, `$1${update.latestVersion}$2`);
561
+ updated = true;
562
+ break;
563
+ }
564
+ }
565
+
566
+ if (updated) {
567
+ result.updated++;
568
+ modified = true;
569
+ } else {
570
+ result.skipped++;
571
+ }
572
+ }
573
+
574
+ if (modified) {
575
+ fs.writeFileSync(`${gradlePath}.backup`, fs.readFileSync(gradlePath));
576
+ fs.writeFileSync(gradlePath, content);
577
+ }
578
+
579
+ return result;
580
+ }
581
+
474
582
  generateReport(result: DependencyAnalysisResult): string {
475
583
  const lines: string[] = [];
476
584
  lines.push("");
477
- lines.push(`${Logger.C.cyan}══════════════════════════════════════════════════════════${Logger.C.reset}`);
585
+ lines.push(`${Logger.C.primary}══════════════════════════════════════════════════════════${Logger.C.reset}`);
478
586
  lines.push(`${Logger.C.bold}📊 ANÁLISE DE DEPENDÊNCIAS${Logger.C.reset}`);
479
- lines.push(`${Logger.C.cyan}══════════════════════════════════════════════════════════${Logger.C.reset}`);
587
+ lines.push(`${Logger.C.primary}══════════════════════════════════════════════════════════${Logger.C.reset}`);
480
588
  lines.push("");
481
589
 
482
590
  // Estatísticas
@@ -487,10 +595,10 @@ export class DependencyAnalyzerService {
487
595
 
488
596
  // Conflitos
489
597
  if (result.conflicts.length > 0) {
490
- lines.push(`${Logger.C.yellow}⚠️ CONFLITOS DE VERSÃO (${result.conflicts.length})${Logger.C.reset}`);
598
+ lines.push(`${Logger.C.warning}⚠️ CONFLITOS DE VERSÃO (${result.conflicts.length})${Logger.C.reset}`);
491
599
  for (const conflict of result.conflicts) {
492
600
  const icon = conflict.severity === "error" ? "✖" : "▲";
493
- const color = conflict.severity === "error" ? Logger.C.red : Logger.C.yellow;
601
+ const color = conflict.severity === "error" ? Logger.C.error : Logger.C.warning;
494
602
  lines.push(` ${color}${icon}${Logger.C.reset} ${conflict.groupId}:${conflict.artifactId}`);
495
603
  lines.push(` Versões: ${conflict.versions.join(", ")}`);
496
604
  }
@@ -503,10 +611,10 @@ export class DependencyAnalyzerService {
503
611
  const minorUpdates = result.updates.filter(u => !u.isMajor);
504
612
 
505
613
  if (minorUpdates.length > 0) {
506
- lines.push(`${Logger.C.green}⬆️ ATUALIZAÇÕES DISPONÍVEIS (${minorUpdates.length})${Logger.C.reset}`);
614
+ lines.push(`${Logger.C.success}⬆️ ATUALIZAÇÕES DISPONÍVEIS (${minorUpdates.length})${Logger.C.reset}`);
507
615
  for (const update of minorUpdates.slice(0, 5)) {
508
- lines.push(` ${Logger.C.green}↑${Logger.C.reset} ${update.groupId}:${update.artifactId}`);
509
- lines.push(` ${update.currentVersion} → ${Logger.C.green}${update.latestVersion}${Logger.C.reset}`);
616
+ lines.push(` ${Logger.C.success}↑${Logger.C.reset} ${update.groupId}:${update.artifactId}`);
617
+ lines.push(` ${update.currentVersion} → ${Logger.C.success}${update.latestVersion}${Logger.C.reset}`);
510
618
  }
511
619
  if (minorUpdates.length > 5) {
512
620
  lines.push(` ${Logger.C.dim}... e mais ${minorUpdates.length - 5}${Logger.C.reset}`);
@@ -515,11 +623,11 @@ export class DependencyAnalyzerService {
515
623
  }
516
624
 
517
625
  if (majorUpdates.length > 0) {
518
- lines.push(`${Logger.C.yellow}⚠️ ATUALIZAÇÕES MAJOR (${majorUpdates.length})${Logger.C.reset}`);
626
+ lines.push(`${Logger.C.warning}⚠️ ATUALIZAÇÕES MAJOR (${majorUpdates.length})${Logger.C.reset}`);
519
627
  lines.push(` ${Logger.C.dim}Podem conter breaking changes${Logger.C.reset}`);
520
628
  for (const update of majorUpdates.slice(0, 3)) {
521
- lines.push(` ${Logger.C.yellow}!${Logger.C.reset} ${update.groupId}:${update.artifactId}`);
522
- lines.push(` ${update.currentVersion} → ${Logger.C.yellow}${update.latestVersion}${Logger.C.reset}`);
629
+ lines.push(` ${Logger.C.warning}!${Logger.C.reset} ${update.groupId}:${update.artifactId}`);
630
+ lines.push(` ${update.currentVersion} → ${Logger.C.warning}${update.latestVersion}${Logger.C.reset}`);
523
631
  }
524
632
  lines.push("");
525
633
  }
@@ -527,7 +635,7 @@ export class DependencyAnalyzerService {
527
635
 
528
636
  // Resumo
529
637
  if (result.conflicts.length === 0 && result.updates.length === 0) {
530
- lines.push(`${Logger.C.green}✔ Todas as dependências estão atualizadas!${Logger.C.reset}`);
638
+ lines.push(`${Logger.C.success}✔ Todas as dependências estão atualizadas!${Logger.C.reset}`);
531
639
  }
532
640
 
533
641
  lines.push("");
@@ -2,7 +2,7 @@ import type { TomcatConfig, AppConfig } from "../types/config";
2
2
  import { Logger } from "../utils/ui";
3
3
  import type { Subprocess } from "bun";
4
4
  import { ProjectService } from "./ProjectService";
5
- import { existsSync, mkdirSync, writeFileSync, promises as fs } from "fs";
5
+ import { existsSync, mkdirSync, writeFileSync, statSync, promises as fs } from "fs";
6
6
  import path from "path";
7
7
  import os from "os";
8
8
 
@@ -98,10 +98,10 @@ export class TomcatService {
98
98
  const agentDir = path.join(os.homedir(), ".xavva", "agents");
99
99
  const agentPath = path.join(agentDir, "hotswap-agent-2.0.3.jar");
100
100
 
101
- if (fs.existsSync(agentPath) && fs.statSync(agentPath).size > 1000) return agentPath;
101
+ if (existsSync(agentPath) && statSync(agentPath).size > 1000) return agentPath;
102
102
 
103
103
  try {
104
- if (!fs.existsSync(agentDir)) fs.mkdirSync(agentDir, { recursive: true });
104
+ if (!existsSync(agentDir)) mkdirSync(agentDir, { recursive: true });
105
105
 
106
106
  Logger.step("Downloading HotswapAgent v2.0.3 (Global)...");
107
107
  const url = "https://github.com/HotswapProjects/HotswapAgent/releases/download/RELEASE-2.0.3/hotswap-agent-2.0.3.jar";
@@ -109,7 +109,7 @@ export class TomcatService {
109
109
  if (!response.ok) throw new Error(`Status: ${response.status}`);
110
110
 
111
111
  const buffer = await response.arrayBuffer();
112
- fs.writeFileSync(agentPath, Buffer.from(buffer));
112
+ writeFileSync(agentPath, Buffer.from(buffer));
113
113
  Logger.success("HotswapAgent v2.0.3 installed globally!");
114
114
  return agentPath;
115
115
  } catch (e) {
@@ -237,18 +237,21 @@ export class TomcatService {
237
237
  private async processLogStream(stream: ReadableStream, clean: boolean, quiet: boolean, verbose: boolean, grep: string) {
238
238
  const reader = stream.getReader();
239
239
  const decoder = new TextDecoder();
240
+ let buffer = "";
240
241
 
241
242
  while (true) {
242
243
  const { done, value } = await reader.read();
243
244
  if (done) break;
244
245
 
245
- const chunk = decoder.decode(value);
246
- const lines = chunk.split(/[\r\n]+/);
246
+ buffer += decoder.decode(value, { stream: true });
247
+ const lines = buffer.split(/\r?\n/);
248
+ buffer = lines.pop() || ""; // Keep incomplete line in buffer
247
249
 
248
250
  for (const line of lines) {
249
251
  const cleanLine = line.trim();
250
252
  if (!cleanLine || cleanLine.startsWith("Listening for transport")) continue;
251
253
 
254
+ // Detect startup completion
252
255
  if (cleanLine.includes("Server startup in") || cleanLine.includes("SEVERE") || cleanLine.includes("Exception")) {
253
256
  const isSuccess = cleanLine.includes("Server startup in");
254
257
  if (this.stopStartupSpinner) {
@@ -260,26 +263,44 @@ export class TomcatService {
260
263
  }
261
264
  }
262
265
 
266
+ // Verbose: formata logs do Tomcat
263
267
  if (verbose) {
264
- Logger.log(cleanLine);
268
+ if (Logger.isTomcatNoise(cleanLine)) {
269
+ continue; // Silencia noise completamente
270
+ }
271
+ const formatted = Logger.formatTomcatLog(cleanLine);
272
+ if (formatted) {
273
+ console.log(formatted);
274
+ }
265
275
  continue;
266
276
  }
267
277
 
278
+ // Clean mode: filtra noise
268
279
  if (clean) {
280
+ // Sempre filtra noise do sistema
281
+ if (Logger.isSystemNoise(cleanLine)) continue;
282
+
283
+ // Quiet mode: só mostra essencial
269
284
  if (quiet && !Logger.isEssential(cleanLine)) {
270
- if (Logger.isSystemNoise(cleanLine)) continue;
271
- if (cleanLine.includes("INFO")) continue;
272
- } else if (Logger.isSystemNoise(cleanLine)) {
273
- continue;
285
+ if (cleanLine.includes("INFO") && !cleanLine.includes("ERROR")) continue;
274
286
  }
275
287
 
288
+ // Grep filter
276
289
  if (grep && !cleanLine.toLowerCase().includes(grep.toLowerCase())) {
277
290
  if (!Logger.isEssential(cleanLine)) continue;
278
291
  }
279
292
 
280
293
  const summarized = Logger.summarize(cleanLine);
281
- if (summarized) Logger.log(summarized);
294
+ if (summarized) {
295
+ // Só mostra se não for vazio (rate limiting)
296
+ if (summarized.trim()) Logger.log(summarized);
297
+ }
282
298
  } else {
299
+ // Non-clean: mostra tudo mas formata
300
+ if (Logger.isSystemNoise(cleanLine)) {
301
+ // Silencia completamente noise em non-clean também
302
+ continue;
303
+ }
283
304
  Logger.log(cleanLine);
284
305
  }
285
306
  }