packages/@aws-cdk/toolkit-lib/lib/toolkit/non-interactive-io-host.ts (79 lines of code) (raw):

import * as chalk from 'chalk'; import type { IIoHost, IoMessage, IoMessageLevel, IoRequest } from '../api/io'; import type { IActivityPrinter } from '../api/shared-private'; import { HistoryActivityPrinter, isMessageRelevantForLevel } from '../api/shared-private'; import { isCI, isTTY } from '../util/shell-env'; export interface NonInteractiveIoHostProps { /** * Determines the verbosity of the output. * * The IoHost will still receive all messages and requests, * but only the messages included in this level will be printed. * * @default 'info' */ readonly logLevel?: IoMessageLevel; /** * Overrides the automatic TTY detection. * * When TTY is disabled, the CLI will have no interactions or color. * * @default - determined from the current process */ readonly isTTY?: boolean; /** * Whether the IoHost is running in CI mode. * * In CI mode, all non-error output goes to stdout instead of stderr. * Set to false in the IoHost constructor it will be overwritten if the CLI CI argument is passed * * @default - determined from the environment, specifically based on `process.env.CI` */ readonly isCI?: boolean; } /** * A simple IO host for a non interactive CLI that writes messages to the console and returns the default answer to all requests. */ export class NonInteractiveIoHost implements IIoHost { /** * Whether the IoHost is running in CI mode. * * In CI mode, all non-error output goes to stdout instead of stderr. */ public readonly isCI: boolean; /** * Whether the host can use interactions and message styling. */ public readonly isTTY: boolean; /** * The current threshold. * * Messages with a lower priority level will be ignored. */ public readonly logLevel: IoMessageLevel; // Stack Activity Printer private readonly activityPrinter: IActivityPrinter; public constructor(props: NonInteractiveIoHostProps = {}) { this.logLevel = props.logLevel ?? 'info'; this.isTTY = props.isTTY ?? isTTY(); this.isCI = props.isCI ?? isCI(); this.activityPrinter = new HistoryActivityPrinter({ stream: this.selectStreamFromLevel('info'), }); } /** * Notifies the host of a message. * The caller waits until the notification completes. */ public async notify(msg: IoMessage<unknown>): Promise<void> { if (isStackActivity(msg)) { return this.activityPrinter.notify(msg); } if (!isMessageRelevantForLevel(msg, this.logLevel)) { return; } const output = this.formatMessage(msg); const stream = this.selectStream(msg); stream?.write(output); } /** * Determines the output stream, based on message and configuration. */ private selectStream(msg: IoMessage<any>): NodeJS.WriteStream | undefined { return this.selectStreamFromLevel(msg.level); } /** * Determines the output stream, based on message level and configuration. */ private selectStreamFromLevel(level: IoMessageLevel): NodeJS.WriteStream { // The stream selection policy for the CLI is the following: // // (1) Messages of level `result` always go to `stdout` // (2) Messages of level `error` always go to `stderr`. // (3a) All remaining messages go to `stderr`. // (3b) If we are in CI mode, all remaining messages go to `stdout`. // switch (level) { case 'error': return process.stderr; case 'result': return process.stdout; default: return this.isCI ? process.stdout : process.stderr; } } /** * Notifies the host of a message that requires a response. * * If the host does not return a response the suggested * default response from the input message will be used. */ public async requestResponse<DataType, ResponseType>(msg: IoRequest<DataType, ResponseType>): Promise<ResponseType> { // in the non-interactive IoHost, no requests are promptable await this.notify(msg); return msg.defaultResponse; } /** * Formats a message for console output with optional color support */ private formatMessage(msg: IoMessage<unknown>): string { // apply provided style or a default style if we're in TTY mode let message_text = this.isTTY ? styleMap[msg.level](msg.message) : msg.message; // prepend timestamp if IoMessageLevel is DEBUG or TRACE. Postpend a newline. return ((msg.level === 'debug' || msg.level === 'trace') ? `[${this.formatTime(msg.time)}] ${message_text}` : message_text) + '\n'; } /** * Formats date to HH:MM:SS */ private formatTime(d: Date): string { const pad = (n: number): string => n.toString().padStart(2, '0'); return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } } const styleMap: Record<IoMessageLevel, (str: string) => string> = { error: chalk.red, warn: chalk.yellow, result: chalk.white, info: chalk.white, debug: chalk.gray, trace: chalk.gray, }; /** * Detect stack activity messages so they can be send to the printer. */ function isStackActivity(msg: IoMessage<unknown>) { return [ 'CDK_TOOLKIT_I5501', 'CDK_TOOLKIT_I5502', 'CDK_TOOLKIT_I5503', ].includes(msg.code); }