From 8f6459b7383f53b5358b948034012062c3d563b1 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Thu, 28 May 2026 12:38:34 +0200 Subject: [PATCH] Migrates `cli completion` and `cli config` commands to Zod. Closes #7290 --- src/m365/cli/commands/app/app-reconsent.ts | 10 + .../completion/completion-clink-update.ts | 10 + .../completion/completion-pwsh-setup.ts | 28 +-- .../completion/completion-pwsh-update.ts | 10 + .../completion/completion-sh-setup.ts | 10 + .../completion/completion-sh-update.ts | 10 + .../cli/commands/config/config-get.spec.ts | 27 +- src/m365/cli/commands/config/config-get.ts | 54 +--- src/m365/cli/commands/config/config-list.ts | 10 + .../cli/commands/config/config-reset.spec.ts | 28 +-- src/m365/cli/commands/config/config-reset.ts | 56 +---- .../cli/commands/config/config-set.spec.ts | 230 ++++++++---------- src/m365/cli/commands/config/config-set.ts | 159 ++++++------ 13 files changed, 299 insertions(+), 343 deletions(-) diff --git a/src/m365/cli/commands/app/app-reconsent.ts b/src/m365/cli/commands/app/app-reconsent.ts index 1ccb4b69060..b056f011524 100644 --- a/src/m365/cli/commands/app/app-reconsent.ts +++ b/src/m365/cli/commands/app/app-reconsent.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { Logger } from '../../../../cli/Logger.js'; @@ -12,6 +14,10 @@ import { browserUtil } from '../../../../utils/browserUtil.js'; import { entraApp } from '../../../../utils/entraApp.js'; import { entraServicePrincipal } from '../../../../utils/entraServicePrincipal.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape +}); + class CliAppReconsentCommand extends GraphCommand { public get name(): string { return commands.APP_RECONSENT; @@ -21,6 +27,10 @@ class CliAppReconsentCommand extends GraphCommand { return 'Reconsent all permission scopes used in CLI for Microsoft 365'; } + public get schema(): z.ZodType { + return options; + } + public async commandAction(logger: Logger): Promise { try { const appId = auth.connection.appId!; diff --git a/src/m365/cli/commands/completion/completion-clink-update.ts b/src/m365/cli/commands/completion/completion-clink-update.ts index e0c7300fa60..f9770804890 100644 --- a/src/m365/cli/commands/completion/completion-clink-update.ts +++ b/src/m365/cli/commands/completion/completion-clink-update.ts @@ -1,8 +1,14 @@ +import { z } from 'zod'; import { autocomplete } from '../../../../autocomplete.js'; import { Logger } from '../../../../cli/Logger.js'; +import { globalOptionsZod } from '../../../../Command.js'; import AnonymousCommand from '../../../base/AnonymousCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape +}); + class CliCompletionClinkUpdateCommand extends AnonymousCommand { public get name(): string { return commands.COMPLETION_CLINK_UPDATE; @@ -12,6 +18,10 @@ class CliCompletionClinkUpdateCommand extends AnonymousCommand { return 'Updates command completion for Clink (cmder)'; } + public get schema(): z.ZodType { + return options; + } + public async commandAction(logger: Logger): Promise { await logger.log(autocomplete.getClinkCompletion()); } diff --git a/src/m365/cli/commands/completion/completion-pwsh-setup.ts b/src/m365/cli/commands/completion/completion-pwsh-setup.ts index ab57ae1f8a7..d54bc87884e 100644 --- a/src/m365/cli/commands/completion/completion-pwsh-setup.ts +++ b/src/m365/cli/commands/completion/completion-pwsh-setup.ts @@ -2,25 +2,27 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import url from 'url'; +import { z } from 'zod'; import { autocomplete } from '../../../../autocomplete.js'; import { Logger } from '../../../../cli/Logger.js'; import { - CommandError + CommandError, globalOptionsZod } from '../../../../Command.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; import AnonymousCommand from '../../../base/AnonymousCommand.js'; import commands from '../../commands.js'; const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); +export const options = z.strictObject({ + ...globalOptionsZod.shape, + profile: z.string().alias('p') +}); +type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - profile: string; -} - class CliCompletionPwshSetupCommand extends AnonymousCommand { public get name(): string { return commands.COMPLETION_PWSH_SETUP; @@ -30,18 +32,8 @@ class CliCompletionPwshSetupCommand extends AnonymousCommand { return 'Sets up command completion for PowerShell'; } - constructor() { - super(); - - this.#initOptions(); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-p, --profile ' - } - ); + public get schema(): z.ZodType { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/cli/commands/completion/completion-pwsh-update.ts b/src/m365/cli/commands/completion/completion-pwsh-update.ts index 1c374d60e79..e22c76b5d79 100644 --- a/src/m365/cli/commands/completion/completion-pwsh-update.ts +++ b/src/m365/cli/commands/completion/completion-pwsh-update.ts @@ -1,8 +1,14 @@ +import { z } from 'zod'; import { autocomplete } from '../../../../autocomplete.js'; import { Logger } from '../../../../cli/Logger.js'; +import { globalOptionsZod } from '../../../../Command.js'; import AnonymousCommand from '../../../base/AnonymousCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape +}); + class CliCompletionPwshUpdateCommand extends AnonymousCommand { public get name(): string { return commands.COMPLETION_PWSH_UPDATE; @@ -12,6 +18,10 @@ class CliCompletionPwshUpdateCommand extends AnonymousCommand { return 'Updates command completion for PowerShell'; } + public get schema(): z.ZodType { + return options; + } + public async commandAction(logger: Logger): Promise { if (this.debug) { await logger.logToStderr('Generating command completion...'); diff --git a/src/m365/cli/commands/completion/completion-sh-setup.ts b/src/m365/cli/commands/completion/completion-sh-setup.ts index ff3e2bb6ba7..31a5e56bc4e 100644 --- a/src/m365/cli/commands/completion/completion-sh-setup.ts +++ b/src/m365/cli/commands/completion/completion-sh-setup.ts @@ -1,8 +1,14 @@ +import { z } from 'zod'; import { autocomplete } from '../../../../autocomplete.js'; import { Logger } from '../../../../cli/Logger.js'; +import { globalOptionsZod } from '../../../../Command.js'; import AnonymousCommand from '../../../base/AnonymousCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape +}); + class CliCompletionShSetupCommand extends AnonymousCommand { public get name(): string { return commands.COMPLETION_SH_SETUP; @@ -12,6 +18,10 @@ class CliCompletionShSetupCommand extends AnonymousCommand { return 'Sets up command completion for Zsh, Bash and Fish'; } + public get schema(): z.ZodType { + return options; + } + public async commandAction(logger: Logger): Promise { if (this.debug) { await logger.logToStderr('Generating command completion...'); diff --git a/src/m365/cli/commands/completion/completion-sh-update.ts b/src/m365/cli/commands/completion/completion-sh-update.ts index 26481c8b624..537a6c658f0 100644 --- a/src/m365/cli/commands/completion/completion-sh-update.ts +++ b/src/m365/cli/commands/completion/completion-sh-update.ts @@ -1,8 +1,14 @@ +import { z } from 'zod'; import { autocomplete } from '../../../../autocomplete.js'; import { Logger } from '../../../../cli/Logger.js'; +import { globalOptionsZod } from '../../../../Command.js'; import AnonymousCommand from '../../../base/AnonymousCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape +}); + class CliCompletionShUpdateCommand extends AnonymousCommand { public get name(): string { return commands.COMPLETION_SH_UPDATE; @@ -12,6 +18,10 @@ class CliCompletionShUpdateCommand extends AnonymousCommand { return 'Updates command completion for Zsh, Bash and Fish'; } + public get schema(): z.ZodType { + return options; + } + public async commandAction(logger: Logger): Promise { if (this.debug) { await logger.logToStderr('Generating command completion...'); diff --git a/src/m365/cli/commands/config/config-get.spec.ts b/src/m365/cli/commands/config/config-get.spec.ts index 8b089861bad..6d3a1c1d7aa 100644 --- a/src/m365/cli/commands/config/config-get.spec.ts +++ b/src/m365/cli/commands/config/config-get.spec.ts @@ -9,16 +9,18 @@ import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './config-get.js'; +import command, { options } from './config-get.js'; describe(commands.CONFIG_GET, () => { let log: any[]; let logger: Logger; let loggerSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').callsFake(() => ''); sinon.stub(session, 'getId').callsFake(() => ''); @@ -70,24 +72,13 @@ describe(commands.CONFIG_GET, () => { assert(loggerSpy.calledWith(undefined)); }); - it('supports specifying key', () => { - const options = command.options; - let containsOptionKey = false; - options.forEach(o => { - if (o.option.indexOf('--key') > -1) { - containsOptionKey = true; - } - }); - assert(containsOptionKey); - }); - - it('fails validation if specified key is invalid ', async () => { - const actual = await command.validate({ options: { key: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if specified key is invalid ', () => { + const actual = commandOptionsSchema.safeParse({ key: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it(`passes validation if setting is set to ${settingsNames.showHelpOnFailure}`, async () => { - const actual = await command.validate({ options: { key: settingsNames.showHelpOnFailure } }, commandInfo); - assert.strictEqual(actual, true); + it(`passes validation if setting is set to ${settingsNames.showHelpOnFailure}`, () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.showHelpOnFailure }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/cli/commands/config/config-get.ts b/src/m365/cli/commands/config/config-get.ts index 8a965d952f5..71592871efc 100644 --- a/src/m365/cli/commands/config/config-get.ts +++ b/src/m365/cli/commands/config/config-get.ts @@ -1,21 +1,24 @@ +import { z } from 'zod'; import { cli } from "../../../../cli/cli.js"; import { Logger } from "../../../../cli/Logger.js"; -import GlobalOptions from "../../../../GlobalOptions.js"; +import { globalOptionsZod } from "../../../../Command.js"; import { settingsNames } from "../../../../settingsNames.js"; import AnonymousCommand from "../../../base/AnonymousCommand.js"; import commands from "../../commands.js"; +const settingNameValues = Object.getOwnPropertyNames(settingsNames) as [string, ...string[]]; + +export const options = z.strictObject({ + ...globalOptionsZod.shape, + key: z.enum(settingNameValues).alias('k') +}); +type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - key: string; -} - class CliConfigGetCommand extends AnonymousCommand { - private static readonly optionNames: string[] = Object.getOwnPropertyNames(settingsNames); - public get name(): string { return commands.CONFIG_GET; } @@ -24,41 +27,8 @@ class CliConfigGetCommand extends AnonymousCommand { return 'Gets value of a CLI for Microsoft 365 configuration option'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - key: args.options.key - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-k, --key ', - autocomplete: CliConfigGetCommand.optionNames - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (CliConfigGetCommand.optionNames.indexOf(args.options.key) < 0) { - return `${args.options.key} is not a valid setting. Allowed values: ${CliConfigGetCommand.optionNames.join(', ')}`; - } - - return true; - } - ); + public get schema(): z.ZodType { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/cli/commands/config/config-list.ts b/src/m365/cli/commands/config/config-list.ts index e40e673c428..25641a41c37 100644 --- a/src/m365/cli/commands/config/config-list.ts +++ b/src/m365/cli/commands/config/config-list.ts @@ -1,8 +1,14 @@ +import { z } from 'zod'; import { cli } from "../../../../cli/cli.js"; import { Logger } from "../../../../cli/Logger.js"; +import { globalOptionsZod } from "../../../../Command.js"; import AnonymousCommand from "../../../base/AnonymousCommand.js"; import commands from "../../commands.js"; +export const options = z.strictObject({ + ...globalOptionsZod.shape +}); + class CliConfigListCommand extends AnonymousCommand { public get name(): string { return commands.CONFIG_LIST; @@ -12,6 +18,10 @@ class CliConfigListCommand extends AnonymousCommand { return 'List all self set CLI for Microsoft 365 configurations'; } + public get schema(): z.ZodType { + return options; + } + public async commandAction(logger: Logger): Promise { await logger.log(cli.getConfig().all); } diff --git a/src/m365/cli/commands/config/config-reset.spec.ts b/src/m365/cli/commands/config/config-reset.spec.ts index cb47d440433..50e4e629e69 100644 --- a/src/m365/cli/commands/config/config-reset.spec.ts +++ b/src/m365/cli/commands/config/config-reset.spec.ts @@ -8,15 +8,17 @@ import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import commands from '../../commands.js'; -import command from './config-reset.js'; +import command, { options } from './config-reset.js'; describe(commands.CONFIG_RESET, () => { let log: any[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').callsFake(() => ''); sinon.stub(session, 'getId').callsFake(() => ''); @@ -100,25 +102,13 @@ describe(commands.CONFIG_RESET, () => { assert.strictEqual(showHelpOnFailureValue, undefined, 'Invalid value'); }); - it('fails validation if specified key is invalid', async () => { - const actual = await command.validate({ options: { key: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if specified key is invalid', () => { + const actual = commandOptionsSchema.safeParse({ key: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation if key is not specified', async () => { - const actual = await command.validate({ options: {} }, commandInfo); - assert.strictEqual(actual, true); - }); - - it('supports specifying key', () => { - const options = command.options; - let containsOptionKey = false; - options.forEach(o => { - if (o.option.indexOf('--key') > -1) { - containsOptionKey = true; - } - }); - - assert(containsOptionKey); + it('passes validation if key is not specified', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/cli/commands/config/config-reset.ts b/src/m365/cli/commands/config/config-reset.ts index 86ec941109c..1b035c67637 100644 --- a/src/m365/cli/commands/config/config-reset.ts +++ b/src/m365/cli/commands/config/config-reset.ts @@ -1,21 +1,24 @@ +import { z } from 'zod'; import { cli } from "../../../../cli/cli.js"; import { Logger } from "../../../../cli/Logger.js"; -import GlobalOptions from "../../../../GlobalOptions.js"; +import { globalOptionsZod } from "../../../../Command.js"; import { settingsNames } from "../../../../settingsNames.js"; import AnonymousCommand from "../../../base/AnonymousCommand.js"; import commands from "../../commands.js"; +const settingNameValues = Object.getOwnPropertyNames(settingsNames) as [string, ...string[]]; + +export const options = z.strictObject({ + ...globalOptionsZod.shape, + key: z.enum(settingNameValues).optional().alias('k') +}); +type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - key?: string; -} - class CliConfigResetCommand extends AnonymousCommand { - private static readonly optionNames: string[] = Object.getOwnPropertyNames(settingsNames); - public get name(): string { return commands.CONFIG_RESET; } @@ -24,43 +27,8 @@ class CliConfigResetCommand extends AnonymousCommand { return 'Resets the specified CLI configuration option to its default value'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - key: args.options.key - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-k, --key [key]', - autocomplete: CliConfigResetCommand.optionNames - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.key) { - if (CliConfigResetCommand.optionNames.indexOf(args.options.key) < 0) { - return `${args.options.key} is not a valid setting. Allowed values: ${CliConfigResetCommand.optionNames.join(', ')}`; - } - } - - return true; - } - ); + public get schema(): z.ZodType { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/cli/commands/config/config-set.spec.ts b/src/m365/cli/commands/config/config-set.spec.ts index c961ea1e984..901b350544b 100644 --- a/src/m365/cli/commands/config/config-set.spec.ts +++ b/src/m365/cli/commands/config/config-set.spec.ts @@ -9,15 +9,17 @@ import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './config-set.js'; +import command, { options } from './config-set.js'; describe(commands.CONFIG_SET, () => { let log: any[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); @@ -205,194 +207,178 @@ describe(commands.CONFIG_SET, () => { }); - it('supports specifying key and value', () => { - const options = command.options; - let containsOptionKey = false; - let containsOptionValue = false; - options.forEach(o => { - if (o.option.indexOf('--key') > -1) { - containsOptionKey = true; - } - - if (o.option.indexOf('--value') > -1) { - containsOptionValue = true; - } - }); - assert(containsOptionKey && containsOptionValue); - }); - - it('fails validation if specified key is invalid ', async () => { - const actual = await command.validate({ options: { key: 'invalid', value: false } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if specified key is invalid ', () => { + const actual = commandOptionsSchema.safeParse({ key: 'invalid', value: 'false' }); + assert.strictEqual(actual.success, false); }); - it(`passes validation if setting is set to ${settingsNames.showHelpOnFailure} and value to true`, async () => { - const actual = await command.validate({ options: { key: settingsNames.showHelpOnFailure, value: 'true' } }, commandInfo); - assert.strictEqual(actual, true); + it(`passes validation if setting is set to ${settingsNames.showHelpOnFailure} and value to true`, () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.showHelpOnFailure, value: 'true' }); + assert.strictEqual(actual.success, true); }); - it(`passes validation if setting is set to ${settingsNames.showHelpOnFailure} and value to false`, async () => { - const actual = await command.validate({ options: { key: settingsNames.showHelpOnFailure, value: 'false' } }, commandInfo); - assert.strictEqual(actual, true); + it(`passes validation if setting is set to ${settingsNames.showHelpOnFailure} and value to false`, () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.showHelpOnFailure, value: 'false' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if specified output type is invalid', async () => { - const actual = await command.validate({ options: { key: settingsNames.output, value: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if specified output type is invalid', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.output, value: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation for output type text', async () => { - const actual = await command.validate({ options: { key: settingsNames.output, value: 'text' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for output type text', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.output, value: 'text' }); + assert.strictEqual(actual.success, true); }); - it('passes validation for output type json', async () => { - const actual = await command.validate({ options: { key: settingsNames.output, value: 'json' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for output type json', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.output, value: 'json' }); + assert.strictEqual(actual.success, true); }); - it('passes validation for output type csv', async () => { - const actual = await command.validate({ options: { key: settingsNames.output, value: 'csv' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for output type csv', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.output, value: 'csv' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if specified authType is invalid', async () => { - const actual = await command.validate({ options: { key: settingsNames.authType, value: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if specified authType is invalid', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.authType, value: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation for authType type deviceCode', async () => { - const actual = await command.validate({ options: { key: settingsNames.authType, value: 'deviceCode' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for authType type deviceCode', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.authType, value: 'deviceCode' }); + assert.strictEqual(actual.success, true); }); - it('passes validation for authType type browser', async () => { - const actual = await command.validate({ options: { key: settingsNames.authType, value: 'browser' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for authType type browser', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.authType, value: 'browser' }); + assert.strictEqual(actual.success, true); }); - it('passes validation for authType type certificate', async () => { - const actual = await command.validate({ options: { key: settingsNames.authType, value: 'certificate' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for authType type certificate', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.authType, value: 'certificate' }); + assert.strictEqual(actual.success, true); }); - it('passes validation for authType type password', async () => { - const actual = await command.validate({ options: { key: settingsNames.authType, value: 'password' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for authType type password', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.authType, value: 'password' }); + assert.strictEqual(actual.success, true); }); - it('passes validation for authType type identity', async () => { - const actual = await command.validate({ options: { key: settingsNames.authType, value: 'identity' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for authType type identity', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.authType, value: 'identity' }); + assert.strictEqual(actual.success, true); }); - it('passes validation for authType type secret', async () => { - const actual = await command.validate({ options: { key: settingsNames.authType, value: 'secret' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for authType type secret', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.authType, value: 'secret' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if specified error output type is invalid', async () => { - const actual = await command.validate({ options: { key: settingsNames.errorOutput, value: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if specified error output type is invalid', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.errorOutput, value: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation for error output stdout', async () => { - const actual = await command.validate({ options: { key: settingsNames.errorOutput, value: 'stdout' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for error output stdout', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.errorOutput, value: 'stdout' }); + assert.strictEqual(actual.success, true); }); - it('passes validation for error output stderr', async () => { - const actual = await command.validate({ options: { key: settingsNames.errorOutput, value: 'stderr' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for error output stderr', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.errorOutput, value: 'stderr' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if specified help mode is invalid', async () => { - const actual = await command.validate({ options: { key: settingsNames.helpMode, value: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if specified help mode is invalid', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.helpMode, value: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation for help mode options', async () => { - const actual = await command.validate({ options: { key: settingsNames.helpMode, value: 'options' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for help mode options', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.helpMode, value: 'options' }); + assert.strictEqual(actual.success, true); }); - it('passes validation for help mode examples', async () => { - const actual = await command.validate({ options: { key: settingsNames.helpMode, value: 'examples' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for help mode examples', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.helpMode, value: 'examples' }); + assert.strictEqual(actual.success, true); }); - it('passes validation for help mode remarks', async () => { - const actual = await command.validate({ options: { key: settingsNames.helpMode, value: 'remarks' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for help mode remarks', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.helpMode, value: 'remarks' }); + assert.strictEqual(actual.success, true); }); - it('passes validation for help mode response', async () => { - const actual = await command.validate({ options: { key: settingsNames.helpMode, value: 'response' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for help mode response', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.helpMode, value: 'response' }); + assert.strictEqual(actual.success, true); }); - it('passes validation for help mode full', async () => { - const actual = await command.validate({ options: { key: settingsNames.helpMode, value: 'full' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for help mode full', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.helpMode, value: 'full' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if specified promptListPageSize value is a string', async () => { - const actual = await command.validate({ options: { key: settingsNames.promptListPageSize, value: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if specified promptListPageSize value is a string', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.promptListPageSize, value: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if specified promptListPageSize value is 0', async () => { - const actual = await command.validate({ options: { key: settingsNames.promptListPageSize, value: 0 } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if specified promptListPageSize value is 0', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.promptListPageSize, value: '0' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if specified promptListPageSize value is negative', async () => { - const actual = await command.validate({ options: { key: settingsNames.promptListPageSize, value: -1 } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if specified promptListPageSize value is negative', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.promptListPageSize, value: '-1' }); + assert.strictEqual(actual.success, false); }); - it('passes validation for number value in promptListPageSize', async () => { - const actual = await command.validate({ options: { key: settingsNames.promptListPageSize, value: 10 } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for number value in promptListPageSize', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.promptListPageSize, value: '10' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if specified help target is invalid', async () => { - const actual = await command.validate({ options: { key: settingsNames.helpTarget, value: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if specified help target is invalid', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.helpTarget, value: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation for help target web', async () => { - const actual = await command.validate({ options: { key: settingsNames.helpTarget, value: 'web' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for help target web', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.helpTarget, value: 'web' }); + assert.strictEqual(actual.success, true); }); - it('passes validation for help target console', async () => { - const actual = await command.validate({ options: { key: settingsNames.helpTarget, value: 'console' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation for help target console', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.helpTarget, value: 'console' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if specified clientId is not a GUID', async () => { - const actual = await command.validate({ options: { key: settingsNames.clientId, value: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if specified clientId is not a GUID', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.clientId, value: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation if specified clientId is a GUID', async () => { - const actual = await command.validate({ options: { key: settingsNames.clientId, value: '00000000-0000-0000-c000-000000000001' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if specified clientId is a GUID', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.clientId, value: '00000000-0000-0000-c000-000000000001' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if specified tenantId is not a GUID or common', async () => { - const actual = await command.validate({ options: { key: settingsNames.tenantId, value: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if specified tenantId is not a GUID or common', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.tenantId, value: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation if specified tenantId is a GUID', async () => { - const actual = await command.validate({ options: { key: settingsNames.tenantId, value: '00000000-0000-0000-c000-000000000001' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if specified tenantId is a GUID', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.tenantId, value: '00000000-0000-0000-c000-000000000001' }); + assert.strictEqual(actual.success, true); }); - it('passes validation if specified tenantId is common', async () => { - const actual = await command.validate({ options: { key: settingsNames.tenantId, value: 'common' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if specified tenantId is common', () => { + const actual = commandOptionsSchema.safeParse({ key: settingsNames.tenantId, value: 'common' }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/cli/commands/config/config-set.ts b/src/m365/cli/commands/config/config-set.ts index 411be8b4283..b69f1a7fa83 100644 --- a/src/m365/cli/commands/config/config-set.ts +++ b/src/m365/cli/commands/config/config-set.ts @@ -1,24 +1,27 @@ +import { z } from 'zod'; import { AuthType } from "../../../../Auth.js"; import { cli } from "../../../../cli/cli.js"; import { Logger } from "../../../../cli/Logger.js"; -import GlobalOptions from "../../../../GlobalOptions.js"; +import { globalOptionsZod } from "../../../../Command.js"; import { settingsNames } from "../../../../settingsNames.js"; import { validation } from "../../../../utils/validation.js"; import AnonymousCommand from "../../../base/AnonymousCommand.js"; import commands from "../../commands.js"; +const settingNameValues = Object.getOwnPropertyNames(settingsNames) as [string, ...string[]]; + +export const options = z.strictObject({ + ...globalOptionsZod.shape, + key: z.enum(settingNameValues).alias('k'), + value: z.string().alias('v') +}); +type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - key: string; - value: string; -} - class CliConfigSetCommand extends AnonymousCommand { - private static readonly optionNames: string[] = Object.getOwnPropertyNames(settingsNames); - public get name(): string { return commands.CONFIG_SET; } @@ -27,89 +30,85 @@ class CliConfigSetCommand extends AnonymousCommand { return 'Manage global configuration settings about the CLI for Microsoft 365'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - this.telemetryProperties[args.options.key] = args.options.value; - }); + public get schema(): z.ZodType { + return options; } - #initOptions(): void { - this.options.unshift( - { - option: '-k, --key ', - autocomplete: CliConfigSetCommand.optionNames - }, - { - option: '-v, --value ' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (CliConfigSetCommand.optionNames.indexOf(args.options.key) < 0) { - return `${args.options.key} is not a valid setting. Allowed values: ${CliConfigSetCommand.optionNames.join(', ')}`; - } - - const allowedOutputs = ['text', 'json', 'csv', 'md', 'none']; - if (args.options.key === settingsNames.output && - allowedOutputs.indexOf(args.options.value) === -1) { - return `${args.options.value} is not a valid value for the option ${args.options.key}. Allowed values: ${allowedOutputs.join(', ')}`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(opts => { + if (opts.key === settingsNames.output) { + return ['text', 'json', 'csv', 'md', 'none'].includes(opts.value); } - - const allowedErrorOutputs = ['stdout', 'stderr']; - if (args.options.key === settingsNames.errorOutput && - allowedErrorOutputs.indexOf(args.options.value) === -1) { - return `${args.options.value} is not a valid value for the option ${args.options.key}. Allowed values: ${allowedErrorOutputs.join(', ')}`; - } - - if (args.options.key === settingsNames.promptListPageSize && - typeof args.options.value !== 'number') { - return `${args.options.value} is not a valid value for the option ${args.options.key}. The value has to be a number.`; + return true; + }, { + error: `The value is not valid for the option ${settingsNames.output}. Allowed values: text, json, csv, md, none`, + path: ['value'] + }) + .refine(opts => { + if (opts.key === settingsNames.errorOutput) { + return ['stdout', 'stderr'].includes(opts.value); } - - if (args.options.key === settingsNames.promptListPageSize && - (args.options.value as unknown as number) <= 0) { - return `${args.options.value} is not a valid value for the option ${args.options.key}. The number has to be higher than 0.`; + return true; + }, { + error: `The value is not valid for the option ${settingsNames.errorOutput}. Allowed values: stdout, stderr`, + path: ['value'] + }) + .refine(opts => { + if (opts.key === settingsNames.promptListPageSize) { + const num = Number(opts.value); + return !isNaN(num) && num > 0; } - - if (args.options.key === settingsNames.helpMode && - cli.helpModes.indexOf(args.options.value) === -1) { - return `${args.options.value} is not a valid value for the option ${args.options.key}. Allowed values: ${cli.helpModes.join(', ')}`; + return true; + }, { + error: `The value is not valid for the option ${settingsNames.promptListPageSize}. The value has to be a number higher than 0.`, + path: ['value'] + }) + .refine(opts => { + if (opts.key === settingsNames.helpMode) { + return cli.helpModes.includes(opts.value); } - - if (args.options.key === settingsNames.authType && - !Object.values(AuthType).map(String).includes(args.options.value)) { - return `${args.options.value} is not a valid value for the option ${args.options.key}. Allowed values: ${Object.values(AuthType).join(', ')}`; + return true; + }, { + error: `The value is not valid for the option ${settingsNames.helpMode}. Allowed values: ${cli.helpModes.join(', ')}`, + path: ['value'] + }) + .refine(opts => { + if (opts.key === settingsNames.authType) { + return Object.values(AuthType).map(String).includes(opts.value); } - - if (args.options.key === settingsNames.helpTarget && - !cli.helpTargets.includes(args.options.value)) { - return `${args.options.value} is not a valid value for the option ${args.options.key}. Allowed values: ${cli.helpTargets.join(', ')}`; + return true; + }, { + error: `The value is not valid for the option ${settingsNames.authType}. Allowed values: ${Object.values(AuthType).join(', ')}`, + path: ['value'] + }) + .refine(opts => { + if (opts.key === settingsNames.helpTarget) { + return cli.helpTargets.includes(opts.value); } - - if (args.options.key === settingsNames.clientId && - !validation.isValidGuid(args.options.value)) { - return `${args.options.value} is not a valid value for the option ${args.options.key}. The value has to be a valid GUID.`; + return true; + }, { + error: `The value is not valid for the option ${settingsNames.helpTarget}. Allowed values: ${cli.helpTargets.join(', ')}`, + path: ['value'] + }) + .refine(opts => { + if (opts.key === settingsNames.clientId) { + return validation.isValidGuid(opts.value); } - - if (args.options.key === settingsNames.tenantId && - !(args.options.value === 'common' || validation.isValidGuid(args.options.value))) { - return `${args.options.value} is not a valid value for the option ${args.options.key}. The value has to be a valid GUID or 'common'.`; + return true; + }, { + error: `The value is not valid for the option ${settingsNames.clientId}. The value has to be a valid GUID.`, + path: ['value'] + }) + .refine(opts => { + if (opts.key === settingsNames.tenantId) { + return opts.value === 'common' || validation.isValidGuid(opts.value); } - return true; - } - ); + }, { + error: `The value is not valid for the option ${settingsNames.tenantId}. The value has to be a valid GUID or 'common'.`, + path: ['value'] + }) as any; } public async commandAction(logger: Logger, args: CommandArgs): Promise {