@archznn/xavva 1.8.0 → 2.0.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 CHANGED
@@ -1,4 +1,4 @@
1
- # XAVVA 🚀 (Windows Only) `v1.8.0`
1
+ # XAVVA 🚀 (Windows Only) `v2.0.0`
2
2
 
3
3
  Xavva é uma CLI de alto desempenho construída com **Bun** para automatizar o ciclo de desenvolvimento de aplicações Java (Maven/Gradle) rodando no Apache Tomcat. Ela foi desenhada especificamente para desenvolvedores que buscam a velocidade de ambientes modernos (como Node.js/Vite) dentro do ecossistema Java Enterprise.
4
4
 
@@ -10,102 +10,69 @@ Desenvolver para Java/Tomcat tradicionalmente envolve ciclos lentos de `clean in
10
10
 
11
11
  ### ⚡ Funcionalidades de Elite
12
12
 
13
+ - **Interactive Dashboard (TUI)**: Um painel em tempo real (`--tui`) com métricas de sistema, status do servidor e atalhos rápidos (Restart, Clear, Quit).
14
+ - **Smart Log Analyzer**: Logs inteligentes que escondem ruídos do framework (Stack Folding) e destacam a causa raiz de erros Java.
13
15
  - **Ultra-Fast Hot Swap**: Compilação incremental e injeção direta de arquivos `.class` e recursos (JSP, HTML, CSS, JS) no Tomcat em execução sem restart.
14
16
  - **Gradle & Maven Native**: Suporte robusto para ambos os ecossistemas, incluindo extração automática de classpath para execução de classes standalone (`run`/`debug`).
15
- - **DCEVM Integration**: O Xavva pode baixar e configurar automaticamente uma JDK com DCEVM (JetBrains Runtime), permitindo mudanças estruturais em classes (novos métodos/campos) em tempo real.
16
- - **API Documentation (Swagger-like)**: Mapeamento estático de endpoints, métodos HTTP e parâmetros diretamente no terminal via `xavva docs`.
17
- - **Live Reload Automático**: Sincronização inteligente que atualiza o browser (Chrome/Edge) após mudanças em JSPs ou recursos estáticos.
18
17
  - **Segurança & Robustez**: Auditoria de dependências (`.jar`) e execução protegida contra *Command Injection* no PowerShell.
19
- - **Pathing JAR (Windows)**: Contorna limites de caracteres do Windows em classpaths gigantes através de geração dinâmica de Manifestos compatíveis com a especificação Java.
20
- - **Auto-Healing**: Diagnóstico e reparo automático de problemas comuns de ambiente, como encoding UTF-8 com BOM.
18
+ - **Pathing JAR (Windows)**: Contorna limites de caracteres do Windows em classpaths gigantes.
19
+ - **Auto-Healing**: Diagnóstico e reparo automático de problemas comuns de ambiente.
21
20
 
22
21
  ---
23
22
 
24
23
  ## 🚀 Começo Rápido
25
24
 
26
- ### Pré-requisitos
27
- - **Windows** (Otimizado para PowerShell Core e Windows PowerShell)
28
- - **Bun** instalado (`powershell -c "irm bun.sh/install.ps1 | iex"`)
29
- - **Tomcat** configurado via variável de ambiente `TOMCAT_HOME` ou `CATALINA_HOME`.
30
-
31
25
  ### Instalação
32
26
  ```powershell
33
27
  # Instalação global via NPM
34
28
  npm install -g @archznn/xavva
35
29
 
36
- # Ou use diretamente via npx
37
- npx @archznn/xavva dev
30
+ # Iniciar em modo Dashboard (TUI)
31
+ xavva dev --tui
38
32
  ```
39
33
 
40
34
  ---
41
35
 
42
36
  ## 📖 Referência de Comandos
43
37
 
44
- O Xavva utiliza uma arquitetura modular de comandos e serviços, garantindo alta performance e extensibilidade.
38
+ O Xavva 2.0 utiliza uma arquitetura modular de comandos e serviços.
45
39
 
46
40
  ### 1. Modo Desenvolvimento (`xavva dev`)
47
41
  O comando principal para o dia a dia. Ativa o monitoramento de arquivos e o Hot-Reload.
48
- - **O que faz**: Compila Java, sincroniza recursos, limpa logs, inicia o Tomcat e monitora mudanças.
49
42
  - **Flags úteis**:
43
+ - `--tui`: Ativa o Dashboard interativo no terminal.
50
44
  - `--no-build`: Pula o build inicial.
51
45
  - `--watch`: Ativa o modo de observação de arquivos (padrão em `dev`).
52
46
  - `--port 8081`: Define uma porta específica para o Tomcat.
53
- - `--dp 9000`: Altera a porta do Debugger (JPDA) (padrão 5005).
54
-
55
- ### 2. Execução de Classes (`xavva run` / `xavva debug`)
56
- Executa classes Java standalone (`public static void main`) com resolução automática de dependências Maven ou Gradle.
57
- - **Inteligência de Classpath**: Gera automaticamente um `classpath.jar` temporário (Pathing JAR) para evitar o erro de "Command line too long" no Windows.
58
- - **Busca por Grep**: Se você fornecer apenas parte do nome da classe, o Xavva a encontrará recursivamente no projeto.
59
-
60
- ### 3. Documentação de API (`xavva docs`)
61
- Gera uma documentação instantânea dos seus controladores Jersey/Spring no terminal.
62
- - Mostra a URL completa, método HTTP e parâmetros (Path, Query, Body).
63
-
64
- ### 4. Diagnóstico e Reparo (`xavva doctor`)
65
- Verifica se o seu ambiente está saudável.
66
- - **`xavva doctor --fix`**:
67
- - Instala o **JetBrains Runtime (DCEVM)** se necessário.
68
- - Remove automaticamente o **BOM (Byte Order Mark)** de arquivos Java que causam erros de compilação.
69
- - Configura o `JAVA_HOME` do sistema.
70
47
 
71
- ### 5. Auditoria de Segurança (`xavva audit`)
72
- Analisa a pasta `WEB-INF/lib` em busca de JARs vulneráveis via integração com **OSV.dev**. Essencial para manter a integridade do projeto antes de deploys em produção.
48
+ ### 2. Configuração de Projeto (`xavva.json`)
49
+ Crie um arquivo `xavva.json` na raiz do seu projeto para salvar suas configurações:
50
+ ```json
51
+ {
52
+ "project": {
53
+ "appName": "meu-app",
54
+ "buildTool": "maven",
55
+ "tui": true
56
+ },
57
+ "tomcat": {
58
+ "port": 8080
59
+ }
60
+ }
61
+ ```
73
62
 
74
- ### 6. Logs em Tempo Real (`xavva logs`)
75
- Exibe os logs do Tomcat filtrando ruídos excessivos e destacando StackTraces importantes. Use `--grep "NomeDaClasse"` para focar em logs específicos.
63
+ ### 3. Execução de Classes (`xavva run` / `xavva debug`)
64
+ Executa classes Java standalone (`public static void main`) com resolução automática de dependências.
76
65
 
77
66
  ---
78
67
 
79
- ## 🏗️ Arquitetura do Sistema
68
+ ## 🏗️ Arquitetura Xavva 2.0
80
69
 
81
70
  O Xavva foi refatorado para uma arquitetura de **Injeção de Dependências** e **Serviços Centralizados**:
82
71
 
83
- - **CommandRegistry**: Gerenciamento modular de comandos via Command Pattern.
84
- - **ProjectService**: Inteligência centralizada para descoberta de diretórios de build, artefatos e classpaths Java.
85
- - **AuditService**: Segurança aprimorada com execução isolada e protegida no PowerShell.
86
- - **TomcatService**: Gerenciamento do ciclo de vida do servidor com suporte a hotswap dinâmico.
87
-
88
- ---
89
-
90
- ## ⚙️ Configuração (Zero Config)
91
-
92
- O Xavva funciona sem arquivos de configuração externos, baseando-se no ambiente:
93
-
94
- | Variável | Descrição |
95
- |----------|-----------|
96
- | `TOMCAT_HOME` | Caminho raiz do seu Apache Tomcat. |
97
- | `JAVA_HOME` | JDK utilizada para compilação e execução. |
98
-
99
- **Dica**: O Xavva cria automaticamente uma pasta `.xavva` no seu projeto para cache e artefatos temporários, e a adiciona ao seu `.gitignore`.
100
-
101
- ---
102
-
103
- ## 🧩 Sincronização de Recursos
104
-
105
- Ao editar um arquivo, o Xavva decide a melhor estratégia:
106
- - **`.java`**: Compila apenas a classe e injeta o bytecode via `fastSync`.
107
- - **`.jsp` / `.html` / `.css`**: Sincroniza o arquivo diretamente na pasta de deploy do Tomcat.
108
- - **`pom.xml` / `build.gradle`**: Identifica mudanças estruturais e sugere um rebuild completo com invalidação de cache inteligente.
72
+ - **DashboardService**: Gerenciamento de interface TUI e interatividade.
73
+ - **LogAnalyzer**: Processamento inteligente de logs e stack traces.
74
+ - **ProjectService**: Inteligência centralizada para descoberta de diretórios e artefatos.
75
+ - **CommandRegistry**: Despacho modular de comandos.
109
76
 
110
77
  ---
111
78
  *Desenvolvido para transformar o legado em produtivo. 🚀*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archznn/xavva",
3
- "version": "1.8.0",
3
+ "version": "2.0.0",
4
4
  "description": "Ultra-fast CLI tool for Java/Tomcat development on Windows with Hot-Reload and Zero Config.",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -27,16 +27,13 @@ export class DeployCommand implements Command {
27
27
  const contextPath = (config.project.appName || "").replace(".war", "");
28
28
 
29
29
  if (!incremental) {
30
- tomcat.clearWebapps(contextPath);
30
+ await tomcat.killConflict();
31
+ await tomcat.clearWebapps();
31
32
 
32
- const buildTask = config.project.skipBuild ? Promise.resolve() : builder.runBuild(incremental);
33
- const killTask = tomcat.killConflict();
34
-
35
33
  if (!config.project.skipBuild) {
36
- Logger.watcher("Preparing environment and building in parallel", "start");
34
+ Logger.watcher("Building project", "start");
35
+ await builder.runBuild(incremental);
37
36
  }
38
-
39
- await Promise.all([buildTask, killTask]);
40
37
 
41
38
  if (!config.project.skipBuild) {
42
39
  Logger.build("Full project build and environment ready");
@@ -1,34 +1,36 @@
1
1
  import type { Command } from "./Command";
2
- import type { AppConfig } from "../types/config";
2
+ import type { AppConfig, CLIArguments } from "../types/config";
3
+ import { DashboardService } from "../services/DashboardService";
4
+ import { LogAnalyzer } from "../services/LogAnalyzer";
3
5
  import { Logger } from "../utils/ui";
4
6
  import path from "path";
5
7
  import fs from "fs";
6
8
 
7
9
  export class LogsCommand implements Command {
8
- async execute(config: AppConfig): Promise<void> {
10
+ constructor(private dashboard?: DashboardService, private logAnalyzer?: LogAnalyzer) {}
11
+
12
+ async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
9
13
  const logPath = path.join(config.tomcat.path, "logs", "catalina.out");
10
14
 
11
15
  if (!fs.existsSync(logPath)) {
12
- Logger.error(`Arquivo de log não encontrado: ${logPath}`);
16
+ const errorMsg = `Arquivo de log não encontrado: ${logPath}`;
17
+ if (this.dashboard) this.dashboard.log(Logger.C.red + errorMsg);
18
+ else Logger.error(errorMsg);
13
19
  return;
14
20
  }
15
21
 
16
- Logger.section(`Monitoring Logs: ${logPath}`);
17
- if (config.tomcat.grep) {
18
- Logger.info("Filter", config.tomcat.grep);
22
+ const analyzer = this.logAnalyzer || new LogAnalyzer(config.project);
23
+ const dashboard = this.dashboard || new DashboardService(config);
24
+
25
+ dashboard.setStatus("LOGGING", Logger.C.green);
26
+
27
+ if (args?.grep) {
28
+ dashboard.log(`${Logger.C.dim}Filter:${Logger.C.reset} ${Logger.C.bold}${args.grep}${Logger.C.reset}`);
19
29
  }
20
30
 
21
31
  const stats = fs.statSync(logPath);
22
32
  let currentSize = stats.size;
23
33
 
24
- const colorize = (line: string): string => {
25
- if (line.match(/SEVERE|ERROR|Exception|Error/i)) return `\x1b[31m${line}\x1b[0m`;
26
- if (line.match(/WARNING|WARN/i)) return `\x1b[33m${line}\x1b[0m`;
27
- if (line.match(/INFO/i)) return `\x1b[36m${line}\x1b[0m`;
28
- if (line.match(/DEBUG/i)) return `\x1b[90m${line}\x1b[0m`;
29
- return line;
30
- };
31
-
32
34
  fs.watch(logPath, (event) => {
33
35
  if (event === "change") {
34
36
  const newStats = fs.statSync(logPath);
@@ -43,18 +45,22 @@ export class LogsCommand implements Command {
43
45
  lines.forEach(line => {
44
46
  if (!line.trim()) return;
45
47
 
46
- if (config.tomcat.grep && !line.toLowerCase().includes(config.tomcat.grep.toLowerCase())) {
48
+ const grep = args?.grep || config.project.grep;
49
+ if (grep && !line.toLowerCase().includes(grep.toLowerCase())) {
47
50
  return;
48
51
  }
49
52
 
50
- process.stdout.write(colorize(line) + "\n");
53
+ const formatted = analyzer.summarize(line);
54
+ if (formatted) {
55
+ dashboard.log(formatted);
56
+ }
51
57
  });
52
58
  });
53
59
 
54
60
  currentSize = newStats.size;
55
61
  } else if (newStats.size < currentSize) {
56
62
  currentSize = newStats.size;
57
- Logger.warn("Arquivo de log foi resetado/rotacionado.");
63
+ dashboard.log(Logger.C.yellow + "Arquivo de log foi resetado/rotacionado.");
58
64
  }
59
65
  }
60
66
  });
@@ -1,7 +1,6 @@
1
1
  import type { Command } from "./Command";
2
2
  import type { AppConfig, CLIArguments } from "../types/config";
3
3
  import { Logger } from "../utils/ui";
4
- import { spawn } from "child_process";
5
4
  import path from "path";
6
5
 
7
6
  export class RunCommand implements Command {
@@ -54,19 +53,20 @@ export class RunCommand implements Command {
54
53
  Logger.warn(`🚀 Executando ${className}...`);
55
54
  }
56
55
 
57
- const bin = process.env.JAVA_HOME ? path.join(process.env.JAVA_HOME, "bin", "java") : "java";
56
+ const bin = process.env.JAVA_HOME ? path.join(process.env.JAVA_HOME, "bin", "java.exe") : "java";
58
57
 
59
- return new Promise((resolve) => {
60
- const child = spawn(bin, javaArgs, {
61
- stdio: "inherit",
62
- shell: false
63
- });
64
-
65
- child.on("exit", () => {
66
- Logger.log(`Sessão de ${isDebug ? "debug" : "execução"} encerrada.`);
67
- resolve();
68
- });
58
+ const proc = Bun.spawn([bin, ...javaArgs], {
59
+ stdout: "inherit",
60
+ stderr: "inherit",
61
+ stdin: "inherit",
62
+ env: {
63
+ ...process.env,
64
+ JAVA_OPTS: "-Xms256m -Xmx1024m"
65
+ } as any
69
66
  });
67
+
68
+ await proc.exited;
69
+ Logger.log(`Sessão de ${isDebug ? "debug" : "execução"} encerrada.`);
70
70
  }
71
71
 
72
72
  private async discoverClass(simpleName: string): Promise<string | null> {
package/src/index.ts CHANGED
@@ -17,6 +17,8 @@ import { BuildService } from "./services/BuildService";
17
17
  import { AuditService } from "./services/AuditService";
18
18
  import { WatcherService } from "./services/WatcherService";
19
19
  import { BuildCacheService } from "./services/BuildCacheService";
20
+ import { DashboardService } from "./services/DashboardService";
21
+ import { LogAnalyzer } from "./services/LogAnalyzer";
20
22
 
21
23
  import pkg from "../package.json";
22
24
  import { Logger } from "./utils/ui";
@@ -33,7 +35,7 @@ async function main() {
33
35
  const commandNames = ["deploy", "build", "start", "dev", "doctor", "run", "debug", "logs", "docs", "audit"];
34
36
  const commandName = positionals.find(p => commandNames.includes(p)) || "deploy";
35
37
 
36
- if (!values.help) {
38
+ if (!values.help && !values.tui) {
37
39
  Logger.banner(commandName);
38
40
  }
39
41
 
@@ -49,18 +51,23 @@ async function main() {
49
51
  const tomcatService = new TomcatService(config.tomcat);
50
52
  tomcatService.setProjectService(projectService);
51
53
  const auditService = new AuditService(config.tomcat);
54
+
55
+ // Xavva 2.0: Dashboard & LogAnalyzer
56
+ const logAnalyzer = new LogAnalyzer(config.project);
57
+ const dashboard = new DashboardService(config);
52
58
 
53
59
  // 2. Registrar Comandos
54
60
  const registry = new CommandRegistry();
55
61
 
56
62
  const deployCmd = new DeployCommand(tomcatService, buildService);
63
+ const logsCmd = new LogsCommand(dashboard, logAnalyzer);
57
64
 
58
65
  registry.register("build", new BuildCommand(buildService));
59
66
  registry.register("start", new StartCommand(tomcatService));
60
67
  registry.register("doctor", new DoctorCommand());
61
68
  registry.register("run", new RunCommand());
62
69
  registry.register("debug", new RunCommand());
63
- registry.register("logs", new LogsCommand());
70
+ registry.register("logs", logsCmd);
64
71
  registry.register("docs", new DocsCommand());
65
72
  registry.register("audit", new AuditCommand(auditService));
66
73
  registry.register("deploy", deployCmd);
@@ -72,7 +79,6 @@ async function main() {
72
79
  await watcher.start();
73
80
  } else {
74
81
  // 3. Executar do Registro
75
- // Ajusta flags baseadas no nome do comando para comandos compartilhados
76
82
  if (commandName === "debug") values.debug = true;
77
83
  if (commandName === "run") values.debug = false;
78
84
 
@@ -41,7 +41,7 @@ export class BuildCacheService {
41
41
  return crypto.createHash("md5").update(hash).digest("hex");
42
42
  }
43
43
 
44
- shouldRebuild(tool: "maven" | "gradle"): boolean {
44
+ shouldRebuild(tool: "maven" | "gradle", projectService?: any): boolean {
45
45
  if (!fs.existsSync(this.cacheFile)) return true;
46
46
 
47
47
  try {
@@ -51,8 +51,16 @@ export class BuildCacheService {
51
51
  // Se o pom/gradle mudou, precisa de rebuild completo
52
52
  if (currentHash !== cache.lastConfigHash) return true;
53
53
 
54
- // Verificar se o artefato (.war) ainda existe
55
- // (Esta parte será integrada ao BuildService)
54
+ // Verificar se o artefato (.war) ainda existe fisicamente
55
+ if (projectService) {
56
+ try {
57
+ const artifact = projectService.getArtifact();
58
+ if (!fs.existsSync(artifact.path)) return true;
59
+ } catch (e) {
60
+ return true;
61
+ }
62
+ }
63
+
56
64
  return false;
57
65
  } catch (e) {
58
66
  return true;
@@ -19,16 +19,22 @@ export class BuildService {
19
19
  }
20
20
 
21
21
  if (!incremental && !this.projectConfig.skipBuild) {
22
- if (!this.projectConfig.clean && !this.cache.shouldRebuild(this.projectConfig.buildTool)) {
22
+ if (!this.projectConfig.clean && !this.cache.shouldRebuild(this.projectConfig.buildTool, this.projectService)) {
23
23
  Logger.success("Build cache hit! Skipping full build.");
24
24
  return;
25
25
  }
26
26
  }
27
27
 
28
28
  const command = [];
29
+ const env = { ...process.env };
29
30
 
30
31
  if (this.projectConfig.buildTool === 'maven') {
31
32
  command.push("mvn");
33
+
34
+ if (!this.cache.shouldRebuild('maven', this.projectService)) {
35
+ command.push("-o");
36
+ }
37
+
32
38
  if (incremental) {
33
39
  command.push("compile");
34
40
  } else {
@@ -38,6 +44,8 @@ export class BuildService {
38
44
  }
39
45
  command.push("-Dmaven.test.skip=true", "-Dmaven.javadoc.skip=true");
40
46
  if (this.projectConfig.profile) command.push(`-P${this.projectConfig.profile}`);
47
+
48
+ env.MAVEN_OPTS = "-Xms512m -Xmx1024m -XX:+UseParallelGC";
41
49
  } else {
42
50
  command.push("gradle");
43
51
  if (incremental) {
@@ -49,13 +57,16 @@ export class BuildService {
49
57
  }
50
58
  command.push("-x", "test", "-x", "javadoc");
51
59
  if (this.projectConfig.profile) command.push(`-Pprofile=${this.projectConfig.profile}`);
60
+
61
+ env.GRADLE_OPTS = "-Xmx1024m -Dorg.gradle.daemon=true";
52
62
  }
53
63
 
54
64
  const stopSpinner = (this.projectConfig.verbose) ? () => {} : Logger.spinner(incremental ? "Incremental compilation" : "Full project build");
55
65
 
56
66
  const proc = Bun.spawn(command, {
57
67
  stdout: "pipe",
58
- stderr: "pipe"
68
+ stderr: "pipe",
69
+ env: env as any
59
70
  });
60
71
 
61
72
  if (this.projectConfig.verbose) {
@@ -0,0 +1,117 @@
1
+ import { Logger } from "../utils/ui";
2
+ import type { AppConfig } from "../types/config";
3
+ import os from "os";
4
+
5
+ export class DashboardService {
6
+ private isTui: boolean;
7
+ private logLines: string[] = [];
8
+ private maxLogLines: number = 0;
9
+ private status: string = "IDLE";
10
+ private statusColor: string = Logger.C.dim;
11
+
12
+ constructor(private config: AppConfig) {
13
+ this.isTui = config.project.tui;
14
+ if (this.isTui) {
15
+ this.maxLogLines = process.stdout.rows - 10;
16
+ this.setupTui();
17
+ }
18
+ }
19
+
20
+ private setupTui() {
21
+ process.stdout.write("\x1B[?1049h"); // Switch to alternate buffer
22
+ process.stdout.write("\x1B[?25l"); // Hide cursor
23
+
24
+ process.stdin.setRawMode(true);
25
+ process.stdin.resume();
26
+ process.stdin.setEncoding("utf8");
27
+
28
+ process.stdin.on("data", (key: string) => {
29
+ // Ctrl+C ou Q para sair
30
+ if (key === "\u0003" || key.toLowerCase() === "q") {
31
+ this.exit();
32
+ }
33
+ if (key.toLowerCase() === "l") {
34
+ this.logLines = [];
35
+ this.render();
36
+ }
37
+ });
38
+
39
+ process.on("SIGINT", () => this.exit());
40
+ process.on("exit", () => this.exit());
41
+
42
+ // Atualiza o dashboard periodicamente para memória/status
43
+ setInterval(() => this.render(), 1000);
44
+ }
45
+
46
+ public onKey(key: string, callback: () => void) {
47
+ process.stdin.on("data", (input: string) => {
48
+ if (input.toLowerCase() === key.toLowerCase()) {
49
+ callback();
50
+ }
51
+ });
52
+ }
53
+
54
+ public setStatus(status: string, color: string = Logger.C.cyan) {
55
+ this.status = status;
56
+ this.statusColor = color;
57
+ this.render();
58
+ }
59
+
60
+ public log(message: string) {
61
+ if (!message) return;
62
+
63
+ if (this.isTui) {
64
+ const lines = message.split("\n");
65
+ this.logLines.push(...lines);
66
+ if (this.logLines.length > 1000) { // Limite de scrollbuffer
67
+ this.logLines = this.logLines.slice(-1000);
68
+ }
69
+ this.render();
70
+ } else {
71
+ console.log(message);
72
+ }
73
+ }
74
+
75
+ private render() {
76
+ if (!this.isTui) return;
77
+
78
+ this.maxLogLines = process.stdout.rows - 8;
79
+
80
+ let output = "\x1B[H"; // Move to 0,0
81
+
82
+ // Header
83
+ const name = (process.cwd().split(/[/\\]/).pop() || "PROJECT").toUpperCase();
84
+ const git = Logger.getGitContext();
85
+ const mem = Math.round((os.totalmem() - os.freemem()) / 1024 / 1024 / 1024 * 10) / 10;
86
+ const totalMem = Math.round(os.totalmem() / 1024 / 1024 / 1024);
87
+
88
+ 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}\n`;
89
+ 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}${git.branch || "unknown"}${Logger.C.reset}\n`;
90
+ output += `${Logger.C.dim}──────────────────────────────────────────────────────────────────────────${Logger.C.reset}\n`;
91
+
92
+ // Logs
93
+ const visibleLogs = this.logLines.slice(-this.maxLogLines);
94
+ for (let i = 0; i < this.maxLogLines; i++) {
95
+ const line = visibleLogs[i] || "";
96
+ // Limpa a linha antes de escrever (ANSI escape EL)
97
+ output += line.substring(0, process.stdout.columns) + "\x1B[K\n";
98
+ }
99
+
100
+ // Footer
101
+ output += `\x1B[${process.stdout.rows - 1};1H`; // Move to last rows
102
+ output += `${Logger.C.dim}──────────────────────────────────────────────────────────────────────────${Logger.C.reset}\n`;
103
+ 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}`;
104
+
105
+ process.stdout.write(output);
106
+ }
107
+
108
+ private exit() {
109
+ if (this.isTui) {
110
+ process.stdout.write("\x1B[?1049l"); // Restore buffer
111
+ process.stdout.write("\x1B[?25h"); // Show cursor
112
+ process.stdin.setRawMode(false);
113
+ process.stdin.pause();
114
+ }
115
+ process.exit(0);
116
+ }
117
+ }
@@ -0,0 +1,140 @@
1
+ import { Logger } from "../utils/ui";
2
+ import type { ProjectConfig } from "../types/config";
3
+
4
+ export class LogAnalyzer {
5
+ private projectPrefixes: string[] = [];
6
+
7
+ constructor(private config: ProjectConfig) {
8
+ // Tentamos inferir prefixos comuns do projeto.
9
+ // Em Java/Maven/Gradle geralmente começam com com.empresa, br.com.etc
10
+ // Podemos extrair isso do ProjectService se necessário, ou usar o appName como base.
11
+ if (config.appName) {
12
+ this.projectPrefixes.push(config.appName.split('-')[0].toLowerCase());
13
+ }
14
+ // Prefixos padrões que queremos destacar se encontrarmos
15
+ this.projectPrefixes.push("com.xavva");
16
+ }
17
+
18
+ setProjectPrefixes(prefixes: string[]) {
19
+ this.projectPrefixes = [...new Set([...this.projectPrefixes, ...prefixes])];
20
+ }
21
+
22
+ public summarize(line: string): string {
23
+ if (Logger.isSystemNoise(line)) return "";
24
+
25
+ // Reuso da lógica existente no Logger.summarize, mas aprimorada
26
+ const startupMatch = line.match(/Server startup in (\[?)(.*?)(\]?)\s*ms/);
27
+ if (startupMatch) {
28
+ const time = (parseInt(startupMatch[2]) / 1000).toFixed(1);
29
+ return `${Logger.C.green}✔ ${Logger.C.bold}Server started in ${time}s`;
30
+ }
31
+
32
+ const deployMatch = line.match(/Deployment of web application archive \[(.*?)\] has finished in \[(.*?)\] ms/);
33
+ if (deployMatch) {
34
+ return `${Logger.C.green}✔ Artifacts deployed`;
35
+ }
36
+
37
+ // Smart Folding para Stack Traces
38
+ if (line.trim().startsWith("at ")) {
39
+ return this.formatStackTraceLine(line);
40
+ }
41
+
42
+ if (line.includes("Caused by:")) {
43
+ return `${Logger.C.bgRed}${Logger.C.white}${Logger.C.bold} ROOT CAUSE ${Logger.C.reset} ${Logger.C.red}${line.trim()}${Logger.C.reset}`;
44
+ }
45
+
46
+ // Hotswap
47
+ const hotswapPattern = /HOTSWAP AGENT:.*? (INFO|WARN|ERROR|RELOAD) (.*?) - (.*)/;
48
+ const hotswapMatch = line.match(hotswapPattern);
49
+ if (hotswapMatch) {
50
+ return this.formatHotswapLine(hotswapMatch);
51
+ }
52
+
53
+ // Tomcat standard logs
54
+ const tomcatPattern = /^(\d{2}-\w{3}-\d{4} \d{2}:\d{2}:\d{2}\.\d{3})\s+(INFO|WARNING|SEVERE|ERROR)\s+\[(.*?)\]\s+(.*)$/;
55
+ const tMatch = line.match(tomcatPattern);
56
+ if (tMatch) {
57
+ return this.formatTomcatLine(tMatch);
58
+ }
59
+
60
+ // Generic [LEVEL] logs
61
+ const logPattern = /^\[(INFO|WARNING|WARN|SEVERE|ERROR)\]\s+(.*)$/;
62
+ const match = line.match(logPattern);
63
+ if (match) {
64
+ return this.formatGenericLog(match);
65
+ }
66
+
67
+ // Se a linha contém Exception mas não é o 'at ', destaca
68
+ if (line.includes("Exception:")) {
69
+ return `${Logger.C.red}${Logger.C.bold}${line.trim()}${Logger.C.reset}`;
70
+ }
71
+
72
+ return "";
73
+ }
74
+
75
+ private formatStackTraceLine(line: string): string {
76
+ const trimmed = line.trim();
77
+ const isProject = this.projectPrefixes.some(p => trimmed.includes(p));
78
+
79
+ if (isProject) {
80
+ return ` ${Logger.C.bold}${Logger.C.yellow}${trimmed}${Logger.C.reset}`;
81
+ } else {
82
+ return ` ${Logger.C.dim}${trimmed}${Logger.C.reset}`;
83
+ }
84
+ }
85
+
86
+ private formatHotswapLine(match: RegExpMatchArray): string {
87
+ const level = match[1];
88
+ let msg = match[3];
89
+
90
+ if (msg.includes("plugin initialized")) return "";
91
+
92
+ if (msg.includes("redefinition") || msg.includes("reloaded") || level === 'RELOAD') {
93
+ if (msg.includes("Reloading classes [")) {
94
+ const classes = msg.match(/\[(.*?)\]/)?.[1] || "";
95
+ const classCount = classes.split(",").length;
96
+ if (classCount > 3) msg = `Reloading ${classCount} classes...`;
97
+ }
98
+ return `${Logger.C.magenta}👀 ${Logger.C.bold}Hotswap:${Logger.C.reset} ${msg.replace(/Class '.*?'/, (m) => Logger.C.bold + m + Logger.C.reset)}`;
99
+ }
100
+
101
+ let color = Logger.C.cyan;
102
+ let symbol = "●";
103
+ if (level === "WARN") { color = Logger.C.yellow; symbol = "▲"; }
104
+ else if (level === "ERROR") { color = Logger.C.red; symbol = "✖"; }
105
+
106
+ return `${color}${symbol} ${Logger.C.bold}Hotswap:${Logger.C.reset} ${msg}`;
107
+ }
108
+
109
+ private formatTomcatLine(match: RegExpMatchArray): string {
110
+ const label = match[2];
111
+ let msg = match[4].trim();
112
+ if (Logger.isSystemNoise(msg)) return "";
113
+
114
+ let color = Logger.C.dim;
115
+ let symbol = "ℹ";
116
+ if (label === "WARNING") { color = Logger.C.yellow; symbol = "▲"; }
117
+ else if (label === "SEVERE" || label === "ERROR") { color = Logger.C.red; symbol = "✖"; }
118
+
119
+ msg = msg.replace(/^(org\.apache|com\.sun|java\..*?|org\.glassfish)\.[a-zA-Z0-9.]+\s/, "").trim();
120
+ if (!msg) return "";
121
+
122
+ return `${color}${symbol} ${msg}`;
123
+ }
124
+
125
+ private formatGenericLog(match: RegExpMatchArray): string {
126
+ const label = match[1];
127
+ let msg = match[2].trim();
128
+ if (msg.includes("Total time:") || msg.includes("Finished at:") || msg.includes("Final Memory:") || msg.includes("-----------------------")) return "";
129
+
130
+ let color = Logger.C.dim;
131
+ let symbol = "ℹ";
132
+ if (label === "WARNING" || label === "WARN") { color = Logger.C.yellow; symbol = "▲"; }
133
+ else if (label === "SEVERE" || label === "ERROR") { color = Logger.C.red; symbol = "✖"; }
134
+
135
+ msg = msg.replace(/^(org\.apache|com\.sun|java\..*?)\.[a-zA-Z0-9.]+\s/, "").trim();
136
+ if (!msg || msg === "]" || msg.includes("Compilation failure")) return "";
137
+
138
+ return `${color}${symbol} ${msg}`;
139
+ }
140
+ }
@@ -42,28 +42,45 @@ export class TomcatService {
42
42
  }
43
43
  }
44
44
 
45
- clearWebapps() {
46
- const fs = require("fs");
45
+ async clearWebapps() {
46
+ const fs = require("fs").promises;
47
+ const { existsSync } = require("fs");
47
48
  const path = require("path");
48
49
  const webappsPath = path.join(this.activeConfig.path, "webapps");
49
50
  const workPath = path.join(this.activeConfig.path, "work");
50
51
  const tempPath = path.join(this.activeConfig.path, "temp");
51
52
 
52
53
  try {
53
- [workPath, tempPath].forEach(p => {
54
- if (fs.existsSync(p)) {
55
- fs.rmSync(p, { recursive: true, force: true });
56
- fs.mkdirSync(p);
54
+ const cleanDir = async (p: string) => {
55
+ if (existsSync(p)) {
56
+ await fs.rm(p, { recursive: true, force: true });
57
+
58
+ // Resiliência para Windows: garante que o diretório foi liberado antes do mkdir
59
+ let retries = 10;
60
+ while (retries > 0 && existsSync(p)) {
61
+ await new Promise(r => setTimeout(r, 50));
62
+ retries--;
63
+ }
64
+
65
+ await fs.mkdir(p, { recursive: true });
66
+ }
67
+ };
68
+
69
+ const tasks: Promise<any>[] = [
70
+ cleanDir(workPath),
71
+ cleanDir(tempPath)
72
+ ];
73
+
74
+ if (existsSync(webappsPath)) {
75
+ const files = await fs.readdir(webappsPath);
76
+ for (const file of files) {
77
+ if (file === "ROOT" || file === "manager" || file === "host-manager") continue;
78
+ const fullPath = path.join(webappsPath, file);
79
+ tasks.push(fs.rm(fullPath, { recursive: true, force: true }));
57
80
  }
58
- });
59
-
60
- const files = fs.readdirSync(webappsPath);
61
- for (const file of files) {
62
- const fullPath = path.join(webappsPath, file);
63
- if (file === "ROOT" || file === "manager" || file === "host-manager") continue;
64
-
65
- fs.rmSync(fullPath, { recursive: true, force: true });
66
81
  }
82
+
83
+ await Promise.all(tasks);
67
84
  } catch (e) {
68
85
  Logger.warn("Não foi possível limpar totalmente a pasta webapps ou cache.");
69
86
  }
@@ -18,6 +18,7 @@ export interface ProjectConfig {
18
18
  debugPort: number;
19
19
  cleanLogs: boolean;
20
20
  grep?: string;
21
+ tui: boolean;
21
22
  }
22
23
 
23
24
  export interface AppConfig {
@@ -44,6 +45,7 @@ export interface CLIArguments {
44
45
  dp?: string;
45
46
  fix?: boolean;
46
47
  incremental?: boolean;
48
+ tui?: boolean;
47
49
  }
48
50
 
49
51
  export interface CommandContext {
@@ -27,12 +27,25 @@ export class ConfigManager {
27
27
  verbose: { type: "boolean", short: "V" },
28
28
  dp: { type: "string" },
29
29
  fix: { type: "boolean" },
30
+ tui: { type: "boolean" },
30
31
  },
31
32
  strict: false,
32
33
  allowPositionals: true,
33
34
  });
34
35
 
35
36
  const cliValues = values as CLIArguments;
37
+
38
+ // Load xavva.json
39
+ const xavvaJsonPath = path.join(process.cwd(), "xavva.json");
40
+ let xavvaJson: Partial<CLIArguments> = {};
41
+ if (fs.existsSync(xavvaJsonPath)) {
42
+ try {
43
+ xavvaJson = JSON.parse(fs.readFileSync(xavvaJsonPath, "utf8"));
44
+ } catch (e) {
45
+ console.error("Error reading xavva.json:", (e as Error).message);
46
+ }
47
+ }
48
+
36
49
  const isDev = positionals.includes("dev");
37
50
  const isRun = positionals.includes("run") || positionals.includes("debug");
38
51
 
@@ -49,24 +62,25 @@ export class ConfigManager {
49
62
 
50
63
  const config: AppConfig = {
51
64
  tomcat: {
52
- path: String(cliValues.path || envTomcatPath),
53
- port: parseInt(String(cliValues.port || "8080")),
65
+ path: String(cliValues.path || xavvaJson.path || envTomcatPath),
66
+ port: parseInt(String(cliValues.port || xavvaJson.port || "8080")),
54
67
  webapps: "webapps",
55
- grep: cliValues.grep ? String(cliValues.grep) : "",
68
+ grep: cliValues.grep || xavvaJson.grep ? String(cliValues.grep || xavvaJson.grep) : "",
56
69
  },
57
70
  project: {
58
- appName: cliValues.name ? String(cliValues.name) : "",
59
- buildTool: (cliValues.tool as "maven" | "gradle") || detectedTool,
60
- profile: String(cliValues.profile || ""),
61
- skipBuild: !!cliValues["no-build"],
62
- skipScan: cliValues.scan !== undefined ? !cliValues.scan : true,
63
- clean: !!cliValues.clean,
64
- cleanLogs: cliValues.verbose ? false : true,
65
- quiet: cliValues.verbose ? false : true,
66
- verbose: !!cliValues.verbose,
67
- debug: !!(cliValues.debug || isDev || isRun),
68
- debugPort: parseInt(String(cliValues.dp || "5005")),
69
- grep: runClass || (cliValues.grep ? String(cliValues.grep) : ""),
71
+ appName: cliValues.name || xavvaJson.name ? String(cliValues.name || xavvaJson.name) : "",
72
+ buildTool: (cliValues.tool as any) || (xavvaJson.tool as any) || detectedTool,
73
+ profile: String(cliValues.profile || xavvaJson.profile || ""),
74
+ skipBuild: !!(cliValues["no-build"] ?? xavvaJson["no-build"]),
75
+ skipScan: cliValues.scan !== undefined ? !cliValues.scan : (xavvaJson.scan !== undefined ? !xavvaJson.scan : true),
76
+ clean: !!(cliValues.clean ?? xavvaJson.clean),
77
+ cleanLogs: (cliValues.verbose ?? xavvaJson.verbose) ? false : true,
78
+ quiet: (cliValues.verbose ?? xavvaJson.verbose) ? false : true,
79
+ verbose: !!(cliValues.verbose ?? xavvaJson.verbose),
80
+ debug: !!(cliValues.debug ?? xavvaJson.debug ?? isDev ?? isRun),
81
+ debugPort: parseInt(String(cliValues.dp || xavvaJson.dp || "5005")),
82
+ grep: runClass || (cliValues.grep || xavvaJson.grep ? String(cliValues.grep || xavvaJson.grep) : ""),
83
+ tui: !!(cliValues.tui ?? xavvaJson.tui),
70
84
  }
71
85
  };
72
86