@archznn/xavva 2.6.0 → 2.7.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 +55 -0
- package/package.json +1 -1
- package/src/commands/DeployCommand.ts +2 -1
- package/src/commands/EncodingCommand.ts +351 -0
- package/src/commands/HelpCommand.ts +15 -0
- package/src/config/versions.ts +63 -0
- package/src/di/container.ts +226 -0
- package/src/errors/ErrorHandler.ts +249 -0
- package/src/errors/XavvaError.ts +273 -0
- package/src/index.ts +98 -96
- package/src/services/AuditService.ts +3 -2
- package/src/services/BrowserService.ts +127 -16
- package/src/services/DeployWatcher.ts +183 -0
- package/src/services/EmbeddedTomcatService.ts +5 -19
- package/src/services/EncodingService.ts +548 -0
- package/src/services/FileWatcher.ts +243 -0
- package/src/services/TomcatService.ts +9 -5
- package/src/types/args.ts +136 -0
- package/src/types/config.ts +6 -0
- package/src/types/index.ts +7 -0
- package/src/utils/PathUtils.ts +221 -0
- package/src/utils/config.ts +6 -0
- package/src/utils/parsers/JavaParser.ts +413 -0
- package/src/utils/platform.ts +2 -2
- package/src/services/WatcherService.ts +0 -117
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileWatcher genérico
|
|
3
|
+
* Responsabilidade única: observar mudanças em arquivos
|
|
4
|
+
* Sem acoplamento com lógica de deploy
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { watch, type FSWatcher } from "fs";
|
|
8
|
+
import { Logger } from "../utils/ui";
|
|
9
|
+
|
|
10
|
+
export interface FileWatcherOptions {
|
|
11
|
+
recursive?: boolean;
|
|
12
|
+
debounceMs?: number;
|
|
13
|
+
coolingMs?: number;
|
|
14
|
+
ignoredPatterns?: RegExp[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FileChangeEvent {
|
|
18
|
+
eventType: "rename" | "change";
|
|
19
|
+
filename: string | null;
|
|
20
|
+
fullPath: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type FileChangeHandler = (event: FileChangeEvent) => void | Promise<void>;
|
|
24
|
+
|
|
25
|
+
export class FileWatcher {
|
|
26
|
+
private watcher: FSWatcher | null = null;
|
|
27
|
+
private options: Required<FileWatcherOptions>;
|
|
28
|
+
private handlers: Map<string, FileChangeHandler> = new Map();
|
|
29
|
+
private debounceTimers: Map<string, Timer> = new Map();
|
|
30
|
+
private coolingFiles: Set<string> = new Set();
|
|
31
|
+
private isWatching = false;
|
|
32
|
+
|
|
33
|
+
private static readonly DEFAULT_OPTIONS: Required<FileWatcherOptions> = {
|
|
34
|
+
recursive: true,
|
|
35
|
+
debounceMs: 300,
|
|
36
|
+
coolingMs: 1000,
|
|
37
|
+
ignoredPatterns: [
|
|
38
|
+
/node_modules/,
|
|
39
|
+
/\.git/,
|
|
40
|
+
/target/,
|
|
41
|
+
/build/,
|
|
42
|
+
/\.xavva/,
|
|
43
|
+
/\.idea/,
|
|
44
|
+
/\.vscode/,
|
|
45
|
+
/dist/,
|
|
46
|
+
/out/,
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
constructor(options: FileWatcherOptions = {}) {
|
|
51
|
+
this.options = { ...FileWatcher.DEFAULT_OPTIONS, ...options };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Registra um handler para um padrão específico
|
|
56
|
+
*/
|
|
57
|
+
on(pattern: string | RegExp, handler: FileChangeHandler): () => void {
|
|
58
|
+
const key = pattern.toString();
|
|
59
|
+
this.handlers.set(key, handler);
|
|
60
|
+
|
|
61
|
+
// Retorna função para remover handler
|
|
62
|
+
return () => this.handlers.delete(key);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Registra handler para qualquer mudança
|
|
67
|
+
*/
|
|
68
|
+
onAny(handler: FileChangeHandler): () => void {
|
|
69
|
+
return this.on("*", handler);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Inicia o watching
|
|
74
|
+
*/
|
|
75
|
+
start(rootPath: string = process.cwd()): void {
|
|
76
|
+
if (this.isWatching) {
|
|
77
|
+
Logger.debug("FileWatcher já está rodando");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.watcher = watch(
|
|
82
|
+
rootPath,
|
|
83
|
+
{ recursive: this.options.recursive },
|
|
84
|
+
(eventType, filename) => this.handleWatchEvent(eventType, filename)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
this.isWatching = true;
|
|
88
|
+
Logger.debug(`FileWatcher iniciado em ${rootPath}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Para o watching
|
|
93
|
+
*/
|
|
94
|
+
stop(): void {
|
|
95
|
+
if (this.watcher) {
|
|
96
|
+
this.watcher.close();
|
|
97
|
+
this.watcher = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Limpa timers pendentes
|
|
101
|
+
this.debounceTimers.forEach(timer => clearTimeout(timer));
|
|
102
|
+
this.debounceTimers.clear();
|
|
103
|
+
|
|
104
|
+
this.isWatching = false;
|
|
105
|
+
Logger.debug("FileWatcher parado");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Verifica se está observando
|
|
110
|
+
*/
|
|
111
|
+
isActive(): boolean {
|
|
112
|
+
return this.isWatching;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Processa evento do fs.watch
|
|
117
|
+
*/
|
|
118
|
+
private handleWatchEvent(
|
|
119
|
+
eventType: "rename" | "change",
|
|
120
|
+
filename: string | null
|
|
121
|
+
): void {
|
|
122
|
+
if (!filename) return;
|
|
123
|
+
|
|
124
|
+
// Ignora arquivos em cooling
|
|
125
|
+
if (this.coolingFiles.has(filename)) return;
|
|
126
|
+
this.addToCooling(filename);
|
|
127
|
+
|
|
128
|
+
// Ignora patterns definidos
|
|
129
|
+
if (this.isIgnored(filename)) return;
|
|
130
|
+
|
|
131
|
+
const fullPath = this.resolvePath(filename);
|
|
132
|
+
const event: FileChangeEvent = { eventType, filename, fullPath };
|
|
133
|
+
|
|
134
|
+
// Aplica debounce
|
|
135
|
+
this.debounce(filename, () => {
|
|
136
|
+
this.notifyHandlers(event);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Adiciona arquivo ao período de cooling
|
|
142
|
+
*/
|
|
143
|
+
private addToCooling(filename: string): void {
|
|
144
|
+
this.coolingFiles.add(filename);
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
this.coolingFiles.delete(filename);
|
|
147
|
+
}, this.options.coolingMs);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Verifica se arquivo deve ser ignorado
|
|
152
|
+
*/
|
|
153
|
+
private isIgnored(filename: string): boolean {
|
|
154
|
+
return this.options.ignoredPatterns.some(pattern => pattern.test(filename));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Resolve caminho completo
|
|
159
|
+
*/
|
|
160
|
+
private resolvePath(filename: string): string {
|
|
161
|
+
// Normaliza separadores de path
|
|
162
|
+
return filename.replace(/\\/g, "/");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Aplica debounce no handler
|
|
167
|
+
*/
|
|
168
|
+
private debounce(key: string, fn: () => void): void {
|
|
169
|
+
const existing = this.debounceTimers.get(key);
|
|
170
|
+
if (existing) {
|
|
171
|
+
clearTimeout(existing);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const timer = setTimeout(() => {
|
|
175
|
+
this.debounceTimers.delete(key);
|
|
176
|
+
fn();
|
|
177
|
+
}, this.options.debounceMs);
|
|
178
|
+
|
|
179
|
+
this.debounceTimers.set(key, timer);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Notifica todos os handlers relevantes
|
|
184
|
+
*/
|
|
185
|
+
private notifyHandlers(event: FileChangeEvent): void {
|
|
186
|
+
// Notifica handlers específicos
|
|
187
|
+
for (const [pattern, handler] of this.handlers) {
|
|
188
|
+
if (this.matchesPattern(event.filename, pattern)) {
|
|
189
|
+
try {
|
|
190
|
+
const result = handler(event);
|
|
191
|
+
if (result instanceof Promise) {
|
|
192
|
+
result.catch(err => {
|
|
193
|
+
Logger.debug(`Erro em handler de watch: ${err.message}`);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
Logger.debug(`Erro em handler de watch: ${(err as Error).message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Verifica se filename match com pattern
|
|
205
|
+
*/
|
|
206
|
+
private matchesPattern(filename: string | null, pattern: string): boolean {
|
|
207
|
+
if (!filename) return false;
|
|
208
|
+
if (pattern === "*") return true;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const regex = new RegExp(pattern);
|
|
212
|
+
return regex.test(filename);
|
|
213
|
+
} catch {
|
|
214
|
+
// Se não for regex válido, trata como string simples
|
|
215
|
+
return filename.includes(pattern);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Helper para criar watcher com configuração padrão de projetos Java
|
|
222
|
+
*/
|
|
223
|
+
export function createJavaFileWatcher(): FileWatcher {
|
|
224
|
+
return new FileWatcher({
|
|
225
|
+
recursive: true,
|
|
226
|
+
debounceMs: 300,
|
|
227
|
+
coolingMs: 1000,
|
|
228
|
+
ignoredPatterns: [
|
|
229
|
+
/node_modules/,
|
|
230
|
+
/\.git/,
|
|
231
|
+
/target/,
|
|
232
|
+
/build/,
|
|
233
|
+
/\.xavva/,
|
|
234
|
+
/\.idea/,
|
|
235
|
+
/\.vscode/,
|
|
236
|
+
/dist/,
|
|
237
|
+
/out/,
|
|
238
|
+
/\.class$/,
|
|
239
|
+
/\.jar$/,
|
|
240
|
+
/\.war$/,
|
|
241
|
+
],
|
|
242
|
+
});
|
|
243
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { TomcatConfig, AppConfig } from "../types
|
|
1
|
+
import type { TomcatConfig, AppConfig } from "../types";
|
|
2
|
+
import { getHotswapAgentUrl, VERSIONS } from "../config/versions";
|
|
3
|
+
import { NetworkError } from "../errors/XavvaError";
|
|
2
4
|
import { Logger } from "../utils/ui";
|
|
3
5
|
import type { Subprocess } from "bun";
|
|
4
6
|
import { ProjectService } from "./ProjectService";
|
|
@@ -21,6 +23,7 @@ export class TomcatService {
|
|
|
21
23
|
public onReady?: () => void;
|
|
22
24
|
private pid: number | null = null;
|
|
23
25
|
private projectService: ProjectService | null = null;
|
|
26
|
+
private hasReadyBeenCalled: boolean = false;
|
|
24
27
|
|
|
25
28
|
constructor(customConfig: TomcatConfig) {
|
|
26
29
|
this.activeConfig = customConfig;
|
|
@@ -136,7 +139,7 @@ export class TomcatService {
|
|
|
136
139
|
|
|
137
140
|
private async ensureHotswapAgent(): Promise<string | null> {
|
|
138
141
|
const agentDir = path.join(os.homedir(), ".xavva", "agents");
|
|
139
|
-
const agentPath = path.join(agentDir,
|
|
142
|
+
const agentPath = path.join(agentDir, `hotswap-agent-${VERSIONS.HOTSWAP_AGENT.VERSION}.jar`);
|
|
140
143
|
|
|
141
144
|
if (existsSync(agentPath) && statSync(agentPath).size > 1000) return agentPath;
|
|
142
145
|
|
|
@@ -173,10 +176,10 @@ export class TomcatService {
|
|
|
173
176
|
const stats = statSync(agentPath);
|
|
174
177
|
Logger.debug(`Escrito: ${stats.size} bytes`);
|
|
175
178
|
|
|
176
|
-
Logger.success(
|
|
179
|
+
Logger.success(`HotswapAgent v${VERSIONS.HOTSWAP_AGENT.VERSION} installed globally!`);
|
|
177
180
|
return agentPath;
|
|
178
181
|
} catch (e: any) {
|
|
179
|
-
|
|
182
|
+
throw new NetworkError(url, e);
|
|
180
183
|
Logger.warn("Usando hot swap padrão da JVM.");
|
|
181
184
|
|
|
182
185
|
// Limpa arquivo parcial se existir
|
|
@@ -331,7 +334,8 @@ export class TomcatService {
|
|
|
331
334
|
this.stopStartupSpinner(isSuccess);
|
|
332
335
|
this.stopStartupSpinner = undefined;
|
|
333
336
|
}
|
|
334
|
-
if (isSuccess && this.onReady) {
|
|
337
|
+
if (isSuccess && this.onReady && !this.hasReadyBeenCalled) {
|
|
338
|
+
this.hasReadyBeenCalled = true;
|
|
335
339
|
this.onReady();
|
|
336
340
|
}
|
|
337
341
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argumentos de linha de comando tipados por contexto
|
|
3
|
+
* Substitui CLIArguments monolítico
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ===== Args Base (comuns a todos os comandos) =====
|
|
7
|
+
export interface BaseArgs {
|
|
8
|
+
help?: boolean;
|
|
9
|
+
version?: boolean;
|
|
10
|
+
verbose?: boolean;
|
|
11
|
+
quiet?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ===== Args de Configuração de Projeto =====
|
|
15
|
+
export interface ProjectArgs {
|
|
16
|
+
tool?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
profile?: string;
|
|
19
|
+
encoding?: string;
|
|
20
|
+
"no-build"?: boolean;
|
|
21
|
+
clean?: boolean;
|
|
22
|
+
war?: boolean;
|
|
23
|
+
cache?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ===== Args de Configuração do Tomcat =====
|
|
27
|
+
export interface TomcatArgs {
|
|
28
|
+
path?: string;
|
|
29
|
+
port?: string;
|
|
30
|
+
"tomcat-version"?: string;
|
|
31
|
+
yes?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ===== Args de Debug/Desenvolvimento =====
|
|
35
|
+
export interface DebugArgs {
|
|
36
|
+
debug?: boolean;
|
|
37
|
+
watch?: boolean;
|
|
38
|
+
tui?: boolean;
|
|
39
|
+
dp?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ===== Args de Análise =====
|
|
43
|
+
export interface AnalysisArgs {
|
|
44
|
+
grep?: string;
|
|
45
|
+
scan?: boolean;
|
|
46
|
+
fix?: boolean;
|
|
47
|
+
output?: string;
|
|
48
|
+
strict?: boolean;
|
|
49
|
+
"update-safe"?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ===== Args de Encoding =====
|
|
53
|
+
export interface EncodingArgs {
|
|
54
|
+
from?: string;
|
|
55
|
+
to?: string;
|
|
56
|
+
backup?: boolean;
|
|
57
|
+
"dry-run"?: boolean;
|
|
58
|
+
force?: boolean;
|
|
59
|
+
src?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ===== Args Específicas de Comandos =====
|
|
63
|
+
export interface DeployArgs extends BaseArgs, ProjectArgs, TomcatArgs, DebugArgs {
|
|
64
|
+
incremental?: boolean;
|
|
65
|
+
changedFiles?: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BuildArgs extends BaseArgs, ProjectArgs {
|
|
69
|
+
// Herda de ProjectArgs
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface StartArgs extends BaseArgs, TomcatArgs, DebugArgs {
|
|
73
|
+
// Herda de TomcatArgs e DebugArgs
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RunArgs extends BaseArgs, DebugArgs {
|
|
77
|
+
// Positional: className
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface LogsArgs extends BaseArgs {
|
|
81
|
+
grep?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface AuditArgs extends BaseArgs, AnalysisArgs {
|
|
85
|
+
// Herda de AnalysisArgs
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface DepsArgs extends BaseArgs, AnalysisArgs {
|
|
89
|
+
// Herda de AnalysisArgs
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface DocsArgs extends BaseArgs {
|
|
93
|
+
output?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ProfilesArgs extends BaseArgs {
|
|
97
|
+
// Sem args específicas
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface DoctorArgs extends BaseArgs {
|
|
101
|
+
fix?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface TomcatCommandArgs extends BaseArgs, TomcatArgs {
|
|
105
|
+
"tomcat-action"?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface EncodingCommandArgs extends BaseArgs, EncodingArgs {
|
|
109
|
+
// Herda de EncodingArgs
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ===== CLIArguments Legado (para compatibilidade) =====
|
|
113
|
+
// Será gradualmente removido
|
|
114
|
+
export interface CLIArguments extends
|
|
115
|
+
BaseArgs,
|
|
116
|
+
ProjectArgs,
|
|
117
|
+
TomcatArgs,
|
|
118
|
+
DebugArgs,
|
|
119
|
+
AnalysisArgs,
|
|
120
|
+
EncodingArgs {
|
|
121
|
+
// Campos adicionais para compatibilidade
|
|
122
|
+
[key: string]: unknown;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ===== Type Guards =====
|
|
126
|
+
export function isDeployArgs(args: CLIArguments): args is DeployArgs {
|
|
127
|
+
return "incremental" in args || "watch" in args;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function isBuildArgs(args: CLIArguments): args is BuildArgs {
|
|
131
|
+
return "tool" in args || "clean" in args;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isEncodingArgs(args: CLIArguments): args is EncodingCommandArgs {
|
|
135
|
+
return "from" in args || "to" in args;
|
|
136
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -62,6 +62,12 @@ export interface CLIArguments {
|
|
|
62
62
|
war?: boolean;
|
|
63
63
|
cache?: boolean;
|
|
64
64
|
changedFiles?: string[];
|
|
65
|
+
from?: string;
|
|
66
|
+
to?: string;
|
|
67
|
+
backup?: boolean;
|
|
68
|
+
"dry-run"?: boolean;
|
|
69
|
+
force?: boolean;
|
|
70
|
+
src?: string;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
export interface CommandContext {
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilitários para manipulação de paths
|
|
3
|
+
* Centraliza lógica comum de normalização e resolução de caminhos
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from "path";
|
|
7
|
+
|
|
8
|
+
export class PathUtils {
|
|
9
|
+
/**
|
|
10
|
+
* Normaliza separadores de path para forward slash
|
|
11
|
+
*/
|
|
12
|
+
static normalizeSeparators(filePath: string): string {
|
|
13
|
+
return filePath.replace(/\\/g, "/");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Converte caminho relativo para absoluto a partir do cwd
|
|
18
|
+
*/
|
|
19
|
+
static resolveFromCwd(relativePath: string): string {
|
|
20
|
+
return path.resolve(process.cwd(), relativePath);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Obtém caminho relativo a partir do diretório raiz do projeto
|
|
25
|
+
*/
|
|
26
|
+
static relativeToProject(absolutePath: string): string {
|
|
27
|
+
return path.relative(process.cwd(), absolutePath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Verifica se caminho está dentro do diretório src/main/java (Maven) ou equivalente
|
|
32
|
+
*/
|
|
33
|
+
static isSourceFile(filePath: string): boolean {
|
|
34
|
+
const normalized = this.normalizeSeparators(filePath);
|
|
35
|
+
return /src\/(main|test)\/(java|resources)/.test(normalized);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extrai package de um arquivo Java baseado no caminho
|
|
40
|
+
*/
|
|
41
|
+
static extractPackageFromPath(javaFilePath: string): string | null {
|
|
42
|
+
const normalized = this.normalizeSeparators(javaFilePath);
|
|
43
|
+
|
|
44
|
+
// Procura por src/main/java ou src/test/java
|
|
45
|
+
const match = normalized.match(/src\/(?:main|test)\/java\/(.*)\/[^/]+\.java$/);
|
|
46
|
+
if (match) {
|
|
47
|
+
return match[1].replace(/\//g, ".");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Converte caminho de .java para .class
|
|
55
|
+
*/
|
|
56
|
+
static javaToClassPath(javaPath: string): string {
|
|
57
|
+
return javaPath.replace(/\.java$/i, ".class");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Converte caminho relativo do source para caminho no diretório de classes
|
|
62
|
+
*/
|
|
63
|
+
static sourceToClassesPath(
|
|
64
|
+
sourcePath: string,
|
|
65
|
+
classesDir: string,
|
|
66
|
+
sourceRoots: string[] = ["src/main/java", "src/test/java", "src"]
|
|
67
|
+
): string {
|
|
68
|
+
const normalized = this.normalizeSeparators(sourcePath);
|
|
69
|
+
|
|
70
|
+
for (const root of sourceRoots) {
|
|
71
|
+
const normalizedRoot = this.normalizeSeparators(root);
|
|
72
|
+
const index = normalized.indexOf(normalizedRoot);
|
|
73
|
+
if (index !== -1) {
|
|
74
|
+
const relativePart = normalized.slice(index + normalizedRoot.length + 1);
|
|
75
|
+
const classFile = this.javaToClassPath(relativePart);
|
|
76
|
+
return path.join(classesDir, classFile);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fallback: assume que é relativo ao classesDir
|
|
81
|
+
return path.join(classesDir, path.basename(this.javaToClassPath(sourcePath)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Encontra o webapp root a partir de um caminho de recurso
|
|
86
|
+
*/
|
|
87
|
+
static findWebappRoot(resourcePath: string): string | null {
|
|
88
|
+
const normalized = this.normalizeSeparators(resourcePath);
|
|
89
|
+
const parts = normalized.split("/");
|
|
90
|
+
|
|
91
|
+
const webappIndex = parts.indexOf("webapp");
|
|
92
|
+
const webContentIndex = parts.indexOf("WebContent");
|
|
93
|
+
const rootIndex = webappIndex !== -1 ? webappIndex : webContentIndex;
|
|
94
|
+
|
|
95
|
+
if (rootIndex !== -1) {
|
|
96
|
+
return parts.slice(0, rootIndex + 1).join("/");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Obtém caminho relativo dentro do webapp
|
|
104
|
+
*/
|
|
105
|
+
static getWebappRelativePath(resourcePath: string): string | null {
|
|
106
|
+
const normalized = this.normalizeSeparators(resourcePath);
|
|
107
|
+
const parts = normalized.split("/");
|
|
108
|
+
|
|
109
|
+
const webappIndex = parts.indexOf("webapp");
|
|
110
|
+
const webContentIndex = parts.indexOf("WebContent");
|
|
111
|
+
const rootIndex = webappIndex !== -1 ? webappIndex : webContentIndex;
|
|
112
|
+
|
|
113
|
+
if (rootIndex !== -1 && rootIndex < parts.length - 1) {
|
|
114
|
+
return parts.slice(rootIndex + 1).join("/");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Verifica se é arquivo de recurso web (JSP, HTML, etc)
|
|
122
|
+
*/
|
|
123
|
+
static isWebResource(filePath: string): boolean {
|
|
124
|
+
return /\.(jsp|html|htm|css|js|xml|properties|json|png|jpg|jpeg|gif|svg|ico)$/i.test(filePath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Verifica se é arquivo Java
|
|
129
|
+
*/
|
|
130
|
+
static isJavaFile(filePath: string): boolean {
|
|
131
|
+
return /\.java$/i.test(filePath);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Verifica se é arquivo de build/configuração
|
|
136
|
+
*/
|
|
137
|
+
static isBuildConfig(filePath: string): boolean {
|
|
138
|
+
return /(pom\.xml|build\.gradle|build\.gradle\.kts|settings\.gradle)$/i.test(filePath);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Combina múltiplos paths de forma segura
|
|
143
|
+
*/
|
|
144
|
+
static combinePaths(...parts: string[]): string {
|
|
145
|
+
return path.join(...parts);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Obtém diretório do arquivo
|
|
150
|
+
*/
|
|
151
|
+
static getDir(filePath: string): string {
|
|
152
|
+
return path.dirname(filePath);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Obtém nome do arquivo
|
|
157
|
+
*/
|
|
158
|
+
static getFilename(filePath: string): string {
|
|
159
|
+
return path.basename(filePath);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Obtém extensão do arquivo
|
|
164
|
+
*/
|
|
165
|
+
static getExtension(filePath: string): string {
|
|
166
|
+
return path.extname(filePath).toLowerCase();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Remove extensão do arquivo
|
|
171
|
+
*/
|
|
172
|
+
static removeExtension(filePath: string): string {
|
|
173
|
+
const ext = path.extname(filePath);
|
|
174
|
+
return ext ? filePath.slice(0, -ext.length) : filePath;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Verifica se caminho está dentro de diretório ignorado
|
|
179
|
+
*/
|
|
180
|
+
static isIgnoredPath(filePath: string, ignoredDirs: string[] = []): boolean {
|
|
181
|
+
const normalized = this.normalizeSeparators(filePath);
|
|
182
|
+
const defaultIgnored = ["target", "build", "node_modules", ".git", ".xavva"];
|
|
183
|
+
const allIgnored = [...defaultIgnored, ...ignoredDirs];
|
|
184
|
+
|
|
185
|
+
return allIgnored.some(dir => {
|
|
186
|
+
// Match exato ou como diretório
|
|
187
|
+
const pattern = new RegExp(`(^|/)${dir}($|/)`);
|
|
188
|
+
return pattern.test(normalized);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Resolve caminho de contexto WAR
|
|
194
|
+
*/
|
|
195
|
+
static resolveContextPath(appName: string): string {
|
|
196
|
+
return appName.replace(/\.war$/i, "");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Exporta funções standalone para conveniência
|
|
201
|
+
export const {
|
|
202
|
+
normalizeSeparators,
|
|
203
|
+
resolveFromCwd,
|
|
204
|
+
relativeToProject,
|
|
205
|
+
isSourceFile,
|
|
206
|
+
extractPackageFromPath,
|
|
207
|
+
javaToClassPath,
|
|
208
|
+
sourceToClassesPath,
|
|
209
|
+
findWebappRoot,
|
|
210
|
+
getWebappRelativePath,
|
|
211
|
+
isWebResource,
|
|
212
|
+
isJavaFile,
|
|
213
|
+
isBuildConfig,
|
|
214
|
+
combinePaths,
|
|
215
|
+
getDir,
|
|
216
|
+
getFilename,
|
|
217
|
+
getExtension,
|
|
218
|
+
removeExtension,
|
|
219
|
+
isIgnoredPath,
|
|
220
|
+
resolveContextPath,
|
|
221
|
+
} = PathUtils;
|
package/src/utils/config.ts
CHANGED
|
@@ -38,6 +38,12 @@ export class ConfigManager {
|
|
|
38
38
|
yes: { type: "boolean", short: "y" },
|
|
39
39
|
war: { type: "boolean", short: "W" },
|
|
40
40
|
cache: { type: "boolean" },
|
|
41
|
+
from: { type: "string" },
|
|
42
|
+
to: { type: "string" },
|
|
43
|
+
backup: { type: "boolean" },
|
|
44
|
+
"dry-run": { type: "boolean" },
|
|
45
|
+
force: { type: "boolean" },
|
|
46
|
+
src: { type: "string" },
|
|
41
47
|
},
|
|
42
48
|
strict: false,
|
|
43
49
|
allowPositionals: true,
|