git-coco 0.20.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18,6 +18,8 @@ var anthropic = require('@langchain/anthropic');
18
18
  var ollama = require('@langchain/ollama');
19
19
  var openai = require('@langchain/openai');
20
20
  var output_parsers = require('@langchain/core/output_parsers');
21
+ require('@langchain/core/utils/json_schema');
22
+ require('@langchain/core/utils/types');
21
23
  var base = require('@langchain/core/language_models/base');
22
24
  var runnables = require('@langchain/core/runnables');
23
25
  var outputs = require('@langchain/core/outputs');
@@ -39,6 +41,8 @@ var child_process = require('child_process');
39
41
  var readline = require('node:readline');
40
42
  var require$$0 = require('stream');
41
43
  var readline$1 = require('readline');
44
+ var core$1 = require('@commitlint/core');
45
+ var url = require('url');
42
46
 
43
47
  function _interopNamespaceDefault(e) {
44
48
  var n = Object.create(null);
@@ -68,7 +72,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline$1);
68
72
  /**
69
73
  * Current build version from package.json
70
74
  */
71
- const BUILD_VERSION = "0.20.1";
75
+ const BUILD_VERSION = "0.21.0";
72
76
 
73
77
  const isInteractive = (config) => {
74
78
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -1200,6 +1204,8 @@ const schema$1 = {
1200
1204
  "text-davinci-edit-001",
1201
1205
  "code-davinci-edit-001",
1202
1206
  "text-embedding-ada-002",
1207
+ "text-embedding-3-small",
1208
+ "text-embedding-3-large",
1203
1209
  "text-similarity-davinci-001",
1204
1210
  "text-similarity-curie-001",
1205
1211
  "text-similarity-babbage-001",
@@ -1235,10 +1241,54 @@ const schema$1 = {
1235
1241
  "gpt-4-vision-preview",
1236
1242
  "gpt-4o",
1237
1243
  "gpt-4o-2024-05-13",
1244
+ "gpt-4o-2024-08-06",
1245
+ "gpt-4o-2024-11-20",
1246
+ "gpt-4o-mini-2024-07-18",
1238
1247
  "gpt-4o-mini",
1248
+ "gpt-4o-search-preview",
1249
+ "gpt-4o-search-preview-2025-03-11",
1250
+ "gpt-4o-mini-search-preview",
1251
+ "gpt-4o-mini-search-preview-2025-03-11",
1252
+ "gpt-4o-audio-preview",
1253
+ "gpt-4o-audio-preview-2024-12-17",
1254
+ "gpt-4o-audio-preview-2024-10-01",
1255
+ "gpt-4o-mini-audio-preview",
1256
+ "gpt-4o-mini-audio-preview-2024-12-17",
1257
+ "o1",
1258
+ "o1-2024-12-17",
1259
+ "o1-mini",
1260
+ "o1-mini-2024-09-12",
1261
+ "o1-preview",
1262
+ "o1-preview-2024-09-12",
1263
+ "o1-pro",
1264
+ "o1-pro-2025-03-19",
1265
+ "o3",
1266
+ "o3-2025-04-16",
1267
+ "o3-mini",
1268
+ "o3-mini-2025-01-31",
1269
+ "o4-mini",
1270
+ "o4-mini-2025-04-16",
1271
+ "chatgpt-4o-latest",
1272
+ "gpt-4o-realtime",
1273
+ "gpt-4o-realtime-preview-2024-10-01",
1274
+ "gpt-4o-realtime-preview-2024-12-17",
1275
+ "gpt-4o-mini-realtime-preview",
1276
+ "gpt-4o-mini-realtime-preview-2024-12-17",
1239
1277
  "gpt-4.1",
1278
+ "gpt-4.1-2025-04-14",
1240
1279
  "gpt-4.1-mini",
1241
- "gpt-4.1-nano"
1280
+ "gpt-4.1-mini-2025-04-14",
1281
+ "gpt-4.1-nano",
1282
+ "gpt-4.1-nano-2025-04-14",
1283
+ "gpt-4.5-preview",
1284
+ "gpt-4.5-preview-2025-02-27",
1285
+ "gpt-5",
1286
+ "gpt-5-2025-08-07",
1287
+ "gpt-5-nano",
1288
+ "gpt-5-nano-2025-08-07",
1289
+ "gpt-5-mini",
1290
+ "gpt-5-mini-2025-08-07",
1291
+ "gpt-5-chat-latest"
1242
1292
  ]
1243
1293
  },
1244
1294
  "OllamaModel": {
@@ -1500,6 +1550,13 @@ const schema$1 = {
1500
1550
  "ChatModel": {
1501
1551
  "type": "string",
1502
1552
  "enum": [
1553
+ "gpt-5",
1554
+ "gpt-5-mini",
1555
+ "gpt-5-nano",
1556
+ "gpt-5-2025-08-07",
1557
+ "gpt-5-mini-2025-08-07",
1558
+ "gpt-5-nano-2025-08-07",
1559
+ "gpt-5-chat-latest",
1503
1560
  "gpt-4.1",
1504
1561
  "gpt-4.1-mini",
1505
1562
  "gpt-4.1-nano",
@@ -6223,8 +6280,6 @@ function getPrompt({ template, variables, fallback }) {
6223
6280
  throw new LangChainExecutionError('getPrompt: Unexpected execution path - neither template nor fallback available', { template, fallback, variables });
6224
6281
  }
6225
6282
 
6226
- new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789");
6227
-
6228
6283
  /**
6229
6284
  * Base interface that all chains must implement.
6230
6285
  */
@@ -6327,7 +6382,7 @@ class BaseChain extends base.BaseLangChain {
6327
6382
  async run(
6328
6383
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6329
6384
  input, config) {
6330
- const inputKeys = this.inputKeys.filter((k) => !this.memory?.memoryKeys.includes(k) ?? true);
6385
+ const inputKeys = this.inputKeys.filter((k) => !this.memory?.memoryKeys.includes(k));
6331
6386
  const isKeylessInput = inputKeys.length <= 1;
6332
6387
  if (!isKeylessInput) {
6333
6388
  throw new Error(`Chain ${this._chainType()} expects multiple inputs, cannot use 'run' `);
@@ -6498,7 +6553,7 @@ function _getLanguageModel(llmLike) {
6498
6553
  * import { ChatOpenAI } from "@langchain/openai";
6499
6554
  *
6500
6555
  * const prompt = ChatPromptTemplate.fromTemplate("Tell me a {adjective} joke");
6501
- * const llm = new ChatOpenAI();
6556
+ * const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
6502
6557
  * const chain = prompt.pipe(llm);
6503
6558
  *
6504
6559
  * const response = await chain.invoke({ adjective: "funny" });
@@ -7114,6 +7169,8 @@ Please follow the guidelines below when writing your commit message:
7114
7169
 
7115
7170
  {{branch_name_context}}
7116
7171
 
7172
+ {{commitlint_rules_context}}
7173
+
7117
7174
  {{format_instructions}}
7118
7175
 
7119
7176
  {{commit_history}}
@@ -7121,7 +7178,7 @@ Please follow the guidelines below when writing your commit message:
7121
7178
  {{additional_context}}
7122
7179
  `;
7123
7180
  // Define the variables that will be passed to the prompt template
7124
- const inputVariables$3 = ['summary', 'format_instructions', 'additional_context', 'commit_history', 'branch_name_context'];
7181
+ const inputVariables$3 = ['summary', 'format_instructions', 'additional_context', 'commit_history', 'branch_name_context', 'commitlint_rules_context'];
7125
7182
  const COMMIT_PROMPT = new prompts$1.PromptTemplate({
7126
7183
  template: template$4,
7127
7184
  inputVariables: inputVariables$3,
@@ -7163,23 +7220,20 @@ Based on the following diff summary, generate a conventional commit message that
7163
7220
 
7164
7221
  {{branch_name_context}}
7165
7222
 
7223
+ {{commitlint_rules_context}}
7224
+
7166
7225
  {{format_instructions}}
7167
7226
 
7168
7227
  {{commit_history}}
7169
7228
 
7170
- {{additional_context}}
7171
-
7172
- Remember:
7173
- - Be concise and precise
7174
- - Focus on WHAT and WHY, not HOW
7175
- - Use imperative mood in both title and body
7176
- - Ensure the title alone provides enough context to understand the change`;
7229
+ {{additional_context}}`;
7177
7230
  const conventionalInputVariables = [
7178
7231
  'summary',
7179
7232
  'additional_context',
7180
7233
  'commit_history',
7181
7234
  'format_instructions',
7182
7235
  'branch_name_context',
7236
+ 'commitlint_rules_context',
7183
7237
  ];
7184
7238
  const CONVENTIONAL_COMMIT_PROMPT = new prompts$1.PromptTemplate({
7185
7239
  template: CONVENTIONAL_TEMPLATE,
@@ -7388,7 +7442,9 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
7388
7442
  continue;
7389
7443
  }
7390
7444
  // Only edit the result in interactive mode if approved
7391
- result = await editResult(result, options);
7445
+ // Use custom edit function if provided, otherwise use default editResult
7446
+ const editFunction = options.review?.customEditFunction || editResult;
7447
+ result = await editFunction(result, options);
7392
7448
  }
7393
7449
  else {
7394
7450
  // In non-interactive mode, we return the result as is to be output to stdout by the caller.
@@ -7572,17 +7628,17 @@ var changelog = {
7572
7628
  const conventionalTypeRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?:/;
7573
7629
  // Regular commit message schema with basic validation
7574
7630
  const CommitMessageResponseSchema = objectType({
7575
- title: stringType(),
7576
- body: stringType(),
7631
+ title: stringType().describe("Title of the commit message"),
7632
+ body: stringType().describe("Body of the commit message"),
7577
7633
  });
7578
7634
  // Conventional commit message schema with strict formatting rules
7579
7635
  const ConventionalCommitMessageResponseSchema = objectType({
7580
7636
  title: stringType()
7581
7637
  .max(50, "Title must be 50 characters or less")
7582
- .refine((title) => conventionalTypeRegex.test(title), "Title must follow Conventional Commits format (e.g., 'feat: add new feature' or 'fix(scope): fix bug')"),
7583
- body: stringType()
7638
+ .refine((title) => conventionalTypeRegex.test(title), "Title must follow Conventional Commits format (e.g., 'feat: add new feature' or 'fix(scope): fix bug')").describe("Title of the commit message"),
7639
+ body: stringType().describe("Body of the commit message")
7584
7640
  // .max(280, "Body must be 280 characters or less"),
7585
- });
7641
+ }).describe("Conventional commit message schema with strict formatting rules");
7586
7642
  const command$3 = 'commit';
7587
7643
  /**
7588
7644
  * Command line options via yargs
@@ -8241,12 +8297,12 @@ function formatSet(input) {
8241
8297
  * const overallChain = new SequentialChain({
8242
8298
  * chains: [
8243
8299
  * new LLMChain({
8244
- * llm: new ChatOpenAI({ temperature: 0 }),
8300
+ * llm: new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 }),
8245
8301
  * prompt: promptTemplate,
8246
8302
  * outputKey: "synopsis",
8247
8303
  * }),
8248
8304
  * new LLMChain({
8249
- * llm: new OpenAI({ temperature: 0 }),
8305
+ * llm: new OpenAI({ model: "gpt-4o-mini", temperature: 0 }),
8250
8306
  * prompt: reviewPromptTemplate,
8251
8307
  * outputKey: "review",
8252
8308
  * }),
@@ -8483,7 +8539,8 @@ class SimpleSequentialChain extends BaseChain {
8483
8539
  /** @ignore */
8484
8540
  _validateChains() {
8485
8541
  for (const chain of this.chains) {
8486
- if (chain.inputKeys.filter((k) => !chain.memory?.memoryKeys.includes(k) ?? true).length !== 1) {
8542
+ if (chain.inputKeys.filter((k) => !chain.memory?.memoryKeys.includes(k))
8543
+ .length !== 1) {
8487
8544
  throw new Error(`Chains used in SimpleSequentialChain should all have one input, got ${chain.inputKeys.length} for ${chain._chainType()}.`);
8488
8545
  }
8489
8546
  if (chain.outputKeys.length !== 1) {
@@ -11037,6 +11094,75 @@ const getTokenCounter = async (modelName) => {
11037
11094
  });
11038
11095
  };
11039
11096
 
11097
+ const COMMITLINT_CONFIG_FILES = [
11098
+ '.commitlintrc',
11099
+ '.commitlintrc.json',
11100
+ '.commitlintrc.yaml',
11101
+ '.commitlintrc.yml',
11102
+ '.commitlintrc.js',
11103
+ '.commitlintrc.cjs',
11104
+ 'commitlint.config.js',
11105
+ 'commitlint.config.cjs',
11106
+ ];
11107
+
11108
+ /**
11109
+ * Finds the project root directory starting from the given current directory.
11110
+ * It checks if the `.git` directory or `package.json` file exists in the current directory or any of its parent directories.
11111
+ * If found, it returns the path to the project root directory.
11112
+ * If not found, it throws an error.
11113
+ *
11114
+ * @param currentDir - The current directory to start searching from.
11115
+ * @returns The path to the project root directory.
11116
+ * @throws Error if the project root directory cannot be found.
11117
+ */
11118
+ function findProjectRoot(currentDir) {
11119
+ const root = path.parse(currentDir).root;
11120
+ while (currentDir !== root) {
11121
+ if (fs.existsSync(path.join(currentDir, '.git')) ||
11122
+ fs.existsSync(path.join(currentDir, 'package.json'))) {
11123
+ return currentDir;
11124
+ }
11125
+ currentDir = path.dirname(currentDir);
11126
+ }
11127
+ throw new Error('Unable to find project root. Are you in the right directory?');
11128
+ }
11129
+
11130
+ /**
11131
+ * Check if a commitlint configuration exists in the project root.
11132
+ */
11133
+ async function hasCommitlintConfig() {
11134
+ const projectRoot = findProjectRoot(process.cwd());
11135
+ if (!projectRoot) {
11136
+ return false;
11137
+ }
11138
+ // Check for dedicated commitlint config files
11139
+ for (const file of COMMITLINT_CONFIG_FILES) {
11140
+ if (fs.existsSync(path.join(projectRoot, file))) {
11141
+ return true;
11142
+ }
11143
+ }
11144
+ // Check for commitlint config in package.json
11145
+ const pkgPath = path.join(projectRoot, 'package.json');
11146
+ if (fs.existsSync(pkgPath)) {
11147
+ try {
11148
+ const pkgContent = fs.readFileSync(pkgPath, 'utf8');
11149
+ const pkg = JSON.parse(pkgContent);
11150
+ if (pkg.commitlint) {
11151
+ return true;
11152
+ }
11153
+ }
11154
+ catch (error) {
11155
+ // Ignore errors reading or parsing package.json
11156
+ }
11157
+ }
11158
+ return false;
11159
+ }
11160
+
11161
+ var hasCommitlintConfig$1 = /*#__PURE__*/Object.freeze({
11162
+ __proto__: null,
11163
+ hasCommitlintConfig: hasCommitlintConfig
11164
+ });
11165
+
11040
11166
  async function noResult$2({ git, logger }) {
11041
11167
  const { staged, unstaged, untracked } = await getChanges({ git });
11042
11168
  const hasStaged = staged && staged.length > 0;
@@ -11091,12 +11217,18 @@ const handler$3 = async (argv, logger) => {
11091
11217
  color: 'yellow',
11092
11218
  });
11093
11219
  }
11220
+ logger.verbose(`→ ${provider} (${model})`, {
11221
+ color: 'green',
11222
+ });
11223
+ const USE_CONVENTIONAL_COMMITS = config.conventionalCommits || argv.conventional;
11094
11224
  async function factory() {
11095
11225
  if (config.noDiff) {
11096
11226
  const status = await git.status();
11097
- return status.files.map(file => ({
11227
+ return status.files.map((file) => ({
11098
11228
  filePath: file.path,
11099
- status: (file.index === 'A' || file.index === '?' ? 'added' : 'modified'),
11229
+ status: (file.index === 'A' || file.index === '?'
11230
+ ? 'added'
11231
+ : 'modified'),
11100
11232
  summary: file.path, // Simplified summary for noDiff
11101
11233
  }));
11102
11234
  }
@@ -11118,12 +11250,13 @@ const handler$3 = async (argv, logger) => {
11118
11250
  options: { tokenizer, git, llm, logger },
11119
11251
  });
11120
11252
  }
11253
+ logger.log(`Generating commit message...${JSON.stringify(config.prompt)}`, { color: 'blue' });
11121
11254
  const commitMsg = await generateAndReviewLoop({
11122
11255
  label: 'commit message',
11123
11256
  options: {
11124
11257
  ...config,
11125
11258
  prompt: config.prompt ||
11126
- (config.conventionalCommits || argv.conventional
11259
+ (USE_CONVENTIONAL_COMMITS
11127
11260
  ? CONVENTIONAL_COMMIT_PROMPT.template
11128
11261
  : COMMIT_PROMPT.template),
11129
11262
  logger,
@@ -11136,21 +11269,23 @@ const handler$3 = async (argv, logger) => {
11136
11269
  retryMessageOnly: 'Restart the function execution from generating the commit message',
11137
11270
  retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
11138
11271
  },
11272
+ customEditFunction: async (message, options) => {
11273
+ const { editCommitMessage } = await Promise.resolve().then(function () { return editCommitMessage$1; });
11274
+ return editCommitMessage(message, options);
11275
+ },
11139
11276
  },
11140
11277
  },
11141
11278
  factory,
11142
11279
  parser,
11143
11280
  agent: async (context, options) => {
11144
- // Check if conventional commits are enabled via config or CLI flag
11145
- const useConventional = config.conventionalCommits || argv.conventional;
11146
11281
  // Select the appropriate schema based on whether conventional commits are enabled
11147
- const schema = useConventional
11282
+ const schema = USE_CONVENTIONAL_COMMITS
11148
11283
  ? ConventionalCommitMessageResponseSchema
11149
11284
  : CommitMessageResponseSchema;
11150
11285
  const formatInstructions = `You must always return valid JSON fenced by a markdown code block. Do not return any additional text. The JSON object you return should match the following schema:
11151
- {{ body: string, title: string }}`;
11286
+ ${schema.description}`;
11152
11287
  // Use conventional commit prompt if enabled
11153
- const promptTemplate = useConventional ? CONVENTIONAL_COMMIT_PROMPT : COMMIT_PROMPT;
11288
+ const promptTemplate = USE_CONVENTIONAL_COMMITS ? CONVENTIONAL_COMMIT_PROMPT : COMMIT_PROMPT;
11154
11289
  const prompt = getPrompt({
11155
11290
  template: options.prompt,
11156
11291
  variables: promptTemplate.inputVariables,
@@ -11180,6 +11315,13 @@ const handler$3 = async (argv, logger) => {
11180
11315
  : config.includeBranchName !== false; // Default to true if not explicitly set to false
11181
11316
  // Create branch name context string based on the configuration
11182
11317
  const branchNameContext = includeBranchName ? `Current git branch name: ${branchName}` : '';
11318
+ // Load commitlint rules context if available
11319
+ const hasCommitLintConfig = await hasCommitlintConfig();
11320
+ let commitlint_rules_context = '';
11321
+ if (USE_CONVENTIONAL_COMMITS || hasCommitLintConfig) {
11322
+ const { getCommitlintRulesContext } = await Promise.resolve().then(function () { return commitlintValidator; });
11323
+ commitlint_rules_context = await getCommitlintRulesContext();
11324
+ }
11183
11325
  // Get variables for the prompt
11184
11326
  const variables = {
11185
11327
  summary: context,
@@ -11187,59 +11329,106 @@ const handler$3 = async (argv, logger) => {
11187
11329
  additional_context: additional_context,
11188
11330
  commit_history: commit_history,
11189
11331
  branch_name_context: branchNameContext,
11332
+ commitlint_rules_context: commitlint_rules_context,
11190
11333
  };
11191
11334
  const maxAttempts = config.service.provider === 'ollama' && 'maxParsingAttempts' in config.service
11192
11335
  ? config.service.maxParsingAttempts || 3
11193
11336
  : 3;
11194
- const commitMsg = await executeChainWithSchema(schema, llm, prompt, variables, {
11195
- retryOptions: {
11196
- maxAttempts,
11197
- onRetry: (attempt, error) => {
11198
- logger.verbose(`Failed to parse commit message (attempt ${attempt}/${maxAttempts}): ${error.message}`, { color: 'yellow' });
11337
+ // Custom retry logic for commitlint validation
11338
+ let retryCount = 0;
11339
+ let validationErrors = '';
11340
+ const generateCommitMessage = async () => {
11341
+ // Update variables with validation errors for retry attempts
11342
+ const currentVariables = {
11343
+ ...variables,
11344
+ additional_context: validationErrors
11345
+ ? `${variables.additional_context}\n\n## Validation Errors from Previous Attempt\nPlease fix the following issues:\n${validationErrors}`
11346
+ : variables.additional_context,
11347
+ };
11348
+ const commitMsg = await executeChainWithSchema(schema, llm, prompt, currentVariables, {
11349
+ retryOptions: {
11350
+ maxAttempts,
11351
+ onRetry: (attempt, error) => {
11352
+ logger.verbose(`Failed to parse commit message (attempt ${attempt}/${maxAttempts}): ${error.message}`, { color: 'yellow' });
11353
+ },
11199
11354
  },
11200
- },
11201
- fallbackParser: (text) => ({
11202
- title: text.split('\n')[0] || 'Auto-generated commit',
11203
- body: text.split('\n').slice(1).join('\n') || 'Generated commit message',
11204
- }),
11205
- onFallback: () => {
11206
- logger.verbose('Max retry attempts reached. Falling back to simple text output.', {
11207
- color: 'red',
11355
+ fallbackParser: (text) => ({
11356
+ title: text.split('\n')[0] || 'Auto-generated commit',
11357
+ body: text.split('\n').slice(1).join('\n') || 'Generated commit message',
11358
+ }),
11359
+ onFallback: () => {
11360
+ logger.verbose('Max retry attempts reached. Falling back to simple text output.', {
11361
+ color: 'red',
11362
+ });
11363
+ },
11364
+ });
11365
+ // Construct the full commit message
11366
+ const appendedText = argv.append ? `\n\n${argv.append}` : '';
11367
+ const ticketId = extractTicketIdFromBranchName(branchName);
11368
+ const ticketFooter = argv.appendTicket && ticketId ? `\n\nPart of **${ticketId}**` : '';
11369
+ const fullMessage = `${commitMsg.title}\n\n${commitMsg.body}${appendedText}${ticketFooter}`;
11370
+ // If commitlint validation is needed, validate the message
11371
+ if (USE_CONVENTIONAL_COMMITS || hasCommitLintConfig) {
11372
+ const { validateCommitMessage, CommitlintValidationError } = await Promise.resolve().then(function () { return commitlintValidator; });
11373
+ const validationResult = await validateCommitMessage(fullMessage);
11374
+ logger.verbose(`Validation result: ${JSON.stringify(validationResult)}`, {
11375
+ color: 'yellow',
11208
11376
  });
11377
+ if (!validationResult.valid) {
11378
+ retryCount++;
11379
+ // Format validation errors for next attempt
11380
+ validationErrors = validationResult.errors.map((error) => `- ${error}`).join('\n');
11381
+ // Auto-retry up to 2 times
11382
+ if (retryCount <= 2) {
11383
+ logger.verbose(`Commit message validation failed (attempt ${retryCount}/2). Retrying with error feedback...`, { color: 'yellow' });
11384
+ throw new CommitlintValidationError(`Validation failed: ${validationResult.errors.join('; ')}`, validationResult, fullMessage);
11385
+ }
11386
+ // After 2 failed attempts, let the user decide
11387
+ const { handleValidationErrors } = await Promise.resolve().then(function () { return commitValidationHandler; });
11388
+ const validationHandlerResult = await handleValidationErrors(fullMessage, validationResult, {
11389
+ logger,
11390
+ interactive: INTERACTIVE,
11391
+ openInEditor: config.openInEditor,
11392
+ });
11393
+ logger.verbose(`Validation handler result: ${JSON.stringify(validationHandlerResult)}`, {
11394
+ color: 'blue',
11395
+ });
11396
+ switch (validationHandlerResult.action) {
11397
+ case 'proceed':
11398
+ return validationHandlerResult.message;
11399
+ case 'edit':
11400
+ return validationHandlerResult.message;
11401
+ case 'regenerate':
11402
+ // Reset retry count and validation errors for fresh attempts
11403
+ retryCount = 0;
11404
+ validationErrors = '';
11405
+ throw new CommitlintValidationError('User requested regeneration', validationResult, fullMessage);
11406
+ case 'abort':
11407
+ logger.log('\nAborting commit due to validation errors.', { color: 'red' });
11408
+ process.exit(1);
11409
+ }
11410
+ }
11411
+ }
11412
+ return fullMessage;
11413
+ };
11414
+ // Custom shouldRetry function for commitlint errors
11415
+ const shouldRetryCommitlint = (error) => {
11416
+ return error.name === 'CommitlintValidationError';
11417
+ };
11418
+ // Use retry wrapper for commitlint validation with up to 4 total attempts
11419
+ // (2 automatic retries + 2 more if user chooses "Try again")
11420
+ return await withRetry(generateCommitMessage, {
11421
+ maxAttempts: 6, // Allow for multiple user retry requests
11422
+ shouldRetry: shouldRetryCommitlint,
11423
+ backoffMs: 0, // No delay needed for commitlint retries
11424
+ onRetry: (attempt, error) => {
11425
+ if (error.name === 'CommitlintValidationError' && attempt <= 2) {
11426
+ // Don't log for auto-retries, we already log in the function
11427
+ return;
11428
+ }
11429
+ logger.verbose(`Retrying commit message generation (attempt ${attempt}): ${error.message}`, { color: 'yellow' });
11209
11430
  },
11210
11431
  });
11211
- // Construct the full commit message
11212
- const appendedText = argv.append ? `\n\n${argv.append}` : '';
11213
- const ticketId = extractTicketIdFromBranchName(branchName);
11214
- const ticketFooter = argv.appendTicket && ticketId ? `\n\nPart of **${ticketId}**` : '';
11215
- const fullMessage = `${commitMsg.title}\n\n${commitMsg.body}${appendedText}${ticketFooter}`;
11216
- // If conventional commits are enabled, validate with commitlint
11217
- if (useConventional) {
11218
- const { validateCommitMessage } = await Promise.resolve().then(function () { return commitlintValidator; });
11219
- const { handleValidationErrors } = await Promise.resolve().then(function () { return commitValidationHandler; });
11220
- const validationResult = await validateCommitMessage(fullMessage);
11221
- const validationHandlerResult = await handleValidationErrors(fullMessage, validationResult, {
11222
- logger,
11223
- interactive: INTERACTIVE,
11224
- openInEditor: config.openInEditor,
11225
- });
11226
- switch (validationHandlerResult.action) {
11227
- case 'proceed':
11228
- // Validation passed, use the message as is
11229
- return validationHandlerResult.message;
11230
- case 'edit':
11231
- // User edited the message, use the edited version
11232
- return validationHandlerResult.message;
11233
- case 'regenerate':
11234
- // User wants to regenerate, throw special error to trigger regeneration
11235
- throw new Error('REGENERATE_COMMIT_MESSAGE');
11236
- case 'abort':
11237
- // User wants to abort or validation failed in non-interactive mode
11238
- logger.log('\nAborting commit due to validation errors.', { color: 'red' });
11239
- process.exit(1);
11240
- }
11241
- }
11242
- return fullMessage;
11243
11432
  },
11244
11433
  noResult: async () => {
11245
11434
  await noResult$2({ git, logger });
@@ -11280,28 +11469,6 @@ const builder$2 = (yargs) => {
11280
11469
  return yargs.options(options$2).usage(getCommandUsageHeader(command$2));
11281
11470
  };
11282
11471
 
11283
- /**
11284
- * Finds the project root directory starting from the given current directory.
11285
- * It checks if the `.git` directory or `package.json` file exists in the current directory or any of its parent directories.
11286
- * If found, it returns the path to the project root directory.
11287
- * If not found, it throws an error.
11288
- *
11289
- * @param currentDir - The current directory to start searching from.
11290
- * @returns The path to the project root directory.
11291
- * @throws Error if the project root directory cannot be found.
11292
- */
11293
- function findProjectRoot(currentDir) {
11294
- const root = path.parse(currentDir).root;
11295
- while (currentDir !== root) {
11296
- if (fs.existsSync(path.join(currentDir, '.git')) ||
11297
- fs.existsSync(path.join(currentDir, 'package.json'))) {
11298
- return currentDir;
11299
- }
11300
- currentDir = path.dirname(currentDir);
11301
- }
11302
- throw new Error('Unable to find project root. Are you in the right directory?');
11303
- }
11304
-
11305
11472
  /**
11306
11473
  * Executes a command as a Promise and returns the result.
11307
11474
  *
@@ -13221,11 +13388,13 @@ function isObservable(obj) {
13221
13388
  return !!obj && (obj instanceof Observable || (isFunction(obj.lift) && isFunction(obj.subscribe)));
13222
13389
  }
13223
13390
 
13224
- var EmptyError = createErrorClass(function (_super) { return function EmptyErrorImpl() {
13225
- _super(this);
13226
- this.name = 'EmptyError';
13227
- this.message = 'no elements in sequence';
13228
- }; });
13391
+ var EmptyError = createErrorClass(function (_super) {
13392
+ return function EmptyErrorImpl() {
13393
+ _super(this);
13394
+ this.name = 'EmptyError';
13395
+ this.message = 'no elements in sequence';
13396
+ };
13397
+ });
13229
13398
 
13230
13399
  function lastValueFrom(source, config) {
13231
13400
  var hasConfig = typeof config === 'object';
@@ -14512,23 +14681,183 @@ y.command(review.command, review.desc, review.builder, review.handler);
14512
14681
  y.command(init.command, init.desc, init.builder, init.handler);
14513
14682
  y.help().parse(process.argv.slice(2));
14514
14683
 
14684
+ /**
14685
+ * Edit a commit message with commitlint validation if config exists
14686
+ */
14687
+ async function editCommitMessage(message, options) {
14688
+ // First, let the user edit the message
14689
+ const editedMessage = await editResult(message, options);
14690
+ // Then validate it against commitlint if config exists
14691
+ const { hasCommitlintConfig } = await Promise.resolve().then(function () { return hasCommitlintConfig$1; });
14692
+ const hasConfig = await hasCommitlintConfig();
14693
+ if (hasConfig) {
14694
+ const { validateCommitMessage } = await Promise.resolve().then(function () { return commitlintValidator; });
14695
+ const { handleValidationErrors } = await Promise.resolve().then(function () { return commitValidationHandler; });
14696
+ const validationResult = await validateCommitMessage(editedMessage);
14697
+ if (!validationResult.valid) {
14698
+ // Show validation errors and get user action
14699
+ const validationHandlerResult = await handleValidationErrors(editedMessage, validationResult, {
14700
+ logger: options.logger,
14701
+ interactive: options.interactive,
14702
+ openInEditor: options.openInEditor,
14703
+ });
14704
+ // Return the result from the validation handler
14705
+ return validationHandlerResult.message;
14706
+ }
14707
+ }
14708
+ return editedMessage;
14709
+ }
14710
+
14711
+ var editCommitMessage$1 = /*#__PURE__*/Object.freeze({
14712
+ __proto__: null,
14713
+ editCommitMessage: editCommitMessage
14714
+ });
14715
+
14716
+ /**
14717
+ * Custom error for commitlint validation failures
14718
+ * This allows the retry system to identify these errors specifically
14719
+ */
14720
+ class CommitlintValidationError extends Error {
14721
+ constructor(message, validationResult, commitMessage) {
14722
+ super(message);
14723
+ this.name = 'CommitlintValidationError';
14724
+ this.validationResult = validationResult;
14725
+ this.commitMessage = commitMessage;
14726
+ }
14727
+ }
14515
14728
  /**
14516
14729
  * Load commitlint configuration
14517
14730
  */
14518
14731
  async function loadCommitlintConfig() {
14519
- // Dynamically import commitlint core
14520
- const commitlint = await import('@commitlint/core');
14521
- const { load } = commitlint;
14732
+ const projectRoot = findProjectRoot(process.cwd());
14733
+ const cwd = projectRoot || process.cwd();
14734
+ // @commitlint/load has issues with ESM configs (e.g. commitlint.config.js with `export default`).
14735
+ // Let's try to load them manually first.
14736
+ const esmConfigCandidates = COMMITLINT_CONFIG_FILES.filter((file) => file.endsWith('.js'));
14737
+ for (const configFile of esmConfigCandidates) {
14738
+ const configPath = path.join(cwd, configFile);
14739
+ if (fs.existsSync(configPath)) {
14740
+ try {
14741
+ const module = await import(url.pathToFileURL(configPath).href);
14742
+ if (module.default &&
14743
+ (Object.keys(module.default.rules || {}).length > 0 ||
14744
+ (module.default.extends && module.default.extends.length > 0))) {
14745
+ // We found a config, now let commitlint process it (for extends etc)
14746
+ return await core$1.load(module.default, { cwd });
14747
+ }
14748
+ }
14749
+ catch (error) {
14750
+ // Failed to import, maybe not an ESM file after all or syntax error.
14751
+ // We will let the standard load take a chance.
14752
+ }
14753
+ }
14754
+ }
14522
14755
  try {
14523
- // Try to load project config
14524
- const config = await load();
14525
- return config;
14756
+ // Let @commitlint/load try to find the config. This works for CJS, JSON, and YAML.
14757
+ const config = await core$1.load({}, { cwd });
14758
+ // Check if a real config was loaded.
14759
+ if (config.extends.length > 0 || Object.keys(config.rules).length > 0) {
14760
+ return config;
14761
+ }
14526
14762
  }
14527
- catch {
14528
- // If no config found or error loading, use conventional config
14529
- return load({
14530
- extends: ['@commitlint/config-conventional'],
14531
- });
14763
+ catch (error) {
14764
+ // Could be an error parsing, or just not found. Fall through to default.
14765
+ }
14766
+ // If nothing worked, fallback to conventional config
14767
+ return core$1.load({
14768
+ extends: ['@commitlint/config-conventional'],
14769
+ });
14770
+ }
14771
+ /**
14772
+ * Format commitlint rules into a human-readable string for AI prompts
14773
+ */
14774
+ function formatCommitlintRulesForPrompt(config) {
14775
+ if (!config.rules || Object.keys(config.rules).length === 0) {
14776
+ return '';
14777
+ }
14778
+ const ruleDescriptions = [];
14779
+ // Add information about extends if present
14780
+ if (config.extends && config.extends.length > 0) {
14781
+ ruleDescriptions.push(`Following ${config.extends.join(', ')} configuration`);
14782
+ }
14783
+ // Process key rules that affect commit message format
14784
+ const rules = config.rules;
14785
+ // Header length rules
14786
+ if (rules['header-max-length']) {
14787
+ const [level, , maxLength] = rules['header-max-length'];
14788
+ if (level > 0) {
14789
+ ruleDescriptions.push(`Header (title) must be ${maxLength} characters or less (including spaces)`);
14790
+ }
14791
+ }
14792
+ if (rules['header-min-length']) {
14793
+ const [level, , minLength] = rules['header-min-length'];
14794
+ if (level > 0) {
14795
+ ruleDescriptions.push(`Header (title) must be at least ${minLength} characters (including spaces)`);
14796
+ }
14797
+ }
14798
+ // Body length rules
14799
+ if (rules['body-max-line-length']) {
14800
+ const [level, , maxLength] = rules['body-max-line-length'];
14801
+ if (level > 0) {
14802
+ ruleDescriptions.push(`Body lines must be ${maxLength} characters or less (including spaces)`);
14803
+ }
14804
+ }
14805
+ // Type rules
14806
+ if (rules['type-enum']) {
14807
+ const [level, , allowedTypes] = rules['type-enum'];
14808
+ if (level > 0 && Array.isArray(allowedTypes)) {
14809
+ ruleDescriptions.push(`Allowed types: ${allowedTypes.join(', ')}`);
14810
+ }
14811
+ }
14812
+ // Case rules
14813
+ if (rules['type-case']) {
14814
+ const [level, , caseType] = rules['type-case'];
14815
+ if (level > 0) {
14816
+ ruleDescriptions.push(`Type must be ${caseType} case`);
14817
+ }
14818
+ }
14819
+ if (rules['subject-case']) {
14820
+ const [level, , caseType] = rules['subject-case'];
14821
+ if (level > 0) {
14822
+ ruleDescriptions.push(`Subject must be ${caseType} case`);
14823
+ }
14824
+ }
14825
+ // Scope rules
14826
+ if (rules['scope-enum']) {
14827
+ const [level, , allowedScopes] = rules['scope-enum'];
14828
+ if (level > 0 && Array.isArray(allowedScopes)) {
14829
+ ruleDescriptions.push(`Allowed scopes: ${allowedScopes.join(', ')}`);
14830
+ }
14831
+ }
14832
+ // Subject rules
14833
+ if (rules['subject-full-stop']) {
14834
+ const [level, condition] = rules['subject-full-stop'];
14835
+ if (level > 0) {
14836
+ const verb = condition === 'always' ? 'must' : 'must not';
14837
+ ruleDescriptions.push(`Subject ${verb} end with a period`);
14838
+ }
14839
+ }
14840
+ if (rules['subject-empty']) {
14841
+ const [level, condition] = rules['subject-empty'];
14842
+ if (level > 0) {
14843
+ const requirement = condition === 'never' ? 'must not be empty' : 'must be empty';
14844
+ ruleDescriptions.push(`Subject ${requirement}`);
14845
+ }
14846
+ }
14847
+ return ruleDescriptions.length > 0
14848
+ ? `## Commitlint Rules\nYour commit message must follow these project-specific rules:\n${ruleDescriptions.map(rule => `- ${rule}`).join('\n')}\n`
14849
+ : '';
14850
+ }
14851
+ /**
14852
+ * Get commitlint rules context for prompt if config exists
14853
+ */
14854
+ async function getCommitlintRulesContext() {
14855
+ try {
14856
+ const config = await loadCommitlintConfig();
14857
+ return formatCommitlintRulesForPrompt(config);
14858
+ }
14859
+ catch (error) {
14860
+ return '';
14532
14861
  }
14533
14862
  }
14534
14863
  /**
@@ -14537,10 +14866,7 @@ async function loadCommitlintConfig() {
14537
14866
  async function validateCommitMessage(message, options = {}) {
14538
14867
  try {
14539
14868
  const config = await loadCommitlintConfig();
14540
- // Dynamically import commitlint lint function
14541
- const commitlint = await import('@commitlint/core');
14542
- const { lint } = commitlint;
14543
- const result = await lint(message, config.rules, options);
14869
+ const result = await core$1.lint(message, config.rules, options);
14544
14870
  return {
14545
14871
  valid: result.valid,
14546
14872
  errors: result.errors.map((error) => error.message),
@@ -14558,6 +14884,9 @@ async function validateCommitMessage(message, options = {}) {
14558
14884
 
14559
14885
  var commitlintValidator = /*#__PURE__*/Object.freeze({
14560
14886
  __proto__: null,
14887
+ CommitlintValidationError: CommitlintValidationError,
14888
+ formatCommitlintRulesForPrompt: formatCommitlintRulesForPrompt,
14889
+ getCommitlintRulesContext: getCommitlintRulesContext,
14561
14890
  loadCommitlintConfig: loadCommitlintConfig,
14562
14891
  validateCommitMessage: validateCommitMessage
14563
14892
  });
@@ -14594,29 +14923,45 @@ async function handleValidationErrors(message, validationResult, options) {
14594
14923
  message: 'How would you like to proceed?:',
14595
14924
  choices: [
14596
14925
  {
14597
- name: 'Edit',
14598
- value: 'edit',
14599
- description: 'Edit the commit message manually',
14926
+ name: 'Try 2 more attempts',
14927
+ value: 'retry',
14928
+ description: 'Let the AI try generating 2 more commit messages with error feedback',
14600
14929
  },
14601
14930
  {
14602
- name: 'Retry',
14603
- value: 'retry',
14604
- description: 'Regenerate a new commit message',
14931
+ name: 'Edit manually',
14932
+ value: 'edit',
14933
+ description: 'Edit the commit message manually to fix the issues',
14605
14934
  },
14606
14935
  {
14607
14936
  name: 'Abort',
14608
14937
  value: 'abort',
14609
- description: 'Abort the commit',
14938
+ description: 'Abort the commit process',
14610
14939
  },
14611
14940
  ],
14612
14941
  });
14613
14942
  switch (choice) {
14614
- case '1': {
14943
+ case 'edit': {
14615
14944
  // Edit message manually
14616
14945
  const editedMessage = await editResult(message, options);
14946
+ // Validate the manually edited message if commitlint config exists
14947
+ const { hasCommitlintConfig } = await Promise.resolve().then(function () { return hasCommitlintConfig$1; });
14948
+ const hasConfig = await hasCommitlintConfig();
14949
+ if (hasConfig) {
14950
+ const { validateCommitMessage } = await Promise.resolve().then(function () { return commitlintValidator; });
14951
+ const editedValidationResult = await validateCommitMessage(editedMessage);
14952
+ if (!editedValidationResult.valid) {
14953
+ // Show validation errors for the edited message
14954
+ options.logger.log('\nEdited commit message also has validation issues:', { color: 'yellow' });
14955
+ editedValidationResult.errors.forEach((error) => {
14956
+ options.logger.log(` • ${error}`, { color: 'red' });
14957
+ });
14958
+ // Recursively handle validation errors for the edited message
14959
+ return await handleValidationErrors(editedMessage, editedValidationResult, options);
14960
+ }
14961
+ }
14617
14962
  return { message: editedMessage, action: 'edit' };
14618
14963
  }
14619
- case '2':
14964
+ case 'retry':
14620
14965
  // Regenerate message
14621
14966
  return { message, action: 'regenerate' };
14622
14967
  default: