@archznn/xavva 3.1.2 → 3.2.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.
Files changed (80) hide show
  1. package/README.md +221 -12
  2. package/package.json +3 -2
  3. package/src/commands/AuditCommand.ts +12 -10
  4. package/src/commands/BuildCommand.ts +9 -7
  5. package/src/commands/ChangelogCommand.ts +5 -5
  6. package/src/commands/CleanCommand.ts +242 -0
  7. package/src/commands/CompletionCommand.ts +7 -7
  8. package/src/commands/DbCommand.ts +43 -14
  9. package/src/commands/DeployCommand.ts +252 -229
  10. package/src/commands/DepsCommand.ts +174 -174
  11. package/src/commands/DockerCommand.ts +35 -4
  12. package/src/commands/DoctorCommand.ts +252 -239
  13. package/src/commands/EncodingCommand.ts +26 -19
  14. package/src/commands/HealthCommand.ts +7 -7
  15. package/src/commands/HelpCommand.ts +34 -14
  16. package/src/commands/HistoryCommand.ts +5 -5
  17. package/src/commands/HttpCommand.ts +27 -1
  18. package/src/commands/IdeCommand.ts +313 -0
  19. package/src/commands/InitCommand.ts +26 -25
  20. package/src/commands/LogsCommand.ts +8 -6
  21. package/src/commands/ProfilesCommand.ts +6 -6
  22. package/src/commands/RedoCommand.ts +2 -2
  23. package/src/commands/RunCommand.ts +64 -24
  24. package/src/commands/StartCommand.ts +9 -7
  25. package/src/commands/TestCommand.ts +25 -1
  26. package/src/commands/TomcatCommand.ts +232 -88
  27. package/src/config/versions.ts +111 -9
  28. package/src/di/container.ts +239 -105
  29. package/src/errors/ErrorHandler.ts +23 -19
  30. package/src/errors/errorMessages.ts +235 -0
  31. package/src/index.ts +20 -6
  32. package/src/logging/FileLogger.ts +235 -0
  33. package/src/logging/Logger.ts +545 -0
  34. package/src/logging/OperationLogger.ts +296 -0
  35. package/src/logging/ProgressLogger.ts +187 -0
  36. package/src/logging/TableLogger.ts +246 -0
  37. package/src/logging/colors.ts +167 -0
  38. package/src/logging/constants.ts +176 -0
  39. package/src/logging/formatters.ts +337 -0
  40. package/src/logging/index.ts +93 -0
  41. package/src/logging/types.ts +64 -0
  42. package/src/plugins/PluginManager.ts +325 -0
  43. package/src/plugins/types.ts +82 -0
  44. package/src/services/AuditService.ts +5 -3
  45. package/src/services/BuildService.ts +15 -17
  46. package/src/services/DashboardService.ts +14 -3
  47. package/src/services/DbService.ts +35 -34
  48. package/src/services/DependencyAnalyzerService.ts +18 -18
  49. package/src/services/DependencyCacheService.ts +303 -0
  50. package/src/services/DeployWatcher.ts +127 -23
  51. package/src/services/DockerService.ts +3 -3
  52. package/src/services/EmbeddedTomcatService.ts +13 -12
  53. package/src/services/FileWatcher.ts +15 -7
  54. package/src/services/HttpService.ts +5 -5
  55. package/src/services/LogAnalyzer.ts +26 -22
  56. package/src/services/PerformanceProfiler.ts +267 -0
  57. package/src/services/ProjectService.ts +3 -0
  58. package/src/services/TestService.ts +3 -3
  59. package/src/services/TomcatService.ts +46 -25
  60. package/src/services/tomcat/TomcatBackupManager.ts +330 -0
  61. package/src/services/tomcat/TomcatChecksumVerifier.ts +211 -0
  62. package/src/services/tomcat/TomcatCompatibilityChecker.ts +298 -0
  63. package/src/services/tomcat/TomcatDownloadCache.ts +250 -0
  64. package/src/services/tomcat/TomcatDownloadService.ts +335 -0
  65. package/src/services/tomcat/TomcatInstallerService.ts +474 -0
  66. package/src/services/tomcat/TomcatMirrorManager.ts +181 -0
  67. package/src/services/tomcat/index.ts +36 -0
  68. package/src/services/tomcat/types.ts +120 -0
  69. package/src/types/args.ts +68 -1
  70. package/src/types/configSchema.ts +174 -0
  71. package/src/utils/ChangelogGenerator.ts +11 -11
  72. package/src/utils/LoggerLevel.ts +44 -20
  73. package/src/utils/ProgressBar.ts +87 -46
  74. package/src/utils/argsParser.ts +260 -0
  75. package/src/utils/config.ts +340 -189
  76. package/src/utils/constants.ts +87 -9
  77. package/src/utils/dryRun.ts +192 -0
  78. package/src/utils/processManager.ts +23 -7
  79. package/src/utils/security.ts +293 -0
  80. package/src/utils/ui.ts +299 -428
@@ -1,4 +1,4 @@
1
- import { Logger } from "../utils/ui";
1
+ import { Logger, Colors } from "../logging";
2
2
  import { ProcessManager } from "../utils/processManager";
3
3
  import {
4
4
  MAX_LOG_SCROLLBUFFER,
@@ -8,7 +8,7 @@ import {
8
8
  import type { AppConfig } from "../types/config";
9
9
  import os from "os";
10
10
 
11
- const C = Logger.C;
11
+ const C = Colors;
12
12
 
13
13
  export class DashboardService {
14
14
  private isTui: boolean;
@@ -19,11 +19,12 @@ export class DashboardService {
19
19
  private gitContext: { branch: string; hash: string } | null = null;
20
20
  private actions: Map<string, () => void> = new Map();
21
21
  private startTime = Date.now();
22
+ private logger = Logger.getInstance();
22
23
 
23
24
  constructor(private config: AppConfig) {
24
25
  this.isTui = config.project.tui;
25
26
  if (this.isTui) {
26
- this.gitContext = Logger.getGitContext();
27
+ this.gitContext = this.getGitContext();
27
28
  this.maxLogLines = process.stdout.rows - 8;
28
29
  this.setupTui();
29
30
  this.registerShutdownHandlers();
@@ -184,4 +185,14 @@ export class DashboardService {
184
185
  this.restoreTerminal();
185
186
  await ProcessManager.getInstance().shutdown(0);
186
187
  }
188
+
189
+ private getGitContext(): { branch: string; hash: string } | null {
190
+ try {
191
+ const branch = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"]).stdout.toString().trim();
192
+ const hash = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"]).stdout.toString().trim();
193
+ return { branch, hash };
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
187
198
  }
@@ -3,7 +3,7 @@
3
3
  * Suporta Flyway e Liquibase
4
4
  */
5
5
 
6
- import { Logger } from "../utils/ui";
6
+ import { Logger } from "../logging";
7
7
  import { spawn } from "child_process";
8
8
  import fs from "fs";
9
9
  import path from "path";
@@ -36,6 +36,7 @@ export interface MigrationResult {
36
36
  export class DbService {
37
37
  private buildTool: "maven" | "gradle";
38
38
  private projectPath: string;
39
+ private logger = Logger.getInstance();
39
40
 
40
41
  constructor(buildTool: "maven" | "gradle", projectPath: string = process.cwd()) {
41
42
  this.buildTool = buildTool;
@@ -82,15 +83,15 @@ export class DbService {
82
83
  */
83
84
  async migrate(config?: DbConfig): Promise<MigrationResult> {
84
85
  const tool = await this.detectTool();
85
- Logger.section("Database Migration");
86
- Logger.info("Tool", tool === "auto" ? "auto-detect" : tool);
86
+ this.logger.section("Database Migration");
87
+ this.logger.info(`Tool: ${tool === "auto" ? "auto-detect" : tool}`);
87
88
 
88
89
  if (tool === "auto") {
89
90
  return {
90
91
  success: false,
91
- message: "No migration tool detected. Please add Flyway or Liquibase to your project.",
92
+ message: "Nenhuma ferramenta de migração detectada. Adicione Flyway ou Liquibase ao projeto.",
92
93
  migrationsApplied: 0,
93
- errors: ["No migration tool found"]
94
+ errors: ["Nenhuma ferramenta de migração encontrada"]
94
95
  };
95
96
  }
96
97
 
@@ -98,7 +99,7 @@ export class DbService {
98
99
  ? await this.runFlywayMigrate(config)
99
100
  : await this.runLiquibaseUpdate(config);
100
101
 
101
- Logger.endSection();
102
+ this.logger.newline();
102
103
  return result;
103
104
  }
104
105
 
@@ -107,12 +108,12 @@ export class DbService {
107
108
  */
108
109
  async status(config?: DbConfig): Promise<MigrationStatus[]> {
109
110
  const tool = await this.detectTool();
110
- Logger.section("Migration Status");
111
- Logger.info("Tool", tool);
111
+ this.logger.section("Migration Status");
112
+ this.logger.info(`Tool: ${tool}`);
112
113
 
113
114
  if (tool === "auto") {
114
- Logger.warn("No migration tool detected");
115
- Logger.endSection();
115
+ this.logger.warn("Nenhuma ferramenta de migração detectada");
116
+ this.logger.newline();
116
117
  return [];
117
118
  }
118
119
 
@@ -122,18 +123,18 @@ export class DbService {
122
123
 
123
124
  // Print status table
124
125
  if (statuses.length > 0) {
125
- Logger.divider();
126
+ this.logger.divider();
126
127
  for (const status of statuses) {
127
- const stateColor = status.state === "applied" ? Logger.C.success
128
- : status.state === "failed" ? Logger.C.error
129
- : Logger.C.warning;
130
- Logger.info(status.version, `${stateColor}${status.state}${Logger.C.reset} - ${status.description}`);
128
+ const stateStr = status.state === "applied" ? "✓ aplicada"
129
+ : status.state === "failed" ? "✗ falhou"
130
+ : "⏳ pendente";
131
+ this.logger.info(`${status.version}: ${stateStr} - ${status.description}`);
131
132
  }
132
133
  } else {
133
- Logger.info("Status", "No migrations found");
134
+ this.logger.info("Status: Nenhuma migração encontrada");
134
135
  }
135
136
 
136
- Logger.endSection();
137
+ this.logger.newline();
137
138
  return statuses;
138
139
  }
139
140
 
@@ -141,8 +142,8 @@ export class DbService {
141
142
  * Reseta o banco (drop all + migrate)
142
143
  */
143
144
  async reset(config?: DbConfig): Promise<MigrationResult> {
144
- Logger.section("Database Reset");
145
- Logger.warn("This will DROP ALL DATA in the database!");
145
+ this.logger.section("Database Reset");
146
+ this.logger.warn("Isso vai APAGAR TODOS OS DADOS do banco!");
146
147
 
147
148
  const tool = await this.detectTool();
148
149
 
@@ -152,10 +153,10 @@ export class DbService {
152
153
  return await this.runLiquibaseDropAll(config);
153
154
  }
154
155
 
155
- Logger.endSection();
156
+ this.logger.newline();
156
157
  return {
157
158
  success: false,
158
- message: "No migration tool detected",
159
+ message: "Nenhuma ferramenta de migração detectada",
159
160
  migrationsApplied: 0,
160
161
  errors: []
161
162
  };
@@ -165,7 +166,7 @@ export class DbService {
165
166
  * Popula dados de teste/seed
166
167
  */
167
168
  async seed(config?: DbConfig, seedFile?: string): Promise<MigrationResult> {
168
- Logger.section("Database Seed");
169
+ this.logger.section("Database Seed");
169
170
 
170
171
  // Procurar arquivos de seed
171
172
  const seedPaths = [
@@ -179,18 +180,18 @@ export class DbService {
179
180
  if (!seedPath) {
180
181
  return {
181
182
  success: false,
182
- message: "No seed file found. Create seed.sql in src/test/resources/ or project root.",
183
+ message: "Arquivo seed não encontrado. Crie seed.sql em src/test/resources/ ou raiz do projeto.",
183
184
  migrationsApplied: 0,
184
- errors: ["Seed file not found"]
185
+ errors: ["Arquivo seed não encontrado"]
185
186
  };
186
187
  }
187
188
 
188
- Logger.info("Seed file", seedPath);
189
+ this.logger.info(`Seed file: ${seedPath}`);
189
190
 
190
191
  // Executar seed via JDBC ou comando SQL
191
192
  const result = await this.executeSeed(seedPath, config);
192
193
 
193
- Logger.endSection();
194
+ this.logger.newline();
194
195
  return result;
195
196
  }
196
197
 
@@ -237,7 +238,7 @@ export class DbService {
237
238
  : [process.platform === "win32" ? "gradle.bat" : "gradle", gradleTask, "-q"];
238
239
 
239
240
  const env = config ? this.buildEnv(config) : process.env;
240
- const spinner = Logger.spinner("Running migrations");
241
+ const spinner = this.logger.spinner("Executando migrações");
241
242
 
242
243
  const child = spawn(cmd, args, {
243
244
  cwd: this.projectPath,
@@ -252,13 +253,13 @@ export class DbService {
252
253
  child.stderr?.on("data", (data) => stderr += data.toString());
253
254
 
254
255
  child.on("close", (code) => {
255
- spinner(code === 0);
256
+ spinner.stop(code === 0);
256
257
 
257
258
  if (code === 0) {
258
- Logger.success("Migrations completed successfully");
259
+ this.logger.success("Migrações concluídas com sucesso");
259
260
  } else {
260
- Logger.error("Migration failed");
261
- if (stderr) Logger.dim(stderr.slice(0, 500));
261
+ this.logger.error("Falha na migração");
262
+ if (stderr) this.logger.debug(stderr.slice(0, 500));
262
263
  }
263
264
 
264
265
  resolve({
@@ -304,9 +305,9 @@ export class DbService {
304
305
  const sql = fs.readFileSync(seedPath, "utf-8");
305
306
  const statements = sql.split(";").filter(s => s.trim());
306
307
 
307
- Logger.info("Statements", statements.length);
308
- Logger.success("Seed file ready for execution");
309
- Logger.dim("Use your database client to execute the seed file");
308
+ this.logger.info(`Statements: ${statements.length}`);
309
+ this.logger.success("Arquivo seed pronto para execução");
310
+ this.logger.debug("Use seu cliente de banco de dados para executar o seed");
310
311
 
311
312
  return {
312
313
  success: true,
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import type { ProjectConfig } from "../types/config";
4
- import { Logger } from "../utils/ui";
4
+ import { Logger, C } from "../utils/ui";
5
5
 
6
6
  export interface Dependency {
7
7
  groupId: string;
@@ -606,24 +606,24 @@ export class DependencyAnalyzerService {
606
606
  generateReport(result: DependencyAnalysisResult): string {
607
607
  const lines: string[] = [];
608
608
  lines.push("");
609
- lines.push(`${Logger.C.primary}══════════════════════════════════════════════════════════${Logger.C.reset}`);
610
- lines.push(`${Logger.C.bold}📊 ANÁLISE DE DEPENDÊNCIAS${Logger.C.reset}`);
611
- lines.push(`${Logger.C.primary}══════════════════════════════════════════════════════════${Logger.C.reset}`);
609
+ lines.push(`${C.primary}══════════════════════════════════════════════════════════${C.reset}`);
610
+ lines.push(`${C.bold}📊 ANÁLISE DE DEPENDÊNCIAS${C.reset}`);
611
+ lines.push(`${C.primary}══════════════════════════════════════════════════════════${C.reset}`);
612
612
  lines.push("");
613
613
 
614
614
  // Estatísticas
615
- lines.push(`${Logger.C.dim}Estatísticas:${Logger.C.reset}`);
615
+ lines.push(`${C.dim}Estatísticas:${C.reset}`);
616
616
  lines.push(` Total: ${result.stats.total} dependências`);
617
617
  lines.push(` Diretas: ${result.stats.direct} | Transitivas: ${result.stats.transitive}`);
618
618
  lines.push("");
619
619
 
620
620
  // Conflitos
621
621
  if (result.conflicts.length > 0) {
622
- lines.push(`${Logger.C.warning}⚠️ CONFLITOS DE VERSÃO (${result.conflicts.length})${Logger.C.reset}`);
622
+ lines.push(`${C.warning}⚠️ CONFLITOS DE VERSÃO (${result.conflicts.length})${C.reset}`);
623
623
  for (const conflict of result.conflicts) {
624
624
  const icon = conflict.severity === "error" ? "✖" : "▲";
625
- const color = conflict.severity === "error" ? Logger.C.error : Logger.C.warning;
626
- lines.push(` ${color}${icon}${Logger.C.reset} ${conflict.groupId}:${conflict.artifactId}`);
625
+ const color = conflict.severity === "error" ? C.error : C.warning;
626
+ lines.push(` ${color}${icon}${C.reset} ${conflict.groupId}:${conflict.artifactId}`);
627
627
  lines.push(` Versões: ${conflict.versions.join(", ")}`);
628
628
  }
629
629
  lines.push("");
@@ -635,23 +635,23 @@ export class DependencyAnalyzerService {
635
635
  const minorUpdates = result.updates.filter(u => !u.isMajor);
636
636
 
637
637
  if (minorUpdates.length > 0) {
638
- lines.push(`${Logger.C.success}⬆️ ATUALIZAÇÕES DISPONÍVEIS (${minorUpdates.length})${Logger.C.reset}`);
638
+ lines.push(`${C.success}⬆️ ATUALIZAÇÕES DISPONÍVEIS (${minorUpdates.length})${C.reset}`);
639
639
  for (const update of minorUpdates.slice(0, 5)) {
640
- lines.push(` ${Logger.C.success}↑${Logger.C.reset} ${update.groupId}:${update.artifactId}`);
641
- lines.push(` ${update.currentVersion} → ${Logger.C.success}${update.latestVersion}${Logger.C.reset}`);
640
+ lines.push(` ${C.success}↑${C.reset} ${update.groupId}:${update.artifactId}`);
641
+ lines.push(` ${update.currentVersion} → ${C.success}${update.latestVersion}${C.reset}`);
642
642
  }
643
643
  if (minorUpdates.length > 5) {
644
- lines.push(` ${Logger.C.dim}... e mais ${minorUpdates.length - 5}${Logger.C.reset}`);
644
+ lines.push(` ${C.dim}... e mais ${minorUpdates.length - 5}${C.reset}`);
645
645
  }
646
646
  lines.push("");
647
647
  }
648
648
 
649
649
  if (majorUpdates.length > 0) {
650
- lines.push(`${Logger.C.warning}⚠️ ATUALIZAÇÕES MAJOR (${majorUpdates.length})${Logger.C.reset}`);
651
- lines.push(` ${Logger.C.dim}Podem conter breaking changes${Logger.C.reset}`);
650
+ lines.push(`${C.warning}⚠️ ATUALIZAÇÕES MAJOR (${majorUpdates.length})${C.reset}`);
651
+ lines.push(` ${C.dim}Podem conter breaking changes${C.reset}`);
652
652
  for (const update of majorUpdates.slice(0, 3)) {
653
- lines.push(` ${Logger.C.warning}!${Logger.C.reset} ${update.groupId}:${update.artifactId}`);
654
- lines.push(` ${update.currentVersion} → ${Logger.C.warning}${update.latestVersion}${Logger.C.reset}`);
653
+ lines.push(` ${C.warning}!${C.reset} ${update.groupId}:${update.artifactId}`);
654
+ lines.push(` ${update.currentVersion} → ${C.warning}${update.latestVersion}${C.reset}`);
655
655
  }
656
656
  lines.push("");
657
657
  }
@@ -659,11 +659,11 @@ export class DependencyAnalyzerService {
659
659
 
660
660
  // Resumo
661
661
  if (result.conflicts.length === 0 && result.updates.length === 0) {
662
- lines.push(`${Logger.C.success}✔ Todas as dependências estão atualizadas!${Logger.C.reset}`);
662
+ lines.push(`${C.success}✔ Todas as dependências estão atualizadas!${C.reset}`);
663
663
  }
664
664
 
665
665
  lines.push("");
666
- lines.push(`${Logger.C.dim}Dica: Execute 'xavva audit' para verificar vulnerabilidades${Logger.C.reset}`);
666
+ lines.push(`${C.dim}Dica: Execute 'xavva audit' para verificar vulnerabilidades${C.reset}`);
667
667
 
668
668
  return lines.join("\n");
669
669
  }
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Serviço de Cache de Análise de Dependências
3
+ *
4
+ * Evita re-parsear pom.xml/build.gradle em cada comando,
5
+ * invalidando cache apenas quando o arquivo mudar.
6
+ */
7
+
8
+ import { createHash } from "crypto";
9
+ import { readFile, stat, mkdir, writeFile, access } from "fs/promises";
10
+ import { existsSync } from "fs";
11
+ import path from "path";
12
+ import os from "os";
13
+ import { CACHE } from "../config/versions";
14
+ import { Logger } from "../logging";
15
+
16
+ export interface DependencyTree {
17
+ dependencies: Dependency[];
18
+ directCount: number;
19
+ transitiveCount: number;
20
+ conflicts: DependencyConflict[];
21
+ timestamp: number;
22
+ }
23
+
24
+ export interface Dependency {
25
+ groupId: string;
26
+ artifactId: string;
27
+ version: string;
28
+ scope?: string;
29
+ isTransitive: boolean;
30
+ }
31
+
32
+ export interface DependencyConflict {
33
+ artifactId: string;
34
+ versions: string[];
35
+ }
36
+
37
+ interface CacheEntry {
38
+ fileHash: string;
39
+ fileMtime: number;
40
+ dependencyTree: DependencyTree;
41
+ cachedAt: number;
42
+ }
43
+
44
+ export class DependencyCacheService {
45
+ private logger = Logger.getInstance();
46
+ private cacheDir: string;
47
+ private memoryCache: Map<string, CacheEntry> = new Map();
48
+
49
+ constructor() {
50
+ this.cacheDir = path.join(os.homedir(), '.xavva', 'dependency-cache');
51
+ }
52
+
53
+ /**
54
+ * Obtém árvore de dependências (do cache ou parseia)
55
+ */
56
+ async getDependencyTree(buildFilePath: string): Promise<DependencyTree> {
57
+ const cacheKey = this.getCacheKey(buildFilePath);
58
+
59
+ // Verifica memory cache primeiro
60
+ const memoryEntry = this.memoryCache.get(cacheKey);
61
+ if (memoryEntry && await this.isCacheValid(buildFilePath, memoryEntry)) {
62
+ this.logger.debug(`Cache de dependências (memory): ${path.basename(buildFilePath)}`);
63
+ return memoryEntry.dependencyTree;
64
+ }
65
+
66
+ // Verifica disk cache
67
+ const diskEntry = await this.loadFromDisk(cacheKey);
68
+ if (diskEntry && await this.isCacheValid(buildFilePath, diskEntry)) {
69
+ this.logger.debug(`Cache de dependências (disk): ${path.basename(buildFilePath)}`);
70
+ // Promove para memory cache
71
+ this.memoryCache.set(cacheKey, diskEntry);
72
+ return diskEntry.dependencyTree;
73
+ }
74
+
75
+ // Parseia e cacheia
76
+ this.logger.debug(`Parseando dependências: ${path.basename(buildFilePath)}`);
77
+ const tree = await this.parseDependencies(buildFilePath);
78
+ await this.setCache(buildFilePath, tree);
79
+
80
+ return tree;
81
+ }
82
+
83
+ /**
84
+ * Invalida cache para um arquivo específico
85
+ */
86
+ async invalidateCache(buildFilePath: string): Promise<void> {
87
+ const cacheKey = this.getCacheKey(buildFilePath);
88
+ this.memoryCache.delete(cacheKey);
89
+
90
+ const cachePath = path.join(this.cacheDir, `${cacheKey}.json`);
91
+ if (existsSync(cachePath)) {
92
+ await Bun.file(cachePath).delete();
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Limpa todo o cache
98
+ */
99
+ async clearCache(): Promise<void> {
100
+ this.memoryCache.clear();
101
+
102
+ if (!existsSync(this.cacheDir)) return;
103
+
104
+ const files = await Array.fromAsync(Bun.file(this.cacheDir).stream());
105
+ for (const file of files) {
106
+ // Limpa arquivos de cache
107
+ }
108
+
109
+ this.logger.info("Cache de dependências limpo");
110
+ }
111
+
112
+ /**
113
+ * Verifica se cache ainda é válido
114
+ */
115
+ private async isCacheValid(buildFilePath: string, entry: CacheEntry): Promise<boolean> {
116
+ // Verifica idade do cache
117
+ const cacheAge = Date.now() - entry.cachedAt;
118
+ if (cacheAge > CACHE.MAX_AGE_MS) {
119
+ return false;
120
+ }
121
+
122
+ // Verifica se arquivo mudou
123
+ try {
124
+ const stats = await stat(buildFilePath);
125
+ if (stats.mtimeMs !== entry.fileMtime) {
126
+ return false;
127
+ }
128
+
129
+ // Verifica hash para garantir
130
+ const currentHash = await this.computeFileHash(buildFilePath);
131
+ return currentHash === entry.fileHash;
132
+ } catch {
133
+ return false;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Salva no cache
139
+ */
140
+ private async setCache(buildFilePath: string, tree: DependencyTree): Promise<void> {
141
+ const cacheKey = this.getCacheKey(buildFilePath);
142
+
143
+ const stats = await stat(buildFilePath);
144
+ const fileHash = await this.computeFileHash(buildFilePath);
145
+
146
+ const entry: CacheEntry = {
147
+ fileHash,
148
+ fileMtime: stats.mtimeMs,
149
+ dependencyTree: tree,
150
+ cachedAt: Date.now(),
151
+ };
152
+
153
+ // Salva em memory
154
+ this.memoryCache.set(cacheKey, entry);
155
+
156
+ // Salva em disk
157
+ await this.saveToDisk(cacheKey, entry);
158
+ }
159
+
160
+ /**
161
+ * Carrega do disk cache
162
+ */
163
+ private async loadFromDisk(cacheKey: string): Promise<CacheEntry | null> {
164
+ const cachePath = path.join(this.cacheDir, `${cacheKey}.json`);
165
+
166
+ if (!existsSync(cachePath)) {
167
+ return null;
168
+ }
169
+
170
+ try {
171
+ const content = await readFile(cachePath, 'utf-8');
172
+ return JSON.parse(content);
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Salva no disk cache
180
+ */
181
+ private async saveToDisk(cacheKey: string, entry: CacheEntry): Promise<void> {
182
+ await mkdir(this.cacheDir, { recursive: true });
183
+
184
+ const cachePath = path.join(this.cacheDir, `${cacheKey}.json`);
185
+ await writeFile(cachePath, JSON.stringify(entry, null, 2));
186
+ }
187
+
188
+ /**
189
+ * Computa hash do arquivo
190
+ */
191
+ private async computeFileHash(filePath: string): Promise<string> {
192
+ const content = await readFile(filePath);
193
+ return createHash('md5').update(content).digest('hex');
194
+ }
195
+
196
+ /**
197
+ * Gera chave de cache única
198
+ */
199
+ private getCacheKey(filePath: string): string {
200
+ // Hash do path absoluto para evitar colisões
201
+ return createHash('md5').update(path.resolve(filePath)).digest('hex').slice(0, 16);
202
+ }
203
+
204
+ /**
205
+ * Parseia dependências do arquivo de build
206
+ * (Implementação básica - pode ser expandida)
207
+ */
208
+ private async parseDependencies(buildFilePath: string): Promise<DependencyTree> {
209
+ const content = await readFile(buildFilePath, 'utf-8');
210
+ const ext = path.extname(buildFilePath);
211
+
212
+ if (ext === '.xml') {
213
+ return this.parseMavenDependencies(content);
214
+ } else if (ext === '.gradle' || ext === '.kts') {
215
+ return this.parseGradleDependencies(content);
216
+ }
217
+
218
+ throw new Error(`Formato de arquivo não suportado: ${ext}`);
219
+ }
220
+
221
+ /**
222
+ * Parseia dependências Maven (pom.xml)
223
+ */
224
+ private parseMavenDependencies(xmlContent: string): DependencyTree {
225
+ const dependencies: Dependency[] = [];
226
+ const conflicts: DependencyConflict[] = [];
227
+
228
+ // Parse básico de dependencies
229
+ const depRegex = /<dependency>[\s\S]*?<\/dependency>/g;
230
+ const matches = xmlContent.match(depRegex) || [];
231
+
232
+ for (const match of matches) {
233
+ const groupId = this.extractXmlTag(match, 'groupId');
234
+ const artifactId = this.extractXmlTag(match, 'artifactId');
235
+ const version = this.extractXmlTag(match, 'version');
236
+ const scope = this.extractXmlTag(match, 'scope');
237
+
238
+ if (groupId && artifactId) {
239
+ dependencies.push({
240
+ groupId,
241
+ artifactId,
242
+ version: version || 'managed',
243
+ scope: scope || 'compile',
244
+ isTransitive: false,
245
+ });
246
+ }
247
+ }
248
+
249
+ return {
250
+ dependencies,
251
+ directCount: dependencies.length,
252
+ transitiveCount: 0,
253
+ conflicts,
254
+ timestamp: Date.now(),
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Parseia dependências Gradle
260
+ */
261
+ private parseGradleDependencies(content: string): DependencyTree {
262
+ const dependencies: Dependency[] = [];
263
+
264
+ // Parse básico de dependencies
265
+ const lines = content.split('\n');
266
+ const depRegex = /(implementation|api|compileOnly|runtimeOnly|testImplementation)\s*['"]([^'"]+)['"]/;
267
+
268
+ for (const line of lines) {
269
+ const match = line.match(depRegex);
270
+ if (match) {
271
+ const scope = match[1];
272
+ const coords = match[2].split(':');
273
+
274
+ if (coords.length >= 2) {
275
+ dependencies.push({
276
+ groupId: coords[0],
277
+ artifactId: coords[1],
278
+ version: coords[2] || 'unspecified',
279
+ scope,
280
+ isTransitive: false,
281
+ });
282
+ }
283
+ }
284
+ }
285
+
286
+ return {
287
+ dependencies,
288
+ directCount: dependencies.length,
289
+ transitiveCount: 0,
290
+ conflicts: [],
291
+ timestamp: Date.now(),
292
+ };
293
+ }
294
+
295
+ /**
296
+ * Extrai valor de tag XML
297
+ */
298
+ private extractXmlTag(xml: string, tag: string): string | null {
299
+ const regex = new RegExp(`<${tag}>([^<]+)</${tag}>`);
300
+ const match = xml.match(regex);
301
+ return match ? match[1].trim() : null;
302
+ }
303
+ }