Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
5 changes: 5 additions & 0 deletions .commitrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feat",
"scope": "CLI",
"format": "Conventional"
}
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
OPENAI_API_KEY=
NODE_ENV=
Empty file added CONTRIBUTING.md
Empty file.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Commit AI :rocket:

AI-powered commit message generator powered by OpenAI. Get meaningful commit messages in seconds!

[![npm version](https://img.shields.io/npm/v/commit-ai)](https://www.npmjs.com/package/commit-ai)

## Installation

```bash
npx commit-ai generate # Generate commit message
```
Usage

Generate Commit Message

bash
Copy
npx commit-ai
# or
git gpt # If aliased
Configure OpenAI Key

```bash
npx commit-ai configure
npx commit-ai g // use alias instead
```
Stores your API key securely in `~/.env`

# How It Works

1. Analyzes git staged changes
2. Sends diff to OpenAI API
3. Generates conventional commit message
4. Verifies with user before committing

# Security

Your OpenAI key is stored locally in `~/.env` and never transmitted elsewhere.

# Contributing

PRs welcome! See CONTRIBUTING.md

# License

MIT

39 changes: 39 additions & 0 deletions cli/commands/configure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import inquirer from 'inquirer';
import fs from 'fs';
import path from 'path';

const envFilePath = path.join(process.cwd(), '.env');

function updateEnvContent(envContent, key, value) {
const regex = new RegExp(`^${key}=.*$`, 'm');
if (regex.test(envContent)) {
// Replace existing key
return envContent.replace(regex, `${key}=${value}`);
} else {
// Append new key
return envContent ? `${envContent}\n${key}=${value}` : `${key}=${value}`;
}
}

export async function configureOpenAIKey() {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'openaiApiKey',
message: 'Please enter your OpenAI API key:',
validate: input => input ? true : 'API key cannot be empty',
},
]);

const { openaiApiKey } = answers;

let envContent = '';
if (fs.existsSync(envFilePath)) {
envContent = fs.readFileSync(envFilePath, 'utf8');
}

const newEnvContent = updateEnvContent(envContent, 'OPENAI_API_KEY', openaiApiKey);

fs.writeFileSync(envFilePath, newEnvContent);
// console.log('OpenAI API key saved to .env file');
}
16 changes: 16 additions & 0 deletions cli/commands/configureCommand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Command } from 'commander';
import { configureOpenAIKey } from './configure.js';
import { Logger } from '../../src/utils/logger.js';

const command = new Command('configure')
.description('Configure your OpenAI API key')
.action(async () => {
try {
await configureOpenAIKey();
} catch (error) {
console.error('Error configuring API key:', error.message);
Logger
}
});

export default command;
51 changes: 51 additions & 0 deletions cli/commands/generate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Command } from 'commander';
import { fetchGitDiff } from '../../src/services/gitService.js';
import { generateCommitMessage } from '../../src/services/abstractionService.js';
import { showSpinner } from '../../src/utils/spinner.js';
import inquirer from 'inquirer';
import { exec } from 'child_process';
import { formatCommitMessage } from '../../src/utils/format.js';

const command = new Command('generate')
.alias('g')
.description('Generate a commit message based on git diff')
.action(async () => {
const spinner = showSpinner('Fetching git diff...');
try {
const diff = await fetchGitDiff();
spinner.stop();
spinner.text = 'Generating commit message...';

const rawMessage = await generateCommitMessage(diff);
spinner.succeed('Commit message generated successfully!');

const formattedMessage = formatCommitMessage(rawMessage);
console.log(`\nYour Commit Message:\n${formattedMessage}\n\n`);

const { shouldCommit } = await inquirer.prompt([
{
type: 'confirm',
name: 'shouldCommit',
message: 'Do you want to commit with the above message?',
default: true,
},
]);

if (shouldCommit) {
exec(`git commit -m "${formattedMessage.replace(/"/g, '\\"')}"`, (err, stdout, stderr) => {
if (err) {
console.error('Error committing changes:', err);
return;
}
console.log('Commit successful!');
});
} else {
console.log('Commit aborted by the user.');
}
} catch (error) {
spinner.fail('Failed to generate commit message');
console.error(error.message);
}
});

export default command;
20 changes: 20 additions & 0 deletions cli/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Command } from "commander";
import generateCommand from "./commands/generate.js";
import configureCommand from "./commands/configureCommand.js"
import { setupLogger } from "../src/utils/logger.js";
import dotenv from 'dotenv';
dotenv.config();

const program = new Command();
setupLogger();

program
.name("commit-gpt")
.description("Generate AI-crafted commit messages")
// .option("-g, --generate", "Generate a commit message")
.version("1.0.0");

program.addCommand(generateCommand);
program.addCommand(configureCommand)

program.parse(process.argv);
20 changes: 20 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "ai-commit-generator",
"bin": {
"ai-commit": "./cli/index.js"
},
"main": "./cli/index.js",
"type": "module",
"dependencies": {
"axios": "^1.7.9",
"chalk": "5.4.1",
"commander": "13.1.0",
"dotenv": "^16.4.7",
"inquirer": "^12.4.1",
"openai": "4.83.0",
"ora": "8.2.0",
"simple-git": "^3.27.0",
"typescript": "5.7.3",
"winston": "^3.17.0"
}
}
9 changes: 9 additions & 0 deletions src/services/abstractionService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Logger } from '../utils/logger.js';
import openaiGenerator from './generators/openai.js';

Logger.info('Initializing commit message generator');

export async function generateCommitMessage(diff) {
const generator = openaiGenerator
return await generator.generate(diff);
}
111 changes: 111 additions & 0 deletions src/services/aiService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { OpenAI } from 'openai';

import axios from 'axios';
import { Logger } from '../utils/logger.js';

import dotenv from 'dotenv';
dotenv.config();


const openaiApiKey = process.env.OPENAI_API_KEY;
const huggingFaceApiKey = process.env.HUGGINGFACE_API_KEY;

const openai = new OpenAI({
apiKey: openaiApiKey,
});

const huggingFaceEndpoint =
'https://api-inference.huggingface.co/models/VincentMuriuki/legal-summarizer';

export const generateCommitMessage = async (diff)=> {
try {
Logger.info('Using OpenAI to draft a commit message...');
const draft = await generateWithOpenAI(diff);

Logger.info('Using Hugging Face to refine the draft...', draft);
return draft;
// return await refineWithHuggingFace(draft);
} catch (error) {
Logger.error('Initial attempt failed. Trying fallback options...', {
error,
});

try {
Logger.info(
'Fallback: Using Hugging Face to generate a commit message...'
);
const draftFromHuggingFace = await generateWithHuggingFace(diff);
return draftFromHuggingFace;
} catch (fallbackError) {
Logger.error('Both services failed. Returning fallback.', {
fallbackError,
});
return 'Fallback: Could not generate a commit message.';
}
}
};

const refineWithHuggingFace = async (draft) => {
try {
const response = await axios.post(
huggingFaceEndpoint,
{ inputs: `Refine this commit message: "${draft}"` },
{ headers: { Authorization: `Bearer ${huggingFaceApiKey}` } }
);

const refinedMessage = response.data[0]?.generated_text?.trim();

if (!refinedMessage) {
throw new Error('Hugging Face returned an empty refinement.');
}

return refinedMessage;
} catch (error) {
Logger.error('Hugging Face refinement failed:', error);
throw error;
}
};

const generateWithOpenAI = async (diff) => {
try {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini', // Or use "gpt-4" for GPT-4
messages: [
// {
// role: 'system',
// content: 'You are a helpful assistant.',
// },
{
role: 'user',
content: `Kindly help in generating a commit message for the below changes \n\n ${diff}`,
},
],
});

return response.choices[0].message.content;
} catch (error) {
Logger.error('Error generating with OpenAI', error);
throw new Error('Failed to generate response with OpenAI.');
}
};

const generateWithHuggingFace = async (diff) => {
try {
const response = await axios.post(
huggingFaceEndpoint,
{ inputs: diff },
{ headers: { Authorization: `Bearer ${huggingFaceApiKey}` } }
);

const commitMessage = response.data[0]?.generated_text?.trim();

if (!commitMessage) {
throw new Error('Hugging Face returned an empty message.');
}

return commitMessage;
} catch (error) {
Logger.error('Hugging Face Error:', error);
throw error;
}
};
61 changes: 61 additions & 0 deletions src/services/generators/openai.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import inquirer from 'inquirer';
import { OpenAI } from 'openai';
import { Logger } from '../../utils/logger.js';
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
dotenv.config();

const huggingFaceApiKey = process.env.HUGGINGFACE_API_KEY;

async function getApiKey() {
if (!process.env.OPENAI_API_KEY) {
process.stdout.write('\n');
const { openaiApiKey } = await inquirer.prompt([
{
type: 'input',
name: 'openaiApiKey',
message: 'Please enter your OpenAI API key:',
validate: input => (input ? true : 'API key cannot be empty'),
},
]);

process.env.OPENAI_API_KEY = openaiApiKey;

const envFilePath = path.join(process.cwd(), '.env');
let envContent = '';
if (fs.existsSync(envFilePath)) {
envContent = fs.readFileSync(envFilePath, 'utf8');
}
const newEnvContent = envContent.includes('OPENAI_API_KEY')
? envContent.replace(/OPENAI_API_KEY=.*/g, `OPENAI_API_KEY=${openaiApiKey}`)
: envContent + `\nOPENAI_API_KEY=${openaiApiKey}`;
fs.writeFileSync(envFilePath, newEnvContent);
return openaiApiKey;
}
return process.env.OPENAI_API_KEY;
}

export const generateWithOpenAI = async (diff) => {
try {
const apiKey = await getApiKey();
const openai = new OpenAI({ apiKey });
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{
role: 'user',
content: `Kindly help in generating a commit message for the below changes:\n\n${diff}`,
},
],
});
return response.choices[0].message.content;
} catch (error) {
Logger.error('Error generating Commit message:', error);
throw new Error('Failed to generate response with OpenAI.');
}
};

export default {
generate: generateWithOpenAI,
};
Loading