diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7b6b76 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Dependencies +node_modules/ + +# Lock files +package-lock.json +bun.lock + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Temporary files +*.tmp +*.temp \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e1616ac --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,43 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +GIMS (Git Made Simple) is an AI-powered Git CLI tool that automatically generates meaningful commit messages from code changes. It's a Node.js package published to npm that integrates with OpenAI GPT-4 and Google Gemini APIs. + +## Architecture + +- **Single-file CLI**: All functionality is contained in `bin/gims.js` +- **AI Integration**: Supports both OpenAI and Google Gemini APIs with intelligent fallback strategies +- **Git Wrapper**: Built on top of `simple-git` library for Git operations +- **Token Management**: Implements sophisticated content chunking to handle large diffs within AI token limits + +## Key Components + +- **Command System**: Uses `commander.js` for CLI argument parsing with aliases (e.g., `g o` for `gims online`) +- **AI Message Generation**: Multi-strategy approach that falls back from full diff → summary → status → truncated content +- **Commit Resolution**: Supports both commit hashes and numbered indices for referencing commits +- **Safe Operations**: Includes error handling for empty repositories and edge cases + +## Environment Setup + +Required environment variables (at least one): +- `OPENAI_API_KEY` - For OpenAI GPT-4o-mini integration +- `GEMINI_API_KEY` - For Google Gemini 2.0 Flash integration + +## Common Commands + +- **Install globally**: `npm install -g .` +- **Test locally**: `node bin/gims.js --help` +- **Test specific command**: `node bin/gims.js suggest` +- **Run with alias**: `g o` (after global install) + +## Development Notes + +- No test framework is currently configured (package.json shows placeholder test script) +- Node.js version requirement: >=20.0.0 +- Uses CommonJS modules (`require`/`module.exports`) +- Dependencies are minimal and focused on core functionality +- Token estimation uses 4 characters per token approximation +- Maximum context limit set conservatively at 100,000 tokens \ No newline at end of file diff --git a/README.md b/README.md index f977bd5..0293ac9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![npm version](https://img.shields.io/npm/v/gims.svg)](https://npmjs.org/package/gims) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Node.js Version](https://img.shields.io/node/v/gims.svg)](https://nodejs.org/) - [![AI Powered](https://img.shields.io/badge/AI-Powered-blueviolet.svg)](https://github.com/yourusername/gims) + [![AI Powered](https://img.shields.io/badge/AI-Powered-blueviolet.svg)](https://github.com/s41r4j/gims) **The AI-powered Git CLI that writes your commit messages for you** @@ -34,10 +34,10 @@ g o # AI analyzes changes, commits with perfect message, and pushes! ## 🌟 Features ### 🤖 **AI-Powered Commit Messages** -- **OpenAI GPT-4** integration for intelligent commit message generation -- **Google Gemini** support for lightning-fast analysis +- **OpenAI GPT-4o-mini** integration for intelligent commit message generation +- **Google Gemini 2.0 Flash** support for lightning-fast analysis - Smart diff analysis that understands your code changes -- Handles large codebases with intelligent summarization +- Handles large codebases with intelligent summarization and token management ### ⚡ **Lightning Fast Workflow** - **One command commits**: `g o` - analyze, commit, and push in seconds @@ -95,6 +95,7 @@ g o | `gims init` | `g i` | Initialize new Git repo | `g i` | | `gims clone ` | `g c` | Clone repository | `g c https://github.com/user/repo` | | `gims suggest` | `g s` | Generate & copy commit message | `g s` | +| `gims commit` | `g cm` | Interactive commit message generation | `g cm` | | `gims local` | `g l` | AI commit locally | `g l` | | `gims online` | `g o` | AI commit + push | `g o` | | `gims pull` | `g p` | Pull latest changes | `g p` | @@ -144,7 +145,7 @@ g p # Pull latest changes g s # Preview AI suggestion g l # Commit locally first # ... test your changes ... -g push # Push when ready +g o # Push with AI commit message ``` ### 🧠 **Smart Branching** @@ -180,6 +181,7 @@ GIMS handles edge cases gracefully: - **📊 Massive changes**: Falls back to status-based analysis - **🛜 No API key**: Uses sensible default messages - **⚠️ API failures**: Graceful degradation with helpful errors +- **🪙 Token limits**: Intelligent content chunking with 100K token limit ## 🤝 Contributing @@ -194,8 +196,8 @@ We love contributions! Here's how to get involved: ### 🐛 Found a Bug? -1. Check [existing issues](https://github.com/yourusername/gims/issues) -2. Create a [new issue](https://github.com/yourusername/gims/issues/new) with: +1. Check [existing issues](https://github.com/s41r4j/gims/issues) +2. Create a [new issue](https://github.com/s41r4j/gims/issues/new) with: - Clear description - Steps to reproduce - Expected vs actual behavior @@ -248,7 +250,7 @@ MIT © [GIMS](https://github.com/s41r4j/gims) **⭐ Star this repo if GIMS makes your Git workflow awesome!** -[Report Bug](https://github.com/yourusername/gims/issues) • [Request Feature](https://github.com/yourusername/gims/issues) • [Documentation](https://github.com/yourusername/gims/wiki) +[Report Bug](https://github.com/s41r4j/gims/issues) • [Request Feature](https://github.com/s41r4j/gims/issues) • [Documentation](https://github.com/s41r4j/gims#readme) *Made with ❤️ by developers who hate writing commit messages* diff --git a/bin/gims.js b/bin/gims.js index d23d76e..31f3a00 100644 --- a/bin/gims.js +++ b/bin/gims.js @@ -5,7 +5,6 @@ */ const { Command } = require('commander'); const simpleGit = require('simple-git'); -const clipboard = require('clipboardy'); const process = require('process'); const { OpenAI } = require('openai'); const { GoogleGenAI } = require('@google/genai'); @@ -116,6 +115,11 @@ async function generateCommitMessage(rawDiff) { return 'Update multiple files'; } + // Check if API key is available + if (!process.env.GEMINI_API_KEY && !process.env.OPENAI_API_KEY) { + return null; // Signal that no API key is available + } + let message = 'Update project code'; // Default fallback try { @@ -125,13 +129,13 @@ async function generateCommitMessage(rawDiff) { model: 'gemini-2.0-flash', contents: prompt }); - message = (await res.response.text()).trim(); + message = res.text.trim(); } else if (process.env.OPENAI_API_KEY) { const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const res = await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }], - temperature: 0.5, + temperature: 0.75, max_tokens: 100 // Limit response length }); message = res.choices[0].message.content.trim(); @@ -176,7 +180,7 @@ program.command('clone ').alias('c') }); program.command('suggest').alias('s') - .description('Suggest commit message and copy to clipboard') + .description('Suggest commit message') .action(async () => { if (!(await hasChanges())) { return console.log('No changes to suggest.'); @@ -185,23 +189,92 @@ program.command('suggest').alias('s') const { all } = await safeLog(); const isFirst = all.length === 0; - // Always add changes first - await git.add('.'); - - // Get the appropriate diff - const rawDiff = await git.diff(['--cached']); + // Get diff of unstaged changes + let rawDiff = await git.diff(); + // If no diff from tracked files, check for untracked files if (!rawDiff.trim()) { - return console.log('No changes to suggest.'); + const status = await git.status(); + if (status.not_added.length > 0) { + // For untracked files, show file list since we can't diff them + rawDiff = `New files:\n${status.not_added.join('\n')}`; + } else { + return console.log('No changes to suggest.'); + } } const msg = await generateCommitMessage(rawDiff); - try { - clipboard.writeSync(msg); - console.log(`Suggested: "${msg}" (copied to clipboard)`); - } catch (error) { - console.log(`Suggested: "${msg}" (clipboard copy failed)`); + if (msg === null) { + return console.log('Please set GEMINI_API_KEY or OPENAI_API_KEY environment variable'); + } + + console.log(`git add . && git commit -m "${msg}"`); + }); + +program.command('commit').alias('cm') + .description('Interactive commit message generation') + .action(async () => { + if (!(await hasChanges())) { + return console.log('No changes to commit.'); + } + + // Get diff for message generation + let rawDiff = await git.diff(); + + // If no diff from tracked files, check for untracked files + if (!rawDiff.trim()) { + const status = await git.status(); + if (status.not_added.length > 0) { + rawDiff = `New files:\n${status.not_added.join('\n')}`; + } else { + return console.log('No changes to commit.'); + } + } + + if (!process.env.GEMINI_API_KEY && !process.env.OPENAI_API_KEY) { + return console.log('Please set GEMINI_API_KEY or OPENAI_API_KEY environment variable'); + } + + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + const askForInput = () => { + return new Promise((resolve) => { + rl.question('> ', (answer) => { + resolve(answer.toLowerCase().trim()); + }); + }); + }; + + let currentMessage = await generateCommitMessage(rawDiff); + console.log(`\n=== Interactive Commit ===`); + console.log(`┌──────────────────────────────────────────────────────────────────────┐`); + console.log(`│ Press "Enter" to generate new message, "c" to commit, or "q" to quit │`); + console.log(`└──────────────────────────────────────────────────────────────────────┘\n`); + console.log(`Suggested: "${currentMessage}"`); + + while (true) { + const input = await askForInput(); + + if (input === 'q' || input === 'quit') { + console.log('Cancelled.'); + rl.close(); + return; + } else if (input === 'c' || input === 'commit') { + await git.add('.'); + await git.commit(currentMessage); + console.log(`Committed: "${currentMessage}"`); + rl.close(); + return; + } else { + // Generate new message + currentMessage = await generateCommitMessage(rawDiff); + console.log(`Suggested: "${currentMessage}"`); + } } }); @@ -226,6 +299,11 @@ program.command('local').alias('l') } const msg = await generateCommitMessage(rawDiff); + + if (msg === null) { + return console.log('Please set GEMINI_API_KEY or OPENAI_API_KEY environment variable'); + } + await git.commit(msg); console.log(`Committed locally: "${msg}"`); }); @@ -251,6 +329,11 @@ program.command('online').alias('o') } const msg = await generateCommitMessage(rawDiff); + + if (msg === null) { + return console.log('Please set GEMINI_API_KEY or OPENAI_API_KEY environment variable'); + } + await git.commit(msg); await git.push(); console.log(`Committed & pushed: "${msg}"`); @@ -302,4 +385,4 @@ program.command('revert ').alias('rv') catch (e) { console.error('Revert error:', e.message); } }); -program.parse(process.argv); \ No newline at end of file +program.parse(process.argv);