@vedanshsharma/commit-gen 1.0.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 ADDED
@@ -0,0 +1,80 @@
1
+ # 🚀 Commit Message Generator (commitgen)
2
+
3
+ A lightning-fast, offline-first CLI tool that reads your staged git diffs and automatically generates professional Conventional Commits using locally running LLMs (via Ollama).
4
+
5
+ *(Replace this text with a Demo GIF once recorded)*
6
+
7
+ ## ✨ Why this tool?
8
+
9
+ - **100% Offline & Private:** Uses local LLMs. Your source code never leaves your machine.
10
+ - **Zero API Costs:** No OpenAI/Anthropic API keys needed.
11
+ - **Professional History:** Strictly adheres to the [Conventional Commits](https://www.conventionalcommits.org/) specification.
12
+ - **Lightning Fast:** Generates and commits in seconds without disrupting your terminal flow.
13
+
14
+ ## 🛠 Prerequisites
15
+
16
+ 1. **Node.js** (v18+ recommended for native `fetch`).
17
+ 2. **Ollama** - You must have [Ollama](https://ollama.com/) installed and running locally.
18
+ 3. **LLM Model** - Download your preferred model (default is `mistral`):
19
+ ```bash
20
+ ollama pull mistral
21
+ ```
22
+
23
+ ## 📦 Installation
24
+
25
+ To install this tool globally on your machine so you can use it in any repository:
26
+
27
+ ```bash
28
+ # Clone the repository
29
+ git clone https://github.com/VSDeadShot/commit-gen.git
30
+ cd commit-gen
31
+
32
+ # Install dependencies and link globally
33
+ npm install
34
+ npm link
35
+ ```
36
+
37
+ ## 🚀 Usage
38
+
39
+ Whenever you have changes ready to commit:
40
+
41
+ 1. Stage your files as usual:
42
+ ```bash
43
+ git add <files>
44
+ ```
45
+
46
+ 2. Run the CLI:
47
+ ```bash
48
+ commitgen
49
+ ```
50
+
51
+ 3. The AI-generated commit message will be presented in a styled box. You can then:
52
+ - **Accept and Commit** instantly
53
+ - **Regenerate** if you want a different variation
54
+ - **Edit Manually** to tweak the generated message inline
55
+ - **Cancel** to abort without committing
56
+
57
+ ### Command Options
58
+
59
+ - **Change Model**: Specify a different Ollama model (defaults to `mistral`).
60
+ ```bash
61
+ commitgen --model llama3.2
62
+ ```
63
+
64
+ - **Dry Run**: Generate and preview the message without actually running `git commit`.
65
+ ```bash
66
+ commitgen --dry-run
67
+ ```
68
+
69
+ - **Gemini Fallback**: Use the Gemini API instead of local Ollama (requires the `GEMINI_API_KEY` environment variable).
70
+ ```bash
71
+ commitgen --gemini
72
+ ```
73
+
74
+ ## 🏗 Tech Stack
75
+
76
+ - **JavaScript / Node.js ES Modules**
77
+ - **Commander.js** - CLI command orchestration
78
+ - **Inquirer.js (Classic)** - Interactive prompts
79
+ - **Chalk** - Premium terminal styling
80
+ - **Native Node Fetch & Child_Process** - Zero bloat system integration
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/index.js';
4
+
5
+ run();
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@vedanshsharma/commit-gen",
3
+ "version": "1.0.0",
4
+ "description": "CLI to generate Conventional Commits using local LLMs or Gemini",
5
+ "bin": {
6
+ "commitgen": "bin/commitgen.js"
7
+ },
8
+ "main": "src/index.js",
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/VSDeadShot/commit-gen.git"
15
+ },
16
+ "keywords": [
17
+ "git",
18
+ "commit",
19
+ "cli",
20
+ "ai",
21
+ "ollama",
22
+ "conventional-commits"
23
+ ],
24
+ "author": "",
25
+ "license": "MIT",
26
+ "type": "module",
27
+ "files": [
28
+ "bin",
29
+ "src",
30
+ "README.md"
31
+ ],
32
+ "bugs": {
33
+ "url": "https://github.com/VSDeadShot/commit-gen/issues"
34
+ },
35
+ "homepage": "https://github.com/VSDeadShot/commit-gen#readme",
36
+ "dependencies": {
37
+ "chalk": "^5.6.2",
38
+ "clipboardy": "^5.3.1",
39
+ "commander": "^15.0.0",
40
+ "inquirer": "^14.0.2"
41
+ }
42
+ }
package/src/config.js ADDED
@@ -0,0 +1,39 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.commitgen');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ const DEFAULT_CONFIG = {
9
+ model: 'mistral'
10
+ };
11
+
12
+ /**
13
+ * Reads the configuration file or returns defaults if it doesn't exist.
14
+ */
15
+ export async function getConfig() {
16
+ try {
17
+ const data = await fs.readFile(CONFIG_FILE, 'utf-8');
18
+ return JSON.parse(data);
19
+ } catch (error) {
20
+ // If file doesn't exist or is invalid, return defaults
21
+ return DEFAULT_CONFIG;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Updates the configuration file with new values.
27
+ */
28
+ export async function setConfig(newConfig) {
29
+ try {
30
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
31
+
32
+ const currentConfig = await getConfig();
33
+ const mergedConfig = { ...currentConfig, ...newConfig };
34
+
35
+ await fs.writeFile(CONFIG_FILE, JSON.stringify(mergedConfig, null, 2));
36
+ } catch (error) {
37
+ throw new Error('Failed to save configuration: ' + error.message);
38
+ }
39
+ }
package/src/gemini.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Sends the prompt to the Gemini API and returns the generated commit message.
3
+ * @param {string} prompt The full prompt containing the diff and instructions
4
+ * @returns {Promise<string>} The generated commit message
5
+ */
6
+ export async function generateCommitMessageGemini(prompt) {
7
+ const apiKey = process.env.GEMINI_API_KEY;
8
+ if (!apiKey) {
9
+ throw new Error('GEMINI_API_KEY environment variable is not set.');
10
+ }
11
+
12
+ try {
13
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`, {
14
+ method: 'POST',
15
+ headers: {
16
+ 'Content-Type': 'application/json',
17
+ },
18
+ body: JSON.stringify({
19
+ contents: [{
20
+ parts: [{ text: prompt }]
21
+ }]
22
+ }),
23
+ });
24
+
25
+ if (!response.ok) {
26
+ const errBody = await response.text();
27
+ throw new Error(`Gemini API returned status ${response.status}: ${errBody}`);
28
+ }
29
+
30
+ const data = await response.json();
31
+
32
+ if (!data.candidates || data.candidates.length === 0) {
33
+ throw new Error('Gemini API returned no candidates.');
34
+ }
35
+
36
+ return data.candidates[0].content.parts[0].text.trim();
37
+ } catch (error) {
38
+ throw new Error('Failed to generate commit message via Gemini: ' + error.message);
39
+ }
40
+ }
package/src/git.js ADDED
@@ -0,0 +1,44 @@
1
+ import { execFile } from 'child_process';
2
+ import util from 'util';
3
+
4
+ const execFilePromise = util.promisify(execFile);
5
+
6
+ /**
7
+ * Checks if the current working directory is inside a git repository.
8
+ * @returns {Promise<boolean>}
9
+ */
10
+ export async function isGitRepo() {
11
+ try {
12
+ await execFilePromise('git', ['rev-parse', '--is-inside-work-tree']);
13
+ return true;
14
+ } catch (error) {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Gets the currently staged git diff.
21
+ * @returns {Promise<string>} The diff output
22
+ */
23
+ export async function getStagedDiff() {
24
+ try {
25
+ const { stdout } = await execFilePromise('git', ['diff', '--staged']);
26
+ return stdout.trim();
27
+ } catch (error) {
28
+ throw new Error('Failed to get staged diff: ' + error.message);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Commits the staged changes with the provided message.
34
+ * @param {string} message The commit message
35
+ * @returns {Promise<string>} stdout from git commit
36
+ */
37
+ export async function commitChanges(message) {
38
+ try {
39
+ const { stdout } = await execFilePromise('git', ['commit', '-m', message]);
40
+ return stdout.trim();
41
+ } catch (error) {
42
+ throw new Error('Failed to commit changes: ' + error.message);
43
+ }
44
+ }
package/src/index.js ADDED
@@ -0,0 +1,112 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { isGitRepo, getStagedDiff, commitChanges } from './git.js';
4
+ import { buildPrompt } from './prompt.js';
5
+ import { generateCommitMessage } from './ollama.js';
6
+ import { generateCommitMessageGemini } from './gemini.js';
7
+ import { displayMessage, promptUserAction, promptManualEdit, promptConfigMenu } from './ui.js';
8
+ import { getConfig, setConfig } from './config.js';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('commitgen')
14
+ .description('CLI to generate Conventional Commits using local LLMs')
15
+ .version('1.0.0');
16
+
17
+ program.command('config')
18
+ .description('Configure default settings')
19
+ .action(async () => {
20
+ try {
21
+ const config = await getConfig();
22
+ console.log(chalk.magenta.bold('\n⚙️ Commitgen Configuration\n'));
23
+ const answers = await promptConfigMenu(config.model);
24
+ await setConfig(answers);
25
+ console.log(chalk.green.bold('\n✅ Configuration saved successfully to ~/.commitgen/config.json!\n'));
26
+ } catch (error) {
27
+ console.log('\n' + chalk.red.bold('❌ Error: ') + error.message);
28
+ process.exit(1);
29
+ }
30
+ });
31
+
32
+ program
33
+ .option('-m, --model <name>', 'Ollama model to use')
34
+ .option('--dry-run', "show generated message but don't commit")
35
+ .option('--gemini', "use Gemini API instead of local Ollama")
36
+ .action(async (options) => {
37
+ try {
38
+ const config = await getConfig();
39
+ const selectedModel = options.model || config.model;
40
+
41
+ // 1. Verify we are in a git repository
42
+ const isRepo = await isGitRepo();
43
+ if (!isRepo) {
44
+ console.log(chalk.red.bold('❌ Error:') + ' You are not inside a git repository.');
45
+ process.exit(1);
46
+ }
47
+
48
+ // 2. Get staged changes
49
+ const diff = await getStagedDiff();
50
+ if (!diff) {
51
+ console.log(chalk.yellow('⚠️ No staged changes found.'));
52
+ console.log(chalk.gray('Run `git add <files>` and try again.'));
53
+ process.exit(0);
54
+ }
55
+
56
+ // 3. Main interactive loop
57
+ while (true) {
58
+ console.log(chalk.gray('\nAnalyzing staged diff and generating message...'));
59
+
60
+ const prompt = buildPrompt(diff);
61
+
62
+ let message;
63
+ if (options.gemini) {
64
+ if (!process.env.GEMINI_API_KEY) {
65
+ console.log(chalk.red.bold('\n❌ Error: ') + 'GEMINI_API_KEY environment variable is missing.');
66
+ console.log(chalk.gray('Set it using: $env:GEMINI_API_KEY="your_key" (PowerShell) or export GEMINI_API_KEY="your_key" (Mac/Linux)\n'));
67
+ process.exit(1);
68
+ }
69
+ message = await generateCommitMessageGemini(prompt);
70
+ } else {
71
+ message = await generateCommitMessage(prompt, selectedModel);
72
+ }
73
+
74
+ displayMessage(message);
75
+
76
+ if (options.dryRun) {
77
+ console.log(chalk.gray('Dry run complete. No changes were committed.\n'));
78
+ process.exit(0);
79
+ }
80
+
81
+ const action = await promptUserAction();
82
+
83
+ if (action === 'accept') {
84
+ await commitChanges(message);
85
+ console.log(chalk.green.bold('\n✅ Successfully committed!'));
86
+ break;
87
+ } else if (action === 'regenerate') {
88
+ // Just continue the loop to regenerate
89
+ continue;
90
+ } else if (action === 'edit') {
91
+ const edited = await promptManualEdit(message);
92
+ if (edited && edited.trim() !== '') {
93
+ await commitChanges(edited.trim());
94
+ console.log(chalk.green.bold('\n✅ Successfully committed with your edits!'));
95
+ } else {
96
+ console.log(chalk.red('\n❌ Commit aborted: Message cannot be empty.'));
97
+ }
98
+ break;
99
+ } else if (action === 'cancel') {
100
+ console.log(chalk.gray('\nProcess cancelled. No changes committed.'));
101
+ break;
102
+ }
103
+ }
104
+ } catch (error) {
105
+ console.log('\n' + chalk.red.bold('❌ Error: ') + error.message);
106
+ process.exit(1);
107
+ }
108
+ });
109
+
110
+ export function run() {
111
+ program.parse(process.argv);
112
+ }
package/src/ollama.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Sends the prompt to the local Ollama instance and returns the generated commit message.
3
+ * @param {string} prompt The full prompt containing the diff and instructions
4
+ * @param {string} model The Ollama model to use (default: 'mistral')
5
+ * @returns {Promise<string>} The generated commit message
6
+ */
7
+ export async function generateCommitMessage(prompt, model = 'mistral') {
8
+ try {
9
+ const response = await fetch('http://localhost:11434/api/generate', {
10
+ method: 'POST',
11
+ headers: {
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ body: JSON.stringify({
15
+ model: model,
16
+ prompt: prompt,
17
+ stream: false, // We want the full response at once
18
+ }),
19
+ });
20
+
21
+ if (!response.ok) {
22
+ throw new Error(`Ollama API returned status ${response.status}: ${response.statusText}`);
23
+ }
24
+
25
+ const data = await response.json();
26
+ return data.response.trim();
27
+ } catch (error) {
28
+ // Handle connection refused specifically to give a friendly error
29
+ if (error.code === 'ECONNREFUSED' || (error.cause && error.cause.code === 'ECONNREFUSED')) {
30
+ throw new Error('Could not connect to Ollama. Please make sure Ollama is running locally on port 11434.');
31
+ }
32
+ throw new Error('Failed to generate commit message: ' + error.message);
33
+ }
34
+ }
package/src/prompt.js ADDED
@@ -0,0 +1,36 @@
1
+ const SYSTEM_PROMPT = `You are a git commit message generator. You follow the Conventional Commits
2
+ specification exactly. Analyze the provided git diff and generate ONE commit
3
+ message. Rules:
4
+ - Format: type(scope): description
5
+ - Type must be one of: feat, fix, refactor, docs, style, test, chore
6
+ - Scope is the main file or module changed (optional but preferred)
7
+ - Description is lowercase, present tense, under 72 characters, no period
8
+ - Return ONLY the commit message, nothing else, no explanation, no markdown
9
+ - Do NOT wrap the output in a code block.`;
10
+
11
+ /**
12
+ * Cleans and truncates the diff to ensure it fits within the context window.
13
+ * @param {string} diff The raw git diff
14
+ * @param {number} maxLength The maximum allowed length for the diff (default 3500)
15
+ * @returns {string} The processed diff
16
+ */
17
+ export function truncateDiff(diff, maxLength = 3500) {
18
+ if (!diff) return '';
19
+
20
+ if (diff.length <= maxLength) {
21
+ return diff;
22
+ }
23
+
24
+ return diff.substring(0, maxLength) + '\n\n... [DIFF TRUNCATED]';
25
+ }
26
+
27
+ /**
28
+ * Builds the final prompt to be sent to the LLM.
29
+ * @param {string} rawDiff The raw git diff from the staged files
30
+ * @returns {string} The complete prompt
31
+ */
32
+ export function buildPrompt(rawDiff) {
33
+ const diff = truncateDiff(rawDiff);
34
+
35
+ return `${SYSTEM_PROMPT}\n\nHere is the git diff:\n\`\`\`diff\n${diff}\n\`\`\`\n\nGenerate the commit message now:`;
36
+ }
package/src/ui.js ADDED
@@ -0,0 +1,70 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+
4
+ /**
5
+ * Displays the generated commit message with a premium, clean aesthetic.
6
+ * @param {string} message The commit message to display
7
+ */
8
+ export function displayMessage(message) {
9
+ console.log('\n' + chalk.magenta.bold('✨ Generated Commit Message ✨'));
10
+ console.log(chalk.gray('─────────────────────────────────────────────'));
11
+ console.log(chalk.cyanBright.bold(message));
12
+ console.log(chalk.gray('─────────────────────────────────────────────\n'));
13
+ }
14
+
15
+ /**
16
+ * Prompts the user for their next action using a styled list.
17
+ * @returns {Promise<string>} The selected action ('accept', 'regenerate', 'edit', 'cancel')
18
+ */
19
+ export async function promptUserAction() {
20
+ const { action } = await inquirer.prompt([
21
+ {
22
+ type: 'select',
23
+ name: 'action',
24
+ message: chalk.white.bold('What would you like to do?'),
25
+ choices: [
26
+ { name: chalk.green('🚀 Accept and Commit'), value: 'accept' },
27
+ { name: chalk.yellow('🔄 Regenerate Message'), value: 'regenerate' },
28
+ { name: chalk.blue('✏️ Edit Manually'), value: 'edit' },
29
+ { name: chalk.red('❌ Cancel Process'), value: 'cancel' }
30
+ ],
31
+ prefix: chalk.magenta('❯') // Overrides the standard '?' prefix
32
+ }
33
+ ]);
34
+ return action;
35
+ }
36
+
37
+ /**
38
+ * Prompts the user to edit the message manually inline.
39
+ * @param {string} currentMessage The default message to pre-fill
40
+ * @returns {Promise<string>} The edited message
41
+ */
42
+ export async function promptManualEdit(currentMessage) {
43
+ const { editedMessage } = await inquirer.prompt([
44
+ {
45
+ type: 'input',
46
+ name: 'editedMessage',
47
+ message: chalk.white.bold('Edit your commit message:'),
48
+ default: currentMessage,
49
+ prefix: chalk.magenta('❯')
50
+ }
51
+ ]);
52
+ return editedMessage;
53
+ }
54
+
55
+ /**
56
+ * Prompts the user to configure their default settings.
57
+ * @param {string} currentModel The current default model
58
+ * @returns {Promise<{model: string}>} The new config answers
59
+ */
60
+ export async function promptConfigMenu(currentModel) {
61
+ return await inquirer.prompt([
62
+ {
63
+ type: 'input',
64
+ name: 'model',
65
+ message: chalk.white.bold('Enter your preferred default Ollama model:'),
66
+ default: currentModel,
67
+ prefix: chalk.magenta('❯')
68
+ }
69
+ ]);
70
+ }