UNPKG

@salesforce/command

Version:
475 lines 20.4 kB
"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