@archznn/xavva 3.0.0 → 3.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,361 @@
1
+ /**
2
+ * Serviço de integração com Docker
3
+ * Gera configs e gerencia containers
4
+ */
5
+
6
+ import { Logger } from "../utils/ui";
7
+ import { spawn } from "child_process";
8
+ import fs from "fs";
9
+ import path from "path";
10
+
11
+ export interface DockerConfig {
12
+ imageName: string;
13
+ tag?: string;
14
+ port?: number;
15
+ javaVersion?: string;
16
+ tomcatVersion?: string;
17
+ }
18
+
19
+ export interface ContainerInfo {
20
+ id: string;
21
+ name: string;
22
+ image: string;
23
+ status: string;
24
+ ports: string;
25
+ created: string;
26
+ }
27
+
28
+ export class DockerService {
29
+ private projectPath: string;
30
+ private projectName: string;
31
+
32
+ constructor(projectPath: string = process.cwd()) {
33
+ this.projectPath = projectPath;
34
+ this.projectName = path.basename(projectPath).toLowerCase().replace(/[^a-z0-9]/g, "-");
35
+ }
36
+
37
+ /**
38
+ * Verifica se Docker está disponível
39
+ */
40
+ async isDockerAvailable(): Promise<boolean> {
41
+ return new Promise((resolve) => {
42
+ const child = spawn("docker", ["--version"], { shell: true });
43
+ child.on("close", (code) => resolve(code === 0));
44
+ child.on("error", () => resolve(false));
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Gera Dockerfile para o projeto
50
+ */
51
+ async generateDockerfile(config: DockerConfig = {}): Promise<void> {
52
+ const imageName = config.imageName || this.projectName;
53
+ const javaVersion = config.javaVersion || "17";
54
+ const port = config.port || 8080;
55
+
56
+ // Detectar build tool
57
+ const hasMaven = fs.existsSync(path.join(this.projectPath, "pom.xml"));
58
+ const hasGradle = fs.existsSync(path.join(this.projectPath, "build.gradle")) ||
59
+ fs.existsSync(path.join(this.projectPath, "build.gradle.kts"));
60
+
61
+ const dockerfile = hasMaven
62
+ ? this.generateMavenDockerfile(javaVersion, port)
63
+ : hasGradle
64
+ ? this.generateGradleDockerfile(javaVersion, port)
65
+ : this.generateGenericDockerfile(javaVersion, port);
66
+
67
+ const dockerfilePath = path.join(this.projectPath, "Dockerfile");
68
+ fs.writeFileSync(dockerfilePath, dockerfile);
69
+
70
+ Logger.success(`Dockerfile generated: ${dockerfilePath}`);
71
+ }
72
+
73
+ /**
74
+ * Gera docker-compose.yml
75
+ */
76
+ async generateCompose(config: DockerConfig = {}): Promise<void> {
77
+ const serviceName = config.imageName || this.projectName;
78
+ const port = config.port || 8080;
79
+
80
+ const compose = `version: '3.8'
81
+
82
+ services:
83
+ ${serviceName}:
84
+ build: .
85
+ ports:
86
+ - "${port}:8080"
87
+ environment:
88
+ - SPRING_PROFILES_ACTIVE=docker
89
+ - JAVA_OPTS=-Xmx512m
90
+ volumes:
91
+ - ./logs:/app/logs
92
+ restart: unless-stopped
93
+ healthcheck:
94
+ test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
95
+ interval: 30s
96
+ timeout: 10s
97
+ retries: 3
98
+ start_period: 40s
99
+
100
+ # Optional: Add database service
101
+ # postgres:
102
+ # image: postgres:15-alpine
103
+ # environment:
104
+ # POSTGRES_DB: ${serviceName}
105
+ # POSTGRES_USER: app
106
+ # POSTGRES_PASSWORD: secret
107
+ # ports:
108
+ # - "5432:5432"
109
+ # volumes:
110
+ # - postgres_data:/var/lib/postgresql/data
111
+
112
+ # volumes:
113
+ # postgres_data:
114
+ `;
115
+
116
+ const composePath = path.join(this.projectPath, "docker-compose.yml");
117
+ fs.writeFileSync(composePath, compose);
118
+
119
+ Logger.success(`docker-compose.yml generated: ${composePath}`);
120
+ }
121
+
122
+ /**
123
+ * Build da imagem Docker
124
+ */
125
+ async buildImage(tag?: string): Promise<boolean> {
126
+ const imageTag = tag || `${this.projectName}:latest`;
127
+
128
+ Logger.section("Building Docker Image");
129
+ Logger.info("Image", imageTag);
130
+
131
+ return new Promise((resolve) => {
132
+ const args = ["build", "-t", imageTag, "."];
133
+ const child = spawn("docker", args, {
134
+ cwd: this.projectPath,
135
+ stdio: "inherit",
136
+ shell: true
137
+ });
138
+
139
+ child.on("close", (code) => {
140
+ if (code === 0) {
141
+ Logger.success(`Image built: ${imageTag}`);
142
+ } else {
143
+ Logger.error("Build failed");
144
+ }
145
+ resolve(code === 0);
146
+ });
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Roda container com hot-reload
152
+ */
153
+ async runDevMode(port?: number): Promise<boolean> {
154
+ const containerPort = port || 8080;
155
+ const containerName = `${this.projectName}-dev`;
156
+
157
+ Logger.section("Running Docker Dev Mode");
158
+ Logger.info("Container", containerName);
159
+ Logger.info("Port", `${containerPort}:8080`);
160
+
161
+ // Verifica se já existe container rodando
162
+ await this.stopContainer(containerName);
163
+
164
+ return new Promise((resolve) => {
165
+ const args = [
166
+ "run", "--rm",
167
+ "--name", containerName,
168
+ "-p", `${containerPort}:8080`,
169
+ "-v", `${this.projectPath}:/app`,
170
+ "-w", "/app",
171
+ "-e", "MAVEN_OPTS=-XX:+UseG1GC",
172
+ "maven:3.9-eclipse-temurin-17",
173
+ "mvn", "tomcat7:run"
174
+ ];
175
+
176
+ const child = spawn("docker", args, {
177
+ stdio: "inherit",
178
+ shell: true
179
+ });
180
+
181
+ child.on("close", (code) => {
182
+ resolve(code === 0);
183
+ });
184
+ });
185
+ }
186
+
187
+ /**
188
+ * Roda docker-compose
189
+ */
190
+ async composeUp(detached: boolean = false): Promise<boolean> {
191
+ Logger.section("Starting Docker Compose");
192
+
193
+ return new Promise((resolve) => {
194
+ const args = ["up"];
195
+ if (detached) args.push("-d");
196
+ args.push("--build");
197
+
198
+ const child = spawn("docker-compose", args, {
199
+ cwd: this.projectPath,
200
+ stdio: "inherit",
201
+ shell: true
202
+ });
203
+
204
+ child.on("close", (code) => {
205
+ if (code === 0 && detached) {
206
+ Logger.success("Containers started");
207
+ this.showContainerStatus();
208
+ }
209
+ resolve(code === 0);
210
+ });
211
+ });
212
+ }
213
+
214
+ /**
215
+ * Para containers do compose
216
+ */
217
+ async composeDown(): Promise<boolean> {
218
+ Logger.section("Stopping Docker Compose");
219
+
220
+ return new Promise((resolve) => {
221
+ const child = spawn("docker-compose", ["down"], {
222
+ cwd: this.projectPath,
223
+ stdio: "inherit",
224
+ shell: true
225
+ });
226
+
227
+ child.on("close", (code) => {
228
+ if (code === 0) {
229
+ Logger.success("Containers stopped");
230
+ }
231
+ resolve(code === 0);
232
+ });
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Lista containers do projeto
238
+ */
239
+ async listContainers(): Promise<ContainerInfo[]> {
240
+ return new Promise((resolve) => {
241
+ const child = spawn("docker", [
242
+ "ps", "-a",
243
+ "--filter", `name=${this.projectName}`,
244
+ "--format", "{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}|{{.CreatedAt}}"
245
+ ], { shell: true });
246
+
247
+ let output = "";
248
+ child.stdout?.on("data", (data) => output += data.toString());
249
+
250
+ child.on("close", () => {
251
+ const containers: ContainerInfo[] = [];
252
+ for (const line of output.trim().split("\n")) {
253
+ const parts = line.split("|");
254
+ if (parts.length >= 6) {
255
+ containers.push({
256
+ id: parts[0].slice(0, 12),
257
+ name: parts[1],
258
+ image: parts[2],
259
+ status: parts[3],
260
+ ports: parts[4],
261
+ created: parts[5]
262
+ });
263
+ }
264
+ }
265
+ resolve(containers);
266
+ });
267
+ });
268
+ }
269
+
270
+ /**
271
+ * Mostra status dos containers
272
+ */
273
+ async showContainerStatus(): Promise<void> {
274
+ const containers = await this.listContainers();
275
+
276
+ if (containers.length === 0) {
277
+ Logger.info("Containers", "None found");
278
+ return;
279
+ }
280
+
281
+ Logger.divider();
282
+ for (const c of containers) {
283
+ const statusColor = c.status.includes("Up") ? Logger.C.success : Logger.C.error;
284
+ Logger.info(c.name, `${statusColor}${c.status}${Logger.C.reset}`);
285
+ if (c.ports) {
286
+ Logger.dim(` ${c.ports}`);
287
+ }
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Para um container específico
293
+ */
294
+ private async stopContainer(name: string): Promise<void> {
295
+ return new Promise((resolve) => {
296
+ const child = spawn("docker", ["stop", name], { shell: true });
297
+ child.on("close", () => resolve());
298
+ });
299
+ }
300
+
301
+ // ===== Dockerfile Generators =====
302
+
303
+ private generateMavenDockerfile(javaVersion: string, port: number): string {
304
+ return `# Build stage
305
+ FROM maven:3.9-eclipse-temurin-${javaVersion}-alpine AS builder
306
+ WORKDIR /app
307
+ COPY pom.xml .
308
+ RUN mvn dependency:go-offline -B
309
+ COPY src ./src
310
+ RUN mvn clean package -DskipTests -B
311
+
312
+ # Runtime stage
313
+ FROM eclipse-temurin:${javaVersion}-jdk-alpine
314
+ WORKDIR /app
315
+ COPY --from=builder /app/target/*.war app.war
316
+
317
+ EXPOSE ${port}
318
+
319
+ # Health check
320
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \\
321
+ CMD wget --no-verbose --tries=1 --spider http://localhost:${port}/ || exit 1
322
+
323
+ ENTRYPOINT ["java", "-jar", "app.jar"]
324
+ `;
325
+ }
326
+
327
+ private generateGradleDockerfile(javaVersion: string, port: number): string {
328
+ return `# Build stage
329
+ FROM gradle:8-jdk${javaVersion}-alpine AS builder
330
+ WORKDIR /app
331
+ COPY build.gradle settings.gradle ./
332
+ COPY gradle ./gradle
333
+ RUN gradle dependencies --no-daemon
334
+ COPY src ./src
335
+ RUN gradle bootWar -x test --no-daemon
336
+
337
+ # Runtime stage
338
+ FROM eclipse-temurin:${javaVersion}-jdk-alpine
339
+ WORKDIR /app
340
+ COPY --from=builder /app/build/libs/*.war app.war
341
+
342
+ EXPOSE ${port}
343
+
344
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \\
345
+ CMD wget --no-verbose --tries=1 --spider http://localhost:${port}/ || exit 1
346
+
347
+ ENTRYPOINT ["java", "-jar", "app.jar"]
348
+ `;
349
+ }
350
+
351
+ private generateGenericDockerfile(javaVersion: string, port: number): string {
352
+ return `FROM eclipse-temurin:${javaVersion}-jdk-alpine
353
+ WORKDIR /app
354
+ COPY . .
355
+ RUN ./mvnw package -DskipTests || ./gradlew bootWar -x test || echo "Build manually"
356
+
357
+ EXPOSE ${port}
358
+ ENTRYPOINT ["java", "-jar", "app.war"]
359
+ `;
360
+ }
361
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Serviço de HTTP Client para testar APIs
3
+ * Similar ao Postman/curl mas integrado com o projeto
4
+ */
5
+
6
+ import { Logger } from "../utils/ui";
7
+ import type { ApiEndpoint } from "../types/endpoint";
8
+
9
+ export interface HttpRequest {
10
+ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
11
+ url: string;
12
+ headers?: Record<string, string>;
13
+ body?: string | object;
14
+ params?: Record<string, string>;
15
+ timeout?: number;
16
+ }
17
+
18
+ export interface HttpResponse {
19
+ status: number;
20
+ statusText: string;
21
+ headers: Record<string, string>;
22
+ body: string | object;
23
+ duration: number;
24
+ size: number;
25
+ }
26
+
27
+ export interface HttpCollection {
28
+ name: string;
29
+ requests: HttpRequest[];
30
+ }
31
+
32
+ export class HttpService {
33
+ private baseUrl: string;
34
+ private defaultHeaders: Record<string, string>;
35
+
36
+ constructor(baseUrl: string = "", defaultHeaders: Record<string, string> = {}) {
37
+ this.baseUrl = baseUrl;
38
+ this.defaultHeaders = {
39
+ "Accept": "application/json",
40
+ "Content-Type": "application/json",
41
+ ...defaultHeaders
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Executa uma requisição HTTP
47
+ */
48
+ async request(req: HttpRequest): Promise<HttpResponse> {
49
+ const startTime = Date.now();
50
+ const url = this.buildUrl(req.url, req.params);
51
+
52
+ Logger.step(`${req.method} ${url}`);
53
+
54
+ const headers = { ...this.defaultHeaders, ...req.headers };
55
+ const body = req.body ? (typeof req.body === "string" ? req.body : JSON.stringify(req.body)) : undefined;
56
+
57
+ try {
58
+ const controller = new AbortController();
59
+ const timeoutId = setTimeout(() => controller.abort(), req.timeout || 30000);
60
+
61
+ const response = await fetch(url, {
62
+ method: req.method,
63
+ headers,
64
+ body,
65
+ signal: controller.signal
66
+ });
67
+
68
+ clearTimeout(timeoutId);
69
+
70
+ const duration = Date.now() - startTime;
71
+ const responseHeaders: Record<string, string> = {};
72
+ response.headers.forEach((value, key) => {
73
+ responseHeaders[key] = value;
74
+ });
75
+
76
+ const responseBody = await this.parseBody(response);
77
+ const size = JSON.stringify(responseBody).length;
78
+
79
+ const result: HttpResponse = {
80
+ status: response.status,
81
+ statusText: response.statusText,
82
+ headers: responseHeaders,
83
+ body: responseBody,
84
+ duration,
85
+ size
86
+ };
87
+
88
+ this.printResponse(result);
89
+ return result;
90
+
91
+ } catch (error) {
92
+ const duration = Date.now() - startTime;
93
+ Logger.error(`Request failed: ${(error as Error).message}`);
94
+ throw error;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Executa múltiplas requisições em sequência
100
+ */
101
+ async runCollection(requests: HttpRequest[]): Promise<HttpResponse[]> {
102
+ Logger.section(`Running ${requests.length} requests`);
103
+ const results: HttpResponse[] = [];
104
+
105
+ for (let i = 0; i < requests.length; i++) {
106
+ Logger.info(`Request`, `${i + 1}/${requests.length}`);
107
+ try {
108
+ const result = await this.request(requests[i]);
109
+ results.push(result);
110
+ } catch (error) {
111
+ Logger.error(`Request ${i + 1} failed`);
112
+ }
113
+ }
114
+
115
+ Logger.endSection();
116
+ return results;
117
+ }
118
+
119
+ /**
120
+ * Testa um endpoint descoberto automaticamente
121
+ */
122
+ async testEndpoint(endpoint: ApiEndpoint, baseUrl: string): Promise<HttpResponse> {
123
+ const url = `${baseUrl}${endpoint.fullPath}`;
124
+
125
+ // Gera um body de exemplo baseado nos parâmetros
126
+ const body = this.generateExampleBody(endpoint);
127
+
128
+ return this.request({
129
+ method: endpoint.method === "ALL" ? "GET" : endpoint.method,
130
+ url,
131
+ body
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Modo interativo - CLI HTTP client
137
+ */
138
+ async interactive(): Promise<void> {
139
+ Logger.section("HTTP Client Interactive Mode");
140
+ Logger.info("Commands:", "GET /path, POST /path, PUT /path, DELETE /path");
141
+ Logger.info("Headers:", "header:Name:Value");
142
+ Logger.info("Body:", "body:{\"key\":\"value\"}");
143
+ Logger.info("Exit:", "quit or exit");
144
+ Logger.endSection();
145
+
146
+ // Implementação básica - em produção usaria readline
147
+ Logger.info("Tip", "Use individual commands instead:");
148
+ Logger.info("Example", "xavva http GET /api/users");
149
+ }
150
+
151
+ /**
152
+ * Carrega uma coleção de requisições de um arquivo
153
+ */
154
+ loadCollection(filePath: string): HttpCollection {
155
+ const fs = require("fs");
156
+ const content = fs.readFileSync(filePath, "utf-8");
157
+ return JSON.parse(content);
158
+ }
159
+
160
+ /**
161
+ * Salva uma coleção de requisições
162
+ */
163
+ saveCollection(collection: HttpCollection, filePath: string): void {
164
+ const fs = require("fs");
165
+ fs.writeFileSync(filePath, JSON.stringify(collection, null, 2));
166
+ }
167
+
168
+ private buildUrl(url: string, params?: Record<string, string>): string {
169
+ let fullUrl = url.startsWith("http") ? url : `${this.baseUrl}${url}`;
170
+
171
+ if (params && Object.keys(params).length > 0) {
172
+ const searchParams = new URLSearchParams();
173
+ for (const [key, value] of Object.entries(params)) {
174
+ searchParams.append(key, value);
175
+ }
176
+ fullUrl += `?${searchParams.toString()}`;
177
+ }
178
+
179
+ return fullUrl;
180
+ }
181
+
182
+ private async parseBody(response: Response): Promise<string | object> {
183
+ const contentType = response.headers.get("content-type") || "";
184
+ const text = await response.text();
185
+
186
+ if (contentType.includes("application/json")) {
187
+ try {
188
+ return JSON.parse(text);
189
+ } catch {
190
+ return text;
191
+ }
192
+ }
193
+
194
+ return text;
195
+ }
196
+
197
+ private printResponse(res: HttpResponse): void {
198
+ const statusColor = res.status < 300 ? Logger.C.success
199
+ : res.status < 400 ? Logger.C.warning
200
+ : Logger.C.error;
201
+
202
+ Logger.divider();
203
+ Logger.info("Status", `${statusColor}${res.status} ${res.statusText}${Logger.C.reset}`);
204
+ Logger.info("Duration", `${res.duration}ms`);
205
+ Logger.info("Size", `${this.formatBytes(res.size)}`);
206
+
207
+ if (typeof res.body === "object") {
208
+ Logger.divider();
209
+ console.log(JSON.stringify(res.body, null, 2));
210
+ } else if (res.body) {
211
+ Logger.divider();
212
+ console.log(res.body.slice(0, 1000));
213
+ if (res.body.length > 1000) {
214
+ Logger.dim(`... (${res.body.length - 1000} more characters)`);
215
+ }
216
+ }
217
+ }
218
+
219
+ private formatBytes(bytes: number): string {
220
+ if (bytes < 1024) return `${bytes} B`;
221
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
222
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
223
+ }
224
+
225
+ private generateExampleBody(endpoint: ApiEndpoint): object | undefined {
226
+ if (endpoint.method === "GET" || endpoint.method === "DELETE") {
227
+ return undefined;
228
+ }
229
+
230
+ const body: Record<string, unknown> = {};
231
+ for (const param of endpoint.parameters) {
232
+ if (param.source === "BODY") {
233
+ // Gera exemplo baseado no tipo
234
+ switch (param.type.toLowerCase()) {
235
+ case "string":
236
+ body[param.name] = "example";
237
+ break;
238
+ case "int":
239
+ case "integer":
240
+ case "long":
241
+ body[param.name] = 123;
242
+ break;
243
+ case "boolean":
244
+ body[param.name] = true;
245
+ break;
246
+ case "double":
247
+ case "float":
248
+ case "bigdecimal":
249
+ body[param.name] = 99.99;
250
+ break;
251
+ default:
252
+ body[param.name] = null;
253
+ }
254
+ }
255
+ }
256
+
257
+ return Object.keys(body).length > 0 ? body : undefined;
258
+ }
259
+ }