@archznn/xavva 2.0.2 → 2.1.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.
@@ -0,0 +1,538 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import type { ProjectConfig } from "../types/config";
4
+ import { Logger } from "../utils/ui";
5
+
6
+ export interface Dependency {
7
+ groupId: string;
8
+ artifactId: string;
9
+ version: string;
10
+ scope?: string;
11
+ type: "direct" | "transitive";
12
+ }
13
+
14
+ export interface DependencyConflict {
15
+ artifactId: string;
16
+ groupId: string;
17
+ versions: string[];
18
+ locations: string[];
19
+ severity: "warning" | "error";
20
+ }
21
+
22
+ export interface DependencyUpdate {
23
+ groupId: string;
24
+ artifactId: string;
25
+ currentVersion: string;
26
+ latestVersion: string;
27
+ isMajor: boolean;
28
+ changelog?: string;
29
+ }
30
+
31
+ export interface DependencyAnalysisResult {
32
+ dependencies: Dependency[];
33
+ conflicts: DependencyConflict[];
34
+ updates: DependencyUpdate[];
35
+ outdated: Dependency[];
36
+ stats: {
37
+ total: number;
38
+ direct: number;
39
+ transitive: number;
40
+ vulnerable: number;
41
+ outdatedCount: number;
42
+ };
43
+ }
44
+
45
+ interface MavenDependency {
46
+ groupId: string;
47
+ artifactId: string;
48
+ version: string;
49
+ scope?: string;
50
+ optional?: boolean;
51
+ }
52
+
53
+ export class DependencyAnalyzerService {
54
+ constructor(private projectConfig: ProjectConfig) {}
55
+
56
+ async analyze(): Promise<DependencyAnalysisResult> {
57
+ const deps = await this.extractDependencies();
58
+ const conflicts = this.detectConflicts(deps);
59
+ const updates = await this.checkUpdates(deps);
60
+ const outdated = deps.filter(d => updates.some(u =>
61
+ u.groupId === d.groupId && u.artifactId === d.artifactId
62
+ ));
63
+
64
+ return {
65
+ dependencies: deps,
66
+ conflicts,
67
+ updates,
68
+ outdated,
69
+ stats: {
70
+ total: deps.length,
71
+ direct: deps.filter(d => d.type === "direct").length,
72
+ transitive: deps.filter(d => d.type === "transitive").length,
73
+ vulnerable: 0, // Será preenchido pelo AuditService
74
+ outdatedCount: outdated.length
75
+ }
76
+ };
77
+ }
78
+
79
+ private async extractDependencies(): Promise<Dependency[]> {
80
+ if (this.projectConfig.buildTool === "maven") {
81
+ return this.extractMavenDependencies();
82
+ } else {
83
+ return this.extractGradleDependencies();
84
+ }
85
+ }
86
+
87
+ private verbose: boolean = false;
88
+
89
+ setVerbose(verbose: boolean) {
90
+ this.verbose = verbose;
91
+ }
92
+
93
+ private async extractMavenDependencies(): Promise<Dependency[]> {
94
+ const deps: Dependency[] = [];
95
+ const pomPath = path.join(process.cwd(), "pom.xml");
96
+
97
+ if (!fs.existsSync(pomPath)) {
98
+ Logger.warn("pom.xml não encontrado");
99
+ return deps;
100
+ }
101
+
102
+ if (this.verbose) {
103
+ Logger.info("Tentando extrair dependências do Maven...", "");
104
+ }
105
+
106
+ // Tentar várias estratégias para extrair dependências
107
+
108
+ // 1. Tentar mvn dependency:tree
109
+ try {
110
+ const mvnCmd = process.platform === "win32" ? "mvn.cmd" : "mvn";
111
+ Logger.step("Executando mvn dependency:tree...");
112
+
113
+ const output = Bun.spawnSync([
114
+ mvnCmd,
115
+ "dependency:tree",
116
+ "-DoutputType=text",
117
+ "-q"
118
+ ], { cwd: process.cwd() });
119
+
120
+ if (output.exitCode === 0) {
121
+ const treeOutput = output.stdout.toString();
122
+ const parsed = this.parseMavenTree(treeOutput);
123
+ if (parsed.length > 0) {
124
+ Logger.success(`Encontradas ${parsed.length} dependências via Maven tree`);
125
+ return parsed;
126
+ }
127
+ } else {
128
+ const errorOutput = output.stderr.toString();
129
+ if (this.verbose) {
130
+ Logger.warn(`mvn dependency:tree falhou: ${errorOutput.substring(0, 200)}`);
131
+ }
132
+ }
133
+ } catch (e) {
134
+ if (this.verbose) {
135
+ Logger.warn(`Não foi possível executar mvn dependency:tree: ${e}`);
136
+ }
137
+ }
138
+
139
+ // 2. Fallback: Parse direto do pom.xml
140
+ Logger.step("Analisando pom.xml diretamente...");
141
+ const pomDeps = this.parsePomDirect();
142
+ if (pomDeps.length > 0) {
143
+ Logger.success(`Encontradas ${pomDeps.length} dependências no pom.xml`);
144
+ return pomDeps;
145
+ }
146
+
147
+ // 3. Último recurso: Verificar pasta lib do Tomcat (se existir)
148
+ const libDeps = await this.extractFromLibFolder();
149
+ if (libDeps.length > 0) {
150
+ Logger.success(`Encontradas ${libDeps.length} dependências na pasta lib`);
151
+ return libDeps;
152
+ }
153
+
154
+ Logger.warn("Nenhuma dependência encontrada");
155
+ return deps;
156
+ }
157
+
158
+ private async extractFromLibFolder(): Promise<Dependency[]> {
159
+ const deps: Dependency[] = [];
160
+ const libPath = path.join(process.cwd(), "target", "dependency");
161
+
162
+ if (!fs.existsSync(libPath)) {
163
+ // Tentar gerar com mvn dependency:copy-dependencies
164
+ try {
165
+ const mvnCmd = process.platform === "win32" ? "mvn.cmd" : "mvn";
166
+ Logger.step("Tentando baixar dependências...");
167
+ Bun.spawnSync([
168
+ mvnCmd,
169
+ "dependency:copy-dependencies",
170
+ "-DoutputDirectory=target/dependency",
171
+ "-q"
172
+ ], { cwd: process.cwd() });
173
+ } catch (e) {
174
+ return deps;
175
+ }
176
+ }
177
+
178
+ if (fs.existsSync(libPath)) {
179
+ const files = fs.readdirSync(libPath).filter(f => f.endsWith(".jar"));
180
+ for (const file of files) {
181
+ const parsed = this.parseJarName(file);
182
+ if (parsed) {
183
+ deps.push({ ...parsed, type: "direct" });
184
+ }
185
+ }
186
+ }
187
+
188
+ return deps;
189
+ }
190
+
191
+ private parseJarName(jarName: string): { groupId: string; artifactId: string; version: string } | null {
192
+ // Remove .jar extension
193
+ const name = jarName.replace(/\.jar$/, "");
194
+
195
+ // Tenta extrair: artifactId-version
196
+ const match = name.match(/^(.+?)-(\d[\d\.-]*(?:-SNAPSHOT)?)$/);
197
+ if (match) {
198
+ return {
199
+ groupId: "unknown", // Não conseguimos extrair do nome do JAR
200
+ artifactId: match[1],
201
+ version: match[2]
202
+ };
203
+ }
204
+
205
+ return null;
206
+ }
207
+
208
+ private parseMavenTree(output: string): Dependency[] {
209
+ const deps: Dependency[] = [];
210
+ const lines = output.split("\n");
211
+ const seen = new Set<string>();
212
+
213
+ for (const line of lines) {
214
+ // Pattern: [INFO] | | \- org.springframework:spring-core:jar:5.3.9:compile
215
+ // ou: [INFO] +- org.springframework:spring-core:jar:5.3.9:compile
216
+ const match = line.match(/[\\|\-+]\s+([^:]+):([^:]+):[^:]+:([^:]+):?(\w+)?/);
217
+ if (match) {
218
+ const [, groupId, artifactId, version, scope] = match;
219
+ const key = `${groupId}:${artifactId}:${version}`;
220
+
221
+ if (!seen.has(key)) {
222
+ seen.add(key);
223
+ // Determina se é direto ou transitivo pelo nível de indentação
224
+ const level = (line.match(/[|\\]/g) || []).length;
225
+ deps.push({
226
+ groupId,
227
+ artifactId,
228
+ version,
229
+ scope: scope || "compile",
230
+ type: level <= 1 ? "direct" : "transitive"
231
+ });
232
+ }
233
+ }
234
+ }
235
+
236
+ return deps;
237
+ }
238
+
239
+ private parsePomDirect(): Dependency[] {
240
+ const deps: Dependency[] = [];
241
+ const pomPath = path.join(process.cwd(), "pom.xml");
242
+
243
+ try {
244
+ const content = fs.readFileSync(pomPath, "utf-8");
245
+
246
+ // Remove comentários XML para não interferir no parse
247
+ const cleanContent = content.replace(/<!--[\s\S]*?-->/g, "");
248
+
249
+ // Parse de dependencies - suporta propriedades ${...}
250
+ const depRegex = /<dependency>\s*([\s\S]*?)<\/dependency>/g;
251
+ let match;
252
+
253
+ while ((match = depRegex.exec(cleanContent)) !== null) {
254
+ const depBlock = match[1];
255
+
256
+ // Extrai groupId
257
+ let groupId = depBlock.match(/<groupId>\s*([^<$]+)\s*<\/groupId>/)?.[1]?.trim();
258
+ // Extrai artifactId
259
+ let artifactId = depBlock.match(/<artifactId>\s*([^<$]+)\s*<\/artifactId>/)?.[1]?.trim();
260
+ // Extrai version
261
+ let version = depBlock.match(/<version>\s*([^<$]+)\s*<\/version>/)?.[1]?.trim();
262
+ // Extrai scope
263
+ const scope = depBlock.match(/<scope>\s*([^<$]+)\s*<\/scope>/)?.[1]?.trim();
264
+
265
+ // Resolve propriedades simples ${property}
266
+ if (version?.startsWith("${")) {
267
+ const propName = version.slice(2, -1);
268
+ const propValue = cleanContent.match(new RegExp(`<${propName}>([^<]+)</${propName}>`))?.[1];
269
+ if (propValue) version = propValue.trim();
270
+ }
271
+
272
+ if (groupId && artifactId) {
273
+ // Se não tiver versão, tenta encontrar no dependencyManagement
274
+ if (!version || version.startsWith("${")) {
275
+ version = this.findVersionInDependencyManagement(cleanContent, groupId, artifactId) || version || "managed";
276
+ }
277
+
278
+ deps.push({
279
+ groupId,
280
+ artifactId,
281
+ version,
282
+ scope,
283
+ type: "direct"
284
+ });
285
+ }
286
+ }
287
+
288
+ Logger.info("Dependências encontradas no pom.xml", String(deps.length));
289
+ } catch (e) {
290
+ Logger.warn(`Não foi possível analisar o pom.xml: ${e}`);
291
+ }
292
+
293
+ return deps;
294
+ }
295
+
296
+ private findVersionInDependencyManagement(content: string, groupId: string, artifactId: string): string | null {
297
+ // Busca no dependencyManagement
298
+ const dmMatch = content.match(/<dependencyManagement>\s*<dependencies>([\s\S]*?)<\/dependencies>\s*<\/dependencyManagement>/);
299
+ if (dmMatch) {
300
+ const dmBlock = dmMatch[1];
301
+ const depRegex = /<dependency>\s*([\s\S]*?)<\/dependency>/g;
302
+ let match;
303
+
304
+ while ((match = depRegex.exec(dmBlock)) !== null) {
305
+ const depBlock = match[1];
306
+ const depGroupId = depBlock.match(/<groupId>\s*([^<]+)\s*<\/groupId>/)?.[1]?.trim();
307
+ const depArtifactId = depBlock.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/)?.[1]?.trim();
308
+ const depVersion = depBlock.match(/<version>\s*([^<]+)\s*<\/version>/)?.[1]?.trim();
309
+
310
+ if (depGroupId === groupId && depArtifactId === artifactId && depVersion) {
311
+ return depVersion;
312
+ }
313
+ }
314
+ }
315
+ return null;
316
+ }
317
+
318
+ private async extractGradleDependencies(): Promise<Dependency[]> {
319
+ const deps: Dependency[] = [];
320
+
321
+ try {
322
+ const gradleCmd = process.platform === "win32" ? "gradle.bat" : "gradle";
323
+ const output = Bun.spawnSync([
324
+ gradleCmd,
325
+ "dependencies",
326
+ "--configuration",
327
+ "runtimeClasspath",
328
+ "-q"
329
+ ], { cwd: process.cwd() });
330
+
331
+ if (output.exitCode === 0) {
332
+ deps.push(...this.parseGradleTree(output.stdout.toString()));
333
+ }
334
+ } catch (e) {
335
+ Logger.warn("Não foi possível executar gradle dependencies");
336
+ }
337
+
338
+ return deps;
339
+ }
340
+
341
+ private parseGradleTree(output: string): Dependency[] {
342
+ const deps: Dependency[] = [];
343
+ const lines = output.split("\n");
344
+ const seen = new Set<string>();
345
+
346
+ for (const line of lines) {
347
+ // Pattern: | | +--- org.springframework:spring-core:5.3.9
348
+ const match = line.match(/[\\|\-+]+\s+([^:]+):([^:]+):([^\s]+)/);
349
+ if (match) {
350
+ const [, groupId, artifactId, version] = match;
351
+ const key = `${groupId}:${artifactId}:${version}`;
352
+
353
+ if (!seen.has(key)) {
354
+ seen.add(key);
355
+ const level = (line.match(/[|\\]/g) || []).length;
356
+ deps.push({
357
+ groupId,
358
+ artifactId,
359
+ version,
360
+ type: level <= 1 ? "direct" : "transitive"
361
+ });
362
+ }
363
+ }
364
+ }
365
+
366
+ return deps;
367
+ }
368
+
369
+ private detectConflicts(deps: Dependency[]): DependencyConflict[] {
370
+ const conflicts: DependencyConflict[] = [];
371
+ const grouped = new Map<string, Dependency[]>();
372
+
373
+ // Agrupar por groupId:artifactId
374
+ for (const dep of deps) {
375
+ const key = `${dep.groupId}:${dep.artifactId}`;
376
+ if (!grouped.has(key)) {
377
+ grouped.set(key, []);
378
+ }
379
+ grouped.get(key)!.push(dep);
380
+ }
381
+
382
+ // Detectar conflitos (mesmo artifact, versões diferentes)
383
+ for (const [key, versions] of grouped) {
384
+ const uniqueVersions = [...new Set(versions.map(v => v.version))];
385
+ if (uniqueVersions.length > 1) {
386
+ const [groupId, artifactId] = key.split(":");
387
+ const hasDirect = versions.some(v => v.type === "direct");
388
+
389
+ conflicts.push({
390
+ groupId,
391
+ artifactId,
392
+ versions: uniqueVersions,
393
+ locations: versions.map(v => v.scope || "compile"),
394
+ severity: hasDirect ? "error" : "warning"
395
+ });
396
+ }
397
+ }
398
+
399
+ return conflicts;
400
+ }
401
+
402
+ private async checkUpdates(deps: Dependency[]): Promise<DependencyUpdate[]> {
403
+ const updates: DependencyUpdate[] = [];
404
+ const directDeps = deps.filter(d => d.type === "direct");
405
+
406
+ // Verificar atualizações em paralelo (com limite)
407
+ const chunkSize = 5;
408
+ for (let i = 0; i < directDeps.length; i += chunkSize) {
409
+ const chunk = directDeps.slice(i, i + chunkSize);
410
+ const promises = chunk.map(dep => this.checkSingleUpdate(dep));
411
+ const results = await Promise.all(promises);
412
+ updates.push(...results.filter((u): u is DependencyUpdate => u !== null));
413
+ }
414
+
415
+ return updates;
416
+ }
417
+
418
+ private async checkSingleUpdate(dep: Dependency): Promise<DependencyUpdate | null> {
419
+ try {
420
+ const latest = await this.fetchLatestVersion(dep.groupId, dep.artifactId);
421
+ if (latest && this.isNewer(latest, dep.version)) {
422
+ return {
423
+ groupId: dep.groupId,
424
+ artifactId: dep.artifactId,
425
+ currentVersion: dep.version,
426
+ latestVersion: latest,
427
+ isMajor: this.isMajorUpdate(dep.version, latest)
428
+ };
429
+ }
430
+ } catch (e) {
431
+ // Silenciar erros de rede
432
+ }
433
+ return null;
434
+ }
435
+
436
+ private async fetchLatestVersion(groupId: string, artifactId: string): Promise<string | null> {
437
+ // Usar Maven Central API
438
+ try {
439
+ const url = `https://search.maven.org/solrsearch/select?q=g:"${groupId}"+AND+a:"${artifactId}"&rows=1&wt=json`;
440
+ const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
441
+ const data = await response.json();
442
+
443
+ if (data.response?.docs?.[0]?.latestVersion) {
444
+ return data.response.docs[0].latestVersion;
445
+ }
446
+ } catch (e) {
447
+ // Fallback silencioso
448
+ }
449
+ return null;
450
+ }
451
+
452
+ private isNewer(latest: string, current: string): boolean {
453
+ return this.compareVersions(latest, current) > 0;
454
+ }
455
+
456
+ private isMajorUpdate(current: string, latest: string): boolean {
457
+ const currentMajor = current.split(".")[0];
458
+ const latestMajor = latest.split(".")[0];
459
+ return currentMajor !== latestMajor;
460
+ }
461
+
462
+ private compareVersions(v1: string, v2: string): number {
463
+ const parts1 = v1.split(/[.-]/).filter(p => /^\d+$/.test(p)).map(Number);
464
+ const parts2 = v2.split(/[.-]/).filter(p => /^\d+$/.test(p)).map(Number);
465
+
466
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
467
+ const p1 = parts1[i] || 0;
468
+ const p2 = parts2[i] || 0;
469
+ if (p1 !== p2) return p1 - p2;
470
+ }
471
+ return 0;
472
+ }
473
+
474
+ generateReport(result: DependencyAnalysisResult): string {
475
+ const lines: string[] = [];
476
+ lines.push("");
477
+ lines.push(`${Logger.C.cyan}══════════════════════════════════════════════════════════${Logger.C.reset}`);
478
+ lines.push(`${Logger.C.bold}📊 ANÁLISE DE DEPENDÊNCIAS${Logger.C.reset}`);
479
+ lines.push(`${Logger.C.cyan}══════════════════════════════════════════════════════════${Logger.C.reset}`);
480
+ lines.push("");
481
+
482
+ // Estatísticas
483
+ lines.push(`${Logger.C.dim}Estatísticas:${Logger.C.reset}`);
484
+ lines.push(` Total: ${result.stats.total} dependências`);
485
+ lines.push(` Diretas: ${result.stats.direct} | Transitivas: ${result.stats.transitive}`);
486
+ lines.push("");
487
+
488
+ // Conflitos
489
+ if (result.conflicts.length > 0) {
490
+ lines.push(`${Logger.C.yellow}⚠️ CONFLITOS DE VERSÃO (${result.conflicts.length})${Logger.C.reset}`);
491
+ for (const conflict of result.conflicts) {
492
+ const icon = conflict.severity === "error" ? "✖" : "▲";
493
+ const color = conflict.severity === "error" ? Logger.C.red : Logger.C.yellow;
494
+ lines.push(` ${color}${icon}${Logger.C.reset} ${conflict.groupId}:${conflict.artifactId}`);
495
+ lines.push(` Versões: ${conflict.versions.join(", ")}`);
496
+ }
497
+ lines.push("");
498
+ }
499
+
500
+ // Atualizações
501
+ if (result.updates.length > 0) {
502
+ const majorUpdates = result.updates.filter(u => u.isMajor);
503
+ const minorUpdates = result.updates.filter(u => !u.isMajor);
504
+
505
+ if (minorUpdates.length > 0) {
506
+ lines.push(`${Logger.C.green}⬆️ ATUALIZAÇÕES DISPONÍVEIS (${minorUpdates.length})${Logger.C.reset}`);
507
+ for (const update of minorUpdates.slice(0, 5)) {
508
+ lines.push(` ${Logger.C.green}↑${Logger.C.reset} ${update.groupId}:${update.artifactId}`);
509
+ lines.push(` ${update.currentVersion} → ${Logger.C.green}${update.latestVersion}${Logger.C.reset}`);
510
+ }
511
+ if (minorUpdates.length > 5) {
512
+ lines.push(` ${Logger.C.dim}... e mais ${minorUpdates.length - 5}${Logger.C.reset}`);
513
+ }
514
+ lines.push("");
515
+ }
516
+
517
+ if (majorUpdates.length > 0) {
518
+ lines.push(`${Logger.C.yellow}⚠️ ATUALIZAÇÕES MAJOR (${majorUpdates.length})${Logger.C.reset}`);
519
+ lines.push(` ${Logger.C.dim}Podem conter breaking changes${Logger.C.reset}`);
520
+ for (const update of majorUpdates.slice(0, 3)) {
521
+ lines.push(` ${Logger.C.yellow}!${Logger.C.reset} ${update.groupId}:${update.artifactId}`);
522
+ lines.push(` ${update.currentVersion} → ${Logger.C.yellow}${update.latestVersion}${Logger.C.reset}`);
523
+ }
524
+ lines.push("");
525
+ }
526
+ }
527
+
528
+ // Resumo
529
+ if (result.conflicts.length === 0 && result.updates.length === 0) {
530
+ lines.push(`${Logger.C.green}✔ Todas as dependências estão atualizadas!${Logger.C.reset}`);
531
+ }
532
+
533
+ lines.push("");
534
+ lines.push(`${Logger.C.dim}Dica: Execute 'xavva audit' para verificar vulnerabilidades${Logger.C.reset}`);
535
+
536
+ return lines.join("\n");
537
+ }
538
+ }
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, statSync } from "fs";
1
+ import { existsSync, readdirSync, statSync, readFileSync } from "fs";
2
2
  import path from "path";
3
3
  import type { ProjectConfig } from "../types/config";
4
4
 
@@ -88,6 +88,44 @@ export class ProjectService {
88
88
  }
89
89
  }
90
90
 
91
+ getAvailableProfiles(): string[] {
92
+ const results: string[] = [];
93
+ const root = process.cwd();
94
+
95
+ if (this.config.buildTool === 'maven') {
96
+ const pomPath = path.join(root, "pom.xml");
97
+ if (existsSync(pomPath)) {
98
+ try {
99
+ const content = readFileSync(pomPath, "utf8");
100
+ // Regex simples para capturar IDs de profiles no pom.xml
101
+ const profileRegex = /<profile>[\s\S]*?<id>(.*?)<\/id>/g;
102
+ let match;
103
+ while ((match = profileRegex.exec(content)) !== null) {
104
+ results.push(match[1]);
105
+ }
106
+ } catch (e) {}
107
+ }
108
+ } else if (this.config.buildTool === 'gradle') {
109
+ const gradlePath = path.join(root, "build.gradle");
110
+ const gradleKtsPath = path.join(root, "build.gradle.kts");
111
+ const targetPath = existsSync(gradlePath) ? gradlePath : existsSync(gradleKtsPath) ? gradleKtsPath : null;
112
+
113
+ if (targetPath) {
114
+ try {
115
+ const content = readFileSync(targetPath, "utf8");
116
+ // Em Gradle, perfis costumam ser tratados via propriedades ou tasks de ambiente
117
+ // Vamos procurar por padrões comuns como "if (project.hasProperty('profile'))"
118
+ // ou simplesmente sugerir o uso de -P
119
+ if (content.includes("project.hasProperty('profile')") || content.includes("-Pprofile")) {
120
+ results.push("(Detectado uso dinâmico de -Pprofile)");
121
+ }
122
+ } catch (e) {}
123
+ }
124
+ }
125
+
126
+ return results;
127
+ }
128
+
91
129
  findAllClassPaths(): string[] {
92
130
  const results: string[] = [];
93
131
  const root = process.cwd();
@@ -2,6 +2,9 @@ import type { TomcatConfig, AppConfig } from "../types/config";
2
2
  import { Logger } from "../utils/ui";
3
3
  import type { Subprocess } from "bun";
4
4
  import { ProjectService } from "./ProjectService";
5
+ import { existsSync, mkdirSync, writeFileSync, promises as fs } from "fs";
6
+ import path from "path";
7
+ import os from "os";
5
8
 
6
9
  export class TomcatService {
7
10
  private activeConfig: TomcatConfig;
@@ -43,9 +46,6 @@ export class TomcatService {
43
46
  }
44
47
 
45
48
  async clearWebapps() {
46
- const fs = require("fs").promises;
47
- const { existsSync } = require("fs");
48
- const path = require("path");
49
49
  const webappsPath = path.join(this.activeConfig.path, "webapps");
50
50
  const workPath = path.join(this.activeConfig.path, "work");
51
51
  const tempPath = path.join(this.activeConfig.path, "temp");
@@ -95,9 +95,6 @@ export class TomcatService {
95
95
  }
96
96
 
97
97
  private async ensureHotswapAgent(): Promise<string | null> {
98
- const fs = require("fs");
99
- const path = require("path");
100
- const os = require("os");
101
98
  const agentDir = path.join(os.homedir(), ".xavva", "agents");
102
99
  const agentPath = path.join(agentDir, "hotswap-agent-2.0.3.jar");
103
100
 
@@ -134,7 +131,7 @@ export class TomcatService {
134
131
 
135
132
  let javaBin = "java";
136
133
  if (process.env.JAVA_HOME) {
137
- javaBin = require("path").join(process.env.JAVA_HOME, "bin", "java.exe");
134
+ javaBin = path.join(process.env.JAVA_HOME, "bin", "java.exe");
138
135
  }
139
136
 
140
137
  const javaVer = Bun.spawnSync([javaBin, "-version"]);
@@ -159,14 +156,13 @@ export class TomcatService {
159
156
  "--add-opens=java.desktop/java.beans=ALL-UNNAMED"
160
157
  );
161
158
 
162
- const fs = require("fs");
163
- const path = require("path");
159
+
164
160
  const xavvaDir = path.join(process.cwd(), ".xavva");
165
- if (!fs.existsSync(xavvaDir)) fs.mkdirSync(xavvaDir, { recursive: true });
161
+ if (!existsSync(xavvaDir)) mkdirSync(xavvaDir, { recursive: true });
166
162
 
167
163
  const propsPath = path.join(xavvaDir, "hotswap-agent.properties");
168
164
  const propsContent = `autoHotswap=true\nautoHotswap.delay=500\nwatchResources=false\nLOGGER=info`;
169
- fs.writeFileSync(propsPath, propsContent);
165
+ writeFileSync(propsPath, propsContent);
170
166
 
171
167
  catalinaOpts.push(`-Dhotswap-agent.properties.path=${propsPath}`);
172
168
 
@@ -186,6 +182,10 @@ export class TomcatService {
186
182
  "-Dorg.apache.jasper.compiler.classdebuginfo=true"
187
183
  );
188
184
 
185
+ if (config.project.encoding) {
186
+ catalinaOpts.push(`-Dfile.encoding=${config.project.encoding}`);
187
+ }
188
+
189
189
  if (config.project.skipScan) {
190
190
  catalinaOpts.push(
191
191
  "-Dtomcat.util.scan.StandardJarScanFilter.jarsToSkip=*.jar",
@@ -1,6 +1,7 @@
1
1
  import { watch } from "fs";
2
2
  import { Logger } from "../utils/ui";
3
3
  import { DeployCommand } from "../commands/DeployCommand";
4
+ import { WATCHER_DEBOUNCE_MS, WATCHER_COOLING_MS } from "../utils/constants";
4
5
  import type { AppConfig } from "../types/config";
5
6
 
6
7
  export class WatcherService {
@@ -19,7 +20,7 @@ export class WatcherService {
19
20
 
20
21
  if (this.coolingFiles.has(filename)) return;
21
22
  this.coolingFiles.add(filename);
22
- setTimeout(() => this.coolingFiles.delete(filename), 500);
23
+ setTimeout(() => this.coolingFiles.delete(filename), WATCHER_COOLING_MS);
23
24
 
24
25
  if (this.isIgnored(filename)) return;
25
26
 
@@ -47,7 +48,7 @@ export class WatcherService {
47
48
  this.debounceTimer = setTimeout(() => {
48
49
  this.run(this.pendingFullBuild ? false : true);
49
50
  this.pendingFullBuild = false;
50
- }, 1000);
51
+ }, WATCHER_DEBOUNCE_MS);
51
52
  });
52
53
  }
53
54
 
@@ -19,6 +19,7 @@ export interface ProjectConfig {
19
19
  cleanLogs: boolean;
20
20
  grep?: string;
21
21
  tui: boolean;
22
+ encoding?: string;
22
23
  }
23
24
 
24
25
  export interface AppConfig {
@@ -31,6 +32,7 @@ export interface CLIArguments {
31
32
  tool?: string;
32
33
  name?: string;
33
34
  port?: string;
35
+ encoding?: string;
34
36
  "no-build"?: boolean;
35
37
  scan?: boolean;
36
38
  clean?: boolean;
@@ -46,6 +48,8 @@ export interface CLIArguments {
46
48
  fix?: boolean;
47
49
  incremental?: boolean;
48
50
  tui?: boolean;
51
+ output?: string;
52
+ strict?: boolean;
49
53
  }
50
54
 
51
55
  export interface CommandContext {