@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.
- package/README.md +79 -1
- package/package.json +1 -1
- package/src/commands/ConfigCommand.ts +48 -0
- package/src/commands/DbCommand.ts +126 -0
- package/src/commands/DockerCommand.ts +133 -0
- package/src/commands/HelpCommand.ts +32 -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 +12 -0
- package/src/index.ts +6 -1
- package/src/services/DbService.ts +357 -0
- package/src/services/DockerService.ts +372 -0
- package/src/services/HttpService.ts +259 -0
- package/src/services/TestService.ts +326 -0
- package/src/types/config.ts +38 -0
- package/src/utils/config.ts +38 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Ultra-fast development toolkit for Java Enterprise (Tomcat) on Windows, Linux & macOS
|
|
4
4
|
|
|
5
|
-
[](https://github.com/leorsousa05/Xavva)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
Xavva is a high-performance CLI built with **Bun** that transforms the Java/Tomcat development experience. It brings modern development workflows (like Node.js/Vite) to the Java Enterprise ecosystem with hot-reload, smart logging, and automated deployment.
|
|
@@ -26,6 +26,11 @@ Xavva is a high-performance CLI built with **Bun** that transforms the Java/Tomc
|
|
|
26
26
|
- ๐ **Command History** โ Track and replay commands with `xavva history` and `xavva redo`
|
|
27
27
|
- ๐ฅ **Health Check** โ Verify environment (Java, ports, memory, disk) with `xavva health`
|
|
28
28
|
- ๐ฎ **Shell Completions** โ Auto-complete for bash, zsh, and fish
|
|
29
|
+
- ๐งช **Test Runner** โ Run JUnit/TestNG tests with watch mode and coverage
|
|
30
|
+
- ๐๏ธ **Database Migrations** โ Flyway/Liquibase integration
|
|
31
|
+
- ๐ **HTTP Client** โ Test APIs without leaving the terminal
|
|
32
|
+
- ๐ณ **Docker Integration** โ Generate configs, build and run containers
|
|
33
|
+
- ๐ **Multi-Environment** โ Dev, test, staging configurations
|
|
29
34
|
|
|
30
35
|
---
|
|
31
36
|
|
|
@@ -80,6 +85,29 @@ xavva history
|
|
|
80
85
|
# Repeat last command
|
|
81
86
|
xavva redo
|
|
82
87
|
|
|
88
|
+
# Run tests
|
|
89
|
+
xavva test
|
|
90
|
+
xavva test --watch
|
|
91
|
+
xavva test --coverage
|
|
92
|
+
|
|
93
|
+
# Database migrations
|
|
94
|
+
xavva db status
|
|
95
|
+
xavva db migrate
|
|
96
|
+
xavva db reset --force
|
|
97
|
+
|
|
98
|
+
# HTTP API testing
|
|
99
|
+
xavva http GET /api/users
|
|
100
|
+
xavva http POST /api/users --body '{"name":"John"}'
|
|
101
|
+
|
|
102
|
+
# Docker integration
|
|
103
|
+
xavva docker init
|
|
104
|
+
xavva docker build
|
|
105
|
+
xavva docker up
|
|
106
|
+
|
|
107
|
+
# Multi-environment
|
|
108
|
+
xavva deploy --env staging
|
|
109
|
+
xavva dev --env dev
|
|
110
|
+
|
|
83
111
|
# Enable shell completions (bash example)
|
|
84
112
|
eval "$(xavva completion bash)"
|
|
85
113
|
```
|
|
@@ -130,6 +158,56 @@ eval "$(xavva completion bash)"
|
|
|
130
158
|
| `xavva redo` | Repeat the last executed command |
|
|
131
159
|
| `xavva completion <shell>` | Generate shell completions (bash/zsh/fish) |
|
|
132
160
|
|
|
161
|
+
### Testing & Database
|
|
162
|
+
|
|
163
|
+
| Command | Description |
|
|
164
|
+
|---------|-------------|
|
|
165
|
+
| `xavva test` | Run all tests (JUnit/TestNG) |
|
|
166
|
+
| `xavva test --watch` | Run tests in watch mode |
|
|
167
|
+
| `xavva test --coverage` | Generate coverage report |
|
|
168
|
+
| `xavva test <filter>` | Run specific test class |
|
|
169
|
+
| `xavva db status` | Show migration status |
|
|
170
|
+
| `xavva db migrate` | Run pending migrations |
|
|
171
|
+
| `xavva db reset --force` | Reset database (โ ๏ธ destructive) |
|
|
172
|
+
| `xavva db seed` | Populate with test data |
|
|
173
|
+
|
|
174
|
+
### HTTP Client
|
|
175
|
+
|
|
176
|
+
| Command | Description |
|
|
177
|
+
|---------|-------------|
|
|
178
|
+
| `xavva http GET <path>` | Send GET request |
|
|
179
|
+
| `xavva http POST <path> --body '{}'` | Send POST request |
|
|
180
|
+
| `xavva http <path> --param key=value` | Add query parameters |
|
|
181
|
+
| `xavva http <path> --header "Auth: token"` | Add custom headers |
|
|
182
|
+
|
|
183
|
+
### Docker
|
|
184
|
+
|
|
185
|
+
| Command | Description |
|
|
186
|
+
|---------|-------------|
|
|
187
|
+
| `xavva docker init` | Generate Dockerfile & docker-compose.yml |
|
|
188
|
+
| `xavva docker build` | Build Docker image |
|
|
189
|
+
| `xavva docker up` | Start with docker-compose |
|
|
190
|
+
| `xavva docker down` | Stop containers |
|
|
191
|
+
| `xavva docker run` | Run development container |
|
|
192
|
+
| `xavva docker status` | Show container status |
|
|
193
|
+
|
|
194
|
+
### Multi-Environment
|
|
195
|
+
|
|
196
|
+
| Command | Description |
|
|
197
|
+
|---------|-------------|
|
|
198
|
+
| `xavva deploy --env <name>` | Deploy to specific environment |
|
|
199
|
+
| `xavva dev --env <name>` | Use environment configuration |
|
|
200
|
+
|
|
201
|
+
Configure environments in `xavva.json`:
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"environments": {
|
|
205
|
+
"dev": { "port": 8080, "profile": "dev" },
|
|
206
|
+
"staging": { "port": 8081, "profile": "staging" }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
133
211
|
---
|
|
134
212
|
|
|
135
213
|
## ๐ฑ Embedded Tomcat
|
package/package.json
CHANGED
|
@@ -141,6 +141,54 @@ export class ConfigCommand implements Command {
|
|
|
141
141
|
default: String(config.tomcatPath || "")
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
|
+
|
|
145
|
+
// Edit environments
|
|
146
|
+
const editEnvs = await confirm({
|
|
147
|
+
message: "Editar environments?",
|
|
148
|
+
default: false
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (editEnvs) {
|
|
152
|
+
await this.editEnvironments(config);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async editEnvironments(config: Record<string, unknown>): Promise<void> {
|
|
157
|
+
const environments = (config.environments as Record<string, unknown>) || {};
|
|
158
|
+
|
|
159
|
+
const envNames = Object.keys(environments);
|
|
160
|
+
if (envNames.length > 0) {
|
|
161
|
+
Logger.info("Environments existentes:", envNames.join(", "));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const addNew = await confirm({
|
|
165
|
+
message: "Adicionar novo environment?",
|
|
166
|
+
default: envNames.length === 0
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (addNew) {
|
|
170
|
+
const name = await input({
|
|
171
|
+
message: "Nome do environment:",
|
|
172
|
+
default: "staging"
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const port = await number({
|
|
176
|
+
message: `Porta para ${name}:`,
|
|
177
|
+
default: 8080
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const profile = await input({
|
|
181
|
+
message: `Profile para ${name}:`,
|
|
182
|
+
default: name
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
environments[name] = {
|
|
186
|
+
port,
|
|
187
|
+
profile
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
config.environments = environments;
|
|
144
192
|
}
|
|
145
193
|
|
|
146
194
|
private async editBuild(config: Record<string, unknown>): Promise<void> {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comando de migraรงรตes de banco de dados
|
|
3
|
+
* xavva db <action> [options]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Command } from "./Command";
|
|
7
|
+
import type { AppConfig, CLIArguments } from "../types/config";
|
|
8
|
+
import { DbService, type DbConfig } from "../services/DbService";
|
|
9
|
+
import { Logger } from "../utils/ui";
|
|
10
|
+
import { ProcessManager } from "../utils/processManager";
|
|
11
|
+
|
|
12
|
+
export class DbCommand implements Command {
|
|
13
|
+
async execute(config: AppConfig, args?: CLIArguments, positionals?: string[]): Promise<void> {
|
|
14
|
+
const processManager = ProcessManager.getInstance();
|
|
15
|
+
const action = positionals?.[1] || "status";
|
|
16
|
+
|
|
17
|
+
const service = new DbService(config.project.buildTool);
|
|
18
|
+
|
|
19
|
+
// Extrair config de DB do environment ou args
|
|
20
|
+
const dbConfig = this.extractDbConfig(config, args);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
switch (action) {
|
|
24
|
+
case "migrate":
|
|
25
|
+
case "up":
|
|
26
|
+
const migrateResult = await service.migrate(dbConfig);
|
|
27
|
+
if (!migrateResult.success) {
|
|
28
|
+
Logger.error(migrateResult.message);
|
|
29
|
+
await processManager.shutdown(1);
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
|
|
33
|
+
case "status":
|
|
34
|
+
case "info":
|
|
35
|
+
await service.status(dbConfig);
|
|
36
|
+
break;
|
|
37
|
+
|
|
38
|
+
case "reset":
|
|
39
|
+
case "clean":
|
|
40
|
+
case "drop":
|
|
41
|
+
if (!args?.force) {
|
|
42
|
+
Logger.warn("This will DELETE all data in the database!");
|
|
43
|
+
Logger.info("Use", "--force to confirm");
|
|
44
|
+
await processManager.shutdown(1);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const resetResult = await service.reset(dbConfig);
|
|
48
|
+
if (!resetResult.success) {
|
|
49
|
+
await processManager.shutdown(1);
|
|
50
|
+
}
|
|
51
|
+
// Roda migraรงรตes novamente apรณs reset
|
|
52
|
+
await service.migrate(dbConfig);
|
|
53
|
+
break;
|
|
54
|
+
|
|
55
|
+
case "seed":
|
|
56
|
+
const seedResult = await service.seed(dbConfig, args?.src);
|
|
57
|
+
if (!seedResult.success) {
|
|
58
|
+
Logger.error(seedResult.message);
|
|
59
|
+
await processManager.shutdown(1);
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
|
|
63
|
+
case "create":
|
|
64
|
+
await this.createMigration(service, args);
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
default:
|
|
68
|
+
Logger.error(`Unknown db action: ${action}`);
|
|
69
|
+
Logger.info("Actions", "migrate, status, reset, seed, create");
|
|
70
|
+
await processManager.shutdown(1);
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
Logger.error(`Database command failed: ${(error as Error).message}`);
|
|
74
|
+
await processManager.shutdown(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private extractDbConfig(config: AppConfig, args?: CLIArguments): DbConfig | undefined {
|
|
79
|
+
// Tenta pegar do environment config
|
|
80
|
+
const envName = config.project.environment;
|
|
81
|
+
const envConfig = envName && config.project.environments?.[envName];
|
|
82
|
+
|
|
83
|
+
if (envConfig?.db) {
|
|
84
|
+
return {
|
|
85
|
+
url: envConfig.db.url || process.env.JDBC_URL || "",
|
|
86
|
+
username: envConfig.db.username || process.env.JDBC_USER || "",
|
|
87
|
+
password: envConfig.db.password || process.env.JDBC_PASSWORD || "",
|
|
88
|
+
driver: envConfig.db.driver
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Fallback para env vars
|
|
93
|
+
if (process.env.JDBC_URL) {
|
|
94
|
+
return {
|
|
95
|
+
url: process.env.JDBC_URL,
|
|
96
|
+
username: process.env.JDBC_USER || "",
|
|
97
|
+
password: process.env.JDBC_PASSWORD || ""
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private async createMigration(service: DbService, args?: CLIArguments): Promise<void> {
|
|
105
|
+
const name = args?.name || "new_migration";
|
|
106
|
+
Logger.section("Create Migration");
|
|
107
|
+
Logger.info("Name", name);
|
|
108
|
+
|
|
109
|
+
// Detecta ferramenta
|
|
110
|
+
const tool = await service.detectTool();
|
|
111
|
+
|
|
112
|
+
if (tool === "flyway") {
|
|
113
|
+
const timestamp = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
114
|
+
const filename = `V${timestamp}__${name}.sql`;
|
|
115
|
+
const filepath = `src/main/resources/db/migration/${filename}`;
|
|
116
|
+
|
|
117
|
+
Logger.success(`Create file: ${filepath}`);
|
|
118
|
+
Logger.dim("-- Add your SQL here");
|
|
119
|
+
} else if (tool === "liquibase") {
|
|
120
|
+
const filename = `${name}.sql`;
|
|
121
|
+
Logger.success(`Add to changelog: db/changelog/${filename}`);
|
|
122
|
+
} else {
|
|
123
|
+
Logger.warn("No migration tool detected");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comando de integraรงรฃo Docker
|
|
3
|
+
* xavva docker <action> [options]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Command } from "./Command";
|
|
7
|
+
import type { AppConfig, CLIArguments } from "../types/config";
|
|
8
|
+
import { DockerService, type DockerConfig } from "../services/DockerService";
|
|
9
|
+
import { Logger } from "../utils/ui";
|
|
10
|
+
import { ProcessManager } from "../utils/processManager";
|
|
11
|
+
|
|
12
|
+
export class DockerCommand implements Command {
|
|
13
|
+
async execute(config: AppConfig, args?: CLIArguments, positionals?: string[]): Promise<void> {
|
|
14
|
+
const processManager = ProcessManager.getInstance();
|
|
15
|
+
const action = positionals?.[1] || "status";
|
|
16
|
+
|
|
17
|
+
const service = new DockerService();
|
|
18
|
+
|
|
19
|
+
// Verifica se Docker estรก disponรญvel
|
|
20
|
+
const isAvailable = await service.isDockerAvailable();
|
|
21
|
+
if (!isAvailable) {
|
|
22
|
+
Logger.error("Docker CLI not found");
|
|
23
|
+
Logger.info("Install", "https://docs.docker.com/get-docker/");
|
|
24
|
+
await processManager.shutdown(1);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Verifica se o daemon estรก rodando
|
|
29
|
+
const isRunning = await service.isDaemonRunning();
|
|
30
|
+
if (!isRunning) {
|
|
31
|
+
Logger.error("Docker daemon is not running");
|
|
32
|
+
Logger.info("Windows", "Start Docker Desktop from the system tray");
|
|
33
|
+
Logger.info("Linux", "Run: sudo systemctl start docker");
|
|
34
|
+
Logger.info("macOS", "Start Docker Desktop from Applications");
|
|
35
|
+
await processManager.shutdown(1);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const dockerConfig: DockerConfig = {
|
|
40
|
+
imageName: (args?.name as string) || config.project.appName,
|
|
41
|
+
tag: (args?.tag as string) || "latest",
|
|
42
|
+
port: args?.port ? parseInt(args.port as string) : config.tomcat.port,
|
|
43
|
+
javaVersion: (args?.["java-version"] as string) || "17",
|
|
44
|
+
tomcatVersion: config.tomcat.version
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
switch (action) {
|
|
49
|
+
case "init":
|
|
50
|
+
await this.handleInit(service, dockerConfig);
|
|
51
|
+
break;
|
|
52
|
+
|
|
53
|
+
case "build":
|
|
54
|
+
const built = await service.buildImage(dockerConfig.tag);
|
|
55
|
+
if (!built) {
|
|
56
|
+
await processManager.shutdown(1);
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
|
|
60
|
+
case "run":
|
|
61
|
+
case "dev":
|
|
62
|
+
const running = await service.runDevMode(dockerConfig.port);
|
|
63
|
+
if (!running) {
|
|
64
|
+
await processManager.shutdown(1);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
|
|
68
|
+
case "up":
|
|
69
|
+
const up = await service.composeUp(args?.detached);
|
|
70
|
+
if (!up) {
|
|
71
|
+
await processManager.shutdown(1);
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
|
|
75
|
+
case "down":
|
|
76
|
+
case "stop":
|
|
77
|
+
const down = await service.composeDown();
|
|
78
|
+
if (!down) {
|
|
79
|
+
await processManager.shutdown(1);
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
|
|
83
|
+
case "status":
|
|
84
|
+
case "ps":
|
|
85
|
+
await service.showContainerStatus();
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
case "push":
|
|
89
|
+
await this.handlePush(service, dockerConfig, args);
|
|
90
|
+
break;
|
|
91
|
+
|
|
92
|
+
default:
|
|
93
|
+
Logger.error(`Unknown docker action: ${action}`);
|
|
94
|
+
Logger.info("Actions", "init, build, run, up, down, status, push");
|
|
95
|
+
await processManager.shutdown(1);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
Logger.error(`Docker command failed: ${(error as Error).message}`);
|
|
99
|
+
await processManager.shutdown(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async handleInit(service: DockerService, config: DockerConfig): Promise<void> {
|
|
104
|
+
Logger.section("Docker Init");
|
|
105
|
+
Logger.info("Generating", "Docker configuration files");
|
|
106
|
+
|
|
107
|
+
await service.generateDockerfile(config);
|
|
108
|
+
await service.generateCompose(config);
|
|
109
|
+
|
|
110
|
+
Logger.divider();
|
|
111
|
+
Logger.info("Next steps", "");
|
|
112
|
+
Logger.log(` ${Logger.C.gray}โ${Logger.C.reset} ${Logger.C.primary}xavva docker build${Logger.C.reset} ${Logger.C.gray}- Build image${Logger.C.reset}`);
|
|
113
|
+
Logger.log(` ${Logger.C.gray}โ${Logger.C.reset} ${Logger.C.primary}xavva docker up${Logger.C.reset} ${Logger.C.gray}- Start containers${Logger.C.reset}`);
|
|
114
|
+
Logger.log(` ${Logger.C.gray}โ${Logger.C.reset} ${Logger.C.primary}xavva docker run${Logger.C.reset} ${Logger.C.gray}- Run dev mode${Logger.C.reset}`);
|
|
115
|
+
Logger.endSection();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async handlePush(service: DockerService, config: DockerConfig, args?: CLIArguments): Promise<void> {
|
|
119
|
+
const registry = args?.registry as string || "docker.io";
|
|
120
|
+
const namespace = args?.namespace as string || "";
|
|
121
|
+
|
|
122
|
+
const fullImageName = namespace
|
|
123
|
+
? `${registry}/${namespace}/${config.imageName}:${config.tag}`
|
|
124
|
+
: `${registry}/${config.imageName}:${config.tag}`;
|
|
125
|
+
|
|
126
|
+
Logger.section("Docker Push");
|
|
127
|
+
Logger.info("Image", fullImageName);
|
|
128
|
+
Logger.info("Registry", registry);
|
|
129
|
+
|
|
130
|
+
Logger.success("Use: docker push " + fullImageName);
|
|
131
|
+
Logger.dim("Make sure you're logged in: docker login " + registry);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -40,6 +40,11 @@ export class HelpCommand implements Command {
|
|
|
40
40
|
${this.c("cyan", "completion")} Generate shell completions (bash/zsh/fish)
|
|
41
41
|
${this.c("cyan", "changelog")} Generate changelog from conventional commits
|
|
42
42
|
|
|
43
|
+
${this.c("magenta", "test")} Run tests (JUnit/TestNG)
|
|
44
|
+
${this.c("magenta", "db")} Database migrations (Flyway/Liquibase)
|
|
45
|
+
${this.c("magenta", "http")} HTTP client for API testing
|
|
46
|
+
${this.c("magenta", "docker")} Docker integration (build, run, compose)
|
|
47
|
+
|
|
43
48
|
${this.c("yellow", "GENERAL OPTIONS")}
|
|
44
49
|
${this.c("cyan", "-p, --path")} <path> Tomcat installation path
|
|
45
50
|
${this.c("cyan", "-t, --tool")} <tool> Build tool: maven | gradle
|
|
@@ -151,6 +156,33 @@ export class HelpCommand implements Command {
|
|
|
151
156
|
xavva deploy --debug-level trace # Trace all operations
|
|
152
157
|
xavva deploy --debug-level silly # Everything including config
|
|
153
158
|
|
|
159
|
+
${this.c("dim", "# Multi-environment")}
|
|
160
|
+
xavva deploy --env staging # Deploy to staging environment
|
|
161
|
+
xavva dev --env dev # Use dev environment config
|
|
162
|
+
|
|
163
|
+
${this.c("dim", "# Test runner")}
|
|
164
|
+
xavva test # Run all tests
|
|
165
|
+
xavva test --watch # Watch mode
|
|
166
|
+
xavva test --coverage # Generate coverage report
|
|
167
|
+
xavva test UserServiceTest # Run specific test class
|
|
168
|
+
|
|
169
|
+
${this.c("dim", "# Database migrations")}
|
|
170
|
+
xavva db status # Show migration status
|
|
171
|
+
xavva db migrate # Run pending migrations
|
|
172
|
+
xavva db reset --force # Reset database (drops all!)
|
|
173
|
+
xavva db seed # Populate with test data
|
|
174
|
+
|
|
175
|
+
${this.c("dim", "# HTTP Client")}
|
|
176
|
+
xavva http GET /api/users # Test endpoint
|
|
177
|
+
xavva http POST /api/users --body '{"name":"John"}'
|
|
178
|
+
xavva http GET /api/users --param page=1 --param size=10
|
|
179
|
+
|
|
180
|
+
${this.c("dim", "# Docker")}
|
|
181
|
+
xavva docker init # Generate Dockerfile & compose
|
|
182
|
+
xavva docker build # Build image
|
|
183
|
+
xavva docker up # Start with docker-compose
|
|
184
|
+
xavva docker run # Run dev container
|
|
185
|
+
|
|
154
186
|
${this.c("yellow", "CONFIGURATION")}
|
|
155
187
|
Settings are loaded from ${this.c("cyan", "xavva.json")} in the project root:
|
|
156
188
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comando de HTTP Client
|
|
3
|
+
* xavva http <method> <url> [options]
|
|
4
|
+
* xavva http --interactive
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Command } from "./Command";
|
|
8
|
+
import type { AppConfig, CLIArguments } from "../types/config";
|
|
9
|
+
import { HttpService, type HttpRequest } from "../services/HttpService";
|
|
10
|
+
import { EndpointService } from "../services/EndpointService";
|
|
11
|
+
import { Logger } from "../utils/ui";
|
|
12
|
+
import { ProcessManager } from "../utils/processManager";
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
|
|
16
|
+
export class HttpCommand implements Command {
|
|
17
|
+
async execute(config: AppConfig, args?: CLIArguments, positionals?: string[]): Promise<void> {
|
|
18
|
+
const processManager = ProcessManager.getInstance();
|
|
19
|
+
|
|
20
|
+
// Modo interativo
|
|
21
|
+
if (args?.interactive || positionals?.length === 1) {
|
|
22
|
+
const baseUrl = `http://localhost:${config.tomcat.port}`;
|
|
23
|
+
const service = new HttpService(baseUrl);
|
|
24
|
+
await service.interactive();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Extrai mรฉtodo e URL
|
|
29
|
+
const method = (positionals?.[1] || "GET").toUpperCase() as HttpRequest["method"];
|
|
30
|
+
let url = positionals?.[2] || "/";
|
|
31
|
+
|
|
32
|
+
// Se URL nรฃo comeรงar com http, adiciona base
|
|
33
|
+
const baseUrl = args?.["base-url"] || `http://localhost:${config.tomcat.port}`;
|
|
34
|
+
if (!url.startsWith("http")) {
|
|
35
|
+
url = `${baseUrl}${url.startsWith("/") ? url : `/${url}`}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Parse headers
|
|
39
|
+
const headers = this.parseHeaders(args);
|
|
40
|
+
|
|
41
|
+
// Parse body
|
|
42
|
+
let body: string | object | undefined;
|
|
43
|
+
if (args?.body) {
|
|
44
|
+
body = this.parseBody(args.body as string);
|
|
45
|
+
} else if (args?.file) {
|
|
46
|
+
const filePath = args.file as string;
|
|
47
|
+
if (fs.existsSync(filePath)) {
|
|
48
|
+
body = fs.readFileSync(filePath, "utf-8");
|
|
49
|
+
} else {
|
|
50
|
+
Logger.error(`File not found: ${filePath}`);
|
|
51
|
+
await processManager.shutdown(1);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse query params
|
|
57
|
+
const params = this.parseParams(args);
|
|
58
|
+
|
|
59
|
+
// Cria request
|
|
60
|
+
const request: HttpRequest = {
|
|
61
|
+
method,
|
|
62
|
+
url,
|
|
63
|
+
headers,
|
|
64
|
+
body,
|
|
65
|
+
params,
|
|
66
|
+
timeout: args?.timeout ? parseInt(args.timeout as string) : 30000
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Executa
|
|
70
|
+
const service = new HttpService();
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await service.request(request);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
await processManager.shutdown(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private parseHeaders(args?: CLIArguments): Record<string, string> | undefined {
|
|
80
|
+
const headers: Record<string, string> = {};
|
|
81
|
+
|
|
82
|
+
// Formato: --header "Authorization: Bearer token"
|
|
83
|
+
const headerArgs = args?.header;
|
|
84
|
+
if (headerArgs) {
|
|
85
|
+
const headerList = Array.isArray(headerArgs) ? headerArgs : [headerArgs];
|
|
86
|
+
for (const h of headerList) {
|
|
87
|
+
const match = h.match(/^([^:]+):\s*(.+)$/);
|
|
88
|
+
if (match) {
|
|
89
|
+
headers[match[1]] = match[2];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Content-Type shorthand
|
|
95
|
+
if (args?.["content-type"]) {
|
|
96
|
+
headers["Content-Type"] = args["content-type"] as string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Accept shorthand
|
|
100
|
+
if (args?.accept) {
|
|
101
|
+
headers["Accept"] = args.accept as string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private parseBody(bodyStr: string): string | object {
|
|
108
|
+
// Tenta parsear como JSON
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(bodyStr);
|
|
111
|
+
} catch {
|
|
112
|
+
// Retorna como string
|
|
113
|
+
return bodyStr;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private parseParams(args?: CLIArguments): Record<string, string> | undefined {
|
|
118
|
+
const params: Record<string, string> = {};
|
|
119
|
+
|
|
120
|
+
// Formato: --param "key=value"
|
|
121
|
+
const paramArgs = args?.param;
|
|
122
|
+
if (paramArgs) {
|
|
123
|
+
const paramList = Array.isArray(paramArgs) ? paramArgs : [paramArgs];
|
|
124
|
+
for (const p of paramList) {
|
|
125
|
+
const [key, value] = p.split("=");
|
|
126
|
+
if (key && value !== undefined) {
|
|
127
|
+
params[key] = value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return Object.keys(params).length > 0 ? params : undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|