ai-commit-guard
Version:
AI-powered pre-commit code review tool that works with any programming language and framework
984 lines (847 loc) • 31.7 kB
JavaScript
const { execSync } = require('child_process');
const { readFileSync, existsSync, writeFileSync, mkdirSync, chmodSync, appendFileSync } = require('fs');
const chalk = require('chalk');
const crypto = require('crypto');
const path = require('path');
const CONFIG = {
TIMEOUT: {
DEFAULT: 30000,
MIN: 5000,
MAX: 120000
},
FILE_SIZE: {
DEFAULT_MAX: 50000,
MIN: 1000,
MAX: 1000000
},
CACHE_DURATION: 24 * 60 * 60 * 1000,
BINARY_EXTENSIONS: [
'.exe', '.dll', '.so', '.dylib', '.bin', '.dat', '.db', '.sqlite',
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', '.ico',
'.mp3', '.wav', '.flac', '.aac', '.ogg', '.mp4', '.avi', '.mkv', '.mov',
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
'.zip', '.rar', '.tar', '.gz', '.7z', '.bz2',
'.ttf', '.otf', '.woff', '.woff2', '.eot'
],
EXCLUDE_PATTERNS: [
'dist/*', 'build/*', 'out/*', 'target/*', 'bin/*', 'obj/*',
'node_modules/*', 'vendor/*', '.venv/*', 'venv/*', '__pycache__/*',
'.git/*', '.svn/*', '.hg/*',
'.vscode/*', '.idea/*', '*.swp', '*.swo', '*~',
'.DS_Store', 'Thumbs.db', 'desktop.ini',
'*.min.js', '*.min.css', '*.bundle.*', '*-lock.json', '*.lock'
],
FILES: {
RULES: '.code-rules.md',
IGNORE: '.ai-guard-ignore',
CACHE_DIR: '.ai-guard-cache'
},
COMMIT_FLAGS: {
SUCCESS: 'AI-REVIEW-PASSED',
TIMEOUT: 'AI-REVIEW-FAILED-TIMEOUT',
ERROR: 'AI-REVIEW-SKIPPED-ERROR'
}
};
const DEFAULT_IGNORE_PATTERNS = [
'*.env*', '*.key', '*.pem', '*.p12', '*.pfx', '*.keystore', '*.jks',
'*password*', '*secret*', '*token*', '*api-key*', '*private*', '*credential*',
'*.log', '*.tmp', '*.temp', '*.cache', '*.pid',
'package-lock.json', 'yarn.lock', 'composer.lock', 'Gemfile.lock', 'Pipfile.lock',
'*.min.js', '*.min.css', '*.bundle.*', '*-lock.*',
'.ai-guard-cache/*',
'.ai-guard-result',
...CONFIG.EXCLUDE_PATTERNS
];
const SENSITIVE_KEYWORDS = ['password', 'secret', 'token', 'key', 'private', 'credential'];
const AI_PROVIDERS = {
OPENAI: {
name: 'openai',
apiUrl: 'https://api.openai.com/v1/chat/completions',
model: 'gpt-4',
headers: (apiKey) => ({
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}),
payload: (prompt, model) => ({
model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.1,
max_tokens: 1000
}),
extractResponse: (data) => data.choices[0].message.content
},
CLAUDE: {
name: 'claude',
apiUrl: 'https://api.anthropic.com/v1/messages',
model: 'claude-3-sonnet-20240229',
headers: (apiKey) => ({
'x-api-key': apiKey,
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01'
}),
payload: (prompt, model) => ({
model,
max_tokens: 1000,
messages: [{ role: 'user', content: prompt }]
}),
extractResponse: (data) => data.content[0].text
},
GEMINI: {
name: 'gemini',
apiUrl: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent',
model: 'gemini-pro',
headers: (apiKey) => ({
'Content-Type': 'application/json'
}),
payload: (prompt) => ({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
temperature: 0.1,
maxOutputTokens: 1000
}
}),
extractResponse: (data) => data.candidates[0].content.parts[0].text,
buildUrl: (apiKey) => `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}`
},
OLLAMA: {
name: 'ollama',
apiUrl: 'http://localhost:11434/api/generate',
model: 'codellama',
headers: () => ({
'Content-Type': 'application/json'
}),
payload: (prompt, model) => ({
model,
prompt,
stream: false,
options: { temperature: 0.1 }
}),
extractResponse: (data) => data.response
},
COHERE: {
name: 'cohere',
apiUrl: 'https://api.cohere.ai/v1/generate',
model: 'command',
headers: (apiKey) => ({
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}),
payload: (prompt, model) => ({
model,
prompt,
max_tokens: 1000,
temperature: 0.1
}),
extractResponse: (data) => data.generations[0].text
}
};
class AICommitGuard {
constructor() {
this.apiKey = this._getApiKey();
this.provider = this._getProvider();
this.timeout = this._getTimeout();
this.maxFileSize = this._getMaxFileSize();
this.model = this._getModel();
}
_getApiKey() {
return process.env.OPENAI_API_KEY ||
process.env.CLAUDE_API_KEY ||
process.env.ANTHROPIC_API_KEY ||
process.env.GEMINI_API_KEY ||
process.env.GOOGLE_API_KEY ||
process.env.COHERE_API_KEY ||
process.env.AI_API_KEY;
}
_getProvider() {
const envProvider = process.env.AI_PROVIDER?.toLowerCase();
if (envProvider && AI_PROVIDERS[envProvider.toUpperCase()]) {
return envProvider;
}
if (process.env.OPENAI_API_KEY) return 'openai';
if (process.env.CLAUDE_API_KEY || process.env.ANTHROPIC_API_KEY) return 'claude';
if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) return 'gemini';
if (process.env.COHERE_API_KEY) return 'cohere';
try {
const { execSync } = require('child_process');
execSync('curl -s http://localhost:11434/api/tags', { timeout: 1000 });
return 'ollama';
} catch {
}
return 'openai';
}
_getTimeout() {
const timeout = parseInt(process.env.AI_GUARD_TIMEOUT) || CONFIG.TIMEOUT.DEFAULT;
return Math.max(CONFIG.TIMEOUT.MIN, Math.min(CONFIG.TIMEOUT.MAX, timeout));
}
_getMaxFileSize() {
const size = parseInt(process.env.AI_GUARD_MAX_FILE_SIZE) || CONFIG.FILE_SIZE.DEFAULT_MAX;
return Math.max(CONFIG.FILE_SIZE.MIN, Math.min(CONFIG.FILE_SIZE.MAX, size));
}
_getModel() {
const provider = this.provider.toUpperCase();
return process.env.AI_MODEL ||
process.env[`${provider}_MODEL`] ||
AI_PROVIDERS[provider].model;
}
_logInfo(message) {
console.log(chalk.blue('ℹ️ ') + chalk.white(message));
}
_logSuccess(message) {
console.log(chalk.green('✅ ') + chalk.white(message));
}
_logWarning(message) {
console.log(chalk.yellow('⚠️ ') + chalk.white(message));
}
_logError(message) {
console.log(chalk.red('❌ ') + chalk.white(message));
}
_logAI(message) {
console.log(chalk.magenta('🤖 ') + chalk.cyan(message));
}
_formatAIResponse(response) {
const lines = response.split('\n');
return lines.map(line => {
const trimmedLine = line.trim();
if (!trimmedLine) return '';
if (trimmedLine.startsWith('•') || trimmedLine.startsWith('-') || trimmedLine.match(/^\d+\./)) {
return chalk.yellow(' ' + trimmedLine);
} else if (trimmedLine.includes('Fix:') || trimmedLine.includes('Solution:') || trimmedLine.includes('Improvement:')) {
return chalk.green(' ' + trimmedLine);
} else if (trimmedLine.includes('Issue:') || trimmedLine.includes('Problem:') || trimmedLine.includes('Error:')) {
return chalk.red(' ' + trimmedLine);
} else if (trimmedLine.includes('Great job') || trimmedLine.includes('excellent') || trimmedLine.includes('good')) {
return chalk.green(' ' + trimmedLine);
} else {
return chalk.white(' ' + trimmedLine);
}
}).join('\n');
}
loadIgnorePatterns() {
const patterns = [...DEFAULT_IGNORE_PATTERNS];
if (existsSync(CONFIG.FILES.IGNORE)) {
try {
const customPatterns = readFileSync(CONFIG.FILES.IGNORE, 'utf8')
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
patterns.push(...customPatterns);
} catch (error) {
this._logWarning(`Could not read ${CONFIG.FILES.IGNORE}: ${error.message}`);
}
}
return patterns;
}
shouldIgnoreFile(filePath, patterns) {
const fileName = path.basename(filePath);
const lowerPath = filePath.toLowerCase();
const lowerName = fileName.toLowerCase();
if (SENSITIVE_KEYWORDS.some(keyword =>
lowerPath.includes(keyword) || lowerName.includes(keyword))) {
return true;
}
return patterns.some(pattern => {
if (pattern.includes('*')) {
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
return regex.test(filePath) || regex.test(fileName);
}
return filePath.includes(pattern) || fileName.includes(pattern);
});
}
getStagedFiles() {
try {
const output = execSync('git diff --cached --name-only', { encoding: 'utf8' });
const allFiles = output.trim().split('\n').filter(file => {
if (!file) return false;
const ext = path.extname(file).toLowerCase();
if (CONFIG.BINARY_EXTENSIONS.includes(ext)) {
return false;
}
return true;
});
const ignorePatterns = this.loadIgnorePatterns();
const filteredFiles = allFiles.filter(file => !this.shouldIgnoreFile(file, ignorePatterns));
const nonBinaryFiles = filteredFiles.filter(file => {
try {
const result = execSync(`git diff --cached --numstat "${file}"`, { encoding: 'utf8' });
return !result.startsWith('-\t-\t');
} catch {
return true;
}
});
const ignoredCount = allFiles.length - nonBinaryFiles.length;
if (ignoredCount > 0) {
this._logInfo(`🔒 Ignored ${ignoredCount} binary/sensitive/excluded files`);
}
return nonBinaryFiles;
} catch (error) {
this._logWarning('Could not get staged files');
return [];
}
}
getChanges(files) {
return files.map(file => {
try {
const diff = execSync(`git diff --cached "${file}"`, { encoding: 'utf8' });
if (diff.length > this.maxFileSize) {
this._logWarning(`${file} is too large (${(diff.length/1024).toFixed(1)}KB), skipping detailed review`);
return `--- ${file} ---\n[File too large for AI review - ${(diff.length/1024).toFixed(1)}KB]`;
}
const filteredDiff = this.filterSensitiveContent(diff);
return `--- ${file} ---\n${filteredDiff}`;
} catch (error) {
this._logWarning(`Could not get diff for ${file}: ${error.message}`);
return '';
}
}).filter(change => change).join('\n\n');
}
filterSensitiveContent(content) {
return content
.replace(/(['"`])sk-[a-zA-Z0-9]{32,}(['"`])/g, '$1[API_KEY_HIDDEN]$2')
.replace(/(['"`])[a-zA-Z0-9]{32,}(['"`])/g, (match, quote1, quote2) => {
const lowerMatch = match.toLowerCase();
if (SENSITIVE_KEYWORDS.some(keyword => lowerMatch.includes(keyword))) {
return `${quote1}[SECRET_HIDDEN]${quote2}`;
}
return match;
})
.replace(/password\s*[:=]\s*['"`][^'"`]+['"`]/gi, 'password: "[PASSWORD_HIDDEN]"')
.replace(/token\s*[:=]\s*['"`][^'"`]+['"`]/gi, 'token: "[TOKEN_HIDDEN]"')
.replace(/(secret|key)\s*[:=]\s*['"`][^'"`]+['"`]/gi, '$1: "[SECRET_HIDDEN]"');
}
loadRules() {
if (!existsSync(CONFIG.FILES.RULES)) {
return `
# Universal Code Review Rules
## Code Quality & Maintainability
- Use meaningful and descriptive names for variables, functions, and classes
- Keep functions focused and concise (prefer under 20-30 lines)
- Avoid magic numbers and strings - use named constants or configuration
- Remove unused variables, imports, and dead code
- Add appropriate comments for complex logic and business rules
## Best Practices
- Follow consistent naming conventions for your language/framework
- Handle errors appropriately - don't ignore or suppress them silently
- Validate inputs and handle edge cases
- Use proper logging instead of debug print statements
- Follow the DRY principle - avoid code duplication
## Security & Safety
- Never commit sensitive data (passwords, API keys, tokens, credentials)
- Validate and sanitize all user inputs
- Use secure coding practices appropriate for your language
- Don't hardcode configuration values that should be external
- Be careful with file permissions and access controls
## Structure & Organization
- Organize code logically with proper separation of concerns
- Use consistent indentation and formatting
- Keep related code together
- Make sure public APIs are well documented
- Consider backward compatibility when making changes
## Testing & Reliability
- Include appropriate error handling and recovery
- Consider edge cases and boundary conditions
- Make sure changes don't break existing functionality
- Add tests for new features when applicable
- Use meaningful commit messages that explain the "why" not just the "what"
Review all code changes regardless of programming language, markup, configuration, or documentation files.
`.trim();
}
try {
return readFileSync(CONFIG.FILES.RULES, 'utf8');
} catch (error) {
this._logWarning(`Could not read ${CONFIG.FILES.RULES}: ${error.message}`);
return this.loadRules();
}
}
async callWithTimeout(apiCall, timeoutMs) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`AI review timeout after ${timeoutMs/1000} seconds`));
}, timeoutMs);
apiCall()
.then(result => {
clearTimeout(timer);
resolve(result);
})
.catch(error => {
clearTimeout(timer);
reject(error);
});
});
}
async callAI(prompt) {
const providerConfig = AI_PROVIDERS[this.provider.toUpperCase()];
if (!providerConfig) {
throw new Error(`Unsupported AI provider: ${this.provider}`);
}
const apiCall = async () => {
let url = providerConfig.apiUrl;
let headers = providerConfig.headers(this.apiKey);
let payload = providerConfig.payload(prompt, this.model);
if (this.provider === 'gemini' && providerConfig.buildUrl) {
url = providerConfig.buildUrl(this.apiKey);
headers = providerConfig.headers();
}
if (this.provider === 'ollama') {
headers = providerConfig.headers();
}
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed (${response.status}): ${errorText}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error.message || JSON.stringify(data.error));
}
return providerConfig.extractResponse(data);
};
return await this.callWithTimeout(apiCall, this.timeout);
}
createPrompt(changes, rules) {
return `
You are an expert code reviewer. Please review the following code changes against the established rules and best practices.
## Review Guidelines:
${rules}
## Code Changes to Review:
${changes}
## Instructions:
- Review ALL changes regardless of programming language (JavaScript, Python, Go, Rust, Java, C++, HTML, CSS, YAML, JSON, Markdown, configuration files, etc.)
- Look for code quality issues, potential bugs, security concerns, and adherence to best practices
- Consider the context and purpose of the changes
- Be constructive and specific in your feedback
## Response Format:
If there are issues that should prevent the commit:
- Start response with "REJECT"
- List specific problems with file names and locations when possible
- Provide clear, actionable solutions for each issue
- Prioritize critical issues (security, bugs) over style issues
If the code meets standards:
- Start response with "APPROVE"
- Optionally suggest minor improvements or praise good practices
- Keep suggestions brief and constructive
## Examples of what to look for:
- Security vulnerabilities (exposed secrets, injection risks, unsafe operations)
- Logic errors and potential bugs
- Performance issues or inefficient patterns
- Maintainability concerns (unclear naming, complex logic)
- Missing error handling
- Configuration or documentation issues
- Inconsistent formatting or style (only if severe)
Focus on functionality, security, and maintainability over minor style preferences.
`.trim();
}
getCacheKey(changes) {
const hash = crypto.createHash('sha256').update(changes + this.provider + this.model).digest('hex');
return hash.substring(0, 16);
}
getCache(key) {
try {
const cacheFile = path.join(CONFIG.FILES.CACHE_DIR, `${key}.json`);
if (existsSync(cacheFile)) {
const data = JSON.parse(readFileSync(cacheFile, 'utf8'));
if (Date.now() - data.timestamp < CONFIG.CACHE_DURATION) {
return data.result;
}
}
} catch (error) {
}
return null;
}
setCache(key, result) {
try {
const { mkdirSync, writeFileSync } = require('fs');
if (!existsSync(CONFIG.FILES.CACHE_DIR)) {
mkdirSync(CONFIG.FILES.CACHE_DIR, { recursive: true });
}
const cacheFile = path.join(CONFIG.FILES.CACHE_DIR, `${key}.json`);
const cacheData = {
result,
timestamp: Date.now(),
provider: this.provider,
model: this.model
};
writeFileSync(cacheFile, JSON.stringify(cacheData, null, 2));
} catch (error) {
this._logWarning(`Could not write cache: ${error.message}`);
}
}
async run() {
try {
if (process.argv.includes('--setup')) {
return this._runSetup();
}
if (process.argv.includes('--commit-msg')) {
return this._handleCommitMsg();
}
if (process.argv.includes('--version') || process.argv.includes('-v')) {
return this._showVersion();
}
if (process.argv.includes('--help') || process.argv.includes('-h')) {
return this._showHelp();
}
if (!this.apiKey && this.provider !== 'ollama') {
this._logWarning('No AI API key found. Please set appropriate API key environment variable');
this._logInfo('Supported providers: OpenAI, Claude, Gemini, Cohere, Ollama');
this._storeReviewResult('ERROR');
process.exit(0);
return;
}
this._logInfo('🔍 Checking staged files...');
const stagedFiles = this.getStagedFiles();
if (stagedFiles.length === 0) {
this._logSuccess('No relevant files to review');
process.exit(0);
}
this._logInfo(`📝 Reviewing ${stagedFiles.length} files using ${this.provider.toUpperCase()}...`);
const changes = this.getChanges(stagedFiles);
if (!changes.trim()) {
this._logWarning('No meaningful changes to review');
process.exit(0);
}
const cacheKey = this.getCacheKey(changes);
let result = this.getCache(cacheKey);
if (result) {
this._logInfo('📦 Using cached result');
} else {
this._logAI(`Sending to ${this.provider.toUpperCase()} for review (timeout: ${this.timeout/1000}s)...`);
try {
const rules = this.loadRules();
const prompt = this.createPrompt(changes, rules);
result = await this.callAI(prompt);
this.setCache(cacheKey, result);
} catch (timeoutError) {
if (timeoutError.message.includes('timeout')) {
this._logWarning(`AI review timed out after ${this.timeout/1000} seconds`);
this._storeReviewResult('TIMEOUT');
process.exit(0);
return;
}
throw timeoutError;
}
}
if (result.toLowerCase().startsWith('reject')) {
this._logError('Code review failed!');
console.log(this._formatAIResponse(result.replace(/^reject/i, '').trim()));
process.exit(1);
} else {
this._logSuccess('Code review passed!');
this._storeReviewResult('SUCCESS');
process.exit(0);
}
} catch (error) {
this._logWarning(`Review failed: ${error.message}`);
this._storeReviewResult('ERROR');
process.exit(0);
}
}
_runSetup() {
try {
console.log('🚀 Setting up AI Commit Guard hooks...\n');
try {
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
} catch (error) {
console.log('❌ Not a git repository. Please run this in a git repository.');
console.log('💡 Initialize git first: git init');
process.exit(1);
}
if (!existsSync('.husky')) {
console.log('📦 Installing husky...');
if (!existsSync('package.json')) {
console.log('📝 Creating package.json...');
execSync('npm init -y', { stdio: 'inherit' });
}
try {
execSync('npm install --save-dev husky', { stdio: 'inherit' });
execSync('npx husky init', { stdio: 'inherit' });
console.log('✅ Husky installed successfully\n');
} catch (error) {
console.log('⚠️ Could not install husky automatically. Please install manually:');
console.log(' npm install --save-dev husky');
console.log(' npx husky init\n');
}
}
if (!existsSync('.husky')) {
mkdirSync('.husky', { recursive: true });
}
console.log('🔍 Setting up pre-commit hook...');
const preCommitHook = `npx ai-commit-guard`;
writeFileSync('.husky/pre-commit', preCommitHook);
if (process.platform !== 'win32') {
try {
chmodSync('.husky/pre-commit', '755');
} catch (error) {
}
}
console.log('📝 Setting up commit-msg hook...');
const commitMsgHook = `npx ai-commit-guard --commit-msg "$1"`;
writeFileSync('.husky/commit-msg', commitMsgHook);
if (process.platform !== 'win32') {
try {
chmodSync('.husky/commit-msg', '755');
} catch (error) {
}
}
console.log('📁 Updating .gitignore...');
const gitignoreEntries = `
# AI Commit Guard
.ai-guard-cache/
.ai-guard-result
`;
let gitignoreContent = '';
if (existsSync('.gitignore')) {
gitignoreContent = readFileSync('.gitignore', 'utf8');
}
if (!gitignoreContent.includes('.ai-guard-cache/')) {
appendFileSync('.gitignore', gitignoreEntries);
console.log('✅ Added AI Commit Guard entries to .gitignore');
}
if (!existsSync('.ai-guard-ignore')) {
console.log('📋 Creating sample .ai-guard-ignore...');
const sampleIgnore = `# AI Commit Guard - Files to exclude from review
# Sensitive files (automatically ignored by default)
*.env*
*.key
*secret*
*password*
# Project-specific ignores
dist/*
build/*
generated/*
legacy-code/*
third-party/*
# Large files
*.min.js
*.bundle.*
package-lock.json
`;
writeFileSync('.ai-guard-ignore', sampleIgnore);
}
if (!existsSync('.code-rules.md')) {
console.log('📜 Creating sample .code-rules.md...');
const sampleRules = `# Project Code Review Rules
## Code Quality
- Use meaningful variable and function names
- Keep functions under 20-30 lines when possible
- No magic numbers or strings - use named constants
- Remove unused imports and variables
- Add comments for complex business logic
## Security & Best Practices
- Never commit sensitive data (API keys, passwords, tokens)
- Validate all user inputs
- Use proper error handling with try-catch blocks
- Follow consistent naming conventions
- Use parameterized queries to prevent SQL injection
## Project-Specific Rules
- Add your team's specific coding standards here
- Language-specific best practices
- Framework conventions (React, Vue, etc.)
- Database and API guidelines
`;
writeFileSync('.code-rules.md', sampleRules);
}
console.log('\n🎉 AI Commit Guard setup completed successfully!\n');
console.log('📋 What happens now:');
console.log(' 1. pre-commit: AI reviews your code changes');
console.log(' 2. commit-msg: Adds appropriate flags to commit message\n');
console.log('📁 Configuration files:');
console.log(' .code-rules.md - Customize your coding standards');
console.log(' .ai-guard-ignore - Exclude files from AI review\n');
console.log('🔑 Set your AI API key:');
console.log(' export OPENAI_API_KEY="sk-your-openai-key"');
console.log(' # or');
console.log(' export CLAUDE_API_KEY="sk-ant-your-claude-key"');
console.log(' export AI_PROVIDER="claude"\n');
console.log('🚀 Try it now:');
console.log(' git add .');
console.log(' git commit -m "feat: your feature description"');
console.log('\n💡 Need help? Check the README.md for more configuration options.');
process.exit(0);
} catch (error) {
console.error('❌ Setup failed:', error.message);
console.log('\n🔧 Manual setup instructions:');
console.log('1. Install husky: npm install --save-dev husky');
console.log('2. Initialize: npx husky init');
console.log('3. Add pre-commit hook: echo "npx ai-commit-guard" > .husky/pre-commit');
console.log('4. Add commit-msg hook: echo "npx ai-commit-guard --commit-msg \\$1" > .husky/commit-msg');
console.log('5. Set permissions: chmod +x .husky/*');
process.exit(1);
}
}
_handleCommitMsg() {
try {
const commitMsgFile = process.argv[process.argv.indexOf('--commit-msg') + 1];
if (!commitMsgFile || !existsSync(commitMsgFile)) {
process.exit(0);
}
const currentMsg = readFileSync(commitMsgFile, 'utf8').trim();
const reviewResult = this._getStoredReviewResult();
if (!reviewResult) {
process.exit(0);
}
let flag;
switch (reviewResult) {
case 'SUCCESS':
flag = CONFIG.COMMIT_FLAGS.SUCCESS;
break;
case 'TIMEOUT':
flag = CONFIG.COMMIT_FLAGS.TIMEOUT;
break;
case 'ERROR':
flag = CONFIG.COMMIT_FLAGS.ERROR;
break;
default:
process.exit(0);
}
if (!currentMsg.includes(`[${flag}]`)) {
const newMsg = `${currentMsg} [${flag}]`;
writeFileSync(commitMsgFile, newMsg);
this._logInfo(`Added flag: [${flag}]`);
}
this._clearStoredReviewResult();
process.exit(0);
} catch (error) {
this._logWarning(`Could not handle commit message: ${error.message}`);
process.exit(0);
}
}
_storeReviewResult(result) {
try {
writeFileSync('.ai-guard-result', result);
} catch (error) {
}
}
_getStoredReviewResult() {
try {
if (existsSync('.ai-guard-result')) {
return readFileSync('.ai-guard-result', 'utf8').trim();
}
} catch (error) {
}
return null;
}
_clearStoredReviewResult() {
try {
const fs = require('fs');
if (existsSync('.ai-guard-result')) {
fs.unlinkSync('.ai-guard-result');
}
} catch (error) {
}
}
_showVersion() {
const packagePath = path.join(__dirname, 'package.json');
let version = '2.0.0';
try {
if (existsSync(packagePath)) {
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
version = pkg.version || version;
}
} catch (error) {
}
console.log(`🤖 AI Commit Guard v${version}`);
console.log('Universal AI-powered pre-commit code review tool');
console.log('');
console.log('Supported AI Providers:');
console.log(' • OpenAI GPT-4');
console.log(' • Anthropic Claude');
console.log(' • Google Gemini');
console.log(' • Cohere Command');
console.log(' • Ollama (Local)');
console.log('');
console.log('More info: https://github.com/ademalkan/ai-commit-guard');
process.exit(0);
}
_showHelp() {
console.log('🤖 AI Commit Guard - Universal AI-powered code review tool');
console.log('');
console.log('USAGE:');
console.log(' ai-commit-guard [OPTIONS]');
console.log('');
console.log('OPTIONS:');
console.log(' --setup Setup AI Commit Guard hooks in current project');
console.log(' --commit-msg <file> Handle commit message (used by git hooks)');
console.log(' --version, -v Show version information');
console.log(' --help, -h Show this help message');
console.log('');
console.log('ENVIRONMENT VARIABLES:');
console.log(' OPENAI_API_KEY OpenAI API key');
console.log(' CLAUDE_API_KEY Anthropic Claude API key');
console.log(' GEMINI_API_KEY Google Gemini API key');
console.log(' COHERE_API_KEY Cohere API key');
console.log(' AI_PROVIDER AI provider (openai|claude|gemini|cohere|ollama)');
console.log(' AI_MODEL Specific model name to use');
console.log(' AI_GUARD_TIMEOUT Review timeout in milliseconds (default: 30000)');
console.log(' AI_GUARD_MAX_FILE_SIZE Max file size in bytes (default: 50000)');
console.log('');
console.log('EXAMPLES:');
console.log(' # Setup in new project');
console.log(' ai-commit-guard --setup');
console.log('');
console.log(' # Set API key and commit');
console.log(' export OPENAI_API_KEY="sk-your-key"');
console.log(' git add .');
console.log(' git commit -m "feat: add new feature"');
console.log('');
console.log(' # Use different AI provider');
console.log(' export CLAUDE_API_KEY="sk-ant-your-key"');
console.log(' export AI_PROVIDER="claude"');
console.log('');
console.log(' # Use local Ollama');
console.log(' export AI_PROVIDER="ollama"');
console.log('');
console.log('CONFIGURATION FILES:');
console.log(' .code-rules.md Custom coding rules and standards');
console.log(' .ai-guard-ignore Files to exclude from AI review');
console.log('');
console.log('For more information, visit:');
console.log(' https://github.com/ademalkan/ai-commit-guard');
process.exit(0);
}
_getOriginalCommitMessage() {
try {
if (process.argv[2] && existsSync(process.argv[2])) {
const commitMsgFile = process.argv[2];
const content = readFileSync(commitMsgFile, 'utf8').trim();
if (content && !content.startsWith('#')) {
return content.split('\n')[0];
}
}
const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
const commitMsgFile = path.join(gitDir, 'COMMIT_EDITMSG');
if (existsSync(commitMsgFile)) {
const content = readFileSync(commitMsgFile, 'utf8').trim();
if (content && !content.startsWith('#')) {
return content.split('\n')[0];
}
}
const args = process.argv.slice(2);
for (let i = 0; i < args.length; i++) {
if (args[i] === '-m' && args[i + 1]) {
return args[i + 1];
}
if (args[i].startsWith('-m=')) {
return args[i].substring(3);
}
if (args[i].startsWith('-m') && args[i].length > 2) {
return args[i].substring(2);
}
}
if (process.env.GIT_COMMIT_MESSAGE) {
return process.env.GIT_COMMIT_MESSAGE;
}
return null;
} catch (error) {
this._logWarning(`Could not get commit message: ${error.message}`);
return null;
}
}
}
if (require.main === module) {
new AICommitGuard().run();
}
module.exports = AICommitGuard;