IO.js

const readline = require("readline");
const exec = require("child-process-promise").exec;
const chalk = require("chalk");

/**
 * Utilities for clean log output and user input.
*/
class IO {

  constructor() {
    this.yesRegex = /^([yY][eE][sS]|[yY])+$/;
    this.isCI = typeof process.env.CI !== "undefined";
    this.statusFormat = chalk.magenta;
    this.questionFormat = chalk.blue;
    this.ciAnswerFormat = chalk.yellow;
    this.formatDefault = chalk.bold;
  }

  /**
   * Provides a section header to mark a section of script running.
   * @param {String} msg the message to print.
   * @return {void}
  */
  status(msg) {
    console.log(`
      ${this.statusFormat(msg)}
    `);
  }

  /**
   * Rings an auditory bell when prompting for input.
   * @return {Promise} resolves when the sound is finished playing, or immediately if bell is disabled.
  */
  bell() {
    if(process.env.CI || process.env.NO_BELL) {
      return Promise.resolve();
    }
    return exec("paplay /usr/share/sounds/ubuntu/notifications/Xylo.ogg");
  }

  /**
   * Queries the user for a "yes/no" question.  If in CI, uses a default, and creates fake console output to simulate
   * asking the question to the user and getting a response, to allow the user to follow along.
   * @param {String} question the text of the question to ask the user.
   * @param {String} consoleDefault the default input if the user doesn't provide input.  Use `"Y"` or `"N"`.
   * @param {String} ciDefault the default input to use in CI.  Use `"Y"` or `"N"`.
   * @return {Promise<Boolean>} `true` if the user answered "yes", `false` if the user answered "no".
   * @example <caption>Basic Usage</caption>
   * const io = new IO();
   * io
   *    .yesNo("Use current directory, instead of '/var/my-app/'?", "Y", "N")
   *    .then(answer => console.log(answer ? "Using current directory." : "Using '/var/my-app/'."));
  */
  yesNo(question, consoleDefault, ciDefault) {
    if(this.isCI) {
      const {y, n} = this.formatYesNo(ciDefault);
      console.log(this.questionFormat(`${question} (${y}/${n}) `) + this.ciAnswerFormat(ciDefault.toUpperCase()));
      return Promise.resolve(this.yesRegex.test(ciDefault));
    }
    else {
      this.bell();
      const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
      const askQuestion = (text) => new Promise(resolve => rl.question(text, resolve));
      const {y, n} = this.formatYesNo(consoleDefault);
      return askQuestion(this.questionFormat(`${question} (${y}/${n}) `))
        .then(answer => {
          rl.close();
          answer = (answer || "").trim();
          return this.yesRegex.test(answer.length > 0 ? answer : consoleDefault);
        });
    }
  }

  /**
   * Provides the format for the "Y/N" prompt.
   * @param {String} defaultVal the default value for the question.
   * @return {Object} `{ y, n }` containing formatted strings for the `Y` and `N` parts of the default input.
   * @private
  */
  formatYesNo(defaultVal) {
    return {
      y: this.yesRegex.test(defaultVal) ? this.formatDefault("Y") : "y",
      n: this.yesRegex.test(defaultVal) ? "n" : this.formatDefault("N"),
    };
  }

}

module.exports = IO;