Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions @commitlint/cz-commitlint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ The interactive process is inspired by [cz-conventional-changelog](https://githu
### Configure commitizen adapter

```bash
npm install --save-dev @commitlint/cz-commitlint commitizen inquirer@9 # inquirer is required as peer dependency
npm install --save-dev @commitlint/cz-commitlint commitizen inquirer@14 # inquirer is required as peer dependency
# or yarn
yarn add -D @commitlint/cz-commitlint commitizen inquirer@9 # inquirer is required as peer dependency
yarn add -D @commitlint/cz-commitlint commitizen inquirer@14 # inquirer is required as peer dependency
```
Comment on lines +16 to 19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Docs still require inquirer 📎 Requirement gap ⚙ Maintainability

Documentation continues to instruct users to install inquirer as the required peer dependency
rather than reflecting a @inquirer/prompts-based prompt engine. This conflicts with the
requirement to update docs away from legacy inquirer guidance.
Agent Prompt
## Issue description
README installation/usage guidance still explicitly requires `inquirer` as a peer dependency, instead of reflecting `@inquirer/prompts` after migration.

## Issue Context
Compliance requires updating dependencies/docs to reflect the new prompt engine and removing explicit legacy inquirer recommendations.

## Fix Focus Areas
- @commitlint/cz-commitlint/README.md[15-19]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


In package.json
Expand Down
6 changes: 3 additions & 3 deletions @commitlint/cz-commitlint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@
"word-wrap": "^1.2.5"
},
"devDependencies": {
"@types/inquirer": "^9.0.7",
"commitizen": "^4.2.4"
"commitizen": "^4.2.4",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dependency Engine Exceeds Package Floor

inquirer@14.0.2 requires Node ^22.13.0, ^20.17.0, or >=23.5.0, while this package still advertises node >=22.12.0. A valid Node 22.12.0 install can satisfy @commitlint/cz-commitlint but fail when engine checks reach the new inquirer dependency or its transitive packages.

"inquirer": "^14.0.2"
},
Comment on lines +46 to 48

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

6. Node engine floor mismatch 🐞 Bug ☼ Reliability

The package engine allows Node >=22.12.0, but the newly added devDependency inquirer@14.0.2
requires Node ^22.13.0 (or newer) per the lockfile. Installs/tests run on Node 22.12.x (allowed by
this package) can fail due to dependency engine constraints.
Agent Prompt
## Issue description
`@commitlint/cz-commitlint` adds `inquirer@^14.0.2` as a devDependency, but `inquirer@14.0.2` requires Node `^22.13.0` while the package advertises `>=22.12.0`. This can break `pnpm install`/CI in environments on 22.12.x.

## Issue Context
The repository/package engines currently permit Node 22.12.x; adding a devDependency with a stricter engine effectively tightens the real supported Node range for contributors/CI.

## Fix Focus Areas
- @commitlint/cz-commitlint/package.json[45-61]
- pnpm-lock.yaml[3680-3683]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 45 to 48
"peerDependencies": {
"commitizen": "^4.0.3",
"inquirer": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0"
"inquirer": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^14.0.0"
},
Comment on lines 45 to 52

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. No @inquirer/prompts dependency 📎 Requirement gap ⚙ Maintainability

The package still declares inquirer as a peer dependency and does not add a direct dependency on
@inquirer/prompts as required by the migration plan. This keeps consumers coupled to inquirer
rather than the intended prompt engine.
Agent Prompt
## Issue description
The package is still centered on `inquirer` (peer + dev dependency) and does not declare `@inquirer/prompts` as required.

## Issue Context
Compliance requires adding/updating `@inquirer/prompts` and removing the legacy peer dependency constraint once migration is complete.

## Fix Focus Areas
- @commitlint/cz-commitlint/package.json[45-52]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 49 to 52
"config": {
"commitizen": {
Expand Down
8 changes: 4 additions & 4 deletions @commitlint/cz-commitlint/src/Process.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
import { QualifiedRules, RuleConfigSeverity, UserPromptConfig } from "@commitlint/types";
import { Answers, DistinctQuestion } from "inquirer";

import process from "./Process.js";
import { PromptAnswers, PromptQuestion } from "./types.js";

const mockShowTitle = vi.fn();
const mockShowValidation = vi.fn((message) => message);

// mock inquirer
const mockPrompt = vi.fn(async function (questions: DistinctQuestion[], answers: Answers) {
const mockPrompt = vi.fn(async function (questions: PromptQuestion[], answers: PromptAnswers) {
for (const { name, message, when, filter, validate } of questions) {
if (typeof when !== "function" || (await when(answers))) {
const title =
Expand All @@ -32,9 +32,9 @@ const mockPrompt = vi.fn(async function (questions: DistinctQuestion[], answers:
}
});

function InquirerFactory(answers: Answers) {
function InquirerFactory(answers: PromptAnswers) {
const inquirer = {
async prompt(questions: DistinctQuestion[]) {
async prompt(questions: PromptQuestion[]) {
await mockPrompt(questions, answers);
return answers;
},
Expand Down
4 changes: 2 additions & 2 deletions @commitlint/cz-commitlint/src/Process.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { QualifiedRules, UserPromptConfig } from "@commitlint/types";
import type { Answers, DistinctQuestion } from "inquirer";
import type { PromptAnswers, PromptQuestion } from "./types.js";

import {
combineCommitMessage as combineBody,
Expand All @@ -20,7 +20,7 @@ export default async function (
rules: QualifiedRules,
prompts: UserPromptConfig,
inquirer: {
prompt(questions: DistinctQuestion[]): Promise<Answers>;
prompt(questions: PromptQuestion[]): Promise<PromptAnswers>;
},
): Promise<string> {
setRules(rules);
Comment on lines 20 to 26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Still uses inquirer.prompt() 📎 Requirement gap ⚙ Maintainability

The runtime still depends on an injected inquirer/inquirerIns instance and executes questions
via inquirer.prompt(questions), rather than migrating to @inquirer/prompts for sequential
prompting. This keeps internals coupled to a legacy inquirer instance and blocks the required
compliance migration away from legacy prompt execution.
Agent Prompt
## Issue description
The adapter and processing flow still rely on a passed `inquirerIns`/`inquirer` instance (via a required `.prompt(...)` method) and execute prompts through `inquirer.prompt(questions)`, instead of using `@inquirer/prompts` for sequential prompting; this violates compliance requirements to remove legacy inquirer prompt execution while also decoupling internals from the injected inquirer instance.

## Issue Context
Compliance requires (1) eliminating runtime calls to `inquirer.prompt(questions[])` in favor of `@inquirer/prompts` while preserving the sequential flow/UX/output, and (2) keeping the external prompter contract stable (as expected by commitizen) while ensuring internals do not require the passed inquirer instance to function (it should be ignored or used only for signature compatibility).

## Fix Focus Areas
- @commitlint/cz-commitlint/src/Process.ts[19-34]
- @commitlint/cz-commitlint/src/index.ts[13-21]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Expand Down
29 changes: 18 additions & 11 deletions @commitlint/cz-commitlint/src/Question.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { describe, test, expect, vi } from "vitest";
import pc from "picocolors";
import inquirer, { Answers, InputQuestionOptions } from "inquirer";
import inquirer from "inquirer";

import Question from "./Question.js";
import { PromptAnswers } from "./types.js";

const MESSAGES = {
skip: "(press enter to skip)",
Expand Down Expand Up @@ -54,12 +55,12 @@ describe("name", () => {
});

describe("type", () => {
test('should return "list" type when enumList is array and multipleSelectDefaultDelimiter is undefined', () => {
test('should return "select" type when enumList is array and multipleSelectDefaultDelimiter is undefined', () => {
const question = new Question("scope", {
...QUESTION_CONFIG,
enumList: ["cli", "core"],
}).question;
expect(question).toHaveProperty("type", "list");
expect(question).toHaveProperty("type", "select");
expect(question).toHaveProperty("choices", ["cli", "core"]);
expect(question).not.toHaveProperty("transformer");
});
Expand All @@ -81,7 +82,7 @@ describe("type", () => {
enumList: ["cli", "core"],
skip: true,
}).question;
expect(question).toHaveProperty("type", "list");
expect(question).toHaveProperty("type", "select");
expect(question).toHaveProperty("choices", [
"cli",
"core",
Expand Down Expand Up @@ -113,6 +114,10 @@ describe("type", () => {
});
});

type QuestionWithTransformer = {
transformer?: (input: string, answers: PromptAnswers, context?: unknown) => string;
};

describe("message", () => {
test("should display title when it is not input", () => {
const question = new Question("body", {
Expand Down Expand Up @@ -176,7 +181,7 @@ describe("message", () => {
test("should execute function beforeQuestionStart when init message", () => {
const mockFn = vi.fn();
class CustomQuestion extends Question {
beforeQuestionStart(answers: Answers): void {
beforeQuestionStart(answers: PromptAnswers): void {
mockFn(answers);
}
}
Expand Down Expand Up @@ -303,7 +308,7 @@ describe("transformer", () => {
fullStopFn: (input: string) => input + "!",
}).question;

expect((question as InputQuestionOptions)?.transformer?.("xxxx", {}, {})).toBe("Xxxx!");
expect((question as QuestionWithTransformer)?.transformer?.("xxxx", {}, {})).toBe("Xxxx!");
});

test("should char count with green color when in the limit range", () => {
Expand All @@ -312,7 +317,7 @@ describe("transformer", () => {
maxLength: 5,
}).question;

expect((question as InputQuestionOptions)?.transformer?.("xxx", {}, {})).toEqual(
expect((question as QuestionWithTransformer)?.transformer?.("xxx", {}, {})).toEqual(
pc.green(`(3) xxx`),
);

Expand All @@ -321,7 +326,7 @@ describe("transformer", () => {
minLength: 2,
}).question;

expect((question as InputQuestionOptions)?.transformer?.("xxx", {}, {})).toEqual(
expect((question as QuestionWithTransformer)?.transformer?.("xxx", {}, {})).toEqual(
pc.green(`(3) xxx`),
);
});
Expand All @@ -332,7 +337,7 @@ describe("transformer", () => {
maxLength: 5,
}).question;

expect((question as InputQuestionOptions)?.transformer?.("xxxxxx", {}, {})).toEqual(
expect((question as QuestionWithTransformer)?.transformer?.("xxxxxx", {}, {})).toEqual(
pc.red(`(6) xxxxxx`),
);

Expand All @@ -341,13 +346,15 @@ describe("transformer", () => {
minLength: 2,
}).question;

expect((question as InputQuestionOptions)?.transformer?.("x", {}, {})).toEqual(pc.red(`(1) x`));
expect((question as QuestionWithTransformer)?.transformer?.("x", {}, {})).toEqual(
pc.red(`(1) x`),
);
});
});

describe("inquirer question", () => {
test('should pass "when" and "default" field to inquirer question', () => {
const when = (answers: Answers) => !!answers.header;
const when = (answers: PromptAnswers) => !!answers.header;
const question = new Question("body", {
...QUESTION_CONFIG,
when,
Expand Down
28 changes: 16 additions & 12 deletions @commitlint/cz-commitlint/src/Question.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
import { PromptMessages, PromptName } from "@commitlint/types";
import pc from "picocolors";
import inquirer, { Answers, ChoiceCollection, DistinctQuestion } from "inquirer";
import inquirer from "inquirer";

import { PromptAnswers, PromptQuestion } from "./types.js";
import { CaseFn } from "./utils/case-fn.js";
import { FullStopFn } from "./utils/full-stop-fn.js";

export type QuestionConfig = {
enumList?: Array<
| string
| {
name: string;
value: string;
}
> | null;
title: string;
messages: PromptMessages;
maxLength?: number;
minLength?: number;
defaultValue?: string;
when?: DistinctQuestion["when"];
when?: PromptQuestion["when"];
skip?: boolean;
enumList?: ChoiceCollection<{
name: string;
value: string;
}> | null;
multipleValueDelimiters?: RegExp;
multipleSelectDefaultDelimiter?: string;
fullStopFn?: FullStopFn;
caseFn?: CaseFn;
};

export default class Question {
private _question: Readonly<DistinctQuestion>;
private _question: Readonly<PromptQuestion>;
private messages: PromptMessages;
private skip: boolean;
private _maxLength: number;
Expand Down Expand Up @@ -68,7 +72,7 @@ export default class Question {

if (enumList && Array.isArray(enumList)) {
this._question = {
type: multipleSelectDefaultDelimiter ? "checkbox" : "list",
type: multipleSelectDefaultDelimiter ? "checkbox" : "select",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Select Type Breaks Older Peers

When a project uses a still-supported inquirer 9-12 peer, enum-backed prompts now send type: "select" directly to inquirer.prompt(). Those versions use list for single-select prompts, so commitizen can fail to render the prompt or fall back to free text for type and scope selections.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

5. Select prompt peer mismatch 🐞 Bug ≡ Correctness

Question now emits type: "select" for single-choice enum questions, but the package still
declares peer compatibility with inquirer ^9^12. Without a fallback/mapping, installs using
those still-allowed peer versions may fail at runtime due to an unsupported question type.
Agent Prompt
## Issue description
`@commitlint/cz-commitlint` now generates enum questions with `type: "select"`, but the peer dependency range still includes inquirer majors that may not support that question type. This creates a runtime compatibility gap relative to the declared peer contract.

## Issue Context
The adapter constructs question objects in `Question` (no access to the passed-in `inquirerIns`), so any compatibility logic must either:
- avoid version-specific question type strings, or
- narrow the supported peer range to match the implementation, or
- restructure question building to allow using the runtime inquirer instance to select an appropriate type.

## Fix Focus Areas
- @commitlint/cz-commitlint/src/Question.ts[73-86]
- @commitlint/cz-commitlint/package.json[49-52]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

choices: skip
? [
...enumList,
Comment on lines 73 to 78

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Uses inquirer.separator() 📎 Requirement gap ≡ Correctness

Choice separators are still implemented via new inquirer.Separator() rather than adopting the
@inquirer/prompts Separator support required for select/checkbox prompts. This risks UX divergence
once prompt execution is migrated.
Agent Prompt
## Issue description
Separators are still created with `inquirer.Separator()` instead of the equivalent Separator supported by `@inquirer/prompts`.

## Issue Context
Compliance requires preserving separator/choice UX while moving to `@inquirer/prompts`.

## Fix Focus Areas
- @commitlint/cz-commitlint/src/Question.ts[73-86]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Expand Down Expand Up @@ -105,7 +109,7 @@ export default class Question {
return this.messages[key] ?? "";
}

get question(): Readonly<DistinctQuestion> {
get question(): Readonly<PromptQuestion> {
return this._question;
}

Expand All @@ -125,7 +129,7 @@ export default class Question {
this._minLength = minLength;
}

protected beforeQuestionStart(_answers: Answers): void {
protected beforeQuestionStart(_answers: PromptAnswers): void {
return;
}

Expand Down Expand Up @@ -172,7 +176,7 @@ export default class Question {
return this.fullStopFn(toCased);
}

protected transformer(input: string, _answers: Answers): string {
protected transformer(input: string, _answers: PromptAnswers): string {
const output = this.filter(input);

if (this.maxLength === Infinity && this.minLength === 0) {
Expand All @@ -183,7 +187,7 @@ export default class Question {
return color("(" + output.length + ") " + output);
}

protected decorateMessage(_answers: Answers): string {
protected decorateMessage(_answers: PromptAnswers): string {
if (this.beforeQuestionStart) {
this.beforeQuestionStart(_answers);
}
Expand Down
6 changes: 3 additions & 3 deletions @commitlint/cz-commitlint/src/SectionBody.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { Answers, DistinctQuestion } from "inquirer";
import wrap from "word-wrap";

import Question from "./Question.js";
import getRuleQuestionConfig from "./services/getRuleQuestionConfig.js";
import { getRule } from "./store/rules.js";
import { PromptAnswers, PromptQuestion } from "./types.js";
import getLeadingBlankFn from "./utils/leading-blank-fn.js";
import { getMaxLength } from "./utils/rules.js";

export function getQuestions(): Array<DistinctQuestion> {
export function getQuestions(): Array<PromptQuestion> {
// body
const questionConfig = getRuleQuestionConfig("body");

if (!questionConfig) return [];
else return [new Question("body", questionConfig).question];
}

export function combineCommitMessage(answers: Answers): string {
export function combineCommitMessage(answers: PromptAnswers): string {
const maxLineLength = getMaxLength(getRule("body", "max-line-length"));
const leadingBlankFn = getLeadingBlankFn(getRule("body", "leading-blank"));
const { body, breakingBody, issuesBody } = answers;
Expand Down
16 changes: 8 additions & 8 deletions @commitlint/cz-commitlint/src/SectionFooter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { PromptName } from "@commitlint/types";
import { Answers, DistinctQuestion } from "inquirer";
import wrap from "word-wrap";

import Question, { QuestionConfig } from "./Question.js";
import getRuleQuestionConfig from "./services/getRuleQuestionConfig.js";
import { getPromptMessages, getPromptQuestions } from "./store/prompts.js";
import { getRule } from "./store/rules.js";
import { PromptAnswers, PromptQuestion } from "./types.js";
import getLeadingBlankFn from "./utils/leading-blank-fn.js";
import { getMaxLength } from "./utils/rules.js";

Expand All @@ -22,15 +22,15 @@ export class FooterQuestion extends Question {
this.footerMaxLength = footerMaxLength ?? Infinity;
this.footerMinLength = footerMinLength ?? 0;
}
beforeQuestionStart(answers: Answers): void {
beforeQuestionStart(answers: PromptAnswers): void {
const footerRemainLength =
this.footerMaxLength - combineCommitMessage(answers).length - "\n".length;
this.maxLength = Math.min(this.maxLength, footerRemainLength);
this.minLength = Math.min(this.minLength, this.footerMinLength);
}
}

export function getQuestions(): Array<DistinctQuestion> {
export function getQuestions(): Array<PromptQuestion> {
const footerQuestionConfig = getRuleQuestionConfig("footer");

if (!footerQuestionConfig) return [];
Expand Down Expand Up @@ -68,15 +68,15 @@ export function getQuestions(): Array<DistinctQuestion> {

if (name === "breakingBody") {
Object.assign(questionConfigs, {
when: (answers: Answers) => {
when: (answers: PromptAnswers) => {
return answers.isBreaking && !answers.body;
},
});
}

if (name === "breaking") {
Object.assign(questionConfigs, {
when: (answers: Answers) => {
when: (answers: PromptAnswers) => {
return answers.isBreaking;
},
});
Expand All @@ -90,15 +90,15 @@ export function getQuestions(): Array<DistinctQuestion> {

if (name === "issuesBody") {
Object.assign(questionConfigs, {
when: (answers: Answers) => {
when: (answers: PromptAnswers) => {
return answers.isIssueAffected && !answers.body && !answers.breakingBody;
},
});
}

if (name === "issues") {
Object.assign(questionConfigs, {
when: (answers: Answers) => {
when: (answers: PromptAnswers) => {
return answers.isIssueAffected;
},
});
Expand All @@ -116,7 +116,7 @@ export function getQuestions(): Array<DistinctQuestion> {
});
}

export function combineCommitMessage(answers: Answers): string {
export function combineCommitMessage(answers: PromptAnswers): string {
// TODO references-empty
// TODO signed-off-by
const maxLineLength = getMaxLength(getRule("footer", "max-line-length"));
Expand Down
Loading