import { styleText } from 'node:util';
import yoctoSpinner from 'yocto-spinner';
import * as inquirer from '@inquirer/prompts';
import { warning, error, info, success } from './figures.js';
const SPINNER_STATUS = {
SUCCESS: 'success',
FAILED: 'failed',
WARN: 'warn',
INFO: 'info'
};
const { SUCCESS, FAILED, WARN, INFO } = SPINNER_STATUS;
const QUESTION_TYPE = {
INPUT: 'input',
NUMBER: 'number',
CONFIRM: 'confirm'
};
const formatter = new Intl.ListFormat('en', { type: 'disjunction' });
function head(text, length = 11) {
return styleText('bold', text.padEnd(length));
}
export default class CLI {
constructor(stream, options = {}) {
const spinnerOptions = options.spinner ?? {};
this.stream = stream || process.stderr;
this.spinner = yoctoSpinner({ stream: this.stream, ...spinnerOptions });
this.SPINNER_STATUS = SPINNER_STATUS;
this.QUESTION_TYPE = QUESTION_TYPE;
this.figureIndent = ' ';
this.assumeYes = false;
}
get eolIndent() {
return `\n${this.figureIndent}`;
}
setFigureIndent(indent) {
this.figureIndent = ' '.repeat(indent);
}
async prompt(question, opts = {
defaultAnswer: true,
noSeparator: false,
questionType: 'confirm'
}) {
if (!opts.noSeparator) {
this.separator();
}
const questionType = opts.questionType || QUESTION_TYPE.CONFIRM;
const availableTypes = Object.values(QUESTION_TYPE);
if (!availableTypes.includes(questionType)) {
throw new Error(
`${questionType} must be one of ${formatter.format(availableTypes)}`);
}
const defaultAnswer = (opts.defaultAnswer === undefined) ||
opts.defaultAnswer;
if (typeof defaultAnswer === 'boolean' &&
questionType !== QUESTION_TYPE.CONFIRM) {
throw new Error(
'defaultAnswer must be provided for non-confirmation prompts');
}
const { isSpinning, text: spinningMessage } = this.spinner;
if (isSpinning) {
this.spinner.stop();
}
if (this.assumeYes) {
if (isSpinning) {
this.spinner.start(spinningMessage);
}
return defaultAnswer;
}
// eslint-disable-next-line import/namespace -- Plugin can't validate computed reference here
const answer = await inquirer[questionType]({
message: question,
default: defaultAnswer
});
if (isSpinning) {
this.spinner.start(spinningMessage);
}
return answer;
}
async promptRadio(message, choices, opts = undefined) {
if (Object.hasOwn(opts, 'defaultAnswer')) {
opts.default = opts.defaultAnswer;
}
return inquirer.select({
...opts,
message, choices,
});
}
async promptCheckbox(message, choices) {
if (this.assumeYes) {
return choices.filter((c) => c.checked).map((c) => c.value);
}
return inquirer.checkbox({ message, choices });
}
async promptSelect(message, choices, opts = {}) {
const defaultAnswer = opts.defaultAnswer ?? choices[0]?.value;
if (this.assumeYes) {
return defaultAnswer;
}
return inquirer.select({
message,
choices,
default: defaultAnswer
});
}
setAssumeYes() {
this.assumeYes = true;
}
startSpinner(text) {
this.spinner.text = text;
this.spinner.start();
}
updateSpinner(text) {
this.spinner.text = text;
}
stopSpinner(rawText, status = SUCCESS) {
let symbol;
switch (status) {
case SUCCESS:
symbol = success;
break;
case FAILED:
symbol = error;
break;
case WARN:
symbol = warning;
break;
case INFO:
symbol = info;
}
const text = `${symbol} ${rawText}`;
if (this.spinner.isSpinning) {
this.spinner.stop(text);
} else {
// A nested operation may already have stopped the shared spinner.
this.log(text);
}
}
write(text) {
this.stream.write(text);
}
log(text) {
this.write(text + '\n');
}
table(first, second = '', length = 11) {
this.log(head(first, length) + second);
}
separator(text = '', length = 80, sep = '-') {
if (!text) {
this.log(sep.repeat(length));
return;
}
const rest = (length - text.length - 2);
const half = sep.repeat(Math.abs(Math.floor(rest / 2)));
if (rest % 2 === 0) {
this.log(`${half} ${styleText('bold', text)} ${half}`);
} else {
this.log(`${half} ${styleText('bold', text)} ${sep}${half}`);
}
}
ok(text, options = {}) {
const prefix = options.newline ? this.eolIndent : this.figureIndent;
this.log(`${prefix}${success} ${text}`);
}
warn(text, options = {}) {
const prefix = options.newline ? this.eolIndent : this.figureIndent;
this.log(prefix + styleText('bold', `${warning} ${text}`));
}
info(text, options = {}) {
const prefix = options.newline ? this.eolIndent : this.figureIndent;
this.log(`${prefix}${info} ${text}`);
}
error(obj, options = {}) {
const prefix = options.newline ? this.eolIndent : this.figureIndent;
if (obj instanceof Error) {
this.log(`${prefix}${error} ${obj.message}`);
this.log(obj.stack);
if (obj.data) {
this.log(JSON.stringify(obj.data, null, 2));
}
} else {
this.log(prefix + styleText('bold', `${error} ${obj}`));
}
}
setExitCode(statusCode) {
process.exitCode = statusCode;
}
}
CLI.SPINNER_STATUS = SPINNER_STATUS;
CLI.QUESTION_TYPE = QUESTION_TYPE;