@gilbert_oliveira/commit-wizard 1.0.9 → 1.0.10

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.
Files changed (2) hide show
  1. package/package.json +14 -10
  2. package/src/index.ts +95 -162
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilbert_oliveira/commit-wizard",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
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",
@@ -27,7 +27,10 @@
27
27
  "husky",
28
28
  "lint-staged"
29
29
  ],
30
- "author": "Gilbert Oliveira",
30
+ "author": "Gilbert Oliveira <contato@gilbert.dev.br>",
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ },
31
34
  "license": "MIT",
32
35
  "bugs": {
33
36
  "url": "https://github.com/gilbert-oliveira/commit-wizard/issues"
@@ -37,19 +40,20 @@
37
40
  "access": "public"
38
41
  },
39
42
  "devDependencies": {
43
+ "@types/bun": "latest",
40
44
  "@types/node": "^22.14.1",
41
- "typescript": "^5.6.3"
45
+ "eslint": "^9.24.0",
46
+ "semver": "^7.7.1",
47
+ "typescript": "^5.8.3"
42
48
  },
43
49
  "dependencies": {
44
- "gpt-tokenizer": "^1.0.0",
45
- "ora": "^8.2.0",
46
- "@types/bun": "latest",
47
- "typescript": "^5.0.0",
48
- "chalk": "^5.3.0",
50
+ "chalk": "^5.4.1",
49
51
  "child_process": "^1.0.2",
50
52
  "fs": "^0.0.1-security",
51
- "inquirer": "^12.0.1",
53
+ "gpt-tokenizer": "^1.0.5",
54
+ "inquirer": "^12.5.2",
55
+ "ora": "^8.2.0",
52
56
  "os": "^0.1.2",
53
57
  "path": "^0.12.7"
54
58
  }
55
- }
59
+ }
package/src/index.ts CHANGED
@@ -1,92 +1,62 @@
1
1
  #!/usr/bin/env ts-node
2
2
 
3
3
  import chalk from 'chalk';
4
+ import { execSync } from 'child_process';
5
+ import fs from 'fs';
4
6
  import inquirer from 'inquirer';
5
7
  import os from 'os';
6
8
  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
9
 
12
- const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
13
- if (!OPENAI_API_KEY) {
14
- throw new Error("Chave da API do OpenAI não configurada. Defina a variável de ambiente OPENAI_API_KEY.");
10
+ // Função para verificar se o comando 'cody' está disponível
11
+ function isCodyInstalled(): boolean {
12
+ try {
13
+ execSync('cody --version', { stdio: 'ignore' });
14
+ return true;
15
+ } catch (error) {
16
+ return false;
17
+ }
15
18
  }
16
19
 
17
- /**
18
- * Realiza a chamada à API do OpenAI.
19
- * @param prompt Texto que será enviado como mensagem do usuário.
20
- * @param mode Define o contexto: 'commit' para gerar mensagem de commit ou outro valor para resumo.
21
- * @returns Resposta da API (string com a mensagem ou o resumo).
22
- */
23
- export async function callOpenAI(prompt: string, mode: string = 'commit'): Promise<string> {
24
- const url = 'https://api.openai.com/v1/chat/completions';
25
-
26
- // Escolhe o prompt inicial de acordo com o modo.
27
- const systemPrompt =
28
- mode === 'commit'
29
- ? "Você é um assistente que gera mensagens de commit seguindo a convenção do Conventional Commits."
30
- : "Você é um assistente que resume alterações de código de forma breve, usando linguagem imperativa em português.";
31
-
32
- const body = {
33
- model: "gpt-4-turbo",
34
- messages: [
35
- { role: "system", content: systemPrompt },
36
- { role: "user", content: prompt }
37
- ],
38
- temperature: 0.2
39
- };
40
-
41
- const response = await fetch(url, {
42
- method: "POST",
43
- headers: {
44
- "Content-Type": "application/json",
45
- "Authorization": `Bearer ${OPENAI_API_KEY}`
46
- },
47
- body: JSON.stringify(body)
48
- });
49
-
50
- if (!response.ok) {
51
- throw new Error(`Erro na API OpenAI: ${response.statusText}`);
20
+ // Função para instalar o 'cody' automaticamente
21
+ function installCody(): void {
22
+ console.log(chalk.blue('🚀 Instalando o cody automaticamente...'));
23
+ try {
24
+ execSync('npm i -g @sourcegraph/cody', { stdio: 'inherit' });
25
+ console.log(chalk.green('✅ Cody instalado com sucesso!'));
26
+ } catch (error) {
27
+ console.error(chalk.red('❌ Erro ao instalar o Cody:'), (error as Error).message);
28
+ process.exit(1);
52
29
  }
53
-
54
- const data = await response.json();
55
- // Retorna a resposta do primeiro "choice".
56
- return data.choices[0].message.content.trim();
57
30
  }
58
31
 
59
- /**
60
- * Divide o diff em chunks menores com base na contagem de tokens.
61
- * Utiliza o gpt-tokenizer para garantir que cada chunk não exceda o limite de tokens.
62
- * @param diff O diff completo em formato de string.
63
- * @param maxTokens Quantidade máxima de tokens permitida para cada chunk (padrão: 1000 tokens).
64
- * @returns Array de strings, cada uma representando um chunk.
65
- */
66
- export function chunkDiff(diff: string, maxTokens: number = 1000): string[] {
67
- // Codifica o diff para obter o array de tokens.
68
- const tokens = encode(diff);
32
+ // Verifica se o 'cody' está instalado, caso contrário, instala
33
+ if (!isCodyInstalled()) {
34
+ installCody();
35
+ }
69
36
 
70
- // Se o diff couber em um único chunk, retorna-o diretamente.
71
- if (tokens.length <= maxTokens) {
72
- return [diff];
37
+ // Função para verificar se está logado no cody
38
+ function isCodyLoggedIn(): boolean {
39
+ try {
40
+ execSync('cody auth whoami', { stdio: 'ignore' });
41
+ return true;
42
+ } catch (error) {
43
+ return false;
73
44
  }
45
+ }
74
46
 
75
- const chunks: string[] = [];
76
-
77
- // Percorre os tokens de forma que cada chunk contenha no máximo maxTokens tokens.
78
- for (let i = 0; i < tokens.length; i += maxTokens) {
79
- const chunkTokens = tokens.slice(i, i + maxTokens);
80
- const chunkText = decode(chunkTokens);
81
- chunks.push(chunkText);
47
+ // Verificar se o usuário está logado no Cody, se não estiver, roda o comando para logar
48
+ if (!isCodyLoggedIn()) {
49
+ console.log(chalk.blue('🔑 Realize o login no Cody...'));
50
+ try {
51
+ execSync('cody auth login --web', { stdio: 'inherit' });
52
+ } catch (error) {
53
+ console.error(chalk.red('❌ Erro ao realizar o login no Cody:'), (error as Error).message);
54
+ process.exit(1);
82
55
  }
83
-
84
- return chunks;
85
56
  }
86
57
 
87
-
88
- // Pré-prompt para a geração da mensagem de commit conforme as convenções
89
- const COMMIT_PROMPT = `
58
+ // Define o prompt do Cody para geração da mensagem de commit
59
+ const CODY_PROMPT = `
90
60
  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/.
91
61
  A mensagem deve começar com um tipo de commit, como:
92
62
  feat: para novas funcionalidades
@@ -116,96 +86,59 @@ Use sempre linguagem imperativa e primeira pessoa do singular, como:
116
86
  Lembre-se: os textos fora do Conventional Commit devem ser em português.
117
87
  `;
118
88
 
119
- async function main(): Promise<void> {
120
- // Verifica se o diretório é um repositório git.
121
- try {
122
- execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
123
- } catch {
124
- console.error(chalk.red('❌ Este diretório não é um repositório git.'));
125
- process.exit(1);
126
- }
89
+ interface CommitAction {
90
+ action: 'confirm' | 'edit' | 'cancel';
91
+ }
127
92
 
128
- // Verifica se alterações staged, desconsiderando arquivos .lock
129
- let stagedFiles: string;
130
- try {
131
- stagedFiles = execSync(
132
- 'git diff --cached --name-only -- . ":(exclude)*.lock"',
133
- { encoding: 'utf8' }
134
- ).toString().trim();
135
- if (!stagedFiles) {
136
- console.log(chalk.yellow('⚠️ Não há alterações staged para o commit.'));
137
- process.exit(0);
138
- }
139
- } catch (error) {
140
- console.error(chalk.red('❌ Erro ao verificar alterações staged:'), error);
141
- process.exit(1);
142
- }
93
+ async function ccm(): Promise<void> {
94
+ const prompt = inquirer.createPromptModule();
143
95
 
144
- // Obtém o diff completo das alterações staged, ignorando arquivos .lock
145
- let diff: string;
96
+ // Verifica se o repositório git está inicializado
146
97
  try {
147
- diff = execSync(
148
- 'git diff --cached -- . ":(exclude)*.lock"',
149
- { encoding: 'utf8' }
150
- );
98
+ console.log(chalk.blue('🔄 Verificando se o diretório é um repositório git...'));
99
+ execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
151
100
  } catch (error) {
152
- console.error(chalk.red('❌ Erro ao obter o diff:'), error);
153
- process.exit(1);
101
+ console.error(chalk.red('❌ Este diretório não é um repositório git.'));
102
+ return;
154
103
  }
155
104
 
156
- // Divide o diff em chunks com base no número máximo de tokens.
157
- const MAX_TOKENS = 1000;
158
- const chunks = chunkDiff(diff, MAX_TOKENS);
159
- let inputForCommit: string;
160
-
161
- if (chunks.length === 1) {
162
- inputForCommit = chunks[0];
163
- } else {
164
- // Se houver vários chunks, gera um resumo para todos eles utilizando um único spinner.
165
- const partialSummaries: string[] = [];
166
- const chunkSummaryPrefix =
167
- "A partir do diff abaixo, extraia um resumo breve das alterações (use linguagem imperativa e em português):";
168
-
169
- // Inicia um spinner único para todo o processo
170
- const spinnerSummary = ora("Gerando resumo do commit.").start();
171
-
172
- try {
173
- for (const chunk of chunks) {
174
- const prompt = `${chunkSummaryPrefix}\n\n${chunk}`;
175
- const summary = await callOpenAI(prompt, 'resumo');
176
- partialSummaries.push(summary);
177
- }
178
- spinnerSummary.succeed("Resumo do commit gerado.");
179
- } catch (error) {
180
- spinnerSummary.fail("Erro ao gerar resumo do commit.");
181
- console.error(chalk.red('❌ Erro ao gerar resumo para o commit:'), error);
182
- process.exit(1);
183
- }
184
- inputForCommit = partialSummaries.join("\n\n");
105
+ // Verifica se alterações staged
106
+ const stagedChanges = execSync('git diff --cached --name-only').toString().trim();
107
+ if (!stagedChanges) {
108
+ console.log(chalk.yellow('⚠️ Não há alterações staged para o commit.'));
109
+ return;
185
110
  }
186
111
 
187
- // Gera a mensagem de commit com o pré-prompt e o diff (ou seus resumos).
188
- const finalPrompt = `${COMMIT_PROMPT}\n\nDiff:\n\n${inputForCommit}`;
189
- const spinnerCommit = ora('Gerando mensagem de commit com base no diff...').start();
112
+ // Cria arquivos temporários para armazenar o prompt e o diff
113
+ const tempPromptPath = path.join(os.tmpdir(), 'CODY_PROMPT.txt');
114
+ const tempDiffPath = path.join(os.tmpdir(), 'CODY_DIFF.patch');
115
+ fs.writeFileSync(tempPromptPath, CODY_PROMPT);
116
+ fs.writeFileSync(tempDiffPath, execSync("git diff --cached --ignore-all-space | grep '^[+-]'").toString());
190
117
 
118
+ // Gera a mensagem do commit usando o diff salvo no arquivo temporário
191
119
  let generatedMessage: string;
192
120
  try {
193
- generatedMessage = await callOpenAI(finalPrompt, 'commit');
194
- // Remove os delimitadores de bloco de código (```)
195
- generatedMessage = generatedMessage.replace(/```/g, '').trim();
196
- spinnerCommit.succeed('Mensagem de commit gerada com sucesso.');
121
+ console.log(chalk.blue.bold('⌛ Gerando mensagem de commit com o Cody...'));
122
+ const response = execSync(
123
+ `cody chat --context-file ${tempDiffPath} --stdin -m "$(cat ${tempPromptPath})"`
124
+ ).toString();
125
+
126
+ // Extrai o bloco de código delimitado por ``` usando regex
127
+ const match = response.match(/```([\s\S]*?)```/);
128
+ generatedMessage = match ? match[1].trim() : response.trim();
129
+
130
+ console.log(chalk.greenBright('\n✨ Mensagem de commit gerada automaticamente:'));
131
+ console.log(chalk.yellowBright(generatedMessage));
197
132
  } catch (error) {
198
- spinnerCommit.fail('Erro ao gerar a mensagem de commit.');
199
- console.error(chalk.red('❌ Erro ao gerar a mensagem de commit:'), error);
200
- process.exit(1);
133
+ console.error(chalk.red('Erro ao gerar mensagem de commit com o Cody:'), (error as Error).message);
134
+ return;
135
+ } finally {
136
+ fs.unlinkSync(tempPromptPath); // Remove o arquivo temporário do prompt
137
+ fs.unlinkSync(tempDiffPath); // Remove o arquivo temporário do diff
201
138
  }
202
139
 
203
- console.log(chalk.greenBright('\n✨ Mensagem de commit gerada automaticamente:'));
204
- console.log(chalk.yellowBright(generatedMessage));
205
-
206
- // Pergunta ao usuário se deseja confirmar, editar ou cancelar o commit.
207
- const promptModule = inquirer.createPromptModule();
208
- const { action } = await promptModule<{ action: 'confirm' | 'edit' | 'cancel' }>([
140
+ // Pergunta ao usuário se ele quer editar, confirmar ou cancelar o commit
141
+ const { action }: CommitAction = await prompt([
209
142
  {
210
143
  type: 'list',
211
144
  name: 'action',
@@ -218,7 +151,7 @@ async function main(): Promise<void> {
218
151
  },
219
152
  ]);
220
153
 
221
- // Cria um arquivo temporário para armazenar a mensagem (para edição se necessário).
154
+ // Caminho temporário para salvar a mensagem gerada
222
155
  const tempFilePath = path.join(os.tmpdir(), 'COMMIT_EDITMSG');
223
156
  fs.writeFileSync(tempFilePath, generatedMessage);
224
157
 
@@ -228,39 +161,39 @@ async function main(): Promise<void> {
228
161
  try {
229
162
  execSync(`${editor} ${tempFilePath}`, { stdio: 'inherit' });
230
163
  } catch (error) {
231
- console.error(chalk.red('❌ Erro ao abrir o editor:'), error);
232
- process.exit(1);
164
+ console.error(chalk.red('❌ Erro ao abrir o editor:'), (error as Error).message);
165
+ return;
233
166
  }
234
167
  } else if (action === 'cancel') {
235
168
  console.log(chalk.yellow('🚫 Commit cancelado pelo usuário.'));
236
169
  fs.unlinkSync(tempFilePath);
237
- process.exit(0);
170
+ return;
238
171
  }
239
172
 
240
- // Lê a mensagem final (após eventual edição).
173
+ // Lê a mensagem do arquivo temporário após a edição
241
174
  const finalMessage = fs.readFileSync(tempFilePath, 'utf8').trim();
175
+
176
+ // Verifica se a mensagem está vazia
242
177
  if (!finalMessage) {
243
- console.error(chalk.red('❌ Nenhuma mensagem inserida, commit cancelado.'));
178
+ console.log(chalk.red('❌ Nenhuma mensagem inserida, commit cancelado.'));
244
179
  fs.unlinkSync(tempFilePath);
245
- process.exit(1);
180
+ return;
246
181
  }
247
182
 
248
- // Captura quaisquer argumentos adicionais passados para o comando.
183
+ // Captura os argumentos adicionais passados ao script
249
184
  const gitArgs = process.argv.slice(2).join(' ');
250
185
  console.log(chalk.blue('🔍 Argumentos adicionais para o commit:'), gitArgs);
251
186
 
252
- // Realiza o commit com a mensagem final.
187
+ // Realiza o commit com a mensagem final e os argumentos adicionais
253
188
  try {
254
- execSync(`git commit -F ${tempFilePath} ${gitArgs}`, { stdio: 'inherit' });
189
+ execSync(`git commit -F ${tempFilePath} ${gitArgs}`);
255
190
  console.log(chalk.green.bold('✅ Commit realizado com sucesso.'));
256
191
  } catch (error) {
257
- console.error(chalk.red('❌ Erro ao realizar o commit:'), error);
192
+ console.error(chalk.red('❌ Erro ao realizar o commit:'), (error as Error).message);
258
193
  } finally {
259
194
  fs.unlinkSync(tempFilePath);
260
195
  }
261
196
  }
262
197
 
263
- main().catch((err) => {
264
- console.error(chalk.red('❌ Erro durante o commit:'), err);
265
- process.exit(1);
266
- });
198
+ // Chama a função principal
199
+ ccm().catch((err) => console.error(chalk.red('❌ Erro durante o commit:'), err));