@elsikora/commitizen-plugin-commitlint-ai
Version:
AI-powered Commitizen adapter with Commitlint integration
317 lines (314 loc) • 12.4 kB
JavaScript
import chalk from 'chalk';
import ora from 'ora';
import prompts from 'prompts';
/**
* Implementation of the CLI interface service using the prompts library.
* Provides methods for interacting with the user through the command line.
*/
class PromptsCliInterface {
/** Reference to the active spinner instance */
// @ts-ignore
spinner;
/**
* Initializes a new instance of the PromptsCliInterface.
* Sets up the spinner for providing visual feedback during operations.
*/
constructor() {
this.spinner = ora();
}
/**
* Clears the console screen.
*/
clear() {
process.stdout.write("\u001Bc");
}
/**
* Displays a confirmation prompt to the user.
* @param {string} message - The message to display to the user
* @param {boolean} isConfirmedByDefault - The default value for the confirmation, defaults to false
* @returns {Promise<boolean>} Promise that resolves to the user's response (true for confirmed, false for declined)
*/
async confirm(message, isConfirmedByDefault = false) {
try {
const response = await prompts({
active: "Yes",
inactive: "No",
// eslint-disable-next-line @elsikora/typescript/naming-convention
initial: isConfirmedByDefault,
message,
name: "value",
type: "toggle",
});
if (response.value === undefined) {
this.error("Operation cancelled by user");
process.exit(0);
}
return response.value;
}
catch {
this.error("Operation cancelled by user");
process.exit(0);
}
}
/**
* Displays an error message to the user.
* @param {string} message - The error message to display
*/
error(message) {
process.stderr.write(`${chalk.red(message)}\n`);
}
/**
* Displays a grouped multi-select prompt to the user.
* @param {string} message - The message to display to the user
* @param {Record<string, Array<ICliInterfaceServiceSelectOptions>>} options - Record of groups and their options
* @param {boolean} isRequired - Whether a selection is required, defaults to false
* @param {Array<string>} initialValues - Initial selected values
* @returns {Promise<Array<T>>} Promise that resolves to an array of selected values
* @template T - The type of the selected values
*/
async groupMultiselect(message, options, isRequired = false, initialValues) {
// Convert options to a flat array with group prefixes
// eslint-disable-next-line @elsikora/typescript/naming-convention
const choices = [];
for (const [group, groupOptions] of Object.entries(options)) {
for (const opt of groupOptions) {
choices.push({
// eslint-disable-next-line @elsikora/typescript/naming-convention
selected: initialValues?.includes(opt.value) ?? false,
title: `${group}: ${opt.label}`,
value: opt.value,
});
}
}
try {
const response = await prompts({
choices,
// eslint-disable-next-line @elsikora/typescript/naming-convention
instructions: false,
message: `${message} (space to select)`,
min: isRequired ? 1 : undefined,
name: "values",
type: "multiselect",
});
if (response.values === undefined) {
this.error("Operation cancelled by user");
process.exit(0);
}
return response.values;
}
catch {
this.error("Operation cancelled by user");
process.exit(0);
}
}
/**
* Handles and displays an error message with additional error details.
* @param {string} message - The error message to display
* @param {unknown} error - The error object or details
*/
handleError(message, error) {
process.stderr.write(`${chalk.red(message)}\n`);
process.stderr.write(`${String(error)}\n`);
}
/**
* Displays an informational message to the user.
* @param {string} message - The info message to display
*/
info(message) {
process.stdout.write(`${chalk.blue(message)}\n`);
}
/**
* Displays a standard message to the user.
* @param {string} message - The message to display
*/
log(message) {
process.stdout.write(`${message}\n`);
}
/**
* Displays a multi-select prompt to the user.
* @param {string} message - The message to display to the user
* @param {Array<ICliInterfaceServiceSelectOptions>} options - Array of options to select from
* @param {boolean} isRequired - Whether a selection is required, defaults to false
* @param {Array<string>} initialValues - Initial selected values
* @returns {Promise<Array<T>>} Promise that resolves to an array of selected values
* @template T - The type of the selected values
*/
async multiselect(message, options, isRequired = false, initialValues) {
// eslint-disable-next-line @elsikora/typescript/naming-convention
const choices = options.map((opt) => ({
// eslint-disable-next-line @elsikora/typescript/naming-convention
selected: initialValues?.includes(opt.value) ?? false,
title: opt.label,
value: opt.value,
}));
try {
const response = await prompts({
choices,
// eslint-disable-next-line @elsikora/typescript/naming-convention
instructions: false,
message: `${message} (space to select)`,
min: isRequired ? 1 : undefined,
name: "values",
type: "multiselect",
});
if (response.values === undefined) {
this.error("Operation cancelled by user");
process.exit(0);
}
return response.values;
}
catch {
this.error("Operation cancelled by user");
process.exit(0);
}
}
/**
* Displays a note to the user with a title and message.
* @param {string} title - The title of the note
* @param {string} message - The message content of the note
*/
note(title, message) {
const lines = message.split("\n");
// eslint-disable-next-line @elsikora/typescript/no-magic-numbers
const width = Math.max(title.length, ...lines.map((line) => line.length)) + 4; // Add padding
const top = `┌${"─".repeat(width)}┐`;
const bottom = `└${"─".repeat(width)}┘`;
// Create middle lines with padding
// eslint-disable-next-line @elsikora/typescript/no-magic-numbers
const paddedTitle = ` ${title.padEnd(width - 2)} `;
// eslint-disable-next-line @elsikora/typescript/no-magic-numbers
const paddedLines = lines.map((line) => ` ${line.padEnd(width - 2)} `);
// Log the note box with styling
process.stdout.write(`${chalk.dim(top)}\n`);
process.stdout.write(`${chalk.dim("│") + chalk.bold(paddedTitle) + chalk.dim("│")}\n`);
if (lines.length > 0) {
// Add a separator line
const separator = `├${"─".repeat(width)}┤`;
process.stdout.write(`${chalk.dim(separator)}\n`);
// Add message content
for (const line of paddedLines) {
process.stdout.write(`${chalk.dim("│") + chalk.dim(line) + chalk.dim("│")}\n`);
}
}
process.stdout.write(`${chalk.dim(bottom)}\n`);
}
/**
* Displays a single select prompt to the user.
* @param {string} message - The message to display to the user
* @param {Array<ICliInterfaceServiceSelectOptions>} options - Array of options to select from
* @param {string} initialValue - Initial selected value
* @returns {Promise<T>} Promise that resolves to the selected value
* @template T - The type of the selected value
*/
async select(message, options, initialValue) {
const choices = options.map((opt) => ({
title: opt.label,
value: opt.value,
}));
const initialIndex = initialValue ? choices.findIndex((choice) => choice.value === initialValue) : undefined;
try {
const response = await prompts({
choices,
initial: initialIndex === -1 ? 0 : initialIndex,
message,
name: "value",
type: "select",
});
if (response.value === undefined) {
this.error("Operation cancelled by user");
process.exit(0);
}
return response.value;
}
catch {
this.error("Operation cancelled by user");
process.exit(0);
}
}
/**
* Starts a spinner with the specified message.
* Stops any existing spinner first.
* @param {string} message - The message to display while the spinner is active
*/
startSpinner(message) {
this.spinner.stop();
this.spinner = ora(message).start();
}
/**
* Stops the current spinner with an optional completion message.
* @param {string} message - Optional message to display when the spinner stops
*/
stopSpinner(message) {
this.spinner.stop();
if (message) {
process.stdout.write(`${message}\n`);
}
}
/**
* Displays a success message to the user.
* @param {string} message - The success message to display
*/
success(message) {
process.stdout.write(`${chalk.green(message)}\n`);
}
/**
* Displays a text input prompt to the user.
* @param {string} message - The message to display to the user
* @param {string} _placeholder - Optional placeholder text for the input field (unused)
* @param {string} initialValue - Optional initial value for the input field
* @param {(value: string) => Error | string | undefined} validate - Optional validation function for the input
* @returns {Promise<string>} Promise that resolves to the user's input text
*/
async text(message, _placeholder, initialValue, validate) {
// Convert the validate function to match prompts' expected format
const promptsValidate = validate
? // eslint-disable-next-line @elsikora/typescript/explicit-function-return-type
(value) => {
const result = validate(value);
if (result === undefined)
return true;
if (typeof result === "string")
return result;
if (result instanceof Error)
return result.message;
return "Invalid input";
}
: undefined;
try {
const response = await prompts({
initial: initialValue,
message,
name: "value",
type: "text",
validate: promptsValidate,
});
if (response.value === undefined) {
this.error("Operation cancelled by user");
process.exit(0);
}
return response.value;
}
catch {
this.error("Operation cancelled by user");
process.exit(0);
}
}
/**
* Update the spinner message without stopping it.
* @param {string} message - The new message to display
*/
updateSpinner(message) {
if (this.spinner?.isSpinning) {
this.spinner.text = message;
}
}
/**
* Displays a warning message to the user.
* @param {string} message - The warning message to display
*/
warn(message) {
process.stdout.write(`${chalk.yellow(message)}\n`);
}
}
export { PromptsCliInterface };
//# sourceMappingURL=prompts-cli-interface.service.js.map