From ff328b703b25efe56647b0e164e7ae4f29859408 Mon Sep 17 00:00:00 2001 From: waldekmastykarz Date: Sat, 6 Dec 2025 13:44:31 +0100 Subject: [PATCH 1/5] Adds prompting on invalid option sets in Zod-enabled commands. Closes #7060 --- src/cli/cli.spec.ts | 158 +++++++++++++++++- src/cli/cli.ts | 57 +++++-- src/index.ts | 11 +- .../commands/adaptivecard-send.ts | 5 +- .../app/commands/permission/permission-add.ts | 5 +- .../booking/commands/business/business-get.ts | 6 +- src/m365/commands/login.ts | 32 +++- .../administrativeunit-get.ts | 6 +- .../administrativeunit-remove.ts | 12 +- .../commands/organization/organization-set.ts | 17 +- .../roleassignment/roleassignment-add.ts | 12 +- .../roledefinition/roledefinition-get.ts | 12 +- .../roledefinition/roledefinition-remove.ts | 12 +- .../roledefinition/roledefinition-set.ts | 17 +- .../commands/user/user-session-revoke.ts | 6 +- .../approleassignment-add.ts | 72 ++++++-- .../commands/environment/environment-get.ts | 6 +- .../directoryextension-add.ts | 6 +- .../directoryextension-get.ts | 18 +- .../directoryextension-list.ts | 6 +- .../directoryextension-remove.ts | 18 +- .../commands/mail/mail-searchfolder-add.ts | 6 +- .../commands/mailbox/mailbox-settings-get.ts | 6 +- .../commands/mailbox/mailbox-settings-set.ts | 11 +- .../commands/environment/environment-get.ts | 6 +- .../commands/environment/environment-get.ts | 6 +- src/m365/pp/commands/website/website-get.ts | 6 +- .../spe/commands/container/container-add.ts | 6 +- .../container-recyclebinitem-list.ts | 6 +- .../container-recyclebinitem-remove.ts | 12 +- .../container-recyclebinitem-restore.ts | 12 +- .../commands/container/container-remove.ts | 12 +- .../containertype/containertype-get.ts | 6 +- .../containertype/containertype-remove.ts | 6 +- .../spo/commands/file/file-version-keep.ts | 6 +- .../spo/commands/homesite/homesite-add.ts | 6 +- .../spo/commands/homesite/homesite-set.ts | 11 +- .../commands/list/list-defaultvalue-clear.ts | 12 +- .../commands/list/list-defaultvalue-get.ts | 6 +- .../commands/list/list-defaultvalue-list.ts | 6 +- .../commands/list/list-defaultvalue-remove.ts | 6 +- .../commands/list/list-defaultvalue-set.ts | 6 +- src/m365/spo/commands/list/list-view-add.ts | 21 ++- src/m365/spo/commands/page/page-get.ts | 6 +- src/m365/spo/commands/web/web-alert-list.ts | 12 +- .../autofillcolumn/autofillcolumn-set.ts | 12 +- src/m365/spp/commands/model/model-apply.ts | 12 +- .../commands/callrecord/callrecord-list.ts | 6 +- .../engage/engage-community-user-add.ts | 24 ++- .../engage/engage-community-user-list.ts | 12 +- .../engage/engage-community-user-remove.ts | 24 ++- .../engage/engage-role-member-list.ts | 6 +- src/utils/prompt.ts | 9 +- 53 files changed, 676 insertions(+), 122 deletions(-) diff --git a/src/cli/cli.spec.ts b/src/cli/cli.spec.ts index cea76042033..7d5128492ea 100644 --- a/src/cli/cli.spec.ts +++ b/src/cli/cli.spec.ts @@ -297,6 +297,66 @@ class MockCommandWithSchemaAndBoolRequiredOption extends AnonymousCommand { } } +const refinedSchemaOptions = z.strictObject({ + ...globalOptionsZod.shape, + authType: z.string().optional(), + userName: z.string().optional(), + password: z.string().optional(), + certificateFile: z.string().optional(), + certificateBase64Encoded: z.string().optional() +}); + +class MockCommandWithRefinedSchema extends AnonymousCommand { + public get name(): string { + return 'cli mock schema refined'; + } + public get description(): string { + return 'Mock command with refined schema'; + } + public get schema(): z.ZodType { + return refinedSchemaOptions; + } + public getRefinedSchema(schema: typeof refinedSchemaOptions): z.ZodObject | undefined { + return schema + .refine(options => options.authType !== 'password' || options.userName, { + error: 'Username is required when using password authentication.', + path: ['userName'], + params: { + customCode: 'required' + } + }) + .refine(options => options.authType !== 'password' || options.password, { + error: 'Password is required when using password authentication.', + 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'], + params: { + customCode: 'optionSet', + options: ['certificateFile', 'certificateBase64Encoded'] + } + }) + .refine(options => options.authType !== 'certificate' || options.certificateFile || options.certificateBase64Encoded, { + error: 'Specify either certificateFile or certificateBase64Encoded.', + path: ['certificateFile'], + params: { + customCode: 'optionSet', + options: ['certificateFile', 'certificateBase64Encoded'] + } + }) + .refine(options => options.authType !== 'invalid' || false, { + error: 'Invalid authentication type.', + path: ['authType'] + }); + } + public async commandAction(): Promise { + } +} + describe('cli', () => { let rootFolder: string; let cliLogStub: sinon.SinonStub; @@ -313,6 +373,7 @@ describe('cli', () => { let mockCommandWithSchema: Command; let mockCommandWithSchemaAndRequiredOptions: Command; let mockCommandWithSchemaAndBoolRequiredOption: Command; + let mockCommandWithRefinedSchema: Command; let log: string[] = []; let mockCommandWithBooleanRewrite: Command; @@ -337,6 +398,7 @@ describe('cli', () => { mockCommandWithSchema = new MockCommandWithSchema(); mockCommandWithSchemaAndRequiredOptions = new MockCommandWithSchemaAndRequiredOptions(); mockCommandWithSchemaAndBoolRequiredOption = new MockCommandWithSchemaAndBoolRequiredOption(); + mockCommandWithRefinedSchema = new MockCommandWithRefinedSchema(); mockCommandWithOptionSets = new MockCommandWithOptionSets(); mockCommandActionSpy = sinon.spy(mockCommand, 'action'); @@ -359,6 +421,7 @@ describe('cli', () => { cli.getCommandInfo(mockCommandWithSchema, 'cli-schema-mock.js', 'help.mdx'), cli.getCommandInfo(mockCommandWithSchemaAndRequiredOptions, 'cli-schema-mock.js', 'help.mdx'), cli.getCommandInfo(mockCommandWithSchemaAndBoolRequiredOption, 'cli-schema-mock.js', 'help.mdx'), + cli.getCommandInfo(mockCommandWithRefinedSchema, 'cli-schema-refined-mock.js', 'help.mdx'), cli.getCommandInfo(cliCompletionUpdateCommand, 'cli/commands/completion/completion-clink-update.js', 'cli/completion/completion-clink-update.mdx'), cli.getCommandInfo(mockCommandWithBooleanRewrite, 'cli-boolean-rewrite-mock.js', 'help.mdx') ]; @@ -395,6 +458,7 @@ describe('cli', () => { cli.loadAllCommandsInfo, cli.getConfig().get, cli.loadCommandFromFile, + cli.promptForValue, browserUtil.open ]); }); @@ -1154,6 +1218,94 @@ describe('cli', () => { }); }); + it(`prompts for missing required options from refined schema when prompting enabled`, async () => { + cli.commandToExecute = cli.commands.find(c => c.name === 'cli mock schema refined'); + const promptInputStub: sinon.SinonStub = sinon.stub(prompt, 'forInput') + .onFirstCall().resolves('user@contoso.com') + .onSecondCall().resolves('pass@word1'); + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return true; + } + return defaultValue; + }); + const executeCommandSpy = sinon.spy(cli, 'executeCommand'); + + await cli.execute(['cli', 'mock', 'schema', 'refined', '--authType', 'password']); + assert(cliErrorStub.calledWith('🌶️ Provide values for the following parameters:')); + assert.strictEqual(promptInputStub.callCount, 2); + assert(executeCommandSpy.called); + }); + + it(`prompts for option set selection from refined schema when prompting enabled`, async () => { + cli.commandToExecute = cli.commands.find(c => c.name === 'cli mock schema refined'); + const promptSelectionStub: sinon.SinonStub = sinon.stub(prompt, 'forSelection').resolves('certificateFile'); + const promptInputStub: sinon.SinonStub = sinon.stub(prompt, 'forInput').resolves('/path/to/cert.pem'); + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return true; + } + return defaultValue; + }); + const executeCommandSpy = sinon.spy(cli, 'executeCommand'); + + await cli.execute(['cli', 'mock', 'schema', 'refined', '--authType', 'certificate']); + assert(cliErrorStub.calledWith('🌶️ Please specify one of the following options:')); + assert(promptSelectionStub.calledOnce); + assert.deepStrictEqual(promptSelectionStub.firstCall.args[0].choices, [ + { name: 'certificateFile', value: 'certificateFile' }, + { name: 'certificateBase64Encoded', value: 'certificateBase64Encoded' } + ]); + assert(promptInputStub.calledOnce); + assert(executeCommandSpy.called); + }); + + it(`exits with error for non-required/non-optionSet errors in refined schema when prompting enabled`, (done) => { + cli.commandToExecute = cli.commands.find(c => c.name === 'cli mock schema refined'); + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return true; + } + return defaultValue; + }); + const executeCommandSpy = sinon.spy(cli, 'executeCommand'); + + cli + .execute(['cli', 'mock', 'schema', 'refined', '--authType', 'invalid']) + .then(_ => done('Promise fulfilled while error expected'), _ => { + try { + assert(executeCommandSpy.notCalled); + done(); + } + catch (e) { + done(e); + } + }); + }); + + it(`exits with proper error when prompting disabled and refined schema validation fails`, (done) => { + cli.commandToExecute = cli.commands.find(c => c.name === 'cli mock schema refined'); + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + return defaultValue; + }); + const executeCommandSpy = sinon.spy(cli, 'executeCommand'); + + cli + .execute(['cli', 'mock', 'schema', 'refined', '--authType', 'password']) + .then(_ => done('Promise fulfilled while error expected'), _ => { + try { + assert(executeCommandSpy.notCalled); + done(); + } + catch (e) { + done(e); + } + }); + }); + it(`executes command when validation passed`, async () => { cli.commandToExecute = cli.commands.find(c => c.name === 'cli mock'); @@ -1725,7 +1877,7 @@ describe('cli', () => { await cli.loadCommandFromArgs(['spo', 'site', 'list']); cli.printAvailableCommands(); - assert(cliLogStub.calledWith(' cli * 11 commands')); + assert(cliLogStub.calledWith(' cli * 12 commands')); }); it(`prints commands from the specified group`, async () => { @@ -1738,7 +1890,7 @@ describe('cli', () => { }; cli.printAvailableCommands(); - assert(cliLogStub.calledWith(' cli mock * 8 commands')); + assert(cliLogStub.calledWith(' cli mock * 9 commands')); }); it(`prints commands from the root group when the specified string doesn't match any group`, async () => { @@ -1751,7 +1903,7 @@ describe('cli', () => { }; cli.printAvailableCommands(); - assert(cliLogStub.calledWith(' cli * 11 commands')); + assert(cliLogStub.calledWith(' cli * 12 commands')); }); it(`runs properly when context file not found`, async () => { diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 7f310e750c0..6b2af5d0769 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 z, { ZodError } from 'zod'; import Command, { CommandArgs, CommandError } from '../Command.js'; import GlobalOptions from '../GlobalOptions.js'; import config from '../config.js'; @@ -186,15 +186,35 @@ async function execute(rawArgs: string[]): Promise { break; } else { - const hasNonRequiredErrors = result.error.issues.some(i => i.code !== 'invalid_type'); const shouldPrompt = cli.getSettingWithDefaultValue(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 @@ -206,15 +226,14 @@ async function execute(rawArgs: string[]): Promise { 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); + } } } } @@ -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..feccaa6581b 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); + } + + await cli.closeWithError(err, cli.optionsFromArgs || { options: {} }); + } })(); diff --git a/src/m365/adaptivecard/commands/adaptivecard-send.ts b/src/m365/adaptivecard/commands/adaptivecard-send.ts index 4de0b587e35..f93b6a25f73 100644 --- a/src/m365/adaptivecard/commands/adaptivecard-send.ts +++ b/src/m365/adaptivecard/commands/adaptivecard-send.ts @@ -42,7 +42,10 @@ class AdaptiveCardSendCommand extends AnonymousCommand { return schema .refine(options => !options.cardData || options.card, { error: 'When you specify cardData, you must also specify card.', - path: ['cardData'] + path: ['cardData'], + params: { + customCode: 'required' + } }) .refine(options => { if (options.card) { diff --git a/src/m365/app/commands/permission/permission-add.ts b/src/m365/app/commands/permission/permission-add.ts index d233d3eb29b..b7b8907e193 100644 --- a/src/m365/app/commands/permission/permission-add.ts +++ b/src/m365/app/commands/permission/permission-add.ts @@ -49,7 +49,10 @@ class AppPermissionAddCommand extends AppCommand { return schema .refine(options => options.applicationPermissions || options.delegatedPermissions, { error: 'Specify at least one of applicationPermissions or delegatedPermissions, or both.', - path: ['delegatedPermissions'] + path: ['delegatedPermissions'], + params: { + customCode: 'required' + } }); } diff --git a/src/m365/booking/commands/business/business-get.ts b/src/m365/booking/commands/business/business-get.ts index 7bbeff1fb8e..4deef4e2f5c 100644 --- a/src/m365/booking/commands/business/business-get.ts +++ b/src/m365/booking/commands/business/business-get.ts @@ -36,7 +36,11 @@ class BookingBusinessGetCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => options.id || options.name, { - error: 'Specify either id or name' + error: 'Specify either id or name', + params: { + customCode: 'optionSet', + options: ['id', 'name'] + } }); } diff --git a/src/m365/commands/login.ts b/src/m365/commands/login.ts index 41511fd9bf3..50df11bfa46 100644 --- a/src/m365/commands/login.ts +++ b/src/m365/commands/login.ts @@ -55,19 +55,32 @@ 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 || @@ -75,13 +88,20 @@ class LoginCommand extends Command { 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' + } }); } diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-get.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-get.ts index a3b59869bdb..e4133e6d4b3 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-get.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-get.ts @@ -36,7 +36,11 @@ class EntraAdministrativeUnitGetCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.id, options.displayName].filter(Boolean).length === 1, { - error: 'Specify either id or displayName' + error: 'Specify either id or displayName', + params: { + customCode: 'optionSet', + options: ['id', 'displayName'] + } }); } diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-remove.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-remove.ts index 0c5a63c1a60..e14d97ba23c 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-remove.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-remove.ts @@ -35,10 +35,18 @@ class EntraAdministrativeUnitRemoveCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => options.id || options.displayName, { - error: 'Specify either id or displayName' + error: 'Specify either id or displayName', + params: { + customCode: 'optionSet', + options: ['id', 'displayName'] + } }) .refine(options => !(options.id && options.displayName), { - error: 'Specify either id or displayName but not both' + error: 'Specify either id or displayName but not both', + params: { + customCode: 'optionSet', + options: ['id', 'displayName'] + } }); } diff --git a/src/m365/entra/commands/organization/organization-set.ts b/src/m365/entra/commands/organization/organization-set.ts index cb7a75617c7..19b843b20ec 100644 --- a/src/m365/entra/commands/organization/organization-set.ts +++ b/src/m365/entra/commands/organization/organization-set.ts @@ -49,15 +49,26 @@ class EntraOrganizationSetCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => !(options.id && options.displayName), { - error: 'Specify either id or displayName, but not both' + error: 'Specify either id or displayName, but not both', + params: { + customCode: 'optionSet', + options: ['id', 'displayName'] + } }) .refine(options => options.id || options.displayName, { - error: 'Specify either id or displayName' + error: 'Specify either id or displayName', + params: { + customCode: 'optionSet', + options: ['id', 'displayName'] + } }) .refine(options => [ options.contactEmail, options.marketingNotificationEmails, options.securityComplianceNotificationMails, options.securityComplianceNotificationPhones, options.statementUrl, options.technicalNotificationMails].filter(o => o !== undefined).length > 0, { - error: 'Specify at least one of the following options: contactEmail, marketingNotificationEmails, securityComplianceNotificationMails, securityComplianceNotificationPhones, statementUrl, or technicalNotificationMails' + error: 'Specify at least one of the following options: contactEmail, marketingNotificationEmails, securityComplianceNotificationMails, securityComplianceNotificationPhones, statementUrl, or technicalNotificationMails', + params: { + customCode: 'required' + } }); } diff --git a/src/m365/entra/commands/roleassignment/roleassignment-add.ts b/src/m365/entra/commands/roleassignment/roleassignment-add.ts index aecfc48d422..92563d6dc0e 100644 --- a/src/m365/entra/commands/roleassignment/roleassignment-add.ts +++ b/src/m365/entra/commands/roleassignment/roleassignment-add.ts @@ -57,12 +57,20 @@ class EntraRoleAssignmentAddCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.roleDefinitionId, options.roleDefinitionName].filter(o => o !== undefined).length === 1, { - error: 'Specify either roleDefinitionId or roleDefinitionName' + error: 'Specify either roleDefinitionId or roleDefinitionName', + params: { + customCode: 'optionSet', + options: ['roleDefinitionId', 'roleDefinitionName'] + } }) .refine(options => Object.values([ options.userId, options.userName, options.administrativeUnitId, options.administrativeUnitName, options.applicationId, options.applicationObjectId, options.applicationName, options.servicePrincipalId, options.servicePrincipalName, options.groupId, options.groupName]).filter(v => typeof v !== 'undefined').length < 2, { - message: 'Provide value for only one of the following parameters: userId, userName, administrativeUnitId, administrativeUnitName, applicationId, applicationObjectId, applicationName, servicePrincipalId, servicePrincipalName, groupId or groupName' + message: 'Provide value for only one of the following parameters: userId, userName, administrativeUnitId, administrativeUnitName, applicationId, applicationObjectId, applicationName, servicePrincipalId, servicePrincipalName, groupId or groupName', + params: { + customCode: 'optionSet', + options: ['userId', 'userName', 'administrativeUnitId', 'administrativeUnitName', 'applicationId', 'applicationObjectId', 'applicationName', 'servicePrincipalId', 'servicePrincipalName', 'groupId', 'groupName'] + } }); } diff --git a/src/m365/entra/commands/roledefinition/roledefinition-get.ts b/src/m365/entra/commands/roledefinition/roledefinition-get.ts index 7245fd02e84..db09a27096f 100644 --- a/src/m365/entra/commands/roledefinition/roledefinition-get.ts +++ b/src/m365/entra/commands/roledefinition/roledefinition-get.ts @@ -36,10 +36,18 @@ class EntraRoleDefinitionGetCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => !options.id !== !options.displayName, { - error: 'Specify either id or displayName, but not both' + error: 'Specify either id or displayName, but not both', + params: { + customCode: 'optionSet', + options: ['id', 'displayName'] + } }) .refine(options => options.id || options.displayName, { - error: 'Specify either id or displayName' + error: 'Specify either id or displayName', + params: { + customCode: 'optionSet', + options: ['id', 'displayName'] + } }) .refine(options => (!options.id && !options.displayName) || options.displayName || (options.id && validation.isValidGuid(options.id)), { error: e => `The '${e.input}' must be a valid GUID`, diff --git a/src/m365/entra/commands/roledefinition/roledefinition-remove.ts b/src/m365/entra/commands/roledefinition/roledefinition-remove.ts index e1a08117705..4877cfe063a 100644 --- a/src/m365/entra/commands/roledefinition/roledefinition-remove.ts +++ b/src/m365/entra/commands/roledefinition/roledefinition-remove.ts @@ -37,10 +37,18 @@ class EntraRoleDefinitionRemoveCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => !options.id !== !options.displayName, { - error: 'Specify either id or displayName, but not both' + error: 'Specify either id or displayName, but not both', + params: { + customCode: 'optionSet', + options: ['id', 'displayName'] + } }) .refine(options => options.id || options.displayName, { - error: 'Specify either id or displayName' + error: 'Specify either id or displayName', + params: { + customCode: 'optionSet', + options: ['id', 'displayName'] + } }) .refine(options => (!options.id && !options.displayName) || options.displayName || (options.id && validation.isValidGuid(options.id)), { error: e => `The '${e.input}' must be a valid GUID`, diff --git a/src/m365/entra/commands/roledefinition/roledefinition-set.ts b/src/m365/entra/commands/roledefinition/roledefinition-set.ts index 72c0baec0a0..a64aac1ba84 100644 --- a/src/m365/entra/commands/roledefinition/roledefinition-set.ts +++ b/src/m365/entra/commands/roledefinition/roledefinition-set.ts @@ -41,17 +41,28 @@ class EntraRoleDefinitionSetCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => !options.id !== !options.displayName, { - error: 'Specify either id or displayName, but not both' + error: 'Specify either id or displayName, but not both', + params: { + customCode: 'optionSet', + options: ['id', 'displayName'] + } }) .refine(options => options.id || options.displayName, { - error: 'Specify either id or displayName' + error: 'Specify either id or displayName', + params: { + customCode: 'optionSet', + options: ['id', 'displayName'] + } }) .refine(options => (!options.id && !options.displayName) || options.displayName || (options.id && validation.isValidGuid(options.id)), { error: e => `The '${e.input}' must be a valid GUID`, path: ['id'] }) .refine(options => Object.values([options.newDisplayName, options.description, options.allowedResourceActions, options.enabled, options.version]).filter(v => typeof v !== 'undefined').length > 0, { - error: 'Provide value for at least one of the following parameters: newDisplayName, description, allowedResourceActions, enabled or version' + error: 'Provide value for at least one of the following parameters: newDisplayName, description, allowedResourceActions, enabled or version', + params: { + customCode: 'required' + } }); } diff --git a/src/m365/entra/commands/user/user-session-revoke.ts b/src/m365/entra/commands/user/user-session-revoke.ts index 943bbd4f0b1..afa7ef3d8fe 100644 --- a/src/m365/entra/commands/user/user-session-revoke.ts +++ b/src/m365/entra/commands/user/user-session-revoke.ts @@ -36,7 +36,11 @@ class EntraUserSessionRevokeCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.userId, options.userName].filter(o => o !== undefined).length === 1, { - error: `Specify either 'userId' or 'userName'.` + error: `Specify either 'userId' or 'userName'.`, + params: { + customCode: 'optionSet', + options: ['userId', 'userName'] + } }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/exo/commands/approleassignment/approleassignment-add.ts b/src/m365/exo/commands/approleassignment/approleassignment-add.ts index c44988cd491..9603b56337f 100644 --- a/src/m365/exo/commands/approleassignment/approleassignment-add.ts +++ b/src/m365/exo/commands/approleassignment/approleassignment-add.ts @@ -51,20 +51,36 @@ class ExoAppRoleAssignmentAddCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => !options.roleDefinitionId !== !options.roleDefinitionName, { - error: 'Specify either roleDefinitionId or roleDefinitionName, but not both' + error: 'Specify either roleDefinitionId or roleDefinitionName, but not both', + params: { + customCode: 'optionSet', + options: ['roleDefinitionId', 'roleDefinitionName'] + } }) .refine(options => options.roleDefinitionId || options.roleDefinitionName, { - error: 'Specify either roleDefinitionId or roleDefinitionName' + error: 'Specify either roleDefinitionId or roleDefinitionName', + params: { + customCode: 'optionSet', + options: ['roleDefinitionId', 'roleDefinitionName'] + } }) .refine(options => (!options.roleDefinitionId && !options.roleDefinitionName) || options.roleDefinitionName || (options.roleDefinitionId && validation.isValidGuid(options.roleDefinitionId)), { error: e => `The '${e.input}' must be a valid GUID`, path: ['roleDefinitionId'] }) .refine(options => !options.principalId !== !options.principalName, { - error: 'Specify either principalId or principalName, but not both' + error: 'Specify either principalId or principalName, but not both', + params: { + customCode: 'optionSet', + options: ['principalId', 'principalName'] + } }) .refine(options => options.principalId || options.principalName, { - error: 'Specify either principalId or principalName' + error: 'Specify either principalId or principalName', + params: { + customCode: 'optionSet', + options: ['principalId', 'principalName'] + } }) .refine(options => (!options.principalId && !options.principalName) || options.principalName || (options.principalId && validation.isValidGuid(options.principalId)), { error: e => `The '${e.input}' must be a valid GUID`, @@ -80,11 +96,19 @@ class ExoAppRoleAssignmentAddCommand extends GraphCommand { }) .refine(options => options.scope !== 'user' || (!options.userId !== !options.userName), { message: "When the scope is set to 'user' specify either userId or userName, but not both", - path: ['scope'] + path: ['scope'], + params: { + customCode: 'optionSet', + options: ['userId', 'userName'] + } }) .refine(options => options.scope !== 'user' || (options.userId || options.userName), { message: "When the scope is set to 'user' specify either userId or userName", - path: ['scope'] + path: ['scope'], + params: { + customCode: 'optionSet', + options: ['userId', 'userName'] + } }) .refine(options => options.scope !== 'user' || (!options.userId && !options.userName) || options.userName || (options.userId && validation.isValidGuid(options.userId)), { error: e => `The '${e.input}' must be a valid GUID`, @@ -100,11 +124,19 @@ class ExoAppRoleAssignmentAddCommand extends GraphCommand { }) .refine(options => options.scope !== 'group' || (!options.groupId !== !options.groupName), { message: "When the scope is set to 'group' specify either groupId or groupName, but not both", - path: ['scope'] + path: ['scope'], + params: { + customCode: 'optionSet', + options: ['groupId', 'groupName'] + } }) .refine(options => options.scope !== 'group' || (options.groupId || options.groupName), { message: "When the scope is set to 'group' specify either groupId or groupName", - path: ['scope'] + path: ['scope'], + params: { + customCode: 'optionSet', + options: ['groupId', 'groupName'] + } }) .refine(options => options.scope !== 'group' || (!options.groupId && !options.groupName) || options.groupName || (options.groupId && validation.isValidGuid(options.groupId)), { error: e => `The '${e.input}' must be a valid GUID`, @@ -116,11 +148,19 @@ class ExoAppRoleAssignmentAddCommand extends GraphCommand { }) .refine(options => options.scope !== 'administrativeUnit' || (!options.administrativeUnitId !== !options.administrativeUnitName), { message: "When the scope is set to 'administrativeUnit' specify either administrativeUnitId or administrativeUnitName, but not both", - path: ['scope'] + path: ['scope'], + params: { + customCode: 'optionSet', + options: ['administrativeUnitId', 'administrativeUnitName'] + } }) .refine(options => options.scope !== 'administrativeUnit' || (options.administrativeUnitId || options.administrativeUnitName), { message: "When the scope is set to 'administrativeUnit' specify either administrativeUnitId or administrativeUnitName", - path: ['scope'] + path: ['scope'], + params: { + customCode: 'optionSet', + options: ['administrativeUnitId', 'administrativeUnitName'] + } }) .refine(options => options.scope !== 'administrativeUnit' || (!options.administrativeUnitId && !options.administrativeUnitName) || options.administrativeUnitName || (options.administrativeUnitId && validation.isValidGuid(options.administrativeUnitId)), { error: e => `The '${e.input}' must be a valid GUID`, @@ -132,11 +172,19 @@ class ExoAppRoleAssignmentAddCommand extends GraphCommand { }) .refine(options => options.scope !== 'custom' || (!options.customAppScopeId !== !options.customAppScopeName), { message: "When the scope is set to 'custom' specify either customAppScopeId or customAppScopeName, but not both", - path: ['scope'] + path: ['scope'], + params: { + customCode: 'optionSet', + options: ['customAppScopeId', 'customAppScopeName'] + } }) .refine(options => options.scope !== 'custom' || (options.customAppScopeId || options.customAppScopeName), { message: "When the scope is set to 'custom' specify either customAppScopeId or customAppScopeName", - path: ['scope'] + path: ['scope'], + params: { + customCode: 'optionSet', + options: ['customAppScopeId', 'customAppScopeName'] + } }) .refine(options => options.scope !== 'custom' || (!options.customAppScopeId && !options.customAppScopeName) || options.customAppScopeName || (options.customAppScopeId && validation.isValidGuid(options.customAppScopeId)), { error: e => `The '${e.input}' must be a valid GUID`, diff --git a/src/m365/flow/commands/environment/environment-get.ts b/src/m365/flow/commands/environment/environment-get.ts index 45f3d80780b..de7473059d9 100644 --- a/src/m365/flow/commands/environment/environment-get.ts +++ b/src/m365/flow/commands/environment/environment-get.ts @@ -35,7 +35,11 @@ class FlowEnvironmentGetCommand extends PowerAutomateCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => !!options.name !== !!options.default, { - error: `Specify either name or default, but not both.` + error: `Specify either name or default, but not both.`, + params: { + customCode: 'optionSet', + options: ['name', 'default'] + } }); } diff --git a/src/m365/graph/commands/directoryextension/directoryextension-add.ts b/src/m365/graph/commands/directoryextension/directoryextension-add.ts index 6a66e74d4f1..43b1a171f1c 100644 --- a/src/m365/graph/commands/directoryextension/directoryextension-add.ts +++ b/src/m365/graph/commands/directoryextension/directoryextension-add.ts @@ -41,7 +41,11 @@ class GraphDirectoryExtensionAddCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => Object.values([options.appId, options.appObjectId, options.appName]).filter(v => typeof v !== 'undefined').length === 1, { - error: 'Specify either appId, appObjectId or appName, but not multiple' + error: 'Specify either appId, appObjectId or appName, but not multiple', + params: { + customCode: 'optionSet', + options: ['appId', 'appObjectId', 'appName'] + } }) .refine(options => (!options.appId && !options.appObjectId && !options.appName) || options.appObjectId || options.appName || (options.appId && validation.isValidGuid(options.appId)), { diff --git a/src/m365/graph/commands/directoryextension/directoryextension-get.ts b/src/m365/graph/commands/directoryextension/directoryextension-get.ts index d51007d9ecd..d962a3cc5a9 100644 --- a/src/m365/graph/commands/directoryextension/directoryextension-get.ts +++ b/src/m365/graph/commands/directoryextension/directoryextension-get.ts @@ -38,13 +38,25 @@ class GraphDirectoryExtensionGetCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => !options.id !== !options.name, { - error: 'Specify either id or name, but not both' + error: 'Specify either id or name, but not both', + params: { + customCode: 'optionSet', + options: ['id', 'name'] + } }) .refine(options => options.id || options.name, { - error: 'Specify either id or name' + error: 'Specify either id or name', + params: { + customCode: 'optionSet', + options: ['id', 'name'] + } }) .refine(options => Object.values([options.appId, options.appObjectId, options.appName]).filter(v => typeof v !== 'undefined').length === 1, { - error: 'Specify either appId, appObjectId or appName, but not multiple' + error: 'Specify either appId, appObjectId or appName, but not multiple', + params: { + customCode: 'optionSet', + options: ['appId', 'appObjectId', 'appName'] + } }); } diff --git a/src/m365/graph/commands/directoryextension/directoryextension-list.ts b/src/m365/graph/commands/directoryextension/directoryextension-list.ts index c8d48b3f3ac..ab9560b077c 100644 --- a/src/m365/graph/commands/directoryextension/directoryextension-list.ts +++ b/src/m365/graph/commands/directoryextension/directoryextension-list.ts @@ -42,7 +42,11 @@ class GraphDirectoryExtensionListCommand extends GraphCommand { return schema .refine(options => ([options.appId, options.appObjectId, options.appName].filter(x => x !== undefined).length <= 1), { - error: 'Specify either appId, appObjectId, or appName, but not multiple.' + error: 'Specify either appId, appObjectId, or appName, but not multiple.', + params: { + customCode: 'optionSet', + options: ['appId', 'appObjectId', 'appName'] + } }); } diff --git a/src/m365/graph/commands/directoryextension/directoryextension-remove.ts b/src/m365/graph/commands/directoryextension/directoryextension-remove.ts index 0570060d1d7..155e0bd7788 100644 --- a/src/m365/graph/commands/directoryextension/directoryextension-remove.ts +++ b/src/m365/graph/commands/directoryextension/directoryextension-remove.ts @@ -39,13 +39,25 @@ class GraphDirectoryExtensionRemoveCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => !options.id !== !options.name, { - error: 'Specify either id or name, but not both' + error: 'Specify either id or name, but not both', + params: { + customCode: 'optionSet', + options: ['id', 'name'] + } }) .refine(options => options.id || options.name, { - error: 'Specify either id or name' + error: 'Specify either id or name', + params: { + customCode: 'optionSet', + options: ['id', 'name'] + } }) .refine(options => Object.values([options.appId, options.appObjectId, options.appName]).filter(v => typeof v !== 'undefined').length === 1, { - error: 'Specify either appId, appObjectId or appName, but not multiple' + error: 'Specify either appId, appObjectId or appName, but not multiple', + params: { + customCode: 'optionSet', + options: ['appId', 'appObjectId', 'appName'] + } }); } diff --git a/src/m365/outlook/commands/mail/mail-searchfolder-add.ts b/src/m365/outlook/commands/mail/mail-searchfolder-add.ts index d84a1e78a15..e49bb0fe41c 100644 --- a/src/m365/outlook/commands/mail/mail-searchfolder-add.ts +++ b/src/m365/outlook/commands/mail/mail-searchfolder-add.ts @@ -44,7 +44,11 @@ class OutlookMailSearchFolderAddCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => !(options.userId && options.userName), { - error: 'Specify either userId or userName, but not both' + error: 'Specify either userId or userName, but not both', + params: { + customCode: 'optionSet', + options: ['userId', 'userName'] + } }); } diff --git a/src/m365/outlook/commands/mailbox/mailbox-settings-get.ts b/src/m365/outlook/commands/mailbox/mailbox-settings-get.ts index 9fa3388ca2a..13c4fbc8050 100644 --- a/src/m365/outlook/commands/mailbox/mailbox-settings-get.ts +++ b/src/m365/outlook/commands/mailbox/mailbox-settings-get.ts @@ -39,7 +39,11 @@ class OutlookMailboxSettingsGetCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => !(options.userId && options.userName), { - error: 'Specify either userId or userName, but not both' + error: 'Specify either userId or userName, but not both', + params: { + customCode: 'optionSet', + options: ['userId', 'userName'] + } }); } diff --git a/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts b/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts index e0093c80d72..a25cdaea3c3 100644 --- a/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts +++ b/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts @@ -58,14 +58,21 @@ class OutlookMailboxSettingsSetCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => !(options.userId && options.userName), { - error: 'Specify either userId or userName, but not both' + error: 'Specify either userId or userName, but not both', + params: { + customCode: 'optionSet', + options: ['userId', 'userName'] + } }) .refine(options => [ options.workingDays, options.workingHoursStartTime, options.workingHoursEndTime, options.workingHoursTimeZone, options.autoReplyStatus, options.autoReplyExternalAudience, options.autoReplyExternalMessage, options.autoReplyInternalMessage, options.autoReplyStartDateTime, options.autoReplyStartTimeZone, options.autoReplyEndDateTime, options.autoReplyEndTimeZone, options.timeFormat, options.timeZone, options.dateFormat, options.delegateMeetingMessageDeliveryOptions, options.language].filter(o => o !== undefined).length > 0, { - error: 'Specify at least one of the following options: workingDays, workingHoursStartTime, workingHoursEndTime, workingHoursTimeZone, autoReplyStatus, autoReplyExternalAudience, autoReplyExternalMessage, autoReplyInternalMessage, autoReplyStartDateTime, autoReplyStartTimeZone, autoReplyEndDateTime, autoReplyEndTimeZone, timeFormat, timeZone, dateFormat, delegateMeetingMessageDeliveryOptions, or language' + error: 'Specify at least one of the following options: workingDays, workingHoursStartTime, workingHoursEndTime, workingHoursTimeZone, autoReplyStatus, autoReplyExternalAudience, autoReplyExternalMessage, autoReplyInternalMessage, autoReplyStartDateTime, autoReplyStartTimeZone, autoReplyEndDateTime, autoReplyEndTimeZone, timeFormat, timeZone, dateFormat, delegateMeetingMessageDeliveryOptions, or language', + params: { + customCode: 'required' + } }); } diff --git a/src/m365/pa/commands/environment/environment-get.ts b/src/m365/pa/commands/environment/environment-get.ts index 83ae4a497a5..d0bef532601 100644 --- a/src/m365/pa/commands/environment/environment-get.ts +++ b/src/m365/pa/commands/environment/environment-get.ts @@ -34,7 +34,11 @@ class PaEnvironmentGetCommand extends PowerAppsCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => !!options.name !== !!options.default, { - error: `Specify either name or default, but not both.` + error: `Specify either name or default, but not both.`, + params: { + customCode: 'optionSet', + options: ['name', 'default'] + } }); } diff --git a/src/m365/pp/commands/environment/environment-get.ts b/src/m365/pp/commands/environment/environment-get.ts index 9ad4b7d198c..60859e2ad2e 100644 --- a/src/m365/pp/commands/environment/environment-get.ts +++ b/src/m365/pp/commands/environment/environment-get.ts @@ -35,7 +35,11 @@ class PpEnvironmentGetCommand extends PowerPlatformCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => !!options.name !== !!options.default, { - error: `Specify either name or default, but not both.` + error: `Specify either name or default, but not both.`, + params: { + customCode: 'optionSet', + options: ['name', 'default'] + } }); } diff --git a/src/m365/pp/commands/website/website-get.ts b/src/m365/pp/commands/website/website-get.ts index 7ac25e1f2a9..b5259c1cdef 100644 --- a/src/m365/pp/commands/website/website-get.ts +++ b/src/m365/pp/commands/website/website-get.ts @@ -39,7 +39,11 @@ class PpWebSiteGetCommand extends PowerPlatformCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.url, options.id, options.name].filter(x => x !== undefined).length === 1, { - error: `Specify either url, id or name, but not multiple.` + error: `Specify either url, id or name, but not multiple.`, + params: { + customCode: 'optionSet', + options: ['url', 'id', 'name'] + } }); } diff --git a/src/m365/spe/commands/container/container-add.ts b/src/m365/spe/commands/container/container-add.ts index abf38e6f8f0..e7ba736404f 100644 --- a/src/m365/spe/commands/container/container-add.ts +++ b/src/m365/spe/commands/container/container-add.ts @@ -43,7 +43,11 @@ class SpeContainerAddCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine((options: Options) => [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options: containerTypeId or containerTypeName.' + error: 'Use one of the following options: containerTypeId or containerTypeName.', + params: { + customCode: 'optionSet', + options: ['containerTypeId', 'containerTypeName'] + } }); } diff --git a/src/m365/spe/commands/container/container-recyclebinitem-list.ts b/src/m365/spe/commands/container/container-recyclebinitem-list.ts index fd15ea3ccf5..d834498ba1b 100644 --- a/src/m365/spe/commands/container/container-recyclebinitem-list.ts +++ b/src/m365/spe/commands/container/container-recyclebinitem-list.ts @@ -38,7 +38,11 @@ class SpeContainerRecycleBinItemListCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine((options: Options) => [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options: containerTypeId or containerTypeName.' + error: 'Use one of the following options: containerTypeId or containerTypeName.', + params: { + customCode: 'optionSet', + options: ['containerTypeId', 'containerTypeName'] + } }); } diff --git a/src/m365/spe/commands/container/container-recyclebinitem-remove.ts b/src/m365/spe/commands/container/container-recyclebinitem-remove.ts index 24ce935370c..e2e0151bdf8 100644 --- a/src/m365/spe/commands/container/container-recyclebinitem-remove.ts +++ b/src/m365/spe/commands/container/container-recyclebinitem-remove.ts @@ -40,10 +40,18 @@ class SpeContainerRecycleBinItemRemoveCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine((options: Options) => [options.id, options.name].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options: id or name.' + error: 'Use one of the following options: id or name.', + params: { + customCode: 'optionSet', + options: ['id', 'name'] + } }) .refine((options: Options) => !options.name || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options when specifying the container name: containerTypeId or containerTypeName.' + error: 'Use one of the following options when specifying the container name: containerTypeId or containerTypeName.', + params: { + customCode: 'optionSet', + options: ['containerTypeId', 'containerTypeName'] + } }) .refine((options: Options) => options.name || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 0, { error: 'Options containerTypeId and containerTypeName are only required when removing a container by name.' diff --git a/src/m365/spe/commands/container/container-recyclebinitem-restore.ts b/src/m365/spe/commands/container/container-recyclebinitem-restore.ts index 79484bfd62c..ec4bf469a52 100644 --- a/src/m365/spe/commands/container/container-recyclebinitem-restore.ts +++ b/src/m365/spe/commands/container/container-recyclebinitem-restore.ts @@ -39,10 +39,18 @@ class SpeContainerRecycleBinItemRestoreCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine((options: Options) => [options.id, options.name].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options: id or name.' + error: 'Use one of the following options: id or name.', + params: { + customCode: 'optionSet', + options: ['id', 'name'] + } }) .refine((options: Options) => !options.name || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options when specifying the container name: containerTypeId or containerTypeName.' + error: 'Use one of the following options when specifying the container name: containerTypeId or containerTypeName.', + params: { + customCode: 'optionSet', + options: ['containerTypeId', 'containerTypeName'] + } }) .refine((options: Options) => options.name || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 0, { error: 'Options containerTypeId and containerTypeName are only required when restoring a container by name.' diff --git a/src/m365/spe/commands/container/container-remove.ts b/src/m365/spe/commands/container/container-remove.ts index 99483355327..56da86c51ab 100644 --- a/src/m365/spe/commands/container/container-remove.ts +++ b/src/m365/spe/commands/container/container-remove.ts @@ -39,10 +39,18 @@ class SpeContainerRemoveCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine((options: Options) => [options.id, options.name].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options: id or name.' + error: 'Use one of the following options: id or name.', + params: { + customCode: 'optionSet', + options: ['id', 'name'] + } }) .refine((options: Options) => !options.name || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options when specifying the container name: containerTypeId or containerTypeName.' + error: 'Use one of the following options when specifying the container name: containerTypeId or containerTypeName.', + params: { + customCode: 'optionSet', + options: ['containerTypeId', 'containerTypeName'] + } }) .refine((options: Options) => options.name || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 0, { error: 'Options containerTypeId and containerTypeName are only required when deleting a container by name.' diff --git a/src/m365/spe/commands/containertype/containertype-get.ts b/src/m365/spe/commands/containertype/containertype-get.ts index 2932701a05e..85bd4762ef0 100644 --- a/src/m365/spe/commands/containertype/containertype-get.ts +++ b/src/m365/spe/commands/containertype/containertype-get.ts @@ -37,7 +37,11 @@ class SpeContainerTypeGetCommand extends GraphDelegatedCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.id, options.name].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options: id or name.' + error: 'Use one of the following options: id or name.', + params: { + customCode: 'optionSet', + options: ['id', 'name'] + } }); } diff --git a/src/m365/spe/commands/containertype/containertype-remove.ts b/src/m365/spe/commands/containertype/containertype-remove.ts index 9a8b159228d..369eb17fefc 100644 --- a/src/m365/spe/commands/containertype/containertype-remove.ts +++ b/src/m365/spe/commands/containertype/containertype-remove.ts @@ -44,7 +44,11 @@ class SpeContainerTypeRemoveCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.id, options.name].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options: id, name.' + error: 'Use one of the following options: id, name.', + params: { + customCode: 'optionSet', + options: ['id', 'name'] + } }); } diff --git a/src/m365/spo/commands/file/file-version-keep.ts b/src/m365/spo/commands/file/file-version-keep.ts index 2042c1c252e..7f1e155808c 100644 --- a/src/m365/spo/commands/file/file-version-keep.ts +++ b/src/m365/spo/commands/file/file-version-keep.ts @@ -43,7 +43,11 @@ class SpoFileVersionKeepCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.fileUrl, options.fileId].filter(o => o !== undefined).length === 1, { - error: `Specify 'fileUrl' or 'fileId', but not both.` + error: `Specify 'fileUrl' or 'fileId', but not both.`, + params: { + customCode: 'optionSet', + options: ['fileUrl', 'fileId'] + } }); } diff --git a/src/m365/spo/commands/homesite/homesite-add.ts b/src/m365/spo/commands/homesite/homesite-add.ts index 331821bfcbb..d10d4f61ec0 100644 --- a/src/m365/spo/commands/homesite/homesite-add.ts +++ b/src/m365/spo/commands/homesite/homesite-add.ts @@ -50,7 +50,11 @@ class SpoHomeSiteAddCommand extends SpoCommand { .refine( (options: Options) => [options.audienceIds, options.audienceNames].filter(o => o !== undefined).length <= 1, { - message: 'You must specify either audienceIds or audienceNames but not both.' + message: 'You must specify either audienceIds or audienceNames but not both.', + params: { + customCode: 'optionSet', + options: ['audienceIds', 'audienceNames'] + } } ); } diff --git a/src/m365/spo/commands/homesite/homesite-set.ts b/src/m365/spo/commands/homesite/homesite-set.ts index 8968f555c84..f086ff7f2a0 100644 --- a/src/m365/spo/commands/homesite/homesite-set.ts +++ b/src/m365/spo/commands/homesite/homesite-set.ts @@ -53,7 +53,11 @@ class SpoHomeSiteSetCommand extends SpoCommand { .refine( (options: Options) => [options.audienceIds, options.audienceNames].filter(o => o !== undefined).length <= 1, { - message: 'You must specify either audienceIds or audienceNames but not both.' + message: 'You must specify either audienceIds or audienceNames but not both.', + params: { + customCode: 'optionSet', + options: ['audienceIds', 'audienceNames'] + } } ) .refine( @@ -65,7 +69,10 @@ class SpoHomeSiteSetCommand extends SpoCommand { options.targetedLicenseType !== undefined || options.order !== undefined, { - message: 'You must specify at least one option to configure.' + message: 'You must specify at least one option to configure.', + params: { + customCode: 'required' + } } ); } diff --git a/src/m365/spo/commands/list/list-defaultvalue-clear.ts b/src/m365/spo/commands/list/list-defaultvalue-clear.ts index 30e98e7dc28..c1f3d9c87cc 100644 --- a/src/m365/spo/commands/list/list-defaultvalue-clear.ts +++ b/src/m365/spo/commands/list/list-defaultvalue-clear.ts @@ -47,10 +47,18 @@ class SpoListDefaultValueClearCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.listId, options.listTitle, options.listUrl].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options: listId, listTitle, listUrl.' + error: 'Use one of the following options: listId, listTitle, listUrl.', + params: { + customCode: 'optionSet', + options: ['listId', 'listTitle', 'listUrl'] + } }) .refine(options => (options.fieldName !== undefined) !== (options.folderUrl !== undefined) || (options.fieldName === undefined && options.folderUrl === undefined), { - error: `Specify 'fieldName' or 'folderUrl', but not both.` + error: `Specify 'fieldName' or 'folderUrl', but not both.`, + params: { + customCode: 'optionSet', + options: ['fieldName', 'folderUrl'] + } }); } diff --git a/src/m365/spo/commands/list/list-defaultvalue-get.ts b/src/m365/spo/commands/list/list-defaultvalue-get.ts index 0617afd4d07..3bae92155e2 100644 --- a/src/m365/spo/commands/list/list-defaultvalue-get.ts +++ b/src/m365/spo/commands/list/list-defaultvalue-get.ts @@ -46,7 +46,11 @@ class SpoListDefaultValueGetCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.listId, options.listTitle, options.listUrl].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options: listId, listTitle, listUrl.' + error: 'Use one of the following options: listId, listTitle, listUrl.', + params: { + customCode: 'optionSet', + options: ['listId', 'listTitle', 'listUrl'] + } }); } diff --git a/src/m365/spo/commands/list/list-defaultvalue-list.ts b/src/m365/spo/commands/list/list-defaultvalue-list.ts index 40f77ea48d9..724d993576a 100644 --- a/src/m365/spo/commands/list/list-defaultvalue-list.ts +++ b/src/m365/spo/commands/list/list-defaultvalue-list.ts @@ -44,7 +44,11 @@ class SpoListDefaultValueListCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.listId, options.listTitle, options.listUrl].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options: listId, listTitle, listUrl.' + error: 'Use one of the following options: listId, listTitle, listUrl.', + params: { + customCode: 'optionSet', + options: ['listId', 'listTitle', 'listUrl'] + } }); } diff --git a/src/m365/spo/commands/list/list-defaultvalue-remove.ts b/src/m365/spo/commands/list/list-defaultvalue-remove.ts index 77a32a0360f..2270826a029 100644 --- a/src/m365/spo/commands/list/list-defaultvalue-remove.ts +++ b/src/m365/spo/commands/list/list-defaultvalue-remove.ts @@ -52,7 +52,11 @@ class SpoListDefaultValueRemoveCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.listId, options.listTitle, options.listUrl].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options: listId, listTitle, listUrl.' + error: 'Use one of the following options: listId, listTitle, listUrl.', + params: { + customCode: 'optionSet', + options: ['listId', 'listTitle', 'listUrl'] + } }); } diff --git a/src/m365/spo/commands/list/list-defaultvalue-set.ts b/src/m365/spo/commands/list/list-defaultvalue-set.ts index f4ac8997e25..7edb7098e05 100644 --- a/src/m365/spo/commands/list/list-defaultvalue-set.ts +++ b/src/m365/spo/commands/list/list-defaultvalue-set.ts @@ -48,7 +48,11 @@ class SpoListDefaultValueSetCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.listId, options.listTitle, options.listUrl].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options: listId, listTitle, listUrl.' + error: 'Use one of the following options: listId, listTitle, listUrl.', + params: { + customCode: 'optionSet', + options: ['listId', 'listTitle', 'listUrl'] + } }); } diff --git a/src/m365/spo/commands/list/list-view-add.ts b/src/m365/spo/commands/list/list-view-add.ts index d50f114b81b..d655dfbf865 100644 --- a/src/m365/spo/commands/list/list-view-add.ts +++ b/src/m365/spo/commands/list/list-view-add.ts @@ -69,25 +69,38 @@ class SpoListViewAddCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine((options: Options) => [options.listId, options.listTitle, options.listUrl].filter(o => o !== undefined).length === 1, { - error: 'Use one of the following options: listId, listTitle, or listUrl.' + error: 'Use one of the following options: listId, listTitle, or listUrl.', + params: { + customCode: 'optionSet', + options: ['listId', 'listTitle', 'listUrl'] + } }) .refine((options: Options) => !options.personal || !options.default, { error: 'Default view cannot be a personal view.' }) .refine((options: Options) => options.type !== 'calendar' || [options.calendarStartDateField, options.calendarEndDateField, options.calendarTitleField].filter(o => o === undefined).length === 0, { - error: 'When type is calendar, do specify calendarStartDateField, calendarEndDateField, and calendarTitleField.' + error: 'When type is calendar, do specify calendarStartDateField, calendarEndDateField, and calendarTitleField.', + params: { + customCode: 'required' + } }) .refine((options: Options) => options.type === 'calendar' || [options.calendarStartDateField, options.calendarEndDateField, options.calendarTitleField].filter(o => o === undefined).length === 3, { error: 'When type is not calendar, do not specify calendarStartDateField, calendarEndDateField, and calendarTitleField.' }) .refine((options: Options) => options.type !== 'kanban' || options.kanbanBucketField !== undefined, { - error: 'When type is kanban, do specify kanbanBucketField.' + error: 'When type is kanban, do specify kanbanBucketField.', + params: { + customCode: 'required' + } }) .refine((options: Options) => options.type === 'kanban' || options.kanbanBucketField === undefined, { error: 'When type is not kanban, do not specify kanbanBucketField.' }) .refine((options: Options) => options.type === 'calendar' || options.fields !== undefined, { - error: 'When type is not calendar, do specify fields.' + error: 'When type is not calendar, do specify fields.', + params: { + customCode: 'required' + } }); } diff --git a/src/m365/spo/commands/page/page-get.ts b/src/m365/spo/commands/page/page-get.ts index 018c46cb6d3..bc8c2d6d0cc 100644 --- a/src/m365/spo/commands/page/page-get.ts +++ b/src/m365/spo/commands/page/page-get.ts @@ -41,7 +41,11 @@ class SpoPageGetCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.name, options.default].filter(x => x !== undefined).length === 1, { - error: `Specify either name or default, but not both.` + error: `Specify either name or default, but not both.`, + params: { + customCode: 'optionSet', + options: ['name', 'default'] + } }); } diff --git a/src/m365/spo/commands/web/web-alert-list.ts b/src/m365/spo/commands/web/web-alert-list.ts index e5a98ae2168..76980abfdda 100644 --- a/src/m365/spo/commands/web/web-alert-list.ts +++ b/src/m365/spo/commands/web/web-alert-list.ts @@ -51,10 +51,18 @@ class SpoWebAlertListCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.listId, options.listUrl, options.listTitle].filter(x => x !== undefined).length <= 1, { - error: `Specify either listId, listUrl, or listTitle, but not more than one.` + error: `Specify either listId, listUrl, or listTitle, but not more than one.`, + params: { + customCode: 'optionSet', + options: ['listId', 'listUrl', 'listTitle'] + } }) .refine(options => [options.userName, options.userId].filter(x => x !== undefined).length <= 1, { - error: `Specify either userName or userId, but not both.` + error: `Specify either userName or userId, but not both.`, + params: { + customCode: 'optionSet', + options: ['userName', 'userId'] + } }); } diff --git a/src/m365/spp/commands/autofillcolumn/autofillcolumn-set.ts b/src/m365/spp/commands/autofillcolumn/autofillcolumn-set.ts index ee3a0ba12ff..da4a43c0c31 100644 --- a/src/m365/spp/commands/autofillcolumn/autofillcolumn-set.ts +++ b/src/m365/spp/commands/autofillcolumn/autofillcolumn-set.ts @@ -83,10 +83,18 @@ class SppAutofillColumnSetCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.columnId, options.columnTitle, options.columnInternalName].filter(Boolean).length === 1, { - message: `Specify exactly one of the following options: 'columnId', 'columnTitle' or 'columnInternalName'.` + message: `Specify exactly one of the following options: 'columnId', 'columnTitle' or 'columnInternalName'.`, + params: { + customCode: 'optionSet', + options: ['columnId', 'columnTitle', 'columnInternalName'] + } }) .refine(options => [options.listTitle, options.listId, options.listUrl].filter(Boolean).length === 1, { - message: `Specify exactly one of the following options: 'listTitle', 'listId' or 'listUrl'.` + message: `Specify exactly one of the following options: 'listTitle', 'listId' or 'listUrl'.`, + params: { + customCode: 'optionSet', + options: ['listTitle', 'listId', 'listUrl'] + } }); } diff --git a/src/m365/spp/commands/model/model-apply.ts b/src/m365/spp/commands/model/model-apply.ts index f490cc29903..aa7d065e4cf 100644 --- a/src/m365/spp/commands/model/model-apply.ts +++ b/src/m365/spp/commands/model/model-apply.ts @@ -52,10 +52,18 @@ class SppModelApplyCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.id, options.title].filter(x => x !== undefined).length === 1, { - error: `Specify exactly one of the following options: 'id' or 'title'.` + error: `Specify exactly one of the following options: 'id' or 'title'.`, + params: { + customCode: 'optionSet', + options: ['id', 'title'] + } }) .refine(options => [options.listTitle, options.listId, options.listUrl].filter(x => x !== undefined).length === 1, { - error: `Specify exactly one of the following options: 'listTitle', 'listId' or 'listUrl'.` + error: `Specify exactly one of the following options: 'listTitle', 'listId' or 'listUrl'.`, + params: { + customCode: 'optionSet', + options: ['listTitle', 'listId', 'listUrl'] + } }); } diff --git a/src/m365/teams/commands/callrecord/callrecord-list.ts b/src/m365/teams/commands/callrecord/callrecord-list.ts index 91b970726f4..166958f642b 100644 --- a/src/m365/teams/commands/callrecord/callrecord-list.ts +++ b/src/m365/teams/commands/callrecord/callrecord-list.ts @@ -69,7 +69,11 @@ class TeamsCallRecordListCommand extends GraphApplicationCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine((options: Options) => [options.userId, options.userName].filter(o => o !== undefined).length <= 1, { - error: 'Use one of the following options: userId or userName but not both.' + error: 'Use one of the following options: userId or userName but not both.', + params: { + customCode: 'optionSet', + options: ['userId', 'userName'] + } }) .refine((options: Options) => [options.startDateTime, options.endDateTime].filter(o => o !== undefined).length <= 1 || new Date(options.startDateTime!) < new Date(options.endDateTime!), { message: 'Value of startDateTime, must be before endDateTime.' diff --git a/src/m365/viva/commands/engage/engage-community-user-add.ts b/src/m365/viva/commands/engage/engage-community-user-add.ts index 4e48636f214..e5e95e51981 100644 --- a/src/m365/viva/commands/engage/engage-community-user-add.ts +++ b/src/m365/viva/commands/engage/engage-community-user-add.ts @@ -47,16 +47,32 @@ class VivaEngageCommunityUserAddCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.communityId, options.communityDisplayName, options.entraGroupId].filter(x => x !== undefined).length === 1, { - error: 'Specify either communityId, communityDisplayName, or entraGroupId, but not multiple.' + error: 'Specify either communityId, communityDisplayName, or entraGroupId, but not multiple.', + params: { + customCode: 'optionSet', + options: ['communityId', 'communityDisplayName', 'entraGroupId'] + } }) .refine(options => options.communityId || options.communityDisplayName || options.entraGroupId, { - error: 'Specify at least one of communityId, communityDisplayName, or entraGroupId.' + error: 'Specify at least one of communityId, communityDisplayName, or entraGroupId.', + params: { + customCode: 'optionSet', + options: ['communityId', 'communityDisplayName', 'entraGroupId'] + } }) .refine(options => options.ids || options.userNames, { - error: 'Specify either of ids or userNames.' + error: 'Specify either of ids or userNames.', + params: { + customCode: 'optionSet', + options: ['ids', 'userNames'] + } }) .refine(options => typeof options.userNames !== 'undefined' || typeof options.ids !== 'undefined', { - error: 'Specify either ids or userNames, but not both.' + error: 'Specify either ids or userNames, but not both.', + params: { + customCode: 'optionSet', + options: ['ids', 'userNames'] + } }); } diff --git a/src/m365/viva/commands/engage/engage-community-user-list.ts b/src/m365/viva/commands/engage/engage-community-user-list.ts index 752d3261011..22070aa70c3 100644 --- a/src/m365/viva/commands/engage/engage-community-user-list.ts +++ b/src/m365/viva/commands/engage/engage-community-user-list.ts @@ -42,10 +42,18 @@ class VivaEngageCommunityUserListCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.communityId, options.communityDisplayName, options.entraGroupId].filter(x => x !== undefined).length === 1, { - error: 'Specify either communityId, communityDisplayName, or entraGroupId, but not multiple.' + error: 'Specify either communityId, communityDisplayName, or entraGroupId, but not multiple.', + params: { + customCode: 'optionSet', + options: ['communityId', 'communityDisplayName', 'entraGroupId'] + } }) .refine(options => options.communityId || options.communityDisplayName || options.entraGroupId, { - error: 'Specify at least one of communityId, communityDisplayName, or entraGroupId.' + error: 'Specify at least one of communityId, communityDisplayName, or entraGroupId.', + params: { + customCode: 'optionSet', + options: ['communityId', 'communityDisplayName', 'entraGroupId'] + } }); } diff --git a/src/m365/viva/commands/engage/engage-community-user-remove.ts b/src/m365/viva/commands/engage/engage-community-user-remove.ts index 3695270a7eb..81acb171bb5 100644 --- a/src/m365/viva/commands/engage/engage-community-user-remove.ts +++ b/src/m365/viva/commands/engage/engage-community-user-remove.ts @@ -44,16 +44,32 @@ class VivaEngageCommunityUserRemoveCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.communityId, options.communityDisplayName, options.entraGroupId].filter(x => x !== undefined).length === 1, { - error: 'Specify either communityId, communityDisplayName, or entraGroupId, but not multiple.' + error: 'Specify either communityId, communityDisplayName, or entraGroupId, but not multiple.', + params: { + customCode: 'optionSet', + options: ['communityId', 'communityDisplayName', 'entraGroupId'] + } }) .refine(options => options.communityId || options.communityDisplayName || options.entraGroupId, { - error: 'Specify at least one of communityId, communityDisplayName, or entraGroupId.' + error: 'Specify at least one of communityId, communityDisplayName, or entraGroupId.', + params: { + customCode: 'optionSet', + options: ['communityId', 'communityDisplayName', 'entraGroupId'] + } }) .refine(options => options.id || options.userName, { - error: 'Specify either of id or userName.' + error: 'Specify either of id or userName.', + params: { + customCode: 'optionSet', + options: ['id', 'userName'] + } }) .refine(options => typeof options.userName !== 'undefined' || typeof options.id !== 'undefined', { - error: 'Specify either id or userName, but not both.' + error: 'Specify either id or userName, but not both.', + params: { + customCode: 'optionSet', + options: ['id', 'userName'] + } }); } diff --git a/src/m365/viva/commands/engage/engage-role-member-list.ts b/src/m365/viva/commands/engage/engage-role-member-list.ts index c05272ff00b..ad238bdc985 100644 --- a/src/m365/viva/commands/engage/engage-role-member-list.ts +++ b/src/m365/viva/commands/engage/engage-role-member-list.ts @@ -33,7 +33,11 @@ class VivaEngageRoleMemberListCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(options => [options.roleId, options.roleName].filter(x => x !== undefined).length === 1, { - error: 'Specify either roleId, or roleName, but not both.' + error: 'Specify either roleId, or roleName, but not both.', + params: { + customCode: 'optionSet', + options: ['roleId', 'roleName'] + } }); } diff --git a/src/utils/prompt.ts b/src/utils/prompt.ts index 843d5b05572..31e8ca2d1a5 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 */ From ffeba94da889c52f638bca98f1e8788cfd3cc8f7 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Sat, 28 Mar 2026 15:09:43 +0100 Subject: [PATCH 2/5] Update src/cli/cli.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/cli/cli.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 6b2af5d0769..1cafb692d8e 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -1082,7 +1082,24 @@ async function promptForOptionSetNameAndValue(args: CommandArgs, options: string 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; + let coercedValue: unknown = optionValue; + + // Basic type coercion to align with how CLI arguments are typically parsed + if (typeof optionValue === 'string') { + const trimmed = optionValue.trim(); + + if (trimmed === 'true') { + coercedValue = true; + } + else if (trimmed === 'false') { + coercedValue = false; + } + else if (trimmed !== '' && !isNaN(Number(trimmed))) { + coercedValue = Number(trimmed); + } + } + + args.options[selectedOptionName] = coercedValue as any; await cli.error(''); } From 236e72f72d4768b77f72481b5435dd639e186d1c Mon Sep 17 00:00:00 2001 From: waldekmastykarz Date: Sat, 28 Mar 2026 15:26:17 +0100 Subject: [PATCH 3/5] Replaces `.some()` with `.length` --- src/cli/cli.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 1cafb692d8e..c4d0c548488 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -206,11 +206,11 @@ async function execute(rawArgs: string[]): Promise { 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)) { + if (otherErrors.length > 0) { return cli.closeWithError(result.error, cli.optionsFromArgs, true); } - if (missingRequiredValuesErrors.some(e => e)) { + if (missingRequiredValuesErrors.length > 0) { await cli.error('🌶️ Provide values for the following parameters:'); for (const error of missingRequiredValuesErrors) { @@ -230,7 +230,7 @@ async function execute(rawArgs: string[]): Promise { continue; } - if (optionSetErrors.some(e => e)) { + if (optionSetErrors.length > 0) { for (const error of optionSetErrors) { await promptForOptionSetNameAndValue(cli.optionsFromArgs, error.params?.options); } From e761312feb964715240d6f3aada8e9e7a8d6fa39 Mon Sep 17 00:00:00 2001 From: waldekmastykarz Date: Sat, 28 Mar 2026 15:26:30 +0100 Subject: [PATCH 4/5] Adds missing tests for coverage --- src/cli/cli.spec.ts | 153 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 3 deletions(-) diff --git a/src/cli/cli.spec.ts b/src/cli/cli.spec.ts index 7d5128492ea..a03177dbfd6 100644 --- a/src/cli/cli.spec.ts +++ b/src/cli/cli.spec.ts @@ -357,6 +357,82 @@ class MockCommandWithRefinedSchema extends AnonymousCommand { } } +const refinedSchemaCoercionOptions = z.strictObject({ + ...globalOptionsZod.shape, + mode: z.string().optional(), + optA: z.coerce.number().optional(), + optB: z.coerce.number().optional(), + optC: z.coerce.boolean().optional(), + optD: z.coerce.boolean().optional(), + optE: z.string().optional(), + optF: z.string().optional() +}); + +class MockCommandWithRefinedSchemaCoercion extends AnonymousCommand { + public get name(): string { + return 'cli mock schema refined coercion'; + } + public get description(): string { + return 'Mock command with refined schema for coercion tests'; + } + public get schema(): z.ZodType { + return refinedSchemaCoercionOptions; + } + public getRefinedSchema(schema: typeof refinedSchemaCoercionOptions): z.ZodObject | undefined { + return schema + .refine(options => !(options.optA && options.optB), { + error: 'Specify either optA or optB, but not both.', + path: ['optA'], + params: { + customCode: 'optionSet', + options: ['optA', 'optB'] + } + }) + .refine(options => options.mode !== 'number' || options.optA !== undefined || options.optB !== undefined, { + error: 'Specify optA or optB.', + path: ['optA'], + params: { + customCode: 'optionSet', + options: ['optA', 'optB'] + } + }) + .refine(options => !(options.optC !== undefined && options.optD !== undefined), { + error: 'Specify either optC or optD, but not both.', + path: ['optC'], + params: { + customCode: 'optionSet', + options: ['optC', 'optD'] + } + }) + .refine(options => options.mode !== 'bool' || options.optC !== undefined || options.optD !== undefined, { + error: 'Specify optC or optD.', + path: ['optC'], + params: { + customCode: 'optionSet', + options: ['optC', 'optD'] + } + }) + .refine(options => !(options.optE && options.optF), { + error: 'Specify either optE or optF, but not both.', + path: ['optE'], + params: { + customCode: 'optionSet', + options: ['optE', 'optF'] + } + }) + .refine(options => options.mode !== 'string' || options.optE || options.optF, { + error: 'Specify optE or optF.', + path: ['optE'], + params: { + customCode: 'optionSet', + options: ['optE', 'optF'] + } + }); + } + public async commandAction(): Promise { + } +} + describe('cli', () => { let rootFolder: string; let cliLogStub: sinon.SinonStub; @@ -374,6 +450,7 @@ describe('cli', () => { let mockCommandWithSchemaAndRequiredOptions: Command; let mockCommandWithSchemaAndBoolRequiredOption: Command; let mockCommandWithRefinedSchema: Command; + let mockCommandWithRefinedSchemaCoercion: Command; let log: string[] = []; let mockCommandWithBooleanRewrite: Command; @@ -399,6 +476,7 @@ describe('cli', () => { mockCommandWithSchemaAndRequiredOptions = new MockCommandWithSchemaAndRequiredOptions(); mockCommandWithSchemaAndBoolRequiredOption = new MockCommandWithSchemaAndBoolRequiredOption(); mockCommandWithRefinedSchema = new MockCommandWithRefinedSchema(); + mockCommandWithRefinedSchemaCoercion = new MockCommandWithRefinedSchemaCoercion(); mockCommandWithOptionSets = new MockCommandWithOptionSets(); mockCommandActionSpy = sinon.spy(mockCommand, 'action'); @@ -422,6 +500,7 @@ describe('cli', () => { cli.getCommandInfo(mockCommandWithSchemaAndRequiredOptions, 'cli-schema-mock.js', 'help.mdx'), cli.getCommandInfo(mockCommandWithSchemaAndBoolRequiredOption, 'cli-schema-mock.js', 'help.mdx'), cli.getCommandInfo(mockCommandWithRefinedSchema, 'cli-schema-refined-mock.js', 'help.mdx'), + cli.getCommandInfo(mockCommandWithRefinedSchemaCoercion, 'cli-schema-refined-coercion-mock.js', 'help.mdx'), cli.getCommandInfo(cliCompletionUpdateCommand, 'cli/commands/completion/completion-clink-update.js', 'cli/completion/completion-clink-update.mdx'), cli.getCommandInfo(mockCommandWithBooleanRewrite, 'cli-boolean-rewrite-mock.js', 'help.mdx') ]; @@ -1306,6 +1385,74 @@ describe('cli', () => { }); }); + it(`coerces 'true' to boolean true when prompting for option set value`, async () => { + cli.commandToExecute = cli.commands.find(c => c.name === 'cli mock schema refined coercion'); + sinon.stub(prompt, 'forSelection').resolves('optC'); + sinon.stub(prompt, 'forInput').resolves('true'); + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return true; + } + return defaultValue; + }); + const executeCommandSpy = sinon.spy(cli, 'executeCommand'); + + await cli.execute(['cli', 'mock', 'schema', 'refined', 'coercion', '--mode', 'bool']); + assert(executeCommandSpy.called); + assert.strictEqual(executeCommandSpy.firstCall.args[1].options.optC, true); + }); + + it(`coerces 'false' to boolean false when prompting for option set value`, async () => { + cli.commandToExecute = cli.commands.find(c => c.name === 'cli mock schema refined coercion'); + sinon.stub(prompt, 'forSelection').resolves('optC'); + sinon.stub(prompt, 'forInput').resolves('false'); + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return true; + } + return defaultValue; + }); + const executeCommandSpy = sinon.spy(cli, 'executeCommand'); + + await cli.execute(['cli', 'mock', 'schema', 'refined', 'coercion', '--mode', 'bool']); + assert(executeCommandSpy.called); + assert.strictEqual(executeCommandSpy.firstCall.args[1].options.optC, false); + }); + + it(`coerces numeric string to number when prompting for option set value`, async () => { + cli.commandToExecute = cli.commands.find(c => c.name === 'cli mock schema refined coercion'); + sinon.stub(prompt, 'forSelection').resolves('optA'); + sinon.stub(prompt, 'forInput').resolves('42'); + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return true; + } + return defaultValue; + }); + const executeCommandSpy = sinon.spy(cli, 'executeCommand'); + + await cli.execute(['cli', 'mock', 'schema', 'refined', 'coercion', '--mode', 'number']); + assert(executeCommandSpy.called); + assert.strictEqual(executeCommandSpy.firstCall.args[1].options.optA, 42); + }); + + it(`keeps string value as string when prompting for option set value`, async () => { + cli.commandToExecute = cli.commands.find(c => c.name === 'cli mock schema refined coercion'); + sinon.stub(prompt, 'forSelection').resolves('optE'); + sinon.stub(prompt, 'forInput').resolves('hello world'); + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return true; + } + return defaultValue; + }); + const executeCommandSpy = sinon.spy(cli, 'executeCommand'); + + await cli.execute(['cli', 'mock', 'schema', 'refined', 'coercion', '--mode', 'string']); + assert(executeCommandSpy.called); + assert.strictEqual(executeCommandSpy.firstCall.args[1].options.optE, 'hello world'); + }); + it(`executes command when validation passed`, async () => { cli.commandToExecute = cli.commands.find(c => c.name === 'cli mock'); @@ -1877,7 +2024,7 @@ describe('cli', () => { await cli.loadCommandFromArgs(['spo', 'site', 'list']); cli.printAvailableCommands(); - assert(cliLogStub.calledWith(' cli * 12 commands')); + assert(cliLogStub.calledWith(' cli * 13 commands')); }); it(`prints commands from the specified group`, async () => { @@ -1890,7 +2037,7 @@ describe('cli', () => { }; cli.printAvailableCommands(); - assert(cliLogStub.calledWith(' cli mock * 9 commands')); + assert(cliLogStub.calledWith(' cli mock * 10 commands')); }); it(`prints commands from the root group when the specified string doesn't match any group`, async () => { @@ -1903,7 +2050,7 @@ describe('cli', () => { }; cli.printAvailableCommands(); - assert(cliLogStub.calledWith(' cli * 12 commands')); + assert(cliLogStub.calledWith(' cli * 13 commands')); }); it(`runs properly when context file not found`, async () => { From 9558dc28ce46f27733154254ada56e8da89dbfd2 Mon Sep 17 00:00:00 2001 From: waldekmastykarz Date: Sat, 18 Apr 2026 14:55:34 +0200 Subject: [PATCH 5/5] Fixes option set continue and XOR validation bug --- src/cli/cli.ts | 2 ++ .../viva/commands/engage/engage-community-user-remove.ts | 9 +-------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/cli/cli.ts b/src/cli/cli.ts index c4d0c548488..1b9bfdc8c0f 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -234,6 +234,8 @@ async function execute(rawArgs: string[]): Promise { for (const error of optionSetErrors) { await promptForOptionSetNameAndValue(cli.optionsFromArgs, error.params?.options); } + + continue; } } } diff --git a/src/m365/viva/commands/engage/engage-community-user-remove.ts b/src/m365/viva/commands/engage/engage-community-user-remove.ts index 81acb171bb5..0f7c02309d0 100644 --- a/src/m365/viva/commands/engage/engage-community-user-remove.ts +++ b/src/m365/viva/commands/engage/engage-community-user-remove.ts @@ -57,14 +57,7 @@ class VivaEngageCommunityUserRemoveCommand extends GraphCommand { options: ['communityId', 'communityDisplayName', 'entraGroupId'] } }) - .refine(options => options.id || options.userName, { - error: 'Specify either of id or userName.', - params: { - customCode: 'optionSet', - options: ['id', 'userName'] - } - }) - .refine(options => typeof options.userName !== 'undefined' || typeof options.id !== 'undefined', { + .refine(options => [options.id, options.userName].filter(o => o !== undefined).length === 1, { error: 'Specify either id or userName, but not both.', params: { customCode: 'optionSet',