gitcleancommit
Version:
A beautiful CLI tool for creating clean, conventional git commits with advanced spell checking and automatic integration
363 lines • 12.7 kB
JavaScript
import inquirer from "inquirer";
import chalk from "chalk";
import { GitCleanSpellChecker } from "./spellcheck.js";
import { executeFullGitWorkflow } from "./git-integration.js";
import { writeFileSync } from "fs";
import boxen from "boxen";
const COMMIT_TYPES = [
{
name: `${chalk.green("ADD")} - Add new code or files`,
value: "ADD",
color: "green",
emoji: "➕",
description: "Added new code or files",
},
{
name: `${chalk.red("FIX")} - A bug fix`,
value: "FIX",
color: "red",
emoji: "🐛",
description: "A bug fix",
},
{
name: `${chalk.yellow("UPDATE")} - Updated a file or code`,
value: "UPDATE",
color: "yellow",
emoji: "🔄",
description: "Updated a file or code",
},
{
name: `${chalk.blue("DOCS")} - Documentation changes`,
value: "DOCS",
color: "blue",
emoji: "📚",
description: "Documentation only changes",
},
{
name: `${chalk.cyan("TEST")} - Adding tests`,
value: "TEST",
color: "cyan",
emoji: "✅",
description: "Adding missing tests or correcting existing tests",
},
{
name: `${chalk.redBright("REMOVE")} - Removing code or files`,
value: "REMOVE",
color: "redBright",
emoji: "🗑️",
description: "Removing code or files",
},
];
// Custom inquirer prompt with real-time spell checking
class SpellCheckPrompt {
constructor(question, readLine, answers) {
this.question = question;
this.rl = readLine;
this.answers = answers;
this.currentText = "";
this.spellErrors = [];
this.status = "pending";
this.keypressListener = null;
}
run() {
return new Promise((resolve, reject) => {
this.done = resolve;
this.render();
this.setupKeyHandlers();
});
}
setupKeyHandlers() {
// Store the keypress listener so we can remove it later
this.keypressListener = async (str, key) => {
if (!key)
return;
if (key.name === "return" || key.name === "enter") {
this.cleanup();
// process.stdout.write("\n");
// Validation
if (this.question.validate) {
const validation = await this.question.validate(this.currentText);
if (validation !== true) {
console.log(chalk.red(`>> ${validation}`));
this.currentText = "";
this.spellErrors = [];
this.render();
this.setupKeyHandlers();
return;
}
}
// Filter
let result = this.currentText;
if (this.question.filter) {
result = await this.question.filter(this.currentText);
}
this.status = "answered";
this.done(result);
return;
}
if (key.name === "escape") {
this.cleanup();
process.exit(0);
}
// Handle text input
if (key.name === "backspace") {
this.currentText = this.currentText.slice(0, -1);
}
else if (str && str.length === 1 && !key.ctrl && !key.meta) {
this.currentText += str;
}
await this.performSpellCheck();
this.render();
};
// Enable keypress events
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.on("keypress", this.keypressListener);
}
cleanup() {
if (this.keypressListener) {
process.stdin.removeListener("keypress", this.keypressListener);
this.keypressListener = null;
}
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
}
async performSpellCheck() {
if (this.currentText.length === 0) {
this.spellErrors = [];
return;
}
clearTimeout(this.spellCheckTimeout);
this.spellCheckTimeout = setTimeout(async () => {
try {
this.spellErrors = await GitCleanSpellChecker.checkSpelling(this.currentText);
this.render();
}
catch (error) {
// Silent error handling for spell check
this.spellErrors = [];
}
}, 200);
}
render() {
if (this.status === "answered")
return;
// Clear current line
process.stdout.write("\r\x1b[K");
// Show question
const questionText = this.question.message;
process.stdout.write(`${chalk.cyan("?")} ${chalk.bold(questionText)} `);
// Show text with spell checking
const displayText = this.createDisplayText();
process.stdout.write(displayText);
}
createDisplayText() {
if (this.spellErrors.length === 0) {
return this.currentText;
}
let result = this.currentText;
// Sort errors by position (descending) to avoid index shifting
const sortedErrors = this.spellErrors.sort((a, b) => b.position.start - a.position.start);
for (const error of sortedErrors) {
const { word, position } = error;
const beforeWord = result.substring(0, position.start);
const afterWord = result.substring(position.end);
// Create red underlined text for misspelled words
const highlightedWord = chalk.red.underline(word);
result = beforeWord + highlightedWord + afterWord;
}
return result;
}
}
// Register custom prompt type with proper typing
inquirer.registerPrompt("spellcheck", SpellCheckPrompt);
function handleEscapeKey() {
const exitBox = boxen(chalk.yellow("⚠️ Operation cancelled by user (ESC pressed)") +
"\n\n" +
chalk.dim("Run the command again when you're ready to commit."), {
padding: 0.5,
margin: 1,
borderColor: "yellow",
borderStyle: "round",
title: "Operation Cancelled",
titleAlignment: "center",
});
console.log(exitBox);
process.exit(0);
}
function setupEscapeHandler() {
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding("utf8");
process.stdin.on("data", (key) => {
const keyString = key.toString();
if (keyString === "\u001B" || keyString === "\u0003") {
handleEscapeKey();
}
});
}
}
function formatCommitMessage(type, header, body, breaking, issues) {
let message = `${type.emoji} ${chalk[type.color](header)}`;
if (body) {
message += `\n\n${chalk.dim(body)}`;
}
if (breaking) {
message += `\n\n${chalk.redBright("💥 BREAKING CHANGE:")} ${chalk.redBright(header)}`;
}
if (issues) {
message += `\n\n${chalk.blue(issues)}`;
}
return message;
}
export async function promptCommit(hookFile) {
setupEscapeHandler();
// Initialize spell checker
await GitCleanSpellChecker.initialize();
console.log(chalk.blue("🔤 Real-time spell checking enabled for text inputs!\n"));
try {
// All questions in one inquirer.prompt with real-time spell checking
const answers = await inquirer.prompt([
{
name: "type",
type: "list",
message: "Select the type of change you're committing:",
choices: COMMIT_TYPES.map((type) => ({
name: type.name,
value: type.value,
short: `${type.emoji} ${type.value}`,
})),
pageSize: 10,
},
{
name: "scope",
type: "input",
message: "What is the scope of this change? (optional):",
filter: (input) => input.trim(),
},
{
name: "message",
type: "spellcheck",
message: "Write a short, commit message:",
validate: (input) => {
if (input.trim().length < 1) {
return "Please enter a commit message.";
}
if (input.trim().length > 72) {
return "Keep the first line under 72 characters.";
}
return true;
},
filter: (input) => input.trim(),
},
{
name: "body",
type: "spellcheck",
message: "Provide a longer description (optional):",
filter: (input) => input.trim(),
},
{
name: "breaking",
type: "confirm",
message: "Are there any breaking changes?",
default: false,
},
{
name: "issues",
type: "input",
message: 'Add issue references (e.g., "fixes #123", "closes #456"):',
filter: (input) => input.trim(),
},
]);
// Find the selected commit type
const selectedType = COMMIT_TYPES.find((type) => type.value === answers.type);
// Build the commit message parts
const breakingPrefix = answers.breaking ? "!" : "";
const scope = answers.scope ? `(${answers.scope})` : "";
const commitHeader = `${answers.type}${scope}${breakingPrefix}: ${answers.message}`;
// Format the full commit message for display
const formattedCommit = formatCommitMessage(selectedType, commitHeader, answers.body, answers.breaking, answers.issues);
// Display the final commit message
console.log(boxen(formattedCommit, {
padding: 0.5,
margin: 0.5,
borderColor: selectedType.color,
borderStyle: "round",
title: "Final Commit Message",
titleAlignment: "center",
}));
// Build the actual commit message for git
let fullCommit = commitHeader;
if (answers.body) {
fullCommit += `\n\n${answers.body}`;
}
if (answers.breaking) {
fullCommit += `\n\nBREAKING CHANGE: ${answers.message}`;
}
if (answers.issues) {
fullCommit += `\n\n${answers.issues}`;
}
// Final confirmation
const { confirm } = await inquirer.prompt([
{
name: "confirm",
type: "confirm",
message: "Ready to commit?",
default: true,
},
]);
if (confirm) {
if (hookFile) {
writeFileSync(hookFile, fullCommit);
console.log(boxen(chalk.green("✅ Commit message created successfully!"), {
padding: 0.5,
margin: 0.5,
borderColor: "green",
borderStyle: "round",
}));
}
else {
try {
await executeFullGitWorkflow(commitHeader, answers.body);
}
catch (error) {
console.error(boxen(chalk.red("❌ Failed to complete git workflow"), {
padding: 0.5,
margin: 0.5,
borderColor: "red",
borderStyle: "round",
}));
process.exit(1);
}
}
}
else {
console.log(boxen(chalk.yellow("❌ Operation cancelled"), {
padding: 0.5,
margin: 0.5,
borderColor: "yellow",
borderStyle: "round",
}));
process.exit(1);
}
}
catch (error) {
if (error && typeof error === "object" && "name" in error) {
if (error.name === "ExitPromptError") {
handleEscapeKey();
}
}
throw error;
}
finally {
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
process.stdin.pause();
}
}
}
//# sourceMappingURL=prompt.js.map