@archznn/xavva 2.8.0 → 3.0.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 +110 -1
- package/package.json +1 -1
- package/src/commands/ChangelogCommand.ts +128 -0
- package/src/commands/HelpCommand.ts +12 -0
- package/src/commands/InitCommand.ts +159 -64
- package/src/di/container.ts +3 -0
- package/src/index.ts +9 -1
- package/src/types/args.ts +1 -0
- package/src/utils/ChangelogGenerator.ts +255 -0
- package/src/utils/LoggerLevel.ts +138 -0
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.
|
|
@@ -21,6 +21,11 @@ Xavva is a high-performance CLI built with **Bun** that transforms the Java/Tomc
|
|
|
21
21
|
- 🐱 **Embedded Tomcat** — Auto-install Tomcat, no manual setup needed
|
|
22
22
|
- 📦 **WAR Generation** — Build as .war file or exploded directory
|
|
23
23
|
- 🔤 **Encoding Converter** — Convert file encodings (UTF-8, Windows-1252, ISO-8859-1) and fix mojibake
|
|
24
|
+
- 🧙 **Interactive Wizard** — `xavva init` for easy project setup
|
|
25
|
+
- 🔔 **Desktop Notifications** — Get notified when builds/deploys complete
|
|
26
|
+
- 📜 **Command History** — Track and replay commands with `xavva history` and `xavva redo`
|
|
27
|
+
- 🏥 **Health Check** — Verify environment (Java, ports, memory, disk) with `xavva health`
|
|
28
|
+
- 🔮 **Shell Completions** — Auto-complete for bash, zsh, and fish
|
|
24
29
|
|
|
25
30
|
---
|
|
26
31
|
|
|
@@ -39,6 +44,9 @@ bunx @archznn/xavva dev
|
|
|
39
44
|
## 🚀 Quick Start
|
|
40
45
|
|
|
41
46
|
```bash
|
|
47
|
+
# Initialize project configuration (interactive wizard)
|
|
48
|
+
xavva init
|
|
49
|
+
|
|
42
50
|
# Start development mode with dashboard
|
|
43
51
|
xavva dev --tui
|
|
44
52
|
|
|
@@ -62,6 +70,18 @@ xavva encoding convert --to cp1252 --backup src/main/java/
|
|
|
62
70
|
|
|
63
71
|
# Use embedded Tomcat (auto-install)
|
|
64
72
|
xavva dev --yes
|
|
73
|
+
|
|
74
|
+
# Check environment health
|
|
75
|
+
xavva health
|
|
76
|
+
|
|
77
|
+
# View command history
|
|
78
|
+
xavva history
|
|
79
|
+
|
|
80
|
+
# Repeat last command
|
|
81
|
+
xavva redo
|
|
82
|
+
|
|
83
|
+
# Enable shell completions (bash example)
|
|
84
|
+
eval "$(xavva completion bash)"
|
|
65
85
|
```
|
|
66
86
|
|
|
67
87
|
---
|
|
@@ -96,6 +116,19 @@ xavva dev --yes
|
|
|
96
116
|
| `xavva docs` | Generate endpoint documentation |
|
|
97
117
|
| `xavva tomcat` | Manage embedded Tomcat installations |
|
|
98
118
|
| `xavva encoding` | Convert file encodings (UTF-8, CP1252, ISO-8859-1) |
|
|
119
|
+
| `xavva health` | Check environment health (Java, ports, memory, disk) |
|
|
120
|
+
|
|
121
|
+
### Project Management
|
|
122
|
+
|
|
123
|
+
| Command | Description |
|
|
124
|
+
| ----------------------- | ---------------------------------------------- |
|
|
125
|
+
| `xavva init` | Initialize project configuration (wizard) |
|
|
126
|
+
| `xavva config` | View current configuration |
|
|
127
|
+
| `xavva config --interactive` | Edit configuration interactively |
|
|
128
|
+
| `xavva history` | Show command history |
|
|
129
|
+
| `xavva history --clear` | Clear command history |
|
|
130
|
+
| `xavva redo` | Repeat the last executed command |
|
|
131
|
+
| `xavva completion <shell>` | Generate shell completions (bash/zsh/fish) |
|
|
99
132
|
|
|
100
133
|
---
|
|
101
134
|
|
|
@@ -278,6 +311,82 @@ Create `xavva.json` in your project root:
|
|
|
278
311
|
| `--cache` | Use build cache (faster) |
|
|
279
312
|
| `-y, --yes` | Auto-install Tomcat (no prompt) |
|
|
280
313
|
| `-V, --verbose` | Detailed output |
|
|
314
|
+
| `-i, --interactive` | Interactive mode (for config) |
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## 🧙 Interactive Wizard
|
|
319
|
+
|
|
320
|
+
Initialize a new project with the interactive setup wizard:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
xavva init
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The wizard will guide you through:
|
|
327
|
+
- Build tool selection (auto-detected from pom.xml or build.gradle)
|
|
328
|
+
- Application name
|
|
329
|
+
- Profile selection (detects profiles from your build file)
|
|
330
|
+
- Tomcat port configuration
|
|
331
|
+
- Embedded Tomcat settings
|
|
332
|
+
- Build cache and TUI preferences
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## 🏥 Health Check
|
|
337
|
+
|
|
338
|
+
Verify your development environment:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
# Check all components
|
|
342
|
+
xavva health
|
|
343
|
+
|
|
344
|
+
# Checks include:
|
|
345
|
+
# - Java version (JDK 11+ recommended)
|
|
346
|
+
# - Maven/Gradle availability
|
|
347
|
+
# - Tomcat configuration
|
|
348
|
+
# - Port availability
|
|
349
|
+
# - Memory and disk space
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## 📜 Command History
|
|
355
|
+
|
|
356
|
+
Track and replay your commands:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
# Show recent commands
|
|
360
|
+
xavva history
|
|
361
|
+
|
|
362
|
+
# Show more entries
|
|
363
|
+
xavva history --limit 20
|
|
364
|
+
|
|
365
|
+
# Clear history
|
|
366
|
+
xavva history --clear
|
|
367
|
+
|
|
368
|
+
# Repeat last command
|
|
369
|
+
xavva redo
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## 🔮 Shell Completions
|
|
375
|
+
|
|
376
|
+
Enable tab completion for your shell:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
# Bash (add to ~/.bashrc)
|
|
380
|
+
eval "$(xavva completion bash)"
|
|
381
|
+
|
|
382
|
+
# Zsh (add to ~/.zshrc)
|
|
383
|
+
eval "$(xavva completion zsh)"
|
|
384
|
+
|
|
385
|
+
# Fish
|
|
386
|
+
xavva completion fish > ~/.config/fish/completions/xavva.fish
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Supported shells: `bash`, `zsh`, `fish`
|
|
281
390
|
|
|
282
391
|
---
|
|
283
392
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { Command } from "./Command";
|
|
2
|
+
import type { AppConfig, CLIArguments } from "../types/config";
|
|
3
|
+
import { ChangelogGenerator } from "../utils/ChangelogGenerator";
|
|
4
|
+
import { Logger } from "../utils/ui";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
|
|
7
|
+
export class ChangelogCommand implements Command {
|
|
8
|
+
async execute(_config: AppConfig, args?: CLIArguments, positionals?: string[]): Promise<void> {
|
|
9
|
+
// Pula o nome do comando "changelog" e pega a ação
|
|
10
|
+
const action = positionals?.find(p => !["changelog", "gen"].includes(p)) || "generate";
|
|
11
|
+
const output = args?.["output"] || args?.["o"] || "CHANGELOG.md";
|
|
12
|
+
|
|
13
|
+
Logger.banner("changelog");
|
|
14
|
+
|
|
15
|
+
switch (action) {
|
|
16
|
+
case "generate":
|
|
17
|
+
case "gen":
|
|
18
|
+
await this.generate(output);
|
|
19
|
+
break;
|
|
20
|
+
case "check":
|
|
21
|
+
case "validate":
|
|
22
|
+
await this.validate();
|
|
23
|
+
break;
|
|
24
|
+
case "preview":
|
|
25
|
+
await this.preview();
|
|
26
|
+
break;
|
|
27
|
+
default:
|
|
28
|
+
this.showHelp();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async generate(output: string): Promise<void> {
|
|
33
|
+
Logger.section("Generating Changelog");
|
|
34
|
+
Logger.step("Analyzing git history...");
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
ChangelogGenerator.generateAndSave(output);
|
|
38
|
+
|
|
39
|
+
if (existsSync(output)) {
|
|
40
|
+
Logger.success(`Changelog generated: ${output}`);
|
|
41
|
+
} else {
|
|
42
|
+
Logger.error("Failed to generate changelog");
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
Logger.error(`Error: ${error}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Logger.done();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async validate(): Promise<void> {
|
|
52
|
+
Logger.section("Validating Conventional Commits");
|
|
53
|
+
|
|
54
|
+
// Check if commits follow conventional commit format
|
|
55
|
+
const { execSync } = await import("child_process");
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const log = execSync(
|
|
59
|
+
'git log --pretty=format:"%s" --no-merges -20',
|
|
60
|
+
{ encoding: "utf-8" }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const commits = log.trim().split("\n");
|
|
64
|
+
const conventionalPattern = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?(!)?: .+/;
|
|
65
|
+
|
|
66
|
+
let valid = 0;
|
|
67
|
+
let invalid = 0;
|
|
68
|
+
|
|
69
|
+
for (const commit of commits) {
|
|
70
|
+
const isValid = conventionalPattern.test(commit);
|
|
71
|
+
if (isValid) {
|
|
72
|
+
valid++;
|
|
73
|
+
Logger.success(commit.slice(0, 60));
|
|
74
|
+
} else {
|
|
75
|
+
invalid++;
|
|
76
|
+
Logger.warn(commit.slice(0, 60));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Logger.newline();
|
|
81
|
+
Logger.info("Summary", `${valid} valid, ${invalid} need improvement`);
|
|
82
|
+
|
|
83
|
+
if (invalid > 0) {
|
|
84
|
+
Logger.dim("\nValid conventional commit types:");
|
|
85
|
+
Logger.dim(" feat, fix, docs, style, refactor, perf,");
|
|
86
|
+
Logger.dim(" test, build, ci, chore, revert");
|
|
87
|
+
Logger.dim("\nExample: feat(auth): add login endpoint");
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
Logger.error(`Failed to validate: ${error}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
Logger.done();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async preview(): Promise<void> {
|
|
97
|
+
Logger.section("Changelog Preview");
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const changelog = ChangelogGenerator.generate();
|
|
101
|
+
// Show only first 50 lines
|
|
102
|
+
const lines = changelog.split("\n").slice(0, 50);
|
|
103
|
+
Logger.log(lines.join("\n"));
|
|
104
|
+
|
|
105
|
+
if (changelog.split("\n").length > 50) {
|
|
106
|
+
Logger.dim("\n... (truncated, use 'generate' to see full)");
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
Logger.error(`Failed to generate preview: ${error}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
Logger.done();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private showHelp(): void {
|
|
116
|
+
Logger.section("Changelog Commands");
|
|
117
|
+
Logger.info("Usage: xavva changelog <action> [options]");
|
|
118
|
+
Logger.newline();
|
|
119
|
+
Logger.log("Actions:");
|
|
120
|
+
Logger.log(` ${Logger.C.primary}generate${Logger.C.reset} Generate CHANGELOG.md (default)`);
|
|
121
|
+
Logger.log(` ${Logger.C.primary}check${Logger.C.reset} Validate conventional commits`);
|
|
122
|
+
Logger.log(` ${Logger.C.primary}preview${Logger.C.reset} Preview changelog without saving`);
|
|
123
|
+
Logger.newline();
|
|
124
|
+
Logger.log("Options:");
|
|
125
|
+
Logger.log(` ${Logger.C.primary}-o, --output${Logger.C.reset} Output file (default: CHANGELOG.md)`);
|
|
126
|
+
Logger.done();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -38,6 +38,7 @@ export class HelpCommand implements Command {
|
|
|
38
38
|
${this.c("cyan", "redo")} Repeat last command
|
|
39
39
|
${this.c("cyan", "health")} Check environment health
|
|
40
40
|
${this.c("cyan", "completion")} Generate shell completions (bash/zsh/fish)
|
|
41
|
+
${this.c("cyan", "changelog")} Generate changelog from conventional commits
|
|
41
42
|
|
|
42
43
|
${this.c("yellow", "GENERAL OPTIONS")}
|
|
43
44
|
${this.c("cyan", "-p, --path")} <path> Tomcat installation path
|
|
@@ -56,6 +57,7 @@ export class HelpCommand implements Command {
|
|
|
56
57
|
${this.c("cyan", "-s, --no-build")} Skip initial build
|
|
57
58
|
${this.c("cyan", "-q, --quiet")} Minimal output
|
|
58
59
|
${this.c("cyan", "-V, --verbose")} Detailed output
|
|
60
|
+
${this.c("cyan", "--debug-level")} <lvl> Debug level: error|warn|info|verbose|trace|silly
|
|
59
61
|
${this.c("cyan", "-h, --help")} Show this help
|
|
60
62
|
${this.c("cyan", "-v, --version")} Show version
|
|
61
63
|
|
|
@@ -139,6 +141,16 @@ export class HelpCommand implements Command {
|
|
|
139
141
|
xavva completion zsh # Generate zsh completions
|
|
140
142
|
eval "$(xavva completion bash)" # Enable in current shell
|
|
141
143
|
|
|
144
|
+
${this.c("dim", "# Changelog")}
|
|
145
|
+
xavva changelog generate # Generate CHANGELOG.md
|
|
146
|
+
xavva changelog check # Validate conventional commits
|
|
147
|
+
xavva changelog preview # Preview without saving
|
|
148
|
+
|
|
149
|
+
${this.c("dim", "# Debug levels")}
|
|
150
|
+
xavva deploy --debug-level verbose # Verbose logging
|
|
151
|
+
xavva deploy --debug-level trace # Trace all operations
|
|
152
|
+
xavva deploy --debug-level silly # Everything including config
|
|
153
|
+
|
|
142
154
|
${this.c("yellow", "CONFIGURATION")}
|
|
143
155
|
Settings are loaded from ${this.c("cyan", "xavva.json")} in the project root:
|
|
144
156
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { input, select, confirm, number } from "@inquirer/prompts";
|
|
2
|
-
import { writeFile, access } from "fs/promises";
|
|
2
|
+
import { writeFile, access, readFile } from "fs/promises";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { constants } from "fs";
|
|
4
|
+
import { constants, existsSync } from "fs";
|
|
5
5
|
import type { Command } from "./Command";
|
|
6
6
|
import type { AppConfig, CLIArguments } from "../types/config";
|
|
7
7
|
import { Logger } from "../utils/ui";
|
|
@@ -9,78 +9,106 @@ import { Logger } from "../utils/ui";
|
|
|
9
9
|
export class InitCommand implements Command {
|
|
10
10
|
async execute(_config: AppConfig, _args?: CLIArguments): Promise<void> {
|
|
11
11
|
Logger.banner("init");
|
|
12
|
-
Logger.section("
|
|
13
|
-
Logger.info("
|
|
12
|
+
Logger.section("Project Setup Wizard");
|
|
13
|
+
Logger.info("Let's configure your Xavva project");
|
|
14
14
|
Logger.newline();
|
|
15
15
|
|
|
16
|
-
//
|
|
16
|
+
// Detect build tool and available profiles
|
|
17
17
|
const buildTool = await this.detectBuildTool();
|
|
18
|
+
const availableProfiles = await this.detectProfiles(buildTool);
|
|
18
19
|
|
|
19
|
-
//
|
|
20
|
+
// Application name
|
|
20
21
|
const appName = await input({
|
|
21
|
-
message: "
|
|
22
|
+
message: "Application name:",
|
|
22
23
|
default: process.cwd().split(/[/\\]/).pop() || "my-app",
|
|
23
|
-
validate: (value) => value.length > 0 || "
|
|
24
|
+
validate: (value) => value.length > 0 || "Name is required"
|
|
24
25
|
});
|
|
25
26
|
|
|
26
|
-
// Profile
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
// Profile selection with explanation
|
|
28
|
+
Logger.newline();
|
|
29
|
+
Logger.dim("The profile is used to activate Maven/Gradle build configurations");
|
|
30
|
+
Logger.dim("(e.g., 'dev' for development, 'prod' for production)");
|
|
31
|
+
|
|
32
|
+
let profile: string;
|
|
33
|
+
|
|
34
|
+
if (availableProfiles.length > 0) {
|
|
35
|
+
// Profiles found in build file
|
|
36
|
+
const profileChoices = [
|
|
37
|
+
...availableProfiles.map(p => ({
|
|
38
|
+
name: `${p.name}${p.description ? ` - ${p.description}` : ''}`,
|
|
39
|
+
value: p.name
|
|
40
|
+
})),
|
|
41
|
+
{ name: "Other (custom)", value: "custom" }
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
profile = await select({
|
|
45
|
+
message: "Select a profile from your build file:",
|
|
46
|
+
choices: profileChoices,
|
|
47
|
+
default: availableProfiles.find(p => p.name === "dev")?.name || availableProfiles[0]?.name
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
// No profiles detected, show common options
|
|
51
|
+
profile = await select({
|
|
52
|
+
message: "Default profile:",
|
|
53
|
+
choices: [
|
|
54
|
+
{ name: "dev - Development environment", value: "dev" },
|
|
55
|
+
{ name: "test - Testing environment", value: "test" },
|
|
56
|
+
{ name: "prod - Production environment", value: "prod" },
|
|
57
|
+
{ name: "Other (custom)", value: "custom" }
|
|
58
|
+
],
|
|
59
|
+
default: "dev"
|
|
60
|
+
});
|
|
61
|
+
}
|
|
37
62
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
if (profile === "custom") {
|
|
64
|
+
profile = await input({
|
|
65
|
+
message: "Profile name:",
|
|
66
|
+
default: "local",
|
|
67
|
+
validate: (value) => value.length > 0 || "Profile name is required"
|
|
68
|
+
});
|
|
69
|
+
}
|
|
42
70
|
|
|
43
|
-
//
|
|
71
|
+
// Tomcat port
|
|
44
72
|
const port = await number({
|
|
45
|
-
message: "
|
|
73
|
+
message: "Tomcat port:",
|
|
46
74
|
default: 8080,
|
|
47
|
-
validate: (value) => (value && value > 0 && value < 65536) || "
|
|
75
|
+
validate: (value) => (value && value > 0 && value < 65536) || "Invalid port"
|
|
48
76
|
}) || 8080;
|
|
49
77
|
|
|
50
|
-
//
|
|
78
|
+
// Optional settings
|
|
51
79
|
Logger.newline();
|
|
52
|
-
Logger.dim("
|
|
80
|
+
Logger.dim("Advanced settings:");
|
|
53
81
|
|
|
54
82
|
const useEmbedded = await confirm({
|
|
55
|
-
message: "
|
|
83
|
+
message: "Use embedded Tomcat (auto-download)?",
|
|
56
84
|
default: true
|
|
57
85
|
});
|
|
58
86
|
|
|
59
87
|
const enableCache = await confirm({
|
|
60
|
-
message: "
|
|
88
|
+
message: "Enable build cache?",
|
|
61
89
|
default: true
|
|
62
90
|
});
|
|
63
91
|
|
|
64
92
|
const enableTui = await confirm({
|
|
65
|
-
message: "
|
|
93
|
+
message: "Enable TUI dashboard?",
|
|
66
94
|
default: true
|
|
67
95
|
});
|
|
68
96
|
|
|
69
97
|
const encoding = await select({
|
|
70
|
-
message: "
|
|
98
|
+
message: "Source encoding:",
|
|
71
99
|
choices: [
|
|
72
|
-
{ name: "UTF-8", value: "UTF-8" },
|
|
73
|
-
{ name: "ISO-8859-1", value: "ISO-8859-1" },
|
|
100
|
+
{ name: "UTF-8 (recommended)", value: "UTF-8" },
|
|
101
|
+
{ name: "ISO-8859-1 (Latin-1)", value: "ISO-8859-1" },
|
|
74
102
|
{ name: "Windows-1252", value: "Windows-1252" }
|
|
75
103
|
],
|
|
76
104
|
default: "UTF-8"
|
|
77
105
|
});
|
|
78
106
|
|
|
79
|
-
//
|
|
107
|
+
// Build config object
|
|
80
108
|
const config: Record<string, unknown> = {
|
|
81
109
|
appName,
|
|
82
110
|
buildTool,
|
|
83
|
-
profile
|
|
111
|
+
profile,
|
|
84
112
|
port,
|
|
85
113
|
cache: enableCache,
|
|
86
114
|
tui: enableTui,
|
|
@@ -89,60 +117,127 @@ export class InitCommand implements Command {
|
|
|
89
117
|
|
|
90
118
|
if (useEmbedded) {
|
|
91
119
|
config.embedded = true;
|
|
92
|
-
config.tomcatVersion =
|
|
120
|
+
config.tomcatVersion = await select({
|
|
121
|
+
message: "Tomcat version:",
|
|
122
|
+
choices: [
|
|
123
|
+
{ name: "10.1.52 (Jakarta EE 10, recommended)", value: "10.1.52" },
|
|
124
|
+
{ name: "9.0.115 (Java EE 8)", value: "9.0.115" },
|
|
125
|
+
{ name: "11.0.18 (Jakarta EE 11, preview)", value: "11.0.18" }
|
|
126
|
+
],
|
|
127
|
+
default: "10.1.52"
|
|
128
|
+
});
|
|
93
129
|
} else {
|
|
94
130
|
const tomcatPath = await input({
|
|
95
|
-
message: "
|
|
131
|
+
message: "Tomcat path (CATALINA_HOME):",
|
|
96
132
|
validate: async (value) => {
|
|
97
|
-
if (!value) return "
|
|
133
|
+
if (!value) return "Path is required";
|
|
98
134
|
try {
|
|
99
135
|
await access(value, constants.R_OK);
|
|
100
136
|
return true;
|
|
101
137
|
} catch {
|
|
102
|
-
return "
|
|
138
|
+
return "Path not accessible";
|
|
103
139
|
}
|
|
104
140
|
}
|
|
105
141
|
});
|
|
106
142
|
config.tomcatPath = tomcatPath;
|
|
107
143
|
}
|
|
108
144
|
|
|
109
|
-
//
|
|
145
|
+
// Save file
|
|
110
146
|
Logger.newline();
|
|
111
|
-
Logger.step("
|
|
147
|
+
Logger.step("Saving configuration...");
|
|
112
148
|
|
|
113
149
|
const configPath = join(process.cwd(), "xavva.json");
|
|
114
150
|
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
115
151
|
|
|
116
|
-
Logger.success(`
|
|
152
|
+
Logger.success(`Configuration saved to ${configPath}`);
|
|
117
153
|
Logger.newline();
|
|
118
|
-
Logger.ready("
|
|
119
|
-
Logger.info("
|
|
120
|
-
Logger.log(` ${Logger.C.gray}│${Logger.C.reset} ${Logger.C.primary}xavva build${Logger.C.reset} ${Logger.C.gray}-
|
|
154
|
+
Logger.ready("Project configured!");
|
|
155
|
+
Logger.info("Next steps:");
|
|
156
|
+
Logger.log(` ${Logger.C.gray}│${Logger.C.reset} ${Logger.C.primary}xavva build${Logger.C.reset} ${Logger.C.gray}- Compile project${Logger.C.reset}`);
|
|
121
157
|
Logger.log(` ${Logger.C.gray}│${Logger.C.reset} ${Logger.C.primary}xavva deploy${Logger.C.reset} ${Logger.C.gray}- Build + deploy${Logger.C.reset}`);
|
|
122
|
-
Logger.log(` ${Logger.C.gray}│${Logger.C.reset} ${Logger.C.primary}xavva
|
|
158
|
+
Logger.log(` ${Logger.C.gray}│${Logger.C.reset} ${Logger.C.primary}xavva health${Logger.C.reset} ${Logger.C.gray}- Check environment${Logger.C.reset}`);
|
|
123
159
|
Logger.done();
|
|
124
160
|
}
|
|
125
161
|
|
|
126
162
|
private async detectBuildTool(): Promise<"maven" | "gradle"> {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
163
|
+
const hasPom = existsSync(join(process.cwd(), "pom.xml"));
|
|
164
|
+
const hasGradle = existsSync(join(process.cwd(), "build.gradle")) ||
|
|
165
|
+
existsSync(join(process.cwd(), "build.gradle.kts"));
|
|
166
|
+
|
|
167
|
+
if (hasPom && !hasGradle) {
|
|
168
|
+
Logger.info("Detected: Maven project");
|
|
130
169
|
return "maven";
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (hasGradle && !hasPom) {
|
|
173
|
+
Logger.info("Detected: Gradle project");
|
|
174
|
+
return "gradle";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (hasPom && hasGradle) {
|
|
178
|
+
Logger.warn("Both pom.xml and build.gradle found");
|
|
179
|
+
const choice = await select({
|
|
180
|
+
message: "Select build tool:",
|
|
181
|
+
choices: [
|
|
182
|
+
{ name: "Maven (pom.xml)", value: "maven" },
|
|
183
|
+
{ name: "Gradle (build.gradle)", value: "gradle" }
|
|
184
|
+
]
|
|
185
|
+
});
|
|
186
|
+
return choice;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Neither found
|
|
190
|
+
const choice = await select({
|
|
191
|
+
message: "Build tool:",
|
|
192
|
+
choices: [
|
|
193
|
+
{ name: "Maven", value: "maven" },
|
|
194
|
+
{ name: "Gradle", value: "gradle" }
|
|
195
|
+
]
|
|
196
|
+
});
|
|
197
|
+
return choice;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async detectProfiles(buildTool: "maven" | "gradle"): Promise<Array<{name: string, description?: string}>> {
|
|
201
|
+
const profiles: Array<{name: string, description?: string}> = [];
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
if (buildTool === "maven") {
|
|
205
|
+
const pomPath = join(process.cwd(), "pom.xml");
|
|
206
|
+
if (existsSync(pomPath)) {
|
|
207
|
+
const content = await readFile(pomPath, "utf-8");
|
|
208
|
+
// Parse profiles from pom.xml
|
|
209
|
+
const profileMatches = content.matchAll(/<profile>[\s\S]*?<id>([^<]+)<\/id>[\s\S]*?<\/profile>/g);
|
|
210
|
+
for (const match of profileMatches) {
|
|
211
|
+
const profileContent = match[0];
|
|
212
|
+
const id = match[1].trim();
|
|
213
|
+
// Try to extract description or properties
|
|
214
|
+
const descMatch = profileContent.match(/<description>([^<]+)<\/description>/);
|
|
215
|
+
const desc = descMatch ? descMatch[1].trim() : undefined;
|
|
216
|
+
profiles.push({ name: id, description: desc });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
const gradlePath = join(process.cwd(), "build.gradle");
|
|
221
|
+
const gradleKtsPath = join(process.cwd(), "build.gradle.kts");
|
|
222
|
+
const gradleFile = existsSync(gradlePath) ? gradlePath : gradleKtsPath;
|
|
223
|
+
|
|
224
|
+
if (existsSync(gradleFile)) {
|
|
225
|
+
const content = await readFile(gradleFile, "utf-8");
|
|
226
|
+
// Look for common profile-like configurations
|
|
227
|
+
// Gradle doesn't have built-in profiles like Maven, but can use:
|
|
228
|
+
// - Properties (-Pprofile=dev)
|
|
229
|
+
// - Custom configurations
|
|
230
|
+
// - apply from: "profiles/${profile}.gradle"
|
|
231
|
+
const profileMatches = content.matchAll(/(?:apply from:|def\s+\w*[Pp]rofile|ext\.\w*[Pp]rofile)\s*=\s*["']([^"']+)["']/g);
|
|
232
|
+
for (const match of profileMatches) {
|
|
233
|
+
profiles.push({ name: match[1] });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
145
236
|
}
|
|
237
|
+
} catch {
|
|
238
|
+
// Ignore errors, return empty profiles
|
|
146
239
|
}
|
|
240
|
+
|
|
241
|
+
return profiles;
|
|
147
242
|
}
|
|
148
243
|
}
|
package/src/di/container.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { HistoryCommand } from "../commands/HistoryCommand";
|
|
|
30
30
|
import { RedoCommand } from "../commands/RedoCommand";
|
|
31
31
|
import { HealthCommand } from "../commands/HealthCommand";
|
|
32
32
|
import { CompletionCommand } from "../commands/CompletionCommand";
|
|
33
|
+
import { ChangelogCommand } from "../commands/ChangelogCommand";
|
|
33
34
|
import { HistoryService } from "../services/HistoryService";
|
|
34
35
|
import { NotificationService } from "../services/NotificationService";
|
|
35
36
|
import type { Command } from "../commands/Command";
|
|
@@ -68,6 +69,7 @@ export interface Commands {
|
|
|
68
69
|
redo: RedoCommand;
|
|
69
70
|
health: HealthCommand;
|
|
70
71
|
completion: CompletionCommand;
|
|
72
|
+
changelog: ChangelogCommand;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
export class DIContainer {
|
|
@@ -163,6 +165,7 @@ export class DIContainer {
|
|
|
163
165
|
redo: new RedoCommand(),
|
|
164
166
|
health: new HealthCommand(),
|
|
165
167
|
completion: new CompletionCommand(),
|
|
168
|
+
changelog: new ChangelogCommand(),
|
|
166
169
|
};
|
|
167
170
|
}
|
|
168
171
|
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { createContainer, type DIContainer } from "./di/container";
|
|
|
5
5
|
import { DeployWatcher } from "./services/DeployWatcher";
|
|
6
6
|
import { ErrorHandler } from "./errors/ErrorHandler";
|
|
7
7
|
import { ProcessManager } from "./utils/processManager";
|
|
8
|
+
import { LoggerLevel } from "./utils/LoggerLevel";
|
|
8
9
|
import pkg from "../package.json";
|
|
9
10
|
import { Logger } from "./utils/ui";
|
|
10
11
|
import type { CLIArguments } from "./types/args";
|
|
@@ -19,12 +20,18 @@ async function main() {
|
|
|
19
20
|
await processManager.shutdown(0);
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// Configura debug level
|
|
24
|
+
if (values["debug-level"]) {
|
|
25
|
+
LoggerLevel.setLevel(values["debug-level"]);
|
|
26
|
+
LoggerLevel.verbose(`Debug level set to: ${values["debug-level"]}`, {});
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
// Identifica comando
|
|
23
30
|
const commandNames = [
|
|
24
31
|
"deploy", "build", "start", "dev", "doctor", "run",
|
|
25
32
|
"debug", "logs", "docs", "audit", "profiles",
|
|
26
33
|
"deps", "tomcat", "encoding", "init", "config",
|
|
27
|
-
"history", "redo", "health", "completion", "help"
|
|
34
|
+
"history", "redo", "health", "completion", "changelog", "help"
|
|
28
35
|
];
|
|
29
36
|
const commandName = positionals.find(p => commandNames.includes(p)) || "deploy";
|
|
30
37
|
|
|
@@ -103,6 +110,7 @@ async function main() {
|
|
|
103
110
|
registry.register("redo", commands.redo);
|
|
104
111
|
registry.register("health", commands.health);
|
|
105
112
|
registry.register("completion", commands.completion);
|
|
113
|
+
registry.register("changelog", commands.changelog);
|
|
106
114
|
|
|
107
115
|
// Configura flags específicas
|
|
108
116
|
if (commandName === "debug") values.debug = true;
|
package/src/types/args.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Logger } from "./ui";
|
|
2
|
+
|
|
3
|
+
export type LogLevel = "silent" | "error" | "warn" | "info" | "verbose" | "trace" | "silly";
|
|
4
|
+
|
|
5
|
+
interface LogLevelConfig {
|
|
6
|
+
value: number;
|
|
7
|
+
color: string;
|
|
8
|
+
prefix: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class LoggerLevel {
|
|
12
|
+
private static currentLevel: LogLevel = "info";
|
|
13
|
+
private static readonly levels: Record<LogLevel, LogLevelConfig> = {
|
|
14
|
+
silent: { value: 0, color: "", prefix: "" },
|
|
15
|
+
error: { value: 1, color: Logger.C.error, prefix: "ERR" },
|
|
16
|
+
warn: { value: 2, color: Logger.C.warning, prefix: "WRN" },
|
|
17
|
+
info: { value: 3, color: Logger.C.info, prefix: "INF" },
|
|
18
|
+
verbose: { value: 4, color: Logger.C.primary, prefix: "VRB" },
|
|
19
|
+
trace: { value: 5, color: Logger.C.gray, prefix: "TRC" },
|
|
20
|
+
silly: { value: 6, color: Logger.C.darkGray, prefix: "SLY" },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
static setLevel(level: LogLevel): void {
|
|
24
|
+
this.currentLevel = level;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static getLevel(): LogLevel {
|
|
28
|
+
return this.currentLevel;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static shouldLog(level: LogLevel): boolean {
|
|
32
|
+
return this.levels[level].value <= this.levels[this.currentLevel].value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private static log(level: LogLevel, message: string, ...args: unknown[]): void {
|
|
36
|
+
if (!this.shouldLog(level)) return;
|
|
37
|
+
|
|
38
|
+
const config = this.levels[level];
|
|
39
|
+
const formatted = args.length > 0
|
|
40
|
+
? this.formatMessage(message, args)
|
|
41
|
+
: message;
|
|
42
|
+
|
|
43
|
+
if (level === "error") {
|
|
44
|
+
Logger.error(formatted);
|
|
45
|
+
} else if (level === "warn") {
|
|
46
|
+
Logger.warn(formatted);
|
|
47
|
+
} else {
|
|
48
|
+
console.log(`${Logger.C.gray}│${Logger.C.reset} ${config.color}[${config.prefix}]${Logger.C.reset} ${formatted}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private static formatMessage(message: string, args: unknown[]): string {
|
|
53
|
+
return args.reduce((msg, arg, index) => {
|
|
54
|
+
const placeholder = `%${index + 1}`;
|
|
55
|
+
const str = typeof arg === "object"
|
|
56
|
+
? JSON.stringify(arg, null, 2)
|
|
57
|
+
: String(arg);
|
|
58
|
+
return msg.replace(placeholder, str);
|
|
59
|
+
}, message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Public logging methods
|
|
63
|
+
static error(message: string, ...args: unknown[]): void {
|
|
64
|
+
this.log("error", message, ...args);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static warn(message: string, ...args: unknown[]): void {
|
|
68
|
+
this.log("warn", message, ...args);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static info(message: string, ...args: unknown[]): void {
|
|
72
|
+
this.log("info", message, ...args);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static verbose(message: string, ...args: unknown[]): void {
|
|
76
|
+
this.log("verbose", message, ...args);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static trace(message: string, ...args: unknown[]): void {
|
|
80
|
+
this.log("trace", message, ...args);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static silly(message: string, ...args: unknown[]): void {
|
|
84
|
+
this.log("silly", message, ...args);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Utility methods for specific debug scenarios
|
|
88
|
+
static debugCommand(command: string, args: string[]): void {
|
|
89
|
+
if (this.shouldLog("verbose")) {
|
|
90
|
+
this.verbose("Executing: %1 %2", command, args.join(" "));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static debugSpawn(cmd: string, options: Record<string, unknown>): void {
|
|
95
|
+
if (this.shouldLog("trace")) {
|
|
96
|
+
this.trace("Spawn: %1 with options: %2", cmd, options);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static debugHttp(method: string, url: string, status?: number): void {
|
|
101
|
+
if (this.shouldLog("verbose")) {
|
|
102
|
+
const statusStr = status !== undefined ? ` -> ${status}` : "";
|
|
103
|
+
this.verbose("HTTP %1 %2%3", method.toUpperCase(), url, statusStr);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static debugFile(operation: string, path: string, details?: unknown): void {
|
|
108
|
+
if (this.shouldLog("trace")) {
|
|
109
|
+
this.trace("File %1: %2 %3", operation, path, details || "");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static debugConfig(key: string, value: unknown): void {
|
|
114
|
+
if (this.shouldLog("silly")) {
|
|
115
|
+
this.silly("Config: %1 = %2", key, value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static debugPerformance(operation: string, durationMs: number): void {
|
|
120
|
+
if (this.shouldLog("verbose")) {
|
|
121
|
+
this.verbose("Performance: %1 took %2ms", operation, durationMs);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static debugTiming(label: string): () => void {
|
|
126
|
+
if (!this.shouldLog("verbose")) {
|
|
127
|
+
return () => {}; // No-op
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const start = performance.now();
|
|
131
|
+
this.verbose("Timing started: %1", label);
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
const duration = Math.round(performance.now() - start);
|
|
135
|
+
this.verbose("Timing ended: %1 took %2ms", label, duration);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|