UNPKG

inquirer

Version:

A collection of common interactive command line user interfaces.

237 lines (236 loc) 8.68 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import readline from 'node:readline'; import { defer, EMPTY, from, of, concatMap, filter, reduce, isObservable, lastValueFrom, } from 'rxjs'; import runAsync from 'run-async'; import MuteStream from 'mute-stream'; import ansiEscapes from 'ansi-escapes'; export const _ = { set: (obj, path = '', value) => { let pointer = obj; path.split('.').forEach((key, index, arr) => { if (key === '__proto__' || key === 'constructor') return; if (index === arr.length - 1) { pointer[key] = value; } else if (!(key in pointer) || typeof pointer[key] !== 'object') { pointer[key] = {}; } pointer = pointer[key]; }); }, get: (obj, path = '', defaultValue) => { const travel = (regexp) => String.prototype.split .call(path, regexp) .filter(Boolean) .reduce( // @ts-expect-error implicit any on res[key] (res, key) => (res !== null && res !== undefined ? res[key] : res), obj); const result = travel(/[,[\]]+?/) || travel(/[,.[\]]+?/); return result === undefined || result === obj ? defaultValue : result; }, }; /** * Resolve a question property value if it is passed as a function. * This method will overwrite the property on the question object with the received value. */ function fetchAsyncQuestionProperty(question, prop, answers) { if (prop in question) { const propGetter = question[prop]; if (typeof propGetter === 'function') { return from(runAsync(propGetter)(answers).then((value) => { return Object.assign(question, { [prop]: value }); })); } } return of(question); } class TTYError extends Error { isTtyError = true; } function setupReadlineOptions(opt = {}) { // Inquirer 8.x: // opt.skipTTYChecks = opt.skipTTYChecks === undefined ? opt.input !== undefined : opt.skipTTYChecks; opt.skipTTYChecks = opt.skipTTYChecks === undefined ? true : opt.skipTTYChecks; // Default `input` to stdin const input = opt.input || process.stdin; // Check if prompt is being called in TTY environment // If it isn't return a failed promise // @ts-expect-error: ignore isTTY type error if (!opt.skipTTYChecks && !input.isTTY) { throw new TTYError('Prompts can not be meaningfully rendered in non-TTY environments'); } // Add mute capabilities to the output const ms = new MuteStream(); ms.pipe(opt.output || process.stdout); const output = ms; return { terminal: true, ...opt, input, output, }; } function isQuestionArray(questions) { return Array.isArray(questions); } function isQuestionMap(questions) { return Object.values(questions).every((maybeQuestion) => typeof maybeQuestion === 'object' && !Array.isArray(maybeQuestion) && maybeQuestion != null); } function isPromptConstructor(prompt) { return (prompt.prototype && 'run' in prompt.prototype && typeof prompt.prototype.run === 'function'); } /** * Base interface class other can inherits from */ export default class PromptsRunner { prompts; answers = {}; process = EMPTY; onClose; opt; rl; constructor(prompts, opt) { this.opt = opt; this.prompts = prompts; } run(questions, answers) { // Keep global reference to the answers this.answers = typeof answers === 'object' ? { ...answers } : {}; let obs; if (isQuestionArray(questions)) { obs = from(questions); } else if (isObservable(questions)) { obs = questions; } else if (isQuestionMap(questions)) { // Case: Called with a set of { name: question } obs = from(Object.entries(questions).map(([name, question]) => { return Object.assign({}, question, { name }); })); } else { // Case: Called with a single question config obs = from([questions]); } this.process = obs.pipe(concatMap((question) => this.processQuestion(question))); const promise = lastValueFrom(this.process.pipe(reduce((answersObj, answer) => { _.set(answersObj, answer.name, answer.answer); return answersObj; }, this.answers))).then(() => this.onCompletion(), (error) => this.onError(error)); return Object.assign(promise, { ui: this }); } /** * Once all prompt are over */ onCompletion() { this.close(); return this.answers; } onError(error) { this.close(); return Promise.reject(error); } processQuestion(question) { question = { ...question }; return defer(() => { const obs = of(question); return obs.pipe(concatMap(this.setDefaultType), concatMap(this.filterIfRunnable), concatMap((question) => fetchAsyncQuestionProperty(question, 'message', this.answers)), concatMap((question) => fetchAsyncQuestionProperty(question, 'default', this.answers)), concatMap((question) => fetchAsyncQuestionProperty(question, 'choices', this.answers)), concatMap((question) => { if ('choices' in question) { // @ts-expect-error question type is too loose question.choices = question.choices.map((choice) => { if (typeof choice === 'string') { return { name: choice, value: choice }; } return choice; }); } return of(question); }), concatMap((question) => this.fetchAnswer(question))); }); } fetchAnswer(question) { const prompt = this.prompts[question.type]; if (prompt == null) { throw new Error(`Prompt for type ${question.type} not found`); } return isPromptConstructor(prompt) ? defer(() => { const rl = readline.createInterface(setupReadlineOptions(this.opt)); rl.resume(); const onClose = () => { rl.removeListener('SIGINT', this.onForceClose); rl.setPrompt(''); rl.output.unmute(); rl.output.write(ansiEscapes.cursorShow); rl.output.end(); rl.close(); }; this.onClose = onClose; this.rl = rl; // Make sure new prompt start on a newline when closing process.on('exit', this.onForceClose); rl.on('SIGINT', this.onForceClose); const activePrompt = new prompt(question, rl, this.answers); return from(activePrompt.run().then((answer) => { onClose(); this.onClose = undefined; this.rl = undefined; return { name: question.name, answer }; })); }) : defer(() => from(prompt(question, this.opt).then((answer) => ({ name: question.name, answer, })))); } /** * Handle the ^C exit */ onForceClose = () => { this.close(); process.kill(process.pid, 'SIGINT'); console.log(''); }; /** * Close the interface and cleanup listeners */ close = () => { // Remove events listeners process.removeListener('exit', this.onForceClose); if (typeof this.onClose === 'function') { this.onClose(); } }; setDefaultType = (question) => { // Default type to input if (!this.prompts[question.type]) { question = Object.assign({}, question, { type: 'input' }); } return defer(() => of(question)); }; filterIfRunnable = (question) => { if (question.askAnswered !== true && _.get(this.answers, question.name) !== undefined) { return EMPTY; } const { when } = question; if (when === false) { return EMPTY; } if (typeof when !== 'function') { return of(question); } return defer(() => from(runAsync(when)(this.answers).then((shouldRun) => { if (shouldRun) { return question; } return; })).pipe(filter((val) => val != null))); }; }