@elsikora/setup-wizard
Version:
Setup Wizard - CLI scaffolding utility
315 lines (312 loc) • 11.8 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() {
// eslint-disable-next-line @elsikora/javascript/no-console
console.clear();
}
/**
* Displays a confirmation prompt to the user.
* @param message - The message to display to the user
* @param isConfirmedByDefault - The default value for the confirmation, defaults to false
* @returns 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 message - The error message to display
*/
error(message) {
console.error(chalk.red(message));
}
/**
* Displays a grouped multi-select prompt to the user.
* @param message - The message to display to the user
* @param options - Record of groups and their options
* @param isRequired - Whether a selection is required, defaults to false
* @param initialValues - Initial selected values
* @returns Promise that resolves to an array of 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 message - The error message to display
* @param error - The error object or details
*/
handleError(message, error) {
console.error(chalk.red(message));
console.error(error);
}
/**
* Displays an informational message to the user.
* @param message - The info message to display
*/
info(message) {
// eslint-disable-next-line @elsikora/javascript/no-console
console.log(chalk.blue(message));
}
/**
* Displays a standard message to the user.
* @param message - The message to display
*/
log(message) {
// eslint-disable-next-line @elsikora/javascript/no-console
console.log(message);
}
/**
* Displays a multi-select prompt to the user.
* @param message - The message to display to the user
* @param options - Array of options to select from
* @param isRequired - Whether a selection is required, defaults to false
* @param initialValues - Initial selected values
* @returns Promise that resolves to an array of 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 title - The title of the note
* @param 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
// eslint-disable-next-line @elsikora/javascript/no-console
console.log(chalk.dim(top));
// eslint-disable-next-line @elsikora/javascript/no-console
console.log(chalk.dim("│") + chalk.bold(paddedTitle) + chalk.dim("│"));
if (lines.length > 0) {
// Add a separator line
// eslint-disable-next-line @elsikora/javascript/no-console
console.log(chalk.dim(`├${"─".repeat(width)}┤`));
// Add message content
for (const line of paddedLines) {
// eslint-disable-next-line @elsikora/javascript/no-console
console.log(chalk.dim("│") + chalk.dim(line) + chalk.dim("│"));
}
}
// eslint-disable-next-line @elsikora/javascript/no-console
console.log(chalk.dim(bottom));
}
/**
* Displays a single select prompt to the user.
* @param message - The message to display to the user
* @param options - Array of options to select from
* @param initialValue - Initial selected value
* @returns Promise that resolves to 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 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 message - Optional message to display when the spinner stops
*/
stopSpinner(message) {
this.spinner.stop();
if (message) {
// eslint-disable-next-line @elsikora/javascript/no-console
console.log(message);
}
}
/**
* Displays a success message to the user.
* @param message - The success message to display
*/
success(message) {
// eslint-disable-next-line @elsikora/javascript/no-console
console.log(chalk.green(message));
}
/**
* Displays a text input prompt to the user.
* @param message - The message to display to the user
* @param _placeholder
* @param initialValue - Optional initial value for the input field
* @param validate - Optional validation function for the input
* @returns 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);
}
}
/**
* Displays a warning message to the user.
* @param message - The warning message to display
*/
warn(message) {
console.warn(chalk.yellow(message));
}
}
export { PromptsCliInterface };
//# sourceMappingURL=prompts-cli-interface.service.js.map