@archznn/xavva 2.3.0 → 2.5.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Ultra-fast development toolkit for Java Enterprise (Tomcat) on Windows
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.3.0-blue.svg)](https://github.com/leorsousa05/Xavva)
5
+ [![Version](https://img.shields.io/badge/version-2.4.0-blue.svg)](https://github.com/leorsousa05/Xavva)
6
6
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7
7
 
8
8
  Xavva is a high-performance CLI built with **Bun** that transforms the Java/Tomcat development experience. It brings modern development workflows (like Node.js/Vite) to the Java Enterprise ecosystem with hot-reload, smart logging, and automated deployment.
@@ -102,16 +102,25 @@ Xavva can automatically download and manage a Tomcat installation for you:
102
102
  # First time usage - auto-install Tomcat
103
103
  xavva dev --yes
104
104
 
105
- # Or install manually
106
- xavva tomcat install
105
+ # List available versions to download
106
+ xavva tomcat list
107
+
108
+ # List already installed versions
109
+ xavva tomcat installed
110
+
111
+ # Install a specific version
112
+ xavva tomcat install 9.0.115
113
+
114
+ # Switch to a version for this project
115
+ xavva tomcat use 9.0.115
107
116
 
108
117
  # Check Tomcat status
109
118
  xavva tomcat status
110
119
 
111
- # List available versions
112
- xavva tomcat list
120
+ # Remove a version
121
+ xavva tomcat uninstall 9.0.115
113
122
 
114
- # Use specific version
123
+ # Or use with flags
115
124
  xavva dev --tomcat-version 9.0.115
116
125
  ```
117
126
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archznn/xavva",
3
- "version": "2.3.0",
3
+ "version": "2.5.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",
@@ -1,5 +1,5 @@
1
1
  import type { AppConfig, CLIArguments } from "../types/config";
2
2
 
3
3
  export interface Command {
4
- execute(config: AppConfig, args?: CLIArguments): Promise<void>;
4
+ execute(config: AppConfig, args?: CLIArguments, positionals?: string[]): Promise<void>;
5
5
  }
@@ -19,18 +19,18 @@ export class CommandRegistry {
19
19
  return this.commands.get(name);
20
20
  }
21
21
 
22
- async execute(name: string, config: AppConfig, args: CLIArguments): Promise<void> {
22
+ async execute(name: string, config: AppConfig, args: CLIArguments, positionals?: string[]): Promise<void> {
23
23
  const command = this.commands.get(name);
24
24
  const processManager = ProcessManager.getInstance();
25
25
 
26
26
  if (!command) {
27
27
  Logger.error(`Comando desconhecido: ${name}`);
28
- await new HelpCommand().execute(config);
28
+ await new HelpCommand().execute(config, args, positionals);
29
29
  await processManager.shutdown(2);
30
30
  }
31
31
 
32
32
  try {
33
- await command.execute(config, args);
33
+ await command.execute(config, args, positionals);
34
34
  } catch (error) {
35
35
  const message = error instanceof Error ? error.message : String(error);
36
36
  Logger.error(`Erro ao executar comando '${name}': ${message}`);
@@ -14,6 +14,7 @@ export class DeployCommand implements Command {
14
14
  async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
15
15
  const incremental = args?.watch && args?.incremental;
16
16
  const isWatching = !!args?.watch;
17
+ const changedFiles = args?.changedFiles;
17
18
  const tomcat = this.tomcat;
18
19
  const builder = this.builder;
19
20
 
@@ -46,11 +47,11 @@ export class DeployCommand implements Command {
46
47
  }
47
48
 
48
49
  if (incremental) {
49
- const actualAppFolder = await builder.syncClasses();
50
+ const actualAppFolder = await builder.syncClasses(changedFiles);
50
51
  const actualContextPath = contextPath || actualAppFolder || "";
51
52
  const actualAppUrl = `http://localhost:${config.tomcat.port}/${actualContextPath}`;
52
53
  await BrowserService.reload(actualAppUrl);
53
- Logger.success("redeploy completed");
54
+ Logger.success(`redeploy completed (${changedFiles?.length || 'all'} file(s))`);
54
55
  return;
55
56
  }
56
57
 
@@ -98,7 +99,7 @@ export class DeployCommand implements Command {
98
99
  const finalAppUrl = `http://localhost:${config.tomcat.port}/${finalContextPath}`;
99
100
 
100
101
  tomcat.onReady = async () => {
101
- await this.handleServerReady(config, finalAppUrl, finalContextPath, tomcat, incremental);
102
+ await this.handleServerReady(config, finalAppUrl, finalContextPath, tomcat, !!incremental);
102
103
  };
103
104
 
104
105
  tomcat.start(config, isWatching);
@@ -28,7 +28,7 @@ export class HelpCommand implements Command {
28
28
  ${this.c("green", "audit")} Security audit of JAR files
29
29
  ${this.c("green", "doctor")} Diagnose and fix environment issues
30
30
  ${this.c("green", "profiles")} List available Maven/Gradle profiles
31
- ${this.c("green", "tomcat")} Manage embedded Tomcat (install, list, status)
31
+ ${this.c("green", "tomcat")} Manage embedded Tomcat (install, list, installed, use, status)
32
32
  ${this.c("green", "docs")} Generate endpoint documentation
33
33
 
34
34
  ${this.c("yellow", "GENERAL OPTIONS")}
@@ -90,9 +90,12 @@ export class HelpCommand implements Command {
90
90
  xavva audit --fix
91
91
 
92
92
  ${this.c("dim", "# Manage embedded Tomcat")}
93
- xavva tomcat install
93
+ xavva tomcat list # List available versions
94
+ xavva tomcat installed # List installed versions
95
+ xavva tomcat install 9.0.115 # Install specific version
96
+ xavva tomcat use 9.0.115 # Switch to version for this project
94
97
  xavva tomcat status
95
- xavva tomcat list
98
+ xavva tomcat uninstall 9.0.115
96
99
 
97
100
  ${this.c("yellow", "CONFIGURATION")}
98
101
  Settings are loaded from ${this.c("cyan", "xavva.json")} in the project root:
@@ -3,32 +3,47 @@ import type { AppConfig, CLIArguments } from "../types/config";
3
3
  import { EmbeddedTomcatService } from "../services/EmbeddedTomcatService";
4
4
  import { Logger } from "../utils/ui";
5
5
  import path from "path";
6
+ import fs from "fs";
6
7
 
7
8
  export class TomcatCommand implements Command {
8
- async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
9
- const action = args?.["tomcat-action"] || "status";
9
+ async execute(config: AppConfig, args?: CLIArguments, positionals?: string[]): Promise<void> {
10
+ // A ação vem como positional após "tomcat" (ex: xavva tomcat list)
11
+ const tomcatIndex = positionals?.indexOf("tomcat") ?? -1;
12
+ const action = positionals && tomcatIndex >= 0 && positionals[tomcatIndex + 1]
13
+ ? positionals[tomcatIndex + 1]
14
+ : "status";
15
+
16
+ // Argumentos extras após a ação (ex: xavva tomcat install 9.0.115)
17
+ const extraArgs = positionals && tomcatIndex >= 0 ? positionals.slice(tomcatIndex + 2) : [];
10
18
 
11
19
  switch (action) {
12
20
  case "install":
13
- await this.handleInstall(config, args);
21
+ await this.handleInstall(config, args, extraArgs);
14
22
  break;
15
23
  case "list":
16
24
  this.handleList();
17
25
  break;
26
+ case "installed":
27
+ this.handleInstalled();
28
+ break;
29
+ case "use":
30
+ await this.handleUse(config, args, extraArgs);
31
+ break;
18
32
  case "uninstall":
19
- await this.handleUninstall(config, args);
33
+ await this.handleUninstall(config, args, extraArgs);
20
34
  break;
21
35
  case "status":
22
36
  await this.handleStatus(config);
23
37
  break;
24
38
  default:
25
39
  Logger.error(`Ação desconhecida: ${action}`);
26
- Logger.info("Ações disponíveis", "install, list, uninstall, status");
40
+ Logger.info("Ações disponíveis", "install, list, installed, use, uninstall, status");
27
41
  }
28
42
  }
29
43
 
30
- private async handleInstall(config: AppConfig, args?: CLIArguments): Promise<void> {
31
- const version = args?.["tomcat-version"] || config.tomcat.version || "10.1.52";
44
+ private async handleInstall(config: AppConfig, args?: CLIArguments, extraArgs: string[] = []): Promise<void> {
45
+ // Versão pode vir de: flag --tomcat-version, argumento posicional, config, ou padrão
46
+ const version = args?.["tomcat-version"] || extraArgs[0] || config.tomcat.version || "10.1.52";
32
47
 
33
48
  // Detectar webapp path
34
49
  const webappPath = config.project.buildTool === "maven"
@@ -57,7 +72,7 @@ export class TomcatCommand implements Command {
57
72
  }
58
73
 
59
74
  private handleList(): void {
60
- Logger.section("Versões Disponíveis");
75
+ Logger.section("Versões Disponíveis para Download");
61
76
  const versions = EmbeddedTomcatService.getAvailableVersions();
62
77
 
63
78
  for (const version of versions) {
@@ -66,10 +81,90 @@ export class TomcatCommand implements Command {
66
81
 
67
82
  Logger.newline();
68
83
  Logger.info("Versão padrão", "10.1.52");
84
+ Logger.newline();
85
+ Logger.info("Dica", "Use 'xavva tomcat installed' para ver versões já instaladas");
86
+ }
87
+
88
+ private handleInstalled(): void {
89
+ const installed = EmbeddedTomcatService.listInstalledVersions();
90
+
91
+ Logger.section("Versões Instaladas");
92
+
93
+ if (installed.length === 0) {
94
+ Logger.warn("Nenhuma versão instalada");
95
+ Logger.info("Dica", "Use 'xavva tomcat install <version>' para instalar");
96
+ return;
97
+ }
98
+
99
+ for (const version of installed) {
100
+ Logger.log(` ${Logger.C.success}✓${Logger.C.reset} ${version}`);
101
+ }
102
+
103
+ Logger.newline();
104
+ Logger.info("Para usar uma versão", "xavva tomcat use <version>");
105
+ }
106
+
107
+ private async handleUse(config: AppConfig, args?: CLIArguments, extraArgs: string[] = []): Promise<void> {
108
+ const version = extraArgs[0] || args?.["tomcat-version"];
109
+
110
+ if (!version) {
111
+ Logger.error("Versão não especificada");
112
+ Logger.info("Uso", "xavva tomcat use <version>");
113
+ Logger.info("Exemplo", "xavva tomcat use 9.0.115");
114
+ Logger.newline();
115
+ Logger.info("Versões instaladas", "");
116
+ this.handleInstalled();
117
+ return;
118
+ }
119
+
120
+ // Verifica se a versão está instalada
121
+ const service = new EmbeddedTomcatService({
122
+ version,
123
+ port: config.tomcat.port,
124
+ webappPath: "."
125
+ });
126
+
127
+ if (!service.checkInstallation()) {
128
+ Logger.warn(`Tomcat ${version} não está instalado`);
129
+ Logger.newline();
130
+ Logger.info("Opções", "");
131
+ Logger.log(` ${Logger.C.primary}1.${Logger.C.reset} Instalar agora: xavva tomcat install ${version}`);
132
+ Logger.log(` ${Logger.C.primary}2.${Logger.C.reset} Ver instaladas: xavva tomcat installed`);
133
+ return;
134
+ }
135
+
136
+ // Salva a versão no xavva.json do projeto
137
+ await this.saveTomcatVersion(version);
138
+
139
+ Logger.success(`Tomcat ${version} configurado para este projeto!`);
140
+ Logger.newline();
141
+ Logger.info("Próximos comandos", "");
142
+ Logger.log(` ${Logger.C.primary}•${Logger.C.reset} xavva dev # Iniciar desenvolvimento`);
143
+ Logger.log(` ${Logger.C.primary}•${Logger.C.reset} xavva deploy # Fazer deploy`);
144
+ }
145
+
146
+ private async saveTomcatVersion(version: string): Promise<void> {
147
+ const xavvaJsonPath = path.join(process.cwd(), "xavva.json");
148
+ let config: any = {};
149
+
150
+ if (fs.existsSync(xavvaJsonPath)) {
151
+ try {
152
+ config = JSON.parse(fs.readFileSync(xavvaJsonPath, "utf-8"));
153
+ } catch (e) {
154
+ // Arquivo existe mas é inválido, começa do zero
155
+ }
156
+ }
157
+
158
+ if (!config.tomcat) config.tomcat = {};
159
+ config.tomcat.version = version;
160
+ config.tomcat.embedded = true;
161
+
162
+ fs.writeFileSync(xavvaJsonPath, JSON.stringify(config, null, 2));
69
163
  }
70
164
 
71
- private async handleUninstall(config: AppConfig, args?: CLIArguments): Promise<void> {
72
- const version = args?.["tomcat-version"] || config.tomcat.version || "10.1.52";
165
+ private async handleUninstall(config: AppConfig, args?: CLIArguments, extraArgs: string[] = []): Promise<void> {
166
+ // Versão pode vir de: flag --tomcat-version, argumento posicional, config, ou padrão
167
+ const version = args?.["tomcat-version"] || extraArgs[0] || config.tomcat.version || "10.1.52";
73
168
 
74
169
  const service = new EmbeddedTomcatService({
75
170
  version,
package/src/index.ts CHANGED
@@ -105,7 +105,7 @@ async function main() {
105
105
  if (commandName === "debug") values.debug = true;
106
106
  if (commandName === "run") values.debug = false;
107
107
 
108
- await registry.execute(commandName, config, values);
108
+ await registry.execute(commandName, config, values, positionals);
109
109
  }
110
110
  }
111
111
 
@@ -129,19 +129,121 @@ export class BuildService {
129
129
  await this.fastSync(srcDir, destDir);
130
130
  }
131
131
 
132
- async syncClasses(customSrc?: string): Promise<string | null> {
132
+ async syncClasses(changedFiles?: string[]): Promise<string | null> {
133
133
  const appFolder = this.projectService.getInferredAppName();
134
134
  const webappPath = path.join(this.tomcatConfig.path, "webapps", appFolder);
135
135
  const targetLib = path.join(webappPath, "WEB-INF", "classes");
136
- const sourceDir = customSrc || this.projectService.getClassesDir();
136
+ const sourceDir = this.projectService.getClassesDir();
137
137
 
138
138
  if (!existsSync(sourceDir)) return null;
139
139
  if (!existsSync(targetLib)) mkdirSync(targetLib, { recursive: true });
140
140
 
141
- await this.fastSync(sourceDir, targetLib);
141
+ // Se temos uma lista específica de arquivos modificados, sincroniza apenas eles
142
+ if (changedFiles && changedFiles.length > 0) {
143
+ await this.syncSpecificFiles(changedFiles, sourceDir, targetLib);
144
+ } else {
145
+ // Caso contrário, sincroniza tudo (comportamento padrão)
146
+ await this.fastSync(sourceDir, targetLib);
147
+ }
148
+
142
149
  return appFolder;
143
150
  }
144
151
 
152
+ /**
153
+ * Sincroniza apenas arquivos específicos baseado nos arquivos .java modificados.
154
+ * Converte .java para .class e sincroniza apenas os arquivos realmente modificados.
155
+ */
156
+ private async syncSpecificFiles(changedFiles: string[], sourceDir: string, targetLib: string): Promise<void> {
157
+ const tasks: Promise<void>[] = [];
158
+ const syncedCount = { value: 0 };
159
+
160
+ for (const javaFile of changedFiles) {
161
+ // Converte caminho do .java para caminho do .class
162
+ // Ex: src/main/java/com/example/Foo.java -> target/classes/com/example/Foo.class
163
+ const relativePath = this.javaToClassPath(javaFile);
164
+ if (!relativePath) continue;
165
+
166
+ const sourcePath = path.join(sourceDir, relativePath);
167
+ const targetPath = path.join(targetLib, relativePath);
168
+
169
+ if (!existsSync(sourcePath)) {
170
+ // Se o .class não existe, talvez seja um arquivo excluído ou inner class
171
+ // Neste caso, faz sync completo como fallback
172
+ continue;
173
+ }
174
+
175
+ tasks.push((async () => {
176
+ const targetDir = path.dirname(targetPath);
177
+ if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
178
+
179
+ const srcStat = statSync(sourcePath);
180
+ const destStat = existsSync(targetPath) ? statSync(targetPath) : null;
181
+
182
+ if (!destStat || srcStat.mtimeMs > destStat.mtimeMs) {
183
+ await fs.copyFile(sourcePath, targetPath);
184
+ syncedCount.value++;
185
+ }
186
+ })());
187
+ }
188
+
189
+ await Promise.all(tasks);
190
+
191
+ // Se não conseguimos sincronizar nenhum arquivo específico, faz sync completo
192
+ if (syncedCount.value === 0) {
193
+ await this.fastSync(sourceDir, targetLib);
194
+ } else if (!this.projectConfig.quiet) {
195
+ Logger.info("sync", `${syncedCount.value} classe(s) sincronizada(s)`);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Converte caminho de arquivo .java para caminho relativo de .class
201
+ */
202
+ private javaToClassPath(javaFile: string): string | null {
203
+ // Remove prefixos comuns de diretórios source
204
+ const parts = javaFile.split(/[/\\]/);
205
+
206
+ // Encontra o índice após "java" ou "src/main/java" ou "src"
207
+ let startIndex = -1;
208
+
209
+ for (let i = 0; i < parts.length; i++) {
210
+ if (parts[i] === "java" && i > 0 && (parts[i-1] === "main" || parts[i-1] === "test")) {
211
+ startIndex = i + 1;
212
+ break;
213
+ }
214
+ }
215
+
216
+ // Se não encontrou padrão maven, tenta achar "src"
217
+ if (startIndex === -1) {
218
+ const srcIndex = parts.indexOf("src");
219
+ if (srcIndex !== -1 && srcIndex < parts.length - 1) {
220
+ // Pula "src" e possível "main/java"
221
+ if (parts[srcIndex + 1] === "main" && parts[srcIndex + 2] === "java") {
222
+ startIndex = srcIndex + 3;
223
+ } else {
224
+ startIndex = srcIndex + 1;
225
+ }
226
+ }
227
+ }
228
+
229
+ // Se ainda não encontrou, assume que o caminho já é relativo ao package
230
+ if (startIndex === -1) {
231
+ startIndex = 0;
232
+ }
233
+
234
+ // Pega o caminho relativo
235
+ const relativeParts = parts.slice(startIndex);
236
+ if (relativeParts.length === 0) return null;
237
+
238
+ // Substitui extensão .java por .class
239
+ const fileName = relativeParts[relativeParts.length - 1];
240
+ if (!fileName || !fileName.endsWith(".java")) return null;
241
+
242
+ relativeParts[relativeParts.length - 1] = fileName.replace(".java", ".class");
243
+
244
+ return path.join(...relativeParts);
245
+ }
246
+
145
247
  private async fastSync(src: string, dest: string) {
146
248
  const entries = readdirSync(src, { withFileTypes: true });
147
249