diff --git a/src/Command.ts b/src/Command.ts index 5ec64687383..bce5dcffbef 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -11,6 +11,7 @@ import { telemetry } from './telemetry.js'; import { accessToken } from './utils/accessToken.js'; import { md } from './utils/md.js'; import { GraphResponseError } from './utils/odata.js'; +import { prompt } from './utils/prompt.js'; interface CommandOption { option: string; @@ -149,7 +150,6 @@ export default abstract class Command { private async validateRequiredOptions(args: CommandArgs, command: CommandInfo): Promise { const shouldPrompt = Cli.getInstance().getSettingWithDefaultValue(settingsNames.prompt, true); - let inquirer: typeof import('inquirer') | undefined; let prompted: boolean = false; for (let i = 0; i < command.options.length; i++) { if (!command.options[i].required || @@ -163,23 +163,21 @@ export default abstract class Command { if (!prompted) { prompted = true; - Cli.log('Provide values for the following parameters:'); + Cli.error('🌶️ Provide values for the following parameters:'); } - if (!inquirer) { - inquirer = await import('inquirer'); - } - - const missingRequireOptionValue = await inquirer.default - .prompt({ - name: 'missingRequireOptionValue', - message: `${command.options[i].name}: ` - }) - .then(result => result.missingRequireOptionValue); + const missingRequireOptionValue = await prompt.forInput<{ missingRequireOptionValue: string }>({ + name: 'missingRequireOptionValue', + message: `${command.options[i].name}: ` + }).then(result => result.missingRequireOptionValue); args.options[command.options[i].name] = missingRequireOptionValue; } + if (prompted) { + Cli.error(''); + } + this.processOptions(args.options); return true; @@ -191,9 +189,7 @@ export default abstract class Command { return true; } - let inquirer: typeof import('inquirer') | undefined; const shouldPrompt = Cli.getInstance().getSettingWithDefaultValue(settingsNames.prompt, true); - const argsOptions: string[] = Object.keys(args.options); for (const optionSet of optionsSets.sort(opt => opt.runsWhen ? 0 : 1)) { @@ -207,7 +203,7 @@ export default abstract class Command { return `Specify one of the following options: ${optionSet.options.join(', ')}.`; } - await this.promptForOptionSetNameAndValue(args, optionSet, inquirer); + await this.promptForOptionSetNameAndValue(args, optionSet); } if (commonOptions.length > 1) { @@ -215,20 +211,17 @@ export default abstract class Command { return `Specify one of the following options: ${optionSet.options.join(', ')}, but not multiple.`; } - await this.promptForSpecificOption(args, commonOptions, inquirer); + await this.promptForSpecificOption(args, commonOptions); } } return true; } - private async promptForOptionSetNameAndValue(args: CommandArgs, optionSet: OptionSet, inquirer?: typeof import('inquirer')): Promise { - if (!inquirer) { - inquirer = await import('inquirer'); - } + private async promptForOptionSetNameAndValue(args: CommandArgs, optionSet: OptionSet): Promise { + Cli.error(`🌶️ Please specify one of the following options:`); - Cli.log(`Please specify one of the following options:`); - const resultOptionName = await inquirer.default.prompt<{ missingRequiredOptionName: string }>({ + const resultOptionName = await prompt.forInput<{ missingRequiredOptionName: string }>({ type: 'list', name: 'missingRequiredOptionName', message: `Option to use:`, @@ -236,24 +229,19 @@ export default abstract class Command { }); const missingRequiredOptionName = resultOptionName.missingRequiredOptionName; - const resultOptionValue = await inquirer.default - .prompt({ - name: 'missingRequiredOptionValue', - message: `${missingRequiredOptionName}:` - }); + const resultOptionValue = await prompt.forInput<{ missingRequiredOptionValue: string }>({ + name: 'missingRequiredOptionValue', + message: `${missingRequiredOptionName}:` + }); args.options[missingRequiredOptionName] = resultOptionValue.missingRequiredOptionValue; - Cli.log(); + Cli.error(''); } - private async promptForSpecificOption(args: CommandArgs, commonOptions: string[], inquirer?: typeof import('inquirer')): Promise { - if (!inquirer) { - inquirer = await import('inquirer'); - } - - Cli.log(`Multiple options for an option set specified. Please specify the correct option that you wish to use.`); + private async promptForSpecificOption(args: CommandArgs, commonOptions: string[]): Promise { + Cli.error(`🌶️ Multiple options for an option set specified. Please specify the correct option that you wish to use.`); - const requiredOptionNameResult = await inquirer.default.prompt<{ missingRequiredOptionName: string }>({ + const requiredOptionNameResult = await prompt.forInput<{ missingRequiredOptionName: string }>({ type: 'list', name: 'missingRequiredOptionName', message: `Option to use:`, @@ -261,7 +249,7 @@ export default abstract class Command { }); commonOptions.filter(y => y !== requiredOptionNameResult.missingRequiredOptionName).map(optionName => args.options[optionName] = undefined); - Cli.log(); + Cli.error(''); } private async validateOutput(args: CommandArgs): Promise { diff --git a/src/cli/Cli.spec.ts b/src/cli/Cli.spec.ts index da1622197aa..e25823dfd9f 100644 --- a/src/cli/Cli.spec.ts +++ b/src/cli/Cli.spec.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import chalk from 'chalk'; import Table from 'easy-table'; import fs from 'fs'; -import inquirer from 'inquirer'; +import { prompt } from '../utils/prompt.js'; import { createRequire } from 'module'; import os from 'os'; import path from 'path'; @@ -283,7 +283,7 @@ describe('Cli', () => { Cli.executeCommand, fs.existsSync, fs.readFileSync, - inquirer.prompt, + prompt.forInput, // eslint-disable-next-line no-console console.log, // eslint-disable-next-line no-console @@ -852,7 +852,7 @@ describe('Cli', () => { }); it(`prompts for required options`, (done) => { - const promptStub: sinon.SinonStub = sinon.stub(inquirer, 'prompt').callsFake(() => Promise.resolve({ missingRequireOptionValue: "test" }) as any); + const promptStub: sinon.SinonStub = sinon.stub(prompt, 'forInput').callsFake(() => Promise.resolve({ missingRequireOptionValue: "test" }) as any); sinon.stub(Cli.getInstance(), 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { return 'true'; @@ -875,7 +875,7 @@ describe('Cli', () => { it(`prompts for optionset name and value when optionset not specified`, async () => { let firstOptionValue = '', secondOptionValue = ''; - const promptStub: sinon.SinonStub = sinon.stub(inquirer, 'prompt').callsFake((opts: any, _) => { + const promptStub: sinon.SinonStub = sinon.stub(prompt, 'forInput').callsFake((opts: any, _) => { if (opts.type === 'list' && opts.name === 'missingRequiredOptionName') { firstOptionValue = opts.choices[0]; secondOptionValue = opts.choices[1]; @@ -904,7 +904,7 @@ describe('Cli', () => { it(`prompts to choose which option you wish to use when multiple options in a specific optionset are specified`, async () => { let firstOptionValue = '', secondOptionValue = ''; - const promptStub: sinon.SinonStub = sinon.stub(inquirer, 'prompt').callsFake((opts: any, _) => { + const promptStub: sinon.SinonStub = sinon.stub(prompt, 'forInput').callsFake((opts: any, _) => { if (opts.type === 'list' && opts.name === 'missingRequiredOptionName') { firstOptionValue = opts.choices[0]; secondOptionValue = opts.choices[1]; @@ -929,7 +929,7 @@ describe('Cli', () => { it(`prompts to choose runsWhen option from optionSet when dependant option is set and prompts for the value`, async () => { let firstOptionValue = '', secondOptionValue = ''; - const promptStub: sinon.SinonStub = sinon.stub(inquirer, 'prompt').callsFake((opts: any, _) => { + const promptStub: sinon.SinonStub = sinon.stub(prompt, 'forInput').callsFake((opts: any, _) => { if (opts.type === 'list' && opts.name === 'missingRequiredOptionName') { firstOptionValue = opts.choices[0]; secondOptionValue = opts.choices[1]; @@ -958,7 +958,7 @@ describe('Cli', () => { it(`prompts to pick one of the options from an optionSet when runsWhen condition is matched`, async () => { let firstOptionValue = '', secondOptionValue = ''; - const promptStub: sinon.SinonStub = sinon.stub(inquirer, 'prompt').callsFake((opts: any, _) => { + const promptStub: sinon.SinonStub = sinon.stub(prompt, 'forInput').callsFake((opts: any, _) => { if (opts.type === 'list' && opts.name === 'missingRequiredOptionName') { firstOptionValue = opts.choices[0]; secondOptionValue = opts.choices[1]; @@ -1100,7 +1100,7 @@ describe('Cli', () => { }); it('calls inquirer when command shows prompt', (done) => { - const promptStub: sinon.SinonStub = sinon.stub(inquirer, 'prompt').callsFake(() => Promise.resolve() as any); + const promptStub: sinon.SinonStub = sinon.stub(prompt, 'forInput').callsFake(() => Promise.resolve() as any); const mockCommandWithPrompt = new MockCommandWithPrompt(); Cli @@ -1249,7 +1249,7 @@ describe('Cli', () => { }); it('calls inquirer when command shows prompt and executed with output', (done) => { - const promptStub: sinon.SinonStub = sinon.stub(inquirer, 'prompt').callsFake(() => Promise.resolve() as any); + const promptStub: sinon.SinonStub = sinon.stub(prompt, 'forInput').callsFake(() => Promise.resolve() as any); const mockCommandWithPrompt = new MockCommandWithPrompt(); Cli @@ -1267,7 +1267,7 @@ describe('Cli', () => { it('calls inquirer when command shows interactive prompt and executed with output', async () => { sinon.stub(Cli.getInstance(), 'getSettingWithDefaultValue').callsFake((() => true)); - const promptStub: sinon.SinonStub = sinon.stub(inquirer, 'prompt').callsFake(() => Promise.resolve({ select: '1' })); + const promptStub: sinon.SinonStub = sinon.stub(prompt, 'forInput').callsFake(() => Promise.resolve({ select: '1' })); const mockCommandWithHandleMultipleResultsFound = new MockCommandWithHandleMultipleResultsFound(); await Cli.executeCommandWithOutput(mockCommandWithHandleMultipleResultsFound, { options: { _: [] } }); diff --git a/src/cli/Cli.ts b/src/cli/Cli.ts index f3b44052acf..c7ba2783736 100644 --- a/src/cli/Cli.ts +++ b/src/cli/Cli.ts @@ -20,6 +20,7 @@ import { validation } from '../utils/validation.js'; import { CommandInfo } from './CommandInfo.js'; import { CommandOptionInfo } from './CommandOptionInfo.js'; import { Logger } from './Logger.js'; +import { prompt } from '../utils/prompt.js'; export interface CommandOutput { stdout: string; @@ -942,7 +943,7 @@ export class Cli { } } - private static async error(message?: any, ...optionalParams: any[]): Promise { + public static async error(message?: any, ...optionalParams: any[]): Promise { const cli = Cli.getInstance(); const spinnerSpinning = cli.spinner.isSpinning; @@ -967,8 +968,6 @@ export class Cli { } public static async prompt(options: any, answers?: any): Promise { - const inquirer = await import('inquirer'); - const cli = Cli.getInstance(); const spinnerSpinning = cli.spinner.isSpinning; @@ -977,7 +976,7 @@ export class Cli { cli.spinner.stop(); } - const response = await inquirer.default.prompt(options, answers) as T; + const response = await prompt.forInput(options, answers) as T; // Restart the spinner if it was running before the prompt /* c8 ignore next 3 */ @@ -998,6 +997,7 @@ export class Cli { type: 'list', name: 'select', default: 0, + prefix: '🌶️ ', message: `${message} Please choose one:`, choices: Object.keys(values) }); diff --git a/src/m365/commands/setup.ts b/src/m365/commands/setup.ts index 3fd21255287..95705c895b8 100644 --- a/src/m365/commands/setup.ts +++ b/src/m365/commands/setup.ts @@ -9,6 +9,7 @@ import { pid } from '../../utils/pid.js'; import AnonymousCommand from '../base/AnonymousCommand.js'; import commands from './commands.js'; import { interactivePreset, powerShellPreset, scriptingPreset } from './setupPresets.js'; +import { prompt } from '../../utils/prompt.js'; interface Preferences { experience?: string; @@ -106,7 +107,7 @@ class SetupCommand extends AnonymousCommand { await logger.logToStderr(`Please, answer the following questions and we'll define a set of settings to best match how you intend to use the CLI.`); await logger.logToStderr(''); - const preferences: Preferences = await Cli.prompt([ + const preferences: Preferences = await prompt.forInput([ { type: 'list', name: 'usageMode', diff --git a/src/utils/prompt.ts b/src/utils/prompt.ts new file mode 100644 index 00000000000..a0e12fb7c96 --- /dev/null +++ b/src/utils/prompt.ts @@ -0,0 +1,19 @@ +import { Cli } from '../cli/Cli.js'; +import { settingsNames } from '../settingsNames.js'; + +let inquirer: typeof import('inquirer') | undefined; + +export const prompt = { + /* c8 ignore next 10 */ + async forInput(config: any, answers?: any): Promise { + if (!inquirer) { + inquirer = await import('inquirer'); + } + + const cli = Cli.getInstance(); + const errorOutput: string = cli.getSettingWithDefaultValue(settingsNames.errorOutput, 'stderr'); + const prompt = inquirer.createPromptModule({ output: errorOutput === 'stderr' ? process.stderr : process.stdout }); + + return await prompt(config, answers) as any; + } +}; \ No newline at end of file