@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.
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Serviço de execução de testes
3
+ * Suporta Maven e Gradle com modo watch
4
+ */
5
+
6
+ import { Logger } from "../utils/ui";
7
+ import { spawn } from "child_process";
8
+ import { watch, type FSWatcher } from "fs";
9
+ import path from "path";
10
+ import { promisify } from "util";
11
+
12
+ export interface TestOptions {
13
+ watch?: boolean;
14
+ coverage?: boolean;
15
+ filter?: string;
16
+ verbose?: boolean;
17
+ failFast?: boolean;
18
+ parallel?: boolean;
19
+ }
20
+
21
+ export interface TestResult {
22
+ success: boolean;
23
+ totalTests: number;
24
+ passed: number;
25
+ failed: number;
26
+ skipped: number;
27
+ duration: number;
28
+ failures: TestFailure[];
29
+ }
30
+
31
+ export interface TestFailure {
32
+ className: string;
33
+ methodName: string;
34
+ message: string;
35
+ stackTrace?: string;
36
+ }
37
+
38
+ export class TestService {
39
+ private buildTool: "maven" | "gradle";
40
+ private watcher: FSWatcher | null = null;
41
+ private isRunning = false;
42
+
43
+ constructor(buildTool: "maven" | "gradle") {
44
+ this.buildTool = buildTool;
45
+ }
46
+
47
+ async runTests(options: TestOptions = {}): Promise<TestResult> {
48
+ if (this.isRunning) {
49
+ Logger.warn("Test execution already in progress...");
50
+ return this.createEmptyResult();
51
+ }
52
+
53
+ this.isRunning = true;
54
+ const startTime = Date.now();
55
+
56
+ try {
57
+ Logger.section("Running Tests");
58
+
59
+ if (options.filter) {
60
+ Logger.info("Filter", options.filter);
61
+ }
62
+ if (options.coverage) {
63
+ Logger.info("Coverage", "enabled");
64
+ }
65
+
66
+ const command = this.buildCommand(options);
67
+ const result = await this.executeTests(command, options.verbose);
68
+
69
+ result.duration = Date.now() - startTime;
70
+ this.printResults(result);
71
+
72
+ return result;
73
+ } finally {
74
+ this.isRunning = false;
75
+ Logger.endSection();
76
+ }
77
+ }
78
+
79
+ startWatch(options: TestOptions = {}): void {
80
+ if (this.watcher) {
81
+ Logger.warn("Test watcher already running");
82
+ return;
83
+ }
84
+
85
+ Logger.section("Test Watch Mode");
86
+ Logger.info("Watching", "src/test/**/*");
87
+ Logger.info("Press", "Ctrl+C to stop");
88
+ Logger.endSection();
89
+
90
+ // Run tests initially
91
+ this.runTests(options);
92
+
93
+ // Watch for changes
94
+ const testPath = path.join(process.cwd(), "src", "test");
95
+ const mainPath = path.join(process.cwd(), "src", "main");
96
+
97
+ this.watcher = watch(
98
+ [testPath, mainPath],
99
+ { recursive: true },
100
+ (eventType, filename) => {
101
+ if (filename && this.isTestFile(filename)) {
102
+ Logger.watch(`Test file changed: ${filename}`);
103
+ this.debounceRun(options);
104
+ }
105
+ }
106
+ );
107
+ }
108
+
109
+ stopWatch(): void {
110
+ if (this.watcher) {
111
+ this.watcher.close();
112
+ this.watcher = null;
113
+ Logger.info("Test watcher stopped");
114
+ }
115
+ }
116
+
117
+ private debounceRun(options: TestOptions): void {
118
+ // Simple debounce
119
+ if (this.debounceTimer) {
120
+ clearTimeout(this.debounceTimer);
121
+ }
122
+ this.debounceTimer = setTimeout(() => {
123
+ this.runTests(options);
124
+ }, 500);
125
+ }
126
+
127
+ private debounceTimer: Timer | null = null;
128
+
129
+ private buildCommand(options: TestOptions): string[] {
130
+ if (this.buildTool === "maven") {
131
+ return this.buildMavenCommand(options);
132
+ } else {
133
+ return this.buildGradleCommand(options);
134
+ }
135
+ }
136
+
137
+ private buildMavenCommand(options: TestOptions): string[] {
138
+ const cmd = process.platform === "win32" ? "mvn.cmd" : "mvn";
139
+ const args: string[] = [cmd];
140
+
141
+ if (options.coverage) {
142
+ args.push("jacoco:prepare-agent");
143
+ }
144
+
145
+ args.push("test");
146
+
147
+ if (options.coverage) {
148
+ args.push("jacoco:report");
149
+ }
150
+
151
+ if (options.filter) {
152
+ // Maven surefire plugin syntax
153
+ args.push(`-Dtest=${options.filter}`);
154
+ }
155
+
156
+ if (options.failFast) {
157
+ args.push("-Dsurefire.failIfNoSpecifiedTests=false");
158
+ }
159
+
160
+ if (options.parallel) {
161
+ args.push("-Dsurefire.parallel=methods");
162
+ args.push("-Dsurefire.threadCount=4");
163
+ }
164
+
165
+ if (!options.verbose) {
166
+ args.push("-q");
167
+ }
168
+
169
+ return args;
170
+ }
171
+
172
+ private buildGradleCommand(options: TestOptions): string[] {
173
+ const cmd = process.platform === "win32" ? "gradle.bat" : "gradle";
174
+ const args: string[] = [cmd, "test"];
175
+
176
+ if (options.coverage) {
177
+ args.push("jacocoTestReport");
178
+ }
179
+
180
+ if (options.filter) {
181
+ args.push(`--tests`, options.filter);
182
+ }
183
+
184
+ if (options.failFast) {
185
+ args.push("--fail-fast");
186
+ }
187
+
188
+ if (options.parallel) {
189
+ args.push("--parallel");
190
+ }
191
+
192
+ if (!options.verbose) {
193
+ args.push("-q");
194
+ }
195
+
196
+ return args;
197
+ }
198
+
199
+ private executeTests(command: string[], verbose: boolean = false): Promise<TestResult> {
200
+ return new Promise((resolve) => {
201
+ const [cmd, ...args] = command;
202
+ const child = spawn(cmd, args, {
203
+ cwd: process.cwd(),
204
+ stdio: verbose ? "inherit" : "pipe",
205
+ shell: process.platform === "win32"
206
+ });
207
+
208
+ let stdout = "";
209
+ let stderr = "";
210
+
211
+ if (!verbose) {
212
+ child.stdout?.on("data", (data) => {
213
+ stdout += data.toString();
214
+ });
215
+ child.stderr?.on("data", (data) => {
216
+ stderr += data.toString();
217
+ });
218
+ }
219
+
220
+ const spinner = verbose ? () => {} : Logger.spinner("Running tests");
221
+
222
+ child.on("close", (code) => {
223
+ spinner(code === 0);
224
+ const result = this.parseTestOutput(stdout + stderr, code === 0);
225
+ resolve(result);
226
+ });
227
+
228
+ child.on("error", (error) => {
229
+ spinner(false);
230
+ Logger.error(`Failed to run tests: ${error.message}`);
231
+ resolve(this.createEmptyResult());
232
+ });
233
+ });
234
+ }
235
+
236
+ private parseTestOutput(output: string, success: boolean): TestResult {
237
+ const result: TestResult = {
238
+ success,
239
+ totalTests: 0,
240
+ passed: 0,
241
+ failed: 0,
242
+ skipped: 0,
243
+ duration: 0,
244
+ failures: []
245
+ };
246
+
247
+ if (this.buildTool === "maven") {
248
+ // Parse Maven Surefire output
249
+ const testsRun = output.match(/Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)/);
250
+ if (testsRun) {
251
+ result.totalTests = parseInt(testsRun[1]);
252
+ result.failed = parseInt(testsRun[2]) + parseInt(testsRun[3]);
253
+ result.skipped = parseInt(testsRun[4]);
254
+ result.passed = result.totalTests - result.failed - result.skipped;
255
+ }
256
+
257
+ // Parse failures
258
+ const failureMatches = output.matchAll(/\[ERROR\]\s+(\S+)\.(\S+)\s+\[(.+?)\]\s+(.+)/g);
259
+ for (const match of failureMatches) {
260
+ result.failures.push({
261
+ className: match[1],
262
+ methodName: match[2],
263
+ message: match[4]
264
+ });
265
+ }
266
+ } else {
267
+ // Parse Gradle output
268
+ const testsRun = output.match(/(\d+) tests completed,(\s*\d+ failed)?/);
269
+ if (testsRun) {
270
+ result.totalTests = parseInt(testsRun[1]);
271
+ const failed = testsRun[2] ? parseInt(testsRun[2].trim()) : 0;
272
+ result.failed = failed;
273
+ result.passed = result.totalTests - result.failed;
274
+ }
275
+ }
276
+
277
+ return result;
278
+ }
279
+
280
+ private printResults(result: TestResult): void {
281
+ Logger.divider();
282
+
283
+ if (result.success && result.failed === 0) {
284
+ Logger.success(`All tests passed! (${result.passed} tests)`);
285
+ } else if (result.failed > 0) {
286
+ Logger.error(`${result.failed} test(s) failed`);
287
+ }
288
+
289
+ Logger.info("Total", result.totalTests);
290
+ Logger.info("Passed", `${Logger.C.success}${result.passed}${Logger.C.reset}`);
291
+ Logger.info("Failed", result.failed > 0 ? `${Logger.C.error}${result.failed}${Logger.C.reset}` : "0");
292
+ Logger.info("Skipped", result.skipped);
293
+ Logger.info("Duration", `${(result.duration / 1000).toFixed(2)}s`);
294
+
295
+ if (result.failures.length > 0) {
296
+ Logger.divider();
297
+ Logger.section("Failures");
298
+ for (const failure of result.failures.slice(0, 5)) {
299
+ Logger.error(`${failure.className}.${failure.methodName}`);
300
+ Logger.dim(` ${failure.message}`);
301
+ }
302
+ if (result.failures.length > 5) {
303
+ Logger.dim(` ... and ${result.failures.length - 5} more`);
304
+ }
305
+ Logger.endSection();
306
+ }
307
+ }
308
+
309
+ private isTestFile(filename: string): boolean {
310
+ return filename.endsWith("Test.java") ||
311
+ filename.endsWith("IT.java") ||
312
+ filename.endsWith("Tests.java");
313
+ }
314
+
315
+ private createEmptyResult(): TestResult {
316
+ return {
317
+ success: false,
318
+ totalTests: 0,
319
+ passed: 0,
320
+ failed: 0,
321
+ skipped: 0,
322
+ duration: 0,
323
+ failures: []
324
+ };
325
+ }
326
+ }
package/src/types/args.ts CHANGED
@@ -9,6 +9,7 @@ export interface BaseArgs {
9
9
  version?: boolean;
10
10
  verbose?: boolean;
11
11
  quiet?: boolean;
12
+ "debug-level"?: "silent" | "error" | "warn" | "info" | "verbose" | "trace" | "silly";
12
13
  }
13
14
 
14
15
  // ===== Args de Configuração de Projeto =====
@@ -1,3 +1,16 @@
1
+ export interface EnvironmentConfig {
2
+ port?: number;
3
+ profile?: string;
4
+ db?: {
5
+ url?: string;
6
+ username?: string;
7
+ password?: string;
8
+ driver?: string;
9
+ };
10
+ tomcat?: Partial<TomcatConfig>;
11
+ env?: Record<string, string>;
12
+ }
13
+
1
14
  export interface TomcatConfig {
2
15
  path: string;
3
16
  port: number;
@@ -24,6 +37,8 @@ export interface ProjectConfig {
24
37
  encoding?: string;
25
38
  war?: boolean;
26
39
  cache?: boolean;
40
+ environment?: string;
41
+ environments?: Record<string, EnvironmentConfig>;
27
42
  }
28
43
 
29
44
  export interface AppConfig {
@@ -68,6 +83,29 @@ export interface CLIArguments {
68
83
  "dry-run"?: boolean;
69
84
  force?: boolean;
70
85
  src?: string;
86
+ // Multi-environment
87
+ env?: string;
88
+ environment?: string;
89
+ // Test runner
90
+ coverage?: boolean;
91
+ "fail-fast"?: boolean;
92
+ parallel?: boolean;
93
+ // HTTP client
94
+ interactive?: boolean;
95
+ "base-url"?: string;
96
+ body?: string;
97
+ file?: string;
98
+ header?: string | string[];
99
+ "content-type"?: string;
100
+ accept?: string;
101
+ param?: string | string[];
102
+ timeout?: string;
103
+ // Docker
104
+ tag?: string;
105
+ "java-version"?: string;
106
+ detached?: boolean;
107
+ registry?: string;
108
+ namespace?: string;
71
109
  }
72
110
 
73
111
  export interface CommandContext {
@@ -0,0 +1,255 @@
1
+ import { execSync } from "child_process";
2
+ import { writeFileSync } from "fs";
3
+ import { join } from "path";
4
+
5
+ interface Commit {
6
+ hash: string;
7
+ date: string;
8
+ message: string;
9
+ type: string;
10
+ scope?: string;
11
+ subject: string;
12
+ breaking: boolean;
13
+ }
14
+
15
+ interface Version {
16
+ version: string;
17
+ date: string;
18
+ commits: Commit[];
19
+ }
20
+
21
+ export class ChangelogGenerator {
22
+ private static readonly TYPES: Record<string, { title: string; emoji: string }> = {
23
+ feat: { title: "Features", emoji: "✨" },
24
+ fix: { title: "Bug Fixes", emoji: "🐛" },
25
+ docs: { title: "Documentation", emoji: "📚" },
26
+ style: { title: "Styles", emoji: "💎" },
27
+ refactor: { title: "Code Refactoring", emoji: "♻️" },
28
+ perf: { title: "Performance", emoji: "⚡" },
29
+ test: { title: "Tests", emoji: "🧪" },
30
+ build: { title: "Build System", emoji: "🏗️" },
31
+ ci: { title: "CI/CD", emoji: "🔄" },
32
+ chore: { title: "Chores", emoji: "🔧" },
33
+ revert: { title: "Reverts", emoji: "⏪" },
34
+ };
35
+
36
+ static generate(): string {
37
+ const commits = this.getCommits();
38
+ const versions = this.groupByVersion(commits);
39
+ return this.formatChangelog(versions);
40
+ }
41
+
42
+ static generateAndSave(outputPath: string = "CHANGELOG.md"): void {
43
+ const changelog = this.generate();
44
+ writeFileSync(outputPath, changelog);
45
+ }
46
+
47
+ private static getCommits(): Commit[] {
48
+ try {
49
+ // Get commits in format: hash|date|message
50
+ const log = execSync(
51
+ 'git log --pretty=format:"%h|%ad|%s" --date=short --no-merges',
52
+ { encoding: "utf-8", cwd: process.cwd() }
53
+ );
54
+
55
+ return log
56
+ .trim()
57
+ .split("\n")
58
+ .map(line => this.parseCommit(line))
59
+ .filter((c): c is Commit => c !== null);
60
+ } catch {
61
+ return [];
62
+ }
63
+ }
64
+
65
+ private static parseCommit(line: string): Commit | null {
66
+ const match = line.match(/^([^|]+)\|([^|]+)\|(.+)$/);
67
+ if (!match) return null;
68
+
69
+ const [, hash, date, message] = match;
70
+ const parsed = this.parseConventionalCommit(message);
71
+
72
+ return {
73
+ hash,
74
+ date,
75
+ message,
76
+ ...parsed,
77
+ };
78
+ }
79
+
80
+ private static parseConventionalCommit(message: string): Omit<Commit, "hash" | "date" | "message"> {
81
+ // Pattern: type(scope)!: subject
82
+ // or: type!: subject
83
+ // or: type(scope): subject
84
+ // or: type: subject
85
+ const pattern = /^(\w+)(?:\(([^)]+)\))?(!)?: (.+)$/;
86
+ const match = message.match(pattern);
87
+
88
+ if (match) {
89
+ const [, type, scope, breaking, subject] = match;
90
+ return {
91
+ type,
92
+ scope,
93
+ subject,
94
+ breaking: !!breaking || subject.includes("BREAKING CHANGE"),
95
+ };
96
+ }
97
+
98
+ // Fallback: treat as chore if doesn't match conventional commit
99
+ return {
100
+ type: "chore",
101
+ subject: message,
102
+ breaking: message.includes("BREAKING CHANGE"),
103
+ };
104
+ }
105
+
106
+ private static groupByVersion(commits: Commit[]): Version[] {
107
+ // Group by version tags
108
+ const versions: Version[] = [];
109
+ let currentVersion = "Unreleased";
110
+ let currentDate = new Date().toISOString().split("T")[0];
111
+ let currentCommits: Commit[] = [];
112
+
113
+ // Try to get version tags
114
+ const tags = this.getVersionTags();
115
+
116
+ if (tags.length === 0) {
117
+ // No tags, all commits are unreleased
118
+ return [{
119
+ version: "Unreleased",
120
+ date: currentDate,
121
+ commits,
122
+ }];
123
+ }
124
+
125
+ // Process commits and assign to versions
126
+ const versionMap = new Map<string, Commit[]>();
127
+
128
+ for (const commit of commits) {
129
+ // Find which version this commit belongs to
130
+ const version = this.findVersionForCommit(commit.hash, tags);
131
+ if (!versionMap.has(version)) {
132
+ versionMap.set(version, []);
133
+ }
134
+ versionMap.get(version)!.push(commit);
135
+ }
136
+
137
+ // Convert to array
138
+ for (const [version, versionCommits] of versionMap) {
139
+ const tagDate = this.getTagDate(version === "Unreleased" ? null : version);
140
+ versions.push({
141
+ version,
142
+ date: tagDate || currentDate,
143
+ commits: versionCommits,
144
+ });
145
+ }
146
+
147
+ // Sort by version (newest first)
148
+ return versions.sort((a, b) => this.compareVersions(b.version, a.version));
149
+ }
150
+
151
+ private static getVersionTags(): string[] {
152
+ try {
153
+ const tags = execSync("git tag -l 'v*' --sort=-v:refname", { encoding: "utf-8" });
154
+ return tags.trim().split("\n").filter(Boolean);
155
+ } catch {
156
+ return [];
157
+ }
158
+ }
159
+
160
+ private static findVersionForCommit(hash: string, tags: string[]): string {
161
+ try {
162
+ // Check if commit is after a specific tag
163
+ for (const tag of tags) {
164
+ const result = execSync(`git merge-base --is-ancestor ${hash} ${tag} && echo "in" || echo "out"`, {
165
+ encoding: "utf-8",
166
+ cwd: process.cwd(),
167
+ });
168
+ if (result.trim() === "in") {
169
+ return tag;
170
+ }
171
+ }
172
+ } catch {
173
+ // Ignore errors
174
+ }
175
+ return "Unreleased";
176
+ }
177
+
178
+ private static getTagDate(tag: string | null): string | null {
179
+ if (!tag) return null;
180
+ try {
181
+ const date = execSync(`git log -1 --format=%ad --date=short ${tag}`, { encoding: "utf-8" });
182
+ return date.trim();
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+
188
+ private static compareVersions(a: string, b: string): number {
189
+ if (a === "Unreleased") return -1;
190
+ if (b === "Unreleased") return 1;
191
+
192
+ const parse = (v: string) => v.replace(/^v/, "").split(".").map(Number);
193
+ const aParts = parse(a);
194
+ const bParts = parse(b);
195
+
196
+ for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
197
+ const aPart = aParts[i] || 0;
198
+ const bPart = bParts[i] || 0;
199
+ if (aPart !== bPart) return aPart - bPart;
200
+ }
201
+ return 0;
202
+ }
203
+
204
+ private static formatChangelog(versions: Version[]): string {
205
+ const lines: string[] = [
206
+ "# Changelog\n",
207
+ "All notable changes to this project will be documented in this file.\n",
208
+ "The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),",
209
+ "and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n",
210
+ ];
211
+
212
+ for (const version of versions) {
213
+ lines.push(this.formatVersion(version));
214
+ }
215
+
216
+ return lines.join("\n");
217
+ }
218
+
219
+ private static formatVersion(version: Version): string {
220
+ const lines: string[] = [
221
+ `## [${version.version}] - ${version.date}`,
222
+ "",
223
+ ];
224
+
225
+ // Group commits by type
226
+ const byType = new Map<string, Commit[]>();
227
+ for (const commit of version.commits) {
228
+ if (!byType.has(commit.type)) {
229
+ byType.set(commit.type, []);
230
+ }
231
+ byType.get(commit.type)!.push(commit);
232
+ }
233
+
234
+ // Output in conventional order
235
+ const typeOrder = Object.keys(this.TYPES);
236
+
237
+ for (const type of typeOrder) {
238
+ const commits = byType.get(type);
239
+ if (!commits || commits.length === 0) continue;
240
+
241
+ const { title, emoji } = this.TYPES[type];
242
+ lines.push(`### ${emoji} ${title}\n`);
243
+
244
+ for (const commit of commits) {
245
+ const scope = commit.scope ? `**${commit.scope}**: ` : "";
246
+ const breaking = commit.breaking ? " 💥 **BREAKING CHANGE**" : "";
247
+ lines.push(`- ${scope}${commit.subject} ([${commit.hash}])${breaking}`);
248
+ }
249
+
250
+ lines.push("");
251
+ }
252
+
253
+ return lines.join("\n");
254
+ }
255
+ }