@archznn/xavva 1.6.5

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.
@@ -0,0 +1,49 @@
1
+ import type { Command } from "./Command";
2
+ import type { AppConfig } from "../types/config";
3
+ import pkg from "../../package.json";
4
+
5
+ export class HelpCommand implements Command {
6
+ async execute(_config: AppConfig): Promise<void> {
7
+ console.log(`
8
+ 🛠️ Xavva CLI v${pkg.version}
9
+ -------------------------------
10
+ A Xavva automatiza o ciclo de vida de aplicações Java (Tomcat).
11
+ Se nenhum comando for fornecido, 'deploy' será executado por padrão.
12
+
13
+ Comandos principais:
14
+ dev 🚀 MODO COMPLETO: Deploy + Watch + Debug + Clean.
15
+ deploy (Padrão) Builda o projeto, move para webapps e inicia o Tomcat.
16
+ start Apenas inicia o servidor (útil quando o .war já foi gerado).
17
+ build Executa apenas a compilação (mvn package ou gradle build).
18
+ doctor 🩺 Verifica o ambiente (Java, Tomcat, Maven, etc).
19
+ docs 📖 Swagger-like: Exibe endpoints e URLs de JSPs.
20
+ audit 🛡️ JAR Audit: Busca vulnerabilidades (CVEs) nas dependências.
21
+ run 🚀 Executa uma classe main (Uso: xavva run NomeDaClasse).
22
+ debug 🐞 Debuga uma classe main (Uso: xavva debug NomeDaClasse).
23
+ logs 📋 Monitora o catalina.out do Tomcat em tempo real.
24
+
25
+ Opções:
26
+ -w, --watch 👀 Hot Reload: monitora arquivos e redeploya automaticamente.
27
+ -d, --debug 🐞 Habilita debug Java (JPDA) na porta 5005.
28
+ -c, --clean 🧹 Logs coloridos e simplificados (recomendado).
29
+ -q, --quiet 🤫 Mostra apenas mensagens essenciais nos logs.
30
+ -V, --verbose 📣 Mostra logs completos do Maven/Gradle.
31
+ -s, --no-build Pula o build (usa o que já estiver na pasta target/build).
32
+ -P, --profile Define o profile (ex: -P prod).
33
+ --fix 🔧 Corrige problemas automaticamente no 'doctor'.
34
+
35
+ Exemplos de uso:
36
+ xavva dev A melhor experiência de dev local.
37
+ xavva -w -c Inicia com auto-reload e logs limpos.
38
+ xavva deploy -d Builda e inicia com debugger habilitado.
39
+ xavva start -c Apenas sobe o servidor com logs limpos.
40
+ xavva build -P dev Apenas compila usando o profile 'dev'.
41
+
42
+ Opções de Configuração:
43
+ -p, --path Caminho do Tomcat (padrão via config.ts)
44
+ -t, --tool Ferramenta (maven/gradle)
45
+ -n, --name Nome do .war (ex: -n ROOT)
46
+ --port Porta HTTP (padrão: 8080)
47
+ `);
48
+ }
49
+ }
@@ -0,0 +1,64 @@
1
+ import type { Command } from "./Command";
2
+ import type { AppConfig } from "../types/config";
3
+ import { Logger } from "../utils/ui";
4
+ import path from "path";
5
+ import fs from "fs";
6
+
7
+ export class LogsCommand implements Command {
8
+ async execute(config: AppConfig): Promise<void> {
9
+ const logPath = path.join(config.tomcat.path, "logs", "catalina.out");
10
+
11
+ if (!fs.existsSync(logPath)) {
12
+ Logger.error(`Arquivo de log não encontrado: ${logPath}`);
13
+ return;
14
+ }
15
+
16
+ Logger.section(`Monitoring Logs: ${logPath}`);
17
+ if (config.tomcat.grep) {
18
+ Logger.info("Filter", config.tomcat.grep);
19
+ }
20
+
21
+ const stats = fs.statSync(logPath);
22
+ let currentSize = stats.size;
23
+
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
+ fs.watch(logPath, (event) => {
33
+ if (event === "change") {
34
+ const newStats = fs.statSync(logPath);
35
+ if (newStats.size > currentSize) {
36
+ const stream = fs.createReadStream(logPath, {
37
+ start: currentSize,
38
+ end: newStats.size
39
+ });
40
+
41
+ stream.on("data", (chunk) => {
42
+ const lines = chunk.toString().split("\n");
43
+ lines.forEach(line => {
44
+ if (!line.trim()) return;
45
+
46
+ if (config.tomcat.grep && !line.toLowerCase().includes(config.tomcat.grep.toLowerCase())) {
47
+ return;
48
+ }
49
+
50
+ process.stdout.write(colorize(line) + "\n");
51
+ });
52
+ });
53
+
54
+ currentSize = newStats.size;
55
+ } else if (newStats.size < currentSize) {
56
+ currentSize = newStats.size;
57
+ Logger.warn("Arquivo de log foi resetado/rotacionado.");
58
+ }
59
+ }
60
+ });
61
+
62
+ return new Promise(() => {});
63
+ }
64
+ }
@@ -0,0 +1,283 @@
1
+ import type { Command } from "./Command";
2
+ import type { AppConfig } from "../types/config";
3
+ import { Logger } from "../utils/ui";
4
+ import { spawn } from "child_process";
5
+ import path from "path";
6
+
7
+ export class RunCommand implements Command {
8
+ constructor(private debug: boolean = true) {}
9
+
10
+ async execute(config: AppConfig): Promise<void> {
11
+ let className = config.project.grep;
12
+
13
+ if (!className) {
14
+ className = await this.loadFromHistory();
15
+ if (!className) {
16
+ Logger.error(`Uso: xavva ${this.debug ? "debug" : "run"} NomeDaClasse`);
17
+ return;
18
+ }
19
+ }
20
+
21
+ if (!className.includes(".")) {
22
+ const discoveredClass = await this.discoverClass(className);
23
+ if (!discoveredClass) return;
24
+ className = discoveredClass;
25
+ }
26
+
27
+ this.saveToHistory(className);
28
+
29
+ if (this.debug) {
30
+ Logger.section(`Interactive Debug: ${className}`);
31
+ } else {
32
+ Logger.section(`Running: ${className}`);
33
+ }
34
+
35
+ const { localCp, dependencyCp } = await this.getClasspath(config);
36
+ const pathingJar = await this.createPathingJar(dependencyCp);
37
+
38
+ const finalCp = `${localCp};${pathingJar}`;
39
+
40
+ const args = [
41
+ "-classpath", finalCp,
42
+ ];
43
+
44
+ if (this.debug) {
45
+ args.push("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005");
46
+ }
47
+
48
+ args.push(className);
49
+
50
+ if (this.debug) {
51
+ Logger.warn(`🚀 Aguardando debugger na porta 5005 para ${className}...`);
52
+ Logger.log(`${"\x1b[36m"}Dica:${"\x1b[0m"} No VS Code ou IntelliJ, use 'Attach to Remote JVM' na porta 5005.\n`);
53
+ } else {
54
+ Logger.warn(`🚀 Executando ${className}...`);
55
+ }
56
+
57
+ const bin = "java";
58
+
59
+ return new Promise((resolve) => {
60
+ const child = spawn(bin, args, {
61
+ stdio: "inherit",
62
+ shell: true
63
+ });
64
+
65
+ child.on("exit", () => {
66
+ Logger.log(`Sessão de ${this.debug ? "debug" : "execução"} encerrada.`);
67
+ resolve();
68
+ });
69
+ });
70
+ }
71
+
72
+ private async discoverClass(simpleName: string): Promise<string | null> {
73
+ const fs = require("fs");
74
+ const glob = require("glob");
75
+ const basePaths = [
76
+ path.join(process.cwd(), "src/main/java"),
77
+ path.join(process.cwd(), "src/test/java"),
78
+ path.join(process.cwd(), "src")
79
+ ];
80
+
81
+ const files: string[] = [];
82
+ const seenFiles = new Set<string>();
83
+
84
+ for (const srcPath of basePaths) {
85
+ if (fs.existsSync(srcPath)) {
86
+ const pattern = path.join(srcPath, "**", `${simpleName}.java`).replace(/\\/g, "/");
87
+ const found = glob.sync(pattern);
88
+ found.forEach((f: string) => {
89
+ const abs = path.resolve(f);
90
+ if (!seenFiles.has(abs)) {
91
+ files.push(abs);
92
+ seenFiles.add(abs);
93
+ }
94
+ });
95
+ }
96
+ }
97
+
98
+ if (files.length === 0) {
99
+ for (const srcPath of basePaths) {
100
+ if (fs.existsSync(srcPath)) {
101
+ const pattern = path.join(srcPath, "**", `*${simpleName}*.java`).replace(/\\/g, "/");
102
+ const found = glob.sync(pattern);
103
+ found.forEach((f: string) => {
104
+ const abs = path.resolve(f);
105
+ if (!seenFiles.has(abs)) {
106
+ files.push(abs);
107
+ seenFiles.add(abs);
108
+ }
109
+ });
110
+ }
111
+ }
112
+ if (files.length === 0) {
113
+ Logger.error(`Classe "${simpleName}" não encontrada nos diretórios de código (src/main/java, src/test/java, src).`);
114
+ return null;
115
+ }
116
+ }
117
+
118
+ const classes = files.map((file: string) => {
119
+ const content = fs.readFileSync(file, "utf8");
120
+ const packageMatch = content.match(/^package\s+([^;]+);/m);
121
+ const packageName = packageMatch ? packageMatch[1] : "";
122
+ const fileName = path.basename(file, ".java");
123
+ return packageName ? `${packageName}.${fileName}` : fileName;
124
+ });
125
+
126
+ const uniqueClasses = Array.from(new Set(classes));
127
+
128
+ if (uniqueClasses.length === 1) {
129
+ return uniqueClasses[0];
130
+ }
131
+
132
+ Logger.warn(`Múltiplas classes encontradas para "${simpleName}":`);
133
+ uniqueClasses.forEach((c, i) => {
134
+ Logger.log(` [${i + 1}] ${c}`);
135
+ });
136
+
137
+ const readline = require("readline").createInterface({
138
+ input: process.stdin,
139
+ output: process.stdout
140
+ });
141
+
142
+ return new Promise((resolve) => {
143
+ readline.question(`\n Escolha a classe (1-${uniqueClasses.length}) ou [C]ancelar: `, (answer: string) => {
144
+ readline.close();
145
+ const idx = parseInt(answer) - 1;
146
+ if (!isNaN(idx) && uniqueClasses[idx]) {
147
+ resolve(uniqueClasses[idx]);
148
+ } else {
149
+ Logger.error("Operação cancelada.");
150
+ resolve(null);
151
+ }
152
+ });
153
+ });
154
+ }
155
+
156
+ private async loadFromHistory(): Promise<string | null> {
157
+ const fs = require("fs");
158
+ const xavvaDir = path.join(process.cwd(), ".xavva");
159
+ const historyFile = path.join(xavvaDir, "history.json");
160
+
161
+ if (!fs.existsSync(historyFile)) return null;
162
+
163
+ try {
164
+ const history: string[] = JSON.parse(fs.readFileSync(historyFile, "utf8"));
165
+ if (history.length === 0) return null;
166
+
167
+ Logger.warn(`Classes executadas recentemente:`);
168
+ history.slice(0, 5).forEach((c, i) => {
169
+ Logger.log(` [${i + 1}] ${c}${i === 0 ? " (Enter)" : ""}`);
170
+ });
171
+
172
+ const readline = require("readline").createInterface({
173
+ input: process.stdin,
174
+ output: process.stdout
175
+ });
176
+
177
+ return new Promise((resolve) => {
178
+ readline.question(`\n Escolha a classe (1-${Math.min(history.length, 5)}) ou [C]ancelar: `, (answer: string) => {
179
+ readline.close();
180
+ if (!answer.trim()) {
181
+ resolve(history[0]);
182
+ return;
183
+ }
184
+ const idx = parseInt(answer) - 1;
185
+ if (!isNaN(idx) && history[idx]) {
186
+ resolve(history[idx]);
187
+ } else {
188
+ resolve(null);
189
+ }
190
+ });
191
+ });
192
+ } catch (e) {
193
+ return null;
194
+ }
195
+ }
196
+
197
+ private saveToHistory(className: string) {
198
+ const fs = require("fs");
199
+ const xavvaDir = path.join(process.cwd(), ".xavva");
200
+ const historyFile = path.join(xavvaDir, "history.json");
201
+
202
+ if (!fs.existsSync(xavvaDir)) fs.mkdirSync(xavvaDir);
203
+
204
+ let history: string[] = [];
205
+ if (fs.existsSync(historyFile)) {
206
+ try {
207
+ history = JSON.parse(fs.readFileSync(historyFile, "utf8"));
208
+ } catch (e) {}
209
+ }
210
+
211
+ history = [className, ...history.filter(c => c !== className)].slice(0, 10);
212
+
213
+ fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
214
+ }
215
+
216
+ private async createPathingJar(dependencyCp: string): Promise<string> {
217
+ const fs = require("fs");
218
+ const xavvaDir = path.join(process.cwd(), ".xavva");
219
+ const jarPath = path.join(xavvaDir, "classpath.jar");
220
+
221
+ const paths = dependencyCp.split(";").filter(p => p.trim());
222
+ const relativePaths = paths.map(p => {
223
+ let rel = path.relative(xavvaDir, p).replace(/\\/g, "/");
224
+ if (fs.statSync(p).isDirectory() && !rel.endsWith("/")) rel += "/";
225
+ return rel;
226
+ }).join(" ");
227
+
228
+ let wrappedCp = "";
229
+ const maxLen = 70;
230
+ for (let i = 0; i < relativePaths.length; i += maxLen) {
231
+ const chunk = relativePaths.substring(i, i + maxLen);
232
+ if (i === 0) {
233
+ wrappedCp += chunk;
234
+ } else {
235
+ wrappedCp += "\r\n " + chunk;
236
+ }
237
+ }
238
+
239
+ const manifestContent = `Manifest-Version: 1.0\r\nClass-Path: ${wrappedCp}\r\n\r\n`;
240
+ const manifestPath = path.join(xavvaDir, "MANIFEST.MF");
241
+ fs.writeFileSync(manifestPath, manifestContent);
242
+
243
+ Bun.spawnSync(["jar", "cfm", jarPath, manifestPath]);
244
+ return jarPath;
245
+ }
246
+
247
+ private async getClasspath(config: AppConfig): Promise<{ localCp: string, dependencyCp: string }> {
248
+ const fs = require("fs");
249
+ const xavvaDir = path.join(process.cwd(), ".xavva");
250
+ const cpFile = path.join(xavvaDir, "classpath.txt");
251
+
252
+ if (!fs.existsSync(xavvaDir)) fs.mkdirSync(xavvaDir);
253
+
254
+ if (!fs.existsSync(cpFile)) {
255
+ const stopSpinner = Logger.spinner("Generating project classpath");
256
+ try {
257
+ if (config.project.buildTool === 'maven') {
258
+ Bun.spawnSync(["mvn", "dependency:build-classpath", `-Dmdep.outputFile=${cpFile}`]);
259
+ } else {
260
+ fs.writeFileSync(cpFile, ".");
261
+ }
262
+ } catch (e) {}
263
+ stopSpinner();
264
+ }
265
+
266
+ const dependencyCp = fs.existsSync(cpFile) ? fs.readFileSync(cpFile, "utf8").trim() : "";
267
+
268
+ const localFolders = [
269
+ "target/classes",
270
+ "target/test-classes",
271
+ "build/classes/java/main",
272
+ "build/classes/java/test",
273
+ "."
274
+ ];
275
+
276
+ const localCp = localFolders
277
+ .map(p => path.join(process.cwd(), p))
278
+ .filter(p => fs.existsSync(p))
279
+ .join(";");
280
+
281
+ return { localCp, dependencyCp };
282
+ }
283
+ }
@@ -0,0 +1,26 @@
1
+ import type { Command } from "./Command";
2
+ import type { AppConfig } from "../types/config";
3
+ import { TomcatService } from "../services/TomcatService";
4
+ import { Logger } from "../utils/ui";
5
+
6
+ export class StartCommand implements Command {
7
+ async execute(config: AppConfig): Promise<void> {
8
+ const tomcat = new TomcatService(config.tomcat);
9
+
10
+ Logger.section("Start Only");
11
+ Logger.info("Port", config.tomcat.port);
12
+ if (config.project.debug) Logger.info("Debugger", "Active (5005)");
13
+
14
+ try {
15
+ Logger.step("Checking ports");
16
+ await tomcat.killConflict();
17
+ Logger.step("Starting Tomcat");
18
+ tomcat.start(config.project.cleanLogs, config.project.debug, config.project.skipScan, config.project.quiet);
19
+
20
+ await new Promise(() => {});
21
+ } catch (error: any) {
22
+ Logger.error(error.message);
23
+ process.exit(1);
24
+ }
25
+ }
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env bun
2
+ import { watch } from "fs";
3
+ import { ConfigManager } from "./utils/config";
4
+ import { BuildCommand } from "./commands/BuildCommand";
5
+ import { DeployCommand } from "./commands/DeployCommand";
6
+ import { StartCommand } from "./commands/StartCommand";
7
+ import { HelpCommand } from "./commands/HelpCommand";
8
+ import { DoctorCommand } from "./commands/DoctorCommand";
9
+ import { RunCommand } from "./commands/RunCommand";
10
+ import { LogsCommand } from "./commands/LogsCommand";
11
+ import { DocsCommand } from "./commands/DocsCommand";
12
+ import { AuditCommand } from "./commands/AuditCommand";
13
+ import { TomcatService } from "./services/TomcatService";
14
+ import { EndpointService } from "./services/EndpointService";
15
+ import pkg from "../package.json";
16
+ import { Logger } from "./utils/ui";
17
+ import path from "path";
18
+
19
+ async function main() {
20
+ const { config, positionals, values } = await ConfigManager.load();
21
+
22
+ if (values.version) {
23
+ console.log(`v${pkg.version}`);
24
+ process.exit(0);
25
+ }
26
+
27
+ const commandNames = ["deploy", "build", "start", "dev", "doctor", "run", "debug", "logs", "docs", "audit"];
28
+ const commandName = positionals.find(p => commandNames.includes(p)) || "deploy";
29
+
30
+ if (!values.help) {
31
+ Logger.banner(commandName);
32
+ }
33
+
34
+ if (values.help) {
35
+ new HelpCommand().execute(config);
36
+ process.exit(0);
37
+ }
38
+
39
+ switch (commandName) {
40
+ case "build":
41
+ await new BuildCommand().execute(config);
42
+ break;
43
+ case "start":
44
+ await new StartCommand().execute(config);
45
+ break;
46
+ case "doctor":
47
+ await new DoctorCommand().execute(config, values);
48
+ break;
49
+ case "run":
50
+ await new RunCommand(false).execute(config);
51
+ break;
52
+ case "debug":
53
+ await new RunCommand(true).execute(config);
54
+ break;
55
+ case "logs":
56
+ await new LogsCommand().execute(config);
57
+ break;
58
+ case "docs":
59
+ await new DocsCommand().execute(config);
60
+ break;
61
+ case "audit":
62
+ await new AuditCommand().execute(config);
63
+ break;
64
+ case "dev":
65
+ case "deploy":
66
+ await handleDeploy(config, values);
67
+ break;
68
+ default:
69
+ console.error(`Comando desconhecido: ${commandName}`);
70
+ new HelpCommand().execute(config);
71
+ process.exit(1);
72
+ }
73
+ }
74
+
75
+ async function handleDeploy(config: any, values: any) {
76
+ const tomcat = new TomcatService(config.tomcat);
77
+ const deployCmd = new DeployCommand(tomcat);
78
+
79
+ if (values.watch) {
80
+ let isDeploying = false;
81
+
82
+ const run = async (incremental = false) => {
83
+ if (isDeploying) return;
84
+ isDeploying = true;
85
+ try {
86
+ await deployCmd.execute(config, incremental, true);
87
+ } catch (e) {
88
+ } finally {
89
+ isDeploying = false;
90
+ }
91
+ };
92
+
93
+ await run(false);
94
+
95
+ let debounceTimer: Timer;
96
+ watch(process.cwd(), { recursive: true }, async (event, filename) => {
97
+ if (!filename) return;
98
+
99
+ const isJava = filename.endsWith(".java") || filename === "pom.xml" || filename === "build.gradle";
100
+ const isResource = filename.endsWith(".jsp") || filename.endsWith(".html") ||
101
+ filename.endsWith(".css") || filename.endsWith(".js") ||
102
+ filename.endsWith(".xml") || filename.endsWith(".properties");
103
+
104
+ const isIgnored = filename.includes("target") ||
105
+ filename.includes("build") ||
106
+ filename.includes("node_modules") ||
107
+ filename.split(/[/\\]/).some(part => part.startsWith("."));
108
+
109
+ if (isIgnored) return;
110
+
111
+ if (isResource && !isJava) {
112
+ const isJsp = filename.endsWith(".jsp");
113
+ let jspUrl = "";
114
+ let isPrivate = false;
115
+
116
+ if (isJsp) {
117
+ const parts = filename.split(/[/\\]/);
118
+ const webappIndex = parts.indexOf("webapp");
119
+ if (webappIndex !== -1) {
120
+ const relPath = parts.slice(webappIndex + 1).join("/");
121
+ isPrivate = relPath.startsWith("WEB-INF") || relPath.startsWith("META-INF");
122
+ const contextPath = (config.project.appName || "").replace(".war", "");
123
+ jspUrl = `http://localhost:${config.tomcat.port}${contextPath ? "/" + contextPath : ""}/${relPath}`;
124
+ }
125
+ }
126
+
127
+ if (isJsp && isPrivate) {
128
+ console.log(`\n ${"\x1b[33m"}🔒${"\x1b[0m"} JSP Privado alterado (WEB-INF): ${filename}`);
129
+ console.log(` ${"\x1b[90m"}Nota: Este arquivo não é acessível via URL direta.${"\x1b[0m"}`);
130
+ } else if (isJsp && jspUrl) {
131
+ console.log(`\n ${"\x1b[32m"}📄${"\x1b[0m"} JSP Atualizado: ${"\x1b[4m"}${jspUrl}${"\x1b[0m"}`);
132
+ } else {
133
+ console.log(`\n ${"\x1b[35m"}⚡${"\x1b[0m"} Recurso alterado: ${filename}`);
134
+ }
135
+
136
+ await deployCmd.syncResource(config, filename);
137
+ return;
138
+ }
139
+
140
+ if (!isJava) return;
141
+
142
+ console.log(`\n ${"\x1b[33m"}👀${"\x1b[0m"} Alteração detectada em: ${filename}`);
143
+ clearTimeout(debounceTimer);
144
+
145
+ debounceTimer = setTimeout(() => {
146
+ run(true);
147
+ }, 1000);
148
+ });
149
+
150
+ } else {
151
+ await deployCmd.execute(config, false, false);
152
+ }
153
+ }
154
+
155
+ main().catch(console.error);