Skip to content
Draft
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
57 changes: 43 additions & 14 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 z, { ZodError } from 'zod';
import Command, { CommandArgs, CommandError } from '../Command.js';
import GlobalOptions from '../GlobalOptions.js';
import config from '../config.js';
Expand Down Expand Up @@ -186,15 +186,35 @@ async function execute(rawArgs: string[]): Promise<void> {
break;
}
else {
const hasNonRequiredErrors = result.error.issues.some(i => i.code !== 'invalid_type');
const shouldPrompt = cli.getSettingWithDefaultValue<boolean>(settingsNames.prompt, true);

if (hasNonRequiredErrors === false &&
shouldPrompt) {
if (!shouldPrompt) {
result.error.issues.forEach(e => {
if (e.code === 'invalid_type' &&
e.input === 'undefined') {
(e.message as any) = `Required option not specified`;
}
});
return cli.closeWithError(result.error, cli.optionsFromArgs, true);
}

const missingRequiredValuesErrors: z.core.$ZodIssue[] = result.error.issues
.filter(e => (e.code === 'invalid_type' && e.input === 'undefined') ||
(e.code === 'custom' && e.params?.customCode === 'required'));
const optionSetErrors: z.core.$ZodIssueCustom[] = result.error.issues
.filter(e => e.code === 'custom' && e.params?.customCode === 'optionSet') as z.core.$ZodIssueCustom[];
const otherErrors: z.core.$ZodIssue[] = result.error.issues
.filter(e => !missingRequiredValuesErrors.includes(e) && !optionSetErrors.includes(e as z.core.$ZodIssueCustom));

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 issue of result.error.issues) {
const optionName = issue.path.join('.');
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!);
// coerce the answer to the correct type
Expand All @@ -206,15 +226,14 @@ async function execute(rawArgs: string[]): Promise<void> {
return cli.closeWithError(e.message, cli.optionsFromArgs, true);
}
}

continue;
}
else {
result.error.issues.forEach(i => {
if (i.code === 'invalid_type' &&
i.input === undefined) {
(i.message as any) = `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);
}
}
}
}
Expand Down Expand Up @@ -1057,6 +1076,16 @@ function shouldTrimOutput(output: string | undefined): boolean {
return output === 'text';
}

async function promptForOptionSetNameAndValue(args: CommandArgs, options: string[]): Promise<void> {
await cli.error(`🌶️ Please specify one of the following options:`);

const selectedOptionName = await prompt.forSelection<string>({ 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,
Expand Down
11 changes: 10 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {} });
}
})();
32 changes: 26 additions & 6 deletions src/m365/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,33 +51,53 @@ class LoginCommand extends Command {
return schema
.refine(options => typeof options.appId !== 'undefined' || cli.getClientId() || options.authType === 'identity' || options.authType === 'federatedIdentity', {
error: `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, {
error: 'Username is required when using password authentication.',
path: ['userName']
path: ['userName'],
params: {
customCode: 'required'
}
})
.refine(options => options.authType !== 'password' || options.password, {
error: 'Password is required when using password authentication.',
path: ['password']
path: ['password'],
params: {
customCode: 'required'
}
})
.refine(options => options.authType !== 'certificate' || !(options.certificateFile && options.certificateBase64Encoded), {
error: '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 ||
options.certificateBase64Encoded ||
cli.getConfig().get(settingsNames.clientCertificateFile) ||
cli.getConfig().get(settingsNames.clientCertificateBase64Encoded), {
error: '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), {
error: 'Secret is required when using secret authentication.',
path: ['secret']
path: ['secret'],
params: {
customCode: 'required'
}
});
}

Expand Down
9 changes: 1 addition & 8 deletions src/utils/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Loading