Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move prompt output to stderr. Closes #5489 #5509

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
};