@salesforce/command
Version:
Salesforce CLI base command class
475 lines • 20.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SfdxCommand = exports.Result = void 0;
/*
* Copyright (c) 2021, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
const core_1 = require("@oclif/core");
const core_2 = require("@salesforce/core");
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const chalk_1 = require("chalk");
const docOpts_1 = require("./docOpts");
const sfdxFlags_1 = require("./sfdxFlags");
const ux_1 = require("./ux");
core_2.Messages.importMessagesDirectory(__dirname);
const messages = core_2.Messages.load('@salesforce/command', 'command', [
'error.RequiresProject',
'error.RequiresUsername',
'warning.ApiVersionOverride',
'error.InvalidVarargsFormat',
'error.DuplicateVarargs',
'error.VarargsRequired',
'error.RequiresDevhubUsername',
]);
/**
* A class that handles command results and formatting. Use this class
* to override command display behavior or to get complex table formatting.
* For simple table formatting, use {@link SfdxCommand.tableColumnData} to
* define a string array of keys to use as table columns.
*/
class Result {
constructor(config = {}) {
this.tableColumnData = config.tableColumnData;
if (config.display) {
this.display = config.display.bind(this);
}
}
display() {
if (this.tableColumnData) {
if (Array.isArray(this.data) && this.data.length) {
this.ux.table(this.data, this.tableColumnData);
}
else {
this.ux.log('No results found.');
}
}
}
}
exports.Result = Result;
/**
*
* @deprecated Use SfCommand from `@salesforce/sf-plugins-core`
*
* A base command that provides convenient access to common SFDX flags, a logger,
* CLI output formatting, scratch orgs, and devhubs. Extend this command and set
* various static properties and a flag configuration to add SFDX behavior.
*
* @extends @oclif/command
* @see https://github.com/oclif/command
*/
class SfdxCommand extends core_1.Command {
constructor() {
super(...arguments);
/** event names to be registered for command specific hooks */
this.lifecycleEventNames = [];
this.isJson = false;
}
// Overrides @oclif/core static flags property. Adds username flags
// if the command supports them. Builds flags defined by the command's
// flagsConfig static property.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static get flags() {
return (0, sfdxFlags_1.buildSfdxFlags)(this.flagsConfig, {
targetdevhubusername: this.supportsDevhubUsername || this.requiresDevhubUsername,
targetusername: this.supportsUsername || this.requiresUsername,
});
}
static get usage() {
return docOpts_1.DocOpts.generate(this);
}
// TypeScript does not yet have assertion-free polymorphic access to a class's static side from the instance side
get statics() {
return this.constructor;
}
static getVarArgsConfig() {
if ((0, ts_types_1.isBoolean)(this.varargs)) {
return this.varargs ? {} : undefined;
}
// Don't let others muck with this commands config
return Object.assign({}, this.varargs);
}
async _run() {
// If a result is defined for the command, use that. Otherwise check for a
// tableColumnData definition directly on the command.
if (!this.statics.result.tableColumnData && this.statics.tableColumnData) {
this.statics.result.tableColumnData = this.statics.tableColumnData;
}
this.result = new Result(this.statics.result);
let err;
try {
await this.init();
return (this.result.data = await this.run());
}
catch (e) {
err = e;
await this.catch(e);
}
finally {
await this.finally(err);
}
}
// Assign this.project if the command requires to be run from within a project.
async assignProject() {
// Throw an error if the command requires to be run from within an SFDX project but we
// don't have a local config.
try {
this.project = await core_2.SfProject.resolve();
}
catch (err) {
if (err instanceof Error && err.name === 'InvalidProjectWorkspace') {
throw messages.createError('error.RequiresProject');
}
throw err;
}
}
// Assign this.org if the command supports or requires a username.
async assignOrg() {
// Create an org from the username and set on this
try {
this.org = await core_2.Org.create({
aliasOrUsername: this.flags.targetusername,
aggregator: this.configAggregator,
});
if (typeof this.flags.apiversion === 'string') {
this.org.getConnection().setApiVersion(this.flags.apiversion);
}
}
catch (err) {
if (this.statics.requiresUsername) {
if (err instanceof Error && (err.name === 'NoUsernameFoundError' || err.name === 'AuthInfoCreationError')) {
throw messages.createError('error.RequiresUsername');
}
throw err;
}
}
}
// Assign this.hubOrg if the command supports or requires a devhub username.
async assignHubOrg() {
// Create an org from the devhub username and set on this
try {
this.hubOrg = await core_2.Org.create({
aliasOrUsername: this.flags.targetdevhubusername,
aggregator: this.configAggregator,
isDevHub: true,
});
if (typeof this.flags.apiversion === 'string') {
this.hubOrg.getConnection().setApiVersion(this.flags.apiversion);
}
}
catch (err) {
// Throw an error if the command requires a devhub and there is no targetdevhubusername
// flag set and no defaultdevhubusername set.
if (this.statics.requiresDevhubUsername && err instanceof Error) {
if (err.name === 'AuthInfoCreationError' || err.name === 'NoUsernameFoundError') {
throw messages.createError('error.RequiresDevhubUsername');
}
throw core_2.SfError.wrap(err);
}
}
}
shouldEmitHelp() {
// If -h was given and this command does not define its own flag with `char: 'h'`,
// indicate that help should be emitted.
if (!this.argv.includes('-h')) {
// If -h was not given, nothing else to do here.
return false;
}
// Check each flag config to see if -h has been overridden...
const flags = this.statics.flags || {};
for (const k of Object.keys(flags)) {
if (k !== 'help' && flags[k].char === 'h') {
// If -h is configured for anything but help, the subclass should handle it itself.
return false;
}
}
// Otherwise, -h was either not overridden by the subclass, or the subclass includes a specific help flag config.
return true;
}
async init() {
// If we made it to the init method, the exit code should not be set yet. It will be
// successful unless the base init or command throws an error.
process.exitCode = 0;
// Ensure this.isJson, this.logger, and this.ux are set before super init, flag parsing, or help generation
// (all of which can throw and prevent these from being available for command error handling).
const isContentTypeJSON = kit_1.env.getString('SFDX_CONTENT_TYPE', '').toUpperCase() === 'JSON';
this.isJson = this.argv.includes('--json') || isContentTypeJSON;
// Regex match on loglevel flag in argv and set on the root logger so the proper log level
// is used. If no match, the default root log level is used.
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
const loglevel = this.argv.join(' ').match(/--loglevel\s*=?\s*([a-z]+)/);
if (loglevel) {
(await core_2.Logger.root()).setLevel(core_2.Logger.getLevelByName(loglevel[1]));
}
await this.initLoggerAndUx();
// If the -h flag is set in argv and not overridden by the subclass, emit help and exit.
if (this.shouldEmitHelp()) {
const Help = await (0, core_1.loadHelpClass)(this.config);
// TODO: figure out how to work around oclif's pjson definition which includes [k: string]: any on PJSON
const help = new Help(this.config, this.config.pjson.helpOptions);
try {
// @ts-ignore this.statics is of type SfdxCommand, which extends Command which it expects
await help.showCommandHelp(this.statics, []);
}
catch {
// fail back to how it was
await help.showHelp(this.argv);
}
return this.exit(0);
}
// Finally invoke the super init now that this.ux is properly configured.
await super.init();
// Turn off strict parsing if varargs are set. Otherwise use static strict setting.
const strict = this.statics.varargs ? !this.statics.varargs : this.statics.strict;
// Parse the command to get flags and args
const { args, flags, argv } = await this.parse({
flags: this.statics.flags,
args: this.statics.args,
strict,
});
this.flags = flags;
this.args = args;
// The json flag was set by the environment variables
if (isContentTypeJSON) {
this.flags.json = true;
}
this.warnIfDeprecated();
// If this command supports varargs, parse them from argv.
if (this.statics.varargs) {
const argVals = Object.values(args);
const varargs = argv.filter((val) => !argVals.includes(val));
this.varargs = this.parseVarargs(varargs);
}
this.logger.info(`Running command [${this.statics.name}] with flags [${JSON.stringify(flags)}] and args [${JSON.stringify(args)}]`);
//
// Verify the command args and flags meet the requirements
//
this.configAggregator = await core_2.SfdxConfigAggregator.create();
// Assign this.project if the command requires to be run from within a project.
if (this.statics.requiresProject) {
await this.assignProject();
}
// Get the apiVersion from the config aggregator and display a warning
// if it's overridden.
const apiVersion = this.configAggregator.getInfo('apiVersion');
if (apiVersion?.value && !flags.apiversion) {
this.ux.warn(messages.getMessage('warning.ApiVersionOverride', [JSON.stringify(apiVersion.value)]));
}
// Assign this.org if the command supports or requires a username.
if (this.statics.supportsUsername || this.statics.requiresUsername) {
await this.assignOrg();
}
// Assign this.hubOrg if the command supports or requires a devhub username.
if (this.statics.supportsDevhubUsername || this.statics.requiresDevhubUsername) {
await this.assignHubOrg();
}
// register event listeners for command specific hooks
await this.hooksFromLifecycleEvent(this.lifecycleEventNames);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types
async catch(err) {
// Let oclif handle exit signal errors.
if (err.code === 'EEXIT') {
throw err;
}
// sfdx-core v3 changed error names to end in "Error"
// to avoid breaking changes across error names across every command that extends SfdxCommand
// remove the "Error" from the end of the name except for the generic SfError
if (err instanceof Error) {
err.name = err.name === 'SfError' ? 'SfError' : err.name.replace(/Error$/, '');
}
await this.initLoggerAndUx();
// Convert all other errors to SfErrors for consistency and set the command name on the error.
const error = core_2.SfError.wrap(err);
error.setContext(this.statics.name);
// tests rely on the falsiness of zero, and real world code might, too
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
process.exitCode = process.exitCode || error.exitCode || 1;
const userDisplayError = Object.assign({ result: error.data, status: error.exitCode }, {
...error.toObject(),
stack: error.fullStack ?? error.stack,
warnings: Array.from(ux_1.UX.warnings),
// keep commandName key for backwards compatibility
commandName: error.context,
});
if (this.isJson) {
// This should default to true, which will require a major version bump.
const sendToStdout = kit_1.env.getBoolean('SFDX_JSON_TO_STDOUT', true);
if (sendToStdout) {
this.ux.logJson(userDisplayError);
}
else {
this.ux.errorJson(userDisplayError);
}
}
else {
this.ux.error(...this.formatError(error));
if (err.data) {
this.result.data = err.data;
this.result.display();
}
}
// Emit an event for the analytics plugin. The ts-ignore is necessary
// because TS is strict about the events that can be emitted on process.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
process.emit('cmdError', err, Object.assign({}, this.flags, this.varargs), this.org ?? this.hubOrg);
}
// eslint-disable-next-line @typescript-eslint/require-await
async finally(err) {
// Only handle success since we're handling errors in the catch
if (!err) {
if (this.isJson) {
let output = this.getJsonResultObject();
if (ux_1.UX.warnings.size > 0) {
output = Object.assign(output, {
warnings: Array.from(ux_1.UX.warnings),
});
}
this.ux.logJson(output);
}
else {
this.result.display();
}
}
}
// If this command is deprecated, emit a warning
warnIfDeprecated() {
if (this.statics.deprecated) {
let def;
if ((0, ts_types_1.has)(this.statics.deprecated, 'version')) {
def = {
name: this.statics.name,
type: 'command',
...this.statics.deprecated,
};
}
else {
def = this.statics.deprecated;
}
this.ux.warn(ux_1.UX.formatDeprecationWarning(def));
}
if (this.statics.flagsConfig) {
// If any deprecated flags were passed, emit warnings
for (const flag of Object.keys(this.flags)) {
const def = this.statics.flagsConfig[flag];
if (def?.deprecated) {
this.ux.warn(ux_1.UX.formatDeprecationWarning({
name: flag,
type: 'flag',
// @ts-ignore
...def.deprecated,
}));
}
}
}
}
getJsonResultObject(result = this.result.data, status = process.exitCode ?? 0) {
return { status, result };
}
parseVarargs(args = []) {
const varargs = {};
const descriptor = this.statics.varargs;
// If this command requires varargs, throw if none are provided.
if (!args.length && !(0, ts_types_1.isBoolean)(descriptor) && descriptor.required) {
throw messages.createError('error.VarargsRequired');
}
// Validate the format of the varargs
args.forEach((arg) => {
const split = arg.split('=');
if (split.length !== 2) {
throw messages.createError('error.InvalidVarargsFormat', [arg]);
}
const [name, value] = split;
if (varargs[name]) {
throw messages.createError('error.DuplicateVarargs', [name]);
}
if (!(0, ts_types_1.isBoolean)(descriptor) && descriptor.validator) {
descriptor.validator(name, value);
}
varargs[name] = value || undefined;
});
return varargs;
}
/**
* Format errors and actions for human consumption. Adds 'ERROR running <command name>',
* and outputs all errors in red. When there are actions, we add 'Try this:' in blue
* followed by each action in red on its own line.
*
* @returns {string[]} Returns decorated messages.
*/
formatError(error) {
const colorizedArgs = [];
const commandName = this.id ?? error.context;
const runningWith = commandName ? ` running ${commandName}` : '';
colorizedArgs.push(chalk_1.default.bold(`ERROR${runningWith}: `));
colorizedArgs.push(chalk_1.default.red(error.message));
// Format any actions.
if ((0, ts_types_1.get)(error, 'actions.length')) {
colorizedArgs.push(`\n\n${chalk_1.default.blue(chalk_1.default.bold('Try this:'))}`);
if (error.actions) {
error.actions.forEach((action) => {
colorizedArgs.push(`\n${chalk_1.default.red(action)}`);
});
}
}
// Prefer the fullStack if one exists, which includes the "caused by".
const stack = error.fullStack ?? error.stack;
if (stack && core_2.Global.getEnvironmentMode() === core_2.Mode.DEVELOPMENT) {
colorizedArgs.push(chalk_1.default.red(`\n*** Internal Diagnostic ***\n\n${stack}\n******\n`));
}
return colorizedArgs;
}
/**
* Initialize logger and ux for the command
*/
async initLoggerAndUx() {
if (!this.logger) {
this.logger = await core_2.Logger.child(this.statics.name);
}
if (!this.ux) {
this.ux = new ux_1.UX(this.logger, !this.isJson);
}
if (this.result && !this.result.ux) {
this.result.ux = this.ux;
}
}
/**
* register events for command specific hooks
*/
async hooksFromLifecycleEvent(lifecycleEventNames) {
const options = {
Command: this.ctor,
argv: this.argv,
commandId: this.id,
};
const lifecycle = core_2.Lifecycle.getInstance();
lifecycleEventNames.forEach((eventName) => {
lifecycle.on(eventName, async (result) => {
await this.config.runHook(eventName, Object.assign(options, { result }));
});
});
}
}
exports.SfdxCommand = SfdxCommand;
// Set to true to add the "targetusername" flag to this command.
SfdxCommand.supportsUsername = false;
// Set to true if this command MUST have a targetusername set, either via
// a flag or by having a default.
SfdxCommand.requiresUsername = false;
// Set to true to add the "targetdevhubusername" flag to this command.
SfdxCommand.supportsDevhubUsername = false;
// Set to true if this command MUST have a targetdevhubusername set, either via
// a flag or by having a default.
SfdxCommand.requiresDevhubUsername = false;
// Set to true if this command MUST be run within a SFDX project.
SfdxCommand.requiresProject = false;
// Use for full control over command output formating and display, or to override
// certain pieces of default display behavior.
SfdxCommand.result = {};
// Use to enable or configure varargs style (key=value) parameters.
SfdxCommand.varargs = false;
//# sourceMappingURL=sfdxCommand.js.map