Skip to content

Commit

Permalink
Move prompt output to stderr. Closes #5489
Browse files Browse the repository at this point in the history
  • Loading branch information
martinlingstuyl committed Sep 26, 2023
1 parent 6fa2bd1 commit 5b7be83
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 51 deletions.
60 changes: 24 additions & 36 deletions src/Command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -149,7 +150,6 @@ export default abstract class Command {
private async validateRequiredOptions(args: CommandArgs, command: CommandInfo): Promise<string | boolean> {
const shouldPrompt = Cli.getInstance().getSettingWithDefaultValue<boolean>(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 ||
Expand All @@ -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;
Expand All @@ -191,9 +189,7 @@ export default abstract class Command {
return true;
}

let inquirer: typeof import('inquirer') | undefined;
const shouldPrompt = Cli.getInstance().getSettingWithDefaultValue<boolean>(settingsNames.prompt, true);

const argsOptions: string[] = Object.keys(args.options);

for (const optionSet of optionsSets.sort(opt => opt.runsWhen ? 0 : 1)) {
Expand All @@ -207,61 +203,53 @@ 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) {
if (!shouldPrompt) {
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<void> {
if (!inquirer) {
inquirer = await import('inquirer');
}
private async promptForOptionSetNameAndValue(args: CommandArgs, optionSet: OptionSet): Promise<void> {
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:`,
choices: optionSet.options
});
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<void> {
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<void> {
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:`,
choices: commonOptions
});

commonOptions.filter(y => y !== requiredOptionNameResult.missingRequiredOptionName).map(optionName => args.options[optionName] = undefined);
Cli.log();
Cli.error('');
}

private async validateOutput(args: CommandArgs): Promise<string | boolean> {
Expand Down
20 changes: 10 additions & 10 deletions src/cli/Cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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';
Expand All @@ -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];
Expand Down Expand Up @@ -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];
Expand All @@ -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];
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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: { _: [] } });
Expand Down
8 changes: 4 additions & 4 deletions src/cli/Cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -942,7 +943,7 @@ export class Cli {
}
}

private static async error(message?: any, ...optionalParams: any[]): Promise<void> {
public static async error(message?: any, ...optionalParams: any[]): Promise<void> {
const cli = Cli.getInstance();
const spinnerSpinning = cli.spinner.isSpinning;

Expand All @@ -967,8 +968,6 @@ export class Cli {
}

public static async prompt<T>(options: any, answers?: any): Promise<T> {
const inquirer = await import('inquirer');

const cli = Cli.getInstance();
const spinnerSpinning = cli.spinner.isSpinning;

Expand All @@ -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 */
Expand All @@ -998,6 +997,7 @@ export class Cli {
type: 'list',
name: 'select',
default: 0,
prefix: '🌶️ ',
message: `${message} Please choose one:`,
choices: Object.keys(values)
});
Expand Down
3 changes: 2 additions & 1 deletion src/m365/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down
19 changes: 19 additions & 0 deletions src/utils/prompt.ts
Original file line number Diff line number Diff line change
@@ -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<T>(config: any, answers?: any): Promise<T> {
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;
}
};

0 comments on commit 5b7be83

Please sign in to comment.