@archznn/xavva 2.9.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Ultra-fast development toolkit for Java Enterprise (Tomcat) on Windows, Linux & macOS
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.6.0-blue.svg)](https://github.com/leorsousa05/Xavva)
5
+ [![Version](https://img.shields.io/badge/version-2.9.0-blue.svg)](https://github.com/leorsousa05/Xavva)
6
6
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archznn/xavva",
3
- "version": "2.9.0",
3
+ "version": "3.0.0",
4
4
  "description": "Ultra-fast CLI tool for Java/Tomcat development with Hot-Reload and Zero Config. Supports Windows, Linux and macOS.",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -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
 
@@ -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
@@ -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 =====
@@ -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
+ }