@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/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.2",
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": "bun run bin/commit-wizard.ts",
19
- "build": "bun build bin/commit-wizard.ts --outdir=dist --target=bun --minify",
20
- "test": "bun test",
21
- "test:watch": "bun test --watch",
22
- "test:coverage": "bun test && node scripts/generate-lcov.js",
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
- "prepublishOnly": "bun run build && bun test",
26
- "start": "bun run bin/commit-wizard.ts",
27
- "ci:test": "bun test --reporter=verbose",
28
- "ci:build": "bun run build",
29
- "ci:lint": "bun run tsc --noEmit",
30
- "ci:security": "bun audit",
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)\/\\.*/); console.log(changed ? '🚀 Mudanças detectadas - deploy recomendado!' : '⚠️ Nenhuma mudança significativa detectada'); process.exit(changed ? 0 : 1);\"",
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
- "simple-git": "^3.25.0",
75
- "dotenv": "^17.2.0"
70
+ "dotenv": "^17.2.0",
71
+ "simple-git": "^3.25.0"
76
72
  },
77
73
  "devDependencies": {
78
- "@types/bun": "latest",
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
- "c8": "^10.1.3",
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
- "prettier": "^3.6.2"
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 bun
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { intro, outro, log } from '@clack/prompts';
4
- import { main } from '../src/core/index.ts';
5
- import { parseArgs, showHelp, showVersion } from '../src/utils/args.ts';
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
- if (import.meta.main) {
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
@@ -1,5 +1,5 @@
1
- import type { Config } from '../config/index.ts';
2
- import type { FileGroup } from './smart-split.ts';
1
+ import type { Config } from '../config/index';
2
+ import type { FileGroup } from './smart-split';
3
3
  import crypto from 'crypto';
4
4
 
5
5
  export interface CacheEntry {
package/src/core/index.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { log } from '@clack/prompts';
2
- import { loadConfig, validateConfig } from '../config/index.ts';
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.ts';
10
- import { generateWithRetry } from './openai.ts';
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.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';
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.ts');
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(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}`);
@@ -1,4 +1,4 @@
1
- import type { Config } from '../config/index.ts';
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 = 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);
@@ -1,7 +1,7 @@
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';
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 = 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', {
@@ -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.ts');
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 = 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
  }
@@ -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.ts');
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.ts');
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.ts');
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(`❌ 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
  }
@@ -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.ts');
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) => `"${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
  }
@@ -558,13 +576,13 @@ export async function handleSmartSplitMode(
558
576
  editCommitMessage,
559
577
  copyToClipboard,
560
578
  showCancellation,
561
- } = await import('../ui/index.ts');
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.ts');
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) => `"${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
  }
@@ -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.ts');
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) => `"${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
  }
@@ -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.ts');
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
- // 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
 
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.ts';
11
+ import type { CommitSuggestion } from '../core/openai';
12
12
 
13
13
  export interface UIAction {
14
14
  action: 'commit' | 'edit' | 'copy' | 'cancel';
@@ -1,5 +1,5 @@
1
1
  import { select, confirm, log, note, isCancel } from '@clack/prompts';
2
- import type { FileGroup } from '../core/smart-split.ts';
2
+ import type { FileGroup } from '../core/smart-split';
3
3
 
4
4
  export interface SmartSplitAction {
5
5
  action: 'proceed' | 'manual' | 'cancel';