UNPKG

@augment-vir/node

Version:

A collection of augments, helpers types, functions, and classes only for Node.js (backend) JavaScript environments.

143 lines (131 loc) 4.78 kB
import {log} from '@augment-vir/common'; import {convertDuration, type AnyDuration} from '@date-vir/duration'; import {createInterface} from 'node:readline'; /** Can't test requiring user input. */ /* node:coverage disable */ /** * Options for {@link askQuestion}. * * @category Node : Terminal : Util * @category Package : @augment-vir/node * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node) */ export type AskQuestionOptions = { timeout: AnyDuration; hideUserInput: boolean; }; const defaultAskQuestionOptions: AskQuestionOptions = { timeout: {seconds: 60}, hideUserInput: false, }; /** * Asks the user a question in their terminal and then waits for them to type something in response. * The response is accepted once the user inputs a new line. * * @category Node : Terminal * @category Package : @augment-vir/node * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node) * @see * - {@link askQuestionUntilConditionMet}: ask a question on loop until the user provides a valid response. */ export async function askQuestion( questionToAsk: string, { hideUserInput = defaultAskQuestionOptions.hideUserInput, timeout = defaultAskQuestionOptions.timeout, }: Partial<AskQuestionOptions> = defaultAskQuestionOptions, ): Promise<string> { const cliInterface = createInterface({ input: process.stdin, output: process.stdout, }); if (hideUserInput) { let promptWritten = false; /** _writeToOutput is not in the types OR in the Node.js documentation but is a thing. */ (cliInterface as unknown as {_writeToOutput: (prompt: string) => void})._writeToOutput = ( prompt, ) => { if (!promptWritten) { ( cliInterface as unknown as {output: {write: (output: string) => void}} ).output.write(prompt); promptWritten = true; } }; } // handle killing the process cliInterface.on('SIGINT', () => { cliInterface.close(); process.stdout.write('\n'); process.kill(process.pid, 'SIGINT'); }); return new Promise((resolve, reject) => { const timeoutMs = convertDuration(timeout, {milliseconds: true}).milliseconds; const timeoutId = timeoutMs ? setTimeout(() => { cliInterface.close(); reject( new Error( `Took too long to respond (over "${Math.floor(timeoutMs / 1000)}" seconds)`, ), ); }, timeoutMs) : undefined; process.stdout.write(questionToAsk + '\n'); cliInterface.question('', (response) => { if (timeoutId != undefined) { clearTimeout(timeoutId); } cliInterface.close(); resolve(response); }); }); } /** * Options for {@link askQuestionUntilConditionMet}. * * @category Node : Terminal : Util * @category Package : @augment-vir/node * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node) */ export type QuestionUntilConditionMetOptions = { questionToAsk: string; /** Callback to call with the user's response to verify if their response is valid. */ verifyResponseCallback: (response: string) => boolean | Promise<boolean>; invalidInputMessage: string; tryCountMax?: number; } & Partial<AskQuestionOptions>; /** * Asks the user a question in their terminal and then waits for them to type something in response. * The response is submitted once the user inputs a new line. If the response fails validation, the * question is presented again. * * @category Node : Terminal * @category Package : @augment-vir/node * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node) * @see * - {@link askQuestion}: ask a question and accept any response. */ export async function askQuestionUntilConditionMet({ questionToAsk, verifyResponseCallback, invalidInputMessage, tryCountMax = 5, ...options }: QuestionUntilConditionMetOptions): Promise<string> { let wasConditionMet = false; let retryCount = 0; let response = ''; while (!wasConditionMet && retryCount <= tryCountMax) { response = (await askQuestion(questionToAsk, options)).trim(); wasConditionMet = await verifyResponseCallback(response); if (!wasConditionMet) { log.error(invalidInputMessage); } retryCount++; } if (retryCount > tryCountMax) { throw new Error(`Max input attempts (${tryCountMax}) exceeded.`); } return response; }