@gilbert_oliveira/commit-wizard 2.0.2 → 2.1.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/README.md +13 -14
- package/dist/commit-wizard.d.ts +1 -0
- package/dist/commit-wizard.js +52 -110
- package/package.json +29 -26
- package/{bin → src/bin}/commit-wizard.ts +6 -5
- package/src/core/cache.ts +2 -2
- package/src/core/index.ts +15 -11
- package/src/core/openai.ts +30 -12
- package/src/core/smart-split.ts +72 -48
- package/src/git/index.ts +16 -4
- package/src/ui/index.ts +1 -1
- package/src/ui/smart-split.ts +1 -1
- package/src/utils/polyfill.ts +0 -82
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.1.0",
|
|
7
7
|
"categories": [
|
|
8
8
|
"Other",
|
|
9
9
|
"SCM Providers"
|
|
@@ -15,27 +15,23 @@
|
|
|
15
15
|
"commit-wizard": "./dist/commit-wizard.js"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"dev": "
|
|
19
|
-
"build": "
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"test
|
|
23
|
-
"test:
|
|
24
|
-
"test:coverage
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"ci:
|
|
30
|
-
"ci:
|
|
31
|
-
"ci:integration": "bun test tests/integration.test.ts tests/smart-split.test.ts",
|
|
18
|
+
"dev": "npm run build && node dist/commit-wizard.js",
|
|
19
|
+
"build": "tsup src/bin/commit-wizard.ts --format esm --dts --out-dir dist --clean --minify --splitting false --shims",
|
|
20
|
+
"build:tsc": "tsc --project tsconfig.json",
|
|
21
|
+
"start": "node dist/commit-wizard.js",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"test:coverage": "vitest run --coverage",
|
|
25
|
+
"test:integration": "vitest run tests/integration.test.ts",
|
|
26
|
+
"test:ci": "vitest run --coverage.enabled --coverage.reporter=lcov --reporter=junit --outputFile=coverage/test-report.junit.xml",
|
|
27
|
+
"lint": "eslint . --ext .ts",
|
|
28
|
+
"format": "prettier --write .",
|
|
29
|
+
"ci:test": "vitest run",
|
|
30
|
+
"ci:build": "npm run build",
|
|
32
31
|
"release:patch": "./scripts/release.sh patch",
|
|
33
32
|
"release:minor": "./scripts/release.sh minor",
|
|
34
33
|
"release:major": "./scripts/release.sh major",
|
|
35
|
-
"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)
|
|
36
|
-
"type-check": "bun run tsc --noEmit",
|
|
37
|
-
"lint": "bun run eslint --fix",
|
|
38
|
-
"format": "bun run prettier --write ."
|
|
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);\""
|
|
39
35
|
},
|
|
40
36
|
"keywords": [
|
|
41
37
|
"git",
|
|
@@ -57,13 +53,13 @@
|
|
|
57
53
|
},
|
|
58
54
|
"homepage": "https://github.com/gilbert-oliveira/commit-wizard#readme",
|
|
59
55
|
"engines": {
|
|
60
|
-
"node": ">=18.0.0"
|
|
61
|
-
"bun": ">=1.0.0"
|
|
56
|
+
"node": ">=18.0.0"
|
|
62
57
|
},
|
|
63
58
|
"files": [
|
|
64
59
|
"bin/",
|
|
65
60
|
"src/",
|
|
66
61
|
"dist/",
|
|
62
|
+
"dist-node/",
|
|
67
63
|
"README.md",
|
|
68
64
|
"LICENSE",
|
|
69
65
|
".commit-wizardrc"
|
|
@@ -71,18 +67,25 @@
|
|
|
71
67
|
"dependencies": {
|
|
72
68
|
"@clack/prompts": "^0.11.0",
|
|
73
69
|
"clipboardy": "^4.0.0",
|
|
74
|
-
"
|
|
75
|
-
"
|
|
70
|
+
"dotenv": "^17.2.0",
|
|
71
|
+
"simple-git": "^3.25.0"
|
|
76
72
|
},
|
|
77
73
|
"devDependencies": {
|
|
78
|
-
"@types/
|
|
74
|
+
"@types/jest": "^30.0.0",
|
|
79
75
|
"@types/node": "^24.0.13",
|
|
80
76
|
"@typescript-eslint/eslint-plugin": "^8.36.0",
|
|
81
77
|
"@typescript-eslint/parser": "^8.36.0",
|
|
82
|
-
"
|
|
78
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
79
|
+
"@vitest/ui": "^3.2.4",
|
|
83
80
|
"chalk": "^5.3.0",
|
|
84
81
|
"eslint": "^9.30.1",
|
|
85
|
-
"
|
|
82
|
+
"jest": "^30.0.4",
|
|
83
|
+
"prettier": "^3.6.2",
|
|
84
|
+
"ts-jest": "^29.4.0",
|
|
85
|
+
"ts-node": "^10.9.2",
|
|
86
|
+
"tsconfig-paths": "^4.2.0",
|
|
87
|
+
"tsup": "^8.5.0",
|
|
88
|
+
"vitest": "^3.2.4"
|
|
86
89
|
},
|
|
87
90
|
"peerDependencies": {
|
|
88
91
|
"typescript": "^5"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { intro, outro, log } from '@clack/prompts';
|
|
4
|
-
import { main } from '
|
|
5
|
-
import { parseArgs, showHelp, showVersion } from '../
|
|
4
|
+
import { main } from '@core/index';
|
|
5
|
+
import { parseArgs, showHelp, showVersion } from '../utils/args';
|
|
6
6
|
|
|
7
7
|
async function run() {
|
|
8
8
|
try {
|
|
@@ -46,6 +46,7 @@ async function run() {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
// Verificar se o arquivo está sendo executado diretamente
|
|
50
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
50
51
|
run();
|
|
51
|
-
}
|
|
52
|
+
}
|
package/src/core/cache.ts
CHANGED
package/src/core/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { log } from '@clack/prompts';
|
|
2
|
-
import { loadConfig, validateConfig } from '../config/index
|
|
2
|
+
import { loadConfig, validateConfig } from '../config/index';
|
|
3
3
|
import {
|
|
4
4
|
isGitRepository,
|
|
5
5
|
getGitStatus,
|
|
6
6
|
getDiffStats,
|
|
7
7
|
executeCommit,
|
|
8
8
|
executeFileCommit,
|
|
9
|
-
} from '../git/index
|
|
10
|
-
import { generateWithRetry } from './openai
|
|
9
|
+
} from '../git/index';
|
|
10
|
+
import { generateWithRetry } from './openai';
|
|
11
11
|
import {
|
|
12
12
|
showCommitPreview,
|
|
13
13
|
editCommitMessage,
|
|
@@ -16,12 +16,12 @@ import {
|
|
|
16
16
|
showCancellation,
|
|
17
17
|
selectFilesForCommit,
|
|
18
18
|
askContinueCommits,
|
|
19
|
-
} from '../ui/index
|
|
20
|
-
import { chooseSplitMode } from '../ui/smart-split
|
|
21
|
-
import { handleSmartSplitMode } from './smart-split
|
|
22
|
-
import { initializeCache } from './cache
|
|
23
|
-
import type { CLIArgs } from '../utils/args
|
|
24
|
-
import type { Config } from '../config/index
|
|
19
|
+
} from '../ui/index';
|
|
20
|
+
import { chooseSplitMode } from '../ui/smart-split';
|
|
21
|
+
import { handleSmartSplitMode } from './smart-split';
|
|
22
|
+
import { initializeCache } from './cache';
|
|
23
|
+
import type { CLIArgs } from '../utils/args';
|
|
24
|
+
import type { Config } from '../config/index';
|
|
25
25
|
|
|
26
26
|
export async function main(
|
|
27
27
|
args: CLIArgs = {
|
|
@@ -248,7 +248,7 @@ async function handleSplitMode(gitStatus: any, config: any, args: CLIArgs) {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
// Gerar diff apenas dos arquivos selecionados
|
|
251
|
-
const { getFileDiff } = await import('../git/index
|
|
251
|
+
const { getFileDiff } = await import('../git/index');
|
|
252
252
|
const fileDiffs = selectedFiles
|
|
253
253
|
.filter((file): file is string => file !== undefined)
|
|
254
254
|
.map((file) => {
|
|
@@ -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Config } from '../config/index
|
|
1
|
+
import type { Config } from '../config/index';
|
|
2
2
|
|
|
3
3
|
export interface CommitSuggestion {
|
|
4
4
|
message: string;
|
|
@@ -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
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Config } from '../config/index
|
|
2
|
-
import type { CLIArgs } from '../utils/args
|
|
3
|
-
import { getCachedAnalysis, setCachedAnalysis } from './cache
|
|
4
|
-
import { showCommitResult } from '../ui/index
|
|
1
|
+
import type { Config } from '../config/index';
|
|
2
|
+
import type { CLIArgs } from '../utils/args';
|
|
3
|
+
import { getCachedAnalysis, setCachedAnalysis } from './cache';
|
|
4
|
+
import { showCommitResult } from '../ui/index';
|
|
5
5
|
import { log } from '@clack/prompts';
|
|
6
6
|
|
|
7
7
|
export interface FileGroup {
|
|
@@ -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', {
|
|
@@ -242,7 +251,7 @@ export async function analyzeFileContext(
|
|
|
242
251
|
* Gera diff para um grupo de arquivos (otimizado para tokens)
|
|
243
252
|
*/
|
|
244
253
|
export async function generateGroupDiff(group: FileGroup): Promise<string> {
|
|
245
|
-
const { getFileDiff } = await import('../git/index
|
|
254
|
+
const { getFileDiff } = await import('../git/index');
|
|
246
255
|
|
|
247
256
|
const diffs = group.files
|
|
248
257
|
.map((file) => {
|
|
@@ -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
|
}
|
|
@@ -416,7 +429,7 @@ export async function handleSmartSplitMode(
|
|
|
416
429
|
|
|
417
430
|
// Mostrar interface de Smart Split para o usuário decidir
|
|
418
431
|
if (!args.yes && !args.silent) {
|
|
419
|
-
const { showSmartSplitGroups } = await import('../ui/smart-split
|
|
432
|
+
const { showSmartSplitGroups } = await import('../ui/smart-split');
|
|
420
433
|
const userAction = await showSmartSplitGroups(analysis.groups);
|
|
421
434
|
|
|
422
435
|
if (userAction.action === 'cancel') {
|
|
@@ -429,7 +442,7 @@ export async function handleSmartSplitMode(
|
|
|
429
442
|
if (userAction.action === 'manual') {
|
|
430
443
|
// Delegar para modo manual - re-executar com flag split
|
|
431
444
|
const newArgs = { ...args, split: true, smartSplit: false };
|
|
432
|
-
const { main } = await import('./index
|
|
445
|
+
const { main } = await import('./index');
|
|
433
446
|
await main(newArgs);
|
|
434
447
|
return;
|
|
435
448
|
}
|
|
@@ -473,12 +486,14 @@ export async function handleSmartSplitMode(
|
|
|
473
486
|
log.info(`🤖 Gerando commit para: ${group.name}`);
|
|
474
487
|
}
|
|
475
488
|
|
|
476
|
-
const { generateWithRetry } = await import('./openai
|
|
489
|
+
const { generateWithRetry } = await import('./openai');
|
|
477
490
|
const result = await generateWithRetry(groupDiff, config, group.files);
|
|
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
|
}
|
|
@@ -503,7 +518,7 @@ export async function handleSmartSplitMode(
|
|
|
503
518
|
// Interface do usuário
|
|
504
519
|
if (args.yes) {
|
|
505
520
|
// Modo automático
|
|
506
|
-
const { executeFileCommit } = await import('../git/index
|
|
521
|
+
const { executeFileCommit } = await import('../git/index');
|
|
507
522
|
let commitResult;
|
|
508
523
|
|
|
509
524
|
// Fazer commit apenas dos arquivos do grupo atual
|
|
@@ -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');
|
|
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
|
}
|
|
@@ -558,13 +576,13 @@ export async function handleSmartSplitMode(
|
|
|
558
576
|
editCommitMessage,
|
|
559
577
|
copyToClipboard,
|
|
560
578
|
showCancellation,
|
|
561
|
-
} = await import('../ui/index
|
|
579
|
+
} = await import('../ui/index');
|
|
562
580
|
|
|
563
581
|
const uiAction = await showCommitPreview(result.suggestion);
|
|
564
582
|
|
|
565
583
|
switch (uiAction.action) {
|
|
566
584
|
case 'commit': {
|
|
567
|
-
const { executeFileCommit } = await import('../git/index
|
|
585
|
+
const { executeFileCommit } = await import('../git/index');
|
|
568
586
|
let commitResult;
|
|
569
587
|
|
|
570
588
|
// Fazer commit apenas dos arquivos do grupo atual
|
|
@@ -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');
|
|
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
|
}
|
|
@@ -612,7 +633,7 @@ export async function handleSmartSplitMode(
|
|
|
612
633
|
case 'edit': {
|
|
613
634
|
const editAction = await editCommitMessage(result.suggestion.message);
|
|
614
635
|
if (editAction.action === 'commit' && editAction.message) {
|
|
615
|
-
const { executeFileCommit } = await import('../git/index
|
|
636
|
+
const { executeFileCommit } = await import('../git/index');
|
|
616
637
|
let editCommitResult;
|
|
617
638
|
|
|
618
639
|
// Fazer commit apenas dos arquivos do grupo atual
|
|
@@ -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');
|
|
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
|
}
|
|
@@ -679,7 +703,7 @@ export async function handleSmartSplitMode(
|
|
|
679
703
|
|
|
680
704
|
// Perguntar se quer continuar (exceto em modo automático)
|
|
681
705
|
if (i < analysis.groups.length - 1 && !args.yes) {
|
|
682
|
-
const { askContinueCommits } = await import('../ui/index
|
|
706
|
+
const { askContinueCommits } = await import('../ui/index');
|
|
683
707
|
const remainingGroups = analysis.groups
|
|
684
708
|
.slice(i + 1)
|
|
685
709
|
.filter((g) => g !== undefined)
|
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/ui/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
isCancel,
|
|
9
9
|
} from '@clack/prompts';
|
|
10
10
|
import clipboardy from 'clipboardy';
|
|
11
|
-
import type { CommitSuggestion } from '../core/openai
|
|
11
|
+
import type { CommitSuggestion } from '../core/openai';
|
|
12
12
|
|
|
13
13
|
export interface UIAction {
|
|
14
14
|
action: 'commit' | 'edit' | 'copy' | 'cancel';
|
package/src/ui/smart-split.ts
CHANGED