@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
@@ -4,7 +4,7 @@ import type { Command } from "./Command";
4
4
  import type { AppConfig, CLIArguments } from "../types/config";
5
5
  import { BuildService } from "../services/BuildService";
6
6
  import { TomcatService } from "../services/TomcatService";
7
- import { Logger } from "../utils/ui";
7
+ import { Logger, OperationLogger } from "../logging";
8
8
  import { EndpointService } from "../services/EndpointService";
9
9
  import { BrowserService } from "../services/BrowserService";
10
10
  import {
@@ -14,260 +14,283 @@ import {
14
14
  } from "../utils/platform";
15
15
 
16
16
  export class DeployCommand implements Command {
17
- constructor(private tomcat: TomcatService, private builder: BuildService) {}
17
+ private logger = Logger.getInstance();
18
18
 
19
- async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
20
- const incremental = args?.watch && args?.incremental;
21
- const isWatching = !!args?.watch;
22
- const changedFiles = args?.changedFiles;
23
- const tomcat = this.tomcat;
24
- const builder = this.builder;
19
+ constructor(private tomcat: TomcatService, private builder: BuildService) {}
25
20
 
26
- if (!incremental) {
27
- this.logConfiguration(config, isWatching);
28
- } else {
29
- Logger.watch("Change detected");
30
- }
31
-
32
- try {
33
- const contextPath = (config.project.appName || "").replace(".war", "");
21
+ async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
22
+ const incremental = args?.watch && args?.incremental;
23
+ const isWatching = !!args?.watch;
24
+ const changedFiles = args?.changedFiles;
25
+ const tomcat = this.tomcat;
26
+ const builder = this.builder;
34
27
 
35
- if (!incremental) {
36
- await tomcat.killConflict();
37
- await tomcat.clearWebapps();
28
+ // Cria operação para rastreamento
29
+ const operation = new OperationLogger(incremental ? 'hot-reload' : 'deploy');
38
30
 
39
- if (!config.project.skipBuild) {
40
- Logger.build("compiling...");
41
- await builder.runBuild(incremental);
42
- }
43
-
44
- if (!config.project.skipBuild) {
45
- Logger.build("completed");
46
- }
47
- } else {
48
- if (!config.project.skipBuild) {
49
- Logger.build("incremental compile...");
50
- await builder.runBuild(incremental);
51
- }
52
- }
31
+ if (!incremental) {
32
+ this.logConfiguration(config, isWatching);
33
+ } else {
34
+ this.logger.debug("Mudança detectada");
35
+ }
36
+
37
+ try {
38
+ const contextPath = (config.project.appName || "").replace(".war", "");
53
39
 
54
- if (incremental) {
55
- const actualAppFolder = await builder.syncClasses(changedFiles);
56
- const actualContextPath = contextPath || actualAppFolder || "";
57
- const actualAppUrl = `http://localhost:${config.tomcat.port}/${actualContextPath}`;
58
- await BrowserService.reload(actualAppUrl);
59
- Logger.success(`redeploy completed (${changedFiles?.length || 'all'} file(s))`);
60
- return;
61
- }
40
+ if (!incremental) {
41
+ operation.start('Iniciando deploy');
42
+
43
+ await tomcat.killConflict();
44
+ await tomcat.clearWebapps();
62
45
 
63
- Logger.server("cleaning webapps...");
64
- const artifactInfo = await builder.deployToWebapps();
65
- Logger.server("artifacts ready");
66
-
67
- const finalContextPath = contextPath || artifactInfo.finalName.replace(".war", "");
68
- const appWebappPath = path.join(config.tomcat.path, "webapps", finalContextPath);
46
+ if (!config.project.skipBuild) {
47
+ const buildStep = operation.step('build', 'Compilando projeto...');
48
+ await builder.runBuild(incremental);
49
+ buildStep.success('Compilação concluída');
50
+ }
51
+ } else {
52
+ if (!config.project.skipBuild) {
53
+ this.logger.debug('Compilação incremental...');
54
+ await builder.runBuild(incremental);
55
+ }
56
+ }
69
57
 
70
- if (artifactInfo.isDirectory) {
71
- // Se é um diretório (exploded), sincronizamos o conteúdo total para a pasta do webapps
72
- if (!fs.existsSync(appWebappPath)) fs.mkdirSync(appWebappPath, { recursive: true });
73
- await builder.syncExploded(artifactInfo.path, appWebappPath);
74
- Logger.server("synced exploded directory");
75
- } else {
76
- if (!fs.existsSync(appWebappPath)) fs.mkdirSync(appWebappPath, { recursive: true });
58
+ if (incremental) {
59
+ const syncStep = operation.step('sync', 'Sincronizando classes...');
60
+ const actualAppFolder = await builder.syncClasses(changedFiles);
61
+ const actualContextPath = contextPath || actualAppFolder || "";
62
+ const actualAppUrl = `http://localhost:${config.tomcat.port}/${actualContextPath}`;
63
+ await BrowserService.reload(actualAppUrl);
64
+
65
+ const fileCount = changedFiles?.length || 0;
66
+ if (fileCount > 0) {
67
+ syncStep.success(`${fileCount} arquivo(s) sincronizado(s)`);
68
+ this.logger.success(`Hot-reload concluído`);
69
+ } else {
70
+ syncStep.success('Hot-reload concluído');
71
+ }
72
+ return;
73
+ }
77
74
 
78
- const artifactStat = fs.statSync(artifactInfo.path);
79
- const webappStat = fs.existsSync(appWebappPath) ? fs.statSync(appWebappPath) : null;
75
+ const deployStep = operation.step('deploy', 'Preparando deploy...');
76
+ const artifactInfo = await builder.deployToWebapps();
77
+
78
+ const finalContextPath = contextPath || artifactInfo.finalName.replace(".war", "");
79
+ const appWebappPath = path.join(config.tomcat.path, "webapps", finalContextPath);
80
80
 
81
- if (!webappStat || artifactStat.mtimeMs > webappStat.mtimeMs) {
82
- try {
83
- Bun.spawnSync(["jar", "xf", artifactInfo.path], { cwd: appWebappPath });
84
- Logger.server("extracted WAR");
85
- } catch (e) {
86
- // Fallback para extração com jar (funciona em todas as plataformas)
87
- if (isWindows()) {
88
- const extractCmd = `Expand-Archive -Path $env:ARTIFACT_PATH -DestinationPath $env:DEST_PATH -Force`;
89
- Bun.spawnSync(["powershell", "-command", extractCmd], {
90
- env: {
91
- ...process.env,
92
- ARTIFACT_PATH: artifactInfo.path,
93
- DEST_PATH: appWebappPath
94
- }
95
- });
96
- } else {
97
- // Linux/Mac: usa unzip ou jar
98
- try {
99
- Bun.spawnSync(["unzip", "-q", "-o", artifactInfo.path, "-d", appWebappPath]);
100
- } catch {
101
- // Fallback final para jar
102
- Bun.spawnSync(getWarExtractCommand(artifactInfo.path, appWebappPath), {
103
- cwd: appWebappPath
104
- });
105
- }
106
- }
107
- Logger.server("extracted WAR (fallback)");
108
- }
109
- } else {
110
- Logger.server("webapp up to date");
111
- }
112
- }
81
+ if (artifactInfo.isDirectory) {
82
+ // Se é um diretório (exploded), sincronizamos o conteúdo total para a pasta do webapps
83
+ if (!fs.existsSync(appWebappPath)) fs.mkdirSync(appWebappPath, { recursive: true });
84
+ await builder.syncExploded(artifactInfo.path, appWebappPath);
85
+ deployStep.update('Diretório exploded sincronizado');
86
+ } else {
87
+ if (!fs.existsSync(appWebappPath)) fs.mkdirSync(appWebappPath, { recursive: true });
113
88
 
114
- this.injectContextConfiguration(appWebappPath);
115
- this.injectHotswapProperties(appWebappPath);
89
+ const artifactStat = fs.statSync(artifactInfo.path);
90
+ const webappStat = fs.existsSync(appWebappPath) ? fs.statSync(appWebappPath) : null;
116
91
 
117
- const finalAppUrl = `http://localhost:${config.tomcat.port}/${finalContextPath}`;
118
-
119
- tomcat.onReady = async () => {
120
- await this.handleServerReady(config, finalAppUrl, finalContextPath, tomcat, !!incremental);
121
- };
92
+ if (!webappStat || artifactStat.mtimeMs > webappStat.mtimeMs) {
93
+ try {
94
+ Bun.spawnSync(["jar", "xf", artifactInfo.path], { cwd: appWebappPath });
95
+ deployStep.update('WAR extraído');
96
+ } catch (e) {
97
+ // Fallback para extração com jar (funciona em todas as plataformas)
98
+ if (isWindows()) {
99
+ const extractCmd = `Expand-Archive -Path $env:ARTIFACT_PATH -DestinationPath $env:DEST_PATH -Force`;
100
+ Bun.spawnSync(["powershell", "-command", extractCmd], {
101
+ env: {
102
+ ...process.env,
103
+ ARTIFACT_PATH: artifactInfo.path,
104
+ DEST_PATH: appWebappPath
105
+ }
106
+ });
107
+ } else {
108
+ // Linux/Mac: usa unzip ou jar
109
+ try {
110
+ Bun.spawnSync(["unzip", "-q", "-o", artifactInfo.path, "-d", appWebappPath]);
111
+ } catch {
112
+ // Fallback final para jar
113
+ Bun.spawnSync(getWarExtractCommand(artifactInfo.path, appWebappPath), {
114
+ cwd: appWebappPath
115
+ });
116
+ }
117
+ }
118
+ deployStep.update('WAR extraído (fallback)');
119
+ }
120
+ } else {
121
+ deployStep.update('Webapp já está atualizado');
122
+ }
123
+ }
124
+
125
+ deployStep.success('Deploy preparado');
122
126
 
123
- tomcat.start(config, isWatching);
124
- } catch (error) {
125
- const message = error instanceof Error ? error.message : String(error);
126
- Logger.error(message);
127
- throw error;
128
- }
129
- }
127
+ this.injectContextConfiguration(appWebappPath);
128
+ this.injectHotswapProperties(appWebappPath);
130
129
 
131
- private logConfiguration(config: AppConfig, isWatching: boolean) {
132
- Logger.section("Configuration");
133
- Logger.config("runtime", config.project.buildTool.toLowerCase());
134
- if (config.project.profile) Logger.config("profile", config.project.profile);
135
- Logger.config("watch", isWatching);
136
- Logger.config("debug", config.project.debug ? `port ${config.project.debugPort}` : false);
130
+ const finalAppUrl = `http://localhost:${config.tomcat.port}/${finalContextPath}`;
131
+
132
+ tomcat.onReady = async () => {
133
+ await this.handleServerReady(config, finalAppUrl, finalContextPath, tomcat, !!incremental);
134
+ };
137
135
 
138
- const javaVer = Bun.spawnSync([getJavaPath(), "-version"]);
139
- const output = (javaVer.stderr.toString() + javaVer.stdout.toString()).toLowerCase();
140
- const hasDcevm = ["dcevm", "jetbrains", "trava", "jbr"].some(v => output.includes(v));
141
-
142
- if (!hasDcevm && isWatching) {
143
- Logger.config("hotswap", "standard");
144
- } else if (hasDcevm) {
145
- Logger.config("hotswap", "dcevm");
146
- }
136
+ const serverStep = operation.step('server', 'Iniciando Tomcat...');
137
+ tomcat.start(config, isWatching);
138
+
139
+ // O serverStep será concluído quando o Tomcat estiver pronto
140
+ // (via callback onReady)
141
+
142
+ } catch (error) {
143
+ const message = error instanceof Error ? error.message : String(error);
144
+ this.logger.error(message);
145
+ operation.fail('Deploy falhou', error as Error);
146
+ throw error;
147
+ }
148
+ }
147
149
 
148
- const srcPath = path.join(process.cwd(), "src");
149
- if (fs.existsSync(srcPath)) {
150
- const contextPath = (config.project.appName || "").replace(".war", "");
151
- const endpoints = EndpointService.scan(srcPath, contextPath);
152
- if (endpoints.length > 0) {
153
- Logger.config("endpoints", endpoints.length);
154
- }
155
- }
156
- Logger.endSection();
157
- }
150
+ private logConfiguration(config: AppConfig, isWatching: boolean) {
151
+ this.logger.section("Configuração");
152
+ this.logger.config("runtime", config.project.buildTool.toLowerCase());
153
+ if (config.project.profile) this.logger.config("profile", config.project.profile);
154
+ this.logger.config("watch", isWatching);
155
+ this.logger.config("debug", config.project.debug ? `porta ${config.project.debugPort}` : false);
158
156
 
159
- private injectContextConfiguration(appPath: string) {
160
- const metaInfPath = path.join(appPath, "META-INF");
161
- if (!fs.existsSync(metaInfPath)) fs.mkdirSync(metaInfPath, { recursive: true });
157
+ const javaVer = Bun.spawnSync([getJavaPath(), "-version"]);
158
+ const output = (javaVer.stderr.toString() + javaVer.stdout.toString()).toLowerCase();
159
+ const hasDcevm = ["dcevm", "jetbrains", "trava", "jbr"].some(v => output.includes(v));
160
+
161
+ if (!hasDcevm && isWatching) {
162
+ this.logger.config("hotswap", "padrão");
163
+ } else if (hasDcevm) {
164
+ this.logger.config("hotswap", "dcevm (avançado)");
165
+ }
162
166
 
163
- const contextPath = path.join(metaInfPath, "context.xml");
164
-
165
- // Aumentamos o cache para 100MB (102400 KB) para evitar avisos de cache insuficiente
166
- const contextContent = `<?xml version="1.0" encoding="UTF-8"?>\n<Context>\n <Resources cachingAllowed="true" cacheMaxSize="102400" />\n</Context>`;
167
+ const srcPath = path.join(process.cwd(), "src");
168
+ if (fs.existsSync(srcPath)) {
169
+ const contextPath = (config.project.appName || "").replace(".war", "");
170
+ const endpoints = EndpointService.scan(srcPath, contextPath);
171
+ if (endpoints.length > 0) {
172
+ this.logger.config("endpoints", endpoints.length);
173
+ }
174
+ }
175
+ this.logger.newline();
176
+ }
167
177
 
168
- try {
169
- fs.writeFileSync(contextPath, contextContent);
170
- } catch (e) {}
171
- }
178
+ private injectContextConfiguration(appPath: string) {
179
+ const metaInfPath = path.join(appPath, "META-INF");
180
+ if (!fs.existsSync(metaInfPath)) fs.mkdirSync(metaInfPath, { recursive: true });
172
181
 
173
- private injectHotswapProperties(appWebappPath: string) {
174
- const webInfClassesDir = path.join(appWebappPath, "WEB-INF", "classes");
175
- if (!fs.existsSync(webInfClassesDir)) fs.mkdirSync(webInfClassesDir, { recursive: true });
176
-
177
- const xavvaProps = path.join(process.cwd(), ".xavva", "hotswap-agent.properties");
178
- if (fs.existsSync(xavvaProps)) {
179
- fs.copyFileSync(xavvaProps, path.join(webInfClassesDir, "hotswap-agent.properties"));
180
- }
181
- }
182
+ const contextPath = path.join(metaInfPath, "context.xml");
183
+
184
+ // Aumentamos o cache para 100MB (102400 KB) para evitar avisos de cache insuficiente
185
+ const contextContent = `<?xml version="1.0" encoding="UTF-8"?>\n<Context>\n <Resources cachingAllowed="true" cacheMaxSize="102400" />\n</Context>`;
182
186
 
183
- private async handleServerReady(config: AppConfig, url: string, context: string, tomcat: TomcatService, incremental: boolean) {
184
- try {
185
- await new Promise(r => setTimeout(r, 1500));
186
- const response = await fetch(url);
187
- if (response.status < 500) {
188
- const memory = await tomcat.getMemoryUsage();
189
- Logger.divider();
190
- Logger.ready("Server ready");
191
- Logger.url("Local", url);
192
- Logger.info("Status", `${response.status}`);
193
- Logger.info("Memory", memory);
194
- Logger.done();
187
+ try {
188
+ fs.writeFileSync(contextPath, contextContent);
189
+ this.logger.debug(`context.xml configurado em ${contextPath}`);
190
+ } catch (e) {
191
+ this.logger.warn(`Não foi possível configurar context.xml: ${(e as Error).message}`);
192
+ }
193
+ }
195
194
 
196
- if (!config.project.quiet) {
197
- this.showEndpointMap(config.tomcat.port, context);
198
- }
199
-
200
- if (incremental) {
201
- await BrowserService.reload(url);
202
- } else {
203
- BrowserService.open(url);
204
- }
205
- } else {
206
- Logger.warn(`App returned status ${response.status}`);
207
- }
208
- } catch (e) {
209
- Logger.error(`Could not connect to ${url}`);
210
- }
211
- }
195
+ private injectHotswapProperties(appWebappPath: string) {
196
+ const webInfClassesDir = path.join(appWebappPath, "WEB-INF", "classes");
197
+ if (!fs.existsSync(webInfClassesDir)) fs.mkdirSync(webInfClassesDir, { recursive: true });
198
+
199
+ const xavvaProps = path.join(process.cwd(), ".xavva", "hotswap-agent.properties");
200
+ if (fs.existsSync(xavvaProps)) {
201
+ fs.copyFileSync(xavvaProps, path.join(webInfClassesDir, "hotswap-agent.properties"));
202
+ this.logger.debug('hotswap-agent.properties copiado');
203
+ }
204
+ }
212
205
 
213
- private showEndpointMap(port: number, context: string) {
214
- const endpoints = EndpointService.scan(path.join(process.cwd(), "src"), context);
215
- if (endpoints.length > 0) {
216
- Logger.section("Endpoints");
217
-
218
- const apis = endpoints.filter(e => e.className !== "JSP");
219
- const jsps = endpoints.filter(e => e.className === "JSP");
206
+ private async handleServerReady(config: AppConfig, url: string, context: string, tomcat: TomcatService, incremental: boolean) {
207
+ try {
208
+ await new Promise(r => setTimeout(r, 1500));
209
+ const response = await fetch(url);
210
+ if (response.status < 500) {
211
+ const memory = await tomcat.getMemoryUsage();
212
+ this.logger.divider();
213
+ this.logger.ready("Servidor pronto");
214
+ this.logger.url("Local", url);
215
+ this.logger.config("Status", response.status);
216
+ this.logger.config("Memória", memory);
217
+ this.logger.newline();
220
218
 
221
- if (apis.length > 0) {
222
- const uniqueApiUrls = [...new Set(apis.map(e => `http://localhost:${port}${e.fullPath}`))];
223
- uniqueApiUrls.forEach(url => Logger.info("", url));
224
- }
219
+ if (!config.project.quiet) {
220
+ this.showEndpointMap(config.tomcat.port, context);
221
+ }
222
+
223
+ if (incremental) {
224
+ await BrowserService.reload(url);
225
+ } else {
226
+ BrowserService.open(url);
227
+ }
228
+ } else {
229
+ this.logger.warn(`Aplicação retornou status ${response.status}`);
230
+ }
231
+ } catch (e) {
232
+ this.logger.error(`Não foi possível conectar em ${url}`);
233
+ }
234
+ }
225
235
 
226
- if (jsps.length > 0) {
227
- Logger.info("JSPs", "");
228
- const uniqueJspUrls = [...new Set(jsps.map(e => `http://localhost:${port}${e.fullPath}`))];
229
- uniqueJspUrls.forEach(url => Logger.info("", ` ${url}`));
230
- }
231
- Logger.endSection();
232
- }
233
- }
236
+ private showEndpointMap(port: number, context: string) {
237
+ const endpoints = EndpointService.scan(path.join(process.cwd(), "src"), context);
238
+ if (endpoints.length > 0) {
239
+ this.logger.section("Endpoints");
240
+
241
+ const apis = endpoints.filter(e => e.className !== "JSP");
242
+ const jsps = endpoints.filter(e => e.className === "JSP");
234
243
 
235
- async syncResource(config: AppConfig, filename: string): Promise<void> {
236
- const contextPath = (config.project.appName || "").replace(".war", "");
237
- const webappsPath = path.join(config.tomcat.path, "webapps");
238
- let appFolder = contextPath;
239
-
240
- if (!appFolder && fs.existsSync(webappsPath)) {
241
- const folders = fs.readdirSync(webappsPath, { withFileTypes: true })
242
- .filter(dirent => dirent.isDirectory() && !["ROOT", "manager", "host-manager", "docs"].includes(dirent.name));
243
- if (folders.length === 1) appFolder = folders[0].name;
244
- }
244
+ if (apis.length > 0) {
245
+ this.logger.info("APIs:", "");
246
+ const uniqueApiUrls = [...new Set(apis.map(e => `http://localhost:${port}${e.fullPath}`))];
247
+ uniqueApiUrls.forEach(url => this.logger.info("", url));
248
+ }
245
249
 
246
- const explodedPath = path.join(webappsPath, appFolder);
247
- if (!appFolder || !fs.existsSync(explodedPath)) return;
250
+ if (jsps.length > 0) {
251
+ this.logger.info("JSPs:", "");
252
+ const uniqueJspUrls = [...new Set(jsps.map(e => `http://localhost:${port}${e.fullPath}`))];
253
+ uniqueJspUrls.forEach(url => this.logger.info("", ` ${url}`));
254
+ }
255
+ this.logger.newline();
256
+ }
257
+ }
248
258
 
249
- const parts = filename.split(/[/\\]/);
250
- const webappIndex = parts.indexOf("webapp");
251
- const webContentIndex = parts.indexOf("WebContent");
252
- const rootIndex = webappIndex !== -1 ? webappIndex : webContentIndex;
253
-
254
- if (rootIndex !== -1) {
255
- const relPath = parts.slice(rootIndex + 1).join(path.sep);
256
- const targetPath = path.join(explodedPath, relPath);
257
-
258
- try {
259
- const targetDir = path.dirname(targetPath);
260
- if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
259
+ async syncResource(config: AppConfig, filename: string): Promise<void> {
260
+ const contextPath = (config.project.appName || "").replace(".war", "");
261
+ const webappsPath = path.join(config.tomcat.path, "webapps");
262
+ let appFolder = contextPath;
263
+
264
+ if (!appFolder && fs.existsSync(webappsPath)) {
265
+ const folders = fs.readdirSync(webappsPath, { withFileTypes: true })
266
+ .filter(dirent => dirent.isDirectory() && !["ROOT", "manager", "host-manager", "docs"].includes(dirent.name));
267
+ if (folders.length === 1) appFolder = folders[0].name;
268
+ }
261
269
 
262
- fs.copyFileSync(filename, targetPath);
263
- Logger.success(`${path.basename(filename)} updated → Tomcat`);
264
-
265
- const appUrl = `http://localhost:${config.tomcat.port}/${appFolder}`;
266
- await BrowserService.reload(appUrl);
267
- Logger.ready(`Browser reloaded`);
268
- } catch (e) {
269
- Logger.error(`Failed to sync resource: ${filename}`);
270
- }
271
- }
272
- }
270
+ const explodedPath = path.join(webappsPath, appFolder);
271
+ if (!appFolder || !fs.existsSync(explodedPath)) return;
272
+
273
+ const parts = filename.split(/[/\\]/);
274
+ const webappIndex = parts.indexOf("webapp");
275
+ const webContentIndex = parts.indexOf("WebContent");
276
+ const rootIndex = webappIndex !== -1 ? webappIndex : webContentIndex;
277
+
278
+ if (rootIndex !== -1) {
279
+ const relPath = parts.slice(rootIndex + 1).join(path.sep);
280
+ const targetPath = path.join(explodedPath, relPath);
281
+
282
+ try {
283
+ const targetDir = path.dirname(targetPath);
284
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
285
+
286
+ fs.copyFileSync(filename, targetPath);
287
+ this.logger.file(path.basename(filename), 'synced');
288
+
289
+ const appUrl = `http://localhost:${config.tomcat.port}/${appFolder}`;
290
+ await BrowserService.reload(appUrl);
291
+ } catch (e) {
292
+ this.logger.error(`Falha ao sincronizar: ${filename}`);
293
+ }
294
+ }
295
+ }
273
296
  }