@archznn/xavva 2.6.0 → 2.8.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,548 @@
1
+ import { promises as fs, existsSync, mkdirSync, createReadStream, createWriteStream } from "fs";
2
+ import path from "path";
3
+ import { Logger } from "../utils/ui";
4
+
5
+ // Mapeamento de encoding aliases
6
+ const ENCODING_ALIASES: Record<string, string> = {
7
+ "utf8": "utf-8",
8
+ "utf-8": "utf-8",
9
+ "cp1252": "windows-1252",
10
+ "windows-1252": "windows-1252",
11
+ "latin1": "iso-8859-1",
12
+ "iso-8859-1": "iso-8859-1",
13
+ "iso88591": "iso-8859-1",
14
+ "cp850": "ibm850",
15
+ };
16
+
17
+ // Encodings suportados
18
+ const SUPPORTED_ENCODINGS = ["utf-8", "windows-1252", "iso-8859-1"];
19
+
20
+ // Bytes caracteristicos de UTF-8 para deteccao
21
+ const UTF8_BOM = Buffer.from([0xEF, 0xBB, 0xBF]);
22
+
23
+ export interface DetectedEncoding {
24
+ encoding: string;
25
+ confidence: number;
26
+ hasBOM: boolean;
27
+ }
28
+
29
+ export class EncodingService {
30
+ private backupDir: string;
31
+
32
+ constructor() {
33
+ this.backupDir = path.join(process.cwd(), ".xavva", "encoding-backups");
34
+ }
35
+
36
+ /**
37
+ * Normaliza o nome do encoding
38
+ */
39
+ normalizeEncoding(encoding: string): string {
40
+ const normalized = encoding.toLowerCase().replace(/[_\s]/g, "-");
41
+ return ENCODING_ALIASES[normalized] || normalized;
42
+ }
43
+
44
+ /**
45
+ * Verifica se um encoding e suportado
46
+ */
47
+ isValidEncoding(encoding: string): boolean {
48
+ const normalized = this.normalizeEncoding(encoding);
49
+ return SUPPORTED_ENCODINGS.includes(normalized);
50
+ }
51
+
52
+ /**
53
+ * Retorna lista de encodings suportados
54
+ */
55
+ getSupportedEncodings(): string[] {
56
+ return [...SUPPORTED_ENCODINGS];
57
+ }
58
+
59
+ /**
60
+ * Detecta o encoding de um buffer
61
+ * Implementacao simplificada baseada em heuristicas
62
+ */
63
+ detectEncoding(buffer: Buffer): DetectedEncoding {
64
+ // Verifica BOM
65
+ if (buffer.length >= 3) {
66
+ if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
67
+ return { encoding: "utf-8", confidence: 1, hasBOM: true };
68
+ }
69
+ if (buffer[0] === 0xFE && buffer[1] === 0xFF) {
70
+ return { encoding: "utf-16be", confidence: 1, hasBOM: true };
71
+ }
72
+ if (buffer[0] === 0xFF && buffer[1] === 0xFE) {
73
+ return { encoding: "utf-16le", confidence: 1, hasBOM: true };
74
+ }
75
+ }
76
+
77
+ // Heuristica: conta bytes validos UTF-8 vs invalidos
78
+ let utf8Valid = 0;
79
+ let utf8Invalid = 0;
80
+ let highBytes = 0; // Bytes > 127
81
+
82
+ for (let i = 0; i < buffer.length; i++) {
83
+ const byte = buffer[i];
84
+
85
+ if (byte > 127) {
86
+ highBytes++;
87
+
88
+ // Verifica se e inicio de sequencia UTF-8 multi-byte
89
+ if ((byte & 0xE0) === 0xC0) {
90
+ // 2-byte sequence
91
+ if (i + 1 < buffer.length && (buffer[i + 1] & 0xC0) === 0x80) {
92
+ utf8Valid++;
93
+ i++;
94
+ } else {
95
+ utf8Invalid++;
96
+ }
97
+ } else if ((byte & 0xF0) === 0xE0) {
98
+ // 3-byte sequence
99
+ if (i + 2 < buffer.length &&
100
+ (buffer[i + 1] & 0xC0) === 0x80 &&
101
+ (buffer[i + 2] & 0xC0) === 0x80) {
102
+ utf8Valid++;
103
+ i += 2;
104
+ } else {
105
+ utf8Invalid++;
106
+ }
107
+ } else if ((byte & 0xF8) === 0xF0) {
108
+ // 4-byte sequence
109
+ if (i + 3 < buffer.length &&
110
+ (buffer[i + 1] & 0xC0) === 0x80 &&
111
+ (buffer[i + 2] & 0xC0) === 0x80 &&
112
+ (buffer[i + 3] & 0xC0) === 0x80) {
113
+ utf8Valid++;
114
+ i += 3;
115
+ } else {
116
+ utf8Invalid++;
117
+ }
118
+ } else {
119
+ utf8Invalid++;
120
+ }
121
+ }
122
+ }
123
+
124
+ // Se tem bytes altos e todos sao validos UTF-8, provavelmente e UTF-8
125
+ if (highBytes > 0 && utf8Invalid === 0 && utf8Valid > 0) {
126
+ return { encoding: "utf-8", confidence: 0.95, hasBOM: false };
127
+ }
128
+
129
+ // Se tem bytes altos invalidos em UTF-8, provavelmente e single-byte encoding
130
+ if (highBytes > 0 && utf8Invalid > 0) {
131
+ // No contexto brasileiro/Windows, provavelmente e Windows-1252
132
+ return { encoding: "windows-1252", confidence: 0.7, hasBOM: false };
133
+ }
134
+
135
+ // Se nao tem bytes altos, pode ser ASCII/UTF-8/Latin1
136
+ return { encoding: "utf-8", confidence: 0.5, hasBOM: false };
137
+ }
138
+
139
+ /**
140
+ * Detecta encoding de um arquivo
141
+ */
142
+ async detectFileEncoding(filePath: string): Promise<DetectedEncoding | null> {
143
+ try {
144
+ const buffer = await fs.readFile(filePath);
145
+ return this.detectEncoding(buffer);
146
+ } catch (e) {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Cria backup de um arquivo antes da conversao
153
+ */
154
+ async createBackup(filePath: string): Promise<string> {
155
+ if (!existsSync(this.backupDir)) {
156
+ mkdirSync(this.backupDir, { recursive: true });
157
+ }
158
+
159
+ const fileName = path.basename(filePath);
160
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
161
+ const backupPath = path.join(this.backupDir, `${fileName}.${timestamp}.bak`);
162
+
163
+ await fs.copyFile(filePath, backupPath);
164
+ return backupPath;
165
+ }
166
+
167
+ /**
168
+ * Converte arquivo de um encoding para outro
169
+ */
170
+ async convertFile(
171
+ filePath: string,
172
+ fromEncoding: string,
173
+ toEncoding: string,
174
+ options: { backup?: boolean; dryRun?: boolean } = {}
175
+ ): Promise<{ success: boolean; message: string; unsupportedChars?: string[] }> {
176
+ try {
177
+ if (!existsSync(filePath)) {
178
+ return { success: false, message: `Arquivo nao encontrado: ${filePath}` };
179
+ }
180
+
181
+ const normalizedFrom = this.normalizeEncoding(fromEncoding);
182
+ const normalizedTo = this.normalizeEncoding(toEncoding);
183
+
184
+ if (normalizedFrom === normalizedTo) {
185
+ return { success: true, message: "Encodings iguais, nenhuma conversao necessaria" };
186
+ }
187
+
188
+ // Le o arquivo como buffer
189
+ const buffer = await fs.readFile(filePath);
190
+
191
+ // Se tem BOM UTF-8, remove para processamento
192
+ let contentBuffer = buffer;
193
+ let hadBOM = false;
194
+ if (buffer.length >= 3 &&
195
+ buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
196
+ contentBuffer = buffer.slice(3);
197
+ hadBOM = true;
198
+ }
199
+
200
+ // Decodifica do encoding de origem
201
+ let content: string;
202
+ try {
203
+ // Usa TextDecoder para encodings padrao ou iconv-lite para Windows-1252
204
+ if (normalizedFrom === "utf-8") {
205
+ content = contentBuffer.toString("utf-8");
206
+ } else {
207
+ // Para Windows-1252 e outros single-byte, usamos decoding manual
208
+ content = this.decodeSingleByte(contentBuffer, normalizedFrom);
209
+ }
210
+ } catch (e) {
211
+ return { success: false, message: `Erro ao decodificar de ${fromEncoding}: ${e}` };
212
+ }
213
+
214
+ if (options.dryRun) {
215
+ return {
216
+ success: true,
217
+ message: `[DRY RUN] Converteria de ${fromEncoding} para ${toEncoding}`
218
+ };
219
+ }
220
+
221
+ // Cria backup se solicitado
222
+ if (options.backup) {
223
+ const backupPath = await this.createBackup(filePath);
224
+ Logger.debug(`Backup criado: ${backupPath}`);
225
+ }
226
+
227
+ // Codifica para o encoding destino
228
+ let outputBuffer: Buffer;
229
+ const unsupportedChars: string[] = [];
230
+
231
+ if (normalizedTo === "utf-8") {
232
+ outputBuffer = Buffer.from(content, "utf-8");
233
+ // Adiciona BOM se o original tinha
234
+ if (hadBOM) {
235
+ outputBuffer = Buffer.concat([UTF8_BOM, outputBuffer]);
236
+ }
237
+ } else {
238
+ const result = this.encodeSingleByte(content, normalizedTo);
239
+ outputBuffer = result.buffer;
240
+ unsupportedChars.push(...result.unsupportedChars);
241
+ }
242
+
243
+ await fs.writeFile(filePath, outputBuffer);
244
+
245
+ let message = `Convertido: ${fromEncoding} -> ${toEncoding}`;
246
+ if (unsupportedChars.length > 0) {
247
+ const unique = [...new Set(unsupportedChars)].slice(0, 5);
248
+ // Mostra o código Unicode em vez do caractere (evita ? no terminal)
249
+ const codes = unique.map(c => `U+${c.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')} (${c})`);
250
+ message += ` (${unsupportedChars.length} caractere(s) nao suportado(s): ${codes.join(", ")})`;
251
+ }
252
+
253
+ return {
254
+ success: true,
255
+ message,
256
+ unsupportedChars: unsupportedChars.length > 0 ? unsupportedChars : undefined
257
+ };
258
+
259
+ } catch (e) {
260
+ const error = e instanceof Error ? e.message : String(e);
261
+ return { success: false, message: `Erro: ${error}` };
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Tenta corrigir mojibake (texto corrompido por encoding errado)
267
+ * Ex: A�o -> Acao
268
+ */
269
+ async fixMojibake(
270
+ filePath: string,
271
+ options: { backup?: boolean; dryRun?: boolean; force?: boolean } = {}
272
+ ): Promise<{ success: boolean; message: string; detectedFrom?: string }> {
273
+ try {
274
+ if (!existsSync(filePath)) {
275
+ return { success: false, message: `Arquivo nao encontrado: ${filePath}` };
276
+ }
277
+
278
+ const buffer = await fs.readFile(filePath);
279
+ const detection = this.detectEncoding(buffer);
280
+
281
+ // Se ja e UTF-8, nao precisa converter (a menos que force=true)
282
+ if (detection.encoding === "utf-8" && detection.confidence > 0.8 && !options.force) {
283
+ // Mas verifica se tem padrões de mojibake comuns
284
+ const content = buffer.toString("utf-8");
285
+ const hasMojibake = /[\u00EF\u00BF\u00BD\u00C3\u00A0-\u00C3\u00B9]/.test(content);
286
+
287
+ if (!hasMojibake) {
288
+ return { success: true, message: "Arquivo ja esta em UTF-8", detectedFrom: "utf-8" };
289
+ }
290
+ // Se tem mojibake, continua com a correção mesmo sendo UTF-8
291
+ }
292
+
293
+ // Tenta converter assumindo que esta em Windows-1252 mas foi lido como UTF-8
294
+ // Ou vice-versa
295
+ let fixed = false;
296
+ let usedEncoding = "";
297
+
298
+ // Tenta interpretar como Windows-1252
299
+ try {
300
+ const asLatin1 = buffer.toString("latin1");
301
+ const reencoded = Buffer.from(asLatin1, "utf-8");
302
+ const redecoded = reencoded.toString("utf-8");
303
+
304
+ // Se o resultado tem caracteres legiveis comuns em portugues, provavelmente funcionou
305
+ if (this.looksLikePortuguese(redecoded)) {
306
+ if (!options.dryRun) {
307
+ if (options.backup) {
308
+ await this.createBackup(filePath);
309
+ }
310
+ await fs.writeFile(filePath, reencoded);
311
+ }
312
+ fixed = true;
313
+ usedEncoding = "windows-1252";
314
+ }
315
+ } catch (e) {
316
+ // ignora
317
+ }
318
+
319
+ if (fixed) {
320
+ return {
321
+ success: true,
322
+ message: options.dryRun ?
323
+ `[DRY RUN] Corrigiria mojibake (detectado: ${usedEncoding})` :
324
+ `Mojibake corrigido! (detectado: ${usedEncoding})`,
325
+ detectedFrom: usedEncoding
326
+ };
327
+ }
328
+
329
+ return {
330
+ success: false,
331
+ message: "Nao foi possivel detectar/corrigir o mojibake automaticamente",
332
+ detectedFrom: detection.encoding
333
+ };
334
+
335
+ } catch (e) {
336
+ const error = e instanceof Error ? e.message : String(e);
337
+ return { success: false, message: `Erro: ${error}` };
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Verifica se uma string parece portugues valido
343
+ */
344
+ private looksLikePortuguese(text: string): boolean {
345
+ // Conta caracteres comuns em portugues
346
+ const portugueseChars = /[\u00E1\u00E0\u00E2\u00E3\u00E9\u00EA\u00ED\u00F3\u00F4\u00F5\u00FA\u00FC\u00E7\u00C1\u00C0\u00C2\u00C3\u00C9\u00CA\u00CD\u00D3\u00D4\u00D5\u00DA\u00DC\u00C7]/g;
347
+ const matches = text.match(portugueseChars);
348
+
349
+ // Se tem caracteres acentuados brasileiros, provavelmente e portugues
350
+ if (matches && matches.length > 0) {
351
+ return true;
352
+ }
353
+
354
+ // Verifica sequencias suspeitas de mojibake
355
+ const mojibakePatterns = /[\u00EF\u00BF\u00BD\u00C3\u00A1]/;
356
+ return !mojibakePatterns.test(text);
357
+ }
358
+
359
+ /**
360
+ * Decodifica buffer single-byte (Windows-1252, ISO-8859-1)
361
+ */
362
+ private decodeSingleByte(buffer: Buffer, encoding: string): string {
363
+ // Mapeamento Windows-1252 para Unicode
364
+ const windows1252Map: Record<number, string> = {
365
+ 0x80: "\u20AC", 0x82: "\u201A", 0x83: "\u0192", 0x84: "\u201E", 0x85: "\u2026",
366
+ 0x86: "\u2020", 0x87: "\u2021", 0x88: "\u02C6", 0x89: "\u2030", 0x8A: "\u0160",
367
+ 0x8B: "\u2039", 0x8C: "\u0152", 0x8E: "\u017D", 0x91: "\u2018", 0x92: "\u2019",
368
+ 0x93: "\u201C", 0x94: "\u201D", 0x95: "\u2022", 0x96: "\u2013", 0x97: "\u2014",
369
+ 0x98: "\u02DC", 0x99: "\u2122", 0x9A: "\u0161", 0x9B: "\u203A", 0x9C: "\u0153",
370
+ 0x9E: "\u017E", 0x9F: "\u0178",
371
+ };
372
+
373
+ let result = "";
374
+ for (let i = 0; i < buffer.length; i++) {
375
+ const byte = buffer[i];
376
+ if (encoding === "windows-1252" && byte >= 0x80 && windows1252Map[byte]) {
377
+ result += windows1252Map[byte];
378
+ } else {
379
+ result += String.fromCharCode(byte);
380
+ }
381
+ }
382
+ return result;
383
+ }
384
+
385
+ /**
386
+ * Codifica string para single-byte encoding
387
+ * Retorna o buffer e lista de caracteres nao suportados
388
+ */
389
+ private encodeSingleByte(text: string, encoding: string): { buffer: Buffer; unsupportedChars: string[] } {
390
+ // Mapeamento COMPLETO Unicode -> Windows-1252 (usando escapes Unicode)
391
+ const toWindows1252: Record<string, number> = {
392
+ // Range 0x80-0x9F (caracteres especiais do Windows-1252)
393
+ "\u20AC": 0x80, "\u201A": 0x82, "\u0192": 0x83, "\u201E": 0x84, "\u2026": 0x85,
394
+ "\u2020": 0x86, "\u2021": 0x87, "\u02C6": 0x88, "\u2030": 0x89, "\u0160": 0x8A,
395
+ "\u2039": 0x8B, "\u0152": 0x8C, "\u017D": 0x8E, "\u2018": 0x91, "\u2019": 0x92,
396
+ "\u201C": 0x93, "\u201D": 0x94, "\u2022": 0x95, "\u2013": 0x96, "\u2014": 0x97,
397
+ "\u02DC": 0x98, "\u2122": 0x99, "\u0161": 0x9A, "\u203A": 0x9B, "\u0153": 0x9C,
398
+ "\u017E": 0x9E, "\u0178": 0x9F,
399
+
400
+ // Range 0xA0-0xBF
401
+ "\u00A0": 0xA0, "\u00A1": 0xA1, "\u00A2": 0xA2, "\u00A3": 0xA3, "\u00A4": 0xA4,
402
+ "\u00A5": 0xA5, "\u00A6": 0xA6, "\u00A7": 0xA7, "\u00A8": 0xA8, "\u00A9": 0xA9,
403
+ "\u00AA": 0xAA, "\u00AB": 0xAB, "\u00AC": 0xAC, "\u00AD": 0xAD, "\u00AE": 0xAE,
404
+ "\u00AF": 0xAF, "\u00B0": 0xB0, "\u00B1": 0xB1, "\u00B2": 0xB2, "\u00B3": 0xB3,
405
+ "\u00B4": 0xB4, "\u00B5": 0xB5, "\u00B6": 0xB6, "\u00B7": 0xB7, "\u00B8": 0xB8,
406
+ "\u00B9": 0xB9, "\u00BA": 0xBA, "\u00BB": 0xBB, "\u00BC": 0xBC, "\u00BD": 0xBD,
407
+ "\u00BE": 0xBE, "\u00BF": 0xBF,
408
+
409
+ // Letras maiusculas acentuadas (0xC0-0xDF)
410
+ "\u00C0": 0xC0, "\u00C1": 0xC1, "\u00C2": 0xC2, "\u00C3": 0xC3, "\u00C4": 0xC4,
411
+ "\u00C5": 0xC5, "\u00C6": 0xC6, "\u00C7": 0xC7, "\u00C8": 0xC8, "\u00C9": 0xC9,
412
+ "\u00CA": 0xCA, "\u00CB": 0xCB, "\u00CC": 0xCC, "\u00CD": 0xCD, "\u00CE": 0xCE,
413
+ "\u00CF": 0xCF, "\u00D0": 0xD0, "\u00D1": 0xD1, "\u00D2": 0xD2, "\u00D3": 0xD3,
414
+ "\u00D4": 0xD4, "\u00D5": 0xD5, "\u00D6": 0xD6, "\u00D7": 0xD7, "\u00D8": 0xD8,
415
+ "\u00D9": 0xD9, "\u00DA": 0xDA, "\u00DB": 0xDB, "\u00DC": 0xDC, "\u00DD": 0xDD,
416
+ "\u00DE": 0xDE, "\u00DF": 0xDF,
417
+
418
+ // Letras minusculas acentuadas (0xE0-0xFF)
419
+ "\u00E0": 0xE0, "\u00E1": 0xE1, "\u00E2": 0xE2, "\u00E3": 0xE3, "\u00E4": 0xE4,
420
+ "\u00E5": 0xE5, "\u00E6": 0xE6, "\u00E7": 0xE7, "\u00E8": 0xE8, "\u00E9": 0xE9,
421
+ "\u00EA": 0xEA, "\u00EB": 0xEB, "\u00EC": 0xEC, "\u00ED": 0xED, "\u00EE": 0xEE,
422
+ "\u00EF": 0xEF, "\u00F0": 0xF0, "\u00F1": 0xF1, "\u00F2": 0xF2, "\u00F3": 0xF3,
423
+ "\u00F4": 0xF4, "\u00F5": 0xF5, "\u00F6": 0xF6, "\u00F7": 0xF7, "\u00F8": 0xF8,
424
+ "\u00F9": 0xF9, "\u00FA": 0xFA, "\u00FB": 0xFB, "\u00FC": 0xFC, "\u00FD": 0xFD,
425
+ "\u00FE": 0xFE, "\u00FF": 0xFF,
426
+ };
427
+
428
+ const bytes: number[] = [];
429
+ const unsupportedChars: string[] = [];
430
+
431
+ for (const char of text) {
432
+ const code = char.charCodeAt(0);
433
+
434
+ if (code < 128) {
435
+ // ASCII direto
436
+ bytes.push(code);
437
+ } else if (encoding === "windows-1252" && toWindows1252[char] !== undefined) {
438
+ bytes.push(toWindows1252[char]);
439
+ } else if (encoding === "iso-8859-1" && code >= 0xA0 && code <= 0xFF) {
440
+ // ISO-8859-1 suporta diretamente 0xA0-0xFF
441
+ bytes.push(code);
442
+ } else {
443
+ // Caractere nao suportado no encoding
444
+ unsupportedChars.push(char);
445
+ bytes.push(0x3F); // ?
446
+ }
447
+ }
448
+
449
+ return { buffer: Buffer.from(bytes), unsupportedChars };
450
+ }
451
+
452
+ /**
453
+ * Lista arquivos de texto em um diretorio
454
+ */
455
+ async findTextFiles(dir: string, extensions: string[] = [".java", ".xml", ".properties", ".txt", ".jsp", ".html", ".js", ".css"]): Promise<string[]> {
456
+ const results: string[] = [];
457
+
458
+ const scan = async (currentDir: string) => {
459
+ try {
460
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
461
+
462
+ for (const entry of entries) {
463
+ const fullPath = path.join(currentDir, entry.name);
464
+
465
+ if (entry.isDirectory()) {
466
+ // Ignora diretorios comuns de build
467
+ if (["node_modules", "target", "build", ".git", ".xavva", "dist", "bin"].includes(entry.name)) {
468
+ continue;
469
+ }
470
+ await scan(fullPath);
471
+ } else if (entry.isFile()) {
472
+ const ext = path.extname(entry.name).toLowerCase();
473
+ if (extensions.includes(ext)) {
474
+ results.push(fullPath);
475
+ }
476
+ }
477
+ }
478
+ } catch (e) {
479
+ // ignora diretorios sem permissao
480
+ }
481
+ };
482
+
483
+ await scan(dir);
484
+ return results;
485
+ }
486
+
487
+ /**
488
+ * Converte todos os arquivos de um diretorio
489
+ */
490
+ async convertDirectory(
491
+ dir: string,
492
+ fromEncoding: string,
493
+ toEncoding: string,
494
+ options: {
495
+ backup?: boolean;
496
+ dryRun?: boolean;
497
+ extensions?: string[];
498
+ } = {}
499
+ ): Promise<{ success: number; failed: number; total: number; totalUnsupported: number }> {
500
+ const files = await this.findTextFiles(dir, options.extensions);
501
+ let success = 0;
502
+ let failed = 0;
503
+ let totalUnsupported = 0;
504
+
505
+ Logger.info("Arquivos encontrados", String(files.length));
506
+
507
+ for (const file of files) {
508
+ const result = await this.convertFile(file, fromEncoding, toEncoding, {
509
+ backup: options.backup,
510
+ dryRun: options.dryRun
511
+ });
512
+
513
+ if (result.success) {
514
+ success++;
515
+ if (result.unsupportedChars) {
516
+ totalUnsupported += result.unsupportedChars.length;
517
+ }
518
+ if (!options.dryRun) {
519
+ Logger.success(`${path.relative(dir, file)}: ${result.message}`);
520
+ } else {
521
+ Logger.info(`${path.relative(dir, file)}`, result.message);
522
+ }
523
+ } else {
524
+ failed++;
525
+ Logger.warn(`${path.relative(dir, file)}: ${result.message}`);
526
+ }
527
+ }
528
+
529
+ return { success, failed, total: files.length, totalUnsupported };
530
+ }
531
+
532
+ /**
533
+ * Detecta encoding de todos os arquivos em um diretorio
534
+ */
535
+ async detectDirectoryEncodings(dir: string): Promise<Map<string, DetectedEncoding>> {
536
+ const files = await this.findTextFiles(dir);
537
+ const results = new Map<string, DetectedEncoding>();
538
+
539
+ for (const file of files) {
540
+ const detection = await this.detectFileEncoding(file);
541
+ if (detection) {
542
+ results.set(file, detection);
543
+ }
544
+ }
545
+
546
+ return results;
547
+ }
548
+ }