@gilbert_oliveira/commit-wizard 1.2.2 → 2.0.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/.commit-wizardrc +17 -0
- package/LICENSE +3 -3
- package/README.md +374 -211
- package/bin/commit-wizard.ts +51 -0
- package/dist/commit-wizard.js +195 -0
- package/package.json +71 -64
- package/src/config/index.ts +237 -0
- package/src/core/cache.ts +210 -0
- package/src/core/index.ts +381 -0
- package/src/core/openai.ts +336 -0
- package/src/core/smart-split.ts +698 -0
- package/src/git/index.ts +177 -0
- package/src/ui/index.ts +204 -0
- package/src/ui/smart-split.ts +141 -0
- package/src/utils/args.ts +56 -0
- package/src/utils/polyfill.ts +82 -0
- package/dist/ai-service.d.ts +0 -44
- package/dist/ai-service.js +0 -287
- package/dist/ai-service.js.map +0 -1
- package/dist/commit-splitter.d.ts +0 -48
- package/dist/commit-splitter.js +0 -227
- package/dist/commit-splitter.js.map +0 -1
- package/dist/config.d.ts +0 -22
- package/dist/config.js +0 -84
- package/dist/config.js.map +0 -1
- package/dist/diff-processor.d.ts +0 -39
- package/dist/diff-processor.js +0 -156
- package/dist/diff-processor.js.map +0 -1
- package/dist/git-utils.d.ts +0 -72
- package/dist/git-utils.js +0 -373
- package/dist/git-utils.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -486
- package/dist/index.js.map +0 -1
|
@@ -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
|
+
}
|