UNPKG

@thissudhir/gitcleancommit

Version:

A beautiful CLI tool for creating clean, conventional git commits with spell checking and automatic integration

321 lines 11 kB
import inquirer from "inquirer"; import chalk from "chalk"; import { checkSpelling } 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", }, ]; function createSquigglyUnderline(text, typos) { let result = text; for (const typo of typos) { const regex = new RegExp(`\\b${typo}\\b`, "gi"); result = result.replace(regex, (match) => { const squiggly = "\u0330".repeat(match.length); return chalk.red(match + squiggly); }); } return result; } function displayTypoWarnings(typos) { if (typos.length > 0) { const warningBox = boxen(chalk.yellow("⚠️ Potential spelling issues detected:\n") + typos .map((typo) => chalk.red(`• ${typo} ${"\u0330".repeat(typo.length)}`)) .join("\n") + chalk.yellow("\n\nPlease review your commit message."), { padding: 1, margin: 1, borderColor: "yellow", borderStyle: "round", title: "Spelling Check", titleAlignment: "center", }); console.log(warningBox); } } 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: 1, margin: 1, borderColor: "yellow", borderStyle: "round", title: "Operation Cancelled", titleAlignment: "center", }); console.log(exitBox); process.exit(0); } // Type guard to check if a key is a color method function isChalkColorMethod(key) { const colorMethods = [ "green", "red", "yellow", "blue", "cyan", "redBright", "white", "black", "gray", "grey", "magenta", "bgGreen", ]; return colorMethods.includes(key); } // Safe color accessor function function getChalkColor(color) { return chalk[color]; } 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(); console.log(boxen(chalk.dim("💡 Tip: Press ESC at any time to cancel"), { padding: 1, margin: { top: 1, bottom: 1 }, borderColor: "blue", borderStyle: "round", })); try { 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: "input", message: "Write a short, imperative tense description of the change:", validate: (input) => { if (input.length < 1) { return "Please enter a commit message."; } if (input.length > 72) { return "Keep the first line under 72 characters."; } return true; }, filter: (input) => input.trim(), }, { name: "body", type: "input", message: "Provide a longer description of the change (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(), }, ]); // Spell check const typos = checkSpelling(answers.message); const bodyTypos = answers.body ? checkSpelling(answers.body) : []; const allTypos = [...typos, ...bodyTypos]; // 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 generated commit message in a box console.log(boxen(formattedCommit, { padding: 1, margin: 1, borderColor: selectedType.color, borderStyle: "round", title: "Generated Commit Message", titleAlignment: "center", })); // Show typo warnings if any if (allTypos.length > 0) { const highlightedHeader = createSquigglyUnderline(commitHeader, typos); let highlightedBody = answers.body || ""; if (answers.body && bodyTypos.length > 0) { highlightedBody = createSquigglyUnderline(answers.body, bodyTypos); } const warningMessage = [ chalk.yellow("Message with issues highlighted:"), `${selectedType.emoji} ${isChalkColorMethod(selectedType.color) ? getChalkColor(selectedType.color)(highlightedHeader) : chalk.white(highlightedHeader)}`, ...(answers.body ? ["", chalk.gray("Body:"), chalk.gray(highlightedBody)] : []), ].join("\n"); console.log(boxen(warningMessage, { padding: 1, margin: { top: 1, bottom: 1 }, borderColor: "yellow", borderStyle: "round", title: "Spelling Issues Detected", titleAlignment: "center", })); displayTypoWarnings(allTypos); } // 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: allTypos.length > 0 ? "Proceed with potential spelling issues?" : "Ready to commit?", default: allTypos.length === 0, }, ]); if (confirm) { if (hookFile) { writeFileSync(hookFile, fullCommit); console.log(boxen(chalk.green("✅ Commit message created successfully!"), { padding: 1, margin: 1, borderColor: "green", borderStyle: "round", })); } else { try { await executeFullGitWorkflow(commitHeader, answers.body); } catch (error) { console.error(boxen(chalk.red("❌ Failed to complete git workflow"), { padding: 1, margin: 1, borderColor: "red", borderStyle: "round", })); process.exit(1); } } } else { console.log(boxen(chalk.yellow("❌ Operation cancelled"), { padding: 1, margin: 1, 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