@archznn/xavva 3.0.0 → 3.1.1

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,372 @@
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
+ * Verifica se o Docker daemon está rodando
50
+ */
51
+ async isDaemonRunning(): Promise<boolean> {
52
+ return new Promise((resolve) => {
53
+ const child = spawn("docker", ["info"], { shell: true });
54
+ child.on("close", (code) => resolve(code === 0));
55
+ child.on("error", () => resolve(false));
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Gera Dockerfile para o projeto
61
+ */
62
+ async generateDockerfile(config: DockerConfig = {}): Promise<void> {
63
+ const imageName = config.imageName || this.projectName;
64
+ const javaVersion = config.javaVersion || "17";
65
+ const port = config.port || 8080;
66
+
67
+ // Detectar build tool
68
+ const hasMaven = fs.existsSync(path.join(this.projectPath, "pom.xml"));
69
+ const hasGradle = fs.existsSync(path.join(this.projectPath, "build.gradle")) ||
70
+ fs.existsSync(path.join(this.projectPath, "build.gradle.kts"));
71
+
72
+ const dockerfile = hasMaven
73
+ ? this.generateMavenDockerfile(javaVersion, port)
74
+ : hasGradle
75
+ ? this.generateGradleDockerfile(javaVersion, port)
76
+ : this.generateGenericDockerfile(javaVersion, port);
77
+
78
+ const dockerfilePath = path.join(this.projectPath, "Dockerfile");
79
+ fs.writeFileSync(dockerfilePath, dockerfile);
80
+
81
+ Logger.success(`Dockerfile generated: ${dockerfilePath}`);
82
+ }
83
+
84
+ /**
85
+ * Gera docker-compose.yml
86
+ */
87
+ async generateCompose(config: DockerConfig = {}): Promise<void> {
88
+ const serviceName = config.imageName || this.projectName;
89
+ const port = config.port || 8080;
90
+
91
+ const compose = `version: '3.8'
92
+
93
+ services:
94
+ ${serviceName}:
95
+ build: .
96
+ ports:
97
+ - "${port}:8080"
98
+ environment:
99
+ - SPRING_PROFILES_ACTIVE=docker
100
+ - JAVA_OPTS=-Xmx512m
101
+ volumes:
102
+ - ./logs:/app/logs
103
+ restart: unless-stopped
104
+ healthcheck:
105
+ test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
106
+ interval: 30s
107
+ timeout: 10s
108
+ retries: 3
109
+ start_period: 40s
110
+
111
+ # Optional: Add database service
112
+ # postgres:
113
+ # image: postgres:15-alpine
114
+ # environment:
115
+ # POSTGRES_DB: ${serviceName}
116
+ # POSTGRES_USER: app
117
+ # POSTGRES_PASSWORD: secret
118
+ # ports:
119
+ # - "5432:5432"
120
+ # volumes:
121
+ # - postgres_data:/var/lib/postgresql/data
122
+
123
+ # volumes:
124
+ # postgres_data:
125
+ `;
126
+
127
+ const composePath = path.join(this.projectPath, "docker-compose.yml");
128
+ fs.writeFileSync(composePath, compose);
129
+
130
+ Logger.success(`docker-compose.yml generated: ${composePath}`);
131
+ }
132
+
133
+ /**
134
+ * Build da imagem Docker
135
+ */
136
+ async buildImage(tag?: string): Promise<boolean> {
137
+ const imageTag = tag || `${this.projectName}:latest`;
138
+
139
+ Logger.section("Building Docker Image");
140
+ Logger.info("Image", imageTag);
141
+
142
+ return new Promise((resolve) => {
143
+ const args = ["build", "-t", imageTag, "."];
144
+ const child = spawn("docker", args, {
145
+ cwd: this.projectPath,
146
+ stdio: "inherit",
147
+ shell: true
148
+ });
149
+
150
+ child.on("close", (code) => {
151
+ if (code === 0) {
152
+ Logger.success(`Image built: ${imageTag}`);
153
+ } else {
154
+ Logger.error("Build failed");
155
+ }
156
+ resolve(code === 0);
157
+ });
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Roda container com hot-reload
163
+ */
164
+ async runDevMode(port?: number): Promise<boolean> {
165
+ const containerPort = port || 8080;
166
+ const containerName = `${this.projectName}-dev`;
167
+
168
+ Logger.section("Running Docker Dev Mode");
169
+ Logger.info("Container", containerName);
170
+ Logger.info("Port", `${containerPort}:8080`);
171
+
172
+ // Verifica se já existe container rodando
173
+ await this.stopContainer(containerName);
174
+
175
+ return new Promise((resolve) => {
176
+ const args = [
177
+ "run", "--rm",
178
+ "--name", containerName,
179
+ "-p", `${containerPort}:8080`,
180
+ "-v", `${this.projectPath}:/app`,
181
+ "-w", "/app",
182
+ "-e", "MAVEN_OPTS=-XX:+UseG1GC",
183
+ "maven:3.9-eclipse-temurin-17",
184
+ "mvn", "tomcat7:run"
185
+ ];
186
+
187
+ const child = spawn("docker", args, {
188
+ stdio: "inherit",
189
+ shell: true
190
+ });
191
+
192
+ child.on("close", (code) => {
193
+ resolve(code === 0);
194
+ });
195
+ });
196
+ }
197
+
198
+ /**
199
+ * Roda docker-compose
200
+ */
201
+ async composeUp(detached: boolean = false): Promise<boolean> {
202
+ Logger.section("Starting Docker Compose");
203
+
204
+ return new Promise((resolve) => {
205
+ const args = ["up"];
206
+ if (detached) args.push("-d");
207
+ args.push("--build");
208
+
209
+ const child = spawn("docker-compose", args, {
210
+ cwd: this.projectPath,
211
+ stdio: "inherit",
212
+ shell: true
213
+ });
214
+
215
+ child.on("close", (code) => {
216
+ if (code === 0 && detached) {
217
+ Logger.success("Containers started");
218
+ this.showContainerStatus();
219
+ }
220
+ resolve(code === 0);
221
+ });
222
+ });
223
+ }
224
+
225
+ /**
226
+ * Para containers do compose
227
+ */
228
+ async composeDown(): Promise<boolean> {
229
+ Logger.section("Stopping Docker Compose");
230
+
231
+ return new Promise((resolve) => {
232
+ const child = spawn("docker-compose", ["down"], {
233
+ cwd: this.projectPath,
234
+ stdio: "inherit",
235
+ shell: true
236
+ });
237
+
238
+ child.on("close", (code) => {
239
+ if (code === 0) {
240
+ Logger.success("Containers stopped");
241
+ }
242
+ resolve(code === 0);
243
+ });
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Lista containers do projeto
249
+ */
250
+ async listContainers(): Promise<ContainerInfo[]> {
251
+ return new Promise((resolve) => {
252
+ const child = spawn("docker", [
253
+ "ps", "-a",
254
+ "--filter", `name=${this.projectName}`,
255
+ "--format", "{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}|{{.CreatedAt}}"
256
+ ], { shell: true });
257
+
258
+ let output = "";
259
+ child.stdout?.on("data", (data) => output += data.toString());
260
+
261
+ child.on("close", () => {
262
+ const containers: ContainerInfo[] = [];
263
+ for (const line of output.trim().split("\n")) {
264
+ const parts = line.split("|");
265
+ if (parts.length >= 6) {
266
+ containers.push({
267
+ id: parts[0].slice(0, 12),
268
+ name: parts[1],
269
+ image: parts[2],
270
+ status: parts[3],
271
+ ports: parts[4],
272
+ created: parts[5]
273
+ });
274
+ }
275
+ }
276
+ resolve(containers);
277
+ });
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Mostra status dos containers
283
+ */
284
+ async showContainerStatus(): Promise<void> {
285
+ const containers = await this.listContainers();
286
+
287
+ if (containers.length === 0) {
288
+ Logger.info("Containers", "None found");
289
+ return;
290
+ }
291
+
292
+ Logger.divider();
293
+ for (const c of containers) {
294
+ const statusColor = c.status.includes("Up") ? Logger.C.success : Logger.C.error;
295
+ Logger.info(c.name, `${statusColor}${c.status}${Logger.C.reset}`);
296
+ if (c.ports) {
297
+ Logger.dim(` ${c.ports}`);
298
+ }
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Para um container específico
304
+ */
305
+ private async stopContainer(name: string): Promise<void> {
306
+ return new Promise((resolve) => {
307
+ const child = spawn("docker", ["stop", name], { shell: true });
308
+ child.on("close", () => resolve());
309
+ });
310
+ }
311
+
312
+ // ===== Dockerfile Generators =====
313
+
314
+ private generateMavenDockerfile(javaVersion: string, port: number): string {
315
+ return `# Build stage
316
+ FROM maven:3.9-eclipse-temurin-${javaVersion}-alpine AS builder
317
+ WORKDIR /app
318
+ COPY pom.xml .
319
+ RUN mvn dependency:go-offline -B
320
+ COPY src ./src
321
+ RUN mvn clean package -DskipTests -B
322
+
323
+ # Runtime stage
324
+ FROM eclipse-temurin:${javaVersion}-jdk-alpine
325
+ WORKDIR /app
326
+ COPY --from=builder /app/target/*.war app.war
327
+
328
+ EXPOSE ${port}
329
+
330
+ # Health check
331
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \\
332
+ CMD wget --no-verbose --tries=1 --spider http://localhost:${port}/ || exit 1
333
+
334
+ ENTRYPOINT ["java", "-jar", "app.jar"]
335
+ `;
336
+ }
337
+
338
+ private generateGradleDockerfile(javaVersion: string, port: number): string {
339
+ return `# Build stage
340
+ FROM gradle:8-jdk${javaVersion}-alpine AS builder
341
+ WORKDIR /app
342
+ COPY build.gradle settings.gradle ./
343
+ COPY gradle ./gradle
344
+ RUN gradle dependencies --no-daemon
345
+ COPY src ./src
346
+ RUN gradle bootWar -x test --no-daemon
347
+
348
+ # Runtime stage
349
+ FROM eclipse-temurin:${javaVersion}-jdk-alpine
350
+ WORKDIR /app
351
+ COPY --from=builder /app/build/libs/*.war app.war
352
+
353
+ EXPOSE ${port}
354
+
355
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \\
356
+ CMD wget --no-verbose --tries=1 --spider http://localhost:${port}/ || exit 1
357
+
358
+ ENTRYPOINT ["java", "-jar", "app.jar"]
359
+ `;
360
+ }
361
+
362
+ private generateGenericDockerfile(javaVersion: string, port: number): string {
363
+ return `FROM eclipse-temurin:${javaVersion}-jdk-alpine
364
+ WORKDIR /app
365
+ COPY . .
366
+ RUN ./mvnw package -DskipTests || ./gradlew bootWar -x test || echo "Build manually"
367
+
368
+ EXPOSE ${port}
369
+ ENTRYPOINT ["java", "-jar", "app.war"]
370
+ `;
371
+ }
372
+ }
@@ -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
+ }