UNPKG

@elsikora/commitizen-plugin-commitlint-ai

Version:
146 lines (142 loc) 6.88 kB
'use strict'; var node_child_process = require('node:child_process'); var node_util = require('node:util'); var chalk = require('chalk'); var index = require('./llm/index.js'); const execAsync = node_util.promisify(node_child_process.exec); /** * Validates a commit message and retries with LLM if there are errors * @param commitConfig The original commit configuration * @param promptContext The prompt context used to generate the commit * @returns A promise that resolves to a valid commit message or null if manual entry is needed */ async function validateAndFixCommitMessage(commitConfig, promptContext) { // Initial commit message const commitMessage = constructCommitMessage(commitConfig); // Validate with commitlint const validation = await validateWithCommitlint(commitMessage); // If valid, return the message if (validation.isValid) { return commitMessage; } // If not valid and we have errors, try to fix with LLM if (!validation.isValid && validation.errors) { console.log(chalk.yellow("Commit message failed validation. Attempting to fix...")); const MAX_RETRIES = 3; let currentConfig = commitConfig; const allErrors = []; // Attempt to fix the commit message up to MAX_RETRIES times // eslint-disable-next-line @elsikora-typescript/typedef for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { // Add the current validation error to our history if (validation.errors) { allErrors.push(validation.errors); } try { // Generate a fixed commit message with accumulated errors currentConfig = await fixCommitMessageWithLLM(currentConfig, validation.errors ?? "", promptContext, allErrors.slice(0, -1)); // Construct and validate the new commit message const fixedCommitMessage = constructCommitMessage(currentConfig); const fixedValidation = await validateWithCommitlint(fixedCommitMessage); // If valid, return the successful message if (fixedValidation.isValid) { console.log(chalk.green(`Commit message fixed successfully on attempt ${attempt + 1}!`)); return fixedCommitMessage; } // If we still have errors, continue with next attempt if (fixedValidation.errors) { console.log(chalk.yellow(`Fix attempt ${attempt + 1} still has errors. ${MAX_RETRIES - attempt - 1} retries left.`)); // Update the validation errors for the next iteration validation.errors = fixedValidation.errors; } } catch (error) { console.error(chalk.red(`Error while trying to fix commit message (attempt ${attempt + 1}):`, error)); break; // Exit retry loop on error } } // If we exhausted all retries and still have issues console.log(chalk.red(`Unable to fix commit message automatically after ${MAX_RETRIES} attempts.`)); console.log(chalk.yellow("Switching to manual commit entry mode...")); return null; // Signal to switch to manual mode } // Default case, return original even if there were issues return commitMessage; } /** * Validates a commit message with commitlint * @param commitMessage The commit message to validate * @returns A promise that resolves to an object with the validation result */ async function validateWithCommitlint(commitMessage) { try { // Create temporary file with commit message const cmd = `echo "${commitMessage}" | npx commitlint`; // eslint-disable-next-line @elsikora-typescript/no-unsafe-call await execAsync(cmd); return { isValid: true }; } catch (error) { // If commitlint exits with non-zero code, it means there are validation errors const typedError = error; return { errors: typedError.stdout ?? typedError.stderr ?? typedError.message, isValid: false, }; } } /** * Constructs a commit message from a CommitConfig object * @param commitConfig The commit configuration * @returns The formatted commit message */ function constructCommitMessage(commitConfig) { const type = commitConfig.type; const scope = commitConfig.scope ? `(${commitConfig.scope})` : ""; const subject = commitConfig.subject; const header = `${type}${scope}: ${subject}`; // Body with optional breaking change let body = ""; if (commitConfig.isBreaking) { body = `BREAKING CHANGE: ${commitConfig.breakingBody ?? "This commit introduces breaking changes."}\n\n`; } if (commitConfig.body) { body += commitConfig.body; } // Footer with issue references let footer = ""; if (commitConfig.issues && commitConfig.issues.length > 0) { footer = `Issues: ${commitConfig.issues.join(", ")}`; } if (commitConfig.references && commitConfig.references.length > 0) { if (footer) footer += "\n"; footer += `References: ${commitConfig.references.join(", ")}`; } // Combine all parts return [header, body, footer].filter(Boolean).join("\n\n"); } /** * Sends a commit message to the LLM for correction with provided errors * @param commitConfig The original commit configuration * @param errors The errors from commitlint * @param promptContext The prompt context to use for regeneration * @param previousErrors Optional accumulated errors from previous attempts * @returns A promise that resolves to a new commit configuration */ async function fixCommitMessageWithLLM(commitConfig, errors, promptContext, previousErrors = []) { // Create a history of all errors to help the LLM understand what needs fixing const errorHistory = [...previousErrors, errors].map((error, index) => `Attempt ${index + 1} errors:\n${error}`).join("\n\n"); // Create an enhanced context that includes the original commit and errors const enhancedContext = { ...promptContext, // Add a note about the previous attempt and errors diff: `${promptContext.diff ?? ""}\n\nCommit message failed validation with these errors:\n${errorHistory}\n\nOriginal commit structure:\n${JSON.stringify(commitConfig, null)}`, }; console.log(chalk.yellow(`Commit message had validation errors. Asking LLM to fix... (Attempt ${previousErrors.length + 1})`)); // Generate a new commit message with the enhanced context return await index.generateCommitMessage(enhancedContext); } exports.validateAndFixCommitMessage = validateAndFixCommitMessage; exports.validateWithCommitlint = validateWithCommitlint; //# sourceMappingURL=commitlintValidator.js.map