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.
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.1";
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(),
7577
- });
7631
+ title: stringType().describe("Title of the commit message"),
7632
+ body: stringType().describe("Body of the commit message"),
7633
+ }).describe("Object with commit message 'title' and 'body'");
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("Object with Conventional Commit message 'title' and 'body' adhering to Conventional Commits specification");
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
  }
@@ -11112,6 +11244,13 @@ const handler$3 = async (argv, logger) => {
11112
11244
  }
11113
11245
  }
11114
11246
  async function parser(changes) {
11247
+ if (config.noDiff) {
11248
+ // When noDiff is enabled, just return a simple summary without parsing file contents
11249
+ const filesSummary = changes
11250
+ .map((change) => `${change.status}: ${change.filePath}`)
11251
+ .join('\n');
11252
+ return `Staged files:\n${filesSummary}`;
11253
+ }
11115
11254
  return await fileChangeParser({
11116
11255
  changes,
11117
11256
  commit: '--staged',
@@ -11123,7 +11262,7 @@ const handler$3 = async (argv, logger) => {
11123
11262
  options: {
11124
11263
  ...config,
11125
11264
  prompt: config.prompt ||
11126
- (config.conventionalCommits || argv.conventional
11265
+ (USE_CONVENTIONAL_COMMITS
11127
11266
  ? CONVENTIONAL_COMMIT_PROMPT.template
11128
11267
  : COMMIT_PROMPT.template),
11129
11268
  logger,
@@ -11136,21 +11275,27 @@ const handler$3 = async (argv, logger) => {
11136
11275
  retryMessageOnly: 'Restart the function execution from generating the commit message',
11137
11276
  retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
11138
11277
  },
11278
+ customEditFunction: async (message, options) => {
11279
+ const { editCommitMessage } = await Promise.resolve().then(function () { return editCommitMessage$1; });
11280
+ return editCommitMessage(message, options);
11281
+ },
11139
11282
  },
11140
11283
  },
11141
11284
  factory,
11142
11285
  parser,
11143
11286
  agent: async (context, options) => {
11144
- // Check if conventional commits are enabled via config or CLI flag
11145
- const useConventional = config.conventionalCommits || argv.conventional;
11146
11287
  // Select the appropriate schema based on whether conventional commits are enabled
11147
- const schema = useConventional
11288
+ const schema = USE_CONVENTIONAL_COMMITS
11148
11289
  ? ConventionalCommitMessageResponseSchema
11149
11290
  : CommitMessageResponseSchema;
11150
11291
  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 }}`;
11292
+ ${schema.description}
11293
+ {
11294
+ "title": "The commit title",
11295
+ "body": "The commit body"
11296
+ }`;
11152
11297
  // Use conventional commit prompt if enabled
11153
- const promptTemplate = useConventional ? CONVENTIONAL_COMMIT_PROMPT : COMMIT_PROMPT;
11298
+ const promptTemplate = USE_CONVENTIONAL_COMMITS ? CONVENTIONAL_COMMIT_PROMPT : COMMIT_PROMPT;
11154
11299
  const prompt = getPrompt({
11155
11300
  template: options.prompt,
11156
11301
  variables: promptTemplate.inputVariables,
@@ -11180,6 +11325,13 @@ const handler$3 = async (argv, logger) => {
11180
11325
  : config.includeBranchName !== false; // Default to true if not explicitly set to false
11181
11326
  // Create branch name context string based on the configuration
11182
11327
  const branchNameContext = includeBranchName ? `Current git branch name: ${branchName}` : '';
11328
+ // Load commitlint rules context if available
11329
+ const hasCommitLintConfig = await hasCommitlintConfig();
11330
+ let commitlint_rules_context = '';
11331
+ if (USE_CONVENTIONAL_COMMITS || hasCommitLintConfig) {
11332
+ const { getCommitlintRulesContext } = await Promise.resolve().then(function () { return commitlintValidator; });
11333
+ commitlint_rules_context = await getCommitlintRulesContext();
11334
+ }
11183
11335
  // Get variables for the prompt
11184
11336
  const variables = {
11185
11337
  summary: context,
@@ -11187,59 +11339,106 @@ const handler$3 = async (argv, logger) => {
11187
11339
  additional_context: additional_context,
11188
11340
  commit_history: commit_history,
11189
11341
  branch_name_context: branchNameContext,
11342
+ commitlint_rules_context: commitlint_rules_context,
11190
11343
  };
11191
11344
  const maxAttempts = config.service.provider === 'ollama' && 'maxParsingAttempts' in config.service
11192
11345
  ? config.service.maxParsingAttempts || 3
11193
11346
  : 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' });
11347
+ // Custom retry logic for commitlint validation
11348
+ let retryCount = 0;
11349
+ let validationErrors = '';
11350
+ const generateCommitMessage = async () => {
11351
+ // Update variables with validation errors for retry attempts
11352
+ const currentVariables = {
11353
+ ...variables,
11354
+ additional_context: validationErrors
11355
+ ? `${variables.additional_context}\n\n## Validation Errors from Previous Attempt\nPlease fix the following issues:\n${validationErrors}`
11356
+ : variables.additional_context,
11357
+ };
11358
+ const commitMsg = await executeChainWithSchema(schema, llm, prompt, currentVariables, {
11359
+ retryOptions: {
11360
+ maxAttempts,
11361
+ onRetry: (attempt, error) => {
11362
+ logger.verbose(`Failed to parse commit message (attempt ${attempt}/${maxAttempts}): ${error.message}`, { color: 'yellow' });
11363
+ },
11199
11364
  },
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',
11365
+ fallbackParser: (text) => ({
11366
+ title: text.split('\n')[0] || 'Auto-generated commit',
11367
+ body: text.split('\n').slice(1).join('\n') || 'Generated commit message',
11368
+ }),
11369
+ onFallback: () => {
11370
+ logger.verbose('Max retry attempts reached. Falling back to simple text output.', {
11371
+ color: 'red',
11372
+ });
11373
+ },
11374
+ });
11375
+ // Construct the full commit message
11376
+ const appendedText = argv.append ? `\n\n${argv.append}` : '';
11377
+ const ticketId = extractTicketIdFromBranchName(branchName);
11378
+ const ticketFooter = argv.appendTicket && ticketId ? `\n\nPart of **${ticketId}**` : '';
11379
+ const fullMessage = `${commitMsg.title}\n\n${commitMsg.body}${appendedText}${ticketFooter}`;
11380
+ // If commitlint validation is needed, validate the message
11381
+ if (USE_CONVENTIONAL_COMMITS || hasCommitLintConfig) {
11382
+ const { validateCommitMessage, CommitlintValidationError } = await Promise.resolve().then(function () { return commitlintValidator; });
11383
+ const validationResult = await validateCommitMessage(fullMessage);
11384
+ logger.verbose(`Validation result: ${JSON.stringify(validationResult)}`, {
11385
+ color: 'yellow',
11208
11386
  });
11387
+ if (!validationResult.valid) {
11388
+ retryCount++;
11389
+ // Format validation errors for next attempt
11390
+ validationErrors = validationResult.errors.map((error) => `- ${error}`).join('\n');
11391
+ // Auto-retry up to 2 times
11392
+ if (retryCount <= 2) {
11393
+ logger.verbose(`Commit message validation failed (attempt ${retryCount}/2). Retrying with error feedback...`, { color: 'yellow' });
11394
+ throw new CommitlintValidationError(`Validation failed: ${validationResult.errors.join('; ')}`, validationResult, fullMessage);
11395
+ }
11396
+ // After 2 failed attempts, let the user decide
11397
+ const { handleValidationErrors } = await Promise.resolve().then(function () { return commitValidationHandler; });
11398
+ const validationHandlerResult = await handleValidationErrors(fullMessage, validationResult, {
11399
+ logger,
11400
+ interactive: INTERACTIVE,
11401
+ openInEditor: config.openInEditor,
11402
+ });
11403
+ logger.verbose(`Validation handler result: ${JSON.stringify(validationHandlerResult)}`, {
11404
+ color: 'blue',
11405
+ });
11406
+ switch (validationHandlerResult.action) {
11407
+ case 'proceed':
11408
+ return validationHandlerResult.message;
11409
+ case 'edit':
11410
+ return validationHandlerResult.message;
11411
+ case 'regenerate':
11412
+ // Reset retry count and validation errors for fresh attempts
11413
+ retryCount = 0;
11414
+ validationErrors = '';
11415
+ throw new CommitlintValidationError('User requested regeneration', validationResult, fullMessage);
11416
+ case 'abort':
11417
+ logger.log('\nAborting commit due to validation errors.', { color: 'red' });
11418
+ process.exit(1);
11419
+ }
11420
+ }
11421
+ }
11422
+ return fullMessage;
11423
+ };
11424
+ // Custom shouldRetry function for commitlint errors
11425
+ const shouldRetryCommitlint = (error) => {
11426
+ return error.name === 'CommitlintValidationError';
11427
+ };
11428
+ // Use retry wrapper for commitlint validation with up to 4 total attempts
11429
+ // (2 automatic retries + 2 more if user chooses "Try again")
11430
+ return await withRetry(generateCommitMessage, {
11431
+ maxAttempts: 6, // Allow for multiple user retry requests
11432
+ shouldRetry: shouldRetryCommitlint,
11433
+ backoffMs: 0, // No delay needed for commitlint retries
11434
+ onRetry: (attempt, error) => {
11435
+ if (error.name === 'CommitlintValidationError' && attempt <= 2) {
11436
+ // Don't log for auto-retries, we already log in the function
11437
+ return;
11438
+ }
11439
+ logger.verbose(`Retrying commit message generation (attempt ${attempt}): ${error.message}`, { color: 'yellow' });
11209
11440
  },
11210
11441
  });
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
11442
  },
11244
11443
  noResult: async () => {
11245
11444
  await noResult$2({ git, logger });
@@ -11280,28 +11479,6 @@ const builder$2 = (yargs) => {
11280
11479
  return yargs.options(options$2).usage(getCommandUsageHeader(command$2));
11281
11480
  };
11282
11481
 
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
11482
  /**
11306
11483
  * Executes a command as a Promise and returns the result.
11307
11484
  *
@@ -13221,11 +13398,13 @@ function isObservable(obj) {
13221
13398
  return !!obj && (obj instanceof Observable || (isFunction(obj.lift) && isFunction(obj.subscribe)));
13222
13399
  }
13223
13400
 
13224
- var EmptyError = createErrorClass(function (_super) { return function EmptyErrorImpl() {
13225
- _super(this);
13226
- this.name = 'EmptyError';
13227
- this.message = 'no elements in sequence';
13228
- }; });
13401
+ var EmptyError = createErrorClass(function (_super) {
13402
+ return function EmptyErrorImpl() {
13403
+ _super(this);
13404
+ this.name = 'EmptyError';
13405
+ this.message = 'no elements in sequence';
13406
+ };
13407
+ });
13229
13408
 
13230
13409
  function lastValueFrom(source, config) {
13231
13410
  var hasConfig = typeof config === 'object';
@@ -14512,23 +14691,183 @@ y.command(review.command, review.desc, review.builder, review.handler);
14512
14691
  y.command(init.command, init.desc, init.builder, init.handler);
14513
14692
  y.help().parse(process.argv.slice(2));
14514
14693
 
14694
+ /**
14695
+ * Edit a commit message with commitlint validation if config exists
14696
+ */
14697
+ async function editCommitMessage(message, options) {
14698
+ // First, let the user edit the message
14699
+ const editedMessage = await editResult(message, options);
14700
+ // Then validate it against commitlint if config exists
14701
+ const { hasCommitlintConfig } = await Promise.resolve().then(function () { return hasCommitlintConfig$1; });
14702
+ const hasConfig = await hasCommitlintConfig();
14703
+ if (hasConfig) {
14704
+ const { validateCommitMessage } = await Promise.resolve().then(function () { return commitlintValidator; });
14705
+ const { handleValidationErrors } = await Promise.resolve().then(function () { return commitValidationHandler; });
14706
+ const validationResult = await validateCommitMessage(editedMessage);
14707
+ if (!validationResult.valid) {
14708
+ // Show validation errors and get user action
14709
+ const validationHandlerResult = await handleValidationErrors(editedMessage, validationResult, {
14710
+ logger: options.logger,
14711
+ interactive: options.interactive,
14712
+ openInEditor: options.openInEditor,
14713
+ });
14714
+ // Return the result from the validation handler
14715
+ return validationHandlerResult.message;
14716
+ }
14717
+ }
14718
+ return editedMessage;
14719
+ }
14720
+
14721
+ var editCommitMessage$1 = /*#__PURE__*/Object.freeze({
14722
+ __proto__: null,
14723
+ editCommitMessage: editCommitMessage
14724
+ });
14725
+
14726
+ /**
14727
+ * Custom error for commitlint validation failures
14728
+ * This allows the retry system to identify these errors specifically
14729
+ */
14730
+ class CommitlintValidationError extends Error {
14731
+ constructor(message, validationResult, commitMessage) {
14732
+ super(message);
14733
+ this.name = 'CommitlintValidationError';
14734
+ this.validationResult = validationResult;
14735
+ this.commitMessage = commitMessage;
14736
+ }
14737
+ }
14515
14738
  /**
14516
14739
  * Load commitlint configuration
14517
14740
  */
14518
14741
  async function loadCommitlintConfig() {
14519
- // Dynamically import commitlint core
14520
- const commitlint = await import('@commitlint/core');
14521
- const { load } = commitlint;
14742
+ const projectRoot = findProjectRoot(process.cwd());
14743
+ const cwd = projectRoot || process.cwd();
14744
+ // @commitlint/load has issues with ESM configs (e.g. commitlint.config.js with `export default`).
14745
+ // Let's try to load them manually first.
14746
+ const esmConfigCandidates = COMMITLINT_CONFIG_FILES.filter((file) => file.endsWith('.js'));
14747
+ for (const configFile of esmConfigCandidates) {
14748
+ const configPath = path.join(cwd, configFile);
14749
+ if (fs.existsSync(configPath)) {
14750
+ try {
14751
+ const module = await import(url.pathToFileURL(configPath).href);
14752
+ if (module.default &&
14753
+ (Object.keys(module.default.rules || {}).length > 0 ||
14754
+ (module.default.extends && module.default.extends.length > 0))) {
14755
+ // We found a config, now let commitlint process it (for extends etc)
14756
+ return await core$1.load(module.default, { cwd });
14757
+ }
14758
+ }
14759
+ catch (error) {
14760
+ // Failed to import, maybe not an ESM file after all or syntax error.
14761
+ // We will let the standard load take a chance.
14762
+ }
14763
+ }
14764
+ }
14522
14765
  try {
14523
- // Try to load project config
14524
- const config = await load();
14525
- return config;
14766
+ // Let @commitlint/load try to find the config. This works for CJS, JSON, and YAML.
14767
+ const config = await core$1.load({}, { cwd });
14768
+ // Check if a real config was loaded.
14769
+ if (config.extends.length > 0 || Object.keys(config.rules).length > 0) {
14770
+ return config;
14771
+ }
14526
14772
  }
14527
- catch {
14528
- // If no config found or error loading, use conventional config
14529
- return load({
14530
- extends: ['@commitlint/config-conventional'],
14531
- });
14773
+ catch (error) {
14774
+ // Could be an error parsing, or just not found. Fall through to default.
14775
+ }
14776
+ // If nothing worked, fallback to conventional config
14777
+ return core$1.load({
14778
+ extends: ['@commitlint/config-conventional'],
14779
+ });
14780
+ }
14781
+ /**
14782
+ * Format commitlint rules into a human-readable string for AI prompts
14783
+ */
14784
+ function formatCommitlintRulesForPrompt(config) {
14785
+ if (!config.rules || Object.keys(config.rules).length === 0) {
14786
+ return '';
14787
+ }
14788
+ const ruleDescriptions = [];
14789
+ // Add information about extends if present
14790
+ if (config.extends && config.extends.length > 0) {
14791
+ ruleDescriptions.push(`Following ${config.extends.join(', ')} configuration`);
14792
+ }
14793
+ // Process key rules that affect commit message format
14794
+ const rules = config.rules;
14795
+ // Header length rules
14796
+ if (rules['header-max-length']) {
14797
+ const [level, , maxLength] = rules['header-max-length'];
14798
+ if (level > 0) {
14799
+ ruleDescriptions.push(`Header (title) must be ${maxLength} characters or less (including spaces)`);
14800
+ }
14801
+ }
14802
+ if (rules['header-min-length']) {
14803
+ const [level, , minLength] = rules['header-min-length'];
14804
+ if (level > 0) {
14805
+ ruleDescriptions.push(`Header (title) must be at least ${minLength} characters (including spaces)`);
14806
+ }
14807
+ }
14808
+ // Body length rules
14809
+ if (rules['body-max-line-length']) {
14810
+ const [level, , maxLength] = rules['body-max-line-length'];
14811
+ if (level > 0) {
14812
+ ruleDescriptions.push(`Body lines must be ${maxLength} characters or less (including spaces)`);
14813
+ }
14814
+ }
14815
+ // Type rules
14816
+ if (rules['type-enum']) {
14817
+ const [level, , allowedTypes] = rules['type-enum'];
14818
+ if (level > 0 && Array.isArray(allowedTypes)) {
14819
+ ruleDescriptions.push(`Allowed types: ${allowedTypes.join(', ')}`);
14820
+ }
14821
+ }
14822
+ // Case rules
14823
+ if (rules['type-case']) {
14824
+ const [level, , caseType] = rules['type-case'];
14825
+ if (level > 0) {
14826
+ ruleDescriptions.push(`Type must be ${caseType} case`);
14827
+ }
14828
+ }
14829
+ if (rules['subject-case']) {
14830
+ const [level, , caseType] = rules['subject-case'];
14831
+ if (level > 0) {
14832
+ ruleDescriptions.push(`Subject must be ${caseType} case`);
14833
+ }
14834
+ }
14835
+ // Scope rules
14836
+ if (rules['scope-enum']) {
14837
+ const [level, , allowedScopes] = rules['scope-enum'];
14838
+ if (level > 0 && Array.isArray(allowedScopes)) {
14839
+ ruleDescriptions.push(`Allowed scopes: ${allowedScopes.join(', ')}`);
14840
+ }
14841
+ }
14842
+ // Subject rules
14843
+ if (rules['subject-full-stop']) {
14844
+ const [level, condition] = rules['subject-full-stop'];
14845
+ if (level > 0) {
14846
+ const verb = condition === 'always' ? 'must' : 'must not';
14847
+ ruleDescriptions.push(`Subject ${verb} end with a period`);
14848
+ }
14849
+ }
14850
+ if (rules['subject-empty']) {
14851
+ const [level, condition] = rules['subject-empty'];
14852
+ if (level > 0) {
14853
+ const requirement = condition === 'never' ? 'must not be empty' : 'must be empty';
14854
+ ruleDescriptions.push(`Subject ${requirement}`);
14855
+ }
14856
+ }
14857
+ return ruleDescriptions.length > 0
14858
+ ? `## Commitlint Rules\nYour commit message must follow these project-specific rules:\n${ruleDescriptions.map(rule => `- ${rule}`).join('\n')}\n`
14859
+ : '';
14860
+ }
14861
+ /**
14862
+ * Get commitlint rules context for prompt if config exists
14863
+ */
14864
+ async function getCommitlintRulesContext() {
14865
+ try {
14866
+ const config = await loadCommitlintConfig();
14867
+ return formatCommitlintRulesForPrompt(config);
14868
+ }
14869
+ catch (error) {
14870
+ return '';
14532
14871
  }
14533
14872
  }
14534
14873
  /**
@@ -14537,10 +14876,7 @@ async function loadCommitlintConfig() {
14537
14876
  async function validateCommitMessage(message, options = {}) {
14538
14877
  try {
14539
14878
  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);
14879
+ const result = await core$1.lint(message, config.rules, options);
14544
14880
  return {
14545
14881
  valid: result.valid,
14546
14882
  errors: result.errors.map((error) => error.message),
@@ -14558,6 +14894,9 @@ async function validateCommitMessage(message, options = {}) {
14558
14894
 
14559
14895
  var commitlintValidator = /*#__PURE__*/Object.freeze({
14560
14896
  __proto__: null,
14897
+ CommitlintValidationError: CommitlintValidationError,
14898
+ formatCommitlintRulesForPrompt: formatCommitlintRulesForPrompt,
14899
+ getCommitlintRulesContext: getCommitlintRulesContext,
14561
14900
  loadCommitlintConfig: loadCommitlintConfig,
14562
14901
  validateCommitMessage: validateCommitMessage
14563
14902
  });
@@ -14594,29 +14933,45 @@ async function handleValidationErrors(message, validationResult, options) {
14594
14933
  message: 'How would you like to proceed?:',
14595
14934
  choices: [
14596
14935
  {
14597
- name: 'Edit',
14598
- value: 'edit',
14599
- description: 'Edit the commit message manually',
14936
+ name: 'Try 2 more attempts',
14937
+ value: 'retry',
14938
+ description: 'Let the AI try generating 2 more commit messages with error feedback',
14600
14939
  },
14601
14940
  {
14602
- name: 'Retry',
14603
- value: 'retry',
14604
- description: 'Regenerate a new commit message',
14941
+ name: 'Edit manually',
14942
+ value: 'edit',
14943
+ description: 'Edit the commit message manually to fix the issues',
14605
14944
  },
14606
14945
  {
14607
14946
  name: 'Abort',
14608
14947
  value: 'abort',
14609
- description: 'Abort the commit',
14948
+ description: 'Abort the commit process',
14610
14949
  },
14611
14950
  ],
14612
14951
  });
14613
14952
  switch (choice) {
14614
- case '1': {
14953
+ case 'edit': {
14615
14954
  // Edit message manually
14616
14955
  const editedMessage = await editResult(message, options);
14956
+ // Validate the manually edited message if commitlint config exists
14957
+ const { hasCommitlintConfig } = await Promise.resolve().then(function () { return hasCommitlintConfig$1; });
14958
+ const hasConfig = await hasCommitlintConfig();
14959
+ if (hasConfig) {
14960
+ const { validateCommitMessage } = await Promise.resolve().then(function () { return commitlintValidator; });
14961
+ const editedValidationResult = await validateCommitMessage(editedMessage);
14962
+ if (!editedValidationResult.valid) {
14963
+ // Show validation errors for the edited message
14964
+ options.logger.log('\nEdited commit message also has validation issues:', { color: 'yellow' });
14965
+ editedValidationResult.errors.forEach((error) => {
14966
+ options.logger.log(` • ${error}`, { color: 'red' });
14967
+ });
14968
+ // Recursively handle validation errors for the edited message
14969
+ return await handleValidationErrors(editedMessage, editedValidationResult, options);
14970
+ }
14971
+ }
14617
14972
  return { message: editedMessage, action: 'edit' };
14618
14973
  }
14619
- case '2':
14974
+ case 'retry':
14620
14975
  // Regenerate message
14621
14976
  return { message, action: 'regenerate' };
14622
14977
  default: