@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/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.1",
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(fileDiffs, config, selectedFiles.filter((file): file is string => file !== undefined));
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}`);
@@ -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 = diff.length > maxDiffLength
41
- ? diff.substring(0, maxDiffLength) + '\n... (diff truncado)'
42
- : diff;
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 = filenames.length > 10
46
- ? `${filenames.length} arquivos: ${filenames.slice(0, 5).join(', ')}...`
47
- : filenames.join(', ');
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
- // Remover backticks se presentes
279
- message = message.replace(/^```\s*/, '').replace(/\s*```$/, '');
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);
@@ -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 = overallDiff.length > maxDiffLength
32
- ? overallDiff.substring(0, maxDiffLength) + '\n... (diff truncado)'
33
- : overallDiff;
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((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>);
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((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[]>);
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(`⚠️ Diff muito grande (${overallDiff.length} chars), usando análise baseada em nomes de arquivos`);
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 = content.length > maxContentLength
298
- ? content.substring(0, maxContentLength) + '\n... (conteúdo truncado)'
299
- : content;
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 = content.length > maxContentLength
345
- ? content.substring(0, maxContentLength) + '\n... (conteúdo truncado)'
346
- : content;
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(`❌ Erro ao gerar commit para ${group.name}: ${result.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) => `"${f}"`).join(' ');
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 "${(result.suggestion.message || '').replace(/"/g, '\\"')}"`,
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) => `"${f}"`).join(' ');
600
+ const filesArg = group.files.map((f) => escapeShellArg(f)).join(' ');
601
+ const escapedMessage = escapeShellArg(commitMessage);
581
602
  execSync(
582
- `git commit ${filesArg} -m "${commitMessage.replace(/"/g, '"')}"`,
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) => `"${f}"`).join(' ');
652
+ const filesArg = group.files.map((f) => escapeShellArg(f)).join(' ');
653
+ const escapedMessage = escapeShellArg(editAction.message || '');
630
654
  execSync(
631
- `git commit ${filesArg} -m "${(editAction.message || '').replace(/"/g, '"')}"`,
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
- // Executar commit
84
- execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
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
- // Commit apenas do arquivo específico
119
- execSync(`git commit "${filename}" -m "${message.replace(/"/g, '\\"')}"`, {
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
 
@@ -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(`[${esc}${csi}][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]`, 'g');
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
- if (typeof globalThis !== 'undefined' && !(globalThis as any).stripVTControlCharacters) {
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;