git-coco 0.20.1 → 0.21.1

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.
@@ -4,13 +4,13 @@ import { ConditionalPromptSelector, isChatModel } from '@langchain/core/example_
4
4
  import yargs from 'yargs';
5
5
  import chalk from 'chalk';
6
6
  import * as fs from 'fs';
7
- import fs__default, { promises } from 'fs';
7
+ import fs__default, { promises, existsSync, readFileSync } from 'fs';
8
8
  import { confirm, editor, select, password, input, Separator, number, rawlist, expand, checkbox, search } from '@inquirer/prompts';
9
9
  import * as ini from 'ini';
10
10
  import * as os from 'os';
11
11
  import os__default from 'os';
12
12
  import * as path from 'path';
13
- import path__default from 'path';
13
+ import path__default, { join } from 'path';
14
14
  import Ajv from 'ajv';
15
15
  import ora from 'ora';
16
16
  import now from 'performance-now';
@@ -19,6 +19,8 @@ import { ChatAnthropic } from '@langchain/anthropic';
19
19
  import { ChatOllama } from '@langchain/ollama';
20
20
  import { ChatOpenAI } from '@langchain/openai';
21
21
  import { BaseOutputParser, OutputParserException, StructuredOutputParser, StringOutputParser } from '@langchain/core/output_parsers';
22
+ import '@langchain/core/utils/json_schema';
23
+ import '@langchain/core/utils/types';
22
24
  import { BaseLangChain, BaseLanguageModel } from '@langchain/core/language_models/base';
23
25
  import { ensureConfig, Runnable } from '@langchain/core/runnables';
24
26
  import { RUN_KEY } from '@langchain/core/outputs';
@@ -40,13 +42,15 @@ import { exec } from 'child_process';
40
42
  import readline$1 from 'node:readline';
41
43
  import require$$0 from 'stream';
42
44
  import * as readline from 'readline';
45
+ import { lint, load } from '@commitlint/core';
46
+ import { pathToFileURL } from 'url';
43
47
 
44
48
  // This file is auto-generated - DO NOT EDIT
45
49
  /* eslint-disable */
46
50
  /**
47
51
  * Current build version from package.json
48
52
  */
49
- const BUILD_VERSION = "0.20.1";
53
+ const BUILD_VERSION = "0.21.1";
50
54
 
51
55
  const isInteractive = (config) => {
52
56
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -1178,6 +1182,8 @@ const schema$1 = {
1178
1182
  "text-davinci-edit-001",
1179
1183
  "code-davinci-edit-001",
1180
1184
  "text-embedding-ada-002",
1185
+ "text-embedding-3-small",
1186
+ "text-embedding-3-large",
1181
1187
  "text-similarity-davinci-001",
1182
1188
  "text-similarity-curie-001",
1183
1189
  "text-similarity-babbage-001",
@@ -1213,10 +1219,54 @@ const schema$1 = {
1213
1219
  "gpt-4-vision-preview",
1214
1220
  "gpt-4o",
1215
1221
  "gpt-4o-2024-05-13",
1222
+ "gpt-4o-2024-08-06",
1223
+ "gpt-4o-2024-11-20",
1224
+ "gpt-4o-mini-2024-07-18",
1216
1225
  "gpt-4o-mini",
1226
+ "gpt-4o-search-preview",
1227
+ "gpt-4o-search-preview-2025-03-11",
1228
+ "gpt-4o-mini-search-preview",
1229
+ "gpt-4o-mini-search-preview-2025-03-11",
1230
+ "gpt-4o-audio-preview",
1231
+ "gpt-4o-audio-preview-2024-12-17",
1232
+ "gpt-4o-audio-preview-2024-10-01",
1233
+ "gpt-4o-mini-audio-preview",
1234
+ "gpt-4o-mini-audio-preview-2024-12-17",
1235
+ "o1",
1236
+ "o1-2024-12-17",
1237
+ "o1-mini",
1238
+ "o1-mini-2024-09-12",
1239
+ "o1-preview",
1240
+ "o1-preview-2024-09-12",
1241
+ "o1-pro",
1242
+ "o1-pro-2025-03-19",
1243
+ "o3",
1244
+ "o3-2025-04-16",
1245
+ "o3-mini",
1246
+ "o3-mini-2025-01-31",
1247
+ "o4-mini",
1248
+ "o4-mini-2025-04-16",
1249
+ "chatgpt-4o-latest",
1250
+ "gpt-4o-realtime",
1251
+ "gpt-4o-realtime-preview-2024-10-01",
1252
+ "gpt-4o-realtime-preview-2024-12-17",
1253
+ "gpt-4o-mini-realtime-preview",
1254
+ "gpt-4o-mini-realtime-preview-2024-12-17",
1217
1255
  "gpt-4.1",
1256
+ "gpt-4.1-2025-04-14",
1218
1257
  "gpt-4.1-mini",
1219
- "gpt-4.1-nano"
1258
+ "gpt-4.1-mini-2025-04-14",
1259
+ "gpt-4.1-nano",
1260
+ "gpt-4.1-nano-2025-04-14",
1261
+ "gpt-4.5-preview",
1262
+ "gpt-4.5-preview-2025-02-27",
1263
+ "gpt-5",
1264
+ "gpt-5-2025-08-07",
1265
+ "gpt-5-nano",
1266
+ "gpt-5-nano-2025-08-07",
1267
+ "gpt-5-mini",
1268
+ "gpt-5-mini-2025-08-07",
1269
+ "gpt-5-chat-latest"
1220
1270
  ]
1221
1271
  },
1222
1272
  "OllamaModel": {
@@ -1478,6 +1528,13 @@ const schema$1 = {
1478
1528
  "ChatModel": {
1479
1529
  "type": "string",
1480
1530
  "enum": [
1531
+ "gpt-5",
1532
+ "gpt-5-mini",
1533
+ "gpt-5-nano",
1534
+ "gpt-5-2025-08-07",
1535
+ "gpt-5-mini-2025-08-07",
1536
+ "gpt-5-nano-2025-08-07",
1537
+ "gpt-5-chat-latest",
1481
1538
  "gpt-4.1",
1482
1539
  "gpt-4.1-mini",
1483
1540
  "gpt-4.1-nano",
@@ -6201,8 +6258,6 @@ function getPrompt({ template, variables, fallback }) {
6201
6258
  throw new LangChainExecutionError('getPrompt: Unexpected execution path - neither template nor fallback available', { template, fallback, variables });
6202
6259
  }
6203
6260
 
6204
- new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789");
6205
-
6206
6261
  /**
6207
6262
  * Base interface that all chains must implement.
6208
6263
  */
@@ -6305,7 +6360,7 @@ class BaseChain extends BaseLangChain {
6305
6360
  async run(
6306
6361
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6307
6362
  input, config) {
6308
- const inputKeys = this.inputKeys.filter((k) => !this.memory?.memoryKeys.includes(k) ?? true);
6363
+ const inputKeys = this.inputKeys.filter((k) => !this.memory?.memoryKeys.includes(k));
6309
6364
  const isKeylessInput = inputKeys.length <= 1;
6310
6365
  if (!isKeylessInput) {
6311
6366
  throw new Error(`Chain ${this._chainType()} expects multiple inputs, cannot use 'run' `);
@@ -6476,7 +6531,7 @@ function _getLanguageModel(llmLike) {
6476
6531
  * import { ChatOpenAI } from "@langchain/openai";
6477
6532
  *
6478
6533
  * const prompt = ChatPromptTemplate.fromTemplate("Tell me a {adjective} joke");
6479
- * const llm = new ChatOpenAI();
6534
+ * const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
6480
6535
  * const chain = prompt.pipe(llm);
6481
6536
  *
6482
6537
  * const response = await chain.invoke({ adjective: "funny" });
@@ -7092,6 +7147,8 @@ Please follow the guidelines below when writing your commit message:
7092
7147
 
7093
7148
  {{branch_name_context}}
7094
7149
 
7150
+ {{commitlint_rules_context}}
7151
+
7095
7152
  {{format_instructions}}
7096
7153
 
7097
7154
  {{commit_history}}
@@ -7099,7 +7156,7 @@ Please follow the guidelines below when writing your commit message:
7099
7156
  {{additional_context}}
7100
7157
  `;
7101
7158
  // Define the variables that will be passed to the prompt template
7102
- const inputVariables$3 = ['summary', 'format_instructions', 'additional_context', 'commit_history', 'branch_name_context'];
7159
+ const inputVariables$3 = ['summary', 'format_instructions', 'additional_context', 'commit_history', 'branch_name_context', 'commitlint_rules_context'];
7103
7160
  const COMMIT_PROMPT = new PromptTemplate({
7104
7161
  template: template$4,
7105
7162
  inputVariables: inputVariables$3,
@@ -7141,23 +7198,20 @@ Based on the following diff summary, generate a conventional commit message that
7141
7198
 
7142
7199
  {{branch_name_context}}
7143
7200
 
7201
+ {{commitlint_rules_context}}
7202
+
7144
7203
  {{format_instructions}}
7145
7204
 
7146
7205
  {{commit_history}}
7147
7206
 
7148
- {{additional_context}}
7149
-
7150
- Remember:
7151
- - Be concise and precise
7152
- - Focus on WHAT and WHY, not HOW
7153
- - Use imperative mood in both title and body
7154
- - Ensure the title alone provides enough context to understand the change`;
7207
+ {{additional_context}}`;
7155
7208
  const conventionalInputVariables = [
7156
7209
  'summary',
7157
7210
  'additional_context',
7158
7211
  'commit_history',
7159
7212
  'format_instructions',
7160
7213
  'branch_name_context',
7214
+ 'commitlint_rules_context',
7161
7215
  ];
7162
7216
  const CONVENTIONAL_COMMIT_PROMPT = new PromptTemplate({
7163
7217
  template: CONVENTIONAL_TEMPLATE,
@@ -7366,7 +7420,9 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
7366
7420
  continue;
7367
7421
  }
7368
7422
  // Only edit the result in interactive mode if approved
7369
- result = await editResult(result, options);
7423
+ // Use custom edit function if provided, otherwise use default editResult
7424
+ const editFunction = options.review?.customEditFunction || editResult;
7425
+ result = await editFunction(result, options);
7370
7426
  }
7371
7427
  else {
7372
7428
  // In non-interactive mode, we return the result as is to be output to stdout by the caller.
@@ -7550,17 +7606,17 @@ var changelog = {
7550
7606
  const conventionalTypeRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?:/;
7551
7607
  // Regular commit message schema with basic validation
7552
7608
  const CommitMessageResponseSchema = objectType({
7553
- title: stringType(),
7554
- body: stringType(),
7555
- });
7609
+ title: stringType().describe("Title of the commit message"),
7610
+ body: stringType().describe("Body of the commit message"),
7611
+ }).describe("Object with commit message 'title' and 'body'");
7556
7612
  // Conventional commit message schema with strict formatting rules
7557
7613
  const ConventionalCommitMessageResponseSchema = objectType({
7558
7614
  title: stringType()
7559
7615
  .max(50, "Title must be 50 characters or less")
7560
- .refine((title) => conventionalTypeRegex.test(title), "Title must follow Conventional Commits format (e.g., 'feat: add new feature' or 'fix(scope): fix bug')"),
7561
- body: stringType()
7616
+ .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"),
7617
+ body: stringType().describe("Body of the commit message")
7562
7618
  // .max(280, "Body must be 280 characters or less"),
7563
- });
7619
+ }).describe("Object with Conventional Commit message 'title' and 'body' adhering to Conventional Commits specification");
7564
7620
  const command$3 = 'commit';
7565
7621
  /**
7566
7622
  * Command line options via yargs
@@ -8219,12 +8275,12 @@ function formatSet(input) {
8219
8275
  * const overallChain = new SequentialChain({
8220
8276
  * chains: [
8221
8277
  * new LLMChain({
8222
- * llm: new ChatOpenAI({ temperature: 0 }),
8278
+ * llm: new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 }),
8223
8279
  * prompt: promptTemplate,
8224
8280
  * outputKey: "synopsis",
8225
8281
  * }),
8226
8282
  * new LLMChain({
8227
- * llm: new OpenAI({ temperature: 0 }),
8283
+ * llm: new OpenAI({ model: "gpt-4o-mini", temperature: 0 }),
8228
8284
  * prompt: reviewPromptTemplate,
8229
8285
  * outputKey: "review",
8230
8286
  * }),
@@ -8461,7 +8517,8 @@ class SimpleSequentialChain extends BaseChain {
8461
8517
  /** @ignore */
8462
8518
  _validateChains() {
8463
8519
  for (const chain of this.chains) {
8464
- if (chain.inputKeys.filter((k) => !chain.memory?.memoryKeys.includes(k) ?? true).length !== 1) {
8520
+ if (chain.inputKeys.filter((k) => !chain.memory?.memoryKeys.includes(k))
8521
+ .length !== 1) {
8465
8522
  throw new Error(`Chains used in SimpleSequentialChain should all have one input, got ${chain.inputKeys.length} for ${chain._chainType()}.`);
8466
8523
  }
8467
8524
  if (chain.outputKeys.length !== 1) {
@@ -11015,6 +11072,75 @@ const getTokenCounter = async (modelName) => {
11015
11072
  });
11016
11073
  };
11017
11074
 
11075
+ const COMMITLINT_CONFIG_FILES = [
11076
+ '.commitlintrc',
11077
+ '.commitlintrc.json',
11078
+ '.commitlintrc.yaml',
11079
+ '.commitlintrc.yml',
11080
+ '.commitlintrc.js',
11081
+ '.commitlintrc.cjs',
11082
+ 'commitlint.config.js',
11083
+ 'commitlint.config.cjs',
11084
+ ];
11085
+
11086
+ /**
11087
+ * Finds the project root directory starting from the given current directory.
11088
+ * It checks if the `.git` directory or `package.json` file exists in the current directory or any of its parent directories.
11089
+ * If found, it returns the path to the project root directory.
11090
+ * If not found, it throws an error.
11091
+ *
11092
+ * @param currentDir - The current directory to start searching from.
11093
+ * @returns The path to the project root directory.
11094
+ * @throws Error if the project root directory cannot be found.
11095
+ */
11096
+ function findProjectRoot(currentDir) {
11097
+ const root = path__default.parse(currentDir).root;
11098
+ while (currentDir !== root) {
11099
+ if (fs__default.existsSync(path__default.join(currentDir, '.git')) ||
11100
+ fs__default.existsSync(path__default.join(currentDir, 'package.json'))) {
11101
+ return currentDir;
11102
+ }
11103
+ currentDir = path__default.dirname(currentDir);
11104
+ }
11105
+ throw new Error('Unable to find project root. Are you in the right directory?');
11106
+ }
11107
+
11108
+ /**
11109
+ * Check if a commitlint configuration exists in the project root.
11110
+ */
11111
+ async function hasCommitlintConfig() {
11112
+ const projectRoot = findProjectRoot(process.cwd());
11113
+ if (!projectRoot) {
11114
+ return false;
11115
+ }
11116
+ // Check for dedicated commitlint config files
11117
+ for (const file of COMMITLINT_CONFIG_FILES) {
11118
+ if (existsSync(join(projectRoot, file))) {
11119
+ return true;
11120
+ }
11121
+ }
11122
+ // Check for commitlint config in package.json
11123
+ const pkgPath = join(projectRoot, 'package.json');
11124
+ if (existsSync(pkgPath)) {
11125
+ try {
11126
+ const pkgContent = readFileSync(pkgPath, 'utf8');
11127
+ const pkg = JSON.parse(pkgContent);
11128
+ if (pkg.commitlint) {
11129
+ return true;
11130
+ }
11131
+ }
11132
+ catch (error) {
11133
+ // Ignore errors reading or parsing package.json
11134
+ }
11135
+ }
11136
+ return false;
11137
+ }
11138
+
11139
+ var hasCommitlintConfig$1 = /*#__PURE__*/Object.freeze({
11140
+ __proto__: null,
11141
+ hasCommitlintConfig: hasCommitlintConfig
11142
+ });
11143
+
11018
11144
  async function noResult$2({ git, logger }) {
11019
11145
  const { staged, unstaged, untracked } = await getChanges({ git });
11020
11146
  const hasStaged = staged && staged.length > 0;
@@ -11069,12 +11195,18 @@ const handler$3 = async (argv, logger) => {
11069
11195
  color: 'yellow',
11070
11196
  });
11071
11197
  }
11198
+ logger.verbose(`→ ${provider} (${model})`, {
11199
+ color: 'green',
11200
+ });
11201
+ const USE_CONVENTIONAL_COMMITS = config.conventionalCommits || argv.conventional;
11072
11202
  async function factory() {
11073
11203
  if (config.noDiff) {
11074
11204
  const status = await git.status();
11075
- return status.files.map(file => ({
11205
+ return status.files.map((file) => ({
11076
11206
  filePath: file.path,
11077
- status: (file.index === 'A' || file.index === '?' ? 'added' : 'modified'),
11207
+ status: (file.index === 'A' || file.index === '?'
11208
+ ? 'added'
11209
+ : 'modified'),
11078
11210
  summary: file.path, // Simplified summary for noDiff
11079
11211
  }));
11080
11212
  }
@@ -11090,6 +11222,13 @@ const handler$3 = async (argv, logger) => {
11090
11222
  }
11091
11223
  }
11092
11224
  async function parser(changes) {
11225
+ if (config.noDiff) {
11226
+ // When noDiff is enabled, just return a simple summary without parsing file contents
11227
+ const filesSummary = changes
11228
+ .map((change) => `${change.status}: ${change.filePath}`)
11229
+ .join('\n');
11230
+ return `Staged files:\n${filesSummary}`;
11231
+ }
11093
11232
  return await fileChangeParser({
11094
11233
  changes,
11095
11234
  commit: '--staged',
@@ -11101,7 +11240,7 @@ const handler$3 = async (argv, logger) => {
11101
11240
  options: {
11102
11241
  ...config,
11103
11242
  prompt: config.prompt ||
11104
- (config.conventionalCommits || argv.conventional
11243
+ (USE_CONVENTIONAL_COMMITS
11105
11244
  ? CONVENTIONAL_COMMIT_PROMPT.template
11106
11245
  : COMMIT_PROMPT.template),
11107
11246
  logger,
@@ -11114,21 +11253,27 @@ const handler$3 = async (argv, logger) => {
11114
11253
  retryMessageOnly: 'Restart the function execution from generating the commit message',
11115
11254
  retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
11116
11255
  },
11256
+ customEditFunction: async (message, options) => {
11257
+ const { editCommitMessage } = await Promise.resolve().then(function () { return editCommitMessage$1; });
11258
+ return editCommitMessage(message, options);
11259
+ },
11117
11260
  },
11118
11261
  },
11119
11262
  factory,
11120
11263
  parser,
11121
11264
  agent: async (context, options) => {
11122
- // Check if conventional commits are enabled via config or CLI flag
11123
- const useConventional = config.conventionalCommits || argv.conventional;
11124
11265
  // Select the appropriate schema based on whether conventional commits are enabled
11125
- const schema = useConventional
11266
+ const schema = USE_CONVENTIONAL_COMMITS
11126
11267
  ? ConventionalCommitMessageResponseSchema
11127
11268
  : CommitMessageResponseSchema;
11128
11269
  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:
11129
- {{ body: string, title: string }}`;
11270
+ ${schema.description}
11271
+ {
11272
+ "title": "The commit title",
11273
+ "body": "The commit body"
11274
+ }`;
11130
11275
  // Use conventional commit prompt if enabled
11131
- const promptTemplate = useConventional ? CONVENTIONAL_COMMIT_PROMPT : COMMIT_PROMPT;
11276
+ const promptTemplate = USE_CONVENTIONAL_COMMITS ? CONVENTIONAL_COMMIT_PROMPT : COMMIT_PROMPT;
11132
11277
  const prompt = getPrompt({
11133
11278
  template: options.prompt,
11134
11279
  variables: promptTemplate.inputVariables,
@@ -11158,6 +11303,13 @@ const handler$3 = async (argv, logger) => {
11158
11303
  : config.includeBranchName !== false; // Default to true if not explicitly set to false
11159
11304
  // Create branch name context string based on the configuration
11160
11305
  const branchNameContext = includeBranchName ? `Current git branch name: ${branchName}` : '';
11306
+ // Load commitlint rules context if available
11307
+ const hasCommitLintConfig = await hasCommitlintConfig();
11308
+ let commitlint_rules_context = '';
11309
+ if (USE_CONVENTIONAL_COMMITS || hasCommitLintConfig) {
11310
+ const { getCommitlintRulesContext } = await Promise.resolve().then(function () { return commitlintValidator; });
11311
+ commitlint_rules_context = await getCommitlintRulesContext();
11312
+ }
11161
11313
  // Get variables for the prompt
11162
11314
  const variables = {
11163
11315
  summary: context,
@@ -11165,59 +11317,106 @@ const handler$3 = async (argv, logger) => {
11165
11317
  additional_context: additional_context,
11166
11318
  commit_history: commit_history,
11167
11319
  branch_name_context: branchNameContext,
11320
+ commitlint_rules_context: commitlint_rules_context,
11168
11321
  };
11169
11322
  const maxAttempts = config.service.provider === 'ollama' && 'maxParsingAttempts' in config.service
11170
11323
  ? config.service.maxParsingAttempts || 3
11171
11324
  : 3;
11172
- const commitMsg = await executeChainWithSchema(schema, llm, prompt, variables, {
11173
- retryOptions: {
11174
- maxAttempts,
11175
- onRetry: (attempt, error) => {
11176
- logger.verbose(`Failed to parse commit message (attempt ${attempt}/${maxAttempts}): ${error.message}`, { color: 'yellow' });
11325
+ // Custom retry logic for commitlint validation
11326
+ let retryCount = 0;
11327
+ let validationErrors = '';
11328
+ const generateCommitMessage = async () => {
11329
+ // Update variables with validation errors for retry attempts
11330
+ const currentVariables = {
11331
+ ...variables,
11332
+ additional_context: validationErrors
11333
+ ? `${variables.additional_context}\n\n## Validation Errors from Previous Attempt\nPlease fix the following issues:\n${validationErrors}`
11334
+ : variables.additional_context,
11335
+ };
11336
+ const commitMsg = await executeChainWithSchema(schema, llm, prompt, currentVariables, {
11337
+ retryOptions: {
11338
+ maxAttempts,
11339
+ onRetry: (attempt, error) => {
11340
+ logger.verbose(`Failed to parse commit message (attempt ${attempt}/${maxAttempts}): ${error.message}`, { color: 'yellow' });
11341
+ },
11177
11342
  },
11178
- },
11179
- fallbackParser: (text) => ({
11180
- title: text.split('\n')[0] || 'Auto-generated commit',
11181
- body: text.split('\n').slice(1).join('\n') || 'Generated commit message',
11182
- }),
11183
- onFallback: () => {
11184
- logger.verbose('Max retry attempts reached. Falling back to simple text output.', {
11185
- color: 'red',
11343
+ fallbackParser: (text) => ({
11344
+ title: text.split('\n')[0] || 'Auto-generated commit',
11345
+ body: text.split('\n').slice(1).join('\n') || 'Generated commit message',
11346
+ }),
11347
+ onFallback: () => {
11348
+ logger.verbose('Max retry attempts reached. Falling back to simple text output.', {
11349
+ color: 'red',
11350
+ });
11351
+ },
11352
+ });
11353
+ // Construct the full commit message
11354
+ const appendedText = argv.append ? `\n\n${argv.append}` : '';
11355
+ const ticketId = extractTicketIdFromBranchName(branchName);
11356
+ const ticketFooter = argv.appendTicket && ticketId ? `\n\nPart of **${ticketId}**` : '';
11357
+ const fullMessage = `${commitMsg.title}\n\n${commitMsg.body}${appendedText}${ticketFooter}`;
11358
+ // If commitlint validation is needed, validate the message
11359
+ if (USE_CONVENTIONAL_COMMITS || hasCommitLintConfig) {
11360
+ const { validateCommitMessage, CommitlintValidationError } = await Promise.resolve().then(function () { return commitlintValidator; });
11361
+ const validationResult = await validateCommitMessage(fullMessage);
11362
+ logger.verbose(`Validation result: ${JSON.stringify(validationResult)}`, {
11363
+ color: 'yellow',
11186
11364
  });
11365
+ if (!validationResult.valid) {
11366
+ retryCount++;
11367
+ // Format validation errors for next attempt
11368
+ validationErrors = validationResult.errors.map((error) => `- ${error}`).join('\n');
11369
+ // Auto-retry up to 2 times
11370
+ if (retryCount <= 2) {
11371
+ logger.verbose(`Commit message validation failed (attempt ${retryCount}/2). Retrying with error feedback...`, { color: 'yellow' });
11372
+ throw new CommitlintValidationError(`Validation failed: ${validationResult.errors.join('; ')}`, validationResult, fullMessage);
11373
+ }
11374
+ // After 2 failed attempts, let the user decide
11375
+ const { handleValidationErrors } = await Promise.resolve().then(function () { return commitValidationHandler; });
11376
+ const validationHandlerResult = await handleValidationErrors(fullMessage, validationResult, {
11377
+ logger,
11378
+ interactive: INTERACTIVE,
11379
+ openInEditor: config.openInEditor,
11380
+ });
11381
+ logger.verbose(`Validation handler result: ${JSON.stringify(validationHandlerResult)}`, {
11382
+ color: 'blue',
11383
+ });
11384
+ switch (validationHandlerResult.action) {
11385
+ case 'proceed':
11386
+ return validationHandlerResult.message;
11387
+ case 'edit':
11388
+ return validationHandlerResult.message;
11389
+ case 'regenerate':
11390
+ // Reset retry count and validation errors for fresh attempts
11391
+ retryCount = 0;
11392
+ validationErrors = '';
11393
+ throw new CommitlintValidationError('User requested regeneration', validationResult, fullMessage);
11394
+ case 'abort':
11395
+ logger.log('\nAborting commit due to validation errors.', { color: 'red' });
11396
+ process.exit(1);
11397
+ }
11398
+ }
11399
+ }
11400
+ return fullMessage;
11401
+ };
11402
+ // Custom shouldRetry function for commitlint errors
11403
+ const shouldRetryCommitlint = (error) => {
11404
+ return error.name === 'CommitlintValidationError';
11405
+ };
11406
+ // Use retry wrapper for commitlint validation with up to 4 total attempts
11407
+ // (2 automatic retries + 2 more if user chooses "Try again")
11408
+ return await withRetry(generateCommitMessage, {
11409
+ maxAttempts: 6, // Allow for multiple user retry requests
11410
+ shouldRetry: shouldRetryCommitlint,
11411
+ backoffMs: 0, // No delay needed for commitlint retries
11412
+ onRetry: (attempt, error) => {
11413
+ if (error.name === 'CommitlintValidationError' && attempt <= 2) {
11414
+ // Don't log for auto-retries, we already log in the function
11415
+ return;
11416
+ }
11417
+ logger.verbose(`Retrying commit message generation (attempt ${attempt}): ${error.message}`, { color: 'yellow' });
11187
11418
  },
11188
11419
  });
11189
- // Construct the full commit message
11190
- const appendedText = argv.append ? `\n\n${argv.append}` : '';
11191
- const ticketId = extractTicketIdFromBranchName(branchName);
11192
- const ticketFooter = argv.appendTicket && ticketId ? `\n\nPart of **${ticketId}**` : '';
11193
- const fullMessage = `${commitMsg.title}\n\n${commitMsg.body}${appendedText}${ticketFooter}`;
11194
- // If conventional commits are enabled, validate with commitlint
11195
- if (useConventional) {
11196
- const { validateCommitMessage } = await Promise.resolve().then(function () { return commitlintValidator; });
11197
- const { handleValidationErrors } = await Promise.resolve().then(function () { return commitValidationHandler; });
11198
- const validationResult = await validateCommitMessage(fullMessage);
11199
- const validationHandlerResult = await handleValidationErrors(fullMessage, validationResult, {
11200
- logger,
11201
- interactive: INTERACTIVE,
11202
- openInEditor: config.openInEditor,
11203
- });
11204
- switch (validationHandlerResult.action) {
11205
- case 'proceed':
11206
- // Validation passed, use the message as is
11207
- return validationHandlerResult.message;
11208
- case 'edit':
11209
- // User edited the message, use the edited version
11210
- return validationHandlerResult.message;
11211
- case 'regenerate':
11212
- // User wants to regenerate, throw special error to trigger regeneration
11213
- throw new Error('REGENERATE_COMMIT_MESSAGE');
11214
- case 'abort':
11215
- // User wants to abort or validation failed in non-interactive mode
11216
- logger.log('\nAborting commit due to validation errors.', { color: 'red' });
11217
- process.exit(1);
11218
- }
11219
- }
11220
- return fullMessage;
11221
11420
  },
11222
11421
  noResult: async () => {
11223
11422
  await noResult$2({ git, logger });
@@ -11258,28 +11457,6 @@ const builder$2 = (yargs) => {
11258
11457
  return yargs.options(options$2).usage(getCommandUsageHeader(command$2));
11259
11458
  };
11260
11459
 
11261
- /**
11262
- * Finds the project root directory starting from the given current directory.
11263
- * It checks if the `.git` directory or `package.json` file exists in the current directory or any of its parent directories.
11264
- * If found, it returns the path to the project root directory.
11265
- * If not found, it throws an error.
11266
- *
11267
- * @param currentDir - The current directory to start searching from.
11268
- * @returns The path to the project root directory.
11269
- * @throws Error if the project root directory cannot be found.
11270
- */
11271
- function findProjectRoot(currentDir) {
11272
- const root = path__default.parse(currentDir).root;
11273
- while (currentDir !== root) {
11274
- if (fs__default.existsSync(path__default.join(currentDir, '.git')) ||
11275
- fs__default.existsSync(path__default.join(currentDir, 'package.json'))) {
11276
- return currentDir;
11277
- }
11278
- currentDir = path__default.dirname(currentDir);
11279
- }
11280
- throw new Error('Unable to find project root. Are you in the right directory?');
11281
- }
11282
-
11283
11460
  /**
11284
11461
  * Executes a command as a Promise and returns the result.
11285
11462
  *
@@ -13199,11 +13376,13 @@ function isObservable(obj) {
13199
13376
  return !!obj && (obj instanceof Observable || (isFunction(obj.lift) && isFunction(obj.subscribe)));
13200
13377
  }
13201
13378
 
13202
- var EmptyError = createErrorClass(function (_super) { return function EmptyErrorImpl() {
13203
- _super(this);
13204
- this.name = 'EmptyError';
13205
- this.message = 'no elements in sequence';
13206
- }; });
13379
+ var EmptyError = createErrorClass(function (_super) {
13380
+ return function EmptyErrorImpl() {
13381
+ _super(this);
13382
+ this.name = 'EmptyError';
13383
+ this.message = 'no elements in sequence';
13384
+ };
13385
+ });
13207
13386
 
13208
13387
  function lastValueFrom(source, config) {
13209
13388
  var hasConfig = typeof config === 'object';
@@ -14490,23 +14669,183 @@ y.command(review.command, review.desc, review.builder, review.handler);
14490
14669
  y.command(init.command, init.desc, init.builder, init.handler);
14491
14670
  y.help().parse(process.argv.slice(2));
14492
14671
 
14672
+ /**
14673
+ * Edit a commit message with commitlint validation if config exists
14674
+ */
14675
+ async function editCommitMessage(message, options) {
14676
+ // First, let the user edit the message
14677
+ const editedMessage = await editResult(message, options);
14678
+ // Then validate it against commitlint if config exists
14679
+ const { hasCommitlintConfig } = await Promise.resolve().then(function () { return hasCommitlintConfig$1; });
14680
+ const hasConfig = await hasCommitlintConfig();
14681
+ if (hasConfig) {
14682
+ const { validateCommitMessage } = await Promise.resolve().then(function () { return commitlintValidator; });
14683
+ const { handleValidationErrors } = await Promise.resolve().then(function () { return commitValidationHandler; });
14684
+ const validationResult = await validateCommitMessage(editedMessage);
14685
+ if (!validationResult.valid) {
14686
+ // Show validation errors and get user action
14687
+ const validationHandlerResult = await handleValidationErrors(editedMessage, validationResult, {
14688
+ logger: options.logger,
14689
+ interactive: options.interactive,
14690
+ openInEditor: options.openInEditor,
14691
+ });
14692
+ // Return the result from the validation handler
14693
+ return validationHandlerResult.message;
14694
+ }
14695
+ }
14696
+ return editedMessage;
14697
+ }
14698
+
14699
+ var editCommitMessage$1 = /*#__PURE__*/Object.freeze({
14700
+ __proto__: null,
14701
+ editCommitMessage: editCommitMessage
14702
+ });
14703
+
14704
+ /**
14705
+ * Custom error for commitlint validation failures
14706
+ * This allows the retry system to identify these errors specifically
14707
+ */
14708
+ class CommitlintValidationError extends Error {
14709
+ constructor(message, validationResult, commitMessage) {
14710
+ super(message);
14711
+ this.name = 'CommitlintValidationError';
14712
+ this.validationResult = validationResult;
14713
+ this.commitMessage = commitMessage;
14714
+ }
14715
+ }
14493
14716
  /**
14494
14717
  * Load commitlint configuration
14495
14718
  */
14496
14719
  async function loadCommitlintConfig() {
14497
- // Dynamically import commitlint core
14498
- const commitlint = await import('@commitlint/core');
14499
- const { load } = commitlint;
14720
+ const projectRoot = findProjectRoot(process.cwd());
14721
+ const cwd = projectRoot || process.cwd();
14722
+ // @commitlint/load has issues with ESM configs (e.g. commitlint.config.js with `export default`).
14723
+ // Let's try to load them manually first.
14724
+ const esmConfigCandidates = COMMITLINT_CONFIG_FILES.filter((file) => file.endsWith('.js'));
14725
+ for (const configFile of esmConfigCandidates) {
14726
+ const configPath = join(cwd, configFile);
14727
+ if (existsSync(configPath)) {
14728
+ try {
14729
+ const module = await import(pathToFileURL(configPath).href);
14730
+ if (module.default &&
14731
+ (Object.keys(module.default.rules || {}).length > 0 ||
14732
+ (module.default.extends && module.default.extends.length > 0))) {
14733
+ // We found a config, now let commitlint process it (for extends etc)
14734
+ return await load(module.default, { cwd });
14735
+ }
14736
+ }
14737
+ catch (error) {
14738
+ // Failed to import, maybe not an ESM file after all or syntax error.
14739
+ // We will let the standard load take a chance.
14740
+ }
14741
+ }
14742
+ }
14500
14743
  try {
14501
- // Try to load project config
14502
- const config = await load();
14503
- return config;
14744
+ // Let @commitlint/load try to find the config. This works for CJS, JSON, and YAML.
14745
+ const config = await load({}, { cwd });
14746
+ // Check if a real config was loaded.
14747
+ if (config.extends.length > 0 || Object.keys(config.rules).length > 0) {
14748
+ return config;
14749
+ }
14504
14750
  }
14505
- catch {
14506
- // If no config found or error loading, use conventional config
14507
- return load({
14508
- extends: ['@commitlint/config-conventional'],
14509
- });
14751
+ catch (error) {
14752
+ // Could be an error parsing, or just not found. Fall through to default.
14753
+ }
14754
+ // If nothing worked, fallback to conventional config
14755
+ return load({
14756
+ extends: ['@commitlint/config-conventional'],
14757
+ });
14758
+ }
14759
+ /**
14760
+ * Format commitlint rules into a human-readable string for AI prompts
14761
+ */
14762
+ function formatCommitlintRulesForPrompt(config) {
14763
+ if (!config.rules || Object.keys(config.rules).length === 0) {
14764
+ return '';
14765
+ }
14766
+ const ruleDescriptions = [];
14767
+ // Add information about extends if present
14768
+ if (config.extends && config.extends.length > 0) {
14769
+ ruleDescriptions.push(`Following ${config.extends.join(', ')} configuration`);
14770
+ }
14771
+ // Process key rules that affect commit message format
14772
+ const rules = config.rules;
14773
+ // Header length rules
14774
+ if (rules['header-max-length']) {
14775
+ const [level, , maxLength] = rules['header-max-length'];
14776
+ if (level > 0) {
14777
+ ruleDescriptions.push(`Header (title) must be ${maxLength} characters or less (including spaces)`);
14778
+ }
14779
+ }
14780
+ if (rules['header-min-length']) {
14781
+ const [level, , minLength] = rules['header-min-length'];
14782
+ if (level > 0) {
14783
+ ruleDescriptions.push(`Header (title) must be at least ${minLength} characters (including spaces)`);
14784
+ }
14785
+ }
14786
+ // Body length rules
14787
+ if (rules['body-max-line-length']) {
14788
+ const [level, , maxLength] = rules['body-max-line-length'];
14789
+ if (level > 0) {
14790
+ ruleDescriptions.push(`Body lines must be ${maxLength} characters or less (including spaces)`);
14791
+ }
14792
+ }
14793
+ // Type rules
14794
+ if (rules['type-enum']) {
14795
+ const [level, , allowedTypes] = rules['type-enum'];
14796
+ if (level > 0 && Array.isArray(allowedTypes)) {
14797
+ ruleDescriptions.push(`Allowed types: ${allowedTypes.join(', ')}`);
14798
+ }
14799
+ }
14800
+ // Case rules
14801
+ if (rules['type-case']) {
14802
+ const [level, , caseType] = rules['type-case'];
14803
+ if (level > 0) {
14804
+ ruleDescriptions.push(`Type must be ${caseType} case`);
14805
+ }
14806
+ }
14807
+ if (rules['subject-case']) {
14808
+ const [level, , caseType] = rules['subject-case'];
14809
+ if (level > 0) {
14810
+ ruleDescriptions.push(`Subject must be ${caseType} case`);
14811
+ }
14812
+ }
14813
+ // Scope rules
14814
+ if (rules['scope-enum']) {
14815
+ const [level, , allowedScopes] = rules['scope-enum'];
14816
+ if (level > 0 && Array.isArray(allowedScopes)) {
14817
+ ruleDescriptions.push(`Allowed scopes: ${allowedScopes.join(', ')}`);
14818
+ }
14819
+ }
14820
+ // Subject rules
14821
+ if (rules['subject-full-stop']) {
14822
+ const [level, condition] = rules['subject-full-stop'];
14823
+ if (level > 0) {
14824
+ const verb = condition === 'always' ? 'must' : 'must not';
14825
+ ruleDescriptions.push(`Subject ${verb} end with a period`);
14826
+ }
14827
+ }
14828
+ if (rules['subject-empty']) {
14829
+ const [level, condition] = rules['subject-empty'];
14830
+ if (level > 0) {
14831
+ const requirement = condition === 'never' ? 'must not be empty' : 'must be empty';
14832
+ ruleDescriptions.push(`Subject ${requirement}`);
14833
+ }
14834
+ }
14835
+ return ruleDescriptions.length > 0
14836
+ ? `## Commitlint Rules\nYour commit message must follow these project-specific rules:\n${ruleDescriptions.map(rule => `- ${rule}`).join('\n')}\n`
14837
+ : '';
14838
+ }
14839
+ /**
14840
+ * Get commitlint rules context for prompt if config exists
14841
+ */
14842
+ async function getCommitlintRulesContext() {
14843
+ try {
14844
+ const config = await loadCommitlintConfig();
14845
+ return formatCommitlintRulesForPrompt(config);
14846
+ }
14847
+ catch (error) {
14848
+ return '';
14510
14849
  }
14511
14850
  }
14512
14851
  /**
@@ -14515,9 +14854,6 @@ async function loadCommitlintConfig() {
14515
14854
  async function validateCommitMessage(message, options = {}) {
14516
14855
  try {
14517
14856
  const config = await loadCommitlintConfig();
14518
- // Dynamically import commitlint lint function
14519
- const commitlint = await import('@commitlint/core');
14520
- const { lint } = commitlint;
14521
14857
  const result = await lint(message, config.rules, options);
14522
14858
  return {
14523
14859
  valid: result.valid,
@@ -14536,6 +14872,9 @@ async function validateCommitMessage(message, options = {}) {
14536
14872
 
14537
14873
  var commitlintValidator = /*#__PURE__*/Object.freeze({
14538
14874
  __proto__: null,
14875
+ CommitlintValidationError: CommitlintValidationError,
14876
+ formatCommitlintRulesForPrompt: formatCommitlintRulesForPrompt,
14877
+ getCommitlintRulesContext: getCommitlintRulesContext,
14539
14878
  loadCommitlintConfig: loadCommitlintConfig,
14540
14879
  validateCommitMessage: validateCommitMessage
14541
14880
  });
@@ -14572,29 +14911,45 @@ async function handleValidationErrors(message, validationResult, options) {
14572
14911
  message: 'How would you like to proceed?:',
14573
14912
  choices: [
14574
14913
  {
14575
- name: 'Edit',
14576
- value: 'edit',
14577
- description: 'Edit the commit message manually',
14914
+ name: 'Try 2 more attempts',
14915
+ value: 'retry',
14916
+ description: 'Let the AI try generating 2 more commit messages with error feedback',
14578
14917
  },
14579
14918
  {
14580
- name: 'Retry',
14581
- value: 'retry',
14582
- description: 'Regenerate a new commit message',
14919
+ name: 'Edit manually',
14920
+ value: 'edit',
14921
+ description: 'Edit the commit message manually to fix the issues',
14583
14922
  },
14584
14923
  {
14585
14924
  name: 'Abort',
14586
14925
  value: 'abort',
14587
- description: 'Abort the commit',
14926
+ description: 'Abort the commit process',
14588
14927
  },
14589
14928
  ],
14590
14929
  });
14591
14930
  switch (choice) {
14592
- case '1': {
14931
+ case 'edit': {
14593
14932
  // Edit message manually
14594
14933
  const editedMessage = await editResult(message, options);
14934
+ // Validate the manually edited message if commitlint config exists
14935
+ const { hasCommitlintConfig } = await Promise.resolve().then(function () { return hasCommitlintConfig$1; });
14936
+ const hasConfig = await hasCommitlintConfig();
14937
+ if (hasConfig) {
14938
+ const { validateCommitMessage } = await Promise.resolve().then(function () { return commitlintValidator; });
14939
+ const editedValidationResult = await validateCommitMessage(editedMessage);
14940
+ if (!editedValidationResult.valid) {
14941
+ // Show validation errors for the edited message
14942
+ options.logger.log('\nEdited commit message also has validation issues:', { color: 'yellow' });
14943
+ editedValidationResult.errors.forEach((error) => {
14944
+ options.logger.log(` • ${error}`, { color: 'red' });
14945
+ });
14946
+ // Recursively handle validation errors for the edited message
14947
+ return await handleValidationErrors(editedMessage, editedValidationResult, options);
14948
+ }
14949
+ }
14595
14950
  return { message: editedMessage, action: 'edit' };
14596
14951
  }
14597
- case '2':
14952
+ case 'retry':
14598
14953
  // Regenerate message
14599
14954
  return { message, action: 'regenerate' };
14600
14955
  default: