UNPKG

auto-ai-review

Version:

AI-powered code review tool

238 lines (237 loc) 8.92 kB
#!/usr/bin/env node /** * Review pending changes using git-diff */ import { simpleGit } from 'simple-git'; import OpenAI from 'openai'; import cliMd from 'cli-markdown'; import { program } from 'commander'; import { SettingsManager } from './settings.js'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { createInterface } from 'readline/promises'; const settingsManager = new SettingsManager(); const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function getPackageVersion() { try { const packageJson = JSON.parse(await fs.readFile(path.join(__dirname, '..', 'package.json'), 'utf-8')); return packageJson.version; } catch (error) { console.error('Warning: Could not read package version:', error); return '1.1.1'; // Fallback version } } async function getDiff(files, sourceBranch, targetBranch) { const git = simpleGit({ baseDir: process.cwd(), }); if (files && files.length > 0) { // For specific files, get their content const fileContents = await Promise.all(files.map(async (file) => { try { const content = await fs.readFile(file, 'utf-8'); return `File: ${path.basename(file)}\n\n${content}\n`; } catch (error) { console.error(`Error reading file ${file}:`, error); return ''; } })); return fileContents.join('\n---\n\n'); } else if (sourceBranch && targetBranch) { // Compare branches return (await git.diff([targetBranch, sourceBranch])).trim(); } else { // Default: staged changes return (await git.diff(['--staged'])).trim(); } } async function getOpenAIResponse(prompt, content, model, apiKey) { const openai = new OpenAI({ apiKey: apiKey || process.env.OPENAI_API_KEY, }); const chatCompletion = await openai.chat.completions.create({ messages: [ { role: 'system', content: prompt, }, { role: 'user', content, }, ], model, }); return chatCompletion.choices[0]?.message?.content || ''; } async function reviewCode(diff) { const settings = await settingsManager.loadSettings(); if (!settings.openaiApiKey && !process.env.OPENAI_API_KEY) { console.error('Error: OpenAI API key not found. Please set it using:\naair settings --key YOUR_API_KEY'); process.exit(1); } if (!diff.length) { console.log('No changes to review'); return; } console.log('Reviewing changes...\n'); const response = await getOpenAIResponse(settings.prompt, diff, settings.model, settings.openaiApiKey); if (response) { console.log('--------------------------------------------------------------'); console.log(cliMd(response)); console.log('--------------------------------------------------------------\n'); } } async function generateCommitMessage(diff) { const settings = await settingsManager.loadSettings(); if (!settings.openaiApiKey && !process.env.OPENAI_API_KEY) { console.error('Error: OpenAI API key not found. Please set it using:\naair settings --key YOUR_API_KEY'); process.exit(1); } if (!diff.length) { console.log('No staged changes to generate commit message for'); return; } console.log('Generating commit message...\n'); const response = await getOpenAIResponse(settings.commitPrompt, diff, settings.model, settings.openaiApiKey); if (response) { console.log('Suggested commit message:\n'); console.log('--------------------------------------------------------------'); console.log(response); console.log('--------------------------------------------------------------\n'); const rl = createInterface({ input: process.stdin, output: process.stdout }); try { // Ask if they want to edit the message const editChoice = await rl.question('Would you like to edit this message? (y/N): '); let finalMessage = response; if (editChoice.toLowerCase() === 'y') { console.log('\nEnter your edited message (press Enter twice to finish):'); const lines = []; let emptyLines = 0; while (emptyLines < 2) { const line = await rl.question(''); if (line.trim() === '') { emptyLines++; } else { emptyLines = 0; } if (emptyLines < 2) { lines.push(line); } } finalMessage = lines.join('\n'); console.log('\nUpdated commit message:\n'); console.log('--------------------------------------------------------------'); console.log(finalMessage); console.log('--------------------------------------------------------------\n'); } // Ask for confirmation const confirmChoice = await rl.question('Create commit with this message? (y/N): '); if (confirmChoice.toLowerCase() === 'y') { const git = simpleGit({ baseDir: process.cwd(), }); await git.commit(finalMessage); console.log('\nCommit created successfully!'); } else { console.log('\nCommit cancelled. You can run the command again to generate a new message.'); } } finally { rl.close(); } } } // Initialize CLI async function initCLI() { const version = await getPackageVersion(); program .name('aair') .description('AI-powered code review tool') .version(version); program .command('review') .description('Review staged changes (default)') .action(async () => { const diff = await getDiff(); await reviewCode(diff); }); program .command('files') .description('Review specific files') .argument('<files...>', 'Files to review') .action(async (files) => { const diff = await getDiff(files); await reviewCode(diff); }); program .command('branch') .description('Review changes between branches') .argument('<source>', 'Source branch (with changes)') .argument('[target]', 'Target branch to compare against', 'main') .action(async (source, target) => { const diff = await getDiff(undefined, source, target); await reviewCode(diff); }); program .command('commit') .description('Generate and interactively edit commit message for staged changes') .action(async () => { const diff = await getDiff(); await generateCommitMessage(diff); }); program .command('settings') .description('Manage settings') .option('-k, --key <key>', 'Set OpenAI API key') .option('-m, --model <model>', 'Set OpenAI model') .option('-p, --prompt <prompt>', 'Set custom review prompt') .option('-c, --commit-prompt <prompt>', 'Set custom commit message prompt') .option('-s, --show', 'Show current settings') .action(async (options) => { if (options.show) { const settings = await settingsManager.loadSettings(); const displaySettings = { ...settings, openaiApiKey: settings.openaiApiKey ? '********' : undefined }; console.log('Current settings:'); console.log(JSON.stringify(displaySettings, null, 2)); return; } const updates = {}; if (options.key) updates.openaiApiKey = options.key; if (options.model) updates.model = options.model; if (options.prompt) updates.prompt = options.prompt; if (options.commitPrompt) updates.commitPrompt = options.commitPrompt; if (Object.keys(updates).length > 0) { const settings = await settingsManager.updateSettings(updates); console.log('Settings updated successfully!'); const displaySettings = { ...settings, openaiApiKey: settings.openaiApiKey ? '********' : undefined }; console.log(JSON.stringify(displaySettings, null, 2)); } }); program.parse(); } // Start the CLI initCLI().catch(error => { console.error('Error:', error); process.exit(1); });