@ 261105.4
current
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;