@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,381 @@
|
|
|
1
|
+
import { log } from '@clack/prompts';
|
|
2
|
+
import { loadConfig, validateConfig } from '../config/index.ts';
|
|
3
|
+
import {
|
|
4
|
+
isGitRepository,
|
|
5
|
+
getGitStatus,
|
|
6
|
+
getDiffStats,
|
|
7
|
+
executeCommit,
|
|
8
|
+
executeFileCommit,
|
|
9
|
+
} from '../git/index.ts';
|
|
10
|
+
import { generateWithRetry } from './openai.ts';
|
|
11
|
+
import {
|
|
12
|
+
showCommitPreview,
|
|
13
|
+
editCommitMessage,
|
|
14
|
+
copyToClipboard,
|
|
15
|
+
showCommitResult,
|
|
16
|
+
showCancellation,
|
|
17
|
+
selectFilesForCommit,
|
|
18
|
+
askContinueCommits,
|
|
19
|
+
} from '../ui/index.ts';
|
|
20
|
+
import { chooseSplitMode } from '../ui/smart-split.ts';
|
|
21
|
+
import { handleSmartSplitMode } from './smart-split.ts';
|
|
22
|
+
import { initializeCache } from './cache.ts';
|
|
23
|
+
import type { CLIArgs } from '../utils/args.ts';
|
|
24
|
+
import type { Config } from '../config/index.ts';
|
|
25
|
+
|
|
26
|
+
export async function main(
|
|
27
|
+
args: CLIArgs = {
|
|
28
|
+
silent: false,
|
|
29
|
+
yes: false,
|
|
30
|
+
auto: false,
|
|
31
|
+
split: false,
|
|
32
|
+
smartSplit: false,
|
|
33
|
+
dryRun: false,
|
|
34
|
+
help: false,
|
|
35
|
+
version: false,
|
|
36
|
+
}
|
|
37
|
+
) {
|
|
38
|
+
if (!args.silent) {
|
|
39
|
+
log.info('🚀 Commit Wizard iniciado!');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Verificar se estamos em um repositório Git
|
|
43
|
+
if (!isGitRepository()) {
|
|
44
|
+
log.error('❌ Não foi encontrado um repositório Git neste diretório.');
|
|
45
|
+
if (!args.silent) {
|
|
46
|
+
log.info(
|
|
47
|
+
'💡 Execute o comando em um diretório com repositório Git inicializado.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Carregar e validar configuração
|
|
54
|
+
if (!args.silent) {
|
|
55
|
+
log.info('⚙️ Carregando configuração...');
|
|
56
|
+
}
|
|
57
|
+
const config = loadConfig();
|
|
58
|
+
|
|
59
|
+
// Inicializar cache global
|
|
60
|
+
initializeCache(config);
|
|
61
|
+
|
|
62
|
+
// Sobrescrever configuração com argumentos CLI
|
|
63
|
+
if (args.split) {
|
|
64
|
+
config.splitCommits = true;
|
|
65
|
+
}
|
|
66
|
+
if (args.dryRun) {
|
|
67
|
+
config.dryRun = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const configErrors = validateConfig(config);
|
|
71
|
+
|
|
72
|
+
if (configErrors.length > 0) {
|
|
73
|
+
log.error('❌ Erros na configuração:');
|
|
74
|
+
configErrors.forEach((error) => log.error(` • ${error}`));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!args.silent) {
|
|
79
|
+
log.success(
|
|
80
|
+
`✅ Configuração carregada (modelo: ${config.openai.model}, idioma: ${config.language})`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Verificar arquivos staged
|
|
85
|
+
if (!args.silent) {
|
|
86
|
+
log.info('📋 Verificando arquivos staged...');
|
|
87
|
+
}
|
|
88
|
+
const gitStatus = getGitStatus();
|
|
89
|
+
|
|
90
|
+
if (!gitStatus.hasStaged) {
|
|
91
|
+
log.warn('⚠️ Nenhum arquivo foi encontrado no stage.');
|
|
92
|
+
if (!args.silent) {
|
|
93
|
+
log.info(
|
|
94
|
+
'💡 Use `git add <arquivo>` para adicionar arquivos ao stage antes de gerar o commit.'
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const diffStats = getDiffStats();
|
|
101
|
+
if (!args.silent) {
|
|
102
|
+
log.success(
|
|
103
|
+
`✅ Encontrados ${gitStatus.stagedFiles.length} arquivo(s) staged:`
|
|
104
|
+
);
|
|
105
|
+
gitStatus.stagedFiles.forEach((file) => log.info(` 📄 ${file}`));
|
|
106
|
+
log.info(
|
|
107
|
+
`📊 Estatísticas: +${diffStats.added} -${diffStats.removed} linhas`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Modo Split: escolher entre smart split e split manual
|
|
112
|
+
if (config.splitCommits || args.smartSplit) {
|
|
113
|
+
if (args.yes) {
|
|
114
|
+
// Modo automático: usar smart split
|
|
115
|
+
return await handleSmartSplitMode(gitStatus, config, args);
|
|
116
|
+
} else {
|
|
117
|
+
// Modo interativo: perguntar qual tipo de split
|
|
118
|
+
const splitAction = await chooseSplitMode();
|
|
119
|
+
|
|
120
|
+
switch (splitAction.action) {
|
|
121
|
+
case 'proceed':
|
|
122
|
+
return await handleSmartSplitMode(gitStatus, config, args);
|
|
123
|
+
case 'manual':
|
|
124
|
+
return await handleSplitMode(gitStatus, config, args);
|
|
125
|
+
case 'cancel':
|
|
126
|
+
showCancellation();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Gerar mensagem de commit com OpenAI
|
|
133
|
+
if (!args.silent) {
|
|
134
|
+
log.info('🤖 Gerando mensagem de commit com IA...');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const result = await generateWithRetry(
|
|
138
|
+
gitStatus.diff,
|
|
139
|
+
config,
|
|
140
|
+
gitStatus.stagedFiles
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
if (!result.success) {
|
|
144
|
+
log.error(`❌ Erro ao gerar commit: ${result.error}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!result.suggestion) {
|
|
149
|
+
log.error('❌ Nenhuma sugestão foi gerada');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!args.silent) {
|
|
154
|
+
log.success('✨ Mensagem de commit gerada!');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Modo Dry Run: apenas mostrar mensagem
|
|
158
|
+
if (config.dryRun) {
|
|
159
|
+
log.info('🔍 Modo Dry Run - Mensagem gerada:');
|
|
160
|
+
log.info(`"${result.suggestion.message}"`);
|
|
161
|
+
log.info('💡 Execute sem --dry-run para fazer o commit');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Modo automático: commit direto
|
|
166
|
+
if (args.yes) {
|
|
167
|
+
const commitResult = executeCommit(result.suggestion.message);
|
|
168
|
+
showCommitResult(
|
|
169
|
+
commitResult.success,
|
|
170
|
+
commitResult.hash,
|
|
171
|
+
commitResult.error
|
|
172
|
+
);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Interface interativa
|
|
177
|
+
while (true) {
|
|
178
|
+
const uiAction = await showCommitPreview(result.suggestion);
|
|
179
|
+
|
|
180
|
+
switch (uiAction.action) {
|
|
181
|
+
case 'commit': {
|
|
182
|
+
// Commit direto com mensagem gerada
|
|
183
|
+
const commitResult = executeCommit(result.suggestion.message);
|
|
184
|
+
showCommitResult(
|
|
185
|
+
commitResult.success,
|
|
186
|
+
commitResult.hash,
|
|
187
|
+
commitResult.error
|
|
188
|
+
);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
case 'edit': {
|
|
192
|
+
// Editar mensagem
|
|
193
|
+
const editAction = await editCommitMessage(result.suggestion.message);
|
|
194
|
+
if (editAction.action === 'cancel') {
|
|
195
|
+
showCancellation();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (editAction.action === 'commit' && editAction.message) {
|
|
199
|
+
const editCommitResult = executeCommit(editAction.message);
|
|
200
|
+
showCommitResult(
|
|
201
|
+
editCommitResult.success,
|
|
202
|
+
editCommitResult.hash,
|
|
203
|
+
editCommitResult.error
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case 'copy': {
|
|
210
|
+
// Copiar para clipboard
|
|
211
|
+
await copyToClipboard(result.suggestion.message);
|
|
212
|
+
if (!args.silent) {
|
|
213
|
+
log.info(
|
|
214
|
+
'🎯 Você pode usar a mensagem copiada com: git commit -m "mensagem"'
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
case 'cancel': {
|
|
220
|
+
// Cancelar operação
|
|
221
|
+
showCancellation();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function handleSplitMode(gitStatus: any, config: any, args: CLIArgs) {
|
|
229
|
+
if (!args.silent) {
|
|
230
|
+
log.info('🔄 Modo Split ativado - Commits separados por arquivo');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let remainingFiles = [
|
|
234
|
+
...(gitStatus as { stagedFiles: string[] }).stagedFiles,
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
while (remainingFiles.length > 0) {
|
|
238
|
+
// Selecionar arquivos para este commit
|
|
239
|
+
const selectedFiles = args.yes
|
|
240
|
+
? [remainingFiles[0]] // Modo automático: um arquivo por vez
|
|
241
|
+
: await selectFilesForCommit(remainingFiles);
|
|
242
|
+
|
|
243
|
+
if (selectedFiles.length === 0) {
|
|
244
|
+
if (!args.silent) {
|
|
245
|
+
log.info('❌ Nenhum arquivo selecionado');
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Gerar diff apenas dos arquivos selecionados
|
|
251
|
+
const { getFileDiff } = await import('../git/index.ts');
|
|
252
|
+
const fileDiffs = selectedFiles
|
|
253
|
+
.filter((file): file is string => file !== undefined)
|
|
254
|
+
.map((file) => {
|
|
255
|
+
try {
|
|
256
|
+
return getFileDiff(file);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
log.error(
|
|
259
|
+
`❌ Erro ao obter diff do arquivo ${file}: ${error instanceof Error ? error.message : 'Erro desconhecido'}`
|
|
260
|
+
);
|
|
261
|
+
return '';
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
.filter((diff) => diff.length > 0)
|
|
265
|
+
.join('\n');
|
|
266
|
+
|
|
267
|
+
if (!fileDiffs) {
|
|
268
|
+
if (!args.silent) {
|
|
269
|
+
log.warn('⚠️ Nenhum diff encontrado para os arquivos selecionados');
|
|
270
|
+
}
|
|
271
|
+
remainingFiles = remainingFiles.filter(
|
|
272
|
+
(file) => !selectedFiles.includes(file)
|
|
273
|
+
);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!args.silent) {
|
|
278
|
+
log.info(`🤖 Gerando commit para: ${selectedFiles.join(', ')}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const result = await generateWithRetry(fileDiffs, config, selectedFiles.filter((file): file is string => file !== undefined));
|
|
282
|
+
|
|
283
|
+
if (!result.success) {
|
|
284
|
+
log.error(`❌ Erro ao gerar commit: ${result.error}`);
|
|
285
|
+
remainingFiles = remainingFiles.filter(
|
|
286
|
+
(file) => !selectedFiles.includes(file)
|
|
287
|
+
);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!result.suggestion) {
|
|
292
|
+
log.error('❌ Nenhuma sugestão foi gerada');
|
|
293
|
+
remainingFiles = remainingFiles.filter(
|
|
294
|
+
(file) => !selectedFiles.includes(file)
|
|
295
|
+
);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Modo Dry Run: apenas mostrar mensagem
|
|
300
|
+
if ((config as Config).dryRun) {
|
|
301
|
+
log.info(`🔍 Dry Run - Mensagem para ${selectedFiles.join(', ')}:`);
|
|
302
|
+
log.info(`"${result.suggestion.message}"`);
|
|
303
|
+
remainingFiles = remainingFiles.filter(
|
|
304
|
+
(file) => !selectedFiles.includes(file)
|
|
305
|
+
);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Modo automático: commit direto
|
|
310
|
+
if (args.yes) {
|
|
311
|
+
// Para múltiplos arquivos, usar commit normal
|
|
312
|
+
// Para arquivo único, usar executeFileCommit
|
|
313
|
+
const commitResult =
|
|
314
|
+
selectedFiles.length === 1 && selectedFiles[0]
|
|
315
|
+
? await executeFileCommit(selectedFiles[0], result.suggestion.message)
|
|
316
|
+
: await executeCommit(result.suggestion.message);
|
|
317
|
+
|
|
318
|
+
showCommitResult(
|
|
319
|
+
commitResult.success,
|
|
320
|
+
commitResult.hash,
|
|
321
|
+
commitResult.error
|
|
322
|
+
);
|
|
323
|
+
} else {
|
|
324
|
+
// Interface interativa para este commit
|
|
325
|
+
const uiAction = await showCommitPreview(result.suggestion);
|
|
326
|
+
|
|
327
|
+
if (uiAction.action === 'commit') {
|
|
328
|
+
const commitResult =
|
|
329
|
+
selectedFiles.length === 1 && selectedFiles[0]
|
|
330
|
+
? await executeFileCommit(
|
|
331
|
+
selectedFiles[0],
|
|
332
|
+
result.suggestion.message
|
|
333
|
+
)
|
|
334
|
+
: await executeCommit(result.suggestion.message);
|
|
335
|
+
showCommitResult(
|
|
336
|
+
commitResult.success,
|
|
337
|
+
commitResult.hash,
|
|
338
|
+
commitResult.error
|
|
339
|
+
);
|
|
340
|
+
} else if (uiAction.action === 'edit') {
|
|
341
|
+
const editAction = await editCommitMessage(result.suggestion.message);
|
|
342
|
+
if (editAction.action === 'commit' && editAction.message) {
|
|
343
|
+
const commitResult =
|
|
344
|
+
selectedFiles.length === 1 && selectedFiles[0]
|
|
345
|
+
? await executeFileCommit(selectedFiles[0], editAction.message)
|
|
346
|
+
: await executeCommit(editAction.message);
|
|
347
|
+
showCommitResult(
|
|
348
|
+
commitResult.success,
|
|
349
|
+
commitResult.hash,
|
|
350
|
+
commitResult.error
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
} else if (uiAction.action === 'copy') {
|
|
354
|
+
await copyToClipboard(result.suggestion.message);
|
|
355
|
+
if (!args.silent) {
|
|
356
|
+
log.info('🎯 Mensagem copiada para clipboard');
|
|
357
|
+
}
|
|
358
|
+
} else if (uiAction.action === 'cancel') {
|
|
359
|
+
showCancellation();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Remover arquivos processados
|
|
365
|
+
remainingFiles = remainingFiles.filter(
|
|
366
|
+
(file) => !selectedFiles.includes(file)
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// Perguntar se quer continuar (exceto em modo automático)
|
|
370
|
+
if (remainingFiles.length > 0 && !args.yes) {
|
|
371
|
+
const continueCommits = await askContinueCommits(remainingFiles);
|
|
372
|
+
if (!continueCommits) {
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!args.silent) {
|
|
379
|
+
log.success('✅ Modo Split concluído!');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import type { Config } from '../config/index.ts';
|
|
2
|
+
|
|
3
|
+
export interface CommitSuggestion {
|
|
4
|
+
message: string;
|
|
5
|
+
type:
|
|
6
|
+
| 'feat'
|
|
7
|
+
| 'fix'
|
|
8
|
+
| 'docs'
|
|
9
|
+
| 'style'
|
|
10
|
+
| 'refactor'
|
|
11
|
+
| 'test'
|
|
12
|
+
| 'chore'
|
|
13
|
+
| 'build'
|
|
14
|
+
| 'ci';
|
|
15
|
+
confidence: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OpenAIResponse {
|
|
19
|
+
success: boolean;
|
|
20
|
+
suggestion?: CommitSuggestion;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Constrói o prompt para a OpenAI baseado no diff e configurações (otimizado)
|
|
26
|
+
*/
|
|
27
|
+
export function buildPrompt(
|
|
28
|
+
diff: string,
|
|
29
|
+
config: Config,
|
|
30
|
+
filenames: string[]
|
|
31
|
+
): string {
|
|
32
|
+
const language = config.language === 'pt' ? 'português' : 'english';
|
|
33
|
+
const styleInstructions = getStyleInstructions(
|
|
34
|
+
config.commitStyle,
|
|
35
|
+
config.language
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Limitar tamanho do diff para economizar tokens
|
|
39
|
+
const maxDiffLength = 6000;
|
|
40
|
+
const truncatedDiff = diff.length > maxDiffLength
|
|
41
|
+
? diff.substring(0, maxDiffLength) + '\n... (diff truncado)'
|
|
42
|
+
: diff;
|
|
43
|
+
|
|
44
|
+
// Simplificar lista de arquivos se houver muitos
|
|
45
|
+
const fileList = filenames.length > 10
|
|
46
|
+
? `${filenames.length} arquivos: ${filenames.slice(0, 5).join(', ')}...`
|
|
47
|
+
: filenames.join(', ');
|
|
48
|
+
|
|
49
|
+
const prompt = `Gere mensagem de commit em ${language} (${config.commitStyle}).
|
|
50
|
+
|
|
51
|
+
Arquivos: ${fileList}
|
|
52
|
+
|
|
53
|
+
${styleInstructions}
|
|
54
|
+
|
|
55
|
+
Diff:
|
|
56
|
+
\`\`\`
|
|
57
|
+
${truncatedDiff}
|
|
58
|
+
\`\`\`
|
|
59
|
+
|
|
60
|
+
Mensagem:`;
|
|
61
|
+
|
|
62
|
+
return prompt;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Obtém instruções específicas baseadas no estilo de commit
|
|
67
|
+
*/
|
|
68
|
+
function getStyleInstructions(style: string, language: string): string {
|
|
69
|
+
const instructions = {
|
|
70
|
+
pt: {
|
|
71
|
+
conventional: `- Use formato: tipo(escopo): descrição
|
|
72
|
+
- Tipos válidos: feat, fix, docs, style, refactor, test, chore, build, ci
|
|
73
|
+
- Exemplo: "feat(auth): adicionar validação de email"
|
|
74
|
+
- Mantenha a primeira linha com até 50 caracteres`,
|
|
75
|
+
|
|
76
|
+
simple: `- Use formato simples e direto
|
|
77
|
+
- Comece com verbo no infinitivo
|
|
78
|
+
- Exemplo: "corrigir validação de formulário"
|
|
79
|
+
- Máximo 50 caracteres`,
|
|
80
|
+
|
|
81
|
+
detailed: `- Primeira linha: resumo em até 50 caracteres
|
|
82
|
+
- Se necessário, adicione corpo explicativo
|
|
83
|
+
- Use presente do indicativo
|
|
84
|
+
- Seja descritivo mas conciso`,
|
|
85
|
+
},
|
|
86
|
+
en: {
|
|
87
|
+
conventional: `- Use format: type(scope): description
|
|
88
|
+
- Valid types: feat, fix, docs, style, refactor, test, chore, build, ci
|
|
89
|
+
- Example: "feat(auth): add email validation"
|
|
90
|
+
- Keep first line under 50 characters`,
|
|
91
|
+
|
|
92
|
+
simple: `- Use simple and direct format
|
|
93
|
+
- Start with imperative verb
|
|
94
|
+
- Example: "fix form validation"
|
|
95
|
+
- Maximum 50 characters`,
|
|
96
|
+
|
|
97
|
+
detailed: `- First line: summary under 50 characters
|
|
98
|
+
- Add explanatory body if needed
|
|
99
|
+
- Use imperative mood
|
|
100
|
+
- Be descriptive but concise`,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const lang = language === 'pt' ? 'pt' : 'en';
|
|
105
|
+
return (
|
|
106
|
+
instructions[lang][style as keyof typeof instructions.pt] ||
|
|
107
|
+
instructions[lang].conventional
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extrai o tipo de commit da mensagem gerada pela OpenAI
|
|
113
|
+
*/
|
|
114
|
+
export function extractCommitTypeFromMessage(
|
|
115
|
+
message: string
|
|
116
|
+
): CommitSuggestion['type'] | null {
|
|
117
|
+
// Padrões para detectar tipos de commit
|
|
118
|
+
const typePatterns = {
|
|
119
|
+
feat: /^(feat|feature)(\([^)]+\))?:/i,
|
|
120
|
+
fix: /^(fix|bugfix)(\([^)]+\))?:/i,
|
|
121
|
+
docs: /^(docs|documentation)(\([^)]+\))?:/i,
|
|
122
|
+
style: /^(style|format)(\([^)]+\))?:/i,
|
|
123
|
+
refactor: /^(refactor|refactoring)(\([^)]+\))?:/i,
|
|
124
|
+
test: /^(test|testing)(\([^)]+\))?:/i,
|
|
125
|
+
chore: /^(chore|maintenance)(\([^)]+\))?:/i,
|
|
126
|
+
build: /^(build|ci)(\([^)]+\))?:/i,
|
|
127
|
+
ci: /^(ci|continuous-integration)(\([^)]+\))?:/i,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
for (const [type, pattern] of Object.entries(typePatterns)) {
|
|
131
|
+
if (pattern.test(message)) {
|
|
132
|
+
return type as CommitSuggestion['type'];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Detecta o tipo de commit baseado no diff
|
|
141
|
+
*/
|
|
142
|
+
export function detectCommitType(
|
|
143
|
+
diff: string,
|
|
144
|
+
filenames: string[]
|
|
145
|
+
): CommitSuggestion['type'] {
|
|
146
|
+
const diffLower = diff.toLowerCase();
|
|
147
|
+
const filesStr = filenames.join(' ').toLowerCase();
|
|
148
|
+
|
|
149
|
+
// Testes
|
|
150
|
+
if (
|
|
151
|
+
filesStr.includes('test') ||
|
|
152
|
+
filesStr.includes('spec') ||
|
|
153
|
+
diffLower.includes('test(')
|
|
154
|
+
) {
|
|
155
|
+
return 'test';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Documentação
|
|
159
|
+
if (
|
|
160
|
+
filesStr.includes('readme') ||
|
|
161
|
+
filesStr.includes('.md') ||
|
|
162
|
+
filesStr.includes('docs')
|
|
163
|
+
) {
|
|
164
|
+
return 'docs';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Build/CI
|
|
168
|
+
if (
|
|
169
|
+
filesStr.includes('package.json') ||
|
|
170
|
+
filesStr.includes('dockerfile') ||
|
|
171
|
+
filesStr.includes('.yml') ||
|
|
172
|
+
filesStr.includes('.yaml') ||
|
|
173
|
+
filesStr.includes('webpack') ||
|
|
174
|
+
filesStr.includes('tsconfig')
|
|
175
|
+
) {
|
|
176
|
+
return 'build';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Styles
|
|
180
|
+
if (
|
|
181
|
+
filesStr.includes('.css') ||
|
|
182
|
+
filesStr.includes('.scss') ||
|
|
183
|
+
diffLower.includes('style') ||
|
|
184
|
+
diffLower.includes('format')
|
|
185
|
+
) {
|
|
186
|
+
return 'style';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Fixes
|
|
190
|
+
if (
|
|
191
|
+
diffLower.includes('fix') ||
|
|
192
|
+
diffLower.includes('bug') ||
|
|
193
|
+
diffLower.includes('error') ||
|
|
194
|
+
diffLower.includes('issue')
|
|
195
|
+
) {
|
|
196
|
+
return 'fix';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Features (padrão para novas funcionalidades)
|
|
200
|
+
if (
|
|
201
|
+
diffLower.includes('add') ||
|
|
202
|
+
diffLower.includes('new') ||
|
|
203
|
+
diffLower.includes('create') ||
|
|
204
|
+
diffLower.includes('implement')
|
|
205
|
+
) {
|
|
206
|
+
return 'feat';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Refactor
|
|
210
|
+
if (
|
|
211
|
+
diffLower.includes('refactor') ||
|
|
212
|
+
diffLower.includes('restructure') ||
|
|
213
|
+
diffLower.includes('rename')
|
|
214
|
+
) {
|
|
215
|
+
return 'refactor';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Default
|
|
219
|
+
return 'chore';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Consome a API da OpenAI para gerar mensagem de commit
|
|
224
|
+
*/
|
|
225
|
+
export async function generateCommitMessage(
|
|
226
|
+
diff: string,
|
|
227
|
+
config: Config,
|
|
228
|
+
filenames: string[]
|
|
229
|
+
): Promise<OpenAIResponse> {
|
|
230
|
+
try {
|
|
231
|
+
if (!config.openai.apiKey) {
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
error:
|
|
235
|
+
'Chave da OpenAI não encontrada. Configure OPENAI_API_KEY nas variáveis de ambiente.',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const prompt = buildPrompt(diff, config, filenames);
|
|
240
|
+
|
|
241
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: {
|
|
244
|
+
Authorization: `Bearer ${config.openai.apiKey}`,
|
|
245
|
+
'Content-Type': 'application/json',
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify({
|
|
248
|
+
model: config.openai.model,
|
|
249
|
+
messages: [
|
|
250
|
+
{
|
|
251
|
+
role: 'user',
|
|
252
|
+
content: prompt,
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
max_tokens: Math.min(config.openai.maxTokens, 150), // Limitar para economizar tokens
|
|
256
|
+
temperature: config.openai.temperature,
|
|
257
|
+
}),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
const errorData = (await response.json().catch(() => ({}))) as any;
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
error: `Erro da OpenAI (${response.status}): ${errorData.error?.message || 'Erro desconhecido'}`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const data = (await response.json()) as any;
|
|
269
|
+
let message = data.choices?.[0]?.message?.content?.trim();
|
|
270
|
+
|
|
271
|
+
if (!message) {
|
|
272
|
+
return {
|
|
273
|
+
success: false,
|
|
274
|
+
error: 'OpenAI retornou resposta vazia',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Remover backticks se presentes
|
|
279
|
+
message = message.replace(/^```\s*/, '').replace(/\s*```$/, '');
|
|
280
|
+
|
|
281
|
+
// Remover quebras de linha extras
|
|
282
|
+
message = message.trim();
|
|
283
|
+
|
|
284
|
+
// Extrair tipo da mensagem gerada pela OpenAI
|
|
285
|
+
const extractedType = extractCommitTypeFromMessage(message);
|
|
286
|
+
const fallbackType = detectCommitType(diff, filenames);
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
success: true,
|
|
290
|
+
suggestion: {
|
|
291
|
+
message,
|
|
292
|
+
type: extractedType || fallbackType,
|
|
293
|
+
confidence: 0.8, // Placeholder - pode ser melhorado
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return {
|
|
298
|
+
success: false,
|
|
299
|
+
error: `Erro ao conectar com OpenAI: ${error instanceof Error ? error.message : 'Erro desconhecido'}`,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Gera mensagem com retry em caso de falha
|
|
306
|
+
*/
|
|
307
|
+
export async function generateWithRetry(
|
|
308
|
+
diff: string,
|
|
309
|
+
config: Config,
|
|
310
|
+
filenames: string[],
|
|
311
|
+
maxRetries: number = 3
|
|
312
|
+
): Promise<OpenAIResponse> {
|
|
313
|
+
let lastError = '';
|
|
314
|
+
|
|
315
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
316
|
+
const result = await generateCommitMessage(diff, config, filenames);
|
|
317
|
+
|
|
318
|
+
if (result.success) {
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
lastError = result.error || 'Erro desconhecido';
|
|
323
|
+
|
|
324
|
+
// Aguardar antes de tentar novamente (exponential backoff)
|
|
325
|
+
if (i < maxRetries - 1) {
|
|
326
|
+
await new Promise((resolve) =>
|
|
327
|
+
setTimeout(resolve, Math.pow(2, i) * 1000)
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
success: false,
|
|
334
|
+
error: `Falha após ${maxRetries} tentativas. Último erro: ${lastError}`,
|
|
335
|
+
};
|
|
336
|
+
}
|