UNPKG

scai

Version:

> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.** 100% local, private, GDPR-friendly, made in Denmark/EU with ā¤ļø.

145 lines (144 loc) • 6.48 kB
// File: src/commands/CommitSuggesterCmd.ts import { execSync, spawnSync } from 'child_process'; import fs from 'fs'; import os from 'os'; import path from 'path'; import chalk from 'chalk'; import { commitSuggesterModule } from '../pipeline/modules/commitSuggesterModule.js'; import { handleChangelogWithCommitMessage } from './ChangeLogUpdateCmd.js'; import { getRl } from './ReadlineSingleton.js'; // --- Use the passed readline instance --- function askUserToChoose(rl, 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`); rl.question(`\nšŸ‘‰ Choose a commit message [1-${suggestions.length + 4}]: `, (answer) => { 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); } }); }); } function askWhichSuggestionToEdit(rl, 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 promptText = chalk.magenta(`\nšŸ‘‰ Choose a commit message to edit [1-${suggestions.length + 1}]: `); rl.question(promptText, (answer) => { 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(rl) { return new Promise((resolve) => { rl.question('\nšŸ“ Enter your custom commit message:\n> ', (input) => { resolve(input.trim()); }); }); } 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; } // --- Main function --- export async function suggestCommitMessage(options) { const { rl, isTemporary } = getRl(); 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; } const input = { query: 'Generate commit messages', content: diff }; const response = await commitSuggesterModule.run(input); const suggestions = response.suggestions || []; if (!suggestions.length) { console.log('āš ļø No commit suggestions generated.'); return; } let message = null; while (message === null) { const choice = await askUserToChoose(rl, suggestions); if (choice === 'regenerate') { console.log('\nšŸ”„ Regenerating suggestions...\n'); const newResponse = await commitSuggesterModule.run(input); suggestions.splice(0, suggestions.length, ...(newResponse.suggestions || [])); continue; } if (choice === 'custom') message = await promptCustomMessage(rl); else if (choice === 'edit') { const editChoice = await askWhichSuggestionToEdit(rl, 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 (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; } 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); } finally { if (isTemporary) { rl.close(); // šŸ‘ˆ THIS is what allows the process to exit } } }