@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.
@@ -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
+ }