@archznn/xavva 1.6.5 → 1.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.
@@ -70,13 +70,129 @@ export class TomcatService {
70
70
  }
71
71
  }
72
72
 
73
- start(cleanLogs: boolean = false, debug: boolean = false, skipScan: boolean = false, quiet: boolean = false) {
73
+ private async ensureHotswapAgent(): Promise<string | null> {
74
+ const fs = require("fs");
75
+ const path = require("path");
76
+ const os = require("os");
77
+ const agentDir = path.join(os.homedir(), ".xavva", "agents");
78
+ const agentPath = path.join(agentDir, "hotswap-agent-2.0.3.jar");
79
+
80
+ if (fs.existsSync(agentPath) && fs.statSync(agentPath).size > 1000) return agentPath;
81
+
82
+ try {
83
+ if (!fs.existsSync(agentDir)) fs.mkdirSync(agentDir, { recursive: true });
84
+
85
+ Logger.step("Downloading HotswapAgent v2.0.3 (Global)...");
86
+ const url = "https://github.com/HotswapProjects/HotswapAgent/releases/download/RELEASE-2.0.3/hotswap-agent-2.0.3.jar";
87
+ const response = await fetch(url);
88
+ if (!response.ok) throw new Error(`Status: ${response.status}`);
89
+
90
+ const buffer = await response.arrayBuffer();
91
+ fs.writeFileSync(agentPath, Buffer.from(buffer));
92
+ Logger.success("HotswapAgent v2.0.3 installed globally!");
93
+ return agentPath;
94
+ } catch (e) {
95
+ Logger.warn("Falha ao baixar HotswapAgent. Usando hot swap padrão da JVM.");
96
+ return null;
97
+ }
98
+ }
99
+
100
+ private findAllClassPaths(buildTool: 'maven' | 'gradle'): string[] {
101
+ const fs = require("fs");
102
+ const path = require("path");
103
+ const results: string[] = [];
104
+ const root = process.cwd();
105
+
106
+ const scan = (dir: string) => {
107
+ try {
108
+ const files = fs.readdirSync(dir, { withFileTypes: true });
109
+ for (const file of files) {
110
+ if (!file.isDirectory()) continue;
111
+
112
+ const name = file.name;
113
+ if (name.startsWith('.') || ['node_modules', 'out', 'bin', 'src', 'webapps', '.xavva'].includes(name)) continue;
114
+
115
+ const fullPath = path.join(dir, name);
116
+
117
+ const isMavenClasses = buildTool === 'maven' && name === 'classes' && dir.endsWith('target');
118
+ const isGradleClasses = buildTool === 'gradle' && name === 'main' && dir.endsWith(path.join('classes', 'java'));
119
+
120
+ if (isMavenClasses || isGradleClasses) {
121
+ results.push(fullPath.replace(/\\/g, "/"));
122
+ } else {
123
+ scan(fullPath);
124
+ }
125
+ }
126
+ } catch (e) {}
127
+ };
128
+
129
+ scan(root);
130
+
131
+ if (results.length === 0) {
132
+ const defaultPath = buildTool === 'maven'
133
+ ? path.join(root, "target", "classes")
134
+ : path.join(root, "build", "classes", "java", "main");
135
+ results.push(defaultPath.replace(/\\/g, "/"));
136
+ }
137
+
138
+ return results;
139
+ }
140
+
141
+ async start(config: any, isWatching: boolean = false) {
74
142
  const binPath = `${this.activeConfig.path}\\bin\\catalina.bat`;
75
- const args = debug ? ["jpda", "run"] : ["run"];
143
+ const args = (config.project.debug || isWatching) ? ["jpda", "run"] : ["run"];
76
144
 
77
145
  const catalinaOpts = [process.env.CATALINA_OPTS || ""];
78
146
 
79
- if (skipScan) {
147
+ if (config.project.debug || isWatching) {
148
+ const agentPath = await this.ensureHotswapAgent();
149
+ if (agentPath) {
150
+ catalinaOpts.push(`-javaagent:${agentPath}`);
151
+
152
+ let javaBin = "java";
153
+ if (process.env.JAVA_HOME) {
154
+ javaBin = require("path").join(process.env.JAVA_HOME, "bin", "java.exe");
155
+ }
156
+
157
+ const javaVer = Bun.spawnSync([javaBin, "-version"]);
158
+ const output = (javaVer.stderr.toString() + javaVer.stdout.toString()).toLowerCase();
159
+
160
+ if (output.includes("dcevm") || output.includes("jbr") || output.includes("trava")) {
161
+ catalinaOpts.push("-XX:+AllowEnhancedClassRedefinition");
162
+ }
163
+
164
+ catalinaOpts.push(
165
+ "--add-opens=java.base/jdk.internal.loader=ALL-UNNAMED",
166
+ "--add-opens=java.base/java.lang=ALL-UNNAMED",
167
+ "--add-opens=java.base/java.io=ALL-UNNAMED",
168
+ "--add-opens=java.base/java.net=ALL-UNNAMED",
169
+ "--add-opens=java.base/java.util=ALL-UNNAMED",
170
+ "--add-opens=java.base/java.util.concurrent=ALL-UNNAMED",
171
+ "--add-opens=java.base/java.security=ALL-UNNAMED",
172
+ "--add-opens=java.base/jdk.internal.reflect=ALL-UNNAMED",
173
+ "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED",
174
+ "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED",
175
+ "--add-opens=java.base/java.util.jar=ALL-UNNAMED",
176
+ "--add-opens=java.desktop/java.beans=ALL-UNNAMED"
177
+ );
178
+
179
+ const fs = require("fs");
180
+ const path = require("path");
181
+ const xavvaDir = path.join(process.cwd(), ".xavva");
182
+ if (!fs.existsSync(xavvaDir)) fs.mkdirSync(xavvaDir, { recursive: true });
183
+
184
+ const classPaths = this.findAllClassPaths(config.project.buildTool);
185
+ const extraClasspath = classPaths.join(",");
186
+
187
+ const propsPath = path.join(xavvaDir, "hotswap-agent.properties");
188
+ const propsContent = `autoHotswap=true\nautoHotswap.delay=3000\nwatchResources=false\nextraClasspath=${extraClasspath}\nLOGGER=info`;
189
+ fs.writeFileSync(propsPath, propsContent);
190
+
191
+ catalinaOpts.push(`-Dhotswap-agent.properties.path=${propsPath}`);
192
+ }
193
+ }
194
+
195
+ if (config.project.skipScan) {
80
196
  catalinaOpts.push(
81
197
  "-Dtomcat.util.scan.StandardJarScanFilter.jarsToSkip=*.jar",
82
198
  "-Dtomcat.util.scan.StandardJarScanFilter.jarsToScan=",
@@ -93,13 +209,18 @@ export class TomcatService {
93
209
  CATALINA_OPTS: catalinaOpts.join(" ").trim()
94
210
  };
95
211
 
96
- if (debug) {
97
- Logger.warn("🐞 Java Debugger habilitado na porta 5005");
212
+ if (process.env.JAVA_HOME) {
213
+ env.JAVA_HOME = process.env.JAVA_HOME;
214
+ env.JRE_HOME = process.env.JAVA_HOME;
215
+ }
216
+
217
+ if (config.project.debug) {
218
+ Logger.debug("Java Debugger habilitado na porta 5005");
98
219
  env.JPDA_ADDRESS = "5005";
99
220
  env.JPDA_TRANSPORT = "dt_socket";
100
221
  }
101
222
 
102
- if (cleanLogs || quiet) {
223
+ if ((config.project.cleanLogs || config.project.quiet) && !config.project.verbose) {
103
224
  this.stopStartupSpinner = Logger.spinner("Starting Tomcat server");
104
225
  }
105
226
 
@@ -111,11 +232,11 @@ export class TomcatService {
111
232
 
112
233
  this.pid = this.currentProcess.pid;
113
234
 
114
- this.processLogStream(this.currentProcess.stdout, cleanLogs, quiet);
115
- this.processLogStream(this.currentProcess.stderr, cleanLogs, quiet);
235
+ this.processLogStream(this.currentProcess.stdout, config.project.cleanLogs, config.project.quiet, config.project.verbose, config.tomcat.grep);
236
+ this.processLogStream(this.currentProcess.stderr, config.project.cleanLogs, config.project.quiet, config.project.verbose, config.tomcat.grep);
116
237
  }
117
238
 
118
- private async processLogStream(stream: ReadableStream, clean: boolean, quiet: boolean) {
239
+ private async processLogStream(stream: ReadableStream, clean: boolean, quiet: boolean, verbose: boolean, grep: string) {
119
240
  const reader = stream.getReader();
120
241
  const decoder = new TextDecoder();
121
242
 
@@ -141,6 +262,11 @@ export class TomcatService {
141
262
  }
142
263
  }
143
264
 
265
+ if (verbose) {
266
+ Logger.log(cleanLine);
267
+ continue;
268
+ }
269
+
144
270
  if (clean) {
145
271
  if (quiet && !Logger.isEssential(cleanLine)) {
146
272
  if (Logger.isSystemNoise(cleanLine)) continue;
@@ -149,14 +275,14 @@ export class TomcatService {
149
275
  continue;
150
276
  }
151
277
 
152
- if (this.activeConfig.grep && !cleanLine.toLowerCase().includes(this.activeConfig.grep.toLowerCase())) {
278
+ if (grep && !cleanLine.toLowerCase().includes(grep.toLowerCase())) {
153
279
  if (!Logger.isEssential(cleanLine)) continue;
154
280
  }
155
281
 
156
282
  const summarized = Logger.summarize(cleanLine);
157
- console.log(summarized);
283
+ if (summarized) Logger.log(summarized);
158
284
  } else {
159
- console.log(cleanLine);
285
+ Logger.log(cleanLine);
160
286
  }
161
287
  }
162
288
  }
@@ -80,7 +80,7 @@ export class ConfigManager {
80
80
  if (fs.existsSync(path.join(process.cwd(), "build.gradle")) || fs.existsSync(path.join(process.cwd(), "build.gradle.kts"))) {
81
81
  return "gradle";
82
82
  }
83
- return "maven"; // Default to maven if not found
83
+ return "maven";
84
84
  }
85
85
 
86
86
  private static ensureGitIgnore() {
package/src/utils/ui.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import pkg from "../../package.json";
2
2
 
3
3
  export class Logger {
4
- private static readonly C = {
4
+ public static readonly C = {
5
5
  reset: "\x1b[0m",
6
6
  cyan: "\x1b[36m",
7
7
  green: "\x1b[32m",
@@ -9,60 +9,124 @@ export class Logger {
9
9
  red: "\x1b[31m",
10
10
  dim: "\x1b[90m",
11
11
  bold: "\x1b[1m",
12
- blue: "\x1b[34m"
12
+ blue: "\x1b[34m",
13
+ magenta: "\x1b[35m",
14
+ bgRed: "\x1b[41m",
15
+ white: "\x1b[37m",
16
+ gray: "\x1b[38;5;240m",
17
+ lightGray: "\x1b[38;5;248m",
18
+ darkGray: "\x1b[38;5;238m"
13
19
  };
14
20
 
15
- static getGitContext(): string {
21
+ private static hotswapPluginsCount = 0;
22
+ private static lastDomain = "";
23
+ private static lastHotswapMsg = "";
24
+ private static activeSpinnerMsg = "";
25
+
26
+ private static write(message: string, isError: boolean = false) {
27
+ if (this.activeSpinnerMsg) {
28
+ process.stdout.write("\r\x1B[K"); // Limpa a linha do spinner
29
+ }
30
+
31
+ if (isError) {
32
+ console.error(message + this.C.reset);
33
+ } else {
34
+ console.log(message + this.C.reset);
35
+ }
36
+
37
+ if (this.activeSpinnerMsg) {
38
+ // Re-imprime o início da linha do spinner para o próximo frame
39
+ process.stdout.write(` ${this.C.cyan}⠋${this.C.reset} ${this.activeSpinnerMsg}...`);
40
+ }
41
+ }
42
+
43
+ static getGitContext(): { branch: string, author: string, hash: string } {
16
44
  try {
17
45
  const branch = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"]).stdout.toString().trim();
18
46
  const author = Bun.spawnSync(["git", "log", "-1", "--format=%an"]).stdout.toString().trim();
19
- return branch ? `${this.C.blue} ${branch}${this.C.reset} ${this.C.dim}(${author})${this.C.reset}` : "";
47
+ const hash = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"]).stdout.toString().trim();
48
+ return { branch, author, hash };
20
49
  } catch (e) {
21
- return "";
50
+ return { branch: "", author: "", hash: "" };
22
51
  }
23
52
  }
24
53
 
25
54
  static banner(command?: string) {
26
55
  console.clear();
27
56
  const git = this.getGitContext();
28
- console.log(`${this.C.cyan}
29
- ${this.C.bold}XAVVA ${this.C.reset}${this.C.dim}v${pkg.version}${this.C.reset} ${git}
30
- ${this.C.dim}──────────────────────────${this.C.reset}`);
31
- if (command) {
32
- console.log(` ${this.C.yellow}${this.C.bold}MODO: ${command.toUpperCase()}${this.C.reset}\n`);
33
- }
57
+ const name = (process.cwd().split(/[/\\]/).pop() || "JAVA").toUpperCase();
58
+
59
+ const width = 62;
60
+ const line = "─".repeat(width);
61
+
62
+ this.write(`${this.C.gray}╭${line}╮`);
63
+ this.write(`${this.C.gray}│ ${this.C.bold}${this.C.blue}${name} CLI${this.C.reset}${" ".repeat(width - name.length - 6)} ${this.C.gray}│`);
64
+
65
+ const info = `Version: ${pkg.version} | Branch: ${git.branch} | ${git.hash}`;
66
+ this.write(`${this.C.gray}│ ${this.C.dim}${info}${" ".repeat(width - info.length - 2)}${this.C.gray}│`);
67
+
68
+ const modeLine = `Mode: ${command?.toUpperCase() || "DEPLOY"}`;
69
+ const status = command === 'dev' ? `${this.C.green}🟢` : `${this.C.blue}🔵`;
70
+ this.write(`${this.C.gray}│ ${this.C.yellow}${this.C.bold}${modeLine}${" ".repeat(width - modeLine.length - 5)}${status} ${this.C.gray}│`);
71
+ this.write(`${this.C.gray}╰${line}╯${this.C.reset}`);
34
72
  }
35
73
 
36
74
  static section(title: string) {
37
- console.log(`\n ${this.C.bold}${this.C.blue}${title.toUpperCase()} ${this.C.reset}`);
38
- console.log(` ${this.C.dim}──────────────────────────${this.C.reset}`);
75
+ this.write(`\n${this.C.bold}${this.C.blue}[${title.toUpperCase()}]${this.C.reset}`);
39
76
  }
40
77
 
41
- static info(label: string, value: string | number | boolean) {
42
- console.log(` ${this.C.cyan}${label.padEnd(12)}${this.C.reset} ${this.C.bold}${value}${this.C.reset}`);
78
+ private static domain(name: string) {
79
+ if (this.lastDomain !== name) {
80
+ this.write(`\n${this.C.bold}${this.C.blue}[${name.toUpperCase()}]${this.C.reset}`);
81
+ this.lastDomain = name;
82
+ }
43
83
  }
44
84
 
45
- static success(msg: string) {
46
- console.log(`\n ${this.C.green}✔ ${msg}${this.C.reset}`);
85
+ static config(label: string, value: string | number | boolean) {
86
+ this.domain("config");
87
+ this.info(label, value);
47
88
  }
48
89
 
49
- static error(msg: string) {
50
- console.error(`\n ${this.C.red} ${msg}${this.C.reset}`);
90
+ static info(label: string, value: string | number | boolean) {
91
+ this.write(` ${this.C.lightGray}${label.padEnd(12)}${this.C.reset} : ${this.C.bold}${value}${this.C.reset}`);
51
92
  }
52
93
 
53
- static warn(msg: string) {
54
- console.log(` ${this.C.yellow}⚠ ${msg}${this.C.reset}`);
94
+ static build(msg: string, status: 'start' | 'success' | 'error' | 'info' = 'success') {
95
+ this.domain("build");
96
+ const symbol = status === 'start' ? `${this.C.blue}▶` : status === 'success' ? `${this.C.green}✔` : status === 'error' ? `${this.C.red}✖` : `${this.C.dim}ℹ`;
97
+ this.write(` ${symbol} ${this.C.reset}${msg}`);
55
98
  }
56
99
 
57
- static log(msg: string) {
58
- console.log(` ${msg}`);
100
+ static server(msg: string, status: 'start' | 'success' | 'error' | 'info' = 'info') {
101
+ this.domain("server");
102
+ const symbol = status === 'start' ? `${this.C.blue}▶` : status === 'success' ? `${this.C.green}✔` : status === 'error' ? `${this.C.red}✖` : `${this.C.dim}ℹ`;
103
+ this.write(` ${symbol} ${this.C.reset}${msg}`);
59
104
  }
60
105
 
61
- static step(msg: string) {
62
- console.log(` ${this.C.dim}➜ ${msg}${this.C.reset}`);
106
+ static health(msg: string, status: 'success' | 'error' | 'warn' = 'success') {
107
+ this.domain("health");
108
+ const symbol = status === 'success' ? `${this.C.green}✔` : status === 'error' ? `${this.C.red}✖` : `${this.C.yellow}⚠`;
109
+ this.write(` ${symbol} ${this.C.reset}${msg}`);
63
110
  }
64
111
 
112
+ static watcher(msg: string, status: 'watch' | 'change' | 'start' | 'success' = 'success') {
113
+ this.domain("watcher");
114
+ const symbol = status === 'watch' ? `${this.C.magenta}👀` : status === 'change' ? `${this.C.yellow}▲` : status === 'start' ? `${this.C.blue}▶` : `${this.C.green}✔`;
115
+ this.write(` ${symbol} ${this.C.reset}${msg}`);
116
+ }
117
+
118
+ static success(msg: string) { this.write(` ${this.C.green}✔ ${msg}`); }
119
+ static error(msg: string) { this.write(` ${this.C.red}✖ ${msg}`, true); }
120
+ static warn(msg: string) { this.write(` ${this.C.yellow}⚠ ${msg}`); }
121
+ static log(msg: string) { this.write(` ${msg}`); }
122
+ static step(msg: string) { this.write(` ${this.C.dim}» ${msg}`); }
123
+ static debug(msg: string) { this.write(` ${this.C.magenta}🐛 ${msg}`); }
124
+ static process(msg: string) { this.write(` ${this.C.blue}▶ ${msg}`); }
125
+ static newline() { this.write(""); }
126
+ static dim(msg: string) { this.write(` ${this.C.dim}${msg}${this.C.reset}`); }
127
+
65
128
  static spinner(msg: string) {
129
+ this.activeSpinnerMsg = msg;
66
130
  const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
67
131
  let i = 0;
68
132
  process.stdout.write("\x1B[?25l");
@@ -74,121 +138,151 @@ export class Logger {
74
138
 
75
139
  return (success = true) => {
76
140
  clearInterval(timer);
141
+ this.activeSpinnerMsg = "";
77
142
  process.stdout.write("\r\x1B[K");
78
143
  process.stdout.write("\x1B[?25h");
79
144
  if (success) {
80
- console.log(` ${this.C.green}✔${this.C.reset} ${msg}`);
145
+ this.write(` ${this.C.green}✔${this.C.reset} ${msg}`);
146
+ } else {
147
+ this.error(`Falha em ${msg}`);
81
148
  }
82
149
  };
83
150
  }
84
151
 
85
152
  static isSystemNoise(line: string): boolean {
86
153
  const noise = [
87
- "Using CATALINA_",
88
- "Using JRE_HOME",
89
- "Using CLASSPATH",
90
- "NOTE: Picked up JDK_JAVA_OPTIONS",
91
- "Command line argument",
92
- "VersionLoggerListener",
93
- "Scanning for projects...",
94
- "Building ",
95
- "--- ",
96
- "+++ ",
97
- "DEBUG: ",
98
- "org.apache.catalina.startup.VersionLoggerListener",
99
- "org.apache.catalina.core.AprLifecycleListener",
100
- "org.apache.coyote.AbstractProtocol.init",
101
- "org.apache.catalina.startup.Catalina.load",
102
- "Arquivos processados em",
103
- "org.apache.jasper.servlet.TldScanner.scanJars",
104
- "Listening for transport dt_socket",
105
- "org.apache.catalina.startup.ExpandWar.expand",
106
- "org.apache.catalina.startup.ContextConfig.configureStart",
107
- "SLF4J: ",
108
- "org.glassfish.jersey.internal.Errors.logErrors",
109
- "contains empty path annotation",
110
- "org.apache.catalina.core.StandardContext.setPath",
111
- "milliseconds"
154
+ "Using CATALINA_", "Using JRE_HOME", "Using CLASSPATH", "NOTE: Picked up JDK_JAVA_OPTIONS",
155
+ "Command line argument", "VersionLoggerListener", "Scanning for projects...",
156
+ "Building ", "--- ", "+++ ", "DEBUG: ", "org.apache.catalina.startup.VersionLoggerListener",
157
+ "org.apache.catalina.core.AprLifecycleListener", "org.apache.coyote.AbstractProtocol.init",
158
+ "org.apache.catalina.startup.Catalina.load", "Arquivos processados em",
159
+ "org.apache.jasper.servlet.TldScanner.scanJars", "Listening for transport dt_socket",
160
+ "org.apache.catalina.startup.ExpandWar.expand", "org.apache.catalina.startup.ContextConfig.configureStart",
161
+ "SLF4J: ", "org.glassfish.jersey.internal.Errors.logErrors", "contains empty path annotation",
162
+ "org.apache.catalina.core.StandardContext.setPath", "milliseconds",
163
+ "org.apache.catalina.startup.HostConfig.deployWAR", "org.apache.catalina.startup.HostConfig.deployDirectory",
164
+ "Deployment of web application", "Deploying web application archive", "at org.apache",
165
+ "Registering directory"
112
166
  ];
113
167
  return noise.some(n => line.includes(n));
114
168
  }
115
169
 
116
170
  static isEssential(line: string): boolean {
117
- return line.includes("SEVERE") ||
118
- line.includes("ERROR") ||
119
- line.includes("Exception") ||
120
- line.includes("Caused by") ||
121
- line.includes("at ") ||
122
- line.includes("... ") ||
123
- line.includes("Server startup in");
171
+ return line.includes("SEVERE") || line.includes("ERROR") || line.includes("Exception") ||
172
+ line.includes("Caused by") || line.includes("at ") || line.includes("... ") ||
173
+ line.includes("Server startup in") || line.includes("HOTSWAP AGENT:");
124
174
  }
125
175
 
126
176
  static summarize(line: string): string {
177
+ if (this.isSystemNoise(line)) return "";
178
+
127
179
  const startupMatch = line.match(/Server startup in (\[?)(.*?)(\]?)\s*ms/);
128
180
  if (startupMatch) {
129
- const time = startupMatch[2] || "???";
130
- return `\n ${this.C.green}${this.C.bold}🚀 TOMCAT PRONTO EM ${time}ms${this.C.reset}\n`;
181
+ const time = (parseInt(startupMatch[2]) / 1000).toFixed(1);
182
+ this.domain("server");
183
+ return `${this.C.green}✔ ${this.C.bold}Server started in ${time}s`;
184
+ }
185
+
186
+ const deployMatch = line.match(/Deployment of web application archive \[(.*?)\] has finished in \[(.*?)\] ms/);
187
+ if (deployMatch) {
188
+ this.domain("build");
189
+ return `${this.C.green}✔ Artifacts deployed`;
190
+ }
191
+
192
+ const hotswapPattern = /HOTSWAP AGENT:.*? (INFO|WARN|ERROR|RELOAD) (.*?) - (.*)/;
193
+ const hotswapMatch = line.match(hotswapPattern);
194
+ if (hotswapMatch) {
195
+ const level = hotswapMatch[1];
196
+ let msg = hotswapMatch[3];
197
+
198
+ if (msg.includes("plugin initialized")) {
199
+ this.hotswapPluginsCount++;
200
+ return "";
201
+ }
202
+
203
+ if (msg.includes("redefinition") || msg.includes("reloaded") || level === 'RELOAD') {
204
+ if (msg.includes("Reloading classes [")) {
205
+ const classes = msg.match(/\[(.*?)\]/)?.[1] || "";
206
+ const classCount = classes.split(",").length;
207
+ if (classCount > 3) msg = `Reloading ${classCount} classes...`;
208
+ }
209
+
210
+ if (msg === this.lastHotswapMsg) return "";
211
+ this.lastHotswapMsg = msg;
212
+
213
+ this.watcher(`Hotswap: ${msg.replace(/Class '.*?'/, (m) => this.C.bold + m + this.C.reset)}`, 'success');
214
+ return "";
215
+ }
216
+
217
+ if (msg.includes("Loading Hotswap agent")) {
218
+ this.domain("server");
219
+ return `${this.C.blue}▶ ${this.C.reset}Initializing Hotswap Agent ${msg.match(/\d+\.\d+\.\d+/)?.[0] || ""}`;
220
+ }
221
+
222
+ if (this.hotswapPluginsCount > 0) {
223
+ const count = this.hotswapPluginsCount;
224
+ this.hotswapPluginsCount = 0;
225
+ this.domain("server");
226
+ this.write(` ${this.C.green}✔ ${this.C.reset}Hotswap ready (Plugins: ${count} loaded)`);
227
+ }
228
+
229
+ let color = this.C.cyan;
230
+ let symbol = "●";
231
+ if (level === "WARN") { color = this.C.yellow; symbol = "▲"; }
232
+ else if (level === "ERROR") { color = this.C.red; symbol = "✖"; }
233
+
234
+ this.domain("server");
235
+ return `${color}${symbol} ${this.C.bold}Hotswap:${this.C.reset} ${msg}`;
236
+ }
237
+
238
+ if (line.includes("java.lang.UnsupportedOperationException") && (line.includes("add a method") || line.includes("change the schema"))) {
239
+ this.domain("watcher");
240
+ this.write(` ${this.C.red}✖ ${this.C.bold}Hotswap Falhou:${this.C.reset} Mudança estrutural detectada (novo método/campo).`);
241
+ this.write(` ${this.C.yellow}💡 Dica: Sua JVM atual não suporta mudar a estrutura da classe. Reinicie o servidor para aplicar.`);
242
+ return "";
131
243
  }
132
244
 
133
- if (line.match(/^\[(INFO|WARN|ERROR)\]\s*$/) || line.includes("--- maven-") || line.includes("--- bpo-")) return "";
134
- if (line.includes("Scanning for projects...") || line.includes("Building bpo-consig") || line.includes("--- ")) return "";
245
+ const tomcatPattern = /^(\d{2}-\w{3}-\d{4} \d{2}:\d{2}:\d{2}\.\d{3})\s+(INFO|WARNING|SEVERE|ERROR)\s+\[(.*?)\]\s+(.*)$/;
246
+ const tMatch = line.match(tomcatPattern);
247
+ if (tMatch) {
248
+ const label = tMatch[2];
249
+ let msg = tMatch[4].trim();
250
+ if (this.isSystemNoise(msg)) return "";
251
+ let color = this.C.dim;
252
+ let symbol = "ℹ";
253
+ if (label === "WARNING") { color = this.C.yellow; symbol = "▲"; }
254
+ else if (label === "SEVERE" || label === "ERROR") { color = this.C.red; symbol = "✖"; }
255
+ msg = msg.replace(/^(org\.apache|com\.sun|java\..*?|org\.glassfish)\.[a-zA-Z0-9.]+\s/, "").trim();
256
+ if (!msg) return "";
257
+ return `${color}${symbol} ${msg}`;
258
+ }
135
259
 
136
260
  const compilationErrorMatch = line.match(/^\[ERROR\]\s+(.*\.java):\[(\d+),(\d+)\]\s+(.*)$/);
137
261
  if (compilationErrorMatch) {
138
262
  const [_, filePath, row, col, msg] = compilationErrorMatch;
139
263
  const fileName = filePath.split(/[/\\]/).pop();
140
-
141
- let contextTip = "";
142
- if (msg.includes("unmappable character") || msg.includes("encoding")) {
143
- contextTip = `\n ${this.C.yellow}💡 Dica: Erro de encoding detectado. O arquivo parece usar um charset (como UTF-8) diferente do configurado no Maven.${this.C.reset}`;
144
- } else if (msg.includes("illegal character")) {
145
- if (msg.includes("\\u00bb") || msg.includes("\\u00bf") || msg.includes("\\u00ef")) {
146
- contextTip = `\n ${this.C.yellow}💡 Dica: UTF-8 BOM detectado! O arquivo tem caracteres invisíveis no início que o Java não aceita. Use 'xavva doctor' para corrigir.${this.C.reset}`;
147
- } else {
148
- contextTip = `\n ${this.C.yellow}💡 Dica: Caractere invisível ou inválido. Tente remover espaços ou quebras de linha estranhas no topo do arquivo.${this.C.reset}`;
149
- }
150
- } else if (msg.includes("cannot find symbol")) {
151
- contextTip = `\n ${this.C.yellow}💡 Dica: Símbolo não encontrado. Verifique se o import está correto ou se a dependência existe.${this.C.reset}`;
152
- }
153
-
154
- return ` ${this.C.red}${this.C.bold}✖ ERROR ${this.C.reset}${this.C.dim}em ${this.C.reset}${this.C.bold}${fileName}${this.C.reset}${this.C.dim}:${row}${this.C.reset}\n ${this.C.red}➜ ${this.C.reset}${msg}${contextTip}\n`;
264
+ return `${this.C.red}✖ ERROR ${this.C.reset}${this.C.dim}em ${this.C.reset}${this.C.bold}${fileName}${this.C.reset}${this.C.dim}:${row}${this.C.reset} ${this.C.red}➜ ${this.C.reset}${msg}`;
155
265
  }
156
266
 
157
267
  const logPattern = /^\[(INFO|WARNING|WARN|SEVERE|ERROR)\]\s+(.*)$/;
158
268
  const match = line.match(logPattern);
159
-
160
269
  if (match) {
161
270
  const label = match[1];
162
- let msg = match[2];
163
-
271
+ let msg = match[2].trim();
164
272
  if (msg.includes("Total time:") || msg.includes("Finished at:") || msg.includes("Final Memory:") || msg.includes("-----------------------")) return "";
165
-
166
- let color = "";
167
- let prefix = "";
168
-
169
- if (label === "INFO") {
170
- color = this.C.dim;
171
- prefix = "ℹ";
172
- } else if (label === "WARNING" || label === "WARN") {
173
- color = this.C.yellow;
174
- prefix = "⚠";
175
- } else if (label === "SEVERE" || label === "ERROR") {
176
- color = this.C.red;
177
- prefix = "✘";
178
- }
179
-
273
+ let color = this.C.dim;
274
+ let symbol = "";
275
+ if (label === "WARNING") { color = this.C.yellow; symbol = ""; }
276
+ else if (label === "SEVERE" || label === "ERROR") { color = this.C.red; symbol = "✖"; }
180
277
  msg = msg.replace(/^(org\.apache|com\.sun|java\..*?)\.[a-zA-Z0-9.]+\s/, "").trim();
181
278
  if (!msg || msg === "]" || msg.includes("Compilation failure")) return "";
182
-
183
- return ` ${color}${prefix} ${msg}${this.C.reset}`;
279
+ return `${color}${symbol} ${msg}`;
184
280
  }
185
281
 
186
- if (line.includes("Exception") || line.includes("at ") || line.includes("Caused by")) {
282
+ if (line.includes("Exception") || line.includes("Caused by") || line.includes("at ")) {
187
283
  const trimmed = line.trim();
188
- if (trimmed.includes("org.apache") || trimmed.includes("java.base") || trimmed.includes("sun.reflect")) {
189
- return ` ${this.C.dim}${trimmed}${this.C.reset}`;
190
- }
191
- return ` ${this.C.yellow}${trimmed}${this.C.reset}`;
284
+ const color = (trimmed.includes("org.apache") || trimmed.includes("java.base") || trimmed.includes("sun.reflect")) ? this.C.dim : this.C.yellow;
285
+ return ` ${color}${trimmed}`;
192
286
  }
193
287
 
194
288
  return "";