@archznn/xavva 2.9.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.
- package/README.md +188 -1
- package/package.json +1 -1
- package/src/commands/ChangelogCommand.ts +128 -0
- package/src/commands/ConfigCommand.ts +48 -0
- package/src/commands/DbCommand.ts +126 -0
- package/src/commands/DockerCommand.ts +122 -0
- package/src/commands/HelpCommand.ts +44 -0
- package/src/commands/HttpCommand.ts +134 -0
- package/src/commands/InitCommand.ts +70 -0
- package/src/commands/TestCommand.ts +63 -0
- package/src/di/container.ts +15 -0
- package/src/index.ts +14 -1
- package/src/services/DbService.ts +357 -0
- package/src/services/DockerService.ts +361 -0
- package/src/services/HttpService.ts +259 -0
- package/src/services/TestService.ts +326 -0
- package/src/types/args.ts +1 -0
- package/src/types/config.ts +38 -0
- package/src/utils/ChangelogGenerator.ts +255 -0
- package/src/utils/LoggerLevel.ts +138 -0
- package/src/utils/config.ts +38 -2
|
@@ -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
|
+
}
|