UNPKG

@poppinss/cliui

Version:

Opinionated UI KIT for Command Line apps

2,008 lines (2,007 loc) 47.6 kB
import { t as TERMINAL_SIZE } from "./helpers-DBWrFADB.js"; import supportsColor from "supports-color"; import colors, { default as poppinssColors } from "@poppinss/colors"; import CliTable from "cli-table3"; import stringWidth from "string-width"; import logUpdate from "log-update"; import prettyHrtime from "pretty-hrtime"; import boxes from "cli-boxes"; //#region src/icons.ts const { platform } = process; /** * A collection of platform specific icons */ const icons = platform === "win32" && !process.env.WT_SESSION ? { tick: "√", cross: "×", bullet: "*", nodejs: "♦", pointer: ">", info: "i", warning: "‼", squareSmallFilled: "[█]", borderVertical: "|" } : { tick: "✔", cross: "✖", bullet: "●", nodejs: "⬢", pointer: "❯", info: "ℹ", warning: "⚠", squareSmallFilled: "◼", borderVertical: "│" }; //#endregion //#region src/colors.ts /** * Returns the colors instance based upon the environment. * * - The "raw" option returns the colors instance that prefix the color * transformations as raw text * - The "silent" option returns the colors instance that performs no * color transformations */ function useColors(options = {}) { if (options.raw) return colors.raw(); if (options.silent) return colors.silent(); return colors.ansi(); } //#endregion //#region src/renderers/console.ts /** * Renders messages to the "stdout" and "stderr" */ var ConsoleRenderer = class { getLogs() { return []; } flushLogs() {} log(message) { console.log(message); } /** * Log message by overwriting the existing one */ logUpdate(message) { logUpdate(message); } /** * Persist the last logged message */ logUpdatePersist() { logUpdate.done(); } /** * Log error */ logError(message) { console.error(message); } }; //#endregion //#region src/table.ts /** * Exposes the API to represent a table */ var Table = class { #state = { head: [], rows: [] }; /** * Size of the largest row for a given * column */ #columnSizes = []; /** * The renderer to use to output logs */ #renderer; /** * Logger configuration options */ #options; /** * The colors reference */ #colors; /** * Whether or not to render full width */ #renderFullWidth = false; /** * The column index that should take remaining * width. */ #fluidColumnIndex = 0; /** * Padding for columns */ #padding = 2; constructor(options = {}) { this.#options = { raw: options.raw === void 0 ? false : options.raw, chars: options.chars }; } /** * Tracking the column size and keeping on the largest * one by tracking the content size */ #storeColumnSize(columns) { columns.forEach((column, index) => { const size = stringWidth(column); const existingSize = this.#columnSizes[index]; if (!existingSize || existingSize < size) this.#columnSizes[index] = size; }); } /** * Computes the col widths based when in fullwidth mode */ #computeColumnsWidth() { /** * Do not compute columns size, when rendering in full-width */ if (!this.#renderFullWidth) return; /** * The terminal columns */ let columns = TERMINAL_SIZE - (this.#columnSizes.length + 1); this.#state.colWidths = this.#state.colWidths || []; this.#columnSizes.forEach((column, index) => { /** * The column width will be the size of the biggest * text + padding left + padding right */ this.#state.colWidths[index] = this.#state.colWidths[index] || column + this.#padding * 2; /** * Compute remaining columns */ columns = columns - this.#state.colWidths[index]; }); /** * If there are remaining columns, then assign them * to the fluid column. */ if (columns) { const index = this.#fluidColumnIndex > this.#columnSizes.length - 1 ? 0 : this.#fluidColumnIndex; this.#state.colWidths[index] = this.#state.colWidths[index] + columns; } } /** * Returns the renderer for rendering the messages */ getRenderer() { if (!this.#renderer) this.#renderer = new ConsoleRenderer(); return this.#renderer; } /** * Define a custom renderer. Logs to "stdout" and "stderr" * by default */ useRenderer(renderer) { this.#renderer = renderer; return this; } /** * Returns the colors implementation in use */ getColors() { if (!this.#colors) this.#colors = useColors(); return this.#colors; } /** * Define a custom colors implementation */ useColors(color) { this.#colors = color; return this; } /** * Define table head */ head(headColumns) { this.#state.head = headColumns; this.#storeColumnSize(headColumns.map((column) => typeof column === "string" ? column : column.content)); return this; } /** * Add a new table row */ row(row) { this.#state.rows.push(row); if (Array.isArray(row)) this.#storeColumnSize(row.map((cell) => typeof cell === "string" ? cell : cell.content)); return this; } /** * Define custom column widths */ columnWidths(widths) { this.#state.colWidths = widths; return this; } /** * Toggle whether or render in full width or not */ fullWidth(renderFullWidth = true) { this.#renderFullWidth = renderFullWidth; return this; } /** * Define the column index that should take * will remaining width when rendering in * full-width */ fluidColumnIndex(index) { this.#fluidColumnIndex = index; return this; } /** * Render table */ render() { if (this.#options.raw) { this.getRenderer().log(this.#state.head.map((col) => typeof col === "string" ? col : col.content).join("|")); this.#state.rows.forEach((row) => { const content = Array.isArray(row) ? row.map((cell) => typeof cell === "string" ? cell : cell.content) : Object.keys(row); this.getRenderer().log(content.join("|")); }); return; } this.#computeColumnsWidth(); /** * Types of "cli-table3" are out of the sync from the * implementation */ const cliTable = new CliTable({ head: this.#state.head, style: { "head": [], "border": ["dim"], "padding-left": 2, "padding-right": 2 }, wordWrap: true, ...this.#state.colWidths ? { colWidths: this.#state.colWidths } : {}, chars: this.#options.chars }); this.#state.rows.forEach((row) => cliTable.push(row)); this.getRenderer().log(cliTable.toString()); } }; //#endregion //#region src/steps.ts /** * Steps class is used to display a series of sequential steps * with counters, titles, content, and a visual left border * connecting them. * * ```ts * const steps = ui.steps() * * steps.add('Install dependencies', 'Run npm install to get started') * steps.add('Configure app', 'Create a .env file with your settings') * steps.add('Start server', 'Use npm start to launch the application') * * steps.render() * ``` */ var Steps = class { /** * Collection of steps to display */ #steps = []; /** * The renderer to use for rendering output */ #renderer; /** * Colors instance for styling output */ #colors; /** * Options for the steps */ #options; constructor(options = {}) { this.#options = { raw: options.raw === void 0 ? false : options.raw }; } /** * Returns the renderer to use for output. * Defaults to ConsoleRenderer if not explicitly set */ getRenderer() { if (!this.#renderer) this.#renderer = new ConsoleRenderer(); return this.#renderer; } /** * Define a custom renderer to use */ useRenderer(renderer) { this.#renderer = renderer; return this; } /** * Returns the colors instance */ getColors() { if (!this.#colors) this.#colors = useColors(); return this.#colors; } /** * Define a custom colors instance */ useColors(colors) { this.#colors = colors; return this; } /** * Add a new step to the collection * * @param title - The step title/heading * @param content - Optional content/description for the step (supports ANSI formatting) */ add(title, content) { this.#steps.push({ title, content }); return this; } /** * Prepare the formatted output without rendering. * Useful for testing or custom output handling. */ prepare() { const colors = this.getColors(); const lines = []; const stepCount = this.#steps.length; this.#steps.forEach((step, index) => { const stepNumber = index + 1; const isLast = stepNumber === stepCount; const counter = colors.cyan(`${stepNumber}.`); const title = colors.bold(step.title); lines.push(`${counter} ${title}`); if (step.content) step.content.split("\n").forEach((line) => { if (this.#options.raw) lines.push(line); else lines.push(` ${line}`); }); if (!isLast && !this.#options.raw) lines.push(""); }); return lines.join("\n"); } /** * Render the steps to the configured renderer */ render() { const output = this.prepare(); this.getRenderer().log(output); } }; //#endregion //#region src/logger/action.ts /** * Exposes the API to print actions in one of the following three states * * - failed * - succeeded * - skipped */ var Action = class { #startTime; /** * Action options */ #options; /** * Action message */ #message; /** * Reference to the colors implementation */ #colors; /** * The renderer to use for writing to the console */ #renderer; /** * Whether or not to display duration of the action */ #displayDuration = false; constructor(message, options = {}) { this.#message = message; this.#startTime = process.hrtime(); this.#options = { dim: options.dim === void 0 ? false : options.dim }; } /** * Format label */ #formatLabel(label, color) { label = this.getColors()[color](`${label.toUpperCase()}:`); if (this.#options.dim) return this.getColors().dim(label); return label; } /** * Format message */ #formatMessage(message) { if (this.#options.dim) return this.getColors().dim(message); return message; } /** * Format the suffix */ #formatSuffix(message) { message = `(${message})`; return this.getColors().dim(message); } /** * Format error */ #formatError(error) { return `\n ${(typeof error === "string" ? error : error.stack || error.message).split("\n").map((line) => { if (this.#options.dim) line = this.getColors().dim(line); return ` ${this.getColors().red(line)}`; }).join("\n")}`; } /** * Returns the renderer for rendering the messages */ getRenderer() { if (!this.#renderer) this.#renderer = new ConsoleRenderer(); return this.#renderer; } /** * Define a custom renderer. */ useRenderer(renderer) { this.#renderer = renderer; return this; } /** * Returns the colors implementation in use */ getColors() { if (!this.#colors) this.#colors = useColors(); return this.#colors; } /** * Define a custom colors implementation */ useColors(color) { this.#colors = color; return this; } /** * Toggle whether to display duration for completed * tasks or not. */ displayDuration(displayDuration = true) { this.#displayDuration = displayDuration; return this; } /** * Prepares the message to mark action as successful */ prepareSucceeded() { let logMessage = `${this.#formatLabel("done", "green")} ${this.#formatMessage(this.#message)}`; if (this.#displayDuration) logMessage = `${logMessage} ${this.#formatSuffix(prettyHrtime(process.hrtime(this.#startTime)))}`; return logMessage; } /** * Mark action as successful */ succeeded() { this.getRenderer().log(this.prepareSucceeded()); } /** * Prepares the message to mark action as skipped */ prepareSkipped(skipReason) { let logMessage = `${this.#formatLabel("skipped", "cyan")} ${this.#formatMessage(this.#message)}`; if (skipReason) logMessage = `${logMessage} ${this.#formatSuffix(skipReason)}`; return logMessage; } /** * Mark action as skipped. An optional skip reason can be * supplied */ skipped(skipReason) { this.getRenderer().log(this.prepareSkipped(skipReason)); } /** * Prepares the message to mark action as failed */ prepareFailed(error) { return `${this.#formatLabel("failed", "red")} ${this.#formatMessage(this.#message)} ${this.#formatError(error)}`; } /** * Mark action as failed. An error message is required */ failed(error) { this.getRenderer().logError(this.prepareFailed(error)); } }; //#endregion //#region src/logger/spinner.ts /** * Textual spinner to print a message with dotted progress * bar. */ var Spinner = class { #animator = { frames: [ ". ", ".. ", "...", " ..", " .", " " ], interval: 200, index: 0, getFrame() { return this.frames[this.index]; }, advance() { this.index = this.index + 1 === this.frames.length ? 0 : this.index + 1; return this.index; } }; /** * The state of the spinner */ #state = "idle"; /** * Spinner message */ #message; /** * The renderer to use for writing to the console */ #renderer; /** * Custom method to handle animation result */ #spinnerWriter; constructor(message) { this.#message = message; } /** * Loop over the message and animate the spinner */ #animate() { if (this.#state !== "running") return; /** * Do not write when in silent mode */ if (this.#message.silent) return; const frame = this.#animator.getFrame(); if (this.#spinnerWriter) this.#spinnerWriter(`${this.#message.render()} ${frame}`); else this.getRenderer().logUpdate(`${this.#message.render()} ${frame}`); setTimeout(() => { this.#animator.advance(); this.#animate(); }, this.#animator.interval); } /** * Returns the renderer for rendering the messages */ getRenderer() { if (!this.#renderer) this.#renderer = new ConsoleRenderer(); return this.#renderer; } /** * Define the custom renderer */ useRenderer(renderer) { this.#renderer = renderer; return this; } /** * Star the spinner */ start() { this.#state = "running"; this.#animate(); return this; } /** * Update spinner */ update(text, options) { if (this.#state !== "running") return this; Object.assign(this.#message, { text, ...options }); return this; } /** * Stop spinner */ stop() { this.#state = "stopped"; this.#animator.index = 0; if (!this.#spinnerWriter && !this.#message.silent) { this.getRenderer().logUpdate(`${this.#message.render()} ${this.#animator.frames[2]}`); this.getRenderer().logUpdatePersist(); } } /** * Tap into spinner to manually write the * output. */ tap(callback) { this.#spinnerWriter = callback; return this; } }; //#endregion //#region src/renderers/memory.ts /** * Keeps log messages within memory. Useful for testing */ var MemoryRenderer = class { #logs = []; getLogs() { return this.#logs; } flushLogs() { this.#logs = []; } /** * Log message */ log(message) { this.#logs.push({ message, stream: "stdout" }); } /** * For memory renderer the logUpdate is similar to log */ logUpdate(message) { this.log(message); } /** * Its a noop */ logUpdatePersist() {} /** * Log message as error */ logError(message) { this.#logs.push({ message, stream: "stderr" }); } }; //#endregion //#region src/logger/main.ts /** * CLI logger to log messages to the console. The output is consistently * formatted. */ var Logger = class { /** * Logger configuration options */ #options; /** * Reference to the colors implementation */ #colors; /** * The renderer to use to output logs */ #renderer; getLogs() { return this.getRenderer().getLogs(); } flushLogs() { this.getRenderer().flushLogs(); } constructor(options = {}) { const dimOutput = options.dim === void 0 ? false : options.dim; this.#options = { dim: dimOutput, dimLabels: options.dimLabels === void 0 ? dimOutput : options.dimLabels }; } /** * Color the logger label */ #colorizeLabel(color, text) { text = this.getColors()[color](text); if (this.#options.dimLabels) return `[ ${this.getColors().dim(text)} ]`; return `[ ${text} ]`; } /** * Returns the label for a given logging type */ #getLabel(type) { switch (type) { case "success": return this.#colorizeLabel("green", type); case "error": case "fatal": return this.#colorizeLabel("red", type); case "warning": return this.#colorizeLabel("yellow", "warn"); case "info": return this.#colorizeLabel("blue", type); case "debug": return this.#colorizeLabel("cyan", type); case "await": return this.#colorizeLabel("cyan", "wait"); } } /** * Appends the suffix to the message */ #addSuffix(message, suffix) { if (!suffix) return message; return `${message} ${this.getColors().dim().yellow(`(${suffix})`)}`; } /** * Appends duration to the message */ #addDuration(message, duration) { if (!duration) return message; return `${message} ${this.getColors().dim(`(${prettyHrtime(process.hrtime(duration))})`)}`; } /** * Prepends the prefix to the message. We do not DIM the prefix, since * gray doesn't have much brightness already */ #addPrefix(message, prefix) { if (!prefix) return message; prefix = prefix.replace(/%time%/, (/* @__PURE__ */ new Date()).toISOString()); return `${this.getColors().dim(`[${prefix}]`)} ${message}`; } /** * Prepends the prefix to the message */ #prefixLabel(message, label) { return `${label} ${message}`; } /** * Decorate message string */ #decorateMessage(message) { if (this.#options.dim) return this.getColors().dim(message); return message; } /** * Decorate error stack */ #formatStack(stack) { if (!stack) return ""; return `\n${stack.split("\n").splice(1).map((line) => { if (this.#options.dim) line = this.getColors().dim(line); return ` ${this.getColors().red(line)}`; }).join("\n")}`; } /** * Returns the renderer for rendering the messages */ getRenderer() { if (!this.#renderer) this.#renderer = new ConsoleRenderer(); return this.#renderer; } /** * Define a custom renderer to output logos */ useRenderer(renderer) { this.#renderer = renderer; return this; } /** * Returns the colors implementation in use */ getColors() { if (!this.#colors) this.#colors = useColors(); return this.#colors; } /** * Define a custom colors implementation */ useColors(color) { this.#colors = color; return this; } /** * Log message */ log(message) { this.getRenderer().log(message); } /** * Log message by updating the existing line */ logUpdate(message) { this.getRenderer().logUpdate(message); } /** * Persist log line written using the `logUpdate` * method. */ logUpdatePersist() { this.getRenderer().logUpdatePersist(); } /** * Log error message using the renderer. It is similar to `console.error` * but uses the underlying renderer instead */ logError(message) { this.getRenderer().logError(message); } /** * Prepares the success message */ prepareSuccess(message, options) { message = this.#decorateMessage(message); message = this.#prefixLabel(message, this.#getLabel("success")); message = this.#addPrefix(message, options?.prefix); message = this.#addSuffix(message, options?.suffix); message = this.#addDuration(message, options?.startTime); return message; } /** * Log success message */ success(message, options) { this.log(this.prepareSuccess(message, options)); } /** * Prepares the error message */ prepareError(message, options) { message = typeof message === "string" ? message : message.message; message = this.#decorateMessage(message); message = this.#prefixLabel(message, this.#getLabel("error")); message = this.#addPrefix(message, options?.prefix); message = this.#addSuffix(message, options?.suffix); message = this.#addDuration(message, options?.startTime); return message; } /** * Log error message */ error(message, options) { this.logError(this.prepareError(message, options)); } /** * Prepares the fatal message */ prepareFatal(message, options) { const stack = this.#formatStack(typeof message === "string" ? void 0 : message.stack); message = typeof message === "string" ? message : message.message; message = this.#decorateMessage(message); message = this.#prefixLabel(message, this.#getLabel("error")); message = this.#addPrefix(message, options?.prefix); message = this.#addSuffix(message, options?.suffix); message = this.#addDuration(message, options?.startTime); return `${message}${stack}`; } /** * Log fatal message */ fatal(message, options) { this.logError(this.prepareFatal(message, options)); } /** * Prepares the warning message */ prepareWarning(message, options) { message = this.#decorateMessage(message); message = this.#prefixLabel(message, this.#getLabel("warning")); message = this.#addPrefix(message, options?.prefix); message = this.#addSuffix(message, options?.suffix); message = this.#addDuration(message, options?.startTime); return message; } /** * Log warning message */ warning(message, options) { this.log(this.prepareWarning(message, options)); } /** * Prepares the info message */ prepareInfo(message, options) { message = this.#decorateMessage(message); message = this.#prefixLabel(message, this.#getLabel("info")); message = this.#addPrefix(message, options?.prefix); message = this.#addSuffix(message, options?.suffix); message = this.#addDuration(message, options?.startTime); return message; } /** * Log info message */ info(message, options) { this.log(this.prepareInfo(message, options)); } /** * Prepares the debug message */ prepareDebug(message, options) { message = this.#decorateMessage(message); message = this.#prefixLabel(message, this.#getLabel("debug")); message = this.#addPrefix(message, options?.prefix); message = this.#addSuffix(message, options?.suffix); message = this.#addDuration(message, options?.startTime); return message; } /** * Log debug message */ debug(message, options) { this.log(this.prepareDebug(message, options)); } /** * Log a message with a spinner */ await(text, options) { return new Spinner({ logger: this, text, ...options, render() { let decorated = this.logger.#decorateMessage(this.text); decorated = this.logger.#prefixLabel(decorated, this.logger.#getLabel("await")); decorated = this.logger.#addPrefix(decorated, this.prefix); decorated = this.logger.#addSuffix(decorated, this.suffix); return decorated; } }).useRenderer(this.getRenderer()); } /** * Initiates a new action */ action(title) { return new Action(title, { dim: this.#options.dim }).useColors(this.getColors()).useRenderer(this.getRenderer()); } /** * Create a new child instance of self */ child(options) { return new this.constructor(options).useColors(this.getColors()).useRenderer(this.getRenderer()); } /** * Create a dummy logger that silently discards all output. * Useful when output must be suppressed, for example * inside a tasks-manager callback where the task widget * owns the terminal. */ dummy() { return new this.constructor().useColors(this.getColors()).useRenderer(new MemoryRenderer()); } }; //#endregion //#region src/instructions.ts /** * The box styling used by the instructions */ const BOX = boxes.round; /** * The API to render instructions wrapped inside a box */ var Instructions = class { #state = { content: [] }; /** * Renderer to use for rendering instructions */ #renderer; /** * Length of the widest line inside instructions content */ #widestLineLength = 0; /** * Number of white spaces on the left of the box */ #leftPadding = 4; /** * Number of white spaces on the right of the box */ #rightPadding = 8; /** * Number of empty lines at the top */ #paddingTop = 1; /** * Number of empty lines at the bottom */ #paddingBottom = 1; /** * Reference to the colors */ #colors; /** * Options */ #options; /** * Draws the border */ #drawBorder = (border, colors) => { return colors.dim(border); }; constructor(options = {}) { this.#options = { icons: options.icons === void 0 ? true : options.icons, raw: options.raw === void 0 ? false : options.raw }; } /** * Returns the length of the horizontal line */ #getHorizontalLength() { return this.#widestLineLength + this.#leftPadding + this.#rightPadding; } /** * Repeats text for given number of times */ #repeat(text, times) { return new Array(times + 1).join(text); } /** * Wraps content inside the left and right vertical lines */ #wrapInVerticalLines(content, leftWhitespace, rightWhitespace) { return `${this.#drawBorder(BOX.left, this.getColors())}${leftWhitespace}${content}${rightWhitespace}${this.#drawBorder(BOX.right, this.getColors())}`; } /** * Returns the top line for the box */ #getTopLine() { const horizontalLength = this.#getHorizontalLength(); const horizontalLine = this.#repeat(this.#drawBorder(BOX.top, this.getColors()), horizontalLength); return `${this.#drawBorder(BOX.topLeft, this.getColors())}${horizontalLine}${this.#drawBorder(BOX.topRight, this.getColors())}`; } /** * Returns the bottom line for the box */ #getBottomLine() { const horizontalLength = this.#getHorizontalLength(); const horizontalLine = this.#repeat(this.#drawBorder(BOX.bottom, this.getColors()), horizontalLength); return `${this.#drawBorder(BOX.bottomLeft, this.getColors())}${horizontalLine}${this.#drawBorder(BOX.bottomRight, this.getColors())}`; } /** * Returns the heading border bottom */ #getHeadingBorderBottom() { const horizontalLength = this.#getHorizontalLength(); const horizontalLine = this.#repeat(this.#drawBorder(boxes.single.top, this.getColors()), horizontalLength); return this.#wrapInVerticalLines(horizontalLine, "", ""); } /** * Decorates the instruction line by wrapping it inside the box * lines */ #getContentLine(line) { const leftWhitespace = this.#repeat(" ", this.#leftPadding); const rightWhitespace = this.#repeat(" ", this.#widestLineLength - line.width + this.#rightPadding); return this.#wrapInVerticalLines(line.text, leftWhitespace, rightWhitespace); } /** * Returns the heading line by applying padding */ #getHeading() { if (!this.#state.heading) return; return this.#getContentLine(this.#state.heading); } /** * Returns the formatted body */ #getBody() { if (!this.#state.content || !this.#state.content.length) return; const top = new Array(this.#paddingTop).fill("").map(this.#getEmptyLineNode); const bottom = new Array(this.#paddingBottom).fill("").map(this.#getEmptyLineNode); return top.concat(this.#state.content).concat(bottom).map((line) => this.#getContentLine(line)).join("\n"); } /** * Returns node for a empty line */ #getEmptyLineNode() { return { text: "", width: 0 }; } /** * Returns the renderer for rendering the messages */ getRenderer() { if (!this.#renderer) this.#renderer = new ConsoleRenderer(); return this.#renderer; } /** * Define a custom renderer. Logs to "stdout" and "stderr" * by default */ useRenderer(renderer) { this.#renderer = renderer; return this; } /** * Returns the colors implementation in use */ getColors() { if (!this.#colors) this.#colors = useColors(); return this.#colors; } /** * Define a custom colors implementation */ useColors(color) { this.#colors = color; return this; } /** * Draw the instructions box in fullscreen */ fullScreen() { this.#widestLineLength = TERMINAL_SIZE - (this.#leftPadding + this.#rightPadding) - 2; return this; } /** * Attach a callback to self draw the borders */ drawBorder(callback) { this.#drawBorder = callback; return this; } /** * Define heading for instructions */ heading(text) { const width = stringWidth(text); if (width > this.#widestLineLength) this.#widestLineLength = width; this.#state.heading = { text, width }; return this; } /** * Add new instruction. Each instruction is rendered * in a new line inside a box */ add(text) { text = this.#options.icons ? `${this.getColors().dim(icons.pointer)} ${text}` : `${text}`; const width = stringWidth(text); if (width > this.#widestLineLength) this.#widestLineLength = width; this.#state.content.push({ text, width }); return this; } prepare() { /** * Render content as it is in raw mode */ if (this.#options.raw) { let output = []; if (this.#state.heading) output.push(this.#state.heading.text); output = output.concat(this.#state.content.map(({ text }) => text)); return output.join("\n"); } const top = this.#getTopLine(); const heading = this.#getHeading(); const headingBorderBottom = this.#getHeadingBorderBottom(); const body = this.#getBody(); const bottom = this.#getBottomLine(); let output = `${top}\n`; /** * Draw heading if it exists */ if (heading) output = `${output}${heading}`; /** * Draw the border between the heading and the body if * both exists */ if (heading && body) output = `${output}\n${headingBorderBottom}\n`; /** * Draw body if it exists */ if (body) output = `${output}${body}`; return `${output}\n${bottom}`; } /** * Render instructions */ render() { this.getRenderer().log(this.prepare()); } }; //#endregion //#region src/tasks/task.ts /** * Task exposes a very simple API to create tasks with states, along with a * listener to listen for the task state updates. * * The task itself has does not render anything to the console. The task * renderers does that. */ var Task = class { #startTime; /** * Last logged line for the task */ #lastLogLine; /** * Define one or more update listeners */ #updateListeners = []; /** * Duration of the task. Updated after the task is over */ #duration; /** * Message set after completing the task. Can be an error or the * a success message */ #completionMessage; /** * Task current state */ #state = "idle"; constructor(title) { this.title = title; } #notifyListeners() { for (let listener of this.#updateListeners) listener(this); } /** * Access the task state */ getState() { return this.#state; } /** * Get the time spent in running the task */ getDuration() { return this.#duration || null; } /** * Get error occurred while running the task */ getError() { return this.#completionMessage || null; } /** * Get task completion success message */ getSuccessMessage() { return typeof this.#completionMessage === "string" ? this.#completionMessage : null; } /** * Last logged line for the task */ getLastLoggedLine() { return this.#lastLogLine || null; } /** * Bind a listener to listen to the state updates of the task */ onUpdate(listener) { this.#updateListeners.push(listener); return this; } /** * Start the task */ start() { this.#state = "running"; this.#startTime = process.hrtime(); this.#notifyListeners(); return this; } /** * Update task with log messages. Based upon the renderer * in use, it may only display one line at a time. */ update(message) { this.#lastLogLine = message; this.#notifyListeners(); return this; } /** * Mark task as completed */ markAsSucceeded(message) { this.#state = "succeeded"; this.#duration = prettyHrtime(process.hrtime(this.#startTime)); this.#completionMessage = message; this.#notifyListeners(); return this; } /** * Mark task as failed */ markAsFailed(error) { this.#state = "failed"; this.#duration = prettyHrtime(process.hrtime(this.#startTime)); this.#completionMessage = error; this.#notifyListeners(); return this; } }; //#endregion //#region src/tasks/renderers/verbose.ts /** * Verbose renderer shows a detailed output of the tasks and the * messages logged by a given task */ var VerboseRenderer = class { /** * Reference to the colors implementation */ #colors; /** * The renderer to use to output logs */ #renderer; /** * List of registered tasks */ #registeredTasks = []; #notifiedTasks = /* @__PURE__ */ new Set(); constructor() {} /** * Format error */ #formatError(error) { if (typeof error === "string") return `${this.#getAnsiIcon("│", "dim")}${this.getColors().red(error)}`; if (!error.stack) return `${this.#getAnsiIcon("│", "dim")}${this.getColors().red(error.message)}`; return `${error.stack.split("\n").map((line) => `${this.#getAnsiIcon("│", "dim")} ${this.getColors().red(line)}`).join("\n")}`; } /** * Returns the ansi icon back when icons are enabled * or an empty string */ #getAnsiIcon(icon, color) { return this.getColors()[color](`${icon} `); } /** * Renders message for a running task */ #renderRunningTask(task) { if (this.#notifiedTasks.has(task.title)) { const lastLoggedLine = task.getLastLoggedLine(); if (lastLoggedLine) this.getRenderer().log(`${this.#getAnsiIcon("│", "dim")}${lastLoggedLine}`); return; } this.getRenderer().log(`${this.#getAnsiIcon("┌", "dim")}${task.title}`); this.#notifiedTasks.add(task.title); } /** * Renders message for a succeeded task */ #renderSucceededTask(task) { const successMessage = task.getSuccessMessage(); const icon = this.#getAnsiIcon("└", "dim"); const status = this.getColors().green(successMessage || "Completed"); const duration = this.getColors().dim(`(${task.getDuration()})`); this.getRenderer().log(`${icon}${status} ${duration}`); } /** * Renders message for a failed task */ #renderFailedTask(task) { const error = task.getError(); if (error) this.getRenderer().logError(this.#formatError(error)); const icon = this.#getAnsiIcon("└", "dim"); const status = this.getColors().red("Failed"); const duration = this.getColors().dim(`(${task.getDuration()})`); this.getRenderer().logError(`${icon}${status} ${duration}`); } /** * Renders a given task */ #renderTask(task) { switch (task.getState()) { case "running": return this.#renderRunningTask(task); case "succeeded": return this.#renderSucceededTask(task); case "failed": return this.#renderFailedTask(task); } } /** * Renders all tasks */ #renderTasks() { this.#registeredTasks.forEach((task) => this.#renderTask(task)); } /** * Returns the renderer for rendering the messages */ getRenderer() { if (!this.#renderer) this.#renderer = new ConsoleRenderer(); return this.#renderer; } /** * Define a custom renderer. Logs to "stdout" and "stderr" * by default */ useRenderer(renderer) { this.#renderer = renderer; return this; } /** * Returns the colors implementation in use */ getColors() { if (!this.#colors) this.#colors = useColors(); return this.#colors; } /** * Define a custom colors implementation */ useColors(color) { this.#colors = color; return this; } /** * Register tasks to render */ tasks(tasks) { this.#registeredTasks = tasks; return this; } /** * Render all tasks */ render() { this.#registeredTasks.forEach((task) => task.onUpdate(($task) => this.#renderTask($task))); this.#renderTasks(); } }; //#endregion //#region src/tasks/renderers/minimal.ts /** * As the name suggests, render tasks in minimal UI for better viewing * experience. */ var MinimalRenderer = class { /** * Renderer options */ #options; /** * Reference to the colors implementation */ #colors; /** * The renderer to use to output logs */ #renderer; /** * List of registered tasks */ #registeredTasks = []; constructor(options) { this.#options = { icons: options.icons === void 0 ? true : options.icons }; } /** * Format error */ #formatError(error) { let message = typeof error === "string" ? error : error.message; message = this.getColors().red(message); return `\n ${message.split("\n").map((line) => `${line}`).join("\n")}`; } /** * Returns the pointer icon, if icons are not disabled. */ #getPointerIcon(color) { const icon = this.#options.icons ? `${icons.pointer} ` : ""; if (!icon) return icon; return this.getColors()[color](icon); } /** * Returns the display string for an idle task */ #renderIdleTask(task) { return `${this.#getPointerIcon("dim")}${this.getColors().dim(task.title)}`; } /** * Returns the display string for a running task */ #renderRunningTask(task) { const lastLogLine = task.getLastLoggedLine(); return `${this.#options.icons ? `${icons.pointer} ${task.title}` : task.title}\n ${lastLogLine || ""}`; } /** * Returns the display string for a failed task */ #renderFailedTask(task) { const pointer = this.#getPointerIcon("red"); const duration = this.getColors().dim(`(${task.getDuration()})`); let message = `${pointer}${task.title} ${duration}`; const error = task.getError(); if (!error) return `${message}\n`; message = `${message}${this.#formatError(error)}`; return message; } /** * Returns the display string for a succeeded task */ #renderSucceededTask(task) { const pointer = this.#getPointerIcon("green"); const duration = this.getColors().dim(`(${task.getDuration()})`); let message = `${pointer}${task.title} ${duration}`; const successMessage = task.getSuccessMessage(); if (!successMessage) return `${message}\n`; message = `${message}\n ${this.getColors().green(successMessage)}`; return message; } /** * Renders a given task */ #renderTask(task) { switch (task.getState()) { case "idle": return this.#renderIdleTask(task); case "running": return this.#renderRunningTask(task); case "succeeded": return this.#renderSucceededTask(task); case "failed": return this.#renderFailedTask(task); } } /** * Renders all tasks */ #renderTasks() { const lastTaskState = this.#registeredTasks[this.#registeredTasks.length - 1].getState(); this.getRenderer().logUpdate(this.#registeredTasks.map((task) => this.#renderTask(task)).join("\n")); if (lastTaskState === "succeeded" || lastTaskState === "failed") this.getRenderer().logUpdatePersist(); } /** * Returns the renderer for rendering the messages */ getRenderer() { if (!this.#renderer) this.#renderer = new ConsoleRenderer(); return this.#renderer; } /** * Define a custom renderer. Logs to "stdout" and "stderr" * by default */ useRenderer(renderer) { this.#renderer = renderer; return this; } /** * Returns the colors implementation in use */ getColors() { if (!this.#colors) this.#colors = useColors(); return this.#colors; } /** * Define a custom colors implementation */ useColors(color) { this.#colors = color; return this; } /** * Register tasks to render */ tasks(tasks) { this.#registeredTasks = tasks; return this; } /** * Render all tasks */ render() { this.#registeredTasks.forEach((task) => { task.onUpdate(() => this.#renderTasks()); }); this.#renderTasks(); } }; //#endregion //#region src/tasks/renderers/raw.ts /** * Raw renderer shows a detailed output of the tasks without using any * ansi characters */ var RawRenderer = class { /** * Reference to the colors implementation */ #colors; /** * The renderer to use to output logs */ #renderer; /** * List of registered tasks */ #registeredTasks = []; #notifiedTasks = /* @__PURE__ */ new Set(); constructor() {} /** * Format error */ #formatError(error) { if (typeof error === "string") return `${this.getColors().red(error)}`; if (!error.stack) return `${this.getColors().red(error.message)}`; return `${error.stack.split("\n").map((line) => ` ${this.getColors().red(line)}`).join("\n")}`; } /** * Renders message for a running task */ #renderRunningTask(task) { if (this.#notifiedTasks.has(task.title)) { const lastLoggedLine = task.getLastLoggedLine(); if (lastLoggedLine) this.getRenderer().log(lastLoggedLine); return; } this.getRenderer().log(`${task.title}\n${new Array(task.title.length + 1).join("-")}`); this.#notifiedTasks.add(task.title); } /** * Renders message for a succeeded task */ #renderSucceededTask(task) { const successMessage = task.getSuccessMessage(); const status = this.getColors().green(successMessage || "completed"); const duration = this.getColors().dim(`(${task.getDuration()})`); this.getRenderer().log(`${status} ${duration}\n`); } /** * Renders message for a failed task */ #renderFailedTask(task) { const error = task.getError(); if (error) this.getRenderer().logError(this.#formatError(error)); const status = this.getColors().red("failed"); const duration = this.getColors().dim(`(${task.getDuration()})`); this.getRenderer().logError(`${status} ${duration}\n`); } /** * Renders a given task */ #renderTask(task) { switch (task.getState()) { case "running": return this.#renderRunningTask(task); case "succeeded": return this.#renderSucceededTask(task); case "failed": return this.#renderFailedTask(task); } } /** * Renders all tasks */ #renderTasks() { this.#registeredTasks.forEach((task) => this.#renderTask(task)); } /** * Returns the renderer for rendering the messages */ getRenderer() { if (!this.#renderer) this.#renderer = new ConsoleRenderer(); return this.#renderer; } /** * Define a custom renderer. Logs to "stdout" and "stderr" * by default */ useRenderer(renderer) { this.#renderer = renderer; return this; } /** * Returns the colors implementation in use */ getColors() { if (!this.#colors) this.#colors = useColors(); return this.#colors; } /** * Define a custom colors implementation */ useColors(color) { this.#colors = color; return this; } /** * Register tasks to render */ tasks(tasks) { this.#registeredTasks = tasks; return this; } /** * Render all tasks */ render() { this.#registeredTasks.forEach((task) => task.onUpdate(($task) => this.#renderTask($task))); this.#renderTasks(); } }; //#endregion //#region src/tasks/manager.ts function TRANSFORM_ERROR(error) { if (typeof error === "string") return { isError: true, message: error }; return error; } /** * Exposes the API to create a group of tasks and run them in sequence */ var TaskManager = class { /** * Last handled error */ error; /** * Options */ #options; /** * The renderer to use for rendering tasks. The verbose renderer is * used When "raw" is set to true. */ #tasksRenderer; /** * A set of created tasks */ #tasks = []; /** * State of the tasks manager */ #state = "idle"; constructor(options = {}) { this.#options = { icons: options.icons === void 0 ? true : options.icons, raw: options.raw === void 0 ? false : options.raw, verbose: options.verbose === void 0 ? false : options.verbose }; /** * Using verbose renderer when in raw mode */ if (this.#options.raw) this.#tasksRenderer = new RawRenderer(); else if (this.#options.verbose) this.#tasksRenderer = new VerboseRenderer(); else /** * Otheriwse using the minimal renderer */ this.#tasksRenderer = new MinimalRenderer({ icons: this.#options.icons }); } /** * Run a given task. The underlying code assumes that tasks are * executed in sequence. */ async #runTask(index) { const task = this.#tasks[index]; if (!task) return; /** * Start the underlying task */ task.task.start(); /** * Update task progress */ const update = (logMessage) => { task.task.update(logMessage); }; /** * Invoke callback */ try { const response = await task.callback({ error: TRANSFORM_ERROR, update }); if (typeof response === "string") { task.task.markAsSucceeded(response); await this.#runTask(index + 1); } else { this.#state = "failed"; task.task.markAsFailed(response); } } catch (error) { this.#state = "failed"; this.error = error; task.task.markAsFailed(error); } } /** * Access the task state */ getState() { return this.#state; } /** * Register a new task */ add(title, callback) { this.#tasks.push({ task: new Task(title), callback }); return this; } /** * Register a new task, when the "conditional = true" */ addIf(conditional, title, callback) { if (conditional) this.add(title, callback); return this; } /** * Register a new task, when the "conditional = false" */ addUnless(conditional, title, callback) { if (!conditional) this.add(title, callback); return this; } /** * Get access to registered tasks */ tasks() { return this.#tasks.map(({ task }) => task); } /** * Returns the renderer for rendering the messages */ getRenderer() { return this.#tasksRenderer.getRenderer(); } /** * Define a custom renderer. Logs to "stdout" and "stderr" * by default */ useRenderer(renderer) { this.#tasksRenderer.useRenderer(renderer); return this; } /** * Define a custom colors implementation */ useColors(color) { this.#tasksRenderer.useColors(color); return this; } /** * Run tasks */ async run() { if (this.#state !== "idle") return; this.#state = "running"; this.#tasksRenderer.tasks(this.tasks()).render(); await this.#runTask(0); if (this.#state === "running") this.#state = "succeeded"; } }; //#endregion //#region index.ts /** * Create a new CLI UI instance. * * - The "raw" mode is tailored for testing * - The "silent" mode should be used when the terminal does not support colors. We * automatically perform the detection */ function cliui(options = {}) { let mode = options.mode; /** * Use silent mode when not explicit mode is defined */ if (!mode && !supportsColor.stdout) mode = "silent"; /** * Renderer to use */ let renderer = mode === "raw" ? new MemoryRenderer() : new ConsoleRenderer(); /** * Colors instance in use */ let colors = useColors({ silent: mode === "silent", raw: mode === "raw" }); /** * Logger */ const logger = new Logger(); logger.useRenderer(renderer); logger.useColors(colors); /** * Render instructions inside a box */ const instructions = () => { const instructionsInstance = new Instructions({ icons: true, raw: mode === "raw" }); instructionsInstance.useRenderer(renderer); instructionsInstance.useColors(colors); return instructionsInstance; }; /** * Similar to instructions. But without the `pointer` icon */ const sticker = () => { const instructionsInstance = new Instructions({ icons: false, raw: mode === "raw" }); instructionsInstance.useRenderer(renderer); instructionsInstance.useColors(colors); return instructionsInstance; }; /** * Initiates a group of tasks */ const tasks = (tasksOptions) => { const manager = new TaskManager({ raw: mode === "raw", ...tasksOptions }); manager.useRenderer(renderer); manager.useColors(colors); return manager; }; /** * Instantiate a new table */ const table = (tableOptions) => { const tableInstance = new Table({ raw: mode === "raw", ...tableOptions }); tableInstance.useRenderer(renderer); tableInstance.useColors(colors); return tableInstance; }; /** * Instantiate a new steps display */ const steps = () => { const stepsInstance = new Steps({ raw: mode === "raw" }); stepsInstance.useRenderer(renderer); stepsInstance.useColors(colors); return stepsInstance; }; return { colors, logger, table, tasks, steps, icons, sticker, instructions, switchMode(modeToUse) { mode = modeToUse; /** * Use memory renderer in raw mode, otherwise switch to * console renderer */ if (mode === "raw") this.useRenderer(new MemoryRenderer()); else this.useRenderer(new ConsoleRenderer()); this.useColors(useColors({ silent: mode === "silent", raw: mode === "raw" })); }, useRenderer(rendererToUse) { renderer = rendererToUse; logger.useRenderer(renderer); }, useColors(colorsToUse) { colors = colorsToUse; logger.useColors(colors); this.colors = colors; } }; } //#endregion export { ConsoleRenderer, Instructions, Logger, MemoryRenderer, Steps, Table, TaskManager, cliui, poppinssColors as colors, icons };