@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