@pnp/cli-microsoft365
Version:
Manage Microsoft 365 and SharePoint Framework projects on any platform
920 lines • 35.8 kB
JavaScript
import Configstore from 'configstore';
import fs from 'fs';
import { createRequire } from 'module';
import os from 'os';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import yargs from 'yargs-parser';
import { ZodError } from 'zod';
import Command, { CommandError } from '../Command.js';
import config from '../config.js';
import request from '../request.js';
import { settingsNames } from '../settingsNames.js';
import { telemetry } from '../telemetry.js';
import { app } from '../utils/app.js';
import { browserUtil } from '../utils/browserUtil.js';
import { formatting } from '../utils/formatting.js';
import { md } from '../utils/md.js';
import { prompt } from '../utils/prompt.js';
import { validation } from '../utils/validation.js';
import { zod } from '../utils/zod.js';
import { timings } from './timings.js';
const require = createRequire(import.meta.url);
const __dirname = fileURLToPath(new URL('.', import.meta.url));
let _config;
const commands = [];
/**
* Command to execute
*/
let commandToExecute;
/**
* Name of the command specified through args
*/
let currentCommandName;
let optionsFromArgs;
const defaultHelpMode = 'options';
const defaultHelpTarget = 'console';
const helpModes = ['options', 'examples', 'remarks', 'permissions', 'response', 'full'];
const helpTargets = ['console', 'web'];
const yargsConfiguration = {
'parse-numbers': true,
'strip-aliased': true,
'strip-dashed': true,
'dot-notation': false,
'boolean-negation': true,
'camel-case-expansion': false
};
function getConfig() {
if (!_config) {
_config = new Configstore(config.configstoreName);
}
return _config;
}
function getSettingWithDefaultValue(settingName, defaultValue) {
const configuredValue = cli.getConfig().get(settingName);
if (typeof configuredValue === 'undefined') {
return defaultValue;
}
else {
return configuredValue;
}
}
function getClientId() {
return cli.getSettingWithDefaultValue(settingsNames.clientId, process.env.CLIMICROSOFT365_ENTRAAPPID);
}
function getTenant() {
return cli.getSettingWithDefaultValue(settingsNames.tenantId, process.env.CLIMICROSOFT365_TENANT || 'common');
}
async function execute(rawArgs) {
const start = process.hrtime.bigint();
// for completion commands we also need information about commands' options
const loadAllCommandInfo = rawArgs.indexOf('completion') > -1;
cli.loadAllCommandsInfo(loadAllCommandInfo);
// check if help for a specific command has been requested using the
// 'm365 help xyz' format. If so, remove 'help' from the array of words
// to use lazy loading commands but keep track of the fact that help should
// be displayed
let showHelp = false;
if (rawArgs.length > 0 && rawArgs[0] === 'help') {
showHelp = true;
rawArgs.shift();
}
// parse args to see if a command has been specified
const parsedArgs = yargs(rawArgs);
// load command
await cli.loadCommandFromArgs(parsedArgs._);
if (cli.commandToExecute) {
// we have found a command to execute. Parse args again taking into
// account short and long options, option types and whether the command
// supports known and unknown options or not
try {
cli.optionsFromArgs = {
options: getCommandOptionsFromArgs(rawArgs, cli.commandToExecute)
};
}
catch (e) {
return cli.closeWithError(e.message, { options: parsedArgs }, false);
}
}
else {
// we need this to properly support displaying commands
// from the current group
cli.optionsFromArgs = {
options: parsedArgs
};
}
// show help if no match found, help explicitly requested or
// no command specified
if (!cli.commandToExecute ||
showHelp ||
parsedArgs.h ||
parsedArgs.help) {
if (parsedArgs.output !== 'none') {
await printHelp(await getHelpMode(parsedArgs));
}
return;
}
delete cli.optionsFromArgs.options._;
delete cli.optionsFromArgs.options['--'];
try {
// replace values staring with @ with file contents
loadOptionValuesFromFiles(cli.optionsFromArgs);
}
catch (e) {
return cli.closeWithError(e, cli.optionsFromArgs);
}
const startProcessing = process.hrtime.bigint();
try {
// process options before passing them on to validation stage
const contextCommandOptions = await loadOptionsFromContext(cli.commandToExecute.options, cli.optionsFromArgs.options.debug);
cli.optionsFromArgs.options = { ...contextCommandOptions, ...cli.optionsFromArgs.options };
await cli.commandToExecute.command.processOptions(cli.optionsFromArgs.options);
const endProcessing = process.hrtime.bigint();
timings.options.push(Number(endProcessing - startProcessing));
}
catch (e) {
const endProcessing = process.hrtime.bigint();
timings.options.push(Number(endProcessing - startProcessing));
return cli.closeWithError(e.message, cli.optionsFromArgs, false);
}
// if output not specified, set the configured output value (if any)
if (cli.optionsFromArgs.options.output === undefined) {
cli.optionsFromArgs.options.output = cli.getSettingWithDefaultValue(settingsNames.output, 'json');
}
let finalArgs = cli.optionsFromArgs.options;
if (cli.commandToExecute?.command.schema) {
while (true) {
const startValidation = process.hrtime.bigint();
const result = cli.commandToExecute.command.getSchemaToParse().safeParse(cli.optionsFromArgs.options);
const endValidation = process.hrtime.bigint();
timings.validation.push(Number(endValidation - startValidation));
if (result.success) {
finalArgs = result.data;
break;
}
else {
const hasNonRequiredErrors = result.error.errors.some(e => e.code !== 'invalid_type' || e.received !== 'undefined');
const shouldPrompt = cli.getSettingWithDefaultValue(settingsNames.prompt, true);
if (hasNonRequiredErrors === false &&
shouldPrompt) {
await cli.error('🌶️ Provide values for the following parameters:');
for (const error of result.error.errors) {
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
try {
const parsed = getCommandOptionsFromArgs([`--${optionName}`, answer], cli.commandToExecute);
cli.optionsFromArgs.options[optionName] = parsed[optionName];
}
catch (e) {
return cli.closeWithError(e.message, cli.optionsFromArgs, true);
}
}
}
else {
result.error.errors.forEach(e => {
if (e.code === 'invalid_type' &&
e.received === 'undefined') {
e.message = `Required option not specified`;
}
});
return cli.closeWithError(result.error, cli.optionsFromArgs, true);
}
}
}
}
else {
const startValidation = process.hrtime.bigint();
const validationResult = await cli.commandToExecute.command.validate(cli.optionsFromArgs, cli.commandToExecute);
const endValidation = process.hrtime.bigint();
timings.validation.push(Number(endValidation - startValidation));
if (validationResult !== true) {
return cli.closeWithError(validationResult, cli.optionsFromArgs, true);
}
}
const end = process.hrtime.bigint();
timings.core.push(Number(end - start));
try {
await cli.executeCommand(cli.commandToExecute.command, { options: finalArgs });
const endTotal = process.hrtime.bigint();
timings.total.push(Number(endTotal - start));
await printTimings(rawArgs);
process.exit(0);
}
catch (err) {
const endTotal = process.hrtime.bigint();
timings.total.push(Number(endTotal - start));
await printTimings(rawArgs);
await cli.closeWithError(err, cli.optionsFromArgs);
/* c8 ignore next */
}
}
async function printTimings(rawArgs) {
if (rawArgs.some(arg => arg === '--debug')) {
await cli.error('');
await cli.error('Timings:');
Object.getOwnPropertyNames(timings).forEach(async (key) => {
await cli.error(`${key}: ${timings[key].reduce((a, b) => a + b, 0) / 1e6}ms`);
});
}
}
async function executeCommand(command, args) {
const logger = {
log: async (message) => {
if (args.options.output !== 'none') {
const output = await cli.formatOutput(command, message, args.options);
cli.log(output);
}
},
logRaw: async (message) => {
if (args.options.output !== 'none') {
cli.log(message);
}
},
logToStderr: async (message) => {
if (args.options.output !== 'none') {
await cli.error(message);
}
}
};
if (args.options.debug) {
await logger.logToStderr(`Executing command ${command.name} with options ${JSON.stringify(args)}`);
}
// store the current command name, if any and set the name to the name of
// the command to execute
const parentCommandName = cli.currentCommandName;
cli.currentCommandName = command.getCommandName(cli.currentCommandName);
const startCommand = process.hrtime.bigint();
try {
await command.action(logger, args);
if (args.options.debug || args.options.verbose) {
const chalk = (await import('chalk')).default;
await logger.logToStderr(chalk.green('DONE'));
}
}
finally {
// restore the original command name
cli.currentCommandName = parentCommandName;
const endCommand = process.hrtime.bigint();
timings.command.push(Number(endCommand - startCommand));
}
}
async function executeCommandWithOutput(command, args, listener) {
const log = [];
const logErr = [];
const logger = {
log: async (message) => {
const formattedMessage = await cli.formatOutput(command, message, args.options);
if (listener && listener.stdout) {
listener.stdout(formattedMessage);
}
log.push(formattedMessage);
},
logRaw: async (message) => {
const formattedMessage = await cli.formatOutput(command, message, args.options);
if (listener && listener.stdout) {
listener.stdout(formattedMessage);
}
log.push(formattedMessage);
},
logToStderr: async (message) => {
if (listener && listener.stderr) {
listener.stderr(message);
}
logErr.push(message);
}
};
if (args.options.debug && args.options.output !== 'none') {
const message = `Executing command ${command.name} with options ${JSON.stringify(args)}`;
if (listener && listener.stderr) {
listener.stderr(message);
}
logErr.push(message);
}
// store the current command name, if any and set the name to the name of
// the command to execute
const parentCommandName = cli.currentCommandName;
cli.currentCommandName = command.getCommandName();
// store the current logger if any
const currentLogger = request.logger;
try {
await command.action(logger, args);
return ({
stdout: log.join(os.EOL),
stderr: logErr.join(os.EOL)
});
}
catch (err) {
// restoring the command and logger is done here instead of in a 'finally' because there were issues with the code coverage tool
// restore the original command name
cli.currentCommandName = parentCommandName;
// restore the original logger
request.logger = currentLogger;
throw {
error: err,
stderr: logErr.join(os.EOL)
};
}
/* c8 ignore next */
finally {
// restore the original command name
cli.currentCommandName = parentCommandName;
// restore the original logger
request.logger = currentLogger;
}
}
function loadAllCommandsInfo(loadFull = false) {
const commandsInfoFileName = loadFull ? 'allCommandsFull.json' : 'allCommands.json';
cli.commands = require(path.join(__dirname, '..', '..', commandsInfoFileName));
}
/**
* Loads command files into CLI based on the specified arguments.
*
* @param commandNameWords Array of words specified as args
*/
async function loadCommandFromArgs(commandNameWords) {
if (commandNameWords.length === 0) {
return;
}
cli.currentCommandName = commandNameWords.join(' ');
const commandFilePath = cli.commands
.find(c => c.name === cli.currentCommandName ||
c.aliases?.find(a => a === cli.currentCommandName))?.file ?? '';
if (commandFilePath) {
await cli.loadCommandFromFile(commandFilePath);
}
}
async function loadOptionsFromContext(commandOptions, debug) {
const filePath = '.m365rc.json';
let m365rc = {};
if (!fs.existsSync(filePath)) {
return;
}
if (debug) {
await cli.error('found .m365rc.json file');
}
try {
const fileContents = fs.readFileSync(filePath, 'utf8');
if (fileContents) {
m365rc = JSON.parse(fileContents);
}
}
catch (e) {
await cli.closeWithError(`Error parsing ${filePath}`, { options: {} });
/* c8 ignore next */
}
if (!m365rc.context) {
return;
}
if (debug) {
await cli.error('found context in .m365rc.json file');
}
const context = m365rc.context;
const foundOptions = {};
await commandOptions.forEach(async (option) => {
if (context[option.name]) {
foundOptions[option.name] = context[option.name];
if (debug) {
await cli.error(`returning ${option.name} option from context`);
}
}
});
return foundOptions;
}
/**
* Loads command from the specified file into CLI. If can't find the file
* or the file doesn't contain a command, loads all available commands.
*
* @param commandFilePathUrl File path of the file with command to load
*/
async function loadCommandFromFile(commandFileUrl) {
const commandsFolder = path.join(__dirname, '../m365');
const filePath = path.join(commandsFolder, commandFileUrl);
if (!fs.existsSync(filePath)) {
// reset command name
cli.currentCommandName = undefined;
return;
}
try {
const command = await import(pathToFileURL(filePath).toString());
if (command.default instanceof Command) {
const commandInfo = cli.commands.find(c => c.file === commandFileUrl);
cli.commandToExecute = cli.getCommandInfo(command.default, commandFileUrl, commandInfo?.help);
}
}
catch { }
}
function getCommandInfo(command, filePath = '', helpFilePath = '') {
const options = command.schema ? zod.schemaToOptionInfo(command.schema) : getCommandOptions(command);
command.optionsInfo = options;
return {
aliases: command.alias(),
name: command.name,
description: command.description,
command: command,
options,
defaultProperties: command.defaultProperties(),
file: filePath,
help: helpFilePath
};
}
function getCommandOptions(command) {
const options = [];
command.options.forEach(option => {
const required = option.option.indexOf('<') > -1;
const optionArgs = option.option.split(/[ ,|]+/);
let short;
let long;
let name = '';
optionArgs.forEach(o => {
if (o.startsWith('--')) {
long = o.replace('--', '');
name = long;
}
else if (o.startsWith('-')) {
short = o.replace('-', '');
name = short;
}
});
options.push({
autocomplete: option.autocomplete,
long: long,
name: name,
required: required,
short: short
});
});
return options;
}
function getCommandOptionsFromArgs(args, commandInfo) {
const yargsOptions = {
alias: {},
configuration: yargsConfiguration
};
let argsToParse = args;
if (commandInfo) {
if (commandInfo.command.schema) {
yargsOptions.string = commandInfo.options.filter(o => o.type === 'string').map(o => o.name);
yargsOptions.boolean = commandInfo.options.filter(o => o.type === 'boolean').map(o => o.name);
yargsOptions.number = commandInfo.options.filter(o => o.type === 'number').map(o => o.name);
argsToParse = getRewrittenArgs(args, yargsOptions.boolean);
}
else {
const commandTypes = commandInfo.command.types;
if (commandTypes) {
yargsOptions.string = commandTypes.string;
// minimist will parse unused boolean options to 'false' (unused options => options that are not included in the args)
// But in the CLI booleans are nullable. They can can be true, false or undefined.
// For this reason we only pass boolean types that are actually used as arg.
yargsOptions.boolean = commandTypes.boolean.filter(optionName => args.some(arg => `--${optionName}` === arg || `-${optionName}` === arg));
}
argsToParse = getRewrittenArgs(args, commandTypes.boolean);
}
commandInfo.options.forEach(option => {
if (option.short && option.long) {
yargsOptions.alias[option.long] = option.short;
}
});
}
return yargs(argsToParse, yargsOptions);
}
/**
* Rewrites arguments (if necessary) before passing them into minimist.
* Currently only boolean values are checked and fixed.
* Args are only checked and rewritten if the option has been added to the 'types.boolean' array.
*/
function getRewrittenArgs(args, booleanTypes) {
if (booleanTypes.length === 0) {
return args;
}
return args.map((arg, index, array) => {
if (arg.startsWith('-') || index === 0) {
return arg;
}
// This line checks if the current arg is a value that belongs to a boolean option.
if (booleanTypes.some(t => `--${t}` === array[index - 1] || `-${t}` === array[index - 1])) {
const rewrittenBoolean = formatting.rewriteBooleanValue(arg);
if (!validation.isValidBoolean(rewrittenBoolean)) {
const optionName = array[index - 1];
throw new Error(`The value '${arg}' for option '${optionName}' is not a valid boolean`);
}
return rewrittenBoolean;
}
return arg;
});
}
async function formatOutput(command, logStatement, options) {
if (logStatement instanceof Date) {
return logStatement.toString();
}
let logStatementType = typeof logStatement;
if (logStatementType === 'undefined') {
return logStatement;
}
// we need to get the list of object's properties to see if the specified
// JMESPath query (if any) filters object's properties or not. We need to
// know this in order to decide if we should use default command's
// properties or custom ones from JMESPath
const originalObject = Array.isArray(logStatement) ? getFirstNonUndefinedArrayItem(logStatement) : logStatement;
const originalProperties = originalObject && typeof originalObject !== 'string' ? Object.getOwnPropertyNames(originalObject) : [];
if (options.query &&
!options.help) {
const jmespath = await import('jmespath');
try {
logStatement = jmespath.search(logStatement, options.query);
}
catch (e) {
const message = `JMESPath query error. ${e.message}. See https://jmespath.org/specification.html for more information`;
await cli.closeWithError(message, { options }, false);
/* c8 ignore next */
}
// we need to update the statement type in case the JMESPath query
// returns an object of different shape than the original message to log
// #2095
logStatementType = typeof logStatement;
}
if (!options.output || options.output === 'json') {
return command.getJsonOutput(logStatement);
}
if (logStatement instanceof CommandError) {
const chalk = (await import('chalk')).default;
return chalk.red(`Error: ${logStatement.message}`);
}
let arrayType = '';
if (!Array.isArray(logStatement)) {
logStatement = [logStatement];
arrayType = logStatementType;
}
else {
for (let i = 0; i < logStatement.length; i++) {
if (Array.isArray(logStatement[i])) {
arrayType = 'array';
break;
}
const t = typeof logStatement[i];
if (t !== 'undefined') {
arrayType = t;
break;
}
}
}
if (arrayType !== 'object') {
return logStatement.join(os.EOL);
}
// if output type has been set to 'text', process the retrieved
// data so that returned objects contain only default properties specified
// on the current command. If there is no current command or the
// command doesn't specify default properties, return original data
if (cli.shouldTrimOutput(options.output)) {
const currentCommand = cli.commandToExecute;
if (arrayType === 'object' &&
currentCommand && currentCommand.defaultProperties) {
// the log statement contains the same properties as the original object
// so it can be filtered following the default properties specified on
// the command
if (JSON.stringify(originalProperties) === JSON.stringify(Object.getOwnPropertyNames(logStatement[0]))) {
// in some cases we return properties wrapped in `value` array
// returned by the API. We'll remove it in the future, but for now
// we'll use a workaround to drop the `value` array here
if (logStatement[0].value &&
Array.isArray(logStatement[0].value)) {
logStatement = logStatement[0].value;
}
logStatement = logStatement.map((s) => formatting.filterObject(s, currentCommand.defaultProperties));
}
}
}
switch (options.output) {
case 'csv':
return command.getCsvOutput(logStatement, options);
case 'md':
return command.getMdOutput(logStatement, command, options);
default:
return command.getTextOutput(logStatement);
}
}
function getFirstNonUndefinedArrayItem(arr) {
for (let i = 0; i < arr.length; i++) {
const a = arr[i];
if (typeof a !== 'undefined') {
return a;
}
}
return undefined;
}
async function printHelp(helpMode, exitCode = 0) {
const properties = {};
if (cli.commandToExecute) {
properties.command = cli.commandToExecute.name;
const helpTarget = getSettingWithDefaultValue(settingsNames.helpTarget, defaultHelpTarget);
if (helpTarget === 'web') {
await openHelpInBrowser();
}
else {
printCommandHelp(helpMode);
}
}
else {
if (cli.currentCommandName && !cli.commands.some(command => command.name.startsWith(cli.currentCommandName))) {
const chalk = (await import('chalk')).default;
await cli.error(chalk.red(`Command '${cli.currentCommandName}' was not found. Below you can find the commands and command groups you can use. For detailed information on a command group, use 'm365 [command group] --help'.`));
}
cli.log();
cli.log(`CLI for Microsoft 365 v${app.packageJson().version}`);
cli.log(`${app.packageJson().description} `);
cli.log();
properties.command = 'commandList';
cli.printAvailableCommands();
}
await telemetry.trackEvent('help', properties);
process.exit(exitCode);
}
async function openHelpInBrowser() {
const pathChunks = cli.commandToExecute.help?.replace(/\\/g, '/').replace(/\.[^/.]+$/, '');
const onlineUrl = `https://pnp.github.io/cli-microsoft365/cmd/${pathChunks}`;
await browserUtil.open(onlineUrl);
}
function printCommandHelp(helpMode) {
const docsRootDir = path.join(__dirname, '..', '..', 'docs');
const helpFilePath = path.join(docsRootDir, 'docs', 'cmd', cli.commandToExecute.help);
if (fs.existsSync(helpFilePath)) {
let helpContents = fs.readFileSync(helpFilePath, 'utf8');
helpContents = getHelpSection(helpMode, helpContents);
helpContents = md.md2plain(helpContents, docsRootDir);
cli.log();
cli.log(helpContents);
}
}
async function getHelpMode(options) {
const { h, help } = options;
if (!h && !help) {
return cli.getSettingWithDefaultValue(settingsNames.helpMode, defaultHelpMode);
}
// user passed -h or --help, let's see if they passed a specific mode
// or requested the default
const helpMode = h ?? help;
if (typeof helpMode === 'boolean' || typeof helpMode !== 'string') {
// requested default mode or passed a number, let's use default
return cli.getSettingWithDefaultValue(settingsNames.helpMode, defaultHelpMode);
}
else {
const lowerCaseHelpMode = helpMode.toLowerCase();
if (cli.helpModes.indexOf(lowerCaseHelpMode) < 0) {
await cli.closeWithError(`Unknown help mode ${helpMode}. Allowed values are ${cli.helpModes.join(', ')} `, { options }, false);
/* c8 ignore next 2 */
return ''; // noop
}
else {
return lowerCaseHelpMode;
}
}
}
function getHelpSection(helpMode, helpContents) {
if (helpMode === 'full') {
return helpContents;
}
// options is the first section, so get help up to options
const titleAndUsage = helpContents.substring(0, helpContents.indexOf('## Options'));
// find the requested section
const sectionLines = [];
const sectionName = helpMode[0].toUpperCase() + helpMode.substring(1);
const lines = helpContents.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.indexOf(`## ${sectionName}`) === 0) {
sectionLines.push(line);
}
else if (sectionLines.length > 0) {
if (line.indexOf('## ') === 0) {
// we've reached the next section, stop
break;
}
else {
sectionLines.push(line);
}
}
}
return titleAndUsage + sectionLines.join('\n');
}
function printAvailableCommands() {
// commands that match the current group
const commandsToPrint = {};
// sub-commands in the current group
const commandGroupsToPrint = {};
// current command group, eg. 'spo', 'spo site'
let currentGroup = '';
const addToList = (commandName, command) => {
const pos = commandName.indexOf(' ', currentGroup.length + 1);
if (pos === -1) {
commandsToPrint[commandName] = command;
}
else {
const subCommandsGroup = commandName.substring(0, pos);
if (!commandGroupsToPrint[subCommandsGroup]) {
commandGroupsToPrint[subCommandsGroup] = 0;
}
commandGroupsToPrint[subCommandsGroup]++;
}
};
// get current command group
if (cli.optionsFromArgs &&
cli.optionsFromArgs.options &&
cli.optionsFromArgs.options._ &&
cli.optionsFromArgs.options._.length > 0) {
currentGroup = cli.optionsFromArgs.options._.join(' ');
if (currentGroup) {
currentGroup += ' ';
}
}
const getCommandsForGroup = () => {
for (let i = 0; i < cli.commands.length; i++) {
const command = cli.commands[i];
if (command.name.startsWith(currentGroup)) {
addToList(command.name, command);
}
if (command.aliases) {
for (let j = 0; j < command.aliases.length; j++) {
const alias = command.aliases[j];
if (alias.startsWith(currentGroup)) {
addToList(alias, command);
}
}
}
}
};
getCommandsForGroup();
if (Object.keys(commandsToPrint).length === 0 &&
Object.keys(commandGroupsToPrint).length === 0) {
// specified string didn't match any commands. Reset group and try again
currentGroup = '';
getCommandsForGroup();
}
const namesOfCommandsToPrint = Object.keys(commandsToPrint);
if (namesOfCommandsToPrint.length > 0) {
// determine the length of the longest command name to pad strings + ' [options]'
const maxLength = Math.max(...namesOfCommandsToPrint.map(s => s.length)) + 10;
cli.log(`Commands:`);
cli.log();
const sortedCommandNamesToPrint = Object.getOwnPropertyNames(commandsToPrint).sort();
sortedCommandNamesToPrint.forEach(commandName => {
cli.log(` ${`${commandName} [options]`.padEnd(maxLength, ' ')} ${commandsToPrint[commandName].description}`);
});
}
const namesOfCommandGroupsToPrint = Object.keys(commandGroupsToPrint);
if (namesOfCommandGroupsToPrint.length > 0) {
if (namesOfCommandsToPrint.length > 0) {
cli.log();
}
// determine the longest command group name to pad strings + ' *'
const maxLength = Math.max(...namesOfCommandGroupsToPrint.map(s => s.length)) + 2;
cli.log(`Commands groups:`);
cli.log();
// sort commands groups (because of aliased commands)
const sortedCommandGroupsToPrint = Object
.keys(commandGroupsToPrint)
.sort()
.reduce((object, key) => {
object[key] = commandGroupsToPrint[key];
return object;
}, {});
for (const commandGroup in sortedCommandGroupsToPrint) {
cli.log(` ${`${commandGroup} *`.padEnd(maxLength, ' ')} ${commandGroupsToPrint[commandGroup]} command${commandGroupsToPrint[commandGroup] === 1 ? '' : 's'}`);
}
}
cli.log();
}
async function closeWithError(error, args, showHelpIfEnabled = false) {
let exitCode = 1;
if (args.options.output === 'none') {
return process.exit(exitCode);
}
let errorMessage = error instanceof CommandError ? error.message : error;
if (error instanceof ZodError) {
errorMessage = error.errors.map(e => (e.path.length > 0 ? `${e.path.join('.')}: ${e.message}` : e.message)).join(os.EOL);
}
if ((!args.options.output || args.options.output === 'json') &&
!cli.getSettingWithDefaultValue(settingsNames.printErrorsAsPlainText, true)) {
errorMessage = JSON.stringify({ error: errorMessage });
}
else {
const chalk = (await import('chalk')).default;
errorMessage = chalk.red(`Error: ${errorMessage}`);
}
if (error instanceof CommandError && error.code) {
exitCode = error.code;
}
await cli.error(errorMessage);
if (showHelpIfEnabled && cli.getSettingWithDefaultValue(settingsNames.showHelpOnFailure, showHelpIfEnabled)) {
await cli.error(`Run 'm365 ${cli.commandToExecute.name} -h' for help.`);
}
process.exit(exitCode);
// will never be run. Required for testing where we're stubbing process.exit
/* c8 ignore next */
throw new Error(errorMessage);
/* c8 ignore next */
}
function log(message, ...optionalParams) {
if (message) {
console.log(message, ...optionalParams);
}
else {
console.log();
}
}
async function error(message, ...optionalParams) {
const errorOutput = cli.getSettingWithDefaultValue(settingsNames.errorOutput, 'stderr');
if (errorOutput === 'stdout') {
console.log(message, ...optionalParams);
}
else {
console.error(message, ...optionalParams);
}
}
async function promptForValue(optionInfo) {
return optionInfo.autocomplete !== undefined
? await prompt.forSelection({
message: `${optionInfo.name}: `,
choices: optionInfo.autocomplete.map((choice) => {
return { name: choice, value: choice };
})
})
: await prompt.forInput({ message: `${optionInfo.name}: ` });
}
async function promptForSelection(config) {
const answer = await prompt.forSelection(config);
await cli.error('');
return answer;
}
async function promptForConfirmation(config) {
const answer = await prompt.forConfirmation(config);
await cli.error('');
return answer;
}
async function promptForInput(config) {
const answer = await prompt.forInput(config);
await cli.error('');
return answer;
}
async function handleMultipleResultsFound(message, values) {
const prompt = cli.getSettingWithDefaultValue(settingsNames.prompt, true);
if (!prompt) {
throw new Error(`${message} Found: ${Object.keys(values).join(', ')}.`);
}
await cli.error(`🌶️ ${message} `);
const choices = Object.keys(values).map((choice) => { return { name: choice, value: choice }; });
const response = await cli.promptForSelection({ message: `Please choose one:`, choices });
return values[response];
}
function loadOptionValuesFromFiles(args) {
const optionNames = Object.getOwnPropertyNames(args.options);
optionNames.forEach(option => {
const value = args.options[option];
if (!value ||
typeof value !== 'string' ||
!value.startsWith('@')) {
return;
}
const filePath = value.substring(1);
// if the file doesn't exist, leave as-is, if it exists replace with
// contents from the file
if (fs.existsSync(filePath)) {
args.options[option] = fs.readFileSync(filePath, 'utf-8');
}
});
}
function shouldTrimOutput(output) {
return output === 'text';
}
export const cli = {
closeWithError,
commands,
commandToExecute,
getClientId,
getConfig,
getTenant,
currentCommandName,
error,
execute,
executeCommand,
executeCommandWithOutput,
formatOutput,
getCommandInfo,
getSettingWithDefaultValue,
handleMultipleResultsFound,
helpModes,
helpTargets,
loadAllCommandsInfo,
loadCommandFromArgs,
loadCommandFromFile,
log,
optionsFromArgs,
printAvailableCommands,
promptForConfirmation,
promptForInput,
promptForSelection,
promptForValue,
shouldTrimOutput,
yargsConfiguration
};
//# sourceMappingURL=cli.js.map