@gilbert_oliveira/commit-wizard 1.0.26 → 1.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilbert_oliveira/commit-wizard",
3
- "version": "1.0.26",
3
+ "version": "1.1.0",
4
4
  "description": "O **Commit Wizard** é uma ferramenta automatizada para geração de mensagens de commit com base na convenção de **Conventional Commits**. Ele ajuda a garantir que suas mensagens de commit sigam um padrão consistente e facilite a comunicação de mudanças no código.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -13,7 +13,17 @@
13
13
  "scripts": {
14
14
  "start": "node dist/index.js",
15
15
  "dev": "tsc --watch",
16
- "build": "tsc"
16
+ "build": "npm run lint && tsc",
17
+ "test": "jest",
18
+ "test:watch": "jest --watch",
19
+ "test:coverage": "jest --coverage",
20
+ "lint": "eslint src/**/*.ts",
21
+ "lint:fix": "eslint src/**/*.ts --fix",
22
+ "format": "prettier --write src/**/*.ts",
23
+ "format:check": "prettier --check src/**/*.ts",
24
+ "prepare": "npm run build",
25
+ "prepack": "npm run build",
26
+ "clean": "rimraf dist"
17
27
  },
18
28
  "repository": {
19
29
  "type": "git",
@@ -28,7 +38,11 @@
28
38
  "commitizen",
29
39
  "commitlint",
30
40
  "husky",
31
- "lint-staged"
41
+ "lint-staged",
42
+ "git",
43
+ "ai",
44
+ "openai",
45
+ "gpt"
32
46
  ],
33
47
  "author": "Gilbert Oliveira <contato@gilbert.dev.br>",
34
48
  "engines": {
@@ -41,10 +55,32 @@
41
55
  "homepage": "https://github.com/gilbert-oliveira/commit-wizard#readme",
42
56
  "dependencies": {
43
57
  "@types/node": "^22.14.1",
58
+ "chalk": "^5.3.0",
59
+ "cli-progress": "^3.12.0",
44
60
  "gpt-tokenizer": "^1.0.0",
45
61
  "inquirer": "^12.5.2",
46
62
  "ora": "^8.2.0",
47
63
  "ts-node": "^10.9.2",
48
64
  "typescript": "^5.8.3"
49
- }
65
+ },
66
+ "devDependencies": {
67
+ "@eslint/js": "^9.0.0",
68
+ "@types/cli-progress": "^3.11.6",
69
+ "@types/inquirer": "^9.0.7",
70
+ "@types/jest": "^29.5.12",
71
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
72
+ "@typescript-eslint/parser": "^8.0.0",
73
+ "eslint": "^9.0.0",
74
+ "eslint-config-prettier": "^9.1.0",
75
+ "eslint-plugin-prettier": "^5.1.3",
76
+ "jest": "^29.7.0",
77
+ "prettier": "^3.2.5",
78
+ "rimraf": "^5.0.5",
79
+ "ts-jest": "^29.1.2"
80
+ },
81
+ "files": [
82
+ "dist",
83
+ "README.md",
84
+ "LICENSE"
85
+ ]
50
86
  }
package/src/index.ts DELETED
@@ -1,279 +0,0 @@
1
- #!/usr/bin/env ts-node
2
-
3
- import chalk from 'chalk';
4
- import inquirer from 'inquirer';
5
- import os from 'os';
6
- import path from 'path';
7
- import fs from 'fs';
8
- import { execSync } from 'child_process';
9
- import ora from 'ora';
10
- import { encode, decode } from 'gpt-tokenizer';
11
-
12
- const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
13
- if (!OPENAI_API_KEY) {
14
- console.log(
15
- chalk.redBright('\n🚨 Erro: A variável de ambiente ') +
16
- chalk.yellow('OPENAI_API_KEY') +
17
- chalk.redBright(' não está definida.\n')
18
- );
19
- console.log(
20
- chalk.white('→ Defina sua chave com: ') +
21
- chalk.cyan('export OPENAI_API_KEY="sua-chave"') +
22
- chalk.white(' ou configure no seu ') +
23
- chalk.cyan('.bashrc') +
24
- chalk.white(' ou ') +
25
- chalk.cyan('.zshrc\n')
26
- );
27
- process.exit(1);
28
- }
29
-
30
- /**
31
- * Realiza a chamada à API do OpenAI.
32
- * @param prompt Texto que será enviado como mensagem do usuário.
33
- * @param mode Define o contexto: 'commit' para gerar mensagem de commit ou outro valor para resumo.
34
- * @returns Resposta da API (string com a mensagem ou o resumo).
35
- */
36
- export async function callOpenAI(prompt: string, mode: string = 'commit'): Promise<string> {
37
- const url = 'https://api.openai.com/v1/chat/completions';
38
-
39
- // Escolhe o prompt inicial de acordo com o modo.
40
- const systemPrompt =
41
- mode === 'commit'
42
- ? "Você é um assistente que gera mensagens de commit seguindo a convenção do Conventional Commits."
43
- : "Você é um assistente que resume alterações de código de forma breve, usando linguagem imperativa em português.";
44
-
45
- const body = {
46
- model: "gpt-4-turbo",
47
- messages: [
48
- { role: "system", content: systemPrompt },
49
- { role: "user", content: prompt }
50
- ],
51
- temperature: 0.2
52
- };
53
-
54
- const response = await fetch(url, {
55
- method: "POST",
56
- headers: {
57
- "Content-Type": "application/json",
58
- "Authorization": `Bearer ${OPENAI_API_KEY}`
59
- },
60
- body: JSON.stringify(body)
61
- });
62
-
63
- if (!response.ok) {
64
- throw new Error(`Erro na API OpenAI: ${response.statusText}`);
65
- }
66
-
67
- const data = await response.json();
68
- // Retorna a resposta do primeiro "choice".
69
- return data.choices[0].message.content.trim();
70
- }
71
-
72
- /**
73
- * Divide o diff em chunks menores com base na contagem de tokens.
74
- * Utiliza o gpt-tokenizer para garantir que cada chunk não exceda o limite de tokens.
75
- * @param diff O diff completo em formato de string.
76
- * @param maxTokens Quantidade máxima de tokens permitida para cada chunk (padrão: 1000 tokens).
77
- * @returns Array de strings, cada uma representando um chunk.
78
- */
79
- export function chunkDiff(diff: string, maxTokens: number = 1000): string[] {
80
- // Codifica o diff para obter o array de tokens.
81
- const tokens = encode(diff);
82
-
83
- // Se o diff couber em um único chunk, retorna-o diretamente.
84
- if (tokens.length <= maxTokens) {
85
- return [diff];
86
- }
87
-
88
- const chunks: string[] = [];
89
-
90
- // Percorre os tokens de forma que cada chunk contenha no máximo maxTokens tokens.
91
- for (let i = 0; i < tokens.length; i += maxTokens) {
92
- const chunkTokens = tokens.slice(i, i + maxTokens);
93
- const chunkText = decode(chunkTokens);
94
- chunks.push(chunkText);
95
- }
96
-
97
- return chunks;
98
- }
99
-
100
-
101
- // Pré-prompt para a geração da mensagem de commit conforme as convenções
102
- const COMMIT_PROMPT = `
103
- Por favor, escreva a mensagem de commit para este diff usando a convenção de Conventional Commits: https://www.conventionalcommits.org/en/v1.0.0/.
104
- A mensagem deve começar com um tipo de commit, como:
105
- feat: para novas funcionalidades
106
- fix: para correções de bugs
107
- chore: para alterações que não afetam a funcionalidade
108
- docs: para mudanças na documentação
109
- style: para alterações no estilo do código (como formatação)
110
- refactor: para alterações no código que não alteram a funcionalidade
111
- perf: para melhorias de desempenho
112
- test: para alterações nos testes
113
- ci: para mudanças no pipeline de integração contínua
114
-
115
- Exemplo:
116
- feat(auth): adicionar suporte ao login com Google
117
-
118
- Caso o commit seja uma alteração significativa (breaking change), inclua um título com \`!\` após o tipo de commit e adicione a explicação em \`BREAKING CHANGE\`:
119
- feat!(auth): reestruturar fluxo de login
120
- BREAKING CHANGE: A API de login foi alterada e não é compatível com versões anteriores.
121
-
122
- Gere também uma descrição mais detalhada do commit, se necessário.
123
-
124
- Use sempre linguagem imperativa e primeira pessoa do singular, como:
125
- - "adiciona recurso"
126
- - "corrige bug"
127
- - "remove arquivo"
128
-
129
- Lembre-se: os textos fora do Conventional Commit devem ser em português.
130
- `;
131
-
132
- async function main(): Promise<void> {
133
- // Verifica se o diretório é um repositório git.
134
- try {
135
- execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
136
- } catch {
137
- console.error(chalk.red('❌ Este diretório não é um repositório git.'));
138
- process.exit(1);
139
- }
140
-
141
- // Verifica se há alterações staged, desconsiderando arquivos .lock
142
- let stagedFiles: string;
143
- try {
144
- stagedFiles = execSync(
145
- 'git diff --cached --name-only -- . ":(exclude)*.lock*"',
146
- { encoding: 'utf8' }
147
- ).toString().trim();
148
- if (!stagedFiles) {
149
- console.log(chalk.yellow('⚠️ Não há alterações staged para o commit.'));
150
- process.exit(0);
151
- }
152
- } catch (error) {
153
- console.error(chalk.red('❌ Erro ao verificar alterações staged:'), error);
154
- process.exit(1);
155
- }
156
-
157
- // Obtém o diff completo das alterações staged, ignorando arquivos .lock*
158
- let diff: string;
159
- try {
160
- diff = execSync(
161
- 'git diff --cached -- . ":(exclude)*.lock*"',
162
- { encoding: 'utf8' }
163
- );
164
- } catch (error) {
165
- console.error(chalk.red('❌ Erro ao obter o diff:'), error);
166
- process.exit(1);
167
- }
168
-
169
- // Divide o diff em chunks com base no número máximo de tokens.
170
- const MAX_TOKENS = 1000;
171
- const chunks = chunkDiff(diff, MAX_TOKENS);
172
- let inputForCommit: string;
173
-
174
- if (chunks.length === 1) {
175
- inputForCommit = chunks[0];
176
- } else {
177
- // Se houver vários chunks, gera um resumo para todos eles utilizando um único spinner.
178
- const partialSummaries: string[] = [];
179
- const chunkSummaryPrefix =
180
- "A partir do diff abaixo, extraia um resumo breve das alterações (use linguagem imperativa e em português):";
181
-
182
- // Inicia um spinner único para todo o processo
183
- const spinnerSummary = ora("Gerando resumo do commit.").start();
184
-
185
- try {
186
- for (const chunk of chunks) {
187
- const prompt = `${chunkSummaryPrefix}\n\n${chunk}`;
188
- const summary = await callOpenAI(prompt, 'resumo');
189
- partialSummaries.push(summary);
190
- }
191
- spinnerSummary.succeed("Resumo do commit gerado.");
192
- } catch (error) {
193
- spinnerSummary.fail("Erro ao gerar resumo do commit.");
194
- console.error(chalk.red('❌ Erro ao gerar resumo para o commit:'), error);
195
- process.exit(1);
196
- }
197
- inputForCommit = partialSummaries.join("\n\n");
198
- }
199
-
200
- // Gera a mensagem de commit com o pré-prompt e o diff (ou seus resumos).
201
- const finalPrompt = `${COMMIT_PROMPT}\n\nDiff:\n\n${inputForCommit}`;
202
- const spinnerCommit = ora('Gerando mensagem de commit com base no diff...').start();
203
-
204
- let generatedMessage: string;
205
- try {
206
- generatedMessage = await callOpenAI(finalPrompt, 'commit');
207
- // Remove os delimitadores de bloco de código (```)
208
- generatedMessage = generatedMessage.replace(/```/g, '').trim();
209
- spinnerCommit.succeed('Mensagem de commit gerada com sucesso.');
210
- } catch (error) {
211
- spinnerCommit.fail('Erro ao gerar a mensagem de commit.');
212
- console.error(chalk.red('❌ Erro ao gerar a mensagem de commit:'), error);
213
- process.exit(1);
214
- }
215
-
216
- console.log(chalk.greenBright('\n✨ Mensagem de commit gerada automaticamente:'));
217
- console.log(chalk.yellowBright(generatedMessage));
218
-
219
- // Pergunta ao usuário se deseja confirmar, editar ou cancelar o commit.
220
- const promptModule = inquirer.createPromptModule();
221
- const { action } = await promptModule<{ action: 'confirm' | 'edit' | 'cancel' }>([
222
- {
223
- type: 'list',
224
- name: 'action',
225
- message: chalk.blue.bold('O que deseja fazer com a mensagem de commit gerada?'),
226
- choices: [
227
- { name: '📌 Confirmar e commitar', value: 'confirm' },
228
- { name: '📝 Editar a mensagem antes de commitar', value: 'edit' },
229
- { name: '🚫 Cancelar o commit', value: 'cancel' },
230
- ],
231
- },
232
- ]);
233
-
234
- // Cria um arquivo temporário para armazenar a mensagem (para edição se necessário).
235
- const tempFilePath = path.join(os.tmpdir(), 'COMMIT_EDITMSG');
236
- fs.writeFileSync(tempFilePath, generatedMessage);
237
-
238
- if (action === 'edit') {
239
- console.log(chalk.cyan('📝 Abrindo editor para edição da mensagem...'));
240
- const editor = process.env.EDITOR || 'nano';
241
- try {
242
- execSync(`${editor} ${tempFilePath}`, { stdio: 'inherit' });
243
- } catch (error) {
244
- console.error(chalk.red('❌ Erro ao abrir o editor:'), error);
245
- process.exit(1);
246
- }
247
- } else if (action === 'cancel') {
248
- console.log(chalk.yellow('🚫 Commit cancelado pelo usuário.'));
249
- fs.unlinkSync(tempFilePath);
250
- process.exit(0);
251
- }
252
-
253
- // Lê a mensagem final (após eventual edição).
254
- const finalMessage = fs.readFileSync(tempFilePath, 'utf8').trim();
255
- if (!finalMessage) {
256
- console.error(chalk.red('❌ Nenhuma mensagem inserida, commit cancelado.'));
257
- fs.unlinkSync(tempFilePath);
258
- process.exit(1);
259
- }
260
-
261
- // Captura quaisquer argumentos adicionais passados para o comando.
262
- const gitArgs = process.argv.slice(2).join(' ');
263
- console.log(chalk.blue('🔍 Argumentos adicionais para o commit:'), gitArgs);
264
-
265
- // Realiza o commit com a mensagem final.
266
- try {
267
- execSync(`git commit -F ${tempFilePath} ${gitArgs}`, { stdio: 'inherit' });
268
- console.log(chalk.green.bold('✅ Commit realizado com sucesso.'));
269
- } catch (error) {
270
- console.error(chalk.red('❌ Erro ao realizar o commit:'), error);
271
- } finally {
272
- fs.unlinkSync(tempFilePath);
273
- }
274
- }
275
-
276
- main().catch((err) => {
277
- console.error(chalk.red('❌ Erro durante o commit:'), err);
278
- process.exit(1);
279
- });
package/tsconfig.json DELETED
@@ -1,24 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ESNext",
5
- "moduleResolution": "node",
6
- "esModuleInterop": true,
7
- "resolveJsonModule": true,
8
- "skipLibCheck": true,
9
- "strict": true,
10
- "forceConsistentCasingInFileNames": true,
11
- "noImplicitAny": true,
12
- "noUnusedLocals": true,
13
- "noUnusedParameters": true,
14
- "skipDefaultLibCheck": true,
15
- "declaration": true,
16
- "outDir": "./dist",
17
- "sourceMap": true
18
- },
19
- "include": ["src"],
20
- "exclude": [
21
- "node_modules",
22
- "dist"
23
- ]
24
- }