@archznn/xavva 2.7.0 → 2.9.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/package.json +4 -2
- package/src/commands/CompletionCommand.ts +212 -0
- package/src/commands/ConfigCommand.ts +184 -0
- package/src/commands/HealthCommand.ts +302 -0
- package/src/commands/HelpCommand.ts +27 -0
- package/src/commands/HistoryCommand.ts +49 -0
- package/src/commands/InitCommand.ts +243 -0
- package/src/commands/RedoCommand.ts +36 -0
- package/src/di/container.ts +23 -0
- package/src/index.ts +39 -1
- package/src/services/EmbeddedTomcatService.ts +62 -18
- package/src/services/HistoryService.ts +73 -0
- package/src/services/NotificationService.ts +145 -0
- package/src/services/TomcatService.ts +52 -23
- package/src/types/args.ts +15 -0
- package/src/utils/ProgressBar.ts +182 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Logger } from "./ui";
|
|
2
|
+
|
|
3
|
+
export interface ProgressBarOptions {
|
|
4
|
+
title: string;
|
|
5
|
+
total: number;
|
|
6
|
+
width?: number;
|
|
7
|
+
showPercentage?: boolean;
|
|
8
|
+
showEta?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ProgressBar {
|
|
12
|
+
private current = 0;
|
|
13
|
+
private startTime: number;
|
|
14
|
+
private options: Required<ProgressBarOptions>;
|
|
15
|
+
private isComplete = false;
|
|
16
|
+
|
|
17
|
+
constructor(options: ProgressBarOptions) {
|
|
18
|
+
this.options = {
|
|
19
|
+
width: 30,
|
|
20
|
+
showPercentage: true,
|
|
21
|
+
showEta: true,
|
|
22
|
+
...options
|
|
23
|
+
};
|
|
24
|
+
this.startTime = Date.now();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
update(current: number): void {
|
|
28
|
+
if (this.isComplete) return;
|
|
29
|
+
this.current = Math.min(current, this.options.total);
|
|
30
|
+
this.render();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
increment(amount = 1): void {
|
|
34
|
+
this.update(this.current + amount);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
complete(): void {
|
|
38
|
+
if (this.isComplete) return;
|
|
39
|
+
this.current = this.options.total;
|
|
40
|
+
this.isComplete = true;
|
|
41
|
+
this.render();
|
|
42
|
+
process.stdout.write("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private render(): void {
|
|
46
|
+
const { title, total, width, showPercentage, showEta } = this.options;
|
|
47
|
+
const ratio = this.current / total;
|
|
48
|
+
const filled = Math.floor(ratio * width);
|
|
49
|
+
const empty = width - filled;
|
|
50
|
+
|
|
51
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
52
|
+
const percentage = Math.floor(ratio * 100);
|
|
53
|
+
|
|
54
|
+
let line = `${Logger.C.gray}│${Logger.C.reset} ${Logger.C.primary}▸${Logger.C.reset} ${Logger.C.dim}${title}${Logger.C.reset} `;
|
|
55
|
+
line += `${Logger.C.primary}[${bar}]${Logger.C.reset}`;
|
|
56
|
+
|
|
57
|
+
if (showPercentage) {
|
|
58
|
+
line += ` ${Logger.C.white}${percentage}%${Logger.C.reset}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (showEta && this.current > 0) {
|
|
62
|
+
const elapsed = Date.now() - this.startTime;
|
|
63
|
+
const eta = Math.ceil((elapsed / this.current) * (total - this.current) / 1000);
|
|
64
|
+
line += ` ${Logger.C.gray}(ETA: ${eta}s)${Logger.C.reset}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Clear line and write new content
|
|
68
|
+
process.stdout.write(`\r${line}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Spinner especializado para diferentes operações
|
|
73
|
+
export class ThemedSpinner {
|
|
74
|
+
private static frames = {
|
|
75
|
+
default: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
76
|
+
dots: ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"],
|
|
77
|
+
line: ["-", "\\", "|", "/"],
|
|
78
|
+
arrow: ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
|
|
79
|
+
pulse: ["█", "▉", "▊", "▋", "▌", "▍", "▎", "▏"],
|
|
80
|
+
moon: ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"]
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
private static colors = {
|
|
84
|
+
default: Logger.C.primary,
|
|
85
|
+
build: Logger.C.warning,
|
|
86
|
+
download: Logger.C.info,
|
|
87
|
+
deploy: Logger.C.success,
|
|
88
|
+
watch: Logger.C.secondary
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
private timer?: Timer;
|
|
92
|
+
private frameIndex = 0;
|
|
93
|
+
|
|
94
|
+
start(
|
|
95
|
+
message: string,
|
|
96
|
+
theme: keyof typeof ThemedSpinner.frames = "default",
|
|
97
|
+
color: keyof typeof ThemedSpinner.colors = "default"
|
|
98
|
+
): { stop: (success?: boolean) => void } {
|
|
99
|
+
const frames = ThemedSpinner.frames[theme];
|
|
100
|
+
const colorCode = ThemedSpinner.colors[color];
|
|
101
|
+
|
|
102
|
+
process.stdout.write(`${Logger.C.gray}│${Logger.C.reset} `);
|
|
103
|
+
process.stdout.write("\x1B[?25l");
|
|
104
|
+
|
|
105
|
+
this.timer = setInterval(() => {
|
|
106
|
+
const frame = frames[this.frameIndex];
|
|
107
|
+
process.stdout.write(`\r${Logger.C.gray}│${Logger.C.reset} ${colorCode}${frame}${Logger.C.reset} ${Logger.C.dim}${message}${Logger.C.reset}`);
|
|
108
|
+
this.frameIndex = (this.frameIndex + 1) % frames.length;
|
|
109
|
+
}, 80);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
stop: (success = true) => {
|
|
113
|
+
if (this.timer) {
|
|
114
|
+
clearInterval(this.timer);
|
|
115
|
+
}
|
|
116
|
+
process.stdout.write("\x1B[?25h");
|
|
117
|
+
const icon = success ? `${Logger.C.success}✓${Logger.C.reset}` : `${Logger.C.error}✗${Logger.C.reset}`;
|
|
118
|
+
console.log(`\r${Logger.C.gray}│${Logger.C.reset} ${icon} ${message}`);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Função auxiliar para downloads com progresso
|
|
125
|
+
export async function downloadWithProgress(
|
|
126
|
+
url: string,
|
|
127
|
+
destination: string,
|
|
128
|
+
title: string
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
const response = await fetch(url);
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
throw new Error(`Failed to download: ${response.statusText}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const totalSize = parseInt(response.headers.get("content-length") || "0");
|
|
136
|
+
if (!totalSize) {
|
|
137
|
+
// Sem content-length, usar spinner simples
|
|
138
|
+
const spinner = new ThemedSpinner();
|
|
139
|
+
const stop = spinner.start(title, "dots", "download");
|
|
140
|
+
|
|
141
|
+
const data = await response.arrayBuffer();
|
|
142
|
+
await Bun.write(destination, data);
|
|
143
|
+
|
|
144
|
+
stop(true);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const progress = new ProgressBar({
|
|
149
|
+
title,
|
|
150
|
+
total: totalSize,
|
|
151
|
+
width: 25
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const reader = response.body?.getReader();
|
|
155
|
+
if (!reader) {
|
|
156
|
+
throw new Error("No response body");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const chunks: Uint8Array[] = [];
|
|
160
|
+
let received = 0;
|
|
161
|
+
|
|
162
|
+
while (true) {
|
|
163
|
+
const { done, value } = await reader.read();
|
|
164
|
+
if (done) break;
|
|
165
|
+
|
|
166
|
+
chunks.push(value);
|
|
167
|
+
received += value.length;
|
|
168
|
+
progress.update(received);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
progress.complete();
|
|
172
|
+
|
|
173
|
+
// Concatena chunks e salva
|
|
174
|
+
const allChunks = new Uint8Array(received);
|
|
175
|
+
let position = 0;
|
|
176
|
+
for (const chunk of chunks) {
|
|
177
|
+
allChunks.set(chunk, position);
|
|
178
|
+
position += chunk.length;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await Bun.write(destination, allChunks);
|
|
182
|
+
}
|