@archznn/xavva 2.3.0 → 2.5.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.
@@ -1,245 +1,288 @@
1
1
  import { Logger } from "../utils/ui";
2
- import { existsSync, mkdirSync, createWriteStream, writeFileSync, promises as fs } from "fs";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ createWriteStream,
6
+ writeFileSync,
7
+ readdirSync,
8
+ promises as fsPromises,
9
+ } from "fs";
3
10
  import path from "path";
4
11
  import os from "os";
5
12
  import { spawn } from "child_process";
6
13
 
7
14
  export interface EmbeddedTomcatOptions {
8
- version?: string;
9
- port?: number;
10
- webappPath: string;
11
- contextPath?: string;
15
+ version?: string;
16
+ port?: number;
17
+ webappPath: string;
18
+ contextPath?: string;
12
19
  }
13
20
 
14
21
  interface DownloadProgress {
15
- downloaded: number;
16
- total: number;
17
- percent: number;
22
+ downloaded: number;
23
+ total: number;
24
+ percent: number;
18
25
  }
19
26
 
20
27
  export class EmbeddedTomcatService {
21
- private readonly baseDir: string;
22
- private readonly version: string;
23
- private port: number;
24
- private webappPath: string;
25
- private contextPath: string;
26
- private tomcatHome: string;
27
- private downloadUrl: string;
28
- private isInstalled: boolean = false;
29
-
30
- // Versões estáveis do Tomcat (atualizadas: 2026-03-04)
31
- private static readonly VERSIONS: Record<string, { url: string; sha512: string }> = {
32
- "10.1.52": {
33
- url: "https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.52/bin/apache-tomcat-10.1.52-windows-x64.zip",
34
- sha512: ""
35
- },
36
- "9.0.115": {
37
- url: "https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.115/bin/apache-tomcat-9.0.115-windows-x64.zip",
38
- sha512: ""
39
- },
40
- "11.0.18": {
41
- url: "https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.18/bin/apache-tomcat-11.0.18-windows-x64.zip",
42
- sha512: ""
43
- }
44
- };
45
-
46
- constructor(options: EmbeddedTomcatOptions) {
47
- this.version = options.version || "10.1.52";
48
- this.port = options.port || 8080;
49
- this.webappPath = path.resolve(options.webappPath);
50
- this.contextPath = options.contextPath || "/";
51
- this.baseDir = path.join(os.homedir(), ".xavva", "tomcat");
52
- this.tomcatHome = path.join(this.baseDir, this.version);
53
-
54
- // Se a versão não está na lista, usa URL padrão
55
- const versionInfo = EmbeddedTomcatService.VERSIONS[this.version];
56
- if (versionInfo) {
57
- this.downloadUrl = versionInfo.url;
58
- } else {
59
- // Tenta inferir URL baseado no padrão Apache
60
- const majorVersion = this.version.split(".")[0];
61
- this.downloadUrl = `https://archive.apache.org/dist/tomcat/tomcat-${majorVersion}/v${this.version}/bin/apache-tomcat-${this.version}-windows-x64.zip`;
62
- }
63
- }
64
-
65
- /**
66
- * Verifica se o Tomcat já está instalado
67
- */
68
- checkInstallation(): boolean {
69
- const catalinaBat = path.join(this.tomcatHome, "bin", "catalina.bat");
70
- this.isInstalled = existsSync(catalinaBat);
71
- return this.isInstalled;
72
- }
73
-
74
- /**
75
- * Retorna o caminho do Tomcat (instalado ou para instalar)
76
- */
77
- getTomcatHome(): string {
78
- return this.tomcatHome;
79
- }
80
-
81
- /**
82
- * Baixa e instala o Tomcat
83
- */
84
- async install(): Promise<boolean> {
85
- if (this.checkInstallation()) {
86
- Logger.info("Tomcat", `Versão ${this.version} já instalada`);
87
- return true;
88
- }
89
-
90
- Logger.section("Instalando Tomcat Embutido");
91
- Logger.info("Versão", this.version);
92
- Logger.info("Destino", this.tomcatHome);
93
-
94
- // Cria diretório base
95
- if (!existsSync(this.baseDir)) {
96
- mkdirSync(this.baseDir, { recursive: true });
97
- }
98
-
99
- const zipPath = path.join(this.baseDir, `apache-tomcat-${this.version}.zip`);
100
-
101
- try {
102
- // Download
103
- await this.downloadFile(this.downloadUrl, zipPath);
104
-
105
- // Extração
106
- await this.extractZip(zipPath, this.baseDir);
107
-
108
- // Renomeia diretório extraído para versão padronizada
109
- const extractedDir = path.join(this.baseDir, `apache-tomcat-${this.version}`);
110
- if (existsSync(extractedDir) && extractedDir !== this.tomcatHome) {
111
- await fs.rename(extractedDir, this.tomcatHome);
112
- }
113
-
114
- // Limpa arquivo zip
115
- await fs.unlink(zipPath).catch(() => {});
116
-
117
- // Configura server.xml
118
- await this.configureServerXml();
119
-
120
- // Configura context.xml para hot-reload
121
- await this.configureContextXml();
122
-
123
- this.isInstalled = true;
124
- Logger.success(`Tomcat ${this.version} instalado com sucesso!`);
125
- return true;
126
-
127
- } catch (error) {
128
- Logger.error(`Falha ao instalar Tomcat: ${error}`);
129
- // Limpa arquivos parciais
130
- if (existsSync(this.tomcatHome)) {
131
- await fs.rm(this.tomcatHome, { recursive: true, force: true });
132
- }
133
- return false;
134
- }
135
- }
136
-
137
- /**
138
- * Configura server.xml com porta personalizada
139
- */
140
- private async configureServerXml(): Promise<void> {
141
- const serverXmlPath = path.join(this.tomcatHome, "conf", "server.xml");
142
-
143
- if (!existsSync(serverXmlPath)) {
144
- throw new Error("server.xml não encontrado após extração");
145
- }
146
-
147
- let content = await fs.readFile(serverXmlPath, "utf-8");
148
-
149
- // Atualiza porta HTTP
150
- content = content.replace(
151
- /<Connector port="8080"/,
152
- `<Connector port="${this.port}"`
153
- );
154
-
155
- // Atualiza porta de shutdown
156
- const shutdownPort = this.port + 1000;
157
- content = content.replace(
158
- /<Server port="8005"/,
159
- `<Server port="${shutdownPort}"`
160
- );
161
-
162
- // Atualiza porta AJP (se existir)
163
- content = content.replace(
164
- /<Connector port="8009"/,
165
- `<Connector port="${this.port + 1001}"`
166
- );
167
-
168
- // Desabilita manager e host-manager em embedded (opcional)
169
- // Remove context do manager para segurança
170
- content = content.replace(
171
- /<Context docBase="manager"[^>]*\/>/g,
172
- "<!-- <Context docBase=\"manager\" ... /> -->"
173
- );
174
-
175
- await fs.writeFile(serverXmlPath, content, "utf-8");
176
- Logger.debug(`server.xml configurado na porta ${this.port}`);
177
- }
178
-
179
- /**
180
- * Configura context.xml para hot-reload
181
- */
182
- private async configureContextXml(): Promise<void> {
183
- const contextXmlPath = path.join(this.tomcatHome, "conf", "context.xml");
184
-
185
- if (!existsSync(contextXmlPath)) return;
186
-
187
- let content = await fs.readFile(contextXmlPath, "utf-8");
188
-
189
- // Adiciona atributos para hot-reload se não existirem
190
- if (!content.includes("reloadable")) {
191
- content = content.replace(
192
- /<Context>/,
193
- '<Context reloadable="true" autoDeploy="true" deployOnStartup="true">'
194
- );
195
- }
196
-
197
- await fs.writeFile(contextXmlPath, content, "utf-8");
198
- }
199
-
200
- /**
201
- * Cria contexto para a aplicação
202
- */
203
- async createAppContext(): Promise<void> {
204
- const webappsDir = path.join(this.tomcatHome, "webapps");
205
-
206
- // Limpa webapps padrão
207
- const defaultApps = ["docs", "examples", "host-manager", "manager", "ROOT"];
208
- for (const app of defaultApps) {
209
- const appPath = path.join(webappsDir, app);
210
- if (existsSync(appPath)) {
211
- await fs.rm(appPath, { recursive: true, force: true });
212
- }
213
- }
214
-
215
- // Cria diretório para a aplicação
216
- const appName = this.contextPath === "/" ? "ROOT" : this.contextPath.replace(/^\//, "");
217
- const appDir = path.join(webappsDir, appName);
218
-
219
- if (existsSync(appDir)) {
220
- await fs.rm(appDir, { recursive: true, force: true });
221
- }
222
-
223
- // Se webappPath é um diretório, cria link/simula deploy
224
- if (existsSync(this.webappPath)) {
225
- // Em Windows, vamos copiar inicialmente (symlink requer privilégios)
226
- // Ou criar um context XML apontando para o diretório
227
- await this.createContextXml(appName);
228
- }
229
- }
230
-
231
- /**
232
- * Cria arquivo context XML para apontar para diretório externo
233
- */
234
- private async createContextXml(appName: string): Promise<void> {
235
- const confDir = path.join(this.tomcatHome, "conf", "Catalina", "localhost");
236
-
237
- if (!existsSync(confDir)) {
238
- mkdirSync(confDir, { recursive: true });
239
- }
240
-
241
- const contextFile = path.join(confDir, `${appName}.xml`);
242
- const content = `<?xml version="1.0" encoding="UTF-8"?>
28
+ private readonly baseDir: string;
29
+ private readonly version: string;
30
+ private port: number;
31
+ private webappPath: string;
32
+ private contextPath: string;
33
+ private tomcatHome: string;
34
+ private downloadUrl: string;
35
+ private isInstalled: boolean = false;
36
+
37
+ // Versões estáveis do Tomcat (atualizadas: 2026-03-04)
38
+ private static readonly VERSIONS: Record<
39
+ string,
40
+ { url: string; sha512: string }
41
+ > = {
42
+ "10.1.52": {
43
+ url: "https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.52/bin/apache-tomcat-10.1.52-windows-x64.zip",
44
+ sha512: "",
45
+ },
46
+ "9.0.115": {
47
+ url: "https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.115/bin/apache-tomcat-9.0.115-windows-x64.zip",
48
+ sha512: "",
49
+ },
50
+ "11.0.18": {
51
+ url: "https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.18/bin/apache-tomcat-11.0.18-windows-x64.zip",
52
+ sha512: "",
53
+ },
54
+ };
55
+
56
+ constructor(options: EmbeddedTomcatOptions) {
57
+ this.version = options.version || "10.1.52";
58
+ this.port = options.port || 8080;
59
+ this.webappPath = path.resolve(options.webappPath);
60
+ this.contextPath = options.contextPath || "/";
61
+ this.baseDir = path.join(os.homedir(), ".xavva", "tomcat");
62
+ this.tomcatHome = path.join(this.baseDir, this.version);
63
+
64
+ // Se a versão não está na lista, usa URL padrão
65
+ const versionInfo = EmbeddedTomcatService.VERSIONS[this.version];
66
+ if (versionInfo) {
67
+ this.downloadUrl = versionInfo.url;
68
+ } else {
69
+ // Tenta inferir URL baseado no padrão Apache
70
+ const majorVersion = this.version.split(".")[0];
71
+ this.downloadUrl = `https://archive.apache.org/dist/tomcat/tomcat-${majorVersion}/v${this.version}/bin/apache-tomcat-${this.version}-windows-x64.zip`;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Verifica se o Tomcat já está instalado
77
+ */
78
+ checkInstallation(): boolean {
79
+ const catalinaBat = path.join(this.tomcatHome, "bin", "catalina.bat");
80
+ this.isInstalled = existsSync(catalinaBat);
81
+ return this.isInstalled;
82
+ }
83
+
84
+ /**
85
+ * Retorna o caminho do Tomcat (instalado ou para instalar)
86
+ */
87
+ getTomcatHome(): string {
88
+ return this.tomcatHome;
89
+ }
90
+
91
+ /**
92
+ * Lista todas as versões instaladas
93
+ */
94
+ static listInstalledVersions(): string[] {
95
+ const baseDir = path.join(os.homedir(), ".xavva", "tomcat");
96
+ if (!existsSync(baseDir)) return [];
97
+
98
+ const versions: string[] = [];
99
+ const entries = readdirSync(baseDir, { withFileTypes: true });
100
+
101
+ for (const entry of entries) {
102
+ if (entry.isDirectory()) {
103
+ const catalinaBat = path.join(
104
+ baseDir,
105
+ entry.name,
106
+ "bin",
107
+ "catalina.bat",
108
+ );
109
+ if (existsSync(catalinaBat)) {
110
+ versions.push(entry.name);
111
+ }
112
+ }
113
+ }
114
+
115
+ return versions.sort();
116
+ }
117
+
118
+ /**
119
+ * Baixa e instala o Tomcat
120
+ */
121
+ async install(): Promise<boolean> {
122
+ if (this.checkInstallation()) {
123
+ Logger.info("Tomcat", `Versão ${this.version} já instalada`);
124
+ return true;
125
+ }
126
+
127
+ Logger.section("Instalando Tomcat Embutido");
128
+ Logger.info("Versão", this.version);
129
+ Logger.info("Destino", this.tomcatHome);
130
+
131
+ // Cria diretório base
132
+ if (!existsSync(this.baseDir)) {
133
+ mkdirSync(this.baseDir, { recursive: true });
134
+ }
135
+
136
+ const zipPath = path.join(
137
+ this.baseDir,
138
+ `apache-tomcat-${this.version}.zip`,
139
+ );
140
+
141
+ try {
142
+ // Download
143
+ await this.downloadFile(this.downloadUrl, zipPath);
144
+
145
+ // Extração
146
+ await this.extractZip(zipPath, this.baseDir);
147
+
148
+ // Renomeia diretório extraído para versão padronizada
149
+ const extractedDir = path.join(
150
+ this.baseDir,
151
+ `apache-tomcat-${this.version}`,
152
+ );
153
+ if (existsSync(extractedDir) && extractedDir !== this.tomcatHome) {
154
+ await fsPromises.rename(extractedDir, this.tomcatHome);
155
+ }
156
+
157
+ // Limpa arquivo zip
158
+ await fsPromises.unlink(zipPath).catch(() => { });
159
+
160
+ // Configura server.xml
161
+ await this.configureServerXml();
162
+
163
+ // Configura context.xml para hot-reload
164
+ await this.configureContextXml();
165
+
166
+ this.isInstalled = true;
167
+ Logger.success(`Tomcat ${this.version} instalado com sucesso!`);
168
+ return true;
169
+ } catch (error) {
170
+ Logger.error(`Falha ao instalar Tomcat: ${error}`);
171
+ // Limpa arquivos parciais
172
+ if (existsSync(this.tomcatHome)) {
173
+ await fsPromises.rm(this.tomcatHome, { recursive: true, force: true });
174
+ }
175
+ return false;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Configura server.xml com porta personalizada
181
+ */
182
+ private async configureServerXml(): Promise<void> {
183
+ const serverXmlPath = path.join(this.tomcatHome, "conf", "server.xml");
184
+
185
+ if (!existsSync(serverXmlPath)) {
186
+ throw new Error("server.xml não encontrado após extração");
187
+ }
188
+
189
+ let content = await fsPromises.readFile(serverXmlPath, "utf-8");
190
+
191
+ // Atualiza porta HTTP
192
+ content = content.replace(
193
+ /<Connector port="8080"/,
194
+ `<Connector port="${this.port}"`,
195
+ );
196
+
197
+ // Atualiza porta de shutdown
198
+ const shutdownPort = this.port + 1000;
199
+ content = content.replace(
200
+ /<Server port="8005"/,
201
+ `<Server port="${shutdownPort}"`,
202
+ );
203
+
204
+ // Atualiza porta AJP (se existir)
205
+ content = content.replace(
206
+ /<Connector port="8009"/,
207
+ `<Connector port="${this.port + 1001}"`,
208
+ );
209
+
210
+ // Desabilita manager e host-manager em embedded (opcional)
211
+ // Remove context do manager para segurança
212
+ content = content.replace(
213
+ /<Context docBase="manager"[^>]*\/>/g,
214
+ '<!-- <Context docBase="manager" ... /> -->',
215
+ );
216
+
217
+ await fsPromises.writeFile(serverXmlPath, content, "utf-8");
218
+ Logger.debug(`server.xml configurado na porta ${this.port}`);
219
+ }
220
+
221
+ /**
222
+ * Configura context.xml para hot-reload
223
+ */
224
+ private async configureContextXml(): Promise<void> {
225
+ const contextXmlPath = path.join(this.tomcatHome, "conf", "context.xml");
226
+
227
+ if (!existsSync(contextXmlPath)) return;
228
+
229
+ let content = await fsPromises.readFile(contextXmlPath, "utf-8");
230
+
231
+ // Adiciona atributos para hot-reload se não existirem
232
+ if (!content.includes("reloadable")) {
233
+ content = content.replace(
234
+ /<Context>/,
235
+ '<Context reloadable="true" autoDeploy="true" deployOnStartup="true">',
236
+ );
237
+ }
238
+
239
+ await fsPromises.writeFile(contextXmlPath, content, "utf-8");
240
+ }
241
+
242
+ /**
243
+ * Cria contexto para a aplicação
244
+ */
245
+ async createAppContext(): Promise<void> {
246
+ const webappsDir = path.join(this.tomcatHome, "webapps");
247
+
248
+ // Limpa webapps padrão
249
+ const defaultApps = ["docs", "examples", "host-manager", "manager", "ROOT"];
250
+ for (const app of defaultApps) {
251
+ const appPath = path.join(webappsDir, app);
252
+ if (existsSync(appPath)) {
253
+ await fsPromises.rm(appPath, { recursive: true, force: true });
254
+ }
255
+ }
256
+
257
+ // Cria diretório para a aplicação
258
+ const appName =
259
+ this.contextPath === "/" ? "ROOT" : this.contextPath.replace(/^\//, "");
260
+ const appDir = path.join(webappsDir, appName);
261
+
262
+ if (existsSync(appDir)) {
263
+ await fsPromises.rm(appDir, { recursive: true, force: true });
264
+ }
265
+
266
+ // Se webappPath é um diretório, cria link/simula deploy
267
+ if (existsSync(this.webappPath)) {
268
+ // Em Windows, vamos copiar inicialmente (symlink requer privilégios)
269
+ // Ou criar um context XML apontando para o diretório
270
+ await this.createContextXml(appName);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Cria arquivo context XML para apontar para diretório externo
276
+ */
277
+ private async createContextXml(appName: string): Promise<void> {
278
+ const confDir = path.join(this.tomcatHome, "conf", "Catalina", "localhost");
279
+
280
+ if (!existsSync(confDir)) {
281
+ mkdirSync(confDir, { recursive: true });
282
+ }
283
+
284
+ const contextFile = path.join(confDir, `${appName}.xml`);
285
+ const content = `<?xml version="1.0" encoding="UTF-8"?>
243
286
  <Context
244
287
  docBase="${this.webappPath.replace(/\\/g, "/")}"
245
288
  reloadable="true"
@@ -248,144 +291,146 @@ export class EmbeddedTomcatService {
248
291
  antiJARLocking="false">
249
292
  </Context>`;
250
293
 
251
- writeFileSync(contextFile, content);
252
- Logger.debug(`Context criado: ${contextFile}`);
253
- }
254
-
255
- /**
256
- * Verifica se porta está disponível
257
- */
258
- async isPortAvailable(): Promise<boolean> {
259
- return new Promise((resolve) => {
260
- const netstat = spawn("cmd", ["/c", `netstat -ano | findstr :${this.port}`]);
261
- let output = "";
262
-
263
- netstat.stdout?.on("data", (data) => {
264
- output += data.toString();
265
- });
266
-
267
- netstat.on("close", () => {
268
- resolve(output.trim().length === 0);
269
- });
270
-
271
- netstat.on("error", () => {
272
- resolve(true); // Assume disponível se não conseguir verificar
273
- });
274
- });
275
- }
276
-
277
- /**
278
- * Encontra próxima porta disponível
279
- */
280
- async findAvailablePort(startPort: number = 8080): Promise<number> {
281
- let port = startPort;
282
- while (!(await this.isPortAvailable())) {
283
- port++;
284
- if (port > 65535) {
285
- throw new Error("Nenhuma porta disponível encontrada");
286
- }
287
- }
288
- this.port = port;
289
- return port;
290
- }
291
-
292
- /**
293
- * Retorna variáveis de ambiente para o Tomcat
294
- */
295
- getEnvironment(): Record<string, string> {
296
- return {
297
- CATALINA_HOME: this.tomcatHome,
298
- CATALINA_BASE: this.tomcatHome,
299
- CATALINA_OPTS: process.env.CATALINA_OPTS || ""
300
- };
301
- }
302
-
303
- /**
304
- * Lista versões disponíveis
305
- */
306
- static getAvailableVersions(): string[] {
307
- return Object.keys(EmbeddedTomcatService.VERSIONS);
308
- }
309
-
310
- /**
311
- * Download com progresso
312
- */
313
- private async downloadFile(url: string, destPath: string): Promise<void> {
314
- const spinner = Logger.spinner(`Baixando Tomcat ${this.version}...`);
315
-
316
- try {
317
- const response = await fetch(url);
318
-
319
- if (!response.ok) {
320
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
321
- }
322
-
323
- const totalSize = parseInt(response.headers.get("content-length") || "0");
324
- const buffer = await response.arrayBuffer();
325
-
326
- writeFileSync(destPath, Buffer.from(buffer));
327
-
328
- spinner(true);
329
-
330
- const sizeMB = (buffer.byteLength / 1024 / 1024).toFixed(1);
331
- Logger.info("Download", `${sizeMB} MB baixados`);
332
-
333
- } catch (error) {
334
- spinner(false);
335
- throw error;
336
- }
337
- }
338
-
339
- /**
340
- * Extrai arquivo ZIP usando PowerShell
341
- */
342
- private async extractZip(zipPath: string, destDir: string): Promise<void> {
343
- const spinner = Logger.spinner("Extraindo arquivos...");
344
-
345
- return new Promise((resolve, reject) => {
346
- const ps = spawn("powershell", [
347
- "-command",
348
- `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`
349
- ]);
350
-
351
- ps.on("close", (code) => {
352
- if (code === 0) {
353
- spinner(true);
354
- resolve();
355
- } else {
356
- spinner(false);
357
- reject(new Error(`Falha ao extrair (código ${code})`));
358
- }
359
- });
360
-
361
- ps.on("error", (err) => {
362
- spinner(false);
363
- reject(err);
364
- });
365
- });
366
- }
367
-
368
- /**
369
- * Remove instalação
370
- */
371
- async uninstall(): Promise<void> {
372
- if (existsSync(this.tomcatHome)) {
373
- await fs.rm(this.tomcatHome, { recursive: true, force: true });
374
- Logger.info("Tomcat", `Versão ${this.version} removida`);
375
- }
376
- }
377
-
378
- /**
379
- * Retorna informações da instalação
380
- */
381
- getInfo(): Record<string, string> {
382
- return {
383
- version: this.version,
384
- home: this.tomcatHome,
385
- port: String(this.port),
386
- installed: this.isInstalled ? "sim" : "não",
387
- webapp: this.webappPath,
388
- context: this.contextPath
389
- };
390
- }
294
+ writeFileSync(contextFile, content);
295
+ Logger.debug(`Context criado: ${contextFile}`);
296
+ }
297
+
298
+ /**
299
+ * Verifica se porta está disponível
300
+ */
301
+ async isPortAvailable(): Promise<boolean> {
302
+ return new Promise((resolve) => {
303
+ const netstat = spawn("cmd", [
304
+ "/c",
305
+ `netstat -ano | findstr :${this.port}`,
306
+ ]);
307
+ let output = "";
308
+
309
+ netstat.stdout?.on("data", (data) => {
310
+ output += data.toString();
311
+ });
312
+
313
+ netstat.on("close", () => {
314
+ resolve(output.trim().length === 0);
315
+ });
316
+
317
+ netstat.on("error", () => {
318
+ resolve(true); // Assume disponível se não conseguir verificar
319
+ });
320
+ });
321
+ }
322
+
323
+ /**
324
+ * Encontra próxima porta disponível
325
+ */
326
+ async findAvailablePort(startPort: number = 8080): Promise<number> {
327
+ let port = startPort;
328
+ while (!(await this.isPortAvailable())) {
329
+ port++;
330
+ if (port > 65535) {
331
+ throw new Error("Nenhuma porta disponível encontrada");
332
+ }
333
+ }
334
+ this.port = port;
335
+ return port;
336
+ }
337
+
338
+ /**
339
+ * Retorna variáveis de ambiente para o Tomcat
340
+ */
341
+ getEnvironment(): Record<string, string> {
342
+ return {
343
+ CATALINA_HOME: this.tomcatHome,
344
+ CATALINA_BASE: this.tomcatHome,
345
+ CATALINA_OPTS: process.env.CATALINA_OPTS || "",
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Lista versões disponíveis
351
+ */
352
+ static getAvailableVersions(): string[] {
353
+ return Object.keys(EmbeddedTomcatService.VERSIONS);
354
+ }
355
+
356
+ /**
357
+ * Download com progresso
358
+ */
359
+ private async downloadFile(url: string, destPath: string): Promise<void> {
360
+ const spinner = Logger.spinner(`Baixando Tomcat ${this.version}...`);
361
+
362
+ try {
363
+ const response = await fetch(url);
364
+
365
+ if (!response.ok) {
366
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
367
+ }
368
+
369
+ const totalSize = parseInt(response.headers.get("content-length") || "0");
370
+ const buffer = await response.arrayBuffer();
371
+
372
+ writeFileSync(destPath, Buffer.from(buffer));
373
+
374
+ spinner(true);
375
+
376
+ const sizeMB = (buffer.byteLength / 1024 / 1024).toFixed(1);
377
+ Logger.info("Download", `${sizeMB} MB baixados`);
378
+ } catch (error) {
379
+ spinner(false);
380
+ throw error;
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Extrai arquivo ZIP usando PowerShell
386
+ */
387
+ private async extractZip(zipPath: string, destDir: string): Promise<void> {
388
+ const spinner = Logger.spinner("Extraindo arquivos...");
389
+
390
+ return new Promise((resolve, reject) => {
391
+ const ps = spawn("powershell", [
392
+ "-command",
393
+ `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`,
394
+ ]);
395
+
396
+ ps.on("close", (code) => {
397
+ if (code === 0) {
398
+ spinner(true);
399
+ resolve();
400
+ } else {
401
+ spinner(false);
402
+ reject(new Error(`Falha ao extrair (código ${code})`));
403
+ }
404
+ });
405
+
406
+ ps.on("error", (err) => {
407
+ spinner(false);
408
+ reject(err);
409
+ });
410
+ });
411
+ }
412
+
413
+ /**
414
+ * Remove instalação
415
+ */
416
+ async uninstall(): Promise<void> {
417
+ if (existsSync(this.tomcatHome)) {
418
+ await fsPromises.rm(this.tomcatHome, { recursive: true, force: true });
419
+ Logger.info("Tomcat", `Versão ${this.version} removida`);
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Retorna informações da instalação
425
+ */
426
+ getInfo(): Record<string, string> {
427
+ return {
428
+ version: this.version,
429
+ home: this.tomcatHome,
430
+ port: String(this.port),
431
+ installed: this.isInstalled ? "sim" : "não",
432
+ webapp: this.webappPath,
433
+ context: this.contextPath,
434
+ };
435
+ }
391
436
  }