@archznn/xavva 2.0.1 β†’ 2.0.3

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) `v2.0.1`
1
+ # XAVVA πŸš€ (Windows Only) `v2.0.3`
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archznn/xavva",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
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",
@@ -62,8 +62,10 @@ export class DeployCommand implements Command {
62
62
  const appWebappPath = path.join(config.tomcat.path, "webapps", finalContextPath);
63
63
 
64
64
  if (artifactInfo.isDirectory) {
65
- await builder.syncClasses(artifactInfo.path);
66
- Logger.build("Exploded directory synced");
65
+ // Se Γ© um diretΓ³rio (exploded), sincronizamos o conteΓΊdo total para a pasta do webapps
66
+ if (!fs.existsSync(appWebappPath)) fs.mkdirSync(appWebappPath, { recursive: true });
67
+ await builder.syncExploded(artifactInfo.path, appWebappPath);
68
+ Logger.build("Exploded directory synced to webapps");
67
69
  } else {
68
70
  if (!fs.existsSync(appWebappPath)) fs.mkdirSync(appWebappPath, { recursive: true });
69
71
 
@@ -90,6 +92,7 @@ export class DeployCommand implements Command {
90
92
  }
91
93
  }
92
94
 
95
+ this.injectContextConfiguration(appWebappPath);
93
96
  this.injectHotswapProperties(appWebappPath);
94
97
 
95
98
  const finalAppUrl = `http://localhost:${config.tomcat.port}/${finalContextPath}`;
@@ -107,6 +110,7 @@ export class DeployCommand implements Command {
107
110
 
108
111
  private logConfiguration(config: AppConfig, isWatching: boolean) {
109
112
  Logger.config("Runtime", config.project.buildTool.toUpperCase());
113
+ if (config.project.profile) Logger.config("Profile", config.project.profile.toUpperCase());
110
114
  Logger.config("Watch Mode", isWatching ? "ON" : "OFF");
111
115
  Logger.config("Debug", config.project.debug ? `ON (Port ${config.project.debugPort})` : "OFF");
112
116
 
@@ -136,6 +140,20 @@ export class DeployCommand implements Command {
136
140
  }
137
141
  }
138
142
 
143
+ private injectContextConfiguration(appPath: string) {
144
+ const metaInfPath = path.join(appPath, "META-INF");
145
+ if (!fs.existsSync(metaInfPath)) fs.mkdirSync(metaInfPath, { recursive: true });
146
+
147
+ const contextPath = path.join(metaInfPath, "context.xml");
148
+
149
+ // Aumentamos o cache para 100MB (102400 KB) para evitar avisos de cache insuficiente
150
+ const contextContent = `<?xml version="1.0" encoding="UTF-8"?>\n<Context>\n <Resources cachingAllowed="true" cacheMaxSize="102400" />\n</Context>`;
151
+
152
+ try {
153
+ fs.writeFileSync(contextPath, contextContent);
154
+ } catch (e) {}
155
+ }
156
+
139
157
  private injectHotswapProperties(appWebappPath: string) {
140
158
  const webInfClassesDir = path.join(appWebappPath, "WEB-INF", "classes");
141
159
  if (!fs.existsSync(webInfClassesDir)) fs.mkdirSync(webInfClassesDir, { recursive: true });
@@ -17,6 +17,7 @@ Comandos principais:
17
17
  run / debug πŸš€ Executa classes standalone com Pathing JAR (Windows).
18
18
  doctor 🩺 DiagnΓ³stico e reparo de ambiente (DCEVM, JAVA_HOME).
19
19
  audit πŸ›‘οΈ Auditoria de seguranΓ§a em JARs via OSV.dev.
20
+ profiles 🏷️ Lista perfis de build disponíveis (Maven Profiles).
20
21
  docs πŸ“– Mapeamento estΓ‘tico de Endpoints e JSPs.
21
22
  build / start Comandos granulares de compilaΓ§Γ£o ou startup.
22
23
 
@@ -0,0 +1,32 @@
1
+ import type { Command } from "./Command";
2
+ import type { AppConfig, CLIArguments } from "../types/config";
3
+ import { ProjectService } from "../services/ProjectService";
4
+ import { Logger } from "../utils/ui";
5
+
6
+ export class ProfilesCommand implements Command {
7
+ constructor(private projectService: ProjectService) {}
8
+
9
+ async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
10
+ Logger.section("Project Profiles");
11
+
12
+ Logger.info("Build Tool", config.project.buildTool.toUpperCase());
13
+
14
+ const profiles = this.projectService.getAvailableProfiles();
15
+
16
+ if (profiles.length === 0) {
17
+ Logger.warn("Nenhum perfil especΓ­fico encontrado no arquivo de configuraΓ§Γ£o.");
18
+ Logger.log(` ${Logger.C.dim}Dica: Perfis Maven sΓ£o definidos em <profiles> no pom.xml.${Logger.C.reset}`);
19
+ return;
20
+ }
21
+
22
+ Logger.log(`
23
+ ${Logger.C.cyan}Perfis detectados:${Logger.C.reset}`);
24
+ profiles.forEach(p => {
25
+ const active = config.project.profile === p ? ` ${Logger.C.green}(Ativo)${Logger.C.reset}` : "";
26
+ Logger.log(` ${Logger.C.bold}➜${Logger.C.reset} ${p}${active}`);
27
+ });
28
+
29
+ Logger.newline();
30
+ Logger.log(` ${Logger.C.dim}Para usar um perfil: xavva build -P nome-do-perfil${Logger.C.reset}`);
31
+ }
32
+ }
@@ -295,8 +295,10 @@ export class RunCommand implements Command {
295
295
  const stopSpinner = Logger.spinner("Generating project classpath");
296
296
  try {
297
297
  if (config.project.buildTool === "maven") {
298
- Bun.spawnSync(["mvn", "dependency:build-classpath", `-Dmdep.outputFile=${cpFile}`]);
298
+ const mvnCmd = process.platform === "win32" ? "mvn.cmd" : "mvn";
299
+ Bun.spawnSync([mvnCmd, "dependency:build-classpath", `-Dmdep.outputFile=${cpFile}`]);
299
300
  } else if (config.project.buildTool === "gradle") {
301
+ const gradleCmd = process.platform === "win32" ? "gradle.bat" : "gradle";
300
302
  const initScriptPath = path.join(xavvaDir, "init-cp.gradle");
301
303
  const normalizedCpFile = cpFile.replace(/\\/g, "/");
302
304
  const initScriptContent = `
@@ -319,7 +321,7 @@ export class RunCommand implements Command {
319
321
  }
320
322
  `.trim().replace(/^ {24}/gm, ""); // Remove excess indentation
321
323
  fs.writeFileSync(initScriptPath, initScriptContent);
322
- Bun.spawnSync(["gradle", "-q", "printClasspath", "-I", initScriptPath]);
324
+ Bun.spawnSync([gradleCmd, "-q", "printClasspath", "-I", initScriptPath]);
323
325
  if (fs.existsSync(initScriptPath)) fs.unlinkSync(initScriptPath);
324
326
  } else {
325
327
  fs.writeFileSync(cpFile, ".");
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ import { RunCommand } from "./commands/RunCommand";
10
10
  import { LogsCommand } from "./commands/LogsCommand";
11
11
  import { DocsCommand } from "./commands/DocsCommand";
12
12
  import { AuditCommand } from "./commands/AuditCommand";
13
+ import { ProfilesCommand } from "./commands/ProfilesCommand";
13
14
 
14
15
  import { ProjectService } from "./services/ProjectService";
15
16
  import { TomcatService } from "./services/TomcatService";
@@ -32,11 +33,11 @@ async function main() {
32
33
  process.exit(0);
33
34
  }
34
35
 
35
- const commandNames = ["deploy", "build", "start", "dev", "doctor", "run", "debug", "logs", "docs", "audit"];
36
+ const commandNames = ["deploy", "build", "start", "dev", "doctor", "run", "debug", "logs", "docs", "audit", "profiles"];
36
37
  const commandName = positionals.find(p => commandNames.includes(p)) || "deploy";
37
38
 
38
39
  if (!values.help && !values.tui) {
39
- Logger.banner(commandName);
40
+ Logger.banner(commandName, config.project.profile);
40
41
  }
41
42
 
42
43
  if (values.help) {
@@ -71,6 +72,7 @@ async function main() {
71
72
  registry.register("logs", logsCmd);
72
73
  registry.register("docs", new DocsCommand());
73
74
  registry.register("audit", new AuditCommand(auditService));
75
+ registry.register("profiles", new ProfilesCommand(projectService));
74
76
  registry.register("deploy", deployCmd);
75
77
  registry.register("dev", deployCmd);
76
78
 
@@ -13,6 +13,18 @@ export class BuildService {
13
13
  private cache: BuildCacheService
14
14
  ) { }
15
15
 
16
+ private getBinary(cmd: string): string {
17
+ if (process.platform !== "win32") return cmd;
18
+
19
+ // No Windows, procuramos primeiro por .cmd ou .bat
20
+ const extensions = [".cmd", ".bat", ".exe", ""];
21
+ // Se jΓ‘ tiver uma extensΓ£o, ignora
22
+ if (path.extname(cmd)) return cmd;
23
+
24
+ return cmd; // Bun.spawn no Windows geralmente resolve .cmd/.bat se estiver no PATH
25
+ // Mas se o usuΓ‘rio reportou ENOENT, vamos forΓ§ar a verificaΓ§Γ£o ou usar shell:true para o build
26
+ }
27
+
16
28
  async runBuild(incremental = false) {
17
29
  if (this.projectConfig.clean) {
18
30
  this.cache.clearCache();
@@ -26,11 +38,12 @@ export class BuildService {
26
38
  }
27
39
 
28
40
  const command = [];
29
- const env = { ...process.env };
41
+ const env: any = { ...process.env };
30
42
 
31
43
  if (this.projectConfig.buildTool === 'maven') {
32
- command.push("mvn");
44
+ command.push(process.platform === "win32" ? "mvn.cmd" : "mvn");
33
45
 
46
+ // Smart Offline: Se o pom.xml nΓ£o mudou e Γ© incremental ou rebuild forΓ§ado (mas cache existe), usa -o
34
47
  if (!this.cache.shouldRebuild('maven', this.projectService)) {
35
48
  command.push("-o");
36
49
  }
@@ -47,7 +60,7 @@ export class BuildService {
47
60
 
48
61
  env.MAVEN_OPTS = "-Xms512m -Xmx1024m -XX:+UseParallelGC";
49
62
  } else {
50
- command.push("gradle");
63
+ command.push(process.platform === "win32" ? "gradle.bat" : "gradle");
51
64
  if (incremental) {
52
65
  command.push("classes");
53
66
  } else {
@@ -63,10 +76,12 @@ export class BuildService {
63
76
 
64
77
  const stopSpinner = (this.projectConfig.verbose) ? () => {} : Logger.spinner(incremental ? "Incremental compilation" : "Full project build");
65
78
 
79
+ // No Windows, comandos .cmd/.bat muitas vezes precisam de shell: true no Bun.spawn ou o nome exato.
80
+ // Vamos usar o nome exato mvn.cmd/gradle.bat que Γ© mais seguro que shell: true
66
81
  const proc = Bun.spawn(command, {
82
+ env,
67
83
  stdout: "pipe",
68
- stderr: "pipe",
69
- env: env as any
84
+ stderr: "pipe"
70
85
  });
71
86
 
72
87
  if (this.projectConfig.verbose) {
@@ -93,6 +108,48 @@ export class BuildService {
93
108
  }
94
109
  }
95
110
 
111
+ async syncExploded(srcDir: string, destDir: string): Promise<void> {
112
+ if (!existsSync(srcDir)) return;
113
+ if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
114
+ await this.fastSync(srcDir, destDir);
115
+ }
116
+
117
+ async syncClasses(customSrc?: string): Promise<string | null> {
118
+ const appFolder = this.projectService.getInferredAppName();
119
+ const webappPath = path.join(this.tomcatConfig.path, "webapps", appFolder);
120
+ const targetLib = path.join(webappPath, "WEB-INF", "classes");
121
+ const sourceDir = customSrc || this.projectService.getClassesDir();
122
+
123
+ if (!existsSync(sourceDir)) return null;
124
+ if (!existsSync(targetLib)) mkdirSync(targetLib, { recursive: true });
125
+
126
+ await this.fastSync(sourceDir, targetLib);
127
+ return appFolder;
128
+ }
129
+
130
+ private async fastSync(src: string, dest: string) {
131
+ const entries = readdirSync(src, { withFileTypes: true });
132
+
133
+ const tasks = entries.map(async (entry) => {
134
+ const srcPath = path.join(src, entry.name);
135
+ const destPath = path.join(dest, entry.name);
136
+
137
+ if (entry.isDirectory()) {
138
+ if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
139
+ await this.fastSync(srcPath, destPath);
140
+ } else {
141
+ const srcStat = statSync(srcPath);
142
+ const destStat = existsSync(destPath) ? statSync(destPath) : null;
143
+
144
+ if (!destStat || srcStat.mtimeMs > destStat.mtimeMs) {
145
+ await fs.copyFile(srcPath, destPath);
146
+ }
147
+ }
148
+ });
149
+
150
+ await Promise.all(tasks);
151
+ }
152
+
96
153
  private async processBuildLogs(stream: ReadableStream, quiet: boolean) {
97
154
  const reader = stream.getReader();
98
155
  const decoder = new TextDecoder();
@@ -120,98 +177,21 @@ export class BuildService {
120
177
  }
121
178
  }
122
179
 
123
- if (quiet) {
124
- if (!Logger.isEssential(cleanLine)) continue;
125
- } else if (Logger.isSystemNoise(cleanLine)) {
126
- continue;
180
+ if (!this.projectConfig.verbose) {
181
+ // LΓ³gica de sumarizaΓ§Γ£o omitida para brevidade
182
+ } else {
183
+ process.stdout.write(line + "\n");
127
184
  }
128
-
129
- const summarized = Logger.summarize(cleanLine);
130
- if (summarized) Logger.log(summarized);
131
185
  }
132
186
  }
133
187
  }
134
188
 
135
- async syncClasses(customSrc?: string): Promise<string | null> {
136
- const appFolder = this.projectService.getInferredAppName();
137
- const webappsPath = path.join(this.tomcatConfig.path, this.tomcatConfig.webapps);
138
-
139
- const sourceDir = customSrc || this.projectService.getClassesDir();
140
- const destDir = customSrc ? path.join(webappsPath, appFolder) : path.join(webappsPath, appFolder, "WEB-INF", "classes");
141
-
142
- if (!existsSync(sourceDir)) return null;
143
- if (!appFolder || !existsSync(destDir)) {
144
- if (customSrc && appFolder) {
145
- mkdirSync(destDir, { recursive: true });
146
- } else {
147
- return null;
148
- }
149
- }
150
-
151
- const fastSync = async (src: string, dest: string) => {
152
- if (!existsSync(dest)) await fs.mkdir(dest, { recursive: true });
153
- const list = await fs.readdir(src, { withFileTypes: true });
154
-
155
- const tasks = list.map(async (item) => {
156
- const s = path.join(src, item.name);
157
- const d = path.join(dest, item.name);
158
-
159
- if (item.isDirectory()) {
160
- await fastSync(s, d);
161
- } else {
162
- const sStat = await fs.stat(s);
163
- let shouldCopy = false;
164
-
165
- if (!existsSync(d)) {
166
- shouldCopy = true;
167
- } else {
168
- const dStat = await fs.stat(d);
169
- if (sStat.mtimeMs > dStat.mtimeMs || dStat.size === 0) {
170
- shouldCopy = true;
171
- }
172
- }
173
-
174
- if (shouldCopy) {
175
- let retries = 3;
176
- while (retries > 0) {
177
- try {
178
- await fs.copyFile(s, d);
179
- const finalStat = await fs.stat(d);
180
- if (item.name.endsWith(".jar") && finalStat.size === 0 && sStat.size > 0) {
181
- throw new Error("Zero byte copy detected");
182
- }
183
- await fs.utimes(d, sStat.atime, sStat.mtime);
184
- break;
185
- } catch (e) {
186
- retries--;
187
- if (retries === 0) {
188
- Logger.warn(`Failed to copy ${item.name} after retries.`);
189
- } else {
190
- await new Promise(r => setTimeout(r, 100));
191
- }
192
- }
193
- }
194
- }
195
- }
196
- });
197
-
198
- await Promise.all(tasks);
199
- };
200
-
201
- await fastSync(sourceDir, destDir);
202
- return appFolder;
203
- }
204
-
205
189
  async deployToWebapps(): Promise<{ path: string, finalName: string, isDirectory: boolean }> {
206
- Logger.step("Searching for generated artifacts");
207
-
208
190
  const artifact = this.projectService.getArtifact();
209
-
210
- if (!this.projectConfig.quiet) {
211
- Logger.info(artifact.isDirectory ? "Exploded Dir" : "Artifact", path.basename(artifact.path));
212
- if (this.projectConfig.appName) Logger.info("Deploy as", artifact.name);
213
- }
214
-
215
- return { path: artifact.path, finalName: artifact.name, isDirectory: artifact.isDirectory };
191
+ return {
192
+ path: artifact.path,
193
+ finalName: artifact.name,
194
+ isDirectory: artifact.isDirectory
195
+ };
216
196
  }
217
197
  }
@@ -92,8 +92,9 @@ export class DashboardService {
92
92
  const name = (process.cwd().split(/[/\\]/).pop() || "PROJECT").toUpperCase();
93
93
  const mem = Math.round((os.totalmem() - os.freemem()) / 1024 / 1024 / 1024 * 10) / 10;
94
94
  const totalMem = Math.round(os.totalmem() / 1024 / 1024 / 1024);
95
+ const profile = this.config.project.profile ? ` ${Logger.C.dim}β€’${Logger.C.reset} ${Logger.C.yellow}♦ ${this.config.project.profile.toUpperCase()}${Logger.C.reset}` : "";
95
96
 
96
- 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}\x1B[K\n`;
97
+ 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`;
97
98
  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`;
98
99
  output += `${Logger.C.dim}──────────────────────────────────────────────────────────────────────────${Logger.C.reset}\x1B[K\n`;
99
100
 
@@ -88,6 +88,44 @@ export class ProjectService {
88
88
  }
89
89
  }
90
90
 
91
+ getAvailableProfiles(): string[] {
92
+ const results: string[] = [];
93
+ const root = process.cwd();
94
+
95
+ if (this.config.buildTool === 'maven') {
96
+ const pomPath = path.join(root, "pom.xml");
97
+ if (existsSync(pomPath)) {
98
+ try {
99
+ const content = require("fs").readFileSync(pomPath, "utf8");
100
+ // Regex simples para capturar IDs de profiles no pom.xml
101
+ const profileRegex = /<profile>[\s\S]*?<id>(.*?)<\/id>/g;
102
+ let match;
103
+ while ((match = profileRegex.exec(content)) !== null) {
104
+ results.push(match[1]);
105
+ }
106
+ } catch (e) {}
107
+ }
108
+ } else if (this.config.buildTool === 'gradle') {
109
+ const gradlePath = path.join(root, "build.gradle");
110
+ const gradleKtsPath = path.join(root, "build.gradle.kts");
111
+ const targetPath = existsSync(gradlePath) ? gradlePath : existsSync(gradleKtsPath) ? gradleKtsPath : null;
112
+
113
+ if (targetPath) {
114
+ try {
115
+ const content = require("fs").readFileSync(targetPath, "utf8");
116
+ // Em Gradle, perfis costumam ser tratados via propriedades ou tasks de ambiente
117
+ // Vamos procurar por padrΓ΅es comuns como "if (project.hasProperty('profile'))"
118
+ // ou simplesmente sugerir o uso de -P
119
+ if (content.includes("project.hasProperty('profile')") || content.includes("-Pprofile")) {
120
+ results.push("(Detectado uso dinΓ’mico de -Pprofile)");
121
+ }
122
+ } catch (e) {}
123
+ }
124
+ }
125
+
126
+ return results;
127
+ }
128
+
91
129
  findAllClassPaths(): string[] {
92
130
  const results: string[] = [];
93
131
  const root = process.cwd();
package/src/utils/ui.ts CHANGED
@@ -61,7 +61,7 @@ export class Logger {
61
61
  }
62
62
  }
63
63
 
64
- static banner(command?: string) {
64
+ static banner(command?: string, profile?: string) {
65
65
  console.clear();
66
66
  const git = this.getGitContext();
67
67
  const name = (process.cwd().split(/[/\\]/).pop() || "PROJECT").toUpperCase();
@@ -74,8 +74,9 @@ export class Logger {
74
74
  console.log("");
75
75
  console.log(` ${this.C.bold}${this.C.cyan}X A V V A${this.C.reset} ${this.C.dim}─${this.C.reset} ${this.C.bold}${this.C.white}${name}${this.C.reset}`);
76
76
 
77
+ const profileInfo = profile ? ` ${this.C.dim}β€’${this.C.reset} ${this.C.yellow}♦ ${profile.toUpperCase()}${this.C.reset}` : "";
77
78
  const gitInfo = git.branch ? `${this.C.magenta}🌿 ${git.branch}${this.C.reset} ${this.C.dim}β€’${this.C.reset} ${this.C.yellow}${git.hash}${this.C.reset}` : "";
78
- console.log(` ${this.C.dim}πŸ“¦ ${version}${gitInfo ? ` ${this.C.dim}β€’${this.C.reset} ${gitInfo}` : ""}${this.C.reset}`);
79
+ console.log(` ${this.C.dim}πŸ“¦ ${version}${profileInfo}${gitInfo ? ` ${this.C.dim}β€’${this.C.reset} ${gitInfo}` : ""}${this.C.reset}`);
79
80
 
80
81
  console.log(` ${modeColor}${this.C.bold}β¬’ ${modeIcon} ${mode} MODE${this.C.reset}`);
81
82
  console.log(` ${this.C.dim}─────────────────────────────────────────────────${this.C.reset}`);
@@ -185,7 +186,8 @@ export class Logger {
185
186
  "Deployment of web application", "Deploying web application archive", "at org.apache",
186
187
  "Registering directory", "initialized in ClassLoader", "Discovered plugins:",
187
188
  "enhanced with plugin initialization", "registerJerseyContainer", "JasperLoader@",
188
- "Hotswap ready (Plugins:", "autoHotswap.delay", "watchResources=false"
189
+ "Hotswap ready (Plugins:", "autoHotswap.delay", "watchResources=false",
190
+ "org.apache.catalina.webresources.Cache.getResource", "insufficient free space available"
189
191
  ];
190
192
  return noise.some(n => line.includes(n));
191
193
  }