import { UserError } from "@cloudflare/workers-utils"; import { isNonInteractiveOrCI } from "@cloudflare/workers-utils"; import chalk from "chalk"; import prompts from "prompts"; import { logger } from "./logger"; import type { TelemetryMessage } from "@cloudflare/workers-utils"; export class NoDefaultValueProvided extends UserError { constructor( options: TelemetryMessage = { telemetryMessage: "dialogs non interactive default missing", } ) { // This is user-facing, so make the message something understandable // It _should_ always be caught and replaced with a more descriptive error // but this is fine as a fallback. super("This command cannot be run in a non-interactive context", options); Object.setPrototypeOf(this, new.target.prototype); } } interface ConfirmOptions { defaultValue?: boolean; fallbackValue?: boolean; } export async function confirm( text: string, { defaultValue = true, fallbackValue = true }: ConfirmOptions = {} ): Promise { if (isNonInteractiveOrCI()) { logger.log(`? ${text}`); logger.log( `🤖 ${chalk.dim( "Using fallback value in non-interactive context:" )} ${chalk.white.bold(fallbackValue ? "yes" : "no")}` ); return fallbackValue; } const { value } = await prompts({ type: "confirm", name: "value", message: text, initial: defaultValue, onState: (state) => { if (state.aborted) { process.nextTick(() => { process.exit(1); }); } }, }); return value; } interface PromptOptions { defaultValue?: string; isSecret?: boolean; validate?: (value: string) => boolean | string | Promise; } export async function prompt( text: string, options: PromptOptions = {} ): Promise { if (isNonInteractiveOrCI()) { if (options?.defaultValue === undefined) { throw new NoDefaultValueProvided({ telemetryMessage: "dialogs prompt default missing", }); } logger.log(`? ${text}`); logger.log( `🤖 ${chalk.dim( "Using default value in non-interactive context:" )} ${chalk.white.bold(options.defaultValue)}` ); return options.defaultValue; } const { value } = await prompts({ type: "text", name: "value", message: text, initial: options?.defaultValue, style: options?.isSecret ? "password" : "default", validate: options.validate, onState: (state) => { if (state.aborted) { process.nextTick(() => { process.exit(1); }); } }, }); return value; } interface SelectOptions { choices: SelectOption[]; defaultOption?: number; fallbackOption?: number; } interface SelectOption { title: string; description?: string; value: Values; } export async function select( text: string, options: SelectOptions ): Promise { if (isNonInteractiveOrCI()) { if (options.fallbackOption === undefined) { throw new NoDefaultValueProvided({ telemetryMessage: "dialogs select fallback missing", }); } logger.log(`? ${text}`); logger.log( `🤖 ${chalk.dim( "Using fallback value in non-interactive context:" )} ${chalk.white.bold(options.choices[options.fallbackOption].title)}` ); return options.choices[options.fallbackOption].value; } const { value } = await prompts({ type: "select", name: "value", message: text, choices: options.choices, initial: options.defaultOption, onState: (state) => { if (state.aborted) { process.nextTick(() => { process.exit(1); }); } }, }); return value; } interface MultiSelectOptions { choices: SelectOption[]; defaultOptions?: number[]; } export async function multiselect( text: string, options: MultiSelectOptions ): Promise { if (isNonInteractiveOrCI()) { if (options?.defaultOptions === undefined) { throw new NoDefaultValueProvided({ telemetryMessage: "dialogs multiselect defaults missing", }); } const defaultTitles = options.defaultOptions.map( (index) => options.choices[index].title ); logger.log(`? ${text}`); logger.log( `🤖 ${chalk.dim( "Using default value(s) in non-interactive context:" )} ${chalk.white.bold(defaultTitles.join(", "))}` ); return options.defaultOptions.map((index) => options.choices[index].value); } const { value } = await prompts({ type: "multiselect", name: "value", message: text, choices: options.choices, instructions: false, hint: "- Space to select. Return to submit", onState: (state) => { if (state.aborted) { process.nextTick(() => { process.exit(1); }); } }, }); return value; }