@archznn/xavva 1.6.5 → 1.8.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.
@@ -1,19 +1,18 @@
1
1
  import type { Command } from "./Command";
2
- import type { AppConfig } from "../types/config";
2
+ import type { AppConfig, CLIArguments } from "../types/config";
3
3
  import { Logger } from "../utils/ui";
4
4
  import { spawn } from "child_process";
5
5
  import path from "path";
6
6
 
7
7
  export class RunCommand implements Command {
8
- constructor(private debug: boolean = true) {}
9
-
10
- async execute(config: AppConfig): Promise<void> {
8
+ async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
9
+ const isDebug = args?.debug !== false; // Default to true if not specified, matching previous behavior
11
10
  let className = config.project.grep;
12
11
 
13
12
  if (!className) {
14
13
  className = await this.loadFromHistory();
15
14
  if (!className) {
16
- Logger.error(`Uso: xavva ${this.debug ? "debug" : "run"} NomeDaClasse`);
15
+ Logger.error(`Uso: xavva ${isDebug ? "debug" : "run"} NomeDaClasse`);
17
16
  return;
18
17
  }
19
18
  }
@@ -26,7 +25,7 @@ export class RunCommand implements Command {
26
25
 
27
26
  this.saveToHistory(className);
28
27
 
29
- if (this.debug) {
28
+ if (isDebug) {
30
29
  Logger.section(`Interactive Debug: ${className}`);
31
30
  } else {
32
31
  Logger.section(`Running: ${className}`);
@@ -37,33 +36,34 @@ export class RunCommand implements Command {
37
36
 
38
37
  const finalCp = `${localCp};${pathingJar}`;
39
38
 
40
- const args = [
39
+ const javaArgs = [
41
40
  "-classpath", finalCp,
42
41
  ];
43
42
 
44
- if (this.debug) {
45
- args.push("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005");
43
+ if (isDebug) {
44
+ javaArgs.push("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005");
46
45
  }
47
46
 
48
- args.push(className);
47
+ javaArgs.push(className);
49
48
 
50
- if (this.debug) {
49
+ if (isDebug) {
51
50
  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`);
51
+ Logger.log(`${Logger.C.cyan}Dica:${Logger.C.reset} No VS Code ou IntelliJ, use 'Attach to Remote JVM' na porta 5005.`);
52
+ Logger.newline();
53
53
  } else {
54
54
  Logger.warn(`🚀 Executando ${className}...`);
55
55
  }
56
56
 
57
- const bin = "java";
57
+ const bin = process.env.JAVA_HOME ? path.join(process.env.JAVA_HOME, "bin", "java") : "java";
58
58
 
59
59
  return new Promise((resolve) => {
60
- const child = spawn(bin, args, {
60
+ const child = spawn(bin, javaArgs, {
61
61
  stdio: "inherit",
62
- shell: true
62
+ shell: false
63
63
  });
64
64
 
65
65
  child.on("exit", () => {
66
- Logger.log(`Sessão de ${this.debug ? "debug" : "execução"} encerrada.`);
66
+ Logger.log(`Sessão de ${isDebug ? "debug" : "execução"} encerrada.`);
67
67
  resolve();
68
68
  });
69
69
  });
@@ -140,7 +140,7 @@ export class RunCommand implements Command {
140
140
  });
141
141
 
142
142
  return new Promise((resolve) => {
143
- readline.question(`\n Escolha a classe (1-${uniqueClasses.length}) ou [C]ancelar: `, (answer: string) => {
143
+ readline.question(` Escolha a classe (1-${uniqueClasses.length}) ou [C]ancelar: `, (answer: string) => {
144
144
  readline.close();
145
145
  const idx = parseInt(answer) - 1;
146
146
  if (!isNaN(idx) && uniqueClasses[idx]) {
@@ -175,7 +175,7 @@ export class RunCommand implements Command {
175
175
  });
176
176
 
177
177
  return new Promise((resolve) => {
178
- readline.question(`\n Escolha a classe (1-${Math.min(history.length, 5)}) ou [C]ancelar: `, (answer: string) => {
178
+ readline.question(` Escolha a classe (1-${Math.min(history.length, 5)}) ou [C]ancelar: `, (answer: string) => {
179
179
  readline.close();
180
180
  if (!answer.trim()) {
181
181
  resolve(history[0]);
@@ -221,22 +221,62 @@ export class RunCommand implements Command {
221
221
  const paths = dependencyCp.split(";").filter(p => p.trim());
222
222
  const relativePaths = paths.map(p => {
223
223
  let rel = path.relative(xavvaDir, p).replace(/\\/g, "/");
224
- if (fs.statSync(p).isDirectory() && !rel.endsWith("/")) rel += "/";
225
- return rel;
224
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory() && !rel.endsWith("/")) rel += "/";
225
+ // Robust URL encoding for Class-Path as per Java Spec
226
+ return encodeURI(rel)
227
+ .replace(/#/g, '%23')
228
+ .replace(/\?/g, '%3F')
229
+ .replace(/%5B/g, '[')
230
+ .replace(/%5D/g, ']');
226
231
  }).join(" ");
227
232
 
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;
233
+ const header = "Class-Path: ";
234
+ let manifestContent = "Manifest-Version: 1.0\r\n";
235
+
236
+ let currentLine = header;
237
+ const parts = relativePaths.split(" ");
238
+
239
+ for (let i = 0; i < parts.length; i++) {
240
+ const part = parts[i] + (i < parts.length - 1 ? " " : "");
241
+
242
+ // Se adicionar o próximo 'part' exceder 70 bytes (margem de segurança antes do CRLF)
243
+ if (Buffer.from(currentLine + part).length > 70) {
244
+ // Se a parte em si for muito longa, precisamos quebrá-la
245
+ if (Buffer.from(" " + part).length > 70) {
246
+ let remainingPart = part;
247
+ while (remainingPart.length > 0) {
248
+ const spaceLeft = 70 - Buffer.from(currentLine).length;
249
+
250
+ // Encontra quantos caracteres de 'remainingPart' cabem no espaço restante
251
+ let fitCount = 0;
252
+ let fitBytes = 0;
253
+ for (let j = 0; j < remainingPart.length; j++) {
254
+ const charBytes = Buffer.from(remainingPart[j]).length;
255
+ if (fitBytes + charBytes > spaceLeft) break;
256
+ fitBytes += charBytes;
257
+ fitCount++;
258
+ }
259
+
260
+ if (fitCount > 0) {
261
+ currentLine += remainingPart.substring(0, fitCount);
262
+ remainingPart = remainingPart.substring(fitCount);
263
+ }
264
+
265
+ if (remainingPart.length > 0) {
266
+ manifestContent += currentLine + "\r\n";
267
+ currentLine = " ";
268
+ }
269
+ }
270
+ } else {
271
+ manifestContent += currentLine + "\r\n";
272
+ currentLine = " " + part;
273
+ }
234
274
  } else {
235
- wrappedCp += "\r\n " + chunk;
275
+ currentLine += part;
236
276
  }
237
277
  }
278
+ manifestContent += currentLine + "\r\n\r\n";
238
279
 
239
- const manifestContent = `Manifest-Version: 1.0\r\nClass-Path: ${wrappedCp}\r\n\r\n`;
240
280
  const manifestPath = path.join(xavvaDir, "MANIFEST.MF");
241
281
  fs.writeFileSync(manifestPath, manifestContent);
242
282
 
@@ -254,22 +294,59 @@ export class RunCommand implements Command {
254
294
  if (!fs.existsSync(cpFile)) {
255
295
  const stopSpinner = Logger.spinner("Generating project classpath");
256
296
  try {
257
- if (config.project.buildTool === 'maven') {
297
+ if (config.project.buildTool === "maven") {
258
298
  Bun.spawnSync(["mvn", "dependency:build-classpath", `-Dmdep.outputFile=${cpFile}`]);
299
+ } else if (config.project.buildTool === "gradle") {
300
+ const initScriptPath = path.join(xavvaDir, "init-cp.gradle");
301
+ const normalizedCpFile = cpFile.replace(/\\/g, "/");
302
+ const initScriptContent = `
303
+ allprojects {
304
+ afterEvaluate { project ->
305
+ if (project.plugins.hasPlugin('java')) {
306
+ tasks.register('printClasspath') {
307
+ doLast {
308
+ def cp = project.sourceSets.main.runtimeClasspath.asPath
309
+ def file = new File("${normalizedCpFile}")
310
+ if (!file.exists()) {
311
+ file.text = cp
312
+ } else {
313
+ file.text = file.text + File.pathSeparator + cp
314
+ }
315
+ }
316
+ }
317
+ }
318
+ }
319
+ }
320
+ `.trim().replace(/^ {24}/gm, ""); // Remove excess indentation
321
+ fs.writeFileSync(initScriptPath, initScriptContent);
322
+ Bun.spawnSync(["gradle", "-q", "printClasspath", "-I", initScriptPath]);
323
+ if (fs.existsSync(initScriptPath)) fs.unlinkSync(initScriptPath);
259
324
  } else {
260
325
  fs.writeFileSync(cpFile, ".");
261
326
  }
262
- } catch (e) {}
327
+ } catch (e) {
328
+ Logger.error(`Falha ao gerar classpath: ${e}`);
329
+ }
263
330
  stopSpinner();
264
331
  }
265
332
 
266
- const dependencyCp = fs.existsSync(cpFile) ? fs.readFileSync(cpFile, "utf8").trim() : "";
333
+ let dependencyCp = fs.existsSync(cpFile) ? fs.readFileSync(cpFile, "utf8").trim() : "";
267
334
 
335
+ // Normalize platform specific separators to semicolon for consistency
336
+ if (path.delimiter !== ";") {
337
+ dependencyCp = dependencyCp.split(path.delimiter).join(";");
338
+ }
339
+
268
340
  const localFolders = [
269
341
  "target/classes",
270
342
  "target/test-classes",
271
343
  "build/classes/java/main",
272
344
  "build/classes/java/test",
345
+ "build/classes/kotlin/main",
346
+ "build/resources/main",
347
+ "build/resources/test",
348
+ "bin/main",
349
+ "bin/test",
273
350
  "."
274
351
  ];
275
352
 
@@ -4,8 +4,10 @@ import { TomcatService } from "../services/TomcatService";
4
4
  import { Logger } from "../utils/ui";
5
5
 
6
6
  export class StartCommand implements Command {
7
+ constructor(private tomcat: TomcatService) {}
8
+
7
9
  async execute(config: AppConfig): Promise<void> {
8
- const tomcat = new TomcatService(config.tomcat);
10
+ const tomcat = this.tomcat;
9
11
 
10
12
  Logger.section("Start Only");
11
13
  Logger.info("Port", config.tomcat.port);
@@ -15,7 +17,7 @@ export class StartCommand implements Command {
15
17
  Logger.step("Checking ports");
16
18
  await tomcat.killConflict();
17
19
  Logger.step("Starting Tomcat");
18
- tomcat.start(config.project.cleanLogs, config.project.debug, config.project.skipScan, config.project.quiet);
20
+ tomcat.start(config, false);
19
21
 
20
22
  await new Promise(() => {});
21
23
  } catch (error: any) {
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
- import { watch } from "fs";
3
2
  import { ConfigManager } from "./utils/config";
3
+ import { CommandRegistry } from "./commands/CommandRegistry";
4
4
  import { BuildCommand } from "./commands/BuildCommand";
5
5
  import { DeployCommand } from "./commands/DeployCommand";
6
6
  import { StartCommand } from "./commands/StartCommand";
@@ -10,17 +10,23 @@ 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
+
14
+ import { ProjectService } from "./services/ProjectService";
13
15
  import { TomcatService } from "./services/TomcatService";
14
- import { EndpointService } from "./services/EndpointService";
16
+ import { BuildService } from "./services/BuildService";
17
+ import { AuditService } from "./services/AuditService";
18
+ import { WatcherService } from "./services/WatcherService";
19
+ import { BuildCacheService } from "./services/BuildCacheService";
20
+
15
21
  import pkg from "../package.json";
16
22
  import { Logger } from "./utils/ui";
17
- import path from "path";
23
+ import type { AppConfig, CLIArguments } from "./types/config";
18
24
 
19
25
  async function main() {
20
26
  const { config, positionals, values } = await ConfigManager.load();
21
27
 
22
28
  if (values.version) {
23
- console.log(`v${pkg.version}`);
29
+ Logger.log(`v${pkg.version}`);
24
30
  process.exit(0);
25
31
  }
26
32
 
@@ -32,123 +38,45 @@ async function main() {
32
38
  }
33
39
 
34
40
  if (values.help) {
35
- new HelpCommand().execute(config);
41
+ new HelpCommand().execute(config, values);
36
42
  process.exit(0);
37
43
  }
38
44
 
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
- }
45
+ // 1. Instanciar Serviços (Injeção de Dependência)
46
+ const projectService = new ProjectService(config.project);
47
+ const buildCacheService = new BuildCacheService();
48
+ const buildService = new BuildService(config.project, config.tomcat, projectService, buildCacheService);
49
+ const tomcatService = new TomcatService(config.tomcat);
50
+ tomcatService.setProjectService(projectService);
51
+ const auditService = new AuditService(config.tomcat);
74
52
 
75
- async function handleDeploy(config: any, values: any) {
76
- const tomcat = new TomcatService(config.tomcat);
77
- const deployCmd = new DeployCommand(tomcat);
53
+ // 2. Registrar Comandos
54
+ const registry = new CommandRegistry();
78
55
 
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
-
56
+ const deployCmd = new DeployCommand(tomcatService, buildService);
57
+
58
+ registry.register("build", new BuildCommand(buildService));
59
+ registry.register("start", new StartCommand(tomcatService));
60
+ registry.register("doctor", new DoctorCommand());
61
+ registry.register("run", new RunCommand());
62
+ registry.register("debug", new RunCommand());
63
+ registry.register("logs", new LogsCommand());
64
+ registry.register("docs", new DocsCommand());
65
+ registry.register("audit", new AuditCommand(auditService));
66
+ registry.register("deploy", deployCmd);
67
+ registry.register("dev", deployCmd);
68
+
69
+ // Caso especial: Watch Mode para Deploy/Dev
70
+ if ((commandName === "deploy" || commandName === "dev") && values.watch) {
71
+ const watcher = new WatcherService(config, deployCmd);
72
+ await watcher.start();
150
73
  } else {
151
- await deployCmd.execute(config, false, false);
74
+ // 3. Executar do Registro
75
+ // Ajusta flags baseadas no nome do comando para comandos compartilhados
76
+ if (commandName === "debug") values.debug = true;
77
+ if (commandName === "run") values.debug = false;
78
+
79
+ await registry.execute(commandName, config, values);
152
80
  }
153
81
  }
154
82
 
@@ -34,7 +34,6 @@ export class AuditService {
34
34
 
35
35
  const stopSpinner = Logger.spinner(`Auditando ${jars.length} dependências`);
36
36
 
37
- // Process in chunks to avoid overwhelming the API
38
37
  const chunkSize = 10;
39
38
  for (let i = 0; i < jars.length; i += chunkSize) {
40
39
  const chunk = jars.slice(i, i + chunkSize);
@@ -52,7 +51,6 @@ export class AuditService {
52
51
  const info = await this.extractJarInfo(jarPath);
53
52
 
54
53
  if (!info.artifactId || !info.version) {
55
- // Fallback to filename parsing if pom.properties is missing
56
54
  const match = jarName.match(/(.+)-([\d\.]+.*)\.jar/);
57
55
  if (match) {
58
56
  info.artifactId = info.artifactId || match[1];
@@ -70,12 +68,10 @@ export class AuditService {
70
68
  }
71
69
 
72
70
  private async extractJarInfo(jarPath: string): Promise<{ groupId?: string, artifactId?: string, version?: string }> {
73
- // We use PowerShell to quickly peek inside the JAR for pom.properties
74
- // This is faster than extracting the whole JAR
75
71
  const normalizedPath = jarPath.split(path.sep).join("/");
76
72
  const psCommand = `
77
73
  Add-Type -AssemblyName System.IO.Compression.FileSystem
78
- $zip = [System.IO.Compression.ZipFile]::OpenRead("${normalizedPath}")
74
+ $zip = [System.IO.Compression.ZipFile]::OpenRead($env:JAR_PATH)
79
75
  $entry = $zip.Entries | Where-Object { $_.FullName -match "pom.properties$" } | Select-Object -First 1
80
76
  if ($entry) {
81
77
  $stream = $entry.Open()
@@ -89,7 +85,12 @@ export class AuditService {
89
85
  `;
90
86
 
91
87
  try {
92
- const proc = Bun.spawn(["powershell", "-command", psCommand]);
88
+ const proc = Bun.spawn(["powershell", "-command", psCommand], {
89
+ env: {
90
+ ...process.env,
91
+ JAR_PATH: normalizedPath
92
+ }
93
+ });
93
94
  const output = await new Response(proc.stdout).text();
94
95
 
95
96
  const groupId = output.match(/groupId=(.*)/)?.[1]?.trim();
@@ -137,7 +138,6 @@ export class AuditService {
137
138
  private extractSeverity(vuln: any): string {
138
139
  if (vuln.database_specific?.severity) return vuln.database_specific.severity;
139
140
  if (vuln.advisories?.[0]?.url?.includes("github.com/advisories")) {
140
- // Try to infer from details if common keywords exist
141
141
  const d = (vuln.details || "").toLowerCase();
142
142
  if (d.includes("critical")) return "CRITICAL";
143
143
  if (d.includes("high")) return "HIGH";
@@ -0,0 +1,41 @@
1
+ import { Logger } from "../utils/ui";
2
+
3
+ export class BrowserService {
4
+ /**
5
+ * Recarrega a aba ativa do browser (Chrome ou Edge) no Windows.
6
+ */
7
+ public static async reload(url: string) {
8
+ if (process.platform !== 'win32') return;
9
+
10
+ // Pequeno delay para garantir que o Tomcat processou o novo contexto
11
+ await new Promise(r => setTimeout(r, 800));
12
+
13
+ const psCommand = `
14
+ $shell = New-Object -ComObject WScript.Shell
15
+ $process = Get-Process | Where-Object { $_.MainWindowTitle -match "Chrome" -or $_.MainWindowTitle -match "Edge" } | Select-Object -First 1
16
+ if ($process) {
17
+ $shell.AppActivate($process.Id)
18
+ Sleep -m 100
19
+ $shell.SendKeys("{F5}")
20
+ }
21
+ `;
22
+
23
+ try {
24
+ Bun.spawn(["powershell", "-command", psCommand]);
25
+ } catch (e) {
26
+ Logger.warn("Não foi possível recarregar o browser automaticamente.");
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Abre a URL no browser padrão do sistema.
32
+ */
33
+ public static open(url: string) {
34
+ if (process.platform === 'win32') {
35
+ Bun.spawn(["cmd", "/c", "start", url]);
36
+ } else {
37
+ const start = process.platform === 'darwin' ? 'open' : 'xdg-open';
38
+ Bun.spawn([start, url]);
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,75 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+
5
+ export interface CacheData {
6
+ lastConfigHash: string;
7
+ lastBuildTime: number;
8
+ }
9
+
10
+ export class BuildCacheService {
11
+ private cacheDir: string;
12
+ private cacheFile: string;
13
+
14
+ constructor() {
15
+ this.cacheDir = path.join(process.cwd(), ".xavva");
16
+ this.cacheFile = path.join(this.cacheDir, "build-cache.json");
17
+ if (!fs.existsSync(this.cacheDir)) {
18
+ fs.mkdirSync(this.cacheDir, { recursive: true });
19
+ }
20
+ }
21
+
22
+ getHash(filePath: string): string {
23
+ if (!fs.existsSync(filePath)) return "";
24
+ const content = fs.readFileSync(filePath);
25
+ return crypto.createHash("md5").update(content).digest("hex");
26
+ }
27
+
28
+ getConfigHash(tool: "maven" | "gradle"): string {
29
+ const file = tool === "maven" ? "pom.xml" : "build.gradle";
30
+ const configPath = path.join(process.cwd(), file);
31
+ let hash = this.getHash(configPath);
32
+
33
+ // Se for gradle, também checar build.gradle.kts e settings
34
+ if (tool === "gradle") {
35
+ const kts = path.join(process.cwd(), "build.gradle.kts");
36
+ const settings = path.join(process.cwd(), "settings.gradle");
37
+ if (fs.existsSync(kts)) hash += this.getHash(kts);
38
+ if (fs.existsSync(settings)) hash += this.getHash(settings);
39
+ }
40
+
41
+ return crypto.createHash("md5").update(hash).digest("hex");
42
+ }
43
+
44
+ shouldRebuild(tool: "maven" | "gradle"): boolean {
45
+ if (!fs.existsSync(this.cacheFile)) return true;
46
+
47
+ try {
48
+ const currentHash = this.getConfigHash(tool);
49
+ const cache: CacheData = JSON.parse(fs.readFileSync(this.cacheFile, "utf-8"));
50
+
51
+ // Se o pom/gradle mudou, precisa de rebuild completo
52
+ if (currentHash !== cache.lastConfigHash) return true;
53
+
54
+ // Verificar se o artefato (.war) ainda existe
55
+ // (Esta parte será integrada ao BuildService)
56
+ return false;
57
+ } catch (e) {
58
+ return true;
59
+ }
60
+ }
61
+
62
+ saveCache(tool: "maven" | "gradle") {
63
+ const data: CacheData = {
64
+ lastConfigHash: this.getConfigHash(tool),
65
+ lastBuildTime: Date.now()
66
+ };
67
+ fs.writeFileSync(this.cacheFile, JSON.stringify(data, null, 2));
68
+ }
69
+
70
+ clearCache() {
71
+ if (fs.existsSync(this.cacheFile)) {
72
+ fs.unlinkSync(this.cacheFile);
73
+ }
74
+ }
75
+ }