@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,302 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { platform, totalmem, freemem, arch } from "os";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import type { Command } from "./Command";
|
|
6
|
+
import type { AppConfig, CLIArguments } from "../types/config";
|
|
7
|
+
import { Logger } from "../utils/ui";
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
interface HealthCheck {
|
|
12
|
+
name: string;
|
|
13
|
+
status: "ok" | "warning" | "error";
|
|
14
|
+
message: string;
|
|
15
|
+
details?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class HealthCommand implements Command {
|
|
19
|
+
async execute(config: AppConfig, _args?: CLIArguments): Promise<void> {
|
|
20
|
+
Logger.banner("health");
|
|
21
|
+
Logger.section("Verificando saúde do ambiente");
|
|
22
|
+
|
|
23
|
+
const checks: HealthCheck[] = [];
|
|
24
|
+
|
|
25
|
+
// Java
|
|
26
|
+
checks.push(await this.checkJava());
|
|
27
|
+
|
|
28
|
+
// Maven/Gradle
|
|
29
|
+
checks.push(await this.checkBuildTool(config.project.buildTool));
|
|
30
|
+
|
|
31
|
+
// Tomcat
|
|
32
|
+
checks.push(await this.checkTomcat(config));
|
|
33
|
+
|
|
34
|
+
// Portas
|
|
35
|
+
checks.push(await this.checkPorts(config.tomcat.port));
|
|
36
|
+
|
|
37
|
+
// Memória
|
|
38
|
+
checks.push(this.checkMemory());
|
|
39
|
+
|
|
40
|
+
// Disco
|
|
41
|
+
checks.push(await this.checkDisk());
|
|
42
|
+
|
|
43
|
+
// Git
|
|
44
|
+
checks.push(this.checkGit());
|
|
45
|
+
|
|
46
|
+
// Exibir resultados
|
|
47
|
+
Logger.newline();
|
|
48
|
+
let errors = 0;
|
|
49
|
+
let warnings = 0;
|
|
50
|
+
|
|
51
|
+
for (const check of checks) {
|
|
52
|
+
const icon = check.status === "ok"
|
|
53
|
+
? `${Logger.C.success}✓${Logger.C.reset}`
|
|
54
|
+
: check.status === "warning"
|
|
55
|
+
? `${Logger.C.warning}⚠${Logger.C.reset}`
|
|
56
|
+
: `${Logger.C.error}✗${Logger.C.reset}`;
|
|
57
|
+
|
|
58
|
+
Logger.log(`${Logger.C.gray}│${Logger.C.reset} ${icon} ${Logger.C.bold}${check.name}${Logger.C.reset}`);
|
|
59
|
+
Logger.log(`${Logger.C.gray}│${Logger.C.reset} ${check.message}`);
|
|
60
|
+
|
|
61
|
+
if (check.details) {
|
|
62
|
+
Logger.log(`${Logger.C.gray}│${Logger.C.reset} ${Logger.C.dim}${check.details}${Logger.C.reset}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (check.status === "error") errors++;
|
|
66
|
+
if (check.status === "warning") warnings++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Logger.endSection();
|
|
70
|
+
|
|
71
|
+
// Summary
|
|
72
|
+
if (errors === 0 && warnings === 0) {
|
|
73
|
+
Logger.ready("Ambiente saudável! ✓");
|
|
74
|
+
} else if (errors === 0) {
|
|
75
|
+
Logger.warn(`${warnings} aviso(s) encontrado(s)`);
|
|
76
|
+
} else {
|
|
77
|
+
Logger.error(`${errors} erro(s) encontrado(s)`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async checkJava(): Promise<HealthCheck> {
|
|
82
|
+
try {
|
|
83
|
+
const { stdout, stderr } = await execAsync("java -version");
|
|
84
|
+
const output = stderr || stdout;
|
|
85
|
+
const versionMatch = output.match(/version "?(\d+\.?\d*)/);
|
|
86
|
+
const version = versionMatch ? versionMatch[1] : "unknown";
|
|
87
|
+
const isDCEVM = output.toLowerCase().includes("dcevm") || output.toLowerCase().includes("jbr");
|
|
88
|
+
|
|
89
|
+
const majorVersion = parseInt(version.split(".")[0]);
|
|
90
|
+
const status = majorVersion >= 11 ? "ok" : "warning";
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
name: "Java",
|
|
94
|
+
status,
|
|
95
|
+
message: `v${version}${isDCEVM ? " + DCEVM" : ""}`,
|
|
96
|
+
details: isDCEVM ? "Hot-reload disponível" : "Considere instalar DCEVM para hot-reload"
|
|
97
|
+
};
|
|
98
|
+
} catch {
|
|
99
|
+
return {
|
|
100
|
+
name: "Java",
|
|
101
|
+
status: "error",
|
|
102
|
+
message: "Java não encontrado",
|
|
103
|
+
details: "Instale o JDK 11+ e configure JAVA_HOME"
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async checkBuildTool(tool: string): Promise<HealthCheck> {
|
|
109
|
+
try {
|
|
110
|
+
if (tool === "maven") {
|
|
111
|
+
const { stdout } = await execAsync("mvn -version");
|
|
112
|
+
const version = stdout.match(/Apache Maven (\d+\.\d+\.\d+)/)?.[1] || "unknown";
|
|
113
|
+
return {
|
|
114
|
+
name: "Maven",
|
|
115
|
+
status: "ok",
|
|
116
|
+
message: `v${version}`
|
|
117
|
+
};
|
|
118
|
+
} else {
|
|
119
|
+
const { stdout } = await execAsync("gradle --version");
|
|
120
|
+
const version = stdout.match(/Gradle (\d+\.\d+\.\d+)/)?.[1] || "unknown";
|
|
121
|
+
return {
|
|
122
|
+
name: "Gradle",
|
|
123
|
+
status: "ok",
|
|
124
|
+
message: `v${version}`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
return {
|
|
129
|
+
name: tool === "maven" ? "Maven" : "Gradle",
|
|
130
|
+
status: "error",
|
|
131
|
+
message: `${tool === "maven" ? "mvn" : "gradle"} não encontrado`,
|
|
132
|
+
details: `Instale ${tool === "maven" ? "Maven" : "Gradle"} e adicione ao PATH`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async checkTomcat(config: AppConfig): Promise<HealthCheck> {
|
|
138
|
+
if (config.tomcat.embedded) {
|
|
139
|
+
return {
|
|
140
|
+
name: "Tomcat",
|
|
141
|
+
status: "ok",
|
|
142
|
+
message: `Embutido v${config.tomcat.version || "10.1.52"}`,
|
|
143
|
+
details: "Auto-download habilitado"
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (existsSync(config.tomcat.path)) {
|
|
148
|
+
const versionFile = `${config.tomcat.path}/bin/version.sh`;
|
|
149
|
+
const versionBat = `${config.tomcat.path}/bin/version.bat`;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const cmd = existsSync(versionBat) ? versionBat : versionFile;
|
|
153
|
+
const { stdout } = await execAsync(cmd);
|
|
154
|
+
const version = stdout.match(/Server version: Apache Tomcat\/(\d+\.\d+\.\d+)/)?.[1] || "unknown";
|
|
155
|
+
return {
|
|
156
|
+
name: "Tomcat",
|
|
157
|
+
status: "ok",
|
|
158
|
+
message: `v${version}`,
|
|
159
|
+
details: config.tomcat.path
|
|
160
|
+
};
|
|
161
|
+
} catch {
|
|
162
|
+
return {
|
|
163
|
+
name: "Tomcat",
|
|
164
|
+
status: "warning",
|
|
165
|
+
message: "Caminho existe mas não foi possível verificar versão",
|
|
166
|
+
details: config.tomcat.path
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
name: "Tomcat",
|
|
173
|
+
status: "error",
|
|
174
|
+
message: "Caminho não encontrado",
|
|
175
|
+
details: `Configure CATALINA_HOME ou use Tomcat embutido`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async checkPorts(port: number): Promise<HealthCheck> {
|
|
180
|
+
try {
|
|
181
|
+
let cmd: string;
|
|
182
|
+
if (platform() === "win32") {
|
|
183
|
+
cmd = `netstat -an | findstr :${port}`;
|
|
184
|
+
} else if (platform() === "darwin") {
|
|
185
|
+
cmd = `lsof -i :${port}`;
|
|
186
|
+
} else {
|
|
187
|
+
cmd = `ss -tuln | grep :${port}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { stdout } = await execAsync(cmd);
|
|
191
|
+
const isInUse = stdout.trim().length > 0;
|
|
192
|
+
|
|
193
|
+
if (isInUse) {
|
|
194
|
+
return {
|
|
195
|
+
name: "Portas",
|
|
196
|
+
status: "warning",
|
|
197
|
+
message: `Porta ${port} em uso`,
|
|
198
|
+
details: "Outro processo pode estar usando a porta"
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
name: "Portas",
|
|
204
|
+
status: "ok",
|
|
205
|
+
message: `Porta ${port} disponível`
|
|
206
|
+
};
|
|
207
|
+
} catch {
|
|
208
|
+
// Comando falhou, assume que porta está livre
|
|
209
|
+
return {
|
|
210
|
+
name: "Portas",
|
|
211
|
+
status: "ok",
|
|
212
|
+
message: `Porta ${port} parece disponível`
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private checkMemory(): HealthCheck {
|
|
218
|
+
const total = totalmem();
|
|
219
|
+
const free = freemem();
|
|
220
|
+
const used = total - free;
|
|
221
|
+
const percentUsed = Math.round((used / total) * 100);
|
|
222
|
+
const freeGB = (free / 1024 / 1024 / 1024).toFixed(1);
|
|
223
|
+
const totalGB = (total / 1024 / 1024 / 1024).toFixed(1);
|
|
224
|
+
|
|
225
|
+
const status = percentUsed > 90 ? "warning" : "ok";
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
name: "Memória",
|
|
229
|
+
status,
|
|
230
|
+
message: `${freeGB}GB livre de ${totalGB}GB`,
|
|
231
|
+
details: `${percentUsed}% em uso`
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async checkDisk(): Promise<HealthCheck> {
|
|
236
|
+
try {
|
|
237
|
+
let cmd: string;
|
|
238
|
+
if (platform() === "win32") {
|
|
239
|
+
cmd = "wmic logicaldisk get size,freespace,caption";
|
|
240
|
+
} else {
|
|
241
|
+
cmd = "df -h .";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const { stdout } = await execAsync(cmd);
|
|
245
|
+
|
|
246
|
+
if (platform() === "win32") {
|
|
247
|
+
const lines = stdout.trim().split("\n").slice(1);
|
|
248
|
+
const mainDisk = lines.find(l => l.includes(":")) || "";
|
|
249
|
+
const parts = mainDisk.trim().split(/\s+/);
|
|
250
|
+
if (parts.length >= 3) {
|
|
251
|
+
const free = parseInt(parts[0]) / 1024 / 1024 / 1024;
|
|
252
|
+
const total = parseInt(parts[1]) / 1024 / 1024 / 1024;
|
|
253
|
+
const percentFree = Math.round((free / total) * 100);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
name: "Disco",
|
|
257
|
+
status: percentFree < 10 ? "warning" : "ok",
|
|
258
|
+
message: `${free.toFixed(1)}GB livre`,
|
|
259
|
+
details: `${percentFree}% disponível`
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
const match = stdout.match(/(\d+)%/);
|
|
264
|
+
if (match) {
|
|
265
|
+
const used = parseInt(match[1]);
|
|
266
|
+
return {
|
|
267
|
+
name: "Disco",
|
|
268
|
+
status: used > 90 ? "warning" : "ok",
|
|
269
|
+
message: `${100 - used}% disponível`,
|
|
270
|
+
details: stdout.split("\n")[1]?.split(/\s+/).pop() || ""
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
throw new Error("Could not parse disk info");
|
|
276
|
+
} catch {
|
|
277
|
+
return {
|
|
278
|
+
name: "Disco",
|
|
279
|
+
status: "warning",
|
|
280
|
+
message: "Não foi possível verificar",
|
|
281
|
+
details: "Verifique manualmente"
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private checkGit(): HealthCheck {
|
|
287
|
+
if (existsSync(".git")) {
|
|
288
|
+
return {
|
|
289
|
+
name: "Git",
|
|
290
|
+
status: "ok",
|
|
291
|
+
message: "Repositório Git detectado"
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
name: "Git",
|
|
297
|
+
status: "warning",
|
|
298
|
+
message: "Sem repositório Git",
|
|
299
|
+
details: "Execute 'git init' para versionamento"
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -31,6 +31,13 @@ export class HelpCommand implements Command {
|
|
|
31
31
|
${this.c("green", "tomcat")} Manage embedded Tomcat (install, list, installed, use, status)
|
|
32
32
|
${this.c("green", "docs")} Generate endpoint documentation
|
|
33
33
|
${this.c("green", "encoding")} Convert file encoding (detect, convert, fix, list)
|
|
34
|
+
|
|
35
|
+
${this.c("cyan", "init")} Initialize project configuration (wizard)
|
|
36
|
+
${this.c("cyan", "config")} View/edit configuration (--interactive)
|
|
37
|
+
${this.c("cyan", "history")} Show command history
|
|
38
|
+
${this.c("cyan", "redo")} Repeat last command
|
|
39
|
+
${this.c("cyan", "health")} Check environment health
|
|
40
|
+
${this.c("cyan", "completion")} Generate shell completions (bash/zsh/fish)
|
|
34
41
|
|
|
35
42
|
${this.c("yellow", "GENERAL OPTIONS")}
|
|
36
43
|
${this.c("cyan", "-p, --path")} <path> Tomcat installation path
|
|
@@ -112,6 +119,26 @@ export class HelpCommand implements Command {
|
|
|
112
119
|
xavva encoding fix src/main/java/MinhaClasse.java # Fix mojibake
|
|
113
120
|
xavva encoding list # List all file encodings
|
|
114
121
|
|
|
122
|
+
${this.c("dim", "# Initialize new project")}
|
|
123
|
+
xavva init # Interactive wizard
|
|
124
|
+
|
|
125
|
+
${this.c("dim", "# Manage configuration")}
|
|
126
|
+
xavva config # View current config
|
|
127
|
+
xavva config --interactive # Edit config interactively
|
|
128
|
+
|
|
129
|
+
${this.c("dim", "# Command history")}
|
|
130
|
+
xavva history # Show recent commands
|
|
131
|
+
xavva history --clear # Clear history
|
|
132
|
+
xavva redo # Repeat last command
|
|
133
|
+
|
|
134
|
+
${this.c("dim", "# Health check")}
|
|
135
|
+
xavva health # Check environment health
|
|
136
|
+
|
|
137
|
+
${this.c("dim", "# Shell completions")}
|
|
138
|
+
xavva completion bash # Generate bash completions
|
|
139
|
+
xavva completion zsh # Generate zsh completions
|
|
140
|
+
eval "$(xavva completion bash)" # Enable in current shell
|
|
141
|
+
|
|
115
142
|
${this.c("yellow", "CONFIGURATION")}
|
|
116
143
|
Settings are loaded from ${this.c("cyan", "xavva.json")} in the project root:
|
|
117
144
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Command } from "./Command";
|
|
2
|
+
import type { AppConfig, CLIArguments } from "../types/config";
|
|
3
|
+
import { HistoryService } from "../services/HistoryService";
|
|
4
|
+
import { Logger } from "../utils/ui";
|
|
5
|
+
|
|
6
|
+
export class HistoryCommand implements Command {
|
|
7
|
+
private historyService = new HistoryService();
|
|
8
|
+
|
|
9
|
+
async execute(_config: AppConfig, args?: CLIArguments): Promise<void> {
|
|
10
|
+
const clear = args?.["clear"] || false;
|
|
11
|
+
const limit = parseInt(String(args?.["limit"] || "10"));
|
|
12
|
+
|
|
13
|
+
if (clear) {
|
|
14
|
+
await this.historyService.clear();
|
|
15
|
+
Logger.success("Histórico limpo!");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const entries = await this.historyService.getRecent(limit);
|
|
20
|
+
const stats = await this.historyService.getStats();
|
|
21
|
+
|
|
22
|
+
Logger.banner("history");
|
|
23
|
+
Logger.section(`Últimos ${entries.length} comandos`);
|
|
24
|
+
|
|
25
|
+
if (entries.length === 0) {
|
|
26
|
+
Logger.dim("Nenhum comando no histórico");
|
|
27
|
+
Logger.endSection();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < entries.length; i++) {
|
|
32
|
+
const entry = entries[i];
|
|
33
|
+
const date = new Date(entry.timestamp);
|
|
34
|
+
const time = date.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
|
|
35
|
+
const icon = entry.success
|
|
36
|
+
? `${Logger.C.success}✓${Logger.C.reset}`
|
|
37
|
+
: `${Logger.C.error}✗${Logger.C.reset}`;
|
|
38
|
+
|
|
39
|
+
const args = entry.args.length > 0 ? entry.args.join(" ") : "";
|
|
40
|
+
const duration = entry.duration ? `${Logger.C.gray}(${entry.duration.toFixed(1)}s)${Logger.C.reset}` : "";
|
|
41
|
+
|
|
42
|
+
Logger.log(`${Logger.C.gray}│${Logger.C.reset} ${Logger.C.dim}${time}${Logger.C.reset} ${icon} ${Logger.C.white}xavva ${entry.command}${Logger.C.reset} ${Logger.C.gray}${args}${Logger.C.reset} ${duration}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Logger.endSection();
|
|
46
|
+
Logger.info(`Total: ${stats.total} | Sucesso: ${stats.successful} | Falha: ${stats.failed}`);
|
|
47
|
+
Logger.dim("Use 'xavva redo' para repetir o último comando");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { input, select, confirm, number } from "@inquirer/prompts";
|
|
2
|
+
import { writeFile, access, readFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { constants, existsSync } from "fs";
|
|
5
|
+
import type { Command } from "./Command";
|
|
6
|
+
import type { AppConfig, CLIArguments } from "../types/config";
|
|
7
|
+
import { Logger } from "../utils/ui";
|
|
8
|
+
|
|
9
|
+
export class InitCommand implements Command {
|
|
10
|
+
async execute(_config: AppConfig, _args?: CLIArguments): Promise<void> {
|
|
11
|
+
Logger.banner("init");
|
|
12
|
+
Logger.section("Project Setup Wizard");
|
|
13
|
+
Logger.info("Let's configure your Xavva project");
|
|
14
|
+
Logger.newline();
|
|
15
|
+
|
|
16
|
+
// Detect build tool and available profiles
|
|
17
|
+
const buildTool = await this.detectBuildTool();
|
|
18
|
+
const availableProfiles = await this.detectProfiles(buildTool);
|
|
19
|
+
|
|
20
|
+
// Application name
|
|
21
|
+
const appName = await input({
|
|
22
|
+
message: "Application name:",
|
|
23
|
+
default: process.cwd().split(/[/\\]/).pop() || "my-app",
|
|
24
|
+
validate: (value) => value.length > 0 || "Name is required"
|
|
25
|
+
});
|
|
26
|
+
|
|
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
|
+
}
|
|
62
|
+
|
|
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
|
+
}
|
|
70
|
+
|
|
71
|
+
// Tomcat port
|
|
72
|
+
const port = await number({
|
|
73
|
+
message: "Tomcat port:",
|
|
74
|
+
default: 8080,
|
|
75
|
+
validate: (value) => (value && value > 0 && value < 65536) || "Invalid port"
|
|
76
|
+
}) || 8080;
|
|
77
|
+
|
|
78
|
+
// Optional settings
|
|
79
|
+
Logger.newline();
|
|
80
|
+
Logger.dim("Advanced settings:");
|
|
81
|
+
|
|
82
|
+
const useEmbedded = await confirm({
|
|
83
|
+
message: "Use embedded Tomcat (auto-download)?",
|
|
84
|
+
default: true
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const enableCache = await confirm({
|
|
88
|
+
message: "Enable build cache?",
|
|
89
|
+
default: true
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const enableTui = await confirm({
|
|
93
|
+
message: "Enable TUI dashboard?",
|
|
94
|
+
default: true
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const encoding = await select({
|
|
98
|
+
message: "Source encoding:",
|
|
99
|
+
choices: [
|
|
100
|
+
{ name: "UTF-8 (recommended)", value: "UTF-8" },
|
|
101
|
+
{ name: "ISO-8859-1 (Latin-1)", value: "ISO-8859-1" },
|
|
102
|
+
{ name: "Windows-1252", value: "Windows-1252" }
|
|
103
|
+
],
|
|
104
|
+
default: "UTF-8"
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Build config object
|
|
108
|
+
const config: Record<string, unknown> = {
|
|
109
|
+
appName,
|
|
110
|
+
buildTool,
|
|
111
|
+
profile,
|
|
112
|
+
port,
|
|
113
|
+
cache: enableCache,
|
|
114
|
+
tui: enableTui,
|
|
115
|
+
encoding
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (useEmbedded) {
|
|
119
|
+
config.embedded = true;
|
|
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
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
const tomcatPath = await input({
|
|
131
|
+
message: "Tomcat path (CATALINA_HOME):",
|
|
132
|
+
validate: async (value) => {
|
|
133
|
+
if (!value) return "Path is required";
|
|
134
|
+
try {
|
|
135
|
+
await access(value, constants.R_OK);
|
|
136
|
+
return true;
|
|
137
|
+
} catch {
|
|
138
|
+
return "Path not accessible";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
config.tomcatPath = tomcatPath;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Save file
|
|
146
|
+
Logger.newline();
|
|
147
|
+
Logger.step("Saving configuration...");
|
|
148
|
+
|
|
149
|
+
const configPath = join(process.cwd(), "xavva.json");
|
|
150
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
151
|
+
|
|
152
|
+
Logger.success(`Configuration saved to ${configPath}`);
|
|
153
|
+
Logger.newline();
|
|
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}`);
|
|
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}`);
|
|
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}`);
|
|
159
|
+
Logger.done();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async detectBuildTool(): Promise<"maven" | "gradle"> {
|
|
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");
|
|
169
|
+
return "maven";
|
|
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
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
// Ignore errors, return empty profiles
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return profiles;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Command } from "./Command";
|
|
2
|
+
import type { AppConfig, CLIArguments } from "../types/config";
|
|
3
|
+
import { HistoryService } from "../services/HistoryService";
|
|
4
|
+
import { Logger } from "../utils/ui";
|
|
5
|
+
import { ProcessManager } from "../utils/processManager";
|
|
6
|
+
|
|
7
|
+
export class RedoCommand implements Command {
|
|
8
|
+
private historyService = new HistoryService();
|
|
9
|
+
|
|
10
|
+
async execute(_config: AppConfig, _args?: CLIArguments): Promise<void> {
|
|
11
|
+
const lastEntry = await this.historyService.getLast();
|
|
12
|
+
|
|
13
|
+
if (!lastEntry) {
|
|
14
|
+
Logger.error("Nenhum comando no histórico");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const args = lastEntry.args.length > 0 ? lastEntry.args.join(" ") : "";
|
|
19
|
+
Logger.banner("redo");
|
|
20
|
+
Logger.info(`Repetindo: ${Logger.C.white}xavva ${lastEntry.command}${Logger.C.reset} ${Logger.C.gray}${args}${Logger.C.reset}`);
|
|
21
|
+
Logger.newline();
|
|
22
|
+
|
|
23
|
+
// Re-executar o comando
|
|
24
|
+
const proc = Bun.spawn([
|
|
25
|
+
"bun", "run", "src/index.ts",
|
|
26
|
+
lastEntry.command,
|
|
27
|
+
...lastEntry.args
|
|
28
|
+
], {
|
|
29
|
+
stdio: "inherit",
|
|
30
|
+
cwd: process.cwd()
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await proc.exited;
|
|
34
|
+
ProcessManager.getInstance().shutdown(proc.exitCode || 0);
|
|
35
|
+
}
|
|
36
|
+
}
|