@gilbert_oliveira/commit-wizard 1.2.2 → 2.0.1

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,698 @@
1
+ import type { Config } from '../config/index.ts';
2
+ import type { CLIArgs } from '../utils/args.ts';
3
+ import { getCachedAnalysis, setCachedAnalysis } from './cache.ts';
4
+ import { showCommitResult } from '../ui/index.ts';
5
+ import { log } from '@clack/prompts';
6
+
7
+ export interface FileGroup {
8
+ id: string;
9
+ name: string;
10
+ description: string;
11
+ files: string[];
12
+ diff: string;
13
+ confidence: number;
14
+ }
15
+
16
+ export interface SmartSplitResult {
17
+ success: boolean;
18
+ groups?: FileGroup[];
19
+ error?: string;
20
+ }
21
+
22
+ /**
23
+ * Constrói prompt otimizado para análise de contexto
24
+ */
25
+ function buildContextAnalysisPrompt(
26
+ files: string[],
27
+ overallDiff: string
28
+ ): string {
29
+ // Limitar o tamanho do diff para evitar exceder tokens
30
+ const maxDiffLength = 8000; // Limite conservador
31
+ const truncatedDiff = overallDiff.length > maxDiffLength
32
+ ? overallDiff.substring(0, maxDiffLength) + '\n... (diff truncado)'
33
+ : overallDiff;
34
+
35
+ // Calcular estatísticas básicas
36
+ const totalFiles = files.length;
37
+ const fileTypes = files.reduce((acc, file) => {
38
+ const ext = file.split('.').pop() || 'sem-extensao';
39
+ acc[ext] = (acc[ext] || 0) + 1;
40
+ return acc;
41
+ }, {} as Record<string, number>);
42
+
43
+ const fileStats = Object.entries(fileTypes)
44
+ .map(([ext, count]) => `${ext}: ${count}`)
45
+ .join(', ');
46
+
47
+ return `Analise os arquivos modificados e agrupe em commits lógicos.
48
+
49
+ ARQUIVOS (${totalFiles}): ${files.join(', ')}
50
+ TIPOS: ${fileStats}
51
+
52
+ DIFF RESUMIDO:
53
+ \`\`\`
54
+ ${truncatedDiff}
55
+ \`\`\`
56
+
57
+ Agrupe arquivos relacionados. Máximo 5 grupos. Responda em JSON:
58
+ {
59
+ "groups": [
60
+ {
61
+ "id": "grupo-1",
62
+ "name": "Nome do Grupo",
63
+ "description": "Descrição breve",
64
+ "files": ["arquivo1.ts", "arquivo2.ts"],
65
+ "confidence": 0.8
66
+ }
67
+ ]
68
+ }`;
69
+ }
70
+
71
+ /**
72
+ * Constrói prompt de fallback usando apenas nomes de arquivos
73
+ */
74
+ function buildFallbackPrompt(files: string[]): string {
75
+ // Agrupar arquivos por diretório
76
+ const filesByDir = files.reduce((acc, file) => {
77
+ const dir = file.split('/').slice(0, -1).join('/') || 'root';
78
+ if (!acc[dir]) acc[dir] = [];
79
+ acc[dir].push(file);
80
+ return acc;
81
+ }, {} as Record<string, string[]>);
82
+
83
+ const dirStats = Object.entries(filesByDir)
84
+ .map(([dir, files]) => `${dir}: ${files.length} arquivo(s)`)
85
+ .join('\n');
86
+
87
+ return `Agrupe estes arquivos em commits lógicos baseado nos diretórios:
88
+
89
+ ARQUIVOS POR DIRETÓRIO:
90
+ ${dirStats}
91
+
92
+ LISTA COMPLETA: ${files.join(', ')}
93
+
94
+ Agrupe por funcionalidade relacionada. Máximo 5 grupos. JSON:
95
+ {
96
+ "groups": [
97
+ {
98
+ "id": "grupo-1",
99
+ "name": "Nome do Grupo",
100
+ "description": "Descrição breve",
101
+ "files": ["arquivo1.ts", "arquivo2.ts"],
102
+ "confidence": 0.7
103
+ }
104
+ ]
105
+ }`;
106
+ }
107
+
108
+ /**
109
+ * Analisa o contexto dos arquivos usando IA
110
+ */
111
+ export async function analyzeFileContext(
112
+ files: string[],
113
+ overallDiff: string,
114
+ config: Config
115
+ ): Promise<SmartSplitResult> {
116
+ try {
117
+ if (!config.openai.apiKey) {
118
+ return {
119
+ success: false,
120
+ error: 'Chave da OpenAI não encontrada',
121
+ };
122
+ }
123
+
124
+ // Verificar cache primeiro
125
+ const cachedResult = getCachedAnalysis(files, overallDiff);
126
+ if (cachedResult.hit && cachedResult.groups) {
127
+ return {
128
+ success: true,
129
+ groups: cachedResult.groups,
130
+ };
131
+ }
132
+
133
+ // Decidir qual prompt usar baseado no tamanho
134
+ const maxDiffLength = 6000; // Limite mais conservador
135
+ const useFallback = overallDiff.length > maxDiffLength;
136
+
137
+ const prompt = useFallback
138
+ ? buildFallbackPrompt(files)
139
+ : buildContextAnalysisPrompt(files, overallDiff);
140
+
141
+ // Log opcional sobre o uso do fallback
142
+ if (useFallback) {
143
+ console.warn(`⚠️ Diff muito grande (${overallDiff.length} chars), usando análise baseada em nomes de arquivos`);
144
+ }
145
+
146
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
147
+ method: 'POST',
148
+ headers: {
149
+ Authorization: `Bearer ${config.openai.apiKey}`,
150
+ 'Content-Type': 'application/json',
151
+ },
152
+ body: JSON.stringify({
153
+ model: config.openai.model,
154
+ messages: [
155
+ {
156
+ role: 'user',
157
+ content: prompt,
158
+ },
159
+ ],
160
+ max_tokens: 800, // Reduzido para economizar tokens
161
+ temperature: 0.3,
162
+ }),
163
+ });
164
+
165
+ if (!response.ok) {
166
+ const errorData = (await response.json().catch(() => ({}))) as any;
167
+ return {
168
+ success: false,
169
+ error: `Erro da OpenAI (${response.status}): ${errorData.error?.message || 'Erro desconhecido'}`,
170
+ };
171
+ }
172
+
173
+ const data = (await response.json()) as any;
174
+ const content = data.choices?.[0]?.message?.content?.trim();
175
+
176
+ if (!content) {
177
+ return {
178
+ success: false,
179
+ error: 'OpenAI retornou resposta vazia',
180
+ };
181
+ }
182
+
183
+ // Extrair JSON da resposta
184
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
185
+ if (!jsonMatch) {
186
+ return {
187
+ success: false,
188
+ error: 'Resposta da OpenAI não contém JSON válido',
189
+ };
190
+ }
191
+
192
+ const analysis = JSON.parse(jsonMatch[0]);
193
+
194
+ if (!analysis.groups || !Array.isArray(analysis.groups)) {
195
+ return {
196
+ success: false,
197
+ error: 'Formato de resposta inválido da OpenAI',
198
+ };
199
+ }
200
+
201
+ // Validar que todos os arquivos estão incluídos
202
+ const allGroupedFiles = analysis.groups.flatMap(
203
+ (group: any) => group.files || []
204
+ );
205
+ const missingFiles = files.filter(
206
+ (file) => !allGroupedFiles.includes(file)
207
+ );
208
+
209
+ if (missingFiles.length > 0) {
210
+ // Adicionar arquivos faltantes ao primeiro grupo
211
+ analysis.groups[0].files = [
212
+ ...(analysis.groups[0].files || []),
213
+ ...missingFiles,
214
+ ];
215
+ }
216
+
217
+ const groups = analysis.groups.map((group: any) => ({
218
+ id: group.id || `group-${Math.random().toString(36).substr(2, 9)}`,
219
+ name: group.name || 'Grupo sem nome',
220
+ description: group.description || 'Sem descrição',
221
+ files: group.files || [],
222
+ diff: '', // Será preenchido depois
223
+ confidence: group.confidence || 0.5,
224
+ }));
225
+
226
+ // Armazenar no cache
227
+ setCachedAnalysis(files, overallDiff, groups);
228
+
229
+ return {
230
+ success: true,
231
+ groups,
232
+ };
233
+ } catch (error) {
234
+ return {
235
+ success: false,
236
+ error: `Erro ao analisar contexto: ${error instanceof Error ? error.message : 'Erro desconhecido'}`,
237
+ };
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Gera diff para um grupo de arquivos (otimizado para tokens)
243
+ */
244
+ export async function generateGroupDiff(group: FileGroup): Promise<string> {
245
+ const { getFileDiff } = await import('../git/index.ts');
246
+
247
+ const diffs = group.files
248
+ .map((file) => {
249
+ try {
250
+ const diff = getFileDiff(file);
251
+ // Limitar tamanho do diff individual
252
+ const maxDiffLength = 4000;
253
+ return diff.length > maxDiffLength
254
+ ? diff.substring(0, maxDiffLength) + '\n... (diff truncado)'
255
+ : diff;
256
+ } catch {
257
+ return '';
258
+ }
259
+ })
260
+ .filter((diff) => diff.length > 0);
261
+
262
+ // Se não há diffs válidos, mas há arquivos no grupo,
263
+ // pode ser que os arquivos sejam novos (untracked) ou foram recriados
264
+ if (diffs.length === 0 && group.files.length > 0) {
265
+ // Verificar se os arquivos existem e são novos
266
+ const { execSync } = await import('child_process');
267
+
268
+ const newFiles = group.files.filter((file) => {
269
+ try {
270
+ // Verificar se o arquivo existe
271
+ execSync(`test -f "${file}"`, { stdio: 'ignore' });
272
+
273
+ // Verificar se é um arquivo novo (não tracked)
274
+ const status = execSync(`git status --porcelain -- "${file}"`, {
275
+ encoding: 'utf-8',
276
+ stdio: 'pipe',
277
+ }).trim();
278
+
279
+ // Se começa com ??, é um arquivo novo
280
+ return status.startsWith('??');
281
+ } catch {
282
+ return false;
283
+ }
284
+ });
285
+
286
+ if (newFiles.length > 0) {
287
+ // Para arquivos novos, criar um diff simulado mais simples
288
+ return newFiles
289
+ .map((file) => {
290
+ try {
291
+ const content = execSync(`cat "${file}"`, {
292
+ encoding: 'utf-8',
293
+ stdio: 'pipe',
294
+ });
295
+ // Limitar conteúdo do arquivo novo
296
+ const maxContentLength = 2000;
297
+ const truncatedContent = content.length > maxContentLength
298
+ ? content.substring(0, maxContentLength) + '\n... (conteúdo truncado)'
299
+ : content;
300
+
301
+ return `diff --git a/${file} b/${file}\nnew file mode 100644\nindex 0000000..${Math.random().toString(36).substr(2, 7)}\n--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${truncatedContent.split('\n').length} @@\n${truncatedContent
302
+ .split('\n')
303
+ .map((line) => `+${line}`)
304
+ .join('\n')}`;
305
+ } catch {
306
+ return '';
307
+ }
308
+ })
309
+ .filter((diff) => diff.length > 0)
310
+ .join('\n');
311
+ }
312
+
313
+ // Verificar se há arquivos que foram deletados e recriados
314
+ const recreatedFiles = group.files.filter((file) => {
315
+ try {
316
+ // Verificar se o arquivo existe
317
+ execSync(`test -f "${file}"`, { stdio: 'ignore' });
318
+
319
+ // Verificar se está no stage mas não tem diff
320
+ const stagedStatus = execSync(`git diff --cached --name-only`, {
321
+ encoding: 'utf-8',
322
+ stdio: 'pipe',
323
+ })
324
+ .trim()
325
+ .split('\n');
326
+
327
+ return stagedStatus.includes(file);
328
+ } catch {
329
+ return false;
330
+ }
331
+ });
332
+
333
+ if (recreatedFiles.length > 0) {
334
+ // Para arquivos recriados, criar um diff que mostra o conteúdo atual
335
+ return recreatedFiles
336
+ .map((file) => {
337
+ try {
338
+ const content = execSync(`cat "${file}"`, {
339
+ encoding: 'utf-8',
340
+ stdio: 'pipe',
341
+ });
342
+ // Limitar conteúdo do arquivo recriado
343
+ const maxContentLength = 2000;
344
+ const truncatedContent = content.length > maxContentLength
345
+ ? content.substring(0, maxContentLength) + '\n... (conteúdo truncado)'
346
+ : content;
347
+
348
+ return `diff --git a/${file} b/${file}\nindex 0000000..${Math.random().toString(36).substr(2, 7)} 100644\n--- a/${file}\n+++ b/${file}\n@@ -1 +1,${truncatedContent.split('\n').length} @@\n${truncatedContent
349
+ .split('\n')
350
+ .map((line) => `+${line}`)
351
+ .join('\n')}`;
352
+ } catch {
353
+ return '';
354
+ }
355
+ })
356
+ .filter((diff) => diff.length > 0)
357
+ .join('\n');
358
+ }
359
+ }
360
+
361
+ // Limitar tamanho total do diff combinado
362
+ const combinedDiff = diffs.join('\n');
363
+ const maxTotalLength = 8000;
364
+
365
+ return combinedDiff.length > maxTotalLength
366
+ ? combinedDiff.substring(0, maxTotalLength) + '\n... (diff total truncado)'
367
+ : combinedDiff;
368
+ }
369
+
370
+ /**
371
+ * Executa o smart split mode
372
+ */
373
+ export async function handleSmartSplitMode(
374
+ gitStatus: any,
375
+ config: Config,
376
+ args: CLIArgs
377
+ ): Promise<void> {
378
+ if (!args.silent) {
379
+ log.info('🧠 Modo Smart Split ativado - Agrupando arquivos por contexto');
380
+ }
381
+
382
+ // Analisar contexto dos arquivos
383
+ if (!args.silent) {
384
+ log.info('🤖 Analisando contexto das mudanças...');
385
+ }
386
+
387
+ const analysis = await analyzeFileContext(
388
+ gitStatus.stagedFiles,
389
+ gitStatus.diff,
390
+ config
391
+ );
392
+
393
+ if (!analysis.success) {
394
+ if (!args.silent) {
395
+ log.error(`❌ Erro na análise de contexto: ${analysis.error}`);
396
+ }
397
+ return;
398
+ }
399
+
400
+ if (!analysis.groups || analysis.groups.length === 0) {
401
+ if (!args.silent) {
402
+ log.error('❌ Nenhum grupo foi criado pela análise');
403
+ }
404
+ return;
405
+ }
406
+
407
+ if (!args.silent) {
408
+ log.success(`✅ ${analysis.groups.length} grupo(s) identificado(s):`);
409
+ analysis.groups.forEach((group, index) => {
410
+ log.info(
411
+ ` ${index + 1}. ${group.name} (${group.files.length} arquivo(s))`
412
+ );
413
+ log.info(` 📄 ${group.files.join(', ')}`);
414
+ });
415
+ }
416
+
417
+ // Mostrar interface de Smart Split para o usuário decidir
418
+ if (!args.yes && !args.silent) {
419
+ const { showSmartSplitGroups } = await import('../ui/smart-split.ts');
420
+ const userAction = await showSmartSplitGroups(analysis.groups);
421
+
422
+ if (userAction.action === 'cancel') {
423
+ if (!args.silent) {
424
+ log.info('❌ Operação cancelada pelo usuário');
425
+ }
426
+ return;
427
+ }
428
+
429
+ if (userAction.action === 'manual') {
430
+ // Delegar para modo manual - re-executar com flag split
431
+ const newArgs = { ...args, split: true, smartSplit: false };
432
+ const { main } = await import('./index.ts');
433
+ await main(newArgs);
434
+ return;
435
+ }
436
+
437
+ // Se o usuário aceitou, segue com os grupos sugeridos
438
+ }
439
+
440
+ // Processar cada grupo
441
+ for (let i = 0; i < analysis.groups.length; i++) {
442
+ const group = analysis.groups[i];
443
+
444
+ if (!group) {
445
+ if (!args.silent) {
446
+ log.error(`❌ Grupo ${i + 1} é undefined`);
447
+ }
448
+ continue;
449
+ }
450
+
451
+ if (!args.silent) {
452
+ log.info(
453
+ `\n🔄 Processando grupo ${i + 1}/${analysis.groups.length}: ${group.name}`
454
+ );
455
+ }
456
+
457
+ // Gerar diff para o grupo
458
+ const groupDiff = await generateGroupDiff(group);
459
+
460
+ if (!groupDiff) {
461
+ if (!args.silent) {
462
+ log.warn(`⚠️ Nenhum diff encontrado para o grupo: ${group.name}`);
463
+ log.info(` 📄 Arquivos: ${group.files.join(', ')}`);
464
+ log.info(
465
+ ` 💡 Possível causa: arquivos novos, deletados/recriados, ou sem mudanças`
466
+ );
467
+ }
468
+ continue;
469
+ }
470
+
471
+ // Gerar mensagem de commit para o grupo
472
+ if (!args.silent) {
473
+ log.info(`🤖 Gerando commit para: ${group.name}`);
474
+ }
475
+
476
+ const { generateWithRetry } = await import('./openai.ts');
477
+ const result = await generateWithRetry(groupDiff, config, group.files);
478
+
479
+ if (!result.success) {
480
+ if (!args.silent) {
481
+ log.error(`❌ Erro ao gerar commit para ${group.name}: ${result.error}`);
482
+ }
483
+ continue;
484
+ }
485
+
486
+ if (!result.suggestion) {
487
+ if (!args.silent) {
488
+ log.error(`❌ Nenhuma sugestão gerada para ${group.name}`);
489
+ }
490
+ continue;
491
+ }
492
+
493
+ // Modo Dry Run
494
+ if (config.dryRun) {
495
+ if (!args.silent) {
496
+ log.info(`🔍 Dry Run - Grupo: ${group.name}`);
497
+ log.info(`📄 Arquivos: ${group.files.join(', ')}`);
498
+ log.info(`💭 Mensagem: "${result.suggestion.message}"`);
499
+ }
500
+ continue;
501
+ }
502
+
503
+ // Interface do usuário
504
+ if (args.yes) {
505
+ // Modo automático
506
+ const { executeFileCommit } = await import('../git/index.ts');
507
+ let commitResult;
508
+
509
+ // Fazer commit apenas dos arquivos do grupo atual
510
+ if (group.files.length === 1 && group.files[0]) {
511
+ commitResult = executeFileCommit(
512
+ group.files[0],
513
+ result.suggestion.message || ''
514
+ );
515
+ } else {
516
+ // Para múltiplos arquivos, usar commit normal mas com apenas os arquivos do grupo
517
+ const { execSync } = await import('child_process');
518
+ try {
519
+ // Fazer commit apenas dos arquivos do grupo
520
+ const filesArg = group.files.map((f) => `"${f}"`).join(' ');
521
+ execSync(
522
+ `git commit ${filesArg} -m "${(result.suggestion.message || '').replace(/"/g, '\\"')}"`,
523
+ {
524
+ stdio: 'pipe',
525
+ }
526
+ );
527
+
528
+ const hash = execSync('git rev-parse HEAD', {
529
+ encoding: 'utf-8',
530
+ stdio: 'pipe',
531
+ }).trim();
532
+
533
+ commitResult = {
534
+ success: true,
535
+ hash,
536
+ message: result.suggestion.message || '',
537
+ };
538
+ } catch (error) {
539
+ commitResult = {
540
+ success: false,
541
+ error:
542
+ error instanceof Error
543
+ ? error.message
544
+ : 'Erro desconhecido ao executar commit',
545
+ };
546
+ }
547
+ }
548
+
549
+ showCommitResult(
550
+ commitResult.success,
551
+ commitResult.hash,
552
+ commitResult.error
553
+ );
554
+ } else {
555
+ // Modo interativo
556
+ const {
557
+ showCommitPreview,
558
+ editCommitMessage,
559
+ copyToClipboard,
560
+ showCancellation,
561
+ } = await import('../ui/index.ts');
562
+
563
+ const uiAction = await showCommitPreview(result.suggestion);
564
+
565
+ switch (uiAction.action) {
566
+ case 'commit': {
567
+ const { executeFileCommit } = await import('../git/index.ts');
568
+ let commitResult;
569
+
570
+ // Fazer commit apenas dos arquivos do grupo atual
571
+ const commitMessage =
572
+ result.suggestion.message || 'Atualização de arquivos';
573
+ if (group.files.length === 1 && group.files[0]) {
574
+ commitResult = executeFileCommit(group.files[0], commitMessage);
575
+ } else {
576
+ // Para múltiplos arquivos, usar commit normal mas com apenas os arquivos do grupo
577
+ const { execSync } = await import('child_process');
578
+ try {
579
+ // Fazer commit apenas dos arquivos do grupo
580
+ const filesArg = group.files.map((f) => `"${f}"`).join(' ');
581
+ execSync(
582
+ `git commit ${filesArg} -m "${commitMessage.replace(/"/g, '"')}"`,
583
+ {
584
+ stdio: 'pipe',
585
+ }
586
+ );
587
+
588
+ const hash = execSync('git rev-parse HEAD', {
589
+ encoding: 'utf-8',
590
+ stdio: 'pipe',
591
+ }).trim();
592
+
593
+ commitResult = { success: true, hash, message: commitMessage };
594
+ } catch (error) {
595
+ commitResult = {
596
+ success: false,
597
+ error:
598
+ error instanceof Error
599
+ ? error.message
600
+ : 'Erro desconhecido ao executar commit',
601
+ };
602
+ }
603
+ }
604
+
605
+ showCommitResult(
606
+ commitResult.success,
607
+ commitResult.hash,
608
+ commitResult.error
609
+ );
610
+ break;
611
+ }
612
+ case 'edit': {
613
+ const editAction = await editCommitMessage(result.suggestion.message);
614
+ if (editAction.action === 'commit' && editAction.message) {
615
+ const { executeFileCommit } = await import('../git/index.ts');
616
+ let editCommitResult;
617
+
618
+ // Fazer commit apenas dos arquivos do grupo atual
619
+ if (group.files.length === 1 && group.files[0]) {
620
+ editCommitResult = executeFileCommit(
621
+ group.files[0],
622
+ editAction.message || ''
623
+ );
624
+ } else {
625
+ // Para múltiplos arquivos, usar commit normal mas com apenas os arquivos do grupo
626
+ const { execSync } = await import('child_process');
627
+ try {
628
+ // Fazer commit apenas dos arquivos do grupo
629
+ const filesArg = group.files.map((f) => `"${f}"`).join(' ');
630
+ execSync(
631
+ `git commit ${filesArg} -m "${(editAction.message || '').replace(/"/g, '"')}"`,
632
+ {
633
+ stdio: 'pipe',
634
+ }
635
+ );
636
+
637
+ const hash = execSync('git rev-parse HEAD', {
638
+ encoding: 'utf-8',
639
+ stdio: 'pipe',
640
+ }).trim();
641
+
642
+ editCommitResult = {
643
+ success: true,
644
+ hash,
645
+ message: editAction.message || '',
646
+ };
647
+ } catch (error) {
648
+ editCommitResult = {
649
+ success: false,
650
+ error:
651
+ error instanceof Error
652
+ ? error.message
653
+ : 'Erro desconhecido ao executar commit',
654
+ };
655
+ }
656
+ }
657
+
658
+ showCommitResult(
659
+ editCommitResult.success,
660
+ editCommitResult.hash,
661
+ editCommitResult.error
662
+ );
663
+ }
664
+ break;
665
+ }
666
+ case 'copy': {
667
+ await copyToClipboard(result.suggestion.message);
668
+ if (!args.silent) {
669
+ log.info('🎯 Mensagem copiada para clipboard');
670
+ }
671
+ break;
672
+ }
673
+ case 'cancel': {
674
+ showCancellation();
675
+ return;
676
+ }
677
+ }
678
+ }
679
+
680
+ // Perguntar se quer continuar (exceto em modo automático)
681
+ if (i < analysis.groups.length - 1 && !args.yes) {
682
+ const { askContinueCommits } = await import('../ui/index.ts');
683
+ const remainingGroups = analysis.groups
684
+ .slice(i + 1)
685
+ .filter((g) => g !== undefined)
686
+ .map((g) => g!.name);
687
+ const continueCommits = await askContinueCommits(remainingGroups);
688
+
689
+ if (!continueCommits) {
690
+ break;
691
+ }
692
+ }
693
+ }
694
+
695
+ if (!args.silent) {
696
+ log.success('✅ Smart Split concluído!');
697
+ }
698
+ }