@archznn/xavva 2.6.0 → 2.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.
package/src/index.ts CHANGED
@@ -1,115 +1,155 @@
1
1
  #!/usr/bin/env bun
2
2
  import { ConfigManager } from "./utils/config";
3
3
  import { CommandRegistry } from "./commands/CommandRegistry";
4
- import { BuildCommand } from "./commands/BuildCommand";
5
- import { DeployCommand } from "./commands/DeployCommand";
6
- import { StartCommand } from "./commands/StartCommand";
7
- import { HelpCommand } from "./commands/HelpCommand";
8
- import { DoctorCommand } from "./commands/DoctorCommand";
9
- import { RunCommand } from "./commands/RunCommand";
10
- import { LogsCommand } from "./commands/LogsCommand";
11
- import { DocsCommand } from "./commands/DocsCommand";
12
- import { AuditCommand } from "./commands/AuditCommand";
13
- import { ProfilesCommand } from "./commands/ProfilesCommand";
14
- import { DepsCommand } from "./commands/DepsCommand";
15
- import { TomcatCommand } from "./commands/TomcatCommand";
16
-
17
- import { ProjectService } from "./services/ProjectService";
18
- import { TomcatService } from "./services/TomcatService";
19
- import { BuildService } from "./services/BuildService";
20
- import { AuditService } from "./services/AuditService";
21
- import { WatcherService } from "./services/WatcherService";
22
- import { BuildCacheService } from "./services/BuildCacheService";
23
- import { DashboardService } from "./services/DashboardService";
24
- import { LogAnalyzer } from "./services/LogAnalyzer";
25
-
4
+ import { createContainer, type DIContainer } from "./di/container";
5
+ import { DeployWatcher } from "./services/DeployWatcher";
6
+ import { ErrorHandler } from "./errors/ErrorHandler";
7
+ import { ProcessManager } from "./utils/processManager";
26
8
  import pkg from "../package.json";
27
9
  import { Logger } from "./utils/ui";
28
- import { ProcessManager } from "./utils/processManager";
29
- import type { AppConfig, CLIArguments } from "./types/config";
10
+ import type { CLIArguments } from "./types/args";
30
11
 
31
12
  async function main() {
32
- const processManager = ProcessManager.getInstance();
33
- const { config, positionals, values } = await ConfigManager.load();
13
+ const processManager = ProcessManager.getInstance();
14
+ const { config, positionals, values } = await ConfigManager.load();
15
+
16
+ // Handler de versão
17
+ if (values.version) {
18
+ Logger.log(`v${pkg.version}`);
19
+ await processManager.shutdown(0);
20
+ }
21
+
22
+ // Identifica comando
23
+ const commandNames = [
24
+ "deploy", "build", "start", "dev", "doctor", "run",
25
+ "debug", "logs", "docs", "audit", "profiles",
26
+ "deps", "tomcat", "encoding", "init", "config",
27
+ "history", "redo", "health", "completion", "help"
28
+ ];
29
+ const commandName = positionals.find(p => commandNames.includes(p)) || "deploy";
30
+
31
+ // Mostra banner (exceto em help ou TUI)
32
+ if (!values.help && !values.tui) {
33
+ Logger.banner(commandName, config.project.profile, config.project.encoding);
34
+ if (config.project.encoding) {
35
+ Logger.config("Encoding", config.project.encoding);
36
+ }
37
+ if (config.tomcat.embedded) {
38
+ Logger.config("Tomcat", `Embutido ${config.tomcat.version}`);
39
+ }
40
+ }
41
+
42
+ // Handler de help
43
+ if (values.help) {
44
+ const { HelpCommand } = await import("./commands/HelpCommand");
45
+ new HelpCommand().execute(config, values as CLIArguments);
46
+ await processManager.shutdown(0);
47
+ }
34
48
 
35
- if (values.version) {
36
- Logger.log(`v${pkg.version}`);
37
- await processManager.shutdown(0);
38
- }
49
+ // Inicializa Container de DI
50
+ let container: DIContainer;
51
+ try {
52
+ container = createContainer(config);
53
+ container.initialize();
54
+ } catch (error) {
55
+ await ErrorHandler.getInstance().handle(error, { phase: "di-initialization" });
56
+ return;
57
+ }
39
58
 
40
- const commandNames = ["deploy", "build", "start", "dev", "doctor", "run", "debug", "logs", "docs", "audit", "profiles", "deps", "tomcat"];
41
- const commandName = positionals.find(p => commandNames.includes(p)) || "deploy";
59
+ const services = container.getAllServices();
60
+ const commands = container.getAllCommands();
42
61
 
43
- if (!values.help && !values.tui) {
44
- Logger.banner(commandName, config.project.profile, config.project.encoding);
45
- if (config.project.encoding) {
46
- Logger.config("Encoding", config.project.encoding);
47
- }
48
- if (config.tomcat.embedded) {
49
- Logger.config("Tomcat", `Embutido ${config.tomcat.version}`);
50
- }
51
- }
62
+ // Configura ações da TUI
63
+ if (values.tui) {
64
+ const deployCmd = commands.deploy;
65
+ services.dashboardService.onAction("r", () => {
66
+ services.dashboardService.log(Logger.C.warning + "Restart manual solicitado via TUI...");
67
+ deployCmd.execute(config, { watch: true, incremental: false });
68
+ });
69
+ }
52
70
 
53
- if (values.help) {
54
- new HelpCommand().execute(config, values);
55
- await processManager.shutdown(0);
56
- }
71
+ // Caso especial: Watch Mode para Deploy/Dev
72
+ if ((commandName === "deploy" || commandName === "dev") && values.watch) {
73
+ const deployCmd = commands.deploy;
74
+ const watcher = new DeployWatcher(config, deployCmd);
75
+
76
+ try {
77
+ await watcher.start();
78
+ } catch (error) {
79
+ await ErrorHandler.getInstance().handle(error, { phase: "watch-mode", command: commandName });
80
+ }
81
+ } else {
82
+ // Executa comando do Registry
83
+ const registry = new CommandRegistry();
84
+
85
+ // Registra todos os comandos
86
+ registry.register("build", commands.build);
87
+ registry.register("start", commands.start);
88
+ registry.register("doctor", commands.doctor);
89
+ registry.register("run", commands.run);
90
+ registry.register("debug", commands.debug);
91
+ registry.register("logs", commands.logs);
92
+ registry.register("docs", commands.docs);
93
+ registry.register("audit", commands.audit);
94
+ registry.register("profiles", commands.profiles);
95
+ registry.register("deps", commands.deps);
96
+ registry.register("tomcat", commands.tomcat);
97
+ registry.register("encoding", commands.encoding);
98
+ registry.register("deploy", commands.deploy);
99
+ registry.register("dev", commands.dev);
100
+ registry.register("init", commands.init);
101
+ registry.register("config", commands.config);
102
+ registry.register("history", commands.history);
103
+ registry.register("redo", commands.redo);
104
+ registry.register("health", commands.health);
105
+ registry.register("completion", commands.completion);
57
106
 
58
- // 1. Instanciar Serviços (Injeção de Dependência)
59
- const projectService = new ProjectService(config.project);
60
- const buildCacheService = new BuildCacheService();
61
- const buildService = new BuildService(config.project, config.tomcat, projectService, buildCacheService);
62
- const tomcatService = new TomcatService(config.tomcat);
63
- tomcatService.setProjectService(projectService);
64
- const auditService = new AuditService(config.tomcat);
65
-
66
- // Xavva 2.0: Dashboard & LogAnalyzer
67
- const logAnalyzer = new LogAnalyzer(config.project);
68
- const dashboard = new DashboardService(config);
69
- Logger.setDashboard(dashboard);
107
+ // Configura flags específicas
108
+ if (commandName === "debug") values.debug = true;
109
+ if (commandName === "run") values.debug = false;
70
110
 
71
- // 2. Registrar Comandos
72
- const registry = new CommandRegistry();
73
-
74
- const deployCmd = new DeployCommand(tomcatService, buildService);
75
- const logsCmd = new LogsCommand(dashboard, logAnalyzer);
76
-
77
- registry.register("build", new BuildCommand(buildService));
78
- registry.register("start", new StartCommand(tomcatService));
79
- registry.register("doctor", new DoctorCommand());
80
- registry.register("run", new RunCommand());
81
- registry.register("debug", new RunCommand());
82
- registry.register("logs", logsCmd);
83
- registry.register("docs", new DocsCommand());
84
- registry.register("audit", new AuditCommand(auditService));
85
- registry.register("profiles", new ProfilesCommand(projectService));
86
- registry.register("deps", new DepsCommand());
87
- registry.register("tomcat", new TomcatCommand());
88
- registry.register("deploy", deployCmd);
89
- registry.register("dev", deployCmd);
111
+ // Registra comando no histórico antes da execução
112
+ const startTime = Date.now();
113
+ let success = true;
90
114
 
91
- // Caso especial: Watch Mode para Deploy/Dev
92
- if ((commandName === "deploy" || commandName === "dev") && values.watch) {
93
- // Registrar ação de restart manual na TUI
94
- if (dashboard.isTuiActive()) {
95
- dashboard.onAction("r", () => {
96
- dashboard.log(Logger.C.warning + "Restart manual solicitado via TUI...");
97
- deployCmd.execute(config, false, true); // Executa deploy completo mas mantém o watch
98
- });
99
- }
115
+ try {
116
+ await registry.execute(commandName, config, values as CLIArguments, positionals);
117
+ } catch (error) {
118
+ success = false;
119
+ await ErrorHandler.getInstance().handle(error, { phase: "command-execution", command: commandName });
120
+ } finally {
121
+ // Salva no histórico
122
+ const duration = (Date.now() - startTime) / 1000;
123
+ const filteredPositionals = positionals.filter(p => p !== commandName && !commandNames.includes(p));
124
+ services.historyService.add({
125
+ command: commandName,
126
+ args: [...filteredPositionals, ...Object.entries(values)
127
+ .filter(([, v]) => v !== undefined && typeof v !== "object")
128
+ .flatMap(([k, v]) => [`--${k}`, String(v)])],
129
+ success,
130
+ duration
131
+ }).catch(() => { /* ignore history errors */ });
100
132
 
101
- const watcher = new WatcherService(config, deployCmd);
102
- await watcher.start();
103
- } else {
104
- // 3. Executar do Registro
105
- if (commandName === "debug") values.debug = true;
106
- if (commandName === "run") values.debug = false;
107
-
108
- await registry.execute(commandName, config, values, positionals);
109
- }
133
+ // Envia notificação para comandos longos
134
+ if (duration > 5 && commandName !== "logs" && commandName !== "history") {
135
+ const { NotificationService } = await import("./services/NotificationService");
136
+ if (success) {
137
+ if (commandName === "build" || commandName === "deploy") {
138
+ NotificationService.buildSuccess(duration);
139
+ } else if (commandName === "start") {
140
+ NotificationService.deployComplete(config.project.appName);
141
+ }
142
+ } else {
143
+ NotificationService.buildFailed(`Comando ${commandName} falhou`);
144
+ }
145
+ }
146
+ }
147
+ }
110
148
  }
111
149
 
150
+ // Entry point com tratamento global de erros
112
151
  main().catch(async (error) => {
113
- console.error(error);
114
- await ProcessManager.getInstance().shutdown(1);
152
+ // Erro não tratado - possível bug
153
+ console.error("Erro fatal não tratado:", error);
154
+ await ProcessManager.getInstance().shutdown(1);
115
155
  });
@@ -1,7 +1,8 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import type { TomcatConfig } from "../types/config";
3
+ import type { TomcatConfig } from "../types";
4
4
  import { Logger } from "../utils/ui";
5
+ import { AuditError } from "../errors/XavvaError";
5
6
 
6
7
  export interface Vulnerability {
7
8
  id: string;
@@ -125,7 +126,7 @@ export class AuditService {
125
126
 
126
127
  return { groupId, artifactId, version };
127
128
  } catch (e) {
128
- return {};
129
+ throw new AuditError(`Falha ao extrair informações do JAR: ${(e as Error).message}`);
129
130
  }
130
131
  }
131
132
 
@@ -2,30 +2,83 @@ import { Logger } from "../utils/ui";
2
2
  import { isWindows, getOpenBrowserArgs } from "../utils/platform";
3
3
 
4
4
  export class BrowserService {
5
+ private static debugPort = 9222;
6
+
5
7
  /**
6
- * Recarrega a aba ativa do browser (Chrome ou Edge).
7
- * Nota: No Linux/Mac, esta funcionalidade é limitada devido às restrições
8
- * de automação de GUI. Recomenda-se usar extensões de Live Reload.
8
+ * Recarrega a aba do browser que corresponde à URL da aplicação.
9
+ * Usa o Chrome DevTools Protocol (CDP) para encontrar e recarregar a aba correta.
10
+ * Nota: No Linux/Mac, esta funcionalidade é limitada.
9
11
  */
10
12
  public static async reload(url: string) {
11
13
  // Pequeno delay para garantir que o Tomcat processou o novo contexto
12
14
  await new Promise(r => setTimeout(r, 800));
13
15
 
14
16
  if (!isWindows()) {
15
- // No Linux/Mac, tenta notificar via Browser Sync ou similar se disponível
16
- // Por enquanto, apenas loga (o usuário pode usar extensões de Live Reload)
17
17
  return;
18
18
  }
19
19
 
20
+ // Usar heredoc para evitar problemas de escaping
20
21
  const psCommand = `
21
- $shell = New-Object -ComObject WScript.Shell
22
- $process = Get-Process | Where-Object { $_.MainWindowTitle -match "Chrome" -or $_.MainWindowTitle -match "Edge" } | Select-Object -First 1
23
- if ($process) {
24
- $shell.AppActivate($process.Id)
25
- Sleep -m 100
26
- $shell.SendKeys("{F5}")
27
- }
28
- `;
22
+ $ErrorActionPreference = 'SilentlyContinue'
23
+ $debugPort = ${this.debugPort}
24
+
25
+ function Reload-TabByUrl {
26
+ param($targetUrl, $port)
27
+
28
+ try {
29
+ $pages = Invoke-RestMethod -Uri "http://localhost:$port/json/list" -TimeoutSec 2
30
+ $targetUri = [System.Uri]$targetUrl
31
+ $targetPath = $targetUri.AbsolutePath.TrimEnd('/')
32
+
33
+ foreach ($page in $pages) {
34
+ if ($page.type -ne "page") { continue }
35
+
36
+ try {
37
+ $pageUri = [System.Uri]$page.url
38
+ if ($pageUri.Host -eq $targetUri.Host -and
39
+ $pageUri.AbsolutePath.TrimEnd('/') -eq $targetPath) {
40
+
41
+ $body = @{ id = 1; method = "Page.reload"; params = @{ ignoreCache = $false } } | ConvertTo-Json -Compress
42
+ Invoke-RestMethod -Uri "http://localhost:$port/json/reload/$($page.id)" -Method Put -Body $body -ContentType "application/json" -TimeoutSec 2 | Out-Null
43
+ return $true
44
+ }
45
+ } catch { continue }
46
+ }
47
+ } catch { }
48
+ return $false
49
+ }
50
+
51
+ $reloaded = Reload-TabByUrl -targetUrl "${url}" -port $debugPort
52
+
53
+ if (-not $reloaded) {
54
+ $shell = New-Object -ComObject WScript.Shell
55
+ $urlObj = [System.Uri]"${url}"
56
+ $context = $urlObj.AbsolutePath.Trim('/').Split('/')[0]
57
+
58
+ $processes = Get-Process | Where-Object {
59
+ ($_.Name -eq "chrome" -or $_.Name -eq "msedge") -and
60
+ $_.MainWindowTitle -and
61
+ ($_.MainWindowTitle -match $context -or $_.MainWindowTitle -match "localhost")
62
+ }
63
+
64
+ if ($processes) {
65
+ $targetProcess = $processes | Select-Object -First 1
66
+ $shell.AppActivate($targetProcess.Id)
67
+ Start-Sleep -Milliseconds 100
68
+ $shell.SendKeys("{F5}")
69
+ } else {
70
+ $anyBrowser = Get-Process | Where-Object {
71
+ $_.Name -eq "chrome" -or $_.Name -eq "msedge"
72
+ } | Select-Object -First 1
73
+
74
+ if ($anyBrowser) {
75
+ $shell.AppActivate($anyBrowser.Id)
76
+ Start-Sleep -Milliseconds 100
77
+ $shell.SendKeys("{F5}")
78
+ }
79
+ }
80
+ }
81
+ `;
29
82
 
30
83
  try {
31
84
  Bun.spawn(["powershell", "-command", psCommand]);
@@ -35,10 +88,68 @@ export class BrowserService {
35
88
  }
36
89
 
37
90
  /**
38
- * Abre a URL no browser padrão do sistema.
91
+ * Abre a URL no browser com remote debugging habilitado (se possível).
92
+ * Isso permite recarregar a aba específica posteriormente via CDP.
39
93
  */
40
94
  public static open(url: string) {
41
- const args = getOpenBrowserArgs(url);
42
- Bun.spawn(args);
95
+ if (!isWindows()) {
96
+ const args = getOpenBrowserArgs(url);
97
+ Bun.spawn(args);
98
+ return;
99
+ }
100
+
101
+ const psCommand = `
102
+ $ErrorActionPreference = 'SilentlyContinue'
103
+ $debugPort = ${this.debugPort}
104
+ $targetUrl = "${url}"
105
+
106
+ $debuggingEnabled = $false
107
+ try {
108
+ $resp = Invoke-RestMethod -Uri "http://localhost:$debugPort/json/version" -TimeoutSec 1
109
+ $debuggingEnabled = $true
110
+ } catch { }
111
+
112
+ # Encontra o executável do Chrome ou Edge
113
+ $browserExe = $null
114
+ $chromePaths = @(
115
+ ("$env:ProgramFiles" + "\\Google\\Chrome\\Application\\chrome.exe"),
116
+ ("$env:LOCALAPPDATA" + "\\Google\\Chrome\\Application\\chrome.exe")
117
+ )
118
+ $edgePaths = @(
119
+ ("$env:ProgramFiles" + "\\Microsoft\\Edge\\Application\\msedge.exe")
120
+ )
121
+
122
+ foreach ($path in $chromePaths) {
123
+ if (Test-Path $path) { $browserExe = $path; break }
124
+ }
125
+ if (-not $browserExe) {
126
+ foreach ($path in $edgePaths) {
127
+ if (Test-Path $path) { $browserExe = $path; break }
128
+ }
129
+ }
130
+
131
+ if ($browserExe) {
132
+ if ($debuggingEnabled) {
133
+ try {
134
+ $body = @{ url = $targetUrl } | ConvertTo-Json -Compress
135
+ Invoke-RestMethod -Uri "http://localhost:$debugPort/json/new" -Method Put -Body $body -ContentType "application/json" -TimeoutSec 3 | Out-Null
136
+ } catch {
137
+ Start-Process $browserExe -ArgumentList $targetUrl
138
+ }
139
+ } else {
140
+ Start-Process $browserExe -ArgumentList $targetUrl, "--remote-debugging-port=$debugPort", "--no-first-run"
141
+ }
142
+ } else {
143
+ Start-Process $targetUrl
144
+ }
145
+ `;
146
+
147
+ try {
148
+ Bun.spawn(["powershell", "-command", psCommand]);
149
+ } catch (e) {
150
+ // Fallback para método padrão
151
+ const args = getOpenBrowserArgs(url);
152
+ Bun.spawn(args);
153
+ }
43
154
  }
44
155
  }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * DeployWatcher - Específico para watch de deploy
3
+ * Usa FileWatcher genérico e adiciona lógica de deploy
4
+ */
5
+
6
+ import { FileWatcher, type FileChangeEvent } from "./FileWatcher";
7
+ import { DeployCommand } from "../commands/DeployCommand";
8
+ import { Logger } from "../utils/ui";
9
+ import type { AppConfig } from "../types/config";
10
+ import { WATCHER_DEBOUNCE_MS, WATCHER_COOLING_MS } from "../utils/constants";
11
+
12
+ export class DeployWatcher {
13
+ private fileWatcher: FileWatcher;
14
+ private isDeploying = false;
15
+ private pendingFullBuild = false;
16
+ private modifiedFiles = new Set<string>();
17
+ private pendingFiles = new Set<string>();
18
+ private hasPendingChanges = false;
19
+
20
+ constructor(
21
+ private config: AppConfig,
22
+ private deployCmd: DeployCommand
23
+ ) {
24
+ this.fileWatcher = new FileWatcher({
25
+ recursive: true,
26
+ debounceMs: WATCHER_DEBOUNCE_MS,
27
+ coolingMs: WATCHER_COOLING_MS,
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Inicia o watch de deploy
33
+ */
34
+ async start(): Promise<void> {
35
+ // Executa deploy inicial
36
+ await this.run(false);
37
+
38
+ // Configura handlers
39
+ this.setupHandlers();
40
+
41
+ // Inicia o watcher
42
+ this.fileWatcher.start();
43
+
44
+ Logger.info("DeployWatcher", "Monitorando alterações...");
45
+ }
46
+
47
+ /**
48
+ * Para o watching
49
+ */
50
+ stop(): void {
51
+ this.fileWatcher.stop();
52
+ }
53
+
54
+ /**
55
+ * Configura handlers para diferentes tipos de arquivos
56
+ */
57
+ private setupHandlers(): void {
58
+ // Handler para configurações de build
59
+ this.fileWatcher.on(/(pom\.xml|build\.gradle|build\.gradle\.kts)$/, (event) => {
60
+ this.handleBuildConfigChange(event);
61
+ });
62
+
63
+ // Handler para arquivos Java
64
+ this.fileWatcher.on(/\.java$/, (event) => {
65
+ this.handleJavaChange(event);
66
+ });
67
+
68
+ // Handler para recursos estáticos (JSP, HTML, CSS, etc)
69
+ this.fileWatcher.on(/\.(jsp|html|css|js|xml|properties)$/, (event) => {
70
+ this.handleResourceChange(event);
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Trata mudança em arquivo de configuração de build
76
+ */
77
+ private async handleBuildConfigChange(event: FileChangeEvent): Promise<void> {
78
+ if (!event.filename) return;
79
+
80
+ Logger.watcher(`Build configuration changed: ${event.filename}`, 'warn');
81
+
82
+ // Limpa cache quando config muda
83
+ const { BuildCacheService } = await import("./BuildCacheService");
84
+ new BuildCacheService().clearCache();
85
+
86
+ this.pendingFullBuild = true;
87
+ }
88
+
89
+ /**
90
+ * Trata mudança em arquivo Java
91
+ */
92
+ private handleJavaChange(event: FileChangeEvent): void {
93
+ if (!event.filename || this.isDeploying) {
94
+ if (event.filename) {
95
+ this.pendingFiles.add(event.filename);
96
+ this.hasPendingChanges = true;
97
+ }
98
+ return;
99
+ }
100
+
101
+ Logger.watcher(event.filename, 'watch');
102
+ this.modifiedFiles.add(event.filename);
103
+
104
+ // Debounce para acumular múltiplas mudanças
105
+ this.scheduleDeploy();
106
+ }
107
+
108
+ /**
109
+ * Trata mudança em recurso estático
110
+ */
111
+ private async handleResourceChange(event: FileChangeEvent): Promise<void> {
112
+ if (!event.filename) return;
113
+
114
+ Logger.watcher(event.filename, 'resource');
115
+
116
+ try {
117
+ await this.deployCmd.syncResource(this.config, event.filename);
118
+ } catch (error) {
119
+ Logger.error(`Falha ao sincronizar recurso: ${event.filename}`);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Agenda deploy após debounce
125
+ */
126
+ private scheduleDeploy(): void {
127
+ // O debounce já é feito pelo FileWatcher
128
+ // Aqui apenas executamos o deploy
129
+ const filesToCompile = [...this.modifiedFiles];
130
+ this.modifiedFiles.clear();
131
+
132
+ this.run(this.pendingFullBuild ? false : true, filesToCompile);
133
+ this.pendingFullBuild = false;
134
+ }
135
+
136
+ /**
137
+ * Executa o deploy
138
+ */
139
+ private async run(incremental = false, changedFiles?: string[]): Promise<void> {
140
+ if (this.isDeploying) return;
141
+
142
+ this.isDeploying = true;
143
+
144
+ try {
145
+ await this.deployCmd.execute(this.config, {
146
+ watch: true,
147
+ incremental,
148
+ changedFiles,
149
+ });
150
+ } catch (error) {
151
+ // Erro já é tratado pelo comando
152
+ } finally {
153
+ this.isDeploying = false;
154
+
155
+ // Processa mudanças pendentes
156
+ if (this.hasPendingChanges) {
157
+ const pending = [...this.pendingFiles];
158
+ this.pendingFiles.clear();
159
+ this.hasPendingChanges = false;
160
+
161
+ Logger.watcher(`Processing ${pending.length} pending change(s)...`, 'warn');
162
+
163
+ setTimeout(() => {
164
+ this.run(true, pending);
165
+ }, 100);
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Verifica se é arquivo de recurso (não Java)
172
+ */
173
+ static isResourceFile(filename: string): boolean {
174
+ return /\.(jsp|html|css|js|xml|properties)$/.test(filename);
175
+ }
176
+
177
+ /**
178
+ * Verifica se é arquivo de configuração de build
179
+ */
180
+ static isBuildConfig(filename: string): boolean {
181
+ return /^(pom\.xml|build\.gradle|build\.gradle\.kts)$/.test(filename);
182
+ }
183
+ }