@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.
@@ -104,6 +104,12 @@ export class InitCommand implements Command {
104
104
  default: "UTF-8"
105
105
  });
106
106
 
107
+ // Multi-environment setup
108
+ const enableMultiEnv = await confirm({
109
+ message: "Configure multiple environments?",
110
+ default: false
111
+ });
112
+
107
113
  // Build config object
108
114
  const config: Record<string, unknown> = {
109
115
  appName,
@@ -142,6 +148,70 @@ export class InitCommand implements Command {
142
148
  config.tomcatPath = tomcatPath;
143
149
  }
144
150
 
151
+ // Add environments if enabled
152
+ if (enableMultiEnv) {
153
+ Logger.newline();
154
+ Logger.dim("Environment Configuration:");
155
+
156
+ const environments: Record<string, unknown> = {};
157
+
158
+ // Dev environment
159
+ const devPort = await number({
160
+ message: "Dev environment port:",
161
+ default: port
162
+ }) || port;
163
+ environments.dev = {
164
+ port: devPort,
165
+ profile: "dev"
166
+ };
167
+
168
+ // Test environment
169
+ const testPort = await number({
170
+ message: "Test environment port:",
171
+ default: port + 1
172
+ }) || port + 1;
173
+ environments.test = {
174
+ port: testPort,
175
+ profile: "test"
176
+ };
177
+
178
+ // Staging environment
179
+ const hasStaging = await confirm({
180
+ message: "Add staging environment?",
181
+ default: true
182
+ });
183
+
184
+ if (hasStaging) {
185
+ const stagingPort = await number({
186
+ message: "Staging environment port:",
187
+ default: port + 2
188
+ }) || port + 2;
189
+ environments.staging = {
190
+ port: stagingPort,
191
+ profile: "staging"
192
+ };
193
+ }
194
+
195
+ config.environments = environments;
196
+
197
+ // Add DB config example
198
+ const addDbExample = await confirm({
199
+ message: "Add database configuration example?",
200
+ default: true
201
+ });
202
+
203
+ if (addDbExample) {
204
+ environments.dev = {
205
+ ...environments.dev,
206
+ db: {
207
+ url: "jdbc:h2:mem:devdb",
208
+ username: "sa",
209
+ password: ""
210
+ }
211
+ };
212
+ }
213
+ }
214
+
145
215
  // Save file
146
216
  Logger.newline();
147
217
  Logger.step("Saving configuration...");
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Comando de execução de testes
3
+ * xavva test [options] [filter]
4
+ */
5
+
6
+ import type { Command } from "./Command";
7
+ import type { AppConfig, CLIArguments } from "../types/config";
8
+ import { TestService } from "../services/TestService";
9
+ import { Logger } from "../utils/ui";
10
+ import { ProcessManager } from "../utils/processManager";
11
+
12
+ export class TestCommand implements Command {
13
+ private service: TestService | null = null;
14
+
15
+ async execute(config: AppConfig, args?: CLIArguments, positionals?: string[]): Promise<void> {
16
+ const processManager = ProcessManager.getInstance();
17
+
18
+ // Extrai filtros de teste dos positionals (após o comando "test")
19
+ const filter = positionals?.slice(1).join(" ") || undefined;
20
+
21
+ // Opções
22
+ const watch = args?.watch || false;
23
+ const coverage = args?.coverage || false;
24
+ const verbose = args?.verbose || false;
25
+ const failFast = args?.["fail-fast"] || false;
26
+ const parallel = args?.parallel || false;
27
+
28
+ this.service = new TestService(config.project.buildTool);
29
+
30
+ try {
31
+ if (watch) {
32
+ this.service.startWatch({
33
+ coverage,
34
+ filter,
35
+ verbose,
36
+ failFast,
37
+ parallel
38
+ });
39
+
40
+ // Mantém processo rodando
41
+ process.on("SIGINT", () => {
42
+ this.service?.stopWatch();
43
+ processManager.shutdown(0);
44
+ });
45
+ } else {
46
+ const result = await this.service.runTests({
47
+ coverage,
48
+ filter,
49
+ verbose,
50
+ failFast,
51
+ parallel
52
+ });
53
+
54
+ if (!result.success) {
55
+ await processManager.shutdown(1);
56
+ }
57
+ }
58
+ } catch (error) {
59
+ Logger.error(`Test execution failed: ${(error as Error).message}`);
60
+ await processManager.shutdown(1);
61
+ }
62
+ }
63
+ }
@@ -32,6 +32,10 @@ import { HealthCommand } from "../commands/HealthCommand";
32
32
  import { CompletionCommand } from "../commands/CompletionCommand";
33
33
  import { ChangelogCommand } from "../commands/ChangelogCommand";
34
34
  import { HistoryService } from "../services/HistoryService";
35
+ import { TestCommand } from "../commands/TestCommand";
36
+ import { DbCommand } from "../commands/DbCommand";
37
+ import { HttpCommand } from "../commands/HttpCommand";
38
+ import { DockerCommand } from "../commands/DockerCommand";
35
39
  import { NotificationService } from "../services/NotificationService";
36
40
  import type { Command } from "../commands/Command";
37
41
  import { Logger } from "../utils/ui";
@@ -70,6 +74,10 @@ export interface Commands {
70
74
  health: HealthCommand;
71
75
  completion: CompletionCommand;
72
76
  changelog: ChangelogCommand;
77
+ test: TestCommand;
78
+ db: DbCommand;
79
+ http: HttpCommand;
80
+ docker: DockerCommand;
73
81
  }
74
82
 
75
83
  export class DIContainer {
@@ -166,6 +174,10 @@ export class DIContainer {
166
174
  health: new HealthCommand(),
167
175
  completion: new CompletionCommand(),
168
176
  changelog: new ChangelogCommand(),
177
+ test: new TestCommand(),
178
+ db: new DbCommand(),
179
+ http: new HttpCommand(),
180
+ docker: new DockerCommand(),
169
181
  };
170
182
  }
171
183
 
package/src/index.ts CHANGED
@@ -31,7 +31,8 @@ async function main() {
31
31
  "deploy", "build", "start", "dev", "doctor", "run",
32
32
  "debug", "logs", "docs", "audit", "profiles",
33
33
  "deps", "tomcat", "encoding", "init", "config",
34
- "history", "redo", "health", "completion", "changelog", "help"
34
+ "history", "redo", "health", "completion", "changelog",
35
+ "test", "db", "http", "docker", "help"
35
36
  ];
36
37
  const commandName = positionals.find(p => commandNames.includes(p)) || "deploy";
37
38
 
@@ -111,6 +112,10 @@ async function main() {
111
112
  registry.register("health", commands.health);
112
113
  registry.register("completion", commands.completion);
113
114
  registry.register("changelog", commands.changelog);
115
+ registry.register("test", commands.test);
116
+ registry.register("db", commands.db);
117
+ registry.register("http", commands.http);
118
+ registry.register("docker", commands.docker);
114
119
 
115
120
  // Configura flags específicas
116
121
  if (commandName === "debug") values.debug = true;
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Serviço de migrações de banco de dados
3
+ * Suporta Flyway e Liquibase
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 type MigrationTool = "flyway" | "liquibase" | "auto";
12
+
13
+ export interface DbConfig {
14
+ url: string;
15
+ username: string;
16
+ password: string;
17
+ driver?: string;
18
+ migrationsPath?: string;
19
+ }
20
+
21
+ export interface MigrationStatus {
22
+ version: string;
23
+ description: string;
24
+ state: "pending" | "applied" | "failed";
25
+ installedOn?: Date;
26
+ executionTime?: number;
27
+ }
28
+
29
+ export interface MigrationResult {
30
+ success: boolean;
31
+ message: string;
32
+ migrationsApplied: number;
33
+ errors: string[];
34
+ }
35
+
36
+ export class DbService {
37
+ private buildTool: "maven" | "gradle";
38
+ private projectPath: string;
39
+
40
+ constructor(buildTool: "maven" | "gradle", projectPath: string = process.cwd()) {
41
+ this.buildTool = buildTool;
42
+ this.projectPath = projectPath;
43
+ }
44
+
45
+ /**
46
+ * Detecta qual ferramenta de migração está configurada no projeto
47
+ */
48
+ async detectTool(): Promise<MigrationTool> {
49
+ const pomPath = path.join(this.projectPath, "pom.xml");
50
+ const buildGradlePath = path.join(this.projectPath, "build.gradle");
51
+ const buildGradleKtsPath = path.join(this.projectPath, "build.gradle.kts");
52
+
53
+ let content = "";
54
+ if (fs.existsSync(pomPath)) {
55
+ content = fs.readFileSync(pomPath, "utf-8");
56
+ } else if (fs.existsSync(buildGradlePath)) {
57
+ content = fs.readFileSync(buildGradlePath, "utf-8");
58
+ } else if (fs.existsSync(buildGradleKtsPath)) {
59
+ content = fs.readFileSync(buildGradleKtsPath, "utf-8");
60
+ }
61
+
62
+ if (content.includes("flyway") || content.includes("flyway-core")) {
63
+ return "flyway";
64
+ }
65
+ if (content.includes("liquibase") || content.includes("liquibase-core")) {
66
+ return "liquibase";
67
+ }
68
+
69
+ // Verificar arquivos de configuração
70
+ if (fs.existsSync(path.join(this.projectPath, "src", "main", "resources", "db", "migration"))) {
71
+ return "flyway";
72
+ }
73
+ if (fs.existsSync(path.join(this.projectPath, "src", "main", "resources", "db", "changelog"))) {
74
+ return "liquibase";
75
+ }
76
+
77
+ return "auto";
78
+ }
79
+
80
+ /**
81
+ * Executa migrações pendentes
82
+ */
83
+ async migrate(config?: DbConfig): Promise<MigrationResult> {
84
+ const tool = await this.detectTool();
85
+ Logger.section("Database Migration");
86
+ Logger.info("Tool", tool === "auto" ? "auto-detect" : tool);
87
+
88
+ if (tool === "auto") {
89
+ return {
90
+ success: false,
91
+ message: "No migration tool detected. Please add Flyway or Liquibase to your project.",
92
+ migrationsApplied: 0,
93
+ errors: ["No migration tool found"]
94
+ };
95
+ }
96
+
97
+ const result = tool === "flyway"
98
+ ? await this.runFlywayMigrate(config)
99
+ : await this.runLiquibaseUpdate(config);
100
+
101
+ Logger.endSection();
102
+ return result;
103
+ }
104
+
105
+ /**
106
+ * Mostra status das migrações
107
+ */
108
+ async status(config?: DbConfig): Promise<MigrationStatus[]> {
109
+ const tool = await this.detectTool();
110
+ Logger.section("Migration Status");
111
+ Logger.info("Tool", tool);
112
+
113
+ if (tool === "auto") {
114
+ Logger.warn("No migration tool detected");
115
+ Logger.endSection();
116
+ return [];
117
+ }
118
+
119
+ const statuses = tool === "flyway"
120
+ ? await this.runFlywayInfo(config)
121
+ : await this.runLiquibaseStatus(config);
122
+
123
+ // Print status table
124
+ if (statuses.length > 0) {
125
+ Logger.divider();
126
+ for (const status of statuses) {
127
+ const stateColor = status.state === "applied" ? Logger.C.success
128
+ : status.state === "failed" ? Logger.C.error
129
+ : Logger.C.warning;
130
+ Logger.info(status.version, `${stateColor}${status.state}${Logger.C.reset} - ${status.description}`);
131
+ }
132
+ } else {
133
+ Logger.info("Status", "No migrations found");
134
+ }
135
+
136
+ Logger.endSection();
137
+ return statuses;
138
+ }
139
+
140
+ /**
141
+ * Reseta o banco (drop all + migrate)
142
+ */
143
+ async reset(config?: DbConfig): Promise<MigrationResult> {
144
+ Logger.section("Database Reset");
145
+ Logger.warn("This will DROP ALL DATA in the database!");
146
+
147
+ const tool = await this.detectTool();
148
+
149
+ if (tool === "flyway") {
150
+ return await this.runFlywayClean(config);
151
+ } else if (tool === "liquibase") {
152
+ return await this.runLiquibaseDropAll(config);
153
+ }
154
+
155
+ Logger.endSection();
156
+ return {
157
+ success: false,
158
+ message: "No migration tool detected",
159
+ migrationsApplied: 0,
160
+ errors: []
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Popula dados de teste/seed
166
+ */
167
+ async seed(config?: DbConfig, seedFile?: string): Promise<MigrationResult> {
168
+ Logger.section("Database Seed");
169
+
170
+ // Procurar arquivos de seed
171
+ const seedPaths = [
172
+ path.join(this.projectPath, "src", "test", "resources", "seed.sql"),
173
+ path.join(this.projectPath, "src", "main", "resources", "seed.sql"),
174
+ path.join(this.projectPath, "seed.sql"),
175
+ ];
176
+
177
+ const seedPath = seedFile || seedPaths.find(p => fs.existsSync(p));
178
+
179
+ if (!seedPath) {
180
+ return {
181
+ success: false,
182
+ message: "No seed file found. Create seed.sql in src/test/resources/ or project root.",
183
+ migrationsApplied: 0,
184
+ errors: ["Seed file not found"]
185
+ };
186
+ }
187
+
188
+ Logger.info("Seed file", seedPath);
189
+
190
+ // Executar seed via JDBC ou comando SQL
191
+ const result = await this.executeSeed(seedPath, config);
192
+
193
+ Logger.endSection();
194
+ return result;
195
+ }
196
+
197
+ // ===== Flyway Commands =====
198
+
199
+ private async runFlywayMigrate(config?: DbConfig): Promise<MigrationResult> {
200
+ return this.runMavenOrGradle("flyway:migrate", "flywayMigrate", config);
201
+ }
202
+
203
+ private async runFlywayInfo(config?: DbConfig): Promise<MigrationStatus[]> {
204
+ const output = await this.runMavenOrGradleOutput("flyway:info", "flywayInfo", config);
205
+ return this.parseFlywayInfo(output);
206
+ }
207
+
208
+ private async runFlywayClean(config?: DbConfig): Promise<MigrationResult> {
209
+ return this.runMavenOrGradle("flyway:clean", "flywayClean", config);
210
+ }
211
+
212
+ // ===== Liquibase Commands =====
213
+
214
+ private async runLiquibaseUpdate(config?: DbConfig): Promise<MigrationResult> {
215
+ return this.runMavenOrGradle("liquibase:update", "liquibaseUpdate", config);
216
+ }
217
+
218
+ private async runLiquibaseStatus(config?: DbConfig): Promise<MigrationStatus[]> {
219
+ const output = await this.runMavenOrGradleOutput("liquibase:status", "liquibaseStatus", config);
220
+ return this.parseLiquibaseStatus(output);
221
+ }
222
+
223
+ private async runLiquibaseDropAll(config?: DbConfig): Promise<MigrationResult> {
224
+ return this.runMavenOrGradle("liquibase:dropAll", "liquibaseDropAll", config);
225
+ }
226
+
227
+ // ===== Generic Execution =====
228
+
229
+ private async runMavenOrGradle(
230
+ mavenGoal: string,
231
+ gradleTask: string,
232
+ config?: DbConfig
233
+ ): Promise<MigrationResult> {
234
+ return new Promise((resolve) => {
235
+ const [cmd, ...args] = this.buildTool === "maven"
236
+ ? [process.platform === "win32" ? "mvn.cmd" : "mvn", mavenGoal, "-q"]
237
+ : [process.platform === "win32" ? "gradle.bat" : "gradle", gradleTask, "-q"];
238
+
239
+ const env = config ? this.buildEnv(config) : process.env;
240
+ const spinner = Logger.spinner("Running migrations");
241
+
242
+ const child = spawn(cmd, args, {
243
+ cwd: this.projectPath,
244
+ env: { ...process.env, ...env },
245
+ shell: process.platform === "win32"
246
+ });
247
+
248
+ let stdout = "";
249
+ let stderr = "";
250
+
251
+ child.stdout?.on("data", (data) => stdout += data.toString());
252
+ child.stderr?.on("data", (data) => stderr += data.toString());
253
+
254
+ child.on("close", (code) => {
255
+ spinner(code === 0);
256
+
257
+ if (code === 0) {
258
+ Logger.success("Migrations completed successfully");
259
+ } else {
260
+ Logger.error("Migration failed");
261
+ if (stderr) Logger.dim(stderr.slice(0, 500));
262
+ }
263
+
264
+ resolve({
265
+ success: code === 0,
266
+ message: code === 0 ? "Success" : stderr || "Failed",
267
+ migrationsApplied: this.countMigrations(stdout),
268
+ errors: code !== 0 ? [stderr] : []
269
+ });
270
+ });
271
+ });
272
+ }
273
+
274
+ private async runMavenOrGradleOutput(
275
+ mavenGoal: string,
276
+ gradleTask: string,
277
+ config?: DbConfig
278
+ ): Promise<string> {
279
+ return new Promise((resolve) => {
280
+ const [cmd, ...args] = this.buildTool === "maven"
281
+ ? [process.platform === "win32" ? "mvn.cmd" : "mvn", mavenGoal]
282
+ : [process.platform === "win32" ? "gradle.bat" : "gradle", gradleTask];
283
+
284
+ const env = config ? this.buildEnv(config) : process.env;
285
+ let output = "";
286
+
287
+ const child = spawn(cmd, args, {
288
+ cwd: this.projectPath,
289
+ env: { ...process.env, ...env },
290
+ shell: process.platform === "win32"
291
+ });
292
+
293
+ child.stdout?.on("data", (data) => output += data.toString());
294
+ child.stderr?.on("data", (data) => output += data.toString());
295
+
296
+ child.on("close", () => resolve(output));
297
+ });
298
+ }
299
+
300
+ private async executeSeed(seedPath: string, config?: DbConfig): Promise<MigrationResult> {
301
+ // Implementação básica - executa via JDBC se possível
302
+ // Ou gera comando SQL para execução manual
303
+
304
+ const sql = fs.readFileSync(seedPath, "utf-8");
305
+ const statements = sql.split(";").filter(s => s.trim());
306
+
307
+ Logger.info("Statements", statements.length);
308
+ Logger.success("Seed file ready for execution");
309
+ Logger.dim("Use your database client to execute the seed file");
310
+
311
+ return {
312
+ success: true,
313
+ message: `Seed file prepared: ${seedPath}`,
314
+ migrationsApplied: 0,
315
+ errors: []
316
+ };
317
+ }
318
+
319
+ private buildEnv(config: DbConfig): Record<string, string> {
320
+ return {
321
+ JDBC_URL: config.url,
322
+ JDBC_USER: config.username,
323
+ JDBC_PASSWORD: config.password,
324
+ ...(config.driver && { JDBC_DRIVER: config.driver })
325
+ };
326
+ }
327
+
328
+ private countMigrations(output: string): number {
329
+ const match = output.match(/Successfully applied (\d+) migration/);
330
+ return match ? parseInt(match[1]) : 0;
331
+ }
332
+
333
+ private parseFlywayInfo(output: string): MigrationStatus[] {
334
+ const statuses: MigrationStatus[] = [];
335
+ const lines = output.split("\n");
336
+
337
+ for (const line of lines) {
338
+ // Parse Flyway info table
339
+ const match = line.match(/\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(.+?)\s*\|/);
340
+ if (match && !line.includes("Version")) {
341
+ statuses.push({
342
+ version: match[1],
343
+ description: match[4].trim(),
344
+ state: match[3].toLowerCase() as MigrationStatus["state"]
345
+ });
346
+ }
347
+ }
348
+
349
+ return statuses;
350
+ }
351
+
352
+ private parseLiquibaseStatus(output: string): MigrationStatus[] {
353
+ const statuses: MigrationStatus[] = [];
354
+ // Simplified parsing - Liquibase output varies
355
+ return statuses;
356
+ }
357
+ }