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
JavaScript
// 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
}
}
}