git-coco 0.20.0 → 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.
@@ -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.0";
53
+ const BUILD_VERSION = "0.21.0";
50
54
 
51
55
  const isInteractive = (config) => {
52
56
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -750,12 +754,12 @@ function loadIgnore(config) {
750
754
  /**
751
755
  * Schema ID for JSON validation
752
756
  */
753
- const SCHEMA_PUBLIC_URL = "https://git-co.co/schema.json";
757
+ const SCHEMA_PUBLIC_URL = "https://coco.griffen.codes/schema.json";
754
758
  /**
755
759
  * Generated JSON schema
756
760
  */
757
761
  const schema$1 = {
758
- "$id": "https://git-co.co/schema.json",
762
+ "$id": "https://coco.griffen.codes/schema.json",
759
763
  "$schema": "http://json-schema.org/draft-07/schema#",
760
764
  "$ref": "#/definitions/ConfigWithServiceObject",
761
765
  "definitions": {
@@ -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(),
7609
+ title: stringType().describe("Title of the commit message"),
7610
+ body: stringType().describe("Body of the commit message"),
7555
7611
  });
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("Conventional commit message schema with strict formatting rules");
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
  }
@@ -11096,12 +11228,13 @@ const handler$3 = async (argv, logger) => {
11096
11228
  options: { tokenizer, git, llm, logger },
11097
11229
  });
11098
11230
  }
11231
+ logger.log(`Generating commit message...${JSON.stringify(config.prompt)}`, { color: 'blue' });
11099
11232
  const commitMsg = await generateAndReviewLoop({
11100
11233
  label: 'commit message',
11101
11234
  options: {
11102
11235
  ...config,
11103
11236
  prompt: config.prompt ||
11104
- (config.conventionalCommits || argv.conventional
11237
+ (USE_CONVENTIONAL_COMMITS
11105
11238
  ? CONVENTIONAL_COMMIT_PROMPT.template
11106
11239
  : COMMIT_PROMPT.template),
11107
11240
  logger,
@@ -11114,21 +11247,23 @@ const handler$3 = async (argv, logger) => {
11114
11247
  retryMessageOnly: 'Restart the function execution from generating the commit message',
11115
11248
  retryFull: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
11116
11249
  },
11250
+ customEditFunction: async (message, options) => {
11251
+ const { editCommitMessage } = await Promise.resolve().then(function () { return editCommitMessage$1; });
11252
+ return editCommitMessage(message, options);
11253
+ },
11117
11254
  },
11118
11255
  },
11119
11256
  factory,
11120
11257
  parser,
11121
11258
  agent: async (context, options) => {
11122
- // Check if conventional commits are enabled via config or CLI flag
11123
- const useConventional = config.conventionalCommits || argv.conventional;
11124
11259
  // Select the appropriate schema based on whether conventional commits are enabled
11125
- const schema = useConventional
11260
+ const schema = USE_CONVENTIONAL_COMMITS
11126
11261
  ? ConventionalCommitMessageResponseSchema
11127
11262
  : CommitMessageResponseSchema;
11128
11263
  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 }}`;
11264
+ ${schema.description}`;
11130
11265
  // Use conventional commit prompt if enabled
11131
- const promptTemplate = useConventional ? CONVENTIONAL_COMMIT_PROMPT : COMMIT_PROMPT;
11266
+ const promptTemplate = USE_CONVENTIONAL_COMMITS ? CONVENTIONAL_COMMIT_PROMPT : COMMIT_PROMPT;
11132
11267
  const prompt = getPrompt({
11133
11268
  template: options.prompt,
11134
11269
  variables: promptTemplate.inputVariables,
@@ -11158,6 +11293,13 @@ const handler$3 = async (argv, logger) => {
11158
11293
  : config.includeBranchName !== false; // Default to true if not explicitly set to false
11159
11294
  // Create branch name context string based on the configuration
11160
11295
  const branchNameContext = includeBranchName ? `Current git branch name: ${branchName}` : '';
11296
+ // Load commitlint rules context if available
11297
+ const hasCommitLintConfig = await hasCommitlintConfig();
11298
+ let commitlint_rules_context = '';
11299
+ if (USE_CONVENTIONAL_COMMITS || hasCommitLintConfig) {
11300
+ const { getCommitlintRulesContext } = await Promise.resolve().then(function () { return commitlintValidator; });
11301
+ commitlint_rules_context = await getCommitlintRulesContext();
11302
+ }
11161
11303
  // Get variables for the prompt
11162
11304
  const variables = {
11163
11305
  summary: context,
@@ -11165,59 +11307,106 @@ const handler$3 = async (argv, logger) => {
11165
11307
  additional_context: additional_context,
11166
11308
  commit_history: commit_history,
11167
11309
  branch_name_context: branchNameContext,
11310
+ commitlint_rules_context: commitlint_rules_context,
11168
11311
  };
11169
11312
  const maxAttempts = config.service.provider === 'ollama' && 'maxParsingAttempts' in config.service
11170
11313
  ? config.service.maxParsingAttempts || 3
11171
11314
  : 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' });
11315
+ // Custom retry logic for commitlint validation
11316
+ let retryCount = 0;
11317
+ let validationErrors = '';
11318
+ const generateCommitMessage = async () => {
11319
+ // Update variables with validation errors for retry attempts
11320
+ const currentVariables = {
11321
+ ...variables,
11322
+ additional_context: validationErrors
11323
+ ? `${variables.additional_context}\n\n## Validation Errors from Previous Attempt\nPlease fix the following issues:\n${validationErrors}`
11324
+ : variables.additional_context,
11325
+ };
11326
+ const commitMsg = await executeChainWithSchema(schema, llm, prompt, currentVariables, {
11327
+ retryOptions: {
11328
+ maxAttempts,
11329
+ onRetry: (attempt, error) => {
11330
+ logger.verbose(`Failed to parse commit message (attempt ${attempt}/${maxAttempts}): ${error.message}`, { color: 'yellow' });
11331
+ },
11177
11332
  },
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',
11333
+ fallbackParser: (text) => ({
11334
+ title: text.split('\n')[0] || 'Auto-generated commit',
11335
+ body: text.split('\n').slice(1).join('\n') || 'Generated commit message',
11336
+ }),
11337
+ onFallback: () => {
11338
+ logger.verbose('Max retry attempts reached. Falling back to simple text output.', {
11339
+ color: 'red',
11340
+ });
11341
+ },
11342
+ });
11343
+ // Construct the full commit message
11344
+ const appendedText = argv.append ? `\n\n${argv.append}` : '';
11345
+ const ticketId = extractTicketIdFromBranchName(branchName);
11346
+ const ticketFooter = argv.appendTicket && ticketId ? `\n\nPart of **${ticketId}**` : '';
11347
+ const fullMessage = `${commitMsg.title}\n\n${commitMsg.body}${appendedText}${ticketFooter}`;
11348
+ // If commitlint validation is needed, validate the message
11349
+ if (USE_CONVENTIONAL_COMMITS || hasCommitLintConfig) {
11350
+ const { validateCommitMessage, CommitlintValidationError } = await Promise.resolve().then(function () { return commitlintValidator; });
11351
+ const validationResult = await validateCommitMessage(fullMessage);
11352
+ logger.verbose(`Validation result: ${JSON.stringify(validationResult)}`, {
11353
+ color: 'yellow',
11186
11354
  });
11355
+ if (!validationResult.valid) {
11356
+ retryCount++;
11357
+ // Format validation errors for next attempt
11358
+ validationErrors = validationResult.errors.map((error) => `- ${error}`).join('\n');
11359
+ // Auto-retry up to 2 times
11360
+ if (retryCount <= 2) {
11361
+ logger.verbose(`Commit message validation failed (attempt ${retryCount}/2). Retrying with error feedback...`, { color: 'yellow' });
11362
+ throw new CommitlintValidationError(`Validation failed: ${validationResult.errors.join('; ')}`, validationResult, fullMessage);
11363
+ }
11364
+ // After 2 failed attempts, let the user decide
11365
+ const { handleValidationErrors } = await Promise.resolve().then(function () { return commitValidationHandler; });
11366
+ const validationHandlerResult = await handleValidationErrors(fullMessage, validationResult, {
11367
+ logger,
11368
+ interactive: INTERACTIVE,
11369
+ openInEditor: config.openInEditor,
11370
+ });
11371
+ logger.verbose(`Validation handler result: ${JSON.stringify(validationHandlerResult)}`, {
11372
+ color: 'blue',
11373
+ });
11374
+ switch (validationHandlerResult.action) {
11375
+ case 'proceed':
11376
+ return validationHandlerResult.message;
11377
+ case 'edit':
11378
+ return validationHandlerResult.message;
11379
+ case 'regenerate':
11380
+ // Reset retry count and validation errors for fresh attempts
11381
+ retryCount = 0;
11382
+ validationErrors = '';
11383
+ throw new CommitlintValidationError('User requested regeneration', validationResult, fullMessage);
11384
+ case 'abort':
11385
+ logger.log('\nAborting commit due to validation errors.', { color: 'red' });
11386
+ process.exit(1);
11387
+ }
11388
+ }
11389
+ }
11390
+ return fullMessage;
11391
+ };
11392
+ // Custom shouldRetry function for commitlint errors
11393
+ const shouldRetryCommitlint = (error) => {
11394
+ return error.name === 'CommitlintValidationError';
11395
+ };
11396
+ // Use retry wrapper for commitlint validation with up to 4 total attempts
11397
+ // (2 automatic retries + 2 more if user chooses "Try again")
11398
+ return await withRetry(generateCommitMessage, {
11399
+ maxAttempts: 6, // Allow for multiple user retry requests
11400
+ shouldRetry: shouldRetryCommitlint,
11401
+ backoffMs: 0, // No delay needed for commitlint retries
11402
+ onRetry: (attempt, error) => {
11403
+ if (error.name === 'CommitlintValidationError' && attempt <= 2) {
11404
+ // Don't log for auto-retries, we already log in the function
11405
+ return;
11406
+ }
11407
+ logger.verbose(`Retrying commit message generation (attempt ${attempt}): ${error.message}`, { color: 'yellow' });
11187
11408
  },
11188
11409
  });
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
11410
  },
11222
11411
  noResult: async () => {
11223
11412
  await noResult$2({ git, logger });
@@ -11258,28 +11447,6 @@ const builder$2 = (yargs) => {
11258
11447
  return yargs.options(options$2).usage(getCommandUsageHeader(command$2));
11259
11448
  };
11260
11449
 
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
11450
  /**
11284
11451
  * Executes a command as a Promise and returns the result.
11285
11452
  *
@@ -13199,11 +13366,13 @@ function isObservable(obj) {
13199
13366
  return !!obj && (obj instanceof Observable || (isFunction(obj.lift) && isFunction(obj.subscribe)));
13200
13367
  }
13201
13368
 
13202
- var EmptyError = createErrorClass(function (_super) { return function EmptyErrorImpl() {
13203
- _super(this);
13204
- this.name = 'EmptyError';
13205
- this.message = 'no elements in sequence';
13206
- }; });
13369
+ var EmptyError = createErrorClass(function (_super) {
13370
+ return function EmptyErrorImpl() {
13371
+ _super(this);
13372
+ this.name = 'EmptyError';
13373
+ this.message = 'no elements in sequence';
13374
+ };
13375
+ });
13207
13376
 
13208
13377
  function lastValueFrom(source, config) {
13209
13378
  var hasConfig = typeof config === 'object';
@@ -14490,23 +14659,183 @@ y.command(review.command, review.desc, review.builder, review.handler);
14490
14659
  y.command(init.command, init.desc, init.builder, init.handler);
14491
14660
  y.help().parse(process.argv.slice(2));
14492
14661
 
14662
+ /**
14663
+ * Edit a commit message with commitlint validation if config exists
14664
+ */
14665
+ async function editCommitMessage(message, options) {
14666
+ // First, let the user edit the message
14667
+ const editedMessage = await editResult(message, options);
14668
+ // Then validate it against commitlint if config exists
14669
+ const { hasCommitlintConfig } = await Promise.resolve().then(function () { return hasCommitlintConfig$1; });
14670
+ const hasConfig = await hasCommitlintConfig();
14671
+ if (hasConfig) {
14672
+ const { validateCommitMessage } = await Promise.resolve().then(function () { return commitlintValidator; });
14673
+ const { handleValidationErrors } = await Promise.resolve().then(function () { return commitValidationHandler; });
14674
+ const validationResult = await validateCommitMessage(editedMessage);
14675
+ if (!validationResult.valid) {
14676
+ // Show validation errors and get user action
14677
+ const validationHandlerResult = await handleValidationErrors(editedMessage, validationResult, {
14678
+ logger: options.logger,
14679
+ interactive: options.interactive,
14680
+ openInEditor: options.openInEditor,
14681
+ });
14682
+ // Return the result from the validation handler
14683
+ return validationHandlerResult.message;
14684
+ }
14685
+ }
14686
+ return editedMessage;
14687
+ }
14688
+
14689
+ var editCommitMessage$1 = /*#__PURE__*/Object.freeze({
14690
+ __proto__: null,
14691
+ editCommitMessage: editCommitMessage
14692
+ });
14693
+
14694
+ /**
14695
+ * Custom error for commitlint validation failures
14696
+ * This allows the retry system to identify these errors specifically
14697
+ */
14698
+ class CommitlintValidationError extends Error {
14699
+ constructor(message, validationResult, commitMessage) {
14700
+ super(message);
14701
+ this.name = 'CommitlintValidationError';
14702
+ this.validationResult = validationResult;
14703
+ this.commitMessage = commitMessage;
14704
+ }
14705
+ }
14493
14706
  /**
14494
14707
  * Load commitlint configuration
14495
14708
  */
14496
14709
  async function loadCommitlintConfig() {
14497
- // Dynamically import commitlint core
14498
- const commitlint = await import('@commitlint/core');
14499
- const { load } = commitlint;
14710
+ const projectRoot = findProjectRoot(process.cwd());
14711
+ const cwd = projectRoot || process.cwd();
14712
+ // @commitlint/load has issues with ESM configs (e.g. commitlint.config.js with `export default`).
14713
+ // Let's try to load them manually first.
14714
+ const esmConfigCandidates = COMMITLINT_CONFIG_FILES.filter((file) => file.endsWith('.js'));
14715
+ for (const configFile of esmConfigCandidates) {
14716
+ const configPath = join(cwd, configFile);
14717
+ if (existsSync(configPath)) {
14718
+ try {
14719
+ const module = await import(pathToFileURL(configPath).href);
14720
+ if (module.default &&
14721
+ (Object.keys(module.default.rules || {}).length > 0 ||
14722
+ (module.default.extends && module.default.extends.length > 0))) {
14723
+ // We found a config, now let commitlint process it (for extends etc)
14724
+ return await load(module.default, { cwd });
14725
+ }
14726
+ }
14727
+ catch (error) {
14728
+ // Failed to import, maybe not an ESM file after all or syntax error.
14729
+ // We will let the standard load take a chance.
14730
+ }
14731
+ }
14732
+ }
14500
14733
  try {
14501
- // Try to load project config
14502
- const config = await load();
14503
- return config;
14734
+ // Let @commitlint/load try to find the config. This works for CJS, JSON, and YAML.
14735
+ const config = await load({}, { cwd });
14736
+ // Check if a real config was loaded.
14737
+ if (config.extends.length > 0 || Object.keys(config.rules).length > 0) {
14738
+ return config;
14739
+ }
14504
14740
  }
14505
- catch {
14506
- // If no config found or error loading, use conventional config
14507
- return load({
14508
- extends: ['@commitlint/config-conventional'],
14509
- });
14741
+ catch (error) {
14742
+ // Could be an error parsing, or just not found. Fall through to default.
14743
+ }
14744
+ // If nothing worked, fallback to conventional config
14745
+ return load({
14746
+ extends: ['@commitlint/config-conventional'],
14747
+ });
14748
+ }
14749
+ /**
14750
+ * Format commitlint rules into a human-readable string for AI prompts
14751
+ */
14752
+ function formatCommitlintRulesForPrompt(config) {
14753
+ if (!config.rules || Object.keys(config.rules).length === 0) {
14754
+ return '';
14755
+ }
14756
+ const ruleDescriptions = [];
14757
+ // Add information about extends if present
14758
+ if (config.extends && config.extends.length > 0) {
14759
+ ruleDescriptions.push(`Following ${config.extends.join(', ')} configuration`);
14760
+ }
14761
+ // Process key rules that affect commit message format
14762
+ const rules = config.rules;
14763
+ // Header length rules
14764
+ if (rules['header-max-length']) {
14765
+ const [level, , maxLength] = rules['header-max-length'];
14766
+ if (level > 0) {
14767
+ ruleDescriptions.push(`Header (title) must be ${maxLength} characters or less (including spaces)`);
14768
+ }
14769
+ }
14770
+ if (rules['header-min-length']) {
14771
+ const [level, , minLength] = rules['header-min-length'];
14772
+ if (level > 0) {
14773
+ ruleDescriptions.push(`Header (title) must be at least ${minLength} characters (including spaces)`);
14774
+ }
14775
+ }
14776
+ // Body length rules
14777
+ if (rules['body-max-line-length']) {
14778
+ const [level, , maxLength] = rules['body-max-line-length'];
14779
+ if (level > 0) {
14780
+ ruleDescriptions.push(`Body lines must be ${maxLength} characters or less (including spaces)`);
14781
+ }
14782
+ }
14783
+ // Type rules
14784
+ if (rules['type-enum']) {
14785
+ const [level, , allowedTypes] = rules['type-enum'];
14786
+ if (level > 0 && Array.isArray(allowedTypes)) {
14787
+ ruleDescriptions.push(`Allowed types: ${allowedTypes.join(', ')}`);
14788
+ }
14789
+ }
14790
+ // Case rules
14791
+ if (rules['type-case']) {
14792
+ const [level, , caseType] = rules['type-case'];
14793
+ if (level > 0) {
14794
+ ruleDescriptions.push(`Type must be ${caseType} case`);
14795
+ }
14796
+ }
14797
+ if (rules['subject-case']) {
14798
+ const [level, , caseType] = rules['subject-case'];
14799
+ if (level > 0) {
14800
+ ruleDescriptions.push(`Subject must be ${caseType} case`);
14801
+ }
14802
+ }
14803
+ // Scope rules
14804
+ if (rules['scope-enum']) {
14805
+ const [level, , allowedScopes] = rules['scope-enum'];
14806
+ if (level > 0 && Array.isArray(allowedScopes)) {
14807
+ ruleDescriptions.push(`Allowed scopes: ${allowedScopes.join(', ')}`);
14808
+ }
14809
+ }
14810
+ // Subject rules
14811
+ if (rules['subject-full-stop']) {
14812
+ const [level, condition] = rules['subject-full-stop'];
14813
+ if (level > 0) {
14814
+ const verb = condition === 'always' ? 'must' : 'must not';
14815
+ ruleDescriptions.push(`Subject ${verb} end with a period`);
14816
+ }
14817
+ }
14818
+ if (rules['subject-empty']) {
14819
+ const [level, condition] = rules['subject-empty'];
14820
+ if (level > 0) {
14821
+ const requirement = condition === 'never' ? 'must not be empty' : 'must be empty';
14822
+ ruleDescriptions.push(`Subject ${requirement}`);
14823
+ }
14824
+ }
14825
+ return ruleDescriptions.length > 0
14826
+ ? `## Commitlint Rules\nYour commit message must follow these project-specific rules:\n${ruleDescriptions.map(rule => `- ${rule}`).join('\n')}\n`
14827
+ : '';
14828
+ }
14829
+ /**
14830
+ * Get commitlint rules context for prompt if config exists
14831
+ */
14832
+ async function getCommitlintRulesContext() {
14833
+ try {
14834
+ const config = await loadCommitlintConfig();
14835
+ return formatCommitlintRulesForPrompt(config);
14836
+ }
14837
+ catch (error) {
14838
+ return '';
14510
14839
  }
14511
14840
  }
14512
14841
  /**
@@ -14515,9 +14844,6 @@ async function loadCommitlintConfig() {
14515
14844
  async function validateCommitMessage(message, options = {}) {
14516
14845
  try {
14517
14846
  const config = await loadCommitlintConfig();
14518
- // Dynamically import commitlint lint function
14519
- const commitlint = await import('@commitlint/core');
14520
- const { lint } = commitlint;
14521
14847
  const result = await lint(message, config.rules, options);
14522
14848
  return {
14523
14849
  valid: result.valid,
@@ -14536,6 +14862,9 @@ async function validateCommitMessage(message, options = {}) {
14536
14862
 
14537
14863
  var commitlintValidator = /*#__PURE__*/Object.freeze({
14538
14864
  __proto__: null,
14865
+ CommitlintValidationError: CommitlintValidationError,
14866
+ formatCommitlintRulesForPrompt: formatCommitlintRulesForPrompt,
14867
+ getCommitlintRulesContext: getCommitlintRulesContext,
14539
14868
  loadCommitlintConfig: loadCommitlintConfig,
14540
14869
  validateCommitMessage: validateCommitMessage
14541
14870
  });
@@ -14572,29 +14901,45 @@ async function handleValidationErrors(message, validationResult, options) {
14572
14901
  message: 'How would you like to proceed?:',
14573
14902
  choices: [
14574
14903
  {
14575
- name: 'Edit',
14576
- value: 'edit',
14577
- description: 'Edit the commit message manually',
14904
+ name: 'Try 2 more attempts',
14905
+ value: 'retry',
14906
+ description: 'Let the AI try generating 2 more commit messages with error feedback',
14578
14907
  },
14579
14908
  {
14580
- name: 'Retry',
14581
- value: 'retry',
14582
- description: 'Regenerate a new commit message',
14909
+ name: 'Edit manually',
14910
+ value: 'edit',
14911
+ description: 'Edit the commit message manually to fix the issues',
14583
14912
  },
14584
14913
  {
14585
14914
  name: 'Abort',
14586
14915
  value: 'abort',
14587
- description: 'Abort the commit',
14916
+ description: 'Abort the commit process',
14588
14917
  },
14589
14918
  ],
14590
14919
  });
14591
14920
  switch (choice) {
14592
- case '1': {
14921
+ case 'edit': {
14593
14922
  // Edit message manually
14594
14923
  const editedMessage = await editResult(message, options);
14924
+ // Validate the manually edited message if commitlint config exists
14925
+ const { hasCommitlintConfig } = await Promise.resolve().then(function () { return hasCommitlintConfig$1; });
14926
+ const hasConfig = await hasCommitlintConfig();
14927
+ if (hasConfig) {
14928
+ const { validateCommitMessage } = await Promise.resolve().then(function () { return commitlintValidator; });
14929
+ const editedValidationResult = await validateCommitMessage(editedMessage);
14930
+ if (!editedValidationResult.valid) {
14931
+ // Show validation errors for the edited message
14932
+ options.logger.log('\nEdited commit message also has validation issues:', { color: 'yellow' });
14933
+ editedValidationResult.errors.forEach((error) => {
14934
+ options.logger.log(` • ${error}`, { color: 'red' });
14935
+ });
14936
+ // Recursively handle validation errors for the edited message
14937
+ return await handleValidationErrors(editedMessage, editedValidationResult, options);
14938
+ }
14939
+ }
14595
14940
  return { message: editedMessage, action: 'edit' };
14596
14941
  }
14597
- case '2':
14942
+ case 'retry':
14598
14943
  // Regenerate message
14599
14944
  return { message, action: 'regenerate' };
14600
14945
  default: