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
55 changes: 42 additions & 13 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 { ZodCustomIssue, ZodError, ZodIssue } from 'zod';
import Command, { CommandArgs, CommandError } from '../Command.js';
import GlobalOptions from '../GlobalOptions.js';
import config from '../config.js';
Expand Down Expand Up @@ -186,14 +186,34 @@ async function execute(rawArgs: string[]): Promise<void> {
break;
}
else {
const hasNonRequiredErrors = result.error.errors.some(e => e.code !== 'invalid_type' || e.received !== 'undefined');
const shouldPrompt = cli.getSettingWithDefaultValue<boolean>(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!);
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.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);
}
}
}
}
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 @@
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: {} });

Check failure on line 28 in src/index.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 22, 22)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
}
})();
32 changes: 26 additions & 6 deletions src/m365/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,33 +52,53 @@ 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 ||
options.certificateBase64Encoded ||
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'
}
});
}

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