diff --git a/src/cli/cli.ts b/src/cli/cli.ts index efd4ea60932..da2fea836b6 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -5,7 +5,7 @@ import os from 'os'; import path from 'path'; import { fileURLToPath, pathToFileURL } from 'url'; import yargs from 'yargs-parser'; -import { ZodError } from 'zod'; +import { ZodCustomIssue, ZodError, ZodIssue } from 'zod'; import Command, { CommandArgs, CommandError } from '../Command.js'; import GlobalOptions from '../GlobalOptions.js'; import config from '../config.js'; @@ -186,14 +186,34 @@ async function execute(rawArgs: string[]): Promise { break; } else { - const hasNonRequiredErrors = result.error.errors.some(e => e.code !== 'invalid_type' || e.received !== 'undefined'); const shouldPrompt = cli.getSettingWithDefaultValue(settingsNames.prompt, true); - if (hasNonRequiredErrors === false && - shouldPrompt) { + if (!shouldPrompt) { + result.error.errors.forEach(e => { + if (e.code === 'invalid_type' && + e.received === 'undefined') { + e.message = `Required option not specified`; + } + }); + return cli.closeWithError(result.error, cli.optionsFromArgs, true); + } + + const missingRequiredValuesErrors: ZodIssue[] = result.error.errors + .filter(e => (e.code === 'invalid_type' && e.received === 'undefined') || + (e.code === 'custom' && e.params?.customCode === 'required')); + const optionSetErrors: ZodCustomIssue[] = result.error.errors + .filter(e => e.code === 'custom' && e.params?.customCode === 'optionSet') as ZodCustomIssue[]; + const otherErrors: ZodIssue[] = result.error.errors + .filter(e => !missingRequiredValuesErrors.includes(e) && !optionSetErrors.includes(e as ZodCustomIssue)); + + if (otherErrors.some(e => e)) { + return cli.closeWithError(result.error, cli.optionsFromArgs, true); + } + + if (missingRequiredValuesErrors.some(e => e)) { await cli.error('🌶️ Provide values for the following parameters:'); - for (const error of result.error.errors) { + for (const error of missingRequiredValuesErrors) { const optionName = error.path.join('.'); const optionInfo = cli.commandToExecute.options.find(o => o.name === optionName); const answer = await cli.promptForValue(optionInfo!); @@ -206,15 +226,14 @@ async function execute(rawArgs: string[]): Promise { return cli.closeWithError(e.message, cli.optionsFromArgs, true); } } + + continue; } - else { - result.error.errors.forEach(e => { - if (e.code === 'invalid_type' && - e.received === 'undefined') { - e.message = `Required option not specified`; - } - }); - return cli.closeWithError(result.error, cli.optionsFromArgs, true); + + if (optionSetErrors.some(e => e)) { + for (const error of optionSetErrors) { + await promptForOptionSetNameAndValue(cli.optionsFromArgs, error.params?.options); + } } } } @@ -1057,6 +1076,16 @@ function shouldTrimOutput(output: string | undefined): boolean { return output === 'text'; } +async function promptForOptionSetNameAndValue(args: CommandArgs, options: string[]): Promise { + await cli.error(`🌶️ Please specify one of the following options:`); + + const selectedOptionName = await prompt.forSelection({ message: `Option to use:`, choices: options.map((choice: any) => { return { name: choice, value: choice }; }) }); + const optionValue = await prompt.forInput({ message: `${selectedOptionName}:` }); + + args.options[selectedOptionName] = optionValue; + await cli.error(''); +} + export const cli = { closeWithError, commands, diff --git a/src/index.ts b/src/index.ts index c1af5314a04..179a7403d4a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,5 +17,14 @@ await (async () => { updateNotifier.default({ pkg: app.packageJson() as any }).notify({ defer: false }); } - await cli.execute(process.argv.slice(2)); + try { + await cli.execute(process.argv.slice(2)); + } + catch (err) { + if (err instanceof Error && err.name === 'ExitPromptError') { + process.exit(1); + } + + cli.closeWithError(err, cli.optionsFromArgs || { options: {} }); + } })(); diff --git a/src/m365/commands/login.ts b/src/m365/commands/login.ts index 60649cce0dc..6d7bb827b4f 100644 --- a/src/m365/commands/login.ts +++ b/src/m365/commands/login.ts @@ -52,19 +52,32 @@ class LoginCommand extends Command { return schema .refine(options => typeof options.appId !== 'undefined' || cli.getClientId() || options.authType === 'identity' || options.authType === 'federatedIdentity', { message: `appId is required. TIP: use the "m365 setup" command to configure the default appId.`, - path: ['appId'] + path: ['appId'], + params: { + customCode: 'required' + } }) .refine(options => options.authType !== 'password' || options.userName, { message: 'Username is required when using password authentication.', - path: ['userName'] + path: ['userName'], + params: { + customCode: 'required' + } }) .refine(options => options.authType !== 'password' || options.password, { message: 'Password is required when using password authentication.', - path: ['password'] + path: ['password'], + params: { + customCode: 'required' + } }) .refine(options => options.authType !== 'certificate' || !(options.certificateFile && options.certificateBase64Encoded), { message: 'Specify either certificateFile or certificateBase64Encoded, but not both.', - path: ['certificateBase64Encoded'] + path: ['certificateBase64Encoded'], + params: { + customCode: 'optionSet', + options: ['certificateFile', 'certificateBase64Encoded'] + } }) .refine(options => options.authType !== 'certificate' || options.certificateFile || @@ -72,13 +85,20 @@ class LoginCommand extends Command { cli.getConfig().get(settingsNames.clientCertificateFile) || cli.getConfig().get(settingsNames.clientCertificateBase64Encoded), { message: 'Specify either certificateFile or certificateBase64Encoded.', - path: ['certificateFile'] + path: ['certificateFile'], + params: { + customCode: 'optionSet', + options: ['certificateFile', 'certificateBase64Encoded'] + } }) .refine(options => options.authType !== 'secret' || options.secret || cli.getConfig().get(settingsNames.clientSecret), { message: 'Secret is required when using secret authentication.', - path: ['secret'] + path: ['secret'], + params: { + customCode: 'required' + } }); } diff --git a/src/utils/prompt.ts b/src/utils/prompt.ts index 56bc9d4e9a7..70177d86397 100644 --- a/src/utils/prompt.ts +++ b/src/utils/prompt.ts @@ -83,14 +83,7 @@ export const prompt = { const errorOutput: string = cli.getSettingWithDefaultValue(settingsNames.errorOutput, 'stderr'); return inquirerInput - .default(config, { output: errorOutput === 'stderr' ? process.stderr : process.stdout }) - .catch(error => { - if (error instanceof Error && error.name === 'ExitPromptError') { - return ''; // noop; handle Ctrl + C - } - - throw error; - }); + .default(config, { output: errorOutput === 'stderr' ? process.stderr : process.stdout }); }, /* c8 ignore next 9 */