@pnp/cli-microsoft365 8.1.0-beta.3dec9fa → 8.1.0-beta.96dc207
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/allCommands.json +1 -1
- package/allCommandsFull.json +1 -1
- package/dist/Auth.js +20 -21
- package/dist/Command.js +50 -5
- package/dist/cli/cli.js +115 -39
- package/dist/config.js +60 -5
- package/dist/m365/base/SpoCommand.js +1 -1
- package/dist/m365/cli/commands/cli-consent.js +2 -2
- package/dist/m365/cli/commands/cli-doctor.js +2 -2
- package/dist/m365/cli/commands/cli-reconsent.js +2 -3
- package/dist/m365/cli/commands/config/config-set.js +12 -3
- package/dist/m365/commands/login.js +74 -102
- package/dist/m365/commands/setup.js +256 -33
- package/dist/m365/connection/commands/connection-list.js +4 -4
- package/dist/m365/connection/commands/connection-remove.js +6 -2
- package/dist/m365/connection/commands/connection-set.js +4 -1
- package/dist/m365/connection/commands/connection-use.js +25 -4
- package/dist/m365/entra/commands/app/app-add.js +52 -288
- package/dist/m365/entra/commands/enterpriseapp/enterpriseapp-remove.js +123 -0
- package/dist/m365/entra/commands/group/group-set.js +256 -0
- package/dist/m365/entra/commands/m365group/m365group-user-add.js +109 -32
- package/dist/m365/entra/commands/m365group/m365group-user-set.js +159 -84
- package/dist/m365/entra/commands/multitenant/multitenant-add.js +65 -0
- package/dist/m365/entra/commands/multitenant/multitenant-remove.js +118 -0
- package/dist/m365/entra/commands/multitenant/multitenant-set.js +72 -0
- package/dist/m365/entra/commands.js +6 -0
- package/dist/m365/flow/commands/flow-get.js +1 -1
- package/dist/m365/onenote/commands/notebook/notebook-add.js +132 -0
- package/dist/m365/onenote/commands.js +1 -0
- package/dist/m365/pa/commands/app/app-export.js +13 -7
- package/dist/m365/spe/ContainerTypeProperties.js +2 -0
- package/dist/m365/spe/commands/containertype/containertype-list.js +49 -0
- package/dist/m365/spe/commands.js +2 -1
- package/dist/m365/spo/commands/applicationcustomizer/applicationcustomizer-get.js +16 -21
- package/dist/m365/spo/commands/commandset/commandset-get.js +31 -17
- package/dist/m365/spo/commands/file/file-roleassignment-add.js +1 -1
- package/dist/m365/spo/commands/file/file-roleinheritance-break.js +1 -1
- package/dist/m365/spo/commands/file/file-roleinheritance-reset.js +1 -1
- package/dist/m365/spo/commands/folder/folder-retentionlabel-ensure.js +1 -1
- package/dist/m365/spo/commands/folder/folder-sharinglink-get.js +86 -0
- package/dist/m365/spo/commands/folder/folder-sharinglink-list.js +110 -0
- package/dist/m365/spo/commands/list/ListInstance.js +6 -1
- package/dist/m365/spo/commands/list/list-get.js +9 -3
- package/dist/m365/spo/commands/list/list-roleassignment-add.js +46 -21
- package/dist/m365/spo/commands/list/list-roleassignment-remove.js +48 -46
- package/dist/m365/spo/commands/site/site-get.js +12 -16
- package/dist/m365/spo/commands/tenant/tenant-applicationcustomizer-get.js +19 -5
- package/dist/m365/spo/commands/tenant/tenant-commandset-get.js +20 -6
- package/dist/m365/spo/commands.js +2 -0
- package/dist/m365/teams/commands/message/message-restore.js +106 -0
- package/dist/m365/teams/commands.js +1 -0
- package/dist/settingsNames.js +7 -1
- package/dist/utils/drive.js +61 -0
- package/dist/utils/entraApp.js +283 -0
- package/dist/utils/formatting.js +16 -0
- package/dist/utils/spo.js +69 -6
- package/dist/utils/zod.js +124 -0
- package/docs/docs/_clisettings.mdx +6 -0
- package/docs/docs/cmd/connection/connection-use.mdx +8 -2
- package/docs/docs/cmd/entra/enterpriseapp/enterpriseapp-remove.mdx +65 -0
- package/docs/docs/cmd/entra/group/group-add.mdx +0 -4
- package/docs/docs/cmd/entra/group/group-set.mdx +89 -0
- package/docs/docs/cmd/entra/m365group/m365group-user-add.mdx +28 -10
- package/docs/docs/cmd/entra/m365group/m365group-user-set.mdx +35 -11
- package/docs/docs/cmd/entra/multitenant/multitenant-add.mdx +107 -0
- package/docs/docs/cmd/entra/multitenant/multitenant-remove.mdx +58 -0
- package/docs/docs/cmd/entra/multitenant/multitenant-set.mdx +53 -0
- package/docs/docs/cmd/flow/flow-get.mdx +149 -283
- package/docs/docs/cmd/onenote/notebook/notebook-add.mdx +169 -0
- package/docs/docs/cmd/pa/app/app-export.mdx +15 -9
- package/docs/docs/cmd/planner/plan/plan-remove.mdx +1 -1
- package/docs/docs/cmd/setup.mdx +16 -3
- package/docs/docs/cmd/spe/containertype/containertype-list.mdx +102 -0
- package/docs/docs/cmd/spo/app/app-uninstall.mdx +1 -1
- package/docs/docs/cmd/spo/applicationcustomizer/applicationcustomizer-get.mdx +87 -38
- package/docs/docs/cmd/spo/applicationcustomizer/applicationcustomizer-list.mdx +22 -28
- package/docs/docs/cmd/spo/commandset/commandset-get.mdx +75 -24
- package/docs/docs/cmd/spo/commandset/commandset-list.mdx +26 -32
- package/docs/docs/cmd/spo/file/file-retentionlabel-ensure.mdx +1 -1
- package/docs/docs/cmd/spo/file/file-roleassignment-add.mdx +2 -2
- package/docs/docs/cmd/spo/file/file-roleassignment-remove.mdx +1 -1
- package/docs/docs/cmd/spo/file/file-roleinheritance-break.mdx +1 -1
- package/docs/docs/cmd/spo/file/file-roleinheritance-reset.mdx +1 -1
- package/docs/docs/cmd/spo/folder/folder-retentionlabel-ensure.mdx +2 -2
- package/docs/docs/cmd/spo/folder/folder-sharinglink-get.mdx +110 -0
- package/docs/docs/cmd/spo/folder/folder-sharinglink-list.mdx +114 -0
- package/docs/docs/cmd/spo/list/list-get.mdx +6 -0
- package/docs/docs/cmd/spo/list/list-roleassignment-add.mdx +15 -3
- package/docs/docs/cmd/spo/list/list-roleassignment-remove.mdx +15 -3
- package/docs/docs/cmd/spo/listitem/listitem-retentionlabel-ensure.mdx +4 -4
- package/docs/docs/cmd/spo/listitem/listitem-retentionlabel-remove.mdx +1 -1
- package/docs/docs/cmd/spo/listitem/listitem-roleassignment-add.mdx +9 -9
- package/docs/docs/cmd/spo/listitem/listitem-roleassignment-remove.mdx +7 -7
- package/docs/docs/cmd/spo/site/site-recyclebinitem-list.mdx +1 -1
- package/docs/docs/cmd/spo/tenant/tenant-applicationcustomizer-get.mdx +79 -30
- package/docs/docs/cmd/spo/tenant/tenant-applicationcustomizer-list.mdx +20 -19
- package/docs/docs/cmd/spo/tenant/tenant-commandset-get.mdx +84 -38
- package/docs/docs/cmd/spo/tenant/tenant-commandset-list.mdx +20 -19
- package/docs/docs/cmd/spo/web/web-roleassignment-add.mdx +1 -1
- package/docs/docs/cmd/spo/web/web-roleassignment-remove.mdx +1 -1
- package/docs/docs/cmd/teams/meeting/meeting-list.mdx +7 -3
- package/docs/docs/cmd/teams/message/message-remove.mdx +2 -1
- package/docs/docs/cmd/teams/message/message-restore.mdx +62 -0
- package/npm-shrinkwrap.json +1002 -1147
- package/package.json +26 -23
package/dist/Auth.js
CHANGED
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
import { AzureCloudInstance } from '@azure/msal-common';
|
|
2
|
+
import assert from 'assert';
|
|
2
3
|
import { CommandError } from './Command.js';
|
|
3
4
|
import { FileTokenStorage } from './auth/FileTokenStorage.js';
|
|
4
5
|
import { msalCachePlugin } from './auth/msalCachePlugin.js';
|
|
5
6
|
import { cli } from './cli/cli.js';
|
|
6
|
-
import config from './config.js';
|
|
7
7
|
import request from './request.js';
|
|
8
8
|
import { settingsNames } from './settingsNames.js';
|
|
9
|
-
import { browserUtil } from './utils/browserUtil.js';
|
|
10
9
|
import * as accessTokenUtil from './utils/accessToken.js';
|
|
11
|
-
import
|
|
10
|
+
import { browserUtil } from './utils/browserUtil.js';
|
|
12
11
|
export var CloudType;
|
|
13
12
|
(function (CloudType) {
|
|
14
|
-
CloudType[
|
|
15
|
-
CloudType[
|
|
16
|
-
CloudType[
|
|
17
|
-
CloudType[
|
|
18
|
-
CloudType[
|
|
13
|
+
CloudType["Public"] = "Public";
|
|
14
|
+
CloudType["USGov"] = "USGov";
|
|
15
|
+
CloudType["USGovHigh"] = "USGovHigh";
|
|
16
|
+
CloudType["USGovDoD"] = "USGovDoD";
|
|
17
|
+
CloudType["China"] = "China";
|
|
19
18
|
})(CloudType || (CloudType = {}));
|
|
20
19
|
export class Connection {
|
|
21
20
|
constructor() {
|
|
22
21
|
this.active = false;
|
|
23
22
|
this.authType = AuthType.DeviceCode;
|
|
24
23
|
this.certificateType = CertificateType.Unknown;
|
|
24
|
+
// ID of the tenant where the Microsoft Entra app is registered; common if multi-tenant
|
|
25
|
+
this.tenant = 'common';
|
|
25
26
|
this.cloudType = CloudType.Public;
|
|
26
27
|
this.accessTokens = {};
|
|
27
|
-
this.appId = config.cliEntraAppId;
|
|
28
|
-
this.tenant = config.tenant;
|
|
29
28
|
this.cloudType = CloudType.Public;
|
|
30
29
|
}
|
|
31
30
|
deactivate() {
|
|
@@ -44,18 +43,18 @@ export class Connection {
|
|
|
44
43
|
this.thumbprint = undefined;
|
|
45
44
|
this.spoUrl = undefined;
|
|
46
45
|
this.spoTenantId = undefined;
|
|
47
|
-
this.appId =
|
|
48
|
-
this.tenant =
|
|
46
|
+
this.appId = cli.getClientId();
|
|
47
|
+
this.tenant = cli.getTenant();
|
|
49
48
|
}
|
|
50
49
|
}
|
|
51
50
|
export var AuthType;
|
|
52
51
|
(function (AuthType) {
|
|
53
|
-
AuthType[
|
|
54
|
-
AuthType[
|
|
55
|
-
AuthType[
|
|
56
|
-
AuthType[
|
|
57
|
-
AuthType[
|
|
58
|
-
AuthType[
|
|
52
|
+
AuthType["DeviceCode"] = "deviceCode";
|
|
53
|
+
AuthType["Password"] = "password";
|
|
54
|
+
AuthType["Certificate"] = "certificate";
|
|
55
|
+
AuthType["Identity"] = "identity";
|
|
56
|
+
AuthType["Browser"] = "browser";
|
|
57
|
+
AuthType["Secret"] = "secret";
|
|
59
58
|
})(AuthType || (AuthType = {}));
|
|
60
59
|
export var CertificateType;
|
|
61
60
|
(function (CertificateType) {
|
|
@@ -691,7 +690,7 @@ export class Auth {
|
|
|
691
690
|
const allConnections = await this.getAllConnections();
|
|
692
691
|
const connection = allConnections.find(i => i.name === name);
|
|
693
692
|
if (!connection) {
|
|
694
|
-
throw new CommandError(`The connection '${name}' cannot be found
|
|
693
|
+
throw new CommandError(`The connection '${name}' cannot be found.`);
|
|
695
694
|
}
|
|
696
695
|
return connection;
|
|
697
696
|
}
|
|
@@ -702,7 +701,7 @@ export class Auth {
|
|
|
702
701
|
const details = {
|
|
703
702
|
connectionName: connection.name,
|
|
704
703
|
connectedAs: connection.identityName,
|
|
705
|
-
authType:
|
|
704
|
+
authType: connection.authType,
|
|
706
705
|
appId: connection.appId,
|
|
707
706
|
appTenant: connection.tenant,
|
|
708
707
|
cloudType: CloudType[connection.cloudType]
|
|
@@ -710,7 +709,7 @@ export class Auth {
|
|
|
710
709
|
return details;
|
|
711
710
|
}
|
|
712
711
|
}
|
|
713
|
-
Auth.cloudEndpoints =
|
|
712
|
+
Auth.cloudEndpoints = {};
|
|
714
713
|
Auth.initialize();
|
|
715
714
|
export default new Auth();
|
|
716
715
|
//# sourceMappingURL=Auth.js.map
|
package/dist/Command.js
CHANGED
|
@@ -5,6 +5,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
5
5
|
};
|
|
6
6
|
var _Command_instances, _Command_initTelemetry, _Command_initOptions, _Command_initValidators;
|
|
7
7
|
import os from 'os';
|
|
8
|
+
import { z } from 'zod';
|
|
8
9
|
import auth from './Auth.js';
|
|
9
10
|
import { cli } from './cli/cli.js';
|
|
10
11
|
import request from './request.js';
|
|
@@ -13,6 +14,7 @@ import { telemetry } from './telemetry.js';
|
|
|
13
14
|
import { accessToken } from './utils/accessToken.js';
|
|
14
15
|
import { md } from './utils/md.js';
|
|
15
16
|
import { prompt } from './utils/prompt.js';
|
|
17
|
+
import { zod } from './utils/zod.js';
|
|
16
18
|
export class CommandError {
|
|
17
19
|
constructor(message, code) {
|
|
18
20
|
this.message = message;
|
|
@@ -25,10 +27,26 @@ export class CommandErrorWithOutput {
|
|
|
25
27
|
this.stderr = stderr;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
30
|
+
export const globalOptionsZod = z.object({
|
|
31
|
+
query: z.string().optional(),
|
|
32
|
+
output: zod.alias('o', z.enum(['csv', 'json', 'md', 'text', 'none']).optional()),
|
|
33
|
+
debug: z.boolean().default(false),
|
|
34
|
+
verbose: z.boolean().default(false)
|
|
35
|
+
});
|
|
28
36
|
class Command {
|
|
29
37
|
get allowedOutputs() {
|
|
30
38
|
return ['csv', 'json', 'md', 'text', 'none'];
|
|
31
39
|
}
|
|
40
|
+
get schema() {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
44
|
+
getRefinedSchema(schema) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
getSchemaToParse() {
|
|
48
|
+
return this.getRefinedSchema(this.schema) ?? this.schema;
|
|
49
|
+
}
|
|
32
50
|
constructor() {
|
|
33
51
|
// These functions must be defined with # so that they're truly private
|
|
34
52
|
// otherwise you'll get a ts2415 error (Types have separate declarations of
|
|
@@ -48,6 +66,9 @@ class Command {
|
|
|
48
66
|
string: []
|
|
49
67
|
};
|
|
50
68
|
this.validators = [];
|
|
69
|
+
// metadata for command's options
|
|
70
|
+
// used for building telemetry
|
|
71
|
+
this.optionsInfo = [];
|
|
51
72
|
__classPrivateFieldGet(this, _Command_instances, "m", _Command_initTelemetry).call(this);
|
|
52
73
|
__classPrivateFieldGet(this, _Command_instances, "m", _Command_initOptions).call(this);
|
|
53
74
|
__classPrivateFieldGet(this, _Command_instances, "m", _Command_initValidators).call(this);
|
|
@@ -90,9 +111,7 @@ class Command {
|
|
|
90
111
|
prompted = true;
|
|
91
112
|
await cli.error('🌶️ Provide values for the following parameters:');
|
|
92
113
|
}
|
|
93
|
-
const answer = optionInfo
|
|
94
|
-
? await prompt.forSelection({ message: `${optionInfo.name}: `, choices: optionInfo.autocomplete.map((choice) => { return { name: choice, value: choice }; }) })
|
|
95
|
-
: await prompt.forInput({ message: `${optionInfo.name}: ` });
|
|
114
|
+
const answer = await cli.promptForValue(optionInfo);
|
|
96
115
|
args.options[optionInfo.name] = answer;
|
|
97
116
|
}
|
|
98
117
|
if (prompted) {
|
|
@@ -410,8 +429,34 @@ class Command {
|
|
|
410
429
|
return '';
|
|
411
430
|
}
|
|
412
431
|
getTelemetryProperties(args) {
|
|
413
|
-
this.
|
|
414
|
-
|
|
432
|
+
if (this.schema) {
|
|
433
|
+
const telemetryProperties = {};
|
|
434
|
+
this.optionsInfo.forEach(o => {
|
|
435
|
+
if (o.required) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (typeof args.options[o.name] === 'undefined') {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
switch (o.type) {
|
|
442
|
+
case 'string':
|
|
443
|
+
telemetryProperties[o.name] = o.autocomplete ? args.options[o.name] : typeof args.options[o.name] !== 'undefined';
|
|
444
|
+
break;
|
|
445
|
+
case 'boolean':
|
|
446
|
+
telemetryProperties[o.name] = args.options[o.name];
|
|
447
|
+
break;
|
|
448
|
+
case 'number':
|
|
449
|
+
telemetryProperties[o.name] = typeof args.options[o.name] !== 'undefined';
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
;
|
|
453
|
+
});
|
|
454
|
+
return telemetryProperties;
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
this.telemetry.forEach(t => t(args));
|
|
458
|
+
return this.telemetryProperties;
|
|
459
|
+
}
|
|
415
460
|
}
|
|
416
461
|
async getTextOutput(logStatement) {
|
|
417
462
|
// display object as a list of key-value pairs
|
package/dist/cli/cli.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import Configstore from 'configstore';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
import minimist from 'minimist';
|
|
4
3
|
import { createRequire } from 'module';
|
|
5
4
|
import os from 'os';
|
|
6
5
|
import path from 'path';
|
|
7
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
7
|
+
import yargs from 'yargs-parser';
|
|
8
|
+
import { ZodError } from 'zod';
|
|
8
9
|
import Command, { CommandError } from '../Command.js';
|
|
9
10
|
import config from '../config.js';
|
|
10
11
|
import request from '../request.js';
|
|
11
12
|
import { settingsNames } from '../settingsNames.js';
|
|
12
13
|
import { telemetry } from '../telemetry.js';
|
|
13
14
|
import { app } from '../utils/app.js';
|
|
15
|
+
import { browserUtil } from '../utils/browserUtil.js';
|
|
14
16
|
import { formatting } from '../utils/formatting.js';
|
|
15
17
|
import { md } from '../utils/md.js';
|
|
16
|
-
import { validation } from '../utils/validation.js';
|
|
17
18
|
import { prompt } from '../utils/prompt.js';
|
|
19
|
+
import { validation } from '../utils/validation.js';
|
|
20
|
+
import { zod } from '../utils/zod.js';
|
|
18
21
|
import { timings } from './timings.js';
|
|
19
|
-
import { browserUtil } from '../utils/browserUtil.js';
|
|
20
22
|
const require = createRequire(import.meta.url);
|
|
21
23
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
22
24
|
let _config;
|
|
@@ -34,6 +36,14 @@ const defaultHelpMode = 'options';
|
|
|
34
36
|
const defaultHelpTarget = 'console';
|
|
35
37
|
const helpModes = ['options', 'examples', 'remarks', 'response', 'full'];
|
|
36
38
|
const helpTargets = ['console', 'web'];
|
|
39
|
+
const yargsConfiguration = {
|
|
40
|
+
'parse-numbers': true,
|
|
41
|
+
'strip-aliased': true,
|
|
42
|
+
'strip-dashed': true,
|
|
43
|
+
'dot-notation': false,
|
|
44
|
+
'boolean-negation': true,
|
|
45
|
+
'camel-case-expansion': false
|
|
46
|
+
};
|
|
37
47
|
function getConfig() {
|
|
38
48
|
if (!_config) {
|
|
39
49
|
_config = new Configstore(config.configstoreName);
|
|
@@ -49,6 +59,12 @@ function getSettingWithDefaultValue(settingName, defaultValue) {
|
|
|
49
59
|
return configuredValue;
|
|
50
60
|
}
|
|
51
61
|
}
|
|
62
|
+
function getClientId() {
|
|
63
|
+
return cli.getSettingWithDefaultValue(settingsNames.clientId, process.env.CLIMICROSOFT365_ENTRAAPPID || process.env.CLIMICROSOFT365_AADAPPID);
|
|
64
|
+
}
|
|
65
|
+
function getTenant() {
|
|
66
|
+
return cli.getSettingWithDefaultValue(settingsNames.tenantId, process.env.CLIMICROSOFT365_TENANT || 'common');
|
|
67
|
+
}
|
|
52
68
|
async function execute(rawArgs) {
|
|
53
69
|
const start = process.hrtime.bigint();
|
|
54
70
|
// for completion commands we also need information about commands' options
|
|
@@ -64,7 +80,7 @@ async function execute(rawArgs) {
|
|
|
64
80
|
rawArgs.shift();
|
|
65
81
|
}
|
|
66
82
|
// parse args to see if a command has been specified
|
|
67
|
-
const parsedArgs =
|
|
83
|
+
const parsedArgs = yargs(rawArgs);
|
|
68
84
|
// load command
|
|
69
85
|
await cli.loadCommandFromArgs(parsedArgs._);
|
|
70
86
|
if (cli.commandToExecute) {
|
|
@@ -77,8 +93,7 @@ async function execute(rawArgs) {
|
|
|
77
93
|
};
|
|
78
94
|
}
|
|
79
95
|
catch (e) {
|
|
80
|
-
|
|
81
|
-
return cli.closeWithError(e.message, optionsWithoutShorts, false);
|
|
96
|
+
return cli.closeWithError(e.message, { options: parsedArgs }, false);
|
|
82
97
|
}
|
|
83
98
|
}
|
|
84
99
|
else {
|
|
@@ -126,18 +141,54 @@ async function execute(rawArgs) {
|
|
|
126
141
|
if (cli.optionsFromArgs.options.output === undefined) {
|
|
127
142
|
cli.optionsFromArgs.options.output = cli.getSettingWithDefaultValue(settingsNames.output, 'json');
|
|
128
143
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
144
|
+
let finalArgs = cli.optionsFromArgs.options;
|
|
145
|
+
if (cli.commandToExecute?.command.schema) {
|
|
146
|
+
while (true) {
|
|
147
|
+
const startValidation = process.hrtime.bigint();
|
|
148
|
+
const result = cli.commandToExecute.command.getSchemaToParse().safeParse(cli.optionsFromArgs.options);
|
|
149
|
+
const endValidation = process.hrtime.bigint();
|
|
150
|
+
timings.validation.push(Number(endValidation - startValidation));
|
|
151
|
+
if (result.success) {
|
|
152
|
+
finalArgs = result.data;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const hasNonRequiredErrors = result.error.errors.some(e => e.code !== 'invalid_type' || e.received !== 'undefined');
|
|
157
|
+
const shouldPrompt = cli.getSettingWithDefaultValue(settingsNames.prompt, true);
|
|
158
|
+
if (hasNonRequiredErrors === false &&
|
|
159
|
+
shouldPrompt) {
|
|
160
|
+
await cli.error('🌶️ Provide values for the following parameters:');
|
|
161
|
+
for (const error of result.error.errors) {
|
|
162
|
+
const optionInfo = cli.commandToExecute.options.find(o => o.name === error.path.join('.'));
|
|
163
|
+
const answer = await cli.promptForValue(optionInfo);
|
|
164
|
+
cli.optionsFromArgs.options[error.path.join('.')] = answer;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
result.error.errors.forEach(e => {
|
|
169
|
+
if (e.code === 'invalid_type' &&
|
|
170
|
+
e.received === 'undefined') {
|
|
171
|
+
e.message = `Required option not specified`;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
return cli.closeWithError(result.error, cli.optionsFromArgs, true);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const startValidation = process.hrtime.bigint();
|
|
181
|
+
const validationResult = await cli.commandToExecute.command.validate(cli.optionsFromArgs, cli.commandToExecute);
|
|
182
|
+
const endValidation = process.hrtime.bigint();
|
|
183
|
+
timings.validation.push(Number(endValidation - startValidation));
|
|
184
|
+
if (validationResult !== true) {
|
|
185
|
+
return cli.closeWithError(validationResult, cli.optionsFromArgs, true);
|
|
186
|
+
}
|
|
135
187
|
}
|
|
136
|
-
cli.optionsFromArgs = removeShortOptions(cli.optionsFromArgs);
|
|
137
188
|
const end = process.hrtime.bigint();
|
|
138
189
|
timings.core.push(Number(end - start));
|
|
139
190
|
try {
|
|
140
|
-
await cli.executeCommand(cli.commandToExecute.command,
|
|
191
|
+
await cli.executeCommand(cli.commandToExecute.command, { options: finalArgs });
|
|
141
192
|
const endTotal = process.hrtime.bigint();
|
|
142
193
|
timings.total.push(Number(endTotal - start));
|
|
143
194
|
await printTimings(rawArgs);
|
|
@@ -347,12 +398,14 @@ async function loadCommandFromFile(commandFileUrl) {
|
|
|
347
398
|
catch { }
|
|
348
399
|
}
|
|
349
400
|
function getCommandInfo(command, filePath = '', helpFilePath = '') {
|
|
401
|
+
const options = command.schema ? zod.schemaToOptions(command.schema) : getCommandOptions(command);
|
|
402
|
+
command.optionsInfo = options;
|
|
350
403
|
return {
|
|
351
404
|
aliases: command.alias(),
|
|
352
405
|
name: command.name,
|
|
353
406
|
description: command.description,
|
|
354
407
|
command: command,
|
|
355
|
-
options
|
|
408
|
+
options,
|
|
356
409
|
defaultProperties: command.defaultProperties(),
|
|
357
410
|
file: filePath,
|
|
358
411
|
help: helpFilePath
|
|
@@ -387,36 +440,43 @@ function getCommandOptions(command) {
|
|
|
387
440
|
return options;
|
|
388
441
|
}
|
|
389
442
|
function getCommandOptionsFromArgs(args, commandInfo) {
|
|
390
|
-
const
|
|
391
|
-
alias: {}
|
|
443
|
+
const yargsOptions = {
|
|
444
|
+
alias: {},
|
|
445
|
+
configuration: yargsConfiguration
|
|
392
446
|
};
|
|
393
447
|
let argsToParse = args;
|
|
394
448
|
if (commandInfo) {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
449
|
+
if (commandInfo.command.schema) {
|
|
450
|
+
yargsOptions.string = commandInfo.options.filter(o => o.type === 'string').map(o => o.name);
|
|
451
|
+
yargsOptions.boolean = commandInfo.options.filter(o => o.type === 'boolean').map(o => o.name);
|
|
452
|
+
yargsOptions.number = commandInfo.options.filter(o => o.type === 'number').map(o => o.name);
|
|
453
|
+
argsToParse = getRewrittenArgs(args, yargsOptions.boolean);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
const commandTypes = commandInfo.command.types;
|
|
457
|
+
if (commandTypes) {
|
|
458
|
+
yargsOptions.string = commandTypes.string;
|
|
459
|
+
// minimist will parse unused boolean options to 'false' (unused options => options that are not included in the args)
|
|
460
|
+
// But in the CLI booleans are nullable. They can can be true, false or undefined.
|
|
461
|
+
// For this reason we only pass boolean types that are actually used as arg.
|
|
462
|
+
yargsOptions.boolean = commandTypes.boolean.filter(optionName => args.some(arg => `--${optionName}` === arg || `-${optionName}` === arg));
|
|
463
|
+
}
|
|
464
|
+
argsToParse = getRewrittenArgs(args, commandTypes.boolean);
|
|
465
|
+
}
|
|
404
466
|
commandInfo.options.forEach(option => {
|
|
405
467
|
if (option.short && option.long) {
|
|
406
|
-
|
|
468
|
+
yargsOptions.alias[option.long] = option.short;
|
|
407
469
|
}
|
|
408
470
|
});
|
|
409
|
-
argsToParse = getRewrittenArgs(args, commandTypes);
|
|
410
471
|
}
|
|
411
|
-
return
|
|
472
|
+
return yargs(argsToParse, yargsOptions);
|
|
412
473
|
}
|
|
413
474
|
/**
|
|
414
475
|
* Rewrites arguments (if necessary) before passing them into minimist.
|
|
415
476
|
* Currently only boolean values are checked and fixed.
|
|
416
477
|
* Args are only checked and rewritten if the option has been added to the 'types.boolean' array.
|
|
417
478
|
*/
|
|
418
|
-
function getRewrittenArgs(args,
|
|
419
|
-
const booleanTypes = commandTypes.boolean;
|
|
479
|
+
function getRewrittenArgs(args, booleanTypes) {
|
|
420
480
|
if (booleanTypes.length === 0) {
|
|
421
481
|
return args;
|
|
422
482
|
}
|
|
@@ -722,6 +782,9 @@ async function closeWithError(error, args, showHelpIfEnabled = false) {
|
|
|
722
782
|
return process.exit(exitCode);
|
|
723
783
|
}
|
|
724
784
|
let errorMessage = error instanceof CommandError ? error.message : error;
|
|
785
|
+
if (error instanceof ZodError) {
|
|
786
|
+
errorMessage = error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(os.EOL);
|
|
787
|
+
}
|
|
725
788
|
if ((!args.options.output || args.options.output === 'json') &&
|
|
726
789
|
!cli.getSettingWithDefaultValue(settingsNames.printErrorsAsPlainText, true)) {
|
|
727
790
|
errorMessage = JSON.stringify({ error: errorMessage });
|
|
@@ -760,6 +823,16 @@ async function error(message, ...optionalParams) {
|
|
|
760
823
|
console.error(message, ...optionalParams);
|
|
761
824
|
}
|
|
762
825
|
}
|
|
826
|
+
async function promptForValue(optionInfo) {
|
|
827
|
+
return optionInfo.autocomplete !== undefined
|
|
828
|
+
? await prompt.forSelection({
|
|
829
|
+
message: `${optionInfo.name}: `,
|
|
830
|
+
choices: optionInfo.autocomplete.map((choice) => {
|
|
831
|
+
return { name: choice, value: choice };
|
|
832
|
+
})
|
|
833
|
+
})
|
|
834
|
+
: await prompt.forInput({ message: `${optionInfo.name}: ` });
|
|
835
|
+
}
|
|
763
836
|
async function promptForSelection(config) {
|
|
764
837
|
const answer = await prompt.forSelection(config);
|
|
765
838
|
await cli.error('');
|
|
@@ -770,6 +843,11 @@ async function promptForConfirmation(config) {
|
|
|
770
843
|
await cli.error('');
|
|
771
844
|
return answer;
|
|
772
845
|
}
|
|
846
|
+
async function promptForInput(config) {
|
|
847
|
+
const answer = await prompt.forInput(config);
|
|
848
|
+
await cli.error('');
|
|
849
|
+
return answer;
|
|
850
|
+
}
|
|
773
851
|
async function handleMultipleResultsFound(message, values) {
|
|
774
852
|
const prompt = cli.getSettingWithDefaultValue(settingsNames.prompt, true);
|
|
775
853
|
if (!prompt) {
|
|
@@ -780,13 +858,6 @@ async function handleMultipleResultsFound(message, values) {
|
|
|
780
858
|
const response = await cli.promptForSelection({ message: `Please choose one:`, choices });
|
|
781
859
|
return values[response];
|
|
782
860
|
}
|
|
783
|
-
function removeShortOptions(args) {
|
|
784
|
-
const filteredArgs = JSON.parse(JSON.stringify(args));
|
|
785
|
-
const optionsToRemove = Object.getOwnPropertyNames(args.options)
|
|
786
|
-
.filter(option => option.length === 1 || option === '--');
|
|
787
|
-
optionsToRemove.forEach(option => delete filteredArgs.options[option]);
|
|
788
|
-
return filteredArgs;
|
|
789
|
-
}
|
|
790
861
|
function loadOptionValuesFromFiles(args) {
|
|
791
862
|
const optionNames = Object.getOwnPropertyNames(args.options);
|
|
792
863
|
optionNames.forEach(option => {
|
|
@@ -811,7 +882,9 @@ export const cli = {
|
|
|
811
882
|
closeWithError,
|
|
812
883
|
commands,
|
|
813
884
|
commandToExecute,
|
|
885
|
+
getClientId,
|
|
814
886
|
getConfig,
|
|
887
|
+
getTenant,
|
|
815
888
|
currentCommandName,
|
|
816
889
|
error,
|
|
817
890
|
execute,
|
|
@@ -830,7 +903,10 @@ export const cli = {
|
|
|
830
903
|
optionsFromArgs,
|
|
831
904
|
printAvailableCommands,
|
|
832
905
|
promptForConfirmation,
|
|
906
|
+
promptForInput,
|
|
833
907
|
promptForSelection,
|
|
834
|
-
|
|
908
|
+
promptForValue,
|
|
909
|
+
shouldTrimOutput,
|
|
910
|
+
yargsConfiguration
|
|
835
911
|
};
|
|
836
912
|
//# sourceMappingURL=cli.js.map
|
package/dist/config.js
CHANGED
|
@@ -1,10 +1,65 @@
|
|
|
1
|
-
import { app } from
|
|
2
|
-
const cliEntraAppId = '31359c7f-bd7e-475c-86db-fdb8c937548e';
|
|
1
|
+
import { app } from './utils/app.js';
|
|
3
2
|
export default {
|
|
3
|
+
allScopes: [
|
|
4
|
+
'https://graph.windows.net/Directory.AccessAsUser.All',
|
|
5
|
+
'https://management.azure.com/user_impersonation',
|
|
6
|
+
'https://admin.services.crm.dynamics.com/user_impersonation',
|
|
7
|
+
'https://graph.microsoft.com/AppCatalog.ReadWrite.All',
|
|
8
|
+
'https://graph.microsoft.com/AuditLog.Read.All',
|
|
9
|
+
'https://graph.microsoft.com/Bookings.Read.All',
|
|
10
|
+
'https://graph.microsoft.com/Calendars.Read',
|
|
11
|
+
'https://graph.microsoft.com/ChannelMember.ReadWrite.All',
|
|
12
|
+
'https://graph.microsoft.com/ChannelMessage.Read.All',
|
|
13
|
+
'https://graph.microsoft.com/ChannelMessage.ReadWrite',
|
|
14
|
+
'https://graph.microsoft.com/ChannelMessage.Send',
|
|
15
|
+
'https://graph.microsoft.com/ChannelSettings.ReadWrite.All',
|
|
16
|
+
'https://graph.microsoft.com/Chat.ReadWrite',
|
|
17
|
+
'https://graph.microsoft.com/Directory.AccessAsUser.All',
|
|
18
|
+
'https://graph.microsoft.com/Directory.ReadWrite.All',
|
|
19
|
+
'https://graph.microsoft.com/ExternalConnection.ReadWrite.All',
|
|
20
|
+
'https://graph.microsoft.com/ExternalItem.ReadWrite.All',
|
|
21
|
+
'https://graph.microsoft.com/Group.ReadWrite.All',
|
|
22
|
+
'https://graph.microsoft.com/IdentityProvider.ReadWrite.All',
|
|
23
|
+
'https://graph.microsoft.com/InformationProtectionPolicy.Read',
|
|
24
|
+
'https://graph.microsoft.com/Mail.Read.Shared',
|
|
25
|
+
'https://graph.microsoft.com/Mail.ReadWrite',
|
|
26
|
+
'https://graph.microsoft.com/Mail.Send',
|
|
27
|
+
'https://graph.microsoft.com/Notes.ReadWrite.All',
|
|
28
|
+
'https://graph.microsoft.com/OnlineMeetingArtifact.Read.All',
|
|
29
|
+
'https://graph.microsoft.com/OnlineMeetings.ReadWrite',
|
|
30
|
+
'https://graph.microsoft.com/OnlineMeetingTranscript.Read.All',
|
|
31
|
+
'https://graph.microsoft.com/PeopleSettings.ReadWrite.All',
|
|
32
|
+
'https://graph.microsoft.com/Place.Read.All',
|
|
33
|
+
'https://graph.microsoft.com/Policy.Read.All',
|
|
34
|
+
'https://graph.microsoft.com/RecordsManagement.ReadWrite.All',
|
|
35
|
+
'https://graph.microsoft.com/Reports.Read.All',
|
|
36
|
+
'https://graph.microsoft.com/RoleAssignmentSchedule.ReadWrite.Directory',
|
|
37
|
+
'https://graph.microsoft.com/RoleEligibilitySchedule.Read.Directory',
|
|
38
|
+
'https://graph.microsoft.com/SecurityEvents.Read.All',
|
|
39
|
+
'https://graph.microsoft.com/ServiceHealth.Read.All',
|
|
40
|
+
'https://graph.microsoft.com/ServiceMessage.Read.All',
|
|
41
|
+
'https://graph.microsoft.com/ServiceMessageViewpoint.Write',
|
|
42
|
+
'https://graph.microsoft.com/Sites.Read.All',
|
|
43
|
+
'https://graph.microsoft.com/Tasks.ReadWrite',
|
|
44
|
+
'https://graph.microsoft.com/Team.Create',
|
|
45
|
+
'https://graph.microsoft.com/TeamMember.ReadWrite.All',
|
|
46
|
+
'https://graph.microsoft.com/TeamsAppInstallation.ReadWriteForUser',
|
|
47
|
+
'https://graph.microsoft.com/TeamSettings.ReadWrite.All',
|
|
48
|
+
'https://graph.microsoft.com/TeamsTab.ReadWrite.All',
|
|
49
|
+
'https://graph.microsoft.com/User.Invite.All',
|
|
50
|
+
'https://manage.office.com/ActivityFeed.Read',
|
|
51
|
+
'https://manage.office.com/ServiceHealth.Read',
|
|
52
|
+
'https://analysis.windows.net/powerbi/api/Dataset.Read.All',
|
|
53
|
+
'https://api.powerapps.com//User',
|
|
54
|
+
'https://microsoft.sharepoint-df.com/AllSites.FullControl',
|
|
55
|
+
'https://microsoft.sharepoint-df.com/TermStore.ReadWrite.All',
|
|
56
|
+
'https://microsoft.sharepoint-df.com/User.ReadWrite.All'
|
|
57
|
+
],
|
|
4
58
|
applicationName: `CLI for Microsoft 365 v${app.packageJson().version}`,
|
|
5
59
|
delimiter: 'm365\$',
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
60
|
+
configstoreName: 'cli-m365-config',
|
|
61
|
+
minimalScopes: [
|
|
62
|
+
'https://graph.microsoft.com/User.Read'
|
|
63
|
+
]
|
|
9
64
|
};
|
|
10
65
|
//# sourceMappingURL=config.js.map
|
|
@@ -94,7 +94,7 @@ export default class SpoCommand extends Command {
|
|
|
94
94
|
catch (error) {
|
|
95
95
|
throw new CommandError(error);
|
|
96
96
|
}
|
|
97
|
-
if (auth.connection.active &&
|
|
97
|
+
if (auth.connection.active && auth.connection.authType === AuthType.Secret) {
|
|
98
98
|
throw new CommandError(`SharePoint does not support authentication using client ID and secret. Please use a different login type to use SharePoint commands.`);
|
|
99
99
|
}
|
|
100
100
|
await super.action(logger, args);
|
|
@@ -4,7 +4,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
4
4
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
5
|
};
|
|
6
6
|
var _CliConsentCommand_instances, _CliConsentCommand_initTelemetry, _CliConsentCommand_initOptions, _CliConsentCommand_initValidators;
|
|
7
|
-
import
|
|
7
|
+
import { cli } from '../../../cli/cli.js';
|
|
8
8
|
import AnonymousCommand from '../../base/AnonymousCommand.js';
|
|
9
9
|
import commands from '../commands.js';
|
|
10
10
|
class CliConsentCommand extends AnonymousCommand {
|
|
@@ -30,7 +30,7 @@ class CliConsentCommand extends AnonymousCommand {
|
|
|
30
30
|
scope = 'https://api.yammer.com/user_impersonation';
|
|
31
31
|
break;
|
|
32
32
|
}
|
|
33
|
-
await logger.log(`To consent permissions for executing ${args.options.service} commands, navigate in your web browser to https://login.microsoftonline.com/${
|
|
33
|
+
await logger.log(`To consent permissions for executing ${args.options.service} commands, navigate in your web browser to https://login.microsoftonline.com/${cli.getTenant()}/oauth2/v2.0/authorize?client_id=${cli.getClientId()}&response_type=code&scope=${encodeURIComponent(scope)}`);
|
|
34
34
|
}
|
|
35
35
|
async action(logger, args) {
|
|
36
36
|
await this.initAction(args, logger);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
|
-
import auth
|
|
2
|
+
import auth from '../../../Auth.js';
|
|
3
3
|
import { cli } from '../../../cli/cli.js';
|
|
4
4
|
import Command from '../../../Command.js';
|
|
5
5
|
import { app } from '../../../utils/app.js';
|
|
@@ -33,7 +33,7 @@ class CliDoctorCommand extends Command {
|
|
|
33
33
|
nodeVersion: process.version,
|
|
34
34
|
cliAadAppId: auth.connection.appId,
|
|
35
35
|
cliAadAppTenant: validation.isValidGuid(auth.connection.tenant) ? 'single' : auth.connection.tenant,
|
|
36
|
-
authMode:
|
|
36
|
+
authMode: auth.connection.authType,
|
|
37
37
|
cliEnvironment: process.env.CLIMICROSOFT365_ENV ? process.env.CLIMICROSOFT365_ENV : '',
|
|
38
38
|
cliConfig: cli.getConfig().all,
|
|
39
39
|
roles: roles,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { cli } from '../../../cli/cli.js';
|
|
2
|
-
import config from '../../../config.js';
|
|
3
2
|
import { settingsNames } from '../../../settingsNames.js';
|
|
4
3
|
import { browserUtil } from '../../../utils/browserUtil.js';
|
|
5
4
|
import AnonymousCommand from '../../base/AnonymousCommand.js';
|
|
@@ -12,9 +11,9 @@ class CliReconsentCommand extends AnonymousCommand {
|
|
|
12
11
|
return 'Returns URL to open in the browser to re-consent CLI for Microsoft 365 Microsoft Entra permissions';
|
|
13
12
|
}
|
|
14
13
|
async commandAction(logger) {
|
|
15
|
-
const url = `https://login.microsoftonline.com/${
|
|
14
|
+
const url = `https://login.microsoftonline.com/${cli.getTenant()}/oauth2/authorize?client_id=${cli.getClientId()}&response_type=code&prompt=admin_consent`;
|
|
16
15
|
if (cli.getSettingWithDefaultValue(settingsNames.autoOpenLinksInBrowser, false) === false) {
|
|
17
|
-
await logger.log(`To re-consent
|
|
16
|
+
await logger.log(`To re-consent your Microsoft Entra application, navigate in your web browser to ${url}.`);
|
|
18
17
|
return;
|
|
19
18
|
}
|
|
20
19
|
await logger.log(`Opening the following page in your browser: ${url}`);
|
|
@@ -4,8 +4,10 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
4
4
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
5
|
};
|
|
6
6
|
var _CliConfigSetCommand_instances, _a, _CliConfigSetCommand_initTelemetry, _CliConfigSetCommand_initOptions, _CliConfigSetCommand_initValidators;
|
|
7
|
+
import { AuthType } from "../../../../Auth.js";
|
|
7
8
|
import { cli } from "../../../../cli/cli.js";
|
|
8
9
|
import { settingsNames } from "../../../../settingsNames.js";
|
|
10
|
+
import { validation } from "../../../../utils/validation.js";
|
|
9
11
|
import AnonymousCommand from "../../../base/AnonymousCommand.js";
|
|
10
12
|
import commands from "../../commands.js";
|
|
11
13
|
class CliConfigSetCommand extends AnonymousCommand {
|
|
@@ -82,15 +84,22 @@ _a = CliConfigSetCommand, _CliConfigSetCommand_instances = new WeakSet(), _CliCo
|
|
|
82
84
|
cli.helpModes.indexOf(args.options.value) === -1) {
|
|
83
85
|
return `${args.options.value} is not a valid value for the option ${args.options.key}. Allowed values: ${cli.helpModes.join(', ')}`;
|
|
84
86
|
}
|
|
85
|
-
const allowedAuthTypes = ['certificate', 'deviceCode', 'password', 'identity', 'browser', 'secret'];
|
|
86
87
|
if (args.options.key === settingsNames.authType &&
|
|
87
|
-
|
|
88
|
-
return `${args.options.value} is not a valid value for the option ${args.options.key}. Allowed values: ${
|
|
88
|
+
!Object.values(AuthType).map(String).includes(args.options.value)) {
|
|
89
|
+
return `${args.options.value} is not a valid value for the option ${args.options.key}. Allowed values: ${Object.values(AuthType).join(', ')}`;
|
|
89
90
|
}
|
|
90
91
|
if (args.options.key === settingsNames.helpTarget &&
|
|
91
92
|
!cli.helpTargets.includes(args.options.value)) {
|
|
92
93
|
return `${args.options.value} is not a valid value for the option ${args.options.key}. Allowed values: ${cli.helpTargets.join(', ')}`;
|
|
93
94
|
}
|
|
95
|
+
if (args.options.key === settingsNames.clientId &&
|
|
96
|
+
!validation.isValidGuid(args.options.value)) {
|
|
97
|
+
return `${args.options.value} is not a valid value for the option ${args.options.key}. The value has to be a valid GUID.`;
|
|
98
|
+
}
|
|
99
|
+
if (args.options.key === settingsNames.tenantId &&
|
|
100
|
+
!(args.options.value === 'common' || validation.isValidGuid(args.options.value))) {
|
|
101
|
+
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'.`;
|
|
102
|
+
}
|
|
94
103
|
return true;
|
|
95
104
|
});
|
|
96
105
|
};
|