UNPKG

scai

Version:

> AI-powered CLI tool for commit messages **and** pull request reviews — using local models.

170 lines (169 loc) • 7.12 kB
// File: src/commands/CommitSuggesterCmd.ts import { execSync } from 'child_process'; import readline from 'readline'; import { commitSuggesterModule } from '../pipeline/modules/commitSuggesterModule.js'; import { handleChangelogWithCommitMessage } from './ChangeLogUpdateCmd.js'; import os from 'os'; import fs from 'fs'; import path from 'path'; import { spawnSync } from 'child_process'; import chalk from 'chalk'; function askUserToChoose(suggestions) { return new Promise((resolve) => { console.log('\nšŸ’” AI-suggested commit messages:\n'); suggestions.forEach((msg, i) => { console.log(`${i + 1}) ${chalk.hex('#FFA500')(`\`${msg}\``)}`); }); console.log('\n---'); console.log(`${suggestions.length + 1}) šŸ” Regenerate suggestions`); console.log(`${suggestions.length + 2}) āœļø Write your own commit message`); console.log(`${suggestions.length + 3}) šŸ–‹ļø Edit a suggested commit message`); console.log(`${suggestions.length + 4}) āŒ Cancel`); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); rl.question(`\nšŸ‘‰ Choose a commit message [1-${suggestions.length + 4}]: `, (answer) => { rl.close(); const choice = parseInt(answer, 10); if (choice === suggestions.length + 1) { resolve('regenerate'); } else if (choice === suggestions.length + 2) { resolve('custom'); } else if (choice === suggestions.length + 3) { resolve('edit'); } else if (choice === suggestions.length + 4) { resolve('cancel'); } else if (!isNaN(choice) && choice >= 1 && choice <= suggestions.length) { resolve(choice - 1); } else { console.log('āš ļø Invalid selection. Using the first suggestion by default.'); resolve(0); } }); }); } async function promptEditCommitMessage(suggestedMessage) { const tmpFilePath = path.join(os.tmpdir(), 'scai-commit-msg.txt'); fs.writeFileSync(tmpFilePath, `# Edit your commit message below.\n# Lines starting with '#' will be ignored.\n\n${suggestedMessage}`); const editor = process.env.EDITOR || (process.platform === 'win32' ? 'notepad' : 'vi'); spawnSync(editor, [tmpFilePath], { stdio: 'inherit' }); const editedContent = fs.readFileSync(tmpFilePath, 'utf-8'); return editedContent .split('\n') .filter(line => !line.trim().startsWith('#')) .join('\n') .trim() || suggestedMessage; } function askWhichSuggestionToEdit(suggestions) { return new Promise((resolve) => { console.log('\nšŸ–‹ļø Select a commit message to edit:\n'); suggestions.forEach((msg, i) => { console.log(`${i + 1}) ${chalk.hex('#FFA500')(`\`${msg}\``)}`); }); console.log(`${suggestions.length + 1}) āŒ Cancel`); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const editPrompt = chalk.magenta(`\nšŸ‘‰ Choose a commit message to edit [1-${suggestions.length + 1}]: `); rl.question(editPrompt, (answer) => { rl.close(); const choice = parseInt(answer, 10); if (!isNaN(choice) && choice >= 1 && choice <= suggestions.length) { resolve(choice - 1); } else if (choice === suggestions.length + 1) { resolve('cancel'); } else { console.log('āš ļø Invalid selection.'); resolve('cancel'); } }); }); } function promptCustomMessage() { return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); rl.question('\nšŸ“ Enter your custom commit message:\n> ', (input) => { rl.close(); resolve(input.trim()); }); }); } export async function suggestCommitMessage(options) { try { let diff = execSync("git diff --cached", { encoding: "utf-8", stdio: "pipe" }).trim(); if (!diff) { diff = execSync("git diff", { encoding: "utf-8", stdio: "pipe" }).trim(); } if (!diff) { console.log('āš ļø No staged changes to suggest a message for.'); return; } // Continue with commit suggestions const response = await commitSuggesterModule.run({ content: diff }); const suggestions = response.suggestions || []; if (!suggestions.length) { console.log('āš ļø No commit suggestions generated.'); return; } let message = null; while (message === null) { const choice = await askUserToChoose(suggestions); if (choice === 'regenerate') { console.log('\nšŸ”„ Regenerating suggestions...\n'); const response = await commitSuggesterModule.run({ content: diff }); suggestions.splice(0, suggestions.length, ...(response.suggestions || [])); continue; } if (choice === 'custom') { message = await promptCustomMessage(); } else if (choice === 'edit') { // Ask which suggestion to edit using a dedicated prompt const editChoice = await askWhichSuggestionToEdit(suggestions); if (typeof editChoice === 'number') { message = await promptEditCommitMessage(suggestions[editChoice]); } else { console.log('āš ļø Edit cancelled, returning to main menu.'); continue; } } else if (choice === 'cancel') { console.log('āŒ Commit cancelled.'); return; } else { message = suggestions[choice]; } } console.log(`\nāœ… Selected commit message:\n${message}\n`); // If changelog option is enabled, generate changelog including the selected commit message if (options.changelog) { await handleChangelogWithCommitMessage(message); } const staged = execSync("git diff --cached", { encoding: "utf-8" }).trim(); if (!staged) { console.log("āš ļø No files are currently staged for commit."); console.log("šŸ‘‰ Please stage your changes with 'git add <files>' and rerun the command."); return; } // Automatically commit the suggested message execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { stdio: 'inherit' }); console.log('āœ… Committed with selected message.'); } catch (err) { console.error('āŒ Error in commit message suggestion:', err.message); } }