@gilbert_oliveira/commit-wizard 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/dist/commit-wizard.js +51 -51
- package/package.json +2 -2
- package/src/core/index.ts +5 -1
- package/src/core/openai.ts +29 -11
- package/src/core/smart-split.ts +59 -35
- package/src/git/index.ts +16 -4
- package/src/utils/polyfill.ts +16 -12
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"displayName": "Commit Wizard",
|
|
4
4
|
"publisher": "gilbert-oliveira",
|
|
5
5
|
"description": "CLI inteligente para gerar mensagens de commit usando OpenAI",
|
|
6
|
-
"version": "2.0.
|
|
6
|
+
"version": "2.0.3",
|
|
7
7
|
"categories": [
|
|
8
8
|
"Other",
|
|
9
9
|
"SCM Providers"
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
"test:watch": "bun test --watch",
|
|
22
22
|
"test:coverage": "bun test && node scripts/generate-lcov.js",
|
|
23
23
|
"test:coverage:report": "c8 --reporter=html --reporter=text --include='src/**/*.ts' --exclude='**/*.test.ts' bun test && open coverage/lcov-report/index.html",
|
|
24
|
-
"test:coverage:upload": "bun test && node scripts/generate-lcov.js && node scripts/upload-codecov.js",
|
|
25
24
|
"prepublishOnly": "bun run build && bun test",
|
|
26
25
|
"start": "bun run bin/commit-wizard.ts",
|
|
27
26
|
"ci:test": "bun test --reporter=verbose",
|
|
@@ -32,6 +31,7 @@
|
|
|
32
31
|
"release:patch": "./scripts/release.sh patch",
|
|
33
32
|
"release:minor": "./scripts/release.sh minor",
|
|
34
33
|
"release:major": "./scripts/release.sh major",
|
|
34
|
+
"check-changes": "node -e \"const { execSync } = require('child_process'); const changed = execSync('git diff --name-only HEAD~1 HEAD', { encoding: 'utf8' }).includes('package.json') || execSync('git diff --name-only HEAD~1 HEAD', { encoding: 'utf8' }).match(/(src|bin|scripts|dist)/\\.*/); console.log(changed ? '🚀 Mudanças detectadas - deploy recomendado!' : '⚠️ Nenhuma mudança significativa detectada'); process.exit(changed ? 0 : 1);\"",
|
|
35
35
|
"type-check": "bun run tsc --noEmit",
|
|
36
36
|
"lint": "bun run eslint --fix",
|
|
37
37
|
"format": "bun run prettier --write ."
|
package/src/core/index.ts
CHANGED
|
@@ -278,7 +278,11 @@ async function handleSplitMode(gitStatus: any, config: any, args: CLIArgs) {
|
|
|
278
278
|
log.info(`🤖 Gerando commit para: ${selectedFiles.join(', ')}`);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
const result = await generateWithRetry(
|
|
281
|
+
const result = await generateWithRetry(
|
|
282
|
+
fileDiffs,
|
|
283
|
+
config,
|
|
284
|
+
selectedFiles.filter((file): file is string => file !== undefined)
|
|
285
|
+
);
|
|
282
286
|
|
|
283
287
|
if (!result.success) {
|
|
284
288
|
log.error(`❌ Erro ao gerar commit: ${result.error}`);
|
package/src/core/openai.ts
CHANGED
|
@@ -37,14 +37,16 @@ export function buildPrompt(
|
|
|
37
37
|
|
|
38
38
|
// Limitar tamanho do diff para economizar tokens
|
|
39
39
|
const maxDiffLength = 6000;
|
|
40
|
-
const truncatedDiff =
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const truncatedDiff =
|
|
41
|
+
diff.length > maxDiffLength
|
|
42
|
+
? diff.substring(0, maxDiffLength) + '\n... (diff truncado)'
|
|
43
|
+
: diff;
|
|
43
44
|
|
|
44
45
|
// Simplificar lista de arquivos se houver muitos
|
|
45
|
-
const fileList =
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const fileList =
|
|
47
|
+
filenames.length > 10
|
|
48
|
+
? `${filenames.length} arquivos: ${filenames.slice(0, 5).join(', ')}...`
|
|
49
|
+
: filenames.join(', ');
|
|
48
50
|
|
|
49
51
|
const prompt = `Gere mensagem de commit em ${language} (${config.commitStyle}).
|
|
50
52
|
|
|
@@ -219,6 +221,25 @@ export function detectCommitType(
|
|
|
219
221
|
return 'chore';
|
|
220
222
|
}
|
|
221
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Processa a mensagem retornada pela OpenAI removendo formatação desnecessária
|
|
226
|
+
*/
|
|
227
|
+
export function processOpenAIMessage(message: string): string {
|
|
228
|
+
// Remover backticks de código APENAS se a mensagem inteira está envolvida em backticks
|
|
229
|
+
// Isso preserva nomes de funções, métodos e variáveis dentro da mensagem
|
|
230
|
+
if (message.match(/^```[\s\S]*```$/)) {
|
|
231
|
+
// Se a mensagem inteira está em um bloco de código, remover as marcações
|
|
232
|
+
message = message
|
|
233
|
+
.replace(/^```(?:plaintext|javascript|typescript|python|java|html|css|json|xml|yaml|yml|bash|shell|text)?\s*/, '')
|
|
234
|
+
.replace(/\s*```$/, '');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Remover quebras de linha extras do início e fim
|
|
238
|
+
message = message.trim();
|
|
239
|
+
|
|
240
|
+
return message;
|
|
241
|
+
}
|
|
242
|
+
|
|
222
243
|
/**
|
|
223
244
|
* Consome a API da OpenAI para gerar mensagem de commit
|
|
224
245
|
*/
|
|
@@ -275,11 +296,8 @@ export async function generateCommitMessage(
|
|
|
275
296
|
};
|
|
276
297
|
}
|
|
277
298
|
|
|
278
|
-
//
|
|
279
|
-
message = message
|
|
280
|
-
|
|
281
|
-
// Remover quebras de linha extras
|
|
282
|
-
message = message.trim();
|
|
299
|
+
// Processar mensagem para remover formatação
|
|
300
|
+
message = processOpenAIMessage(message);
|
|
283
301
|
|
|
284
302
|
// Extrair tipo da mensagem gerada pela OpenAI
|
|
285
303
|
const extractedType = extractCommitTypeFromMessage(message);
|
package/src/core/smart-split.ts
CHANGED
|
@@ -28,17 +28,21 @@ function buildContextAnalysisPrompt(
|
|
|
28
28
|
): string {
|
|
29
29
|
// Limitar o tamanho do diff para evitar exceder tokens
|
|
30
30
|
const maxDiffLength = 8000; // Limite conservador
|
|
31
|
-
const truncatedDiff =
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const truncatedDiff =
|
|
32
|
+
overallDiff.length > maxDiffLength
|
|
33
|
+
? overallDiff.substring(0, maxDiffLength) + '\n... (diff truncado)'
|
|
34
|
+
: overallDiff;
|
|
34
35
|
|
|
35
36
|
// Calcular estatísticas básicas
|
|
36
37
|
const totalFiles = files.length;
|
|
37
|
-
const fileTypes = files.reduce(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
const fileTypes = files.reduce(
|
|
39
|
+
(acc, file) => {
|
|
40
|
+
const ext = file.split('.').pop() || 'sem-extensao';
|
|
41
|
+
acc[ext] = (acc[ext] || 0) + 1;
|
|
42
|
+
return acc;
|
|
43
|
+
},
|
|
44
|
+
{} as Record<string, number>
|
|
45
|
+
);
|
|
42
46
|
|
|
43
47
|
const fileStats = Object.entries(fileTypes)
|
|
44
48
|
.map(([ext, count]) => `${ext}: ${count}`)
|
|
@@ -73,12 +77,15 @@ Agrupe arquivos relacionados. Máximo 5 grupos. Responda em JSON:
|
|
|
73
77
|
*/
|
|
74
78
|
function buildFallbackPrompt(files: string[]): string {
|
|
75
79
|
// Agrupar arquivos por diretório
|
|
76
|
-
const filesByDir = files.reduce(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
const filesByDir = files.reduce(
|
|
81
|
+
(acc, file) => {
|
|
82
|
+
const dir = file.split('/').slice(0, -1).join('/') || 'root';
|
|
83
|
+
if (!acc[dir]) acc[dir] = [];
|
|
84
|
+
acc[dir].push(file);
|
|
85
|
+
return acc;
|
|
86
|
+
},
|
|
87
|
+
{} as Record<string, string[]>
|
|
88
|
+
);
|
|
82
89
|
|
|
83
90
|
const dirStats = Object.entries(filesByDir)
|
|
84
91
|
.map(([dir, files]) => `${dir}: ${files.length} arquivo(s)`)
|
|
@@ -133,14 +140,16 @@ export async function analyzeFileContext(
|
|
|
133
140
|
// Decidir qual prompt usar baseado no tamanho
|
|
134
141
|
const maxDiffLength = 6000; // Limite mais conservador
|
|
135
142
|
const useFallback = overallDiff.length > maxDiffLength;
|
|
136
|
-
|
|
137
|
-
const prompt = useFallback
|
|
143
|
+
|
|
144
|
+
const prompt = useFallback
|
|
138
145
|
? buildFallbackPrompt(files)
|
|
139
146
|
: buildContextAnalysisPrompt(files, overallDiff);
|
|
140
147
|
|
|
141
148
|
// Log opcional sobre o uso do fallback
|
|
142
149
|
if (useFallback) {
|
|
143
|
-
console.warn(
|
|
150
|
+
console.warn(
|
|
151
|
+
`⚠️ Diff muito grande (${overallDiff.length} chars), usando análise baseada em nomes de arquivos`
|
|
152
|
+
);
|
|
144
153
|
}
|
|
145
154
|
|
|
146
155
|
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
@@ -250,7 +259,7 @@ export async function generateGroupDiff(group: FileGroup): Promise<string> {
|
|
|
250
259
|
const diff = getFileDiff(file);
|
|
251
260
|
// Limitar tamanho do diff individual
|
|
252
261
|
const maxDiffLength = 4000;
|
|
253
|
-
return diff.length > maxDiffLength
|
|
262
|
+
return diff.length > maxDiffLength
|
|
254
263
|
? diff.substring(0, maxDiffLength) + '\n... (diff truncado)'
|
|
255
264
|
: diff;
|
|
256
265
|
} catch {
|
|
@@ -294,10 +303,12 @@ export async function generateGroupDiff(group: FileGroup): Promise<string> {
|
|
|
294
303
|
});
|
|
295
304
|
// Limitar conteúdo do arquivo novo
|
|
296
305
|
const maxContentLength = 2000;
|
|
297
|
-
const truncatedContent =
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
306
|
+
const truncatedContent =
|
|
307
|
+
content.length > maxContentLength
|
|
308
|
+
? content.substring(0, maxContentLength) +
|
|
309
|
+
'\n... (conteúdo truncado)'
|
|
310
|
+
: content;
|
|
311
|
+
|
|
301
312
|
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
313
|
.split('\n')
|
|
303
314
|
.map((line) => `+${line}`)
|
|
@@ -341,10 +352,12 @@ export async function generateGroupDiff(group: FileGroup): Promise<string> {
|
|
|
341
352
|
});
|
|
342
353
|
// Limitar conteúdo do arquivo recriado
|
|
343
354
|
const maxContentLength = 2000;
|
|
344
|
-
const truncatedContent =
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
355
|
+
const truncatedContent =
|
|
356
|
+
content.length > maxContentLength
|
|
357
|
+
? content.substring(0, maxContentLength) +
|
|
358
|
+
'\n... (conteúdo truncado)'
|
|
359
|
+
: content;
|
|
360
|
+
|
|
348
361
|
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
362
|
.split('\n')
|
|
350
363
|
.map((line) => `+${line}`)
|
|
@@ -361,8 +374,8 @@ export async function generateGroupDiff(group: FileGroup): Promise<string> {
|
|
|
361
374
|
// Limitar tamanho total do diff combinado
|
|
362
375
|
const combinedDiff = diffs.join('\n');
|
|
363
376
|
const maxTotalLength = 8000;
|
|
364
|
-
|
|
365
|
-
return combinedDiff.length > maxTotalLength
|
|
377
|
+
|
|
378
|
+
return combinedDiff.length > maxTotalLength
|
|
366
379
|
? combinedDiff.substring(0, maxTotalLength) + '\n... (diff total truncado)'
|
|
367
380
|
: combinedDiff;
|
|
368
381
|
}
|
|
@@ -478,7 +491,9 @@ export async function handleSmartSplitMode(
|
|
|
478
491
|
|
|
479
492
|
if (!result.success) {
|
|
480
493
|
if (!args.silent) {
|
|
481
|
-
log.error(
|
|
494
|
+
log.error(
|
|
495
|
+
`❌ Erro ao gerar commit para ${group.name}: ${result.error}`
|
|
496
|
+
);
|
|
482
497
|
}
|
|
483
498
|
continue;
|
|
484
499
|
}
|
|
@@ -515,11 +530,14 @@ export async function handleSmartSplitMode(
|
|
|
515
530
|
} else {
|
|
516
531
|
// Para múltiplos arquivos, usar commit normal mas com apenas os arquivos do grupo
|
|
517
532
|
const { execSync } = await import('child_process');
|
|
533
|
+
// Importar função de escape do módulo git
|
|
534
|
+
const { escapeShellArg } = await import('../git/index.ts');
|
|
518
535
|
try {
|
|
519
536
|
// Fazer commit apenas dos arquivos do grupo
|
|
520
|
-
const filesArg = group.files.map((f) =>
|
|
537
|
+
const filesArg = group.files.map((f) => escapeShellArg(f)).join(' ');
|
|
538
|
+
const escapedMessage = escapeShellArg(result.suggestion.message || '');
|
|
521
539
|
execSync(
|
|
522
|
-
`git commit ${filesArg} -m
|
|
540
|
+
`git commit ${filesArg} -m ${escapedMessage}`,
|
|
523
541
|
{
|
|
524
542
|
stdio: 'pipe',
|
|
525
543
|
}
|
|
@@ -575,11 +593,14 @@ export async function handleSmartSplitMode(
|
|
|
575
593
|
} else {
|
|
576
594
|
// Para múltiplos arquivos, usar commit normal mas com apenas os arquivos do grupo
|
|
577
595
|
const { execSync } = await import('child_process');
|
|
596
|
+
// Importar função de escape do módulo git
|
|
597
|
+
const { escapeShellArg } = await import('../git/index.ts');
|
|
578
598
|
try {
|
|
579
599
|
// Fazer commit apenas dos arquivos do grupo
|
|
580
|
-
const filesArg = group.files.map((f) =>
|
|
600
|
+
const filesArg = group.files.map((f) => escapeShellArg(f)).join(' ');
|
|
601
|
+
const escapedMessage = escapeShellArg(commitMessage);
|
|
581
602
|
execSync(
|
|
582
|
-
`git commit ${filesArg} -m
|
|
603
|
+
`git commit ${filesArg} -m ${escapedMessage}`,
|
|
583
604
|
{
|
|
584
605
|
stdio: 'pipe',
|
|
585
606
|
}
|
|
@@ -624,11 +645,14 @@ export async function handleSmartSplitMode(
|
|
|
624
645
|
} else {
|
|
625
646
|
// Para múltiplos arquivos, usar commit normal mas com apenas os arquivos do grupo
|
|
626
647
|
const { execSync } = await import('child_process');
|
|
648
|
+
// Importar função de escape do módulo git
|
|
649
|
+
const { escapeShellArg } = await import('../git/index.ts');
|
|
627
650
|
try {
|
|
628
651
|
// Fazer commit apenas dos arquivos do grupo
|
|
629
|
-
const filesArg = group.files.map((f) =>
|
|
652
|
+
const filesArg = group.files.map((f) => escapeShellArg(f)).join(' ');
|
|
653
|
+
const escapedMessage = escapeShellArg(editAction.message || '');
|
|
630
654
|
execSync(
|
|
631
|
-
`git commit ${filesArg} -m
|
|
655
|
+
`git commit ${filesArg} -m ${escapedMessage}`,
|
|
632
656
|
{
|
|
633
657
|
stdio: 'pipe',
|
|
634
658
|
}
|
package/src/git/index.ts
CHANGED
|
@@ -13,6 +13,15 @@ export interface GitCommitResult {
|
|
|
13
13
|
error?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Escapa caracteres especiais para uso seguro em comandos shell
|
|
18
|
+
*/
|
|
19
|
+
export function escapeShellArg(arg: string): string {
|
|
20
|
+
// Para mensagens de commit, usamos aspas simples que são mais seguras
|
|
21
|
+
// Escapamos apenas aspas simples dentro da string
|
|
22
|
+
return `'${arg.replace(/'/g, "'\"'\"'")}'`;
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
/**
|
|
17
26
|
* Verifica se estamos em um repositório Git
|
|
18
27
|
*/
|
|
@@ -80,8 +89,9 @@ export function getFileDiff(filename: string): string {
|
|
|
80
89
|
*/
|
|
81
90
|
export function executeCommit(message: string): GitCommitResult {
|
|
82
91
|
try {
|
|
83
|
-
//
|
|
84
|
-
|
|
92
|
+
// Usar aspas simples para evitar problemas com caracteres especiais
|
|
93
|
+
const escapedMessage = escapeShellArg(message);
|
|
94
|
+
execSync(`git commit -m ${escapedMessage}`, {
|
|
85
95
|
stdio: 'pipe',
|
|
86
96
|
});
|
|
87
97
|
|
|
@@ -115,8 +125,10 @@ export function executeFileCommit(
|
|
|
115
125
|
message: string
|
|
116
126
|
): GitCommitResult {
|
|
117
127
|
try {
|
|
118
|
-
//
|
|
119
|
-
|
|
128
|
+
// Usar aspas simples para evitar problemas com caracteres especiais
|
|
129
|
+
const escapedMessage = escapeShellArg(message);
|
|
130
|
+
const escapedFilename = escapeShellArg(filename);
|
|
131
|
+
execSync(`git commit ${escapedFilename} -m ${escapedMessage}`, {
|
|
120
132
|
stdio: 'pipe',
|
|
121
133
|
});
|
|
122
134
|
|
package/src/utils/polyfill.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Polyfill para stripVTControlCharacters para compatibilidade com Bun/Node.js
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Este polyfill resolve problemas de compatibilidade onde o Bun não mapeia
|
|
5
5
|
* corretamente a função stripVTControlCharacters do módulo util do Node.js.
|
|
6
6
|
* A função foi adicionada ao Node.js v16.14.0+ mas pode não estar disponível
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/**
|
|
11
11
|
* Remove caracteres de controle VT de uma string
|
|
12
12
|
* Implementação baseada na função nativa do Node.js
|
|
13
|
-
*
|
|
13
|
+
*
|
|
14
14
|
* @param str - String da qual remover os caracteres de controle
|
|
15
15
|
* @returns String limpa sem caracteres de controle VT/ANSI
|
|
16
16
|
*/
|
|
@@ -23,19 +23,22 @@ function stripVTControlCharacters(str: string): string {
|
|
|
23
23
|
// Baseada na implementação oficial do Node.js
|
|
24
24
|
const esc = String.fromCharCode(27); // ESC character (\u001B)
|
|
25
25
|
const csi = String.fromCharCode(155); // CSI character (\u009B)
|
|
26
|
-
const ansiRegex = new RegExp(
|
|
27
|
-
|
|
26
|
+
const ansiRegex = new RegExp(
|
|
27
|
+
`[${esc}${csi}][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]`,
|
|
28
|
+
'g'
|
|
29
|
+
);
|
|
30
|
+
|
|
28
31
|
return str.replace(ansiRegex, '');
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
// Interceptar require/import do módulo util antes de qualquer outra coisa
|
|
32
|
-
|
|
35
|
+
|
|
33
36
|
const Module = require('module');
|
|
34
37
|
const originalRequire = Module.prototype.require;
|
|
35
38
|
|
|
36
|
-
Module.prototype.require = function(id: string) {
|
|
39
|
+
Module.prototype.require = function (id: string) {
|
|
37
40
|
const result = originalRequire.apply(this, arguments);
|
|
38
|
-
|
|
41
|
+
|
|
39
42
|
// Se estiver importando o módulo util e stripVTControlCharacters não existe, adicionar
|
|
40
43
|
if (id === 'util' && result && !result.stripVTControlCharacters) {
|
|
41
44
|
result.stripVTControlCharacters = stripVTControlCharacters;
|
|
@@ -47,11 +50,10 @@ Module.prototype.require = function(id: string) {
|
|
|
47
50
|
configurable: false,
|
|
48
51
|
});
|
|
49
52
|
}
|
|
50
|
-
|
|
53
|
+
|
|
51
54
|
return result;
|
|
52
55
|
};
|
|
53
56
|
|
|
54
|
-
|
|
55
57
|
declare global {
|
|
56
58
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
57
59
|
namespace NodeJS {
|
|
@@ -62,15 +64,17 @@ declare global {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
// Disponibilizar globalmente também como fallback
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
typeof globalThis !== 'undefined' &&
|
|
70
|
+
!(globalThis as any).stripVTControlCharacters
|
|
71
|
+
) {
|
|
67
72
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
73
|
(globalThis as any).stripVTControlCharacters = stripVTControlCharacters;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
// Tentar aplicar diretamente ao módulo util se possível
|
|
72
77
|
try {
|
|
73
|
-
|
|
74
78
|
const util = require('util');
|
|
75
79
|
if (!util.stripVTControlCharacters) {
|
|
76
80
|
util.stripVTControlCharacters = stripVTControlCharacters;
|