@archznn/xavva 3.1.3 → 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.
- package/README.md +221 -12
- package/package.json +3 -2
- package/src/commands/AuditCommand.ts +12 -10
- package/src/commands/BuildCommand.ts +9 -7
- package/src/commands/ChangelogCommand.ts +5 -5
- package/src/commands/CleanCommand.ts +242 -0
- package/src/commands/CompletionCommand.ts +7 -7
- package/src/commands/DbCommand.ts +33 -31
- package/src/commands/DeployCommand.ts +252 -229
- package/src/commands/DepsCommand.ts +174 -174
- package/src/commands/DockerCommand.ts +14 -14
- package/src/commands/DoctorCommand.ts +252 -239
- package/src/commands/EncodingCommand.ts +19 -19
- package/src/commands/HealthCommand.ts +7 -7
- package/src/commands/HelpCommand.ts +34 -14
- package/src/commands/HistoryCommand.ts +5 -5
- package/src/commands/HttpCommand.ts +6 -6
- package/src/commands/IdeCommand.ts +313 -0
- package/src/commands/InitCommand.ts +26 -25
- package/src/commands/LogsCommand.ts +8 -6
- package/src/commands/ProfilesCommand.ts +6 -6
- package/src/commands/RedoCommand.ts +2 -2
- package/src/commands/RunCommand.ts +64 -24
- package/src/commands/StartCommand.ts +9 -7
- package/src/commands/TestCommand.ts +4 -4
- package/src/commands/TomcatCommand.ts +219 -100
- package/src/config/versions.ts +111 -9
- package/src/di/container.ts +239 -105
- package/src/errors/ErrorHandler.ts +23 -19
- package/src/errors/errorMessages.ts +235 -0
- package/src/index.ts +11 -3
- package/src/logging/FileLogger.ts +235 -0
- package/src/logging/Logger.ts +545 -0
- package/src/logging/OperationLogger.ts +296 -0
- package/src/logging/ProgressLogger.ts +187 -0
- package/src/logging/TableLogger.ts +246 -0
- package/src/logging/colors.ts +167 -0
- package/src/logging/constants.ts +176 -0
- package/src/logging/formatters.ts +337 -0
- package/src/logging/index.ts +93 -0
- package/src/logging/types.ts +64 -0
- package/src/plugins/PluginManager.ts +325 -0
- package/src/plugins/types.ts +82 -0
- package/src/services/AuditService.ts +5 -3
- package/src/services/BuildService.ts +15 -17
- package/src/services/DashboardService.ts +14 -3
- package/src/services/DbService.ts +35 -34
- package/src/services/DependencyAnalyzerService.ts +18 -18
- package/src/services/DependencyCacheService.ts +303 -0
- package/src/services/DeployWatcher.ts +127 -23
- package/src/services/DockerService.ts +3 -3
- package/src/services/EmbeddedTomcatService.ts +13 -12
- package/src/services/FileWatcher.ts +15 -7
- package/src/services/HttpService.ts +5 -5
- package/src/services/LogAnalyzer.ts +26 -22
- package/src/services/PerformanceProfiler.ts +267 -0
- package/src/services/ProjectService.ts +3 -0
- package/src/services/TestService.ts +3 -3
- package/src/services/TomcatService.ts +46 -25
- package/src/services/tomcat/TomcatBackupManager.ts +330 -0
- package/src/services/tomcat/TomcatChecksumVerifier.ts +211 -0
- package/src/services/tomcat/TomcatCompatibilityChecker.ts +298 -0
- package/src/services/tomcat/TomcatDownloadCache.ts +250 -0
- package/src/services/tomcat/TomcatDownloadService.ts +335 -0
- package/src/services/tomcat/TomcatInstallerService.ts +474 -0
- package/src/services/tomcat/TomcatMirrorManager.ts +181 -0
- package/src/services/tomcat/index.ts +36 -0
- package/src/services/tomcat/types.ts +120 -0
- package/src/types/args.ts +68 -1
- package/src/types/configSchema.ts +174 -0
- package/src/utils/ChangelogGenerator.ts +11 -11
- package/src/utils/LoggerLevel.ts +44 -20
- package/src/utils/ProgressBar.ts +87 -46
- package/src/utils/argsParser.ts +260 -0
- package/src/utils/config.ts +340 -189
- package/src/utils/constants.ts +87 -9
- package/src/utils/dryRun.ts +192 -0
- package/src/utils/processManager.ts +23 -7
- package/src/utils/security.ts +293 -0
- package/src/utils/ui.ts +299 -428
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Logger } from "../
|
|
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 =
|
|
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 =
|
|
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 "../
|
|
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
|
-
|
|
86
|
-
|
|
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: "
|
|
92
|
+
message: "Nenhuma ferramenta de migração detectada. Adicione Flyway ou Liquibase ao projeto.",
|
|
92
93
|
migrationsApplied: 0,
|
|
93
|
-
errors: ["
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
111
|
+
this.logger.section("Migration Status");
|
|
112
|
+
this.logger.info(`Tool: ${tool}`);
|
|
112
113
|
|
|
113
114
|
if (tool === "auto") {
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
126
|
+
this.logger.divider();
|
|
126
127
|
for (const status of statuses) {
|
|
127
|
-
const
|
|
128
|
-
: status.state === "failed" ?
|
|
129
|
-
:
|
|
130
|
-
|
|
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
|
-
|
|
134
|
+
this.logger.info("Status: Nenhuma migração encontrada");
|
|
134
135
|
}
|
|
135
136
|
|
|
136
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
156
|
+
this.logger.newline();
|
|
156
157
|
return {
|
|
157
158
|
success: false,
|
|
158
|
-
message: "
|
|
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
|
-
|
|
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: "
|
|
183
|
+
message: "Arquivo seed não encontrado. Crie seed.sql em src/test/resources/ ou raiz do projeto.",
|
|
183
184
|
migrationsApplied: 0,
|
|
184
|
-
errors: ["
|
|
185
|
+
errors: ["Arquivo seed não encontrado"]
|
|
185
186
|
};
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
259
|
+
this.logger.success("Migrações concluídas com sucesso");
|
|
259
260
|
} else {
|
|
260
|
-
|
|
261
|
-
if (stderr)
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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(`${
|
|
610
|
-
lines.push(`${
|
|
611
|
-
lines.push(`${
|
|
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(`${
|
|
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(`${
|
|
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" ?
|
|
626
|
-
lines.push(` ${color}${icon}${
|
|
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(`${
|
|
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(` ${
|
|
641
|
-
lines.push(` ${update.currentVersion} → ${
|
|
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(` ${
|
|
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(`${
|
|
651
|
-
lines.push(` ${
|
|
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(` ${
|
|
654
|
-
lines.push(` ${update.currentVersion} → ${
|
|
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(`${
|
|
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(`${
|
|
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
|
+
}
|