meteor/meteor

View on GitHub
tools/console/console.js

Summary

Maintainability
F
5 days
Test Coverage
///  This class provides a set of utility functions for printing to the terminal
///  in the Meteor tool.
///
///  When you intend for your messages to be read by humans, you should use the
///  following functions to print to the terminal. They will automatically line
///  wrap output to either the width of the terminal, or 80 characters. They
///  will also end in a new line.
///
////   - Console.info : Print to stdout.
///    - Console.error: Print to stderr.
///    - Console.warn: Prints to stderr, if warnings are enabled.
///    - Console.debug: Prints to stdout, if debug is enabled.
///
/// Sometimes, there is a phrase that shouldn't be split up over multiple
/// lines (for example, 'meteor update'). When applicable, please use the
/// following functions (Some of them add additional formatting, especially when
/// pretty-print is turned on):
///
///    - Console.command: things to enter on the command-line, such as
///      'meteor update' or 'cd foobar'.
///    - Console.url: URLs, such as 'www.meteor.com'
///    - Console.path: filepaths outside of Console.command.
///    - Console.noWrap: anything else that you don't want line-wrapped.
///
/// Here is a contrived example:
///   Console.info(
///     "For more information, please run", Console.command("meteor show"),
///     "or check out the new releases at", Console.url("www.meteor.com"),
///     "or look at", Console.path(filepath), ". You are currently running",
///     "Console.noWrap("Meteor version 1.5") + ".");
///
/// The Console.info/Console.error/Console.warn/Console.debug functions also
/// take in Console.options, as a last (optional) argument. These allow you to
/// set an indent or use a bulletpoint. You can check out their API below. If
/// possible, you might also want to use one of the existing wrapper functions,
/// such as Console.labelWarning or Console.arrowInfo.
///
/// Output intended for machines (or pre-formatted in specific ways) should NOT
/// be line-wrapped. Do not wrap these things: JSON output, error stack traces,
/// logs from other programs, etc. For those, you should use the 'raw'
/// version of the API:
///
///    - Console.rawInfo: Like Console.info, but without formatting.
///    - Console.rawError: Like Console.error, but without formatting.
///    - Console.rawWarn: Like Console.warn, but without formatting.
///    - Console.rawDebug: Like Console.debug, but without formatting.
///
/// DO NOT use Console.command/Console.url/Console.path with the raw functions!
/// (They will change your output in ways that you probably do not want). These
/// don't auto-linewrap, end in a newline, or take in Console.options.
///
/// Here is are some examples:
///     Console.rawInfo(JSON.stringify(myData, null, 2));
///     Console.rawError(err.stack + "\n");
///
/// In addition to printing functions, the Console class provides progress bar
/// support, that is mostly handled through buildmessage.js.
import { createInterface } from "readline";
import { format as utilFormat }  from "util";
import { getRootProgress } from "../utils/buildmessage.js";
import chalk from "chalk";
import { onExit as cleanupOnExit } from "../tool-env/cleanup.js";
import wordwrap from "wordwrap";
import {
  isEmacs,
  sleepMs,
  Throttled,
  ThrottledYield,
} from "../utils/utils.js";

const PROGRESS_DEBUG = !!process.env.METEOR_PROGRESS_DEBUG;
// Set the default CR to \r unless we're running with cmd
const CARRIAGE_RETURN = process.platform === 'win32' &&
      process.stdout.isTTY &&
      process.argv[1].toLowerCase().includes('cmd') ? new Array(249).join('\b') : '\r';

const FORCE_PRETTY = process.env.METEOR_PRETTY_OUTPUT &&
  process.env.METEOR_PRETTY_OUTPUT != '0';

const STATUS_MAX_LENGTH = 40;

const PROGRESS_MAX_WIDTH = 40;
const PROGRESS_BAR_FORMAT = '[:bar] :percent :etas';
const TEMP_STATUS_LENGTH = STATUS_MAX_LENGTH + 12;

const STATUS_INTERVAL_MS = 50;
const PROGRESS_THROTTLE_MS = 300;

// Message to show when we don't know what we're doing
// XXX: ? FALLBACK_STATUS = 'Pondering';
const FALLBACK_STATUS = '';

// If there is a part of the larger text, and we really want to make sure that
// it doesn't get split up, we will replace the space with a utf character that
// we are not likely to use anywhere else. This one looks like the a BLACK SUN
// WITH RAYS. We intentionally want to NOT use a space-like character: it should
// be obvious that something has gone wrong if this ever gets printed.
const SPACE_REPLACEMENT = '\u2600';
// In Javascript, replace only replaces the first occurrence and this is the
// proposed alternative.
const replaceAll = (str, search, replace) => str.split(search).join(replace);

let spacesArray = new Array(200).join(' ');
const spacesString = (length) => {
  if (length > spacesArray.length) {
    spacesArray = new Array(length * 2).join(' ');
  }
  return spacesArray.substring(0, length);
};
const ARROW = "=> ";


const toFixedLength = (text, length) => {
  text = text || "";

  // pad or truncate `text` to length
  var pad = length - text.length;
  if (pad < 0) {
    // Truncate
    text = text.substring(0, length - 3) + "...";
  } else if (pad > 0) {
    // Pad
    text = text + spacesString(pad);
  }
  return text;
};

// No-op progress display, that means we don't have to handle the 'no progress
// display' case
class ProgressDisplayNone {
  depaint() {
    // No-op
  }

  repaint() {
    // No-op
  }
}

// Status display only, primarily for use with emacs
// No fancy terminal support available, but we have a TTY.
// Print messages that will be overwritten because they
// end in `\r`.
// Status message mode is where we see status messages but not the
// fancy progress bar.  It's used when we detect a "pseudo-TTY"
// of the type used by Emacs, and possibly SSH.
//
// XXX DELETE THIS MODE since the progress bar now uses "\r".
// But first we have to throttle progress bar updates so that
// Emacs doesn't get overwhelemd (we should throttle them anyway).
// There's also a bug when using the progress bar in Emacs where
// the cursor doesn't seem to return to column 0.
class ProgressDisplayStatus {
  constructor(console) {
    this._console = console;
    this._stream = console._stream;

    this._status = null;
    this._wroteStatusMessage = false;
  }

  depaint() {
    // For the non-progress-bar status mode, we may need to
    // clear some characters that we printed with a trailing `\r`.
    if (this._wroteStatusMessage) {
      var spaces = spacesString(TEMP_STATUS_LENGTH + 1);
      this._stream.write(spaces + CARRIAGE_RETURN);
      this._wroteStatusMessage = false;
    }
  }

  repaint() {
    // We don't repaint after a log message (is that right?)
  }

  updateStatus(status) {
    if (status == this._status) {
      return;
    }

    this._status = status;
    this._render();
  }

  _render() {
    var text = this._status;
    if (text) {
      text = toFixedLength(text, STATUS_MAX_LENGTH);
    }

    if (text) {
      // the number of characters besides `text` here must
      // be accounted for in TEMP_STATUS_LENGTH.
      this._stream.write('  (  ' + text + '  ... )' + CARRIAGE_RETURN);
      this._wroteStatusMessage = true;
    }
  }
}

class SpinnerRenderer {
  constructor() {
    this.frames = ['-', '\\', '|', '/'];
    this.start = +(new Date);
    this.interval = 250;
    //// I looked at some Unicode indeterminate progress indicators, such as:
    ////
    //// spinner = "▁▃▄▅▆▇▆▅▄▃".split('');
    //// spinner = "▉▊▋▌▍▎▏▎▍▌▋▊▉".split('');
    //// spinner = "▏▎▍▌▋▊▉▊▋▌▍▎▏▁▃▄▅▆▇▆▅▄▃".split('');
    //// spinner = "▉▊▋▌▍▎▏▎▍▌▋▊▉▇▆▅▄▃▁▃▄▅▆▇".split('');
    //// spinner = "⠉⠒⠤⣀⠤⠒".split('');
    ////
    //// but none of them really seemed like an improvement. I think
    //// the case for using unicode would be stronger in a determinate
    //// progress indicator.
    ////
    //// There are also some four-frame options such as ◐◓◑◒ at
    ////   http://stackoverflow.com/a/2685827/157965
    //// but all of the ones I tried look terrible in the terminal.
  }

  asString() {
    var now = +(new Date);

    var t = now - this.start;
    var frame = Math.floor(t / this.interval) % this.frames.length;
    return this.frames[frame];
  }
}

// Renders a progressbar.  Based on the npm 'progress' module, but tailored to our needs (i.e. renders to string)
class ProgressBarRenderer {
  constructor(format, options) {
    options = options || Object.create(null);

    this.fmt = format;
    this.curr = 0;
    this.total = 100;
    this.maxWidth = options.maxWidth || this.total;
    this.chars = {
      complete   : '=',
      incomplete : ' '
    };
  }

  asString(availableSpace) {
    var ratio = this.curr / this.total;
    ratio = Math.min(Math.max(ratio, 0), 1);

    var percent = ratio * 100;
    var incomplete, complete, completeLength;
    var elapsed = new Date - this.start;
    var eta = (percent == 100) ? 0 : elapsed * (this.total / this.curr - 1);

    /* populate the bar template with percentages and timestamps */
    var str = this.fmt
      .replace(':current', this.curr)
      .replace(':total', this.total)
      .replace(':elapsed', isNaN(elapsed) ? '0.0' : (elapsed / 1000).toFixed(1))
      .replace(':eta', (isNaN(eta) || ! isFinite(eta)) ? '0.0' : (eta / 1000).toFixed(1))
      .replace(':percent', percent.toFixed(0) + '%');

    /* compute the available space (non-zero) for the bar */
    var width = Math.min(this.maxWidth, availableSpace - str.replace(':bar', '').length);

    /* NOTE: the following assumes the user has one ':bar' token */
    completeLength = Math.round(width * ratio);
    complete = Array(completeLength + 1).join(this.chars.complete);
    incomplete = Array(width - completeLength + 1).join(this.chars.incomplete);

    /* fill in the actual progress bar */
    str = str.replace(':bar', complete + incomplete);

    return str;
  }
}


class ProgressDisplayFull {
  constructor(console) {
    this._console = console;
    this._stream = console._stream;

    this._status = '';

    var options = {
      complete: '=',
      incomplete: ' ',
      maxWidth: PROGRESS_MAX_WIDTH,
      total: 100
    };
    this._progressBarRenderer = new ProgressBarRenderer(PROGRESS_BAR_FORMAT, options);
    this._progressBarRenderer.start = new Date();

    this._headless = !! (
      process.env.METEOR_HEADLESS &&
      JSON.parse(process.env.METEOR_HEADLESS)
    );

    this._spinnerRenderer = new SpinnerRenderer();

    this._fraction = undefined;

    this._printedLength = 0;

    this._lastWrittenLine = null;
    this._lastWrittenTime = 0;
    this._renderTimeout = null;
  }

  depaint() {
    this._clearDelayedRender();
    this._stream.write(spacesString(this._printedLength) + CARRIAGE_RETURN);
  }

  updateStatus(status) {
    if (status == this._status) {
      return;
    }

    this._status = status;
    this._render();
  }

  updateProgress(fraction, startTime) {
    this._fraction = fraction;
    if (fraction !== undefined) {
      this._progressBarRenderer.curr = Math.floor(fraction * this._progressBarRenderer.total);
    }
    if (startTime) {
      this._progressBarRenderer.start = startTime;
    }

    if (!this._rerenderTimeout && this._lastWrittenTime) {
      this._rerenderTimeout = setTimeout(() => {
        this._rerenderTimeout = null;
        this._render()
      }, PROGRESS_THROTTLE_MS);
    } else if (this._lastWrittenTime === 0) {
      this._render();
    }
  }

  repaint() {
    this._render();
  }

  setHeadless(headless) {
    this._headless = !! headless;
  }

  _clearDelayedRender() {
    if (this._rerenderTimeout) {
      clearTimeout(this._rerenderTimeout);
      this._rerenderTimeout = null;
    }
  }

  _render() {
    if (this._rerenderTimeout) {
      this._clearDelayedRender();
    }

    // XXX: Or maybe just jump to the correct position?
    var progressGraphic = '';

    // The cursor appears in position 0; we indent it a little to avoid this
    // This also means it appears less important, which is good
    var indentColumns = 3;

    var streamColumns = this._console.width();
    var statusColumns;
    var progressColumns;
    if (! streamColumns) {
      statusColumns = STATUS_MAX_LENGTH;
      progressColumns = 0;
    } else {
      statusColumns = Math.min(STATUS_MAX_LENGTH, streamColumns - indentColumns);
      progressColumns = Math.min(PROGRESS_MAX_WIDTH, streamColumns - indentColumns - statusColumns);
    }

    if (this._fraction !== undefined && progressColumns > 16) {
      // 16 is a heuristic number that allows enough space for a meaningful progress bar
      progressGraphic = "  " + this._progressBarRenderer.asString(progressColumns - 2);

    } else if (! this._headless && progressColumns > 3) {
      // 3 = 2 spaces + 1 spinner character
      progressGraphic = "  " + this._spinnerRenderer.asString();

    } else if (new Date - this._lastWrittenTime > 5 * 60 * 1000) {
      // Print something every five minutes, to avoid test timeouts.
      progressGraphic = "  [ProgressDisplayFull keepalive]";
      this._lastWrittenLine = null; // Force printing.
    }

    if (this._status || progressGraphic) {
      // XXX: Just update the graphic, to avoid text flicker?

      var line = spacesString(indentColumns);
      var length = indentColumns;

      if (this._status) {
        var fixedLength = toFixedLength(this._status, statusColumns);
        line += chalk.bold(fixedLength);
        length += statusColumns;
      } else {
        line += spacesString(statusColumns);
        length += statusColumns;
      }

      line += progressGraphic + CARRIAGE_RETURN;
      length += progressGraphic.length;

      if (this._headless &&
          line === this._lastWrittenLine) {
        // Don't write the exact same line twice in a row.
        return;
      }

      this.depaint();

      this._stream.write(line);
      this._lastWrittenLine = line;
      this._lastWrittenTime = +new Date;
      this._printedLength = length;
    }
  }
}

class StatusPoller {
  constructor(console) {
    // The current progress we are watching
    this._watching = null;

    this._console = console;

    this._pollPromise = null;
    this._throttledStatusPoll = new Throttled({
      interval: STATUS_INTERVAL_MS
    });
    this._startPoller();
    this._stop = false;
  }

  _startPoller() {
    if (this._pollPromise) {
      throw new Error("Already started");
    }

    this._pollPromise = (async() => {
      sleepMs(STATUS_INTERVAL_MS);
      while (! this._stop) {
        this.statusPoll();
        sleepMs(STATUS_INTERVAL_MS);
      }
    })();
  }

  stop() {
    this._stop = true;
  }

  statusPoll() {
    if (this._throttledStatusPoll.isAllowed()) {
      this._statusPoll();
    }
  }

  _statusPoll() {
    // XXX: Early exit here if we're not showing status at all?

    var rootProgress = getRootProgress();
    if (PROGRESS_DEBUG) {
      // It can be handy for dev purposes to see all the executing tasks
      rootProgress.dump(process.stdout, {skipDone: true});
    }

    const reportState = (state, startTime) => {
      var progressDisplay = this._console._progressDisplay;
      // Do the % computation, if it is going to be used
      if (progressDisplay.updateProgress) {
        if (state.end === undefined || state.end == 0) {
          progressDisplay.updateProgress(undefined, startTime);
        } else {
          var fraction = state.done ? 1.0 : (state.current / state.end);

          if (! isNaN(fraction) && fraction >= 0) {
            progressDisplay.updateProgress(fraction, startTime);
          } else {
            progressDisplay.updateProgress(0, startTime);
          }
        }
      }
    };

    var watching = (rootProgress ? rootProgress.getCurrentProgress() : null);

    if (this._watching === watching) {
      // We need to do this to keep the spinner spinning
      // XXX: Should we _only_ do this when we're showing the spinner?
      reportState(watching.getState(), watching.startTime);
      return;
    }

    this._watching = watching;

    var title = (watching != null ? watching.title : null) || FALLBACK_STATUS;

    var progressDisplay = this._console._progressDisplay;
    progressDisplay.updateStatus && progressDisplay.updateStatus(title);

    if (watching) {
      watching.addWatcher((state) => {
        if (watching != this._watching) {
          // No longer active
          // XXX: De-register with watching? (we don't bother right now because dead tasks tell no status)
          return;
        }

        reportState(state, watching.startTime);
      });
    }
  }
}

// We use a special class to represent the options that we send to the Console
// because it allows us to call 'instance of' on the last argument of variadic
// functions. This allows us to keep the signature of our custom output
// functions (ex: info) roughly the same as the originals.
class ConsoleOptions {
  constructor(o) {
    this.options = o;
  }
}

const LEVEL_CODE_ERROR = 4;
const LEVEL_CODE_WARN = 3;
const LEVEL_CODE_INFO = 2;
const LEVEL_CODE_DEBUG = 1;

export const LEVEL_ERROR = { code: LEVEL_CODE_ERROR };
export const LEVEL_WARN = { code: LEVEL_CODE_WARN };
export const LEVEL_INFO = { code: LEVEL_CODE_INFO };
export const LEVEL_DEBUG = { code: LEVEL_CODE_DEBUG };

// This base class is just here to preserve some of the "static properties"
// which were being set on the `Console.prototype` prior to this being a
// `class`.  In the future, if static properties eventually work their way
// into the language, this can be moved into the `Console` class.
class ConsoleBase {}
Object.assign(ConsoleBase.prototype, {
  // Log levels
  LEVEL_ERROR,
  LEVEL_WARN,
  LEVEL_INFO,
  LEVEL_DEBUG,

  // Other Console constants.
  CARRIAGE_RETURN,
});

class Console extends ConsoleBase {
  constructor(options) {
    super();

    options = options || Object.create(null);

    this._headless = !! (
      process.env.METEOR_HEADLESS &&
      JSON.parse(process.env.METEOR_HEADLESS)
    );

    // The progress display we are showing on-screen
    this._progressDisplay = new ProgressDisplayNone(this);

    this._statusPoller = null;

    this._throttledYield = new ThrottledYield();

    this.verbose = false;
    this._simpleDebug = false;

    // Legacy helpers
    this.stdout = Object.create(null);
    this.stderr = Object.create(null);

    this._stream = process.stdout;

    this._pretty = (FORCE_PRETTY !== undefined ? FORCE_PRETTY : false);
    this._progressDisplayEnabled = false;

    this._logThreshold = LEVEL_CODE_INFO;
    var logspec = process.env.METEOR_LOG;

    if (logspec) {
      logspec = logspec.trim().toLowerCase();
      if (logspec === 'debug') {
        this._logThreshold = LEVEL_CODE_DEBUG;
      }
    }

    if (process.env.METEOR_SIMPLE_DEBUG) {
      this._simpleDebug = true;
    }

    cleanupOnExit((sig) => {
      this.enableProgressDisplay(false);
    });
  }

  setPretty(pretty) {
    // If we're being forced, do nothing.
    if (FORCE_PRETTY !== undefined) {
      return;
    }
    // If no change, do nothing.
    if (this._pretty === pretty) {
      return;
    }
    this._pretty = pretty;
    this._updateProgressDisplay();
  }

  // Runs f with the progress display visible (ie, with progress display enabled
  // and pretty). Resets both flags to their original values after f runs.
  withProgressDisplayVisible(f) {
    var originalPretty = this._pretty;
    var originalProgressDisplayEnabled = this._progressDisplayEnabled;

    // Turn both flags on.
    this._pretty = this._progressDisplayEnabled = true;

    // Update the screen if anything changed.
    if (! originalPretty || ! originalProgressDisplayEnabled) {
      this._updateProgressDisplay();
    }

    try {
      return f();
    } finally {
      // Reset the flags.
      this._pretty = originalPretty;
      this._progressDisplayEnabled = originalProgressDisplayEnabled;
      // Update the screen if anything changed.
      if (! originalPretty || ! originalProgressDisplayEnabled) {
        this._updateProgressDisplay();
      }
    }
  }

  setVerbose(verbose) {
    this.verbose = verbose;
  }

  // Get the current width of the Console.
  width() {
    var width = 80;
    var stream = process.stdout;
    if (stream && stream.isTTY && stream.columns) {
      width = stream.columns;
    }

    // On Windows cmd.exe splits long lines into smaller chunks by inserting the
    // '\r\n' symbols into the stream, this is what cmd.exe does instead of
    // reflowing the text. We cannot control it. For some unknown reason, even
    // when the output line is less than number of columns (usually 80), cmd.exe
    // would still insert new-line chars. These chars break our repainting that
    // relies on the previous chars to be erasable with '\b' (end-line chars
    // can't be erased this way). This is why we report a smaller number than it
    // is in reality, for safety.
    if (process.platform === 'win32') {
      width -= 5;
    }

    return width;
  }

  // This can be called during long lived operations; it will keep the spinner spinning.
  // (This code used to be in Patience.nudge)
  //
  // It's frustrating when you write code that takes a while, either because it
  // uses a lot of CPU or because it uses a lot of network/IO.  In Node,
  // consuming lots of CPU without yielding is especially bad.
  // Other IO/network tasks will stall, and you can't even kill the process!
  //
  // Within any code that may burn CPU for too long, call `Console.nudge()`.
  // If it's been a while since your last yield, your Fiber will sleep momentarily.
  // It will also update the spinner if there is one and it's been a while.
  // The caller should be OK with yielding --- it has to be in a Fiber and it can't be
  // anything that depends for correctness on not yielding.  You can also call nudge(false)
  // if you just want to update the spinner and not yield, but you should avoid this.
  nudge(canYield) {
    if (this._statusPoller) {
      this._statusPoller.statusPoll();
    }
    if (canYield === undefined || canYield === true) {
      this._throttledYield.yield();
    }
  }

  // Initializes and returns a new ConsoleOptions object. Takes in the following
  // Console options to pass to _wrapText eventually.
  //
  //   - bulletPoint: start the first line with a given string, then offset the
  //     subsequent lines by the length of that string. For example, if the
  //     bulletpoint is " => ", we would get:
  //     " => some long message starts here
  //          and then continues here."
  //   - indent: offset the entire string by a specific number of
  //     characters. For example:
  //     "  This entire message is indented
  //        by two characters."
  //
  // Passing in both options will offset the bulletPoint by the indentation,
  // like so:
  //  "  this message is indented by two."
  //  "  => this message indented by two and
  //        and also starts with an arrow."
  //
  options(o) {
    // (This design pattern allows us to call 'instance of' on the
    // ConsoleOptions in parseVariadicInput, by ensuring that the object created
    // with Console.options is, in fact, a new object.
    return new ConsoleOptions(o);
  }

  // Deal with the arguments to a variadic print function that also takes an
  // optional ConsoleOptions argument at the end.
  //
  // Returns an object with keys:
  //  - options: The options that were passed in, or an empty object.
  //  - message: Arguments to the original function, parsed as a string.
  //
  _parseVariadicInput(args) {
    var msgArgs;
    var options;
    // If the last argument is an instance of ConsoleOptions, then we should
    // separate it out, and only send the first N-1 arguments to be parsed as a
    // message.
    const lastArg = args && args.length && args[args.length - 1];
    if (lastArg instanceof ConsoleOptions) {
      msgArgs = args.slice(0, -1);
      options = lastArg.options;
    } else {
      msgArgs = args;
      options = Object.create(null);
    }
    var message = this._format(msgArgs);
    return { message: message, options: options };
  }

  isLevelEnabled(levelCode) {
    return (this.verbose || this._logThreshold <= levelCode);
  }

  isDebugEnabled() {
    return this.isLevelEnabled(LEVEL_CODE_DEBUG);
  }


  // Don't pretty-fy this output by trying to, for example, line-wrap it. Just
  // print it to the screen as it is.
  rawDebug(...args) {
    if (! this.isDebugEnabled()) {
      return;
    }

    var message = this._format(args);
    this._print(LEVEL_DEBUG, message);
  }

  // Don't use console and so it does not affect tests.
  // like this.fullBuffer from matcher.
  simpleDebug(...args) {
    if (! this._simpleDebug) {
      return;
    }

    var message = this._format(args);
    process.stdout.write( '\n' + message + '\n');
  }

  // By default, Console.debug automatically line wraps the output.
  //
  // Takes in an optional Console.options({}) argument at the end, with the
  // following keys:
  //   - bulletPoint: start the first line with a given string, then offset the
  //     subsequent lines by the length of that string. See _wrap for more details.
  //   - indent: offset the entire string by a specific number of
  //     characters. See _wrap for more details.
  //
  debug(...args) {
    if (! this.isDebugEnabled()) { return; }

    var message = this._prettifyMessage(args);
    this._print(LEVEL_DEBUG, message);
  }

  isInfoEnabled() {
    return this.isLevelEnabled(LEVEL_CODE_INFO);
  }

  // Don't pretty-fy this output by trying to, for example, line-wrap it. Just
  // print it to the screen as it is.
  rawInfo(...args) {
    if (! this.isInfoEnabled()) {
      return;
    }

    var message = this._format(args);
    this._print(LEVEL_INFO, message);
  }

  // Generally, we want to process the output for legibility, for example, by
  // wrapping it. For raw output (ex: stack traces, user logs, etc), use the
  // rawInfo function. For more information about options, see: debug.
  info(...args) {
    if (! this.isInfoEnabled()) { return; }

    var message = this._prettifyMessage(args);
    this._print(LEVEL_INFO, message);
  }

  isWarnEnabled() {
    return this.isLevelEnabled(LEVEL_CODE_WARN);
  }

  rawWarn(...args) {
    if (! this.isWarnEnabled()) {
      return;
    }

    var message = this._format(args);
    this._print(LEVEL_WARN, message);
  }

  // Generally, we want to process the output for legibility, for example, by
  // wrapping it. For raw output (ex: stack traces, user logs, etc), use the
  // rawWarn function. For more information about options, see: debug.
  warn(...args) {
    if (! this.isWarnEnabled()) { return; }

    var message = this._prettifyMessage(args);
    this._print(LEVEL_WARN, message);
  }

  rawError(...args) {
    var message = this._format(args);
    this._print(LEVEL_ERROR, message);
  }

  // Generally, we want to process the output for legibility, for example, by
  // wrapping it. For raw output (ex: stack traces, user logs, etc), use the
  // rawError function. For more information about options, see: debug.
  error(...args) {
    var message = this._prettifyMessage(args);
    this._print(LEVEL_ERROR, message);
  }

  // Prints a special ANSI sequence that "clears" the screen (on most terminal
  // emulators just scrolls the contents down and resets the position).
  // References: http://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes
  clear() {
    this.rawInfo('\u001b[2J\u001b[0;0H');
  }

  _prettifyMessage(msgArguments) {
    var parsedArgs = this._parseVariadicInput(msgArguments);
    var wrapOpts = {
      indent: parsedArgs.options.indent,
      bulletPoint: parsedArgs.options.bulletPoint
    };

    var wrappedMessage = this._wrapText(parsedArgs.message, wrapOpts);
    wrappedMessage += "\n";
    return wrappedMessage;
  }

  _print(level, message) {
    // We need to hide the progress bar/spinner before printing the message
    var progressDisplay = this._progressDisplay;
    progressDisplay.depaint();

    // stdout/stderr is determined by the log level
    // XXX: We should probably just implement Loggers with observers
    var dest = process.stdout;
    if (level) {
      switch (level.code) {
        case LEVEL_CODE_ERROR:
          dest = process.stderr;
          break;
        case LEVEL_CODE_WARN:
          dest = process.stderr;
          break;
      }
    }

    // Pick the color/weight if in pretty mode
    var style = null;
    if (level && this._pretty) {
      switch (level.code) {
        case LEVEL_CODE_ERROR:
          style = chalk.bold.red;
          break;
        case LEVEL_CODE_WARN:
          style = chalk.red;
          break;
      }
    }

    if (style) {
      dest.write(style(message));
    } else {
      dest.write(message);
    }

    // XXX: Pause before showing the progress display, to prevent
    // flicker/spewing messages
    // Repaint the progress display
    progressDisplay.repaint();
  }

  // A wrapper around Console.info. Prints the message out in green (if pretty),
  // with the CHECKMARK as the bullet point in front of it.
  success(message, uglySuccessKeyword = "success") {
    var checkmark;

    if (! this._pretty) {
      return this.info(`${message}: ${uglySuccessKeyword}`);
    }

    if (process.platform === "win32") {
      checkmark = chalk.green('SUCCESS');
    } else {
      checkmark = chalk.green('\u2713'); // CHECKMARK
    }

    return this.info(
        chalk.green(message),
        this.options({ bulletPoint: checkmark  + " "}));
  }

  // Wrapper around Console.info. Prints the message out in red (if pretty)
  // with the BALLOT X as the bullet point in front of it.
  failInfo(message) {
    return this._fail(message, "info");
  }

  // Wrapper around Console.warn. Prints the message out in red (if pretty)
  // with the ascii x as the bullet point in front of it.
  failWarn(message) {
    return this._fail(message, "warn");
  }

  // Print the message in red (if pretty) with an x bullet point in front of it.
  _fail(message, printFn) {
    if (! this._pretty) {
      return this[printFn](message);
    }

    var xmark = chalk.red('\u2717');
    return this[printFn](
        chalk.red(message),
        this.options({ bulletPoint: xmark + " " }));
  }

  // Wrapper around Console.warn that prints a large "WARNING" label in front.
  labelWarn(message) {
    return this.warn(message, this.options({ bulletPoint: "WARNING: " }));
  }

  // Wrappers around Console functions to prints an "=> " in front. Optional
  // indent to indent the arrow.
  arrowError(message, indent) {
    return this._arrowPrint("error", message, indent);
  }
  arrowWarn(message, indent) {
    return this._arrowPrint("warn", message, indent);
  }
  arrowInfo(message, indent) {
    return this._arrowPrint("info", message, indent);
  }
  _arrowPrint(printFn, message, indent) {
    indent = indent || 0;
    return this[printFn](
      message,
      this.options({ bulletPoint: ARROW, indent: indent }));
  }

  // A wrapper around console.error. Given an error and some background
  // information, print out the correct set of messages depending on verbose
  // level, etc.
  printError(err, info) {
    var message = err.message;
    if (! message) {
      message = "Unexpected error";
      if (this.verbose) {
        message += " (" + err.toString() + ")";
      }
    }

    if (info) {
      message = info + ": " + message;
    }

    this.error(message);
    if (this.verbose && err.stack) {
      this.rawInfo(err.stack + "\n");
    }
  }

  // A wrapper to print out buildmessage errors.
  printMessages(messages) {
    if (messages.hasMessages()) {
      this.error("\n" + messages.formatMessages());
    }
  }

  // Wrap commands in this function -- it ensures that commands don't get line
  // wrapped (ie: print 'meteor' at the end of the line, and 'create --example'
  // at the beginning of the next one).
  //
  // To use, wrap commands that you send into print functions with this
  // function, like so: Console.info(text + Console.command("meteor create
  // --example leaderboard") + moretext).
  //
  // If pretty print is on, this will also bold the commands.
  command(message) {
    var unwrapped = this.noWrap(message);
    return this.bold(unwrapped);
  }

  // Underline the URLs (if pretty print is on).
  url(message) {
    // If we are going to print URLs with spaces, we should turn spaces into
    // things browsers understand.
    var unspaced =
          replaceAll(message, ' ', '%20');
    // There is no need to call noWrap here, since that only handles spaces (and
    // we have done that). If it ever handles things other than spaces, we
    // should make sure to call it here.
    return this.underline(unspaced);
  }

  // Format a filepath to not wrap. This does NOT automatically escape spaces
  // (ie: add a slash in front so the user could copy paste the file path into a
  // terminal).
  path(message) {
    // Make sure that we don't wrap this.
    var unwrapped = this.noWrap(message);
    return this.bold(unwrapped);
  }

  // Do not wrap this substring when you send it into a non-raw print function.
  // DO NOT print the result of this call with a raw function.
  noWrap(message) {
    var noBlanks = replaceAll(message, ' ', SPACE_REPLACEMENT);
    return noBlanks;
  }

  // A wrapper around the underline functionality of chalk.
  underline(message) {
    if (! this._pretty) {
      return message;
    }
    return chalk.underline(message);
  }

  // A wrapper around the bold functionality of chalk.
  bold(message) {
    if (! this._pretty) {
      return message;
    }
    return chalk.bold(message);
  }

  // Prints a two column table in a nice format (The first column is printed
  // entirely, the second only as space permits).
  //  options:
  //      - level: Allows to print to stderr, instead of stdout. Set the print
  //        level with Console.LEVEL_INFO, Console.LEVEL_ERROR, etc.
  //      - ignoreWidth: ignore the width of the terminal, and go over the
  //        character limit instead of trailing off with '...'. Useful for
  //        printing directories, for example.
  //      - indent: indent the entire table by a given number of spaces.
  printTwoColumns(rows, options) {
    options = options || Object.create(null);

    var longest = '';
    rows.forEach(row => {
      var col0 = row[0] || '';
      if (col0.length > longest.length) {
        longest = col0;
      }
    });

    var pad = longest.replace(/./g, ' ');
    var width = this.width();
    var indent =
      options.indent ? Array(options.indent + 1).join(' ') : "";

    var out = '';
    rows.forEach(row => {
      var col0 = row[0] || '';
      var col1 = row[1] || '';
      var line = indent + this.bold(col0) + pad.substr(col0.length);
      line += "  " + col1;
      if (! options.ignoreWidth && line.length > width) {
        line = line.substr(0, width - 3) + '...';
      }
      out += line + "\n";
    });

    var level = options.level || this.LEVEL_INFO;
    out += "\n";
    this._print(level, out);

    return out;
  }

  // Format logs according to the spec in utils.
  _format(logArguments) {
    return utilFormat(...logArguments);
  }

  // Wraps long strings to the length of user's terminal. Inserts linebreaks
  // between words when nearing the end of the line. Returns the wrapped string
  // and takes the following arguments:
  //
  // text: the text to wrap
  // options:
  //   - bulletPoint: (see: Console.options)
  //   - indent: (see: Console.options)
  //
  _wrapText(text, options) {
    options = options || Object.create(null);

    // Compute the maximum offset on the bulk of the message.
    var maxIndent = 0;
    if (options.indent && options.indent > 0) {
      maxIndent = maxIndent + options.indent;
    }
    if (options.bulletPoint) {
      maxIndent = maxIndent + options.bulletPoint.length;
    }

    // Get the maximum width, or if we are not running in a terminal (self-test,
    // for example), default to 80 columns.
    var max = this.width();

    var wrappedText;
    if (process.env.METEOR_NO_WORDWRAP) {
      var indent =
        options.indent ? Array(options.indent + 1).join(' ') : "";
      if (options.bulletPoint) {
        wrappedText = options.bulletPoint + text;
      } else {
        wrappedText = text;
      }
      wrappedText = wrappedText.split('\n').map(s => {
        if (s === "") {
          return "";
        }
        return indent + s;
      }).join('\n');

    } else {
      // Wrap the text using the npm wordwrap library.
      wrappedText = wordwrap(maxIndent, max)(text);

      // Insert the start string, if applicable.
      if (options.bulletPoint) {
        // Save the initial indent level.
        var initIndent = options.indent ?
              wrappedText.substring(0, options.indent) : "";
        // Add together the initial indent (if any), the bullet point and the
        // remainder of the message.
        wrappedText = initIndent + options.bulletPoint +
          wrappedText.substring(maxIndent);
      }
    }

    // If we have previously replaces any spaces, now is the time to bring them
    // back.
    wrappedText = replaceAll(wrappedText, SPACE_REPLACEMENT, ' ');
    return wrappedText;
  }


  // Enables the progress bar, or disables it when called with (false)
  enableProgressDisplay(enabled) {
    // No arg => enable
    if (enabled === undefined) {
      enabled = true;
    }

    if (this._progressDisplayEnabled === enabled) {
      return;
    }

    this._progressDisplayEnabled = enabled;
    this._updateProgressDisplay();
  }

  // In response to a change in setPretty or enableProgressDisplay,
  // configure the appropriate progressDisplay
  _updateProgressDisplay() {
    var newProgressDisplay;

    if (! this._progressDisplayEnabled) {
      newProgressDisplay = new ProgressDisplayNone();
    } else if ((! this._stream.isTTY) || (! this._pretty)) {
      // No progress bar if not in pretty / on TTY.
      newProgressDisplay = new ProgressDisplayNone(this);
    } else if (isEmacs() || this.isPseudoTTY()) {
      // Resort to a more basic mode if we're in an environment which
      // misbehaves when using clearLine() and cursorTo(...).
      newProgressDisplay = new ProgressDisplayStatus(this);
    } else {
      // Otherwise we can do the full progress bar
      newProgressDisplay = new ProgressDisplayFull(this);
    }

    // Start/stop the status poller, so we never block exit
    if (this._progressDisplayEnabled) {
      if (! this._statusPoller) {
        this._statusPoller = new StatusPoller(this);
      }
    } else {
      if (this._statusPoller) {
        this._statusPoller.stop();
        this._statusPoller = null;
      }
    }

    this._setProgressDisplay(newProgressDisplay);
  }

  isPseudoTTY() {
    return this._stream && this._stream.isTTY && ! this._stream.columns;
  }

  isHeadless() {
    return this._headless;
  }

  isInteractive() {
    return ! this._headless;
  }

  setHeadless(headless = true) {
    this._headless = !! headless;

    if (this._progressDisplay &&
        this._progressDisplay.setHeadless) {
      this._progressDisplay.setHeadless(this._headless);
    }
  }

  _setProgressDisplay(newProgressDisplay) {
    // XXX: Optimize case of no-op transitions? (same mode -> same mode)

    var oldProgressDisplay = this._progressDisplay;
    oldProgressDisplay.depaint();

    this._progressDisplay = newProgressDisplay;
  }

  // options:
  //   - echo (boolean): defaults to true
  //   - prompt (string)
  //   - stream: defaults to process.stdout (you might want process.stderr)
  readLine(options) {
    options = Object.assign(Object.create(null), {
      echo: true,
      stream: this._stream
    }, options);

    var silentStream = {
      write: function () {
      },
      on: function () {
      },
      end: function () {
      },
      isTTY: options.stream.isTTY,
      removeListener: function () {
      }
    };

    var previousProgressDisplay = this._progressDisplay;
    this._setProgressDisplay(new ProgressDisplayNone());

    // Read a line, throwing away the echoed characters into our dummy stream.
    var rl = createInterface({
      input: process.stdin,
      output: options.echo ? options.stream : silentStream,
      // `terminal: options.stream.isTTY` is the default, but emacs shell users
      // don't want fancy ANSI.
      terminal: options.stream.isTTY && ! isEmacs()
    });

    if (! options.echo) {
      options.stream.write(options.prompt);
    } else {
      rl.setPrompt(options.prompt);
      rl.prompt();
    }

    return new Promise((resolve) => {
      rl.on('line', line => {
        rl.close();
        if (! options.echo) {
          options.stream.write("\n");
        }
        this._setProgressDisplay(previousProgressDisplay);
        resolve(line);
      });
    }).await();
  }
}

const yellow  =
  (text, ...values) =>
     `\x1b[33m${ String.raw({ raw: text }, ...values) }\x1b[0m`
const red =
  (text, ...values) =>
    `\x1b[31m${ String.raw({ raw: text }, ...values) }\x1b[0m`;
const purple =
  (text, ...values) =>
    `\x1b[35m${ String.raw({ raw: text }, ...values) }\x1b[0m`;
const green =
  (text, ...values) =>
    `\x1b[32m${ String.raw({ raw: text }, ...values) }\x1b[0m`;
const blue =
  (text, ...values) =>
    `\x1b[34m${ String.raw({ raw: text }, ...values) }\x1b[0m`;

const colors = {
  yellow,
  red,
  purple,
  green,
  blue,
};

exports.colors = colors;
exports.Console = new Console;