UNPKG

17.7 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright (c) 2018, salesforce.com, inc.
4 * All rights reserved.
5 * SPDX-License-Identifier: BSD-3-Clause
6 * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7 */
8Object.defineProperty(exports, "__esModule", { value: true });
9const command_1 = require("@oclif/command");
10const core_1 = require("@salesforce/core");
11const kit_1 = require("@salesforce/kit");
12const ts_types_1 = require("@salesforce/ts-types");
13const ts_types_2 = require("@salesforce/ts-types");
14const chalk_1 = require("chalk");
15const docOpts_1 = require("./docOpts");
16const sfdxFlags_1 = require("./sfdxFlags");
17const ux_1 = require("./ux");
18core_1.Messages.importMessagesDirectory(__dirname);
19/**
20 * A class that handles command results and formatting. Use this class
21 * to override command display behavior or to get complex table formatting.
22 * For simple table formatting, use {@link SfdxCommand.tableColumnData} to
23 * define a string array of keys to use as table columns.
24 */
25class Result {
26 constructor(config = {}) {
27 this.tableColumnData = config.tableColumnData;
28 if (config.display) {
29 this.display = config.display.bind(this);
30 }
31 }
32 display() {
33 if (this.tableColumnData) {
34 if (Array.isArray(this.data) && this.data.length) {
35 this.ux.table(this.data, this.tableColumnData);
36 }
37 else {
38 this.ux.log('No results found.');
39 }
40 }
41 }
42}
43exports.Result = Result;
44/**
45 * A base command that provides convenient access to common SFDX flags, a logger,
46 * CLI output formatting, scratch orgs, and devhubs. Extend this command and set
47 * various static properties and a flag configuration to add SFDX behavior.
48 *
49 * @extends @oclif/command
50 * @see https://github.com/oclif/command
51 */
52class SfdxCommand extends command_1.default {
53 constructor() {
54 super(...arguments);
55 this.isJson = false;
56 }
57 // Overrides @oclif/command static flags property. Adds username flags
58 // if the command supports them. Builds flags defined by the command's
59 // flagsConfig static property.
60 // tslint:disable-next-line no-any (matches oclif)
61 static get flags() {
62 return sfdxFlags_1.buildSfdxFlags(this.flagsConfig, {
63 targetdevhubusername: !!(this.supportsDevhubUsername || this.requiresDevhubUsername),
64 targetusername: !!(this.supportsUsername || this.requiresUsername)
65 });
66 }
67 static get usage() {
68 return docOpts_1.DocOpts.generate(this);
69 }
70 static getVarArgsConfig() {
71 if (ts_types_1.isBoolean(this.varargs)) {
72 return this.varargs ? {} : undefined;
73 }
74 // Don't let others muck with this commands config
75 return Object.assign({}, this.varargs);
76 }
77 // TypeScript does not yet have assertion-free polymorphic access to a class's static side from the instance side
78 get statics() {
79 return this.constructor;
80 }
81 async _run() {
82 // If a result is defined for the command, use that. Otherwise check for a
83 // tableColumnData definition directly on the command.
84 if (!this.statics.result.tableColumnData && this.statics.tableColumnData) {
85 this.statics.result.tableColumnData = this.statics.tableColumnData;
86 }
87 this.result = new Result(this.statics.result);
88 let err;
89 try {
90 await this.init();
91 return (this.result.data = await this.run());
92 }
93 catch (e) {
94 err = e;
95 await this.catch(e);
96 }
97 finally {
98 await this.finally(err);
99 }
100 }
101 // Assign this.project if the command requires to be run from within a project.
102 async assignProject() {
103 // Throw an error if the command requires to be run from within an SFDX project but we
104 // don't have a local config.
105 try {
106 this.project = await core_1.SfdxProject.resolve();
107 }
108 catch (err) {
109 if (err.name === 'InvalidProjectWorkspace') {
110 throw core_1.SfdxError.create('@salesforce/command', 'command', 'RequiresProjectError');
111 }
112 throw err;
113 }
114 }
115 // Assign this.org if the command supports or requires a username.
116 async assignOrg() {
117 // Create an org from the username and set on this
118 try {
119 this.org = await core_1.Org.create({
120 aliasOrUsername: this.flags.targetusername,
121 aggregator: this.configAggregator
122 });
123 if (this.flags.apiversion) {
124 this.org.getConnection().setApiVersion(this.flags.apiversion);
125 }
126 }
127 catch (err) {
128 if (this.statics.requiresUsername) {
129 if (err.name === 'NoUsername') {
130 throw core_1.SfdxError.create('@salesforce/command', 'command', 'RequiresUsernameError');
131 }
132 throw err;
133 }
134 }
135 }
136 // Assign this.hubOrg if the command supports or requires a devhub username.
137 async assignHubOrg() {
138 // Create an org from the devhub username and set on this
139 try {
140 this.hubOrg = await core_1.Org.create({
141 aliasOrUsername: this.flags.targetdevhubusername,
142 aggregator: this.configAggregator,
143 isDevHub: true
144 });
145 if (this.flags.apiversion) {
146 this.hubOrg.getConnection().setApiVersion(this.flags.apiversion);
147 }
148 }
149 catch (err) {
150 // Throw an error if the command requires a devhub and there is no targetdevhubusername
151 // flag set and no defaultdevhubusername set.
152 if (this.statics.requiresDevhubUsername) {
153 if (err.name === 'AuthInfoCreationError' || err.name === 'NoUsername') {
154 throw core_1.SfdxError.create('@salesforce/command', 'command', 'RequiresDevhubUsernameError');
155 }
156 throw core_1.SfdxError.wrap(err);
157 }
158 }
159 }
160 shouldEmitHelp() {
161 // If -h was given and this command does not define its own flag with `char: 'h'`,
162 // indicate that help should be emitted.
163 if (!this.argv.includes('-h')) {
164 // If -h was not given, nothing else to do here.
165 return false;
166 }
167 // Check each flag config to see if -h has been overridden...
168 const flags = this.statics.flags || {};
169 for (const k of Object.keys(flags)) {
170 if (k !== 'help' && flags[k].char === 'h') {
171 // If -h is configured for anything but help, the subclass should handle it itself.
172 return false;
173 }
174 }
175 // Otherwise, -h was either not overridden by the subclass, or the subclass includes a specific help flag config.
176 return true;
177 }
178 async init() {
179 // If we made it to the init method, the exit code should not be set yet. It will be
180 // successful unless the base init or command throws an error.
181 process.exitCode = 0;
182 // Ensure this.isJson, this.logger, and this.ux are set before super init, flag parsing, or help generation
183 // (all of which can throw and prevent these from being available for command error handling).
184 const isContentTypeJSON = kit_1.env.getString('SFDX_CONTENT_TYPE', '').toUpperCase() === 'JSON';
185 this.isJson = this.argv.includes('--json') || isContentTypeJSON;
186 // Regex match on loglevel flag in argv and set on the root logger so the proper log level
187 // is used. If no match, the default root log level is used.
188 const loglevel = this.argv.join(' ').match(/--loglevel\s*=?\s*([a-z]+)/);
189 if (loglevel) {
190 (await core_1.Logger.root()).setLevel(core_1.Logger.getLevelByName(loglevel[1]));
191 }
192 await this.initLoggerAndUx();
193 // If the -h flag is set in argv and not overridden by the subclass, emit help and exit.
194 if (this.shouldEmitHelp()) {
195 this._help();
196 }
197 // Finally invoke the super init now that this.ux is properly configured.
198 await super.init();
199 // Load messages
200 const messages = core_1.Messages.loadMessages('@salesforce/command', 'command');
201 // Turn off strict parsing if varargs are set. Otherwise use static strict setting.
202 const strict = this.statics.varargs ? !this.statics.varargs : this.statics.strict;
203 // Parse the command to get flags and args
204 const { args, flags, argv } = this.parse({
205 flags: this.statics.flags,
206 args: this.statics.args,
207 strict
208 });
209 this.flags = flags;
210 this.args = args;
211 // The json flag was set by the environment variables
212 if (isContentTypeJSON) {
213 this.flags.json = true;
214 }
215 this.warnIfDeprecated();
216 // If this command supports varargs, parse them from argv.
217 if (this.statics.varargs) {
218 const argVals = Object.values(args);
219 const varargs = argv.filter(val => !argVals.includes(val));
220 this.varargs = this.parseVarargs(varargs);
221 }
222 this.logger.info(`Running command [${this.statics.name}] with flags [${JSON.stringify(flags)}] and args [${JSON.stringify(args)}]`);
223 //
224 // Verify the command args and flags meet the requirements
225 //
226 this.configAggregator = await core_1.ConfigAggregator.create();
227 // Assign this.project if the command requires to be run from within a project.
228 if (this.statics.requiresProject) {
229 await this.assignProject();
230 }
231 // Get the apiVersion from the config aggregator and display a warning
232 // if it's overridden.
233 const apiVersion = this.configAggregator.getInfo('apiVersion');
234 if (apiVersion && apiVersion.value && !flags.apiversion) {
235 this.ux.warn(messages.getMessage('apiVersionOverrideWarning', [JSON.stringify(apiVersion.value)]));
236 }
237 // Assign this.org if the command supports or requires a username.
238 if (this.statics.supportsUsername || this.statics.requiresUsername) {
239 await this.assignOrg();
240 }
241 // Assign this.hubOrg if the command supports or requires a devhub username.
242 if (this.statics.supportsDevhubUsername || this.statics.requiresDevhubUsername) {
243 await this.assignHubOrg();
244 }
245 }
246 // tslint:disable no-reserved-keywords
247 // tslint:disable-next-line no-any (matches oclif)
248 async catch(err) {
249 // Let oclif handle exit signal errors.
250 if (err.code === 'EEXIT') {
251 throw err;
252 }
253 await this.initLoggerAndUx();
254 // Convert all other errors to SfdxErrors for consistency and set the command name on the error.
255 const error = err.setCommandName ? err : core_1.SfdxError.wrap(err);
256 error.setCommandName(this.statics.name);
257 process.exitCode = process.exitCode || error.exitCode || 1;
258 const userDisplayError = Object.assign(this.getJsonResultObject(error.data, error.exitCode), Object.assign({}, error.toObject(), { stack: error.stack, warnings: Array.from(ux_1.UX.warnings) }));
259 if (this.isJson) {
260 // This should default to true, which will require a major version bump.
261 const sendToStdout = kit_1.env.getBoolean('SFDX_JSON_TO_STDOUT');
262 if (sendToStdout) {
263 this.ux.logJson(userDisplayError);
264 }
265 else {
266 this.ux.errorJson(userDisplayError);
267 }
268 }
269 else {
270 this.ux.error(...this.formatError(error));
271 if (err.data) {
272 this.result.data = err.data;
273 this.result.display();
274 }
275 }
276 // Emit an event for the analytics plugin. The ts-ignore is necessary
277 // because TS is strict about the events that can be emitted on process.
278 // @ts-ignore
279 process.emit('cmdError', err, Object.assign({}, this.flags, this.varargs), this.org || this.hubOrg);
280 }
281 async finally(err) {
282 // Only handle success since we're handling errors in the catch
283 if (!err) {
284 if (this.isJson) {
285 let output = this.getJsonResultObject();
286 if (ux_1.UX.warnings.size > 0) {
287 output = Object.assign(output, {
288 warnings: Array.from(ux_1.UX.warnings)
289 });
290 }
291 this.ux.logJson(output);
292 }
293 else {
294 this.result.display();
295 }
296 }
297 }
298 // If this command is deprecated, emit a warning
299 warnIfDeprecated() {
300 if (this.statics.deprecated) {
301 let def;
302 if (ts_types_2.has(this.statics.deprecated, 'version')) {
303 def = Object.assign({ name: this.statics.name, type: 'command' }, this.statics.deprecated);
304 }
305 else {
306 def = this.statics.deprecated;
307 }
308 this.ux.warn(ux_1.UX.formatDeprecationWarning(def));
309 }
310 if (this.statics.flagsConfig) {
311 // If any deprecated flags were passed, emit warnings
312 for (const flag of Object.keys(this.flags)) {
313 const def = this.statics.flagsConfig[flag];
314 if (def && def.deprecated) {
315 this.ux.warn(ux_1.UX.formatDeprecationWarning(Object.assign({ name: flag, type: 'flag' }, def.deprecated)));
316 }
317 }
318 }
319 }
320 getJsonResultObject(result = this.result.data, status = process.exitCode || 0) {
321 return { status, result };
322 }
323 parseVarargs(args = []) {
324 const varargs = {};
325 const descriptor = this.statics.varargs;
326 // If this command requires varargs, throw if none are provided.
327 if (!args.length && !ts_types_1.isBoolean(descriptor) && descriptor.required) {
328 throw core_1.SfdxError.create('@salesforce/command', 'command', 'VarargsRequired');
329 }
330 // Validate the format of the varargs
331 args.forEach(arg => {
332 const split = arg.split('=');
333 if (split.length !== 2) {
334 throw core_1.SfdxError.create('@salesforce/command', 'command', 'InvalidVarargsFormat', [arg]);
335 }
336 const [name, value] = split;
337 if (varargs[name]) {
338 throw core_1.SfdxError.create('@salesforce/command', 'command', 'DuplicateVararg', [name]);
339 }
340 if (!ts_types_1.isBoolean(descriptor) && descriptor.validator) {
341 descriptor.validator(name, value);
342 }
343 varargs[name] = value || undefined;
344 });
345 return varargs;
346 }
347 /**
348 * Format errors and actions for human consumption. Adds 'ERROR running <command name>',
349 * and outputs all errors in red. When there are actions, we add 'Try this:' in blue
350 * followed by each action in red on its own line.
351 * @returns {string[]} Returns decorated messages.
352 */
353 formatError(error) {
354 const colorizedArgs = [];
355 // We should remove error.commandName since we should always use the actual command id.
356 const commandName = this.id || error.commandName;
357 const runningWith = commandName ? ` running ${commandName}` : '';
358 colorizedArgs.push(chalk_1.default.bold(`ERROR${runningWith}: `));
359 colorizedArgs.push(chalk_1.default.red(error.message));
360 // Format any actions.
361 if (ts_types_1.get(error, 'actions.length')) {
362 colorizedArgs.push(`\n\n${chalk_1.default.blue(chalk_1.default.bold('Try this:'))}`);
363 if (error.actions) {
364 error.actions.forEach(action => {
365 colorizedArgs.push(`\n${chalk_1.default.red(action)}`);
366 });
367 }
368 }
369 if (error.stack && core_1.Global.getEnvironmentMode() === core_1.Mode.DEVELOPMENT) {
370 colorizedArgs.push(chalk_1.default.red(`\n*** Internal Diagnostic ***\n\n${error.stack}\n******\n`));
371 }
372 return colorizedArgs;
373 }
374 /**
375 * Initialize logger and ux for the command
376 */
377 async initLoggerAndUx() {
378 if (!this.logger) {
379 this.logger = await core_1.Logger.child(this.statics.name);
380 }
381 if (!this.ux) {
382 this.ux = new ux_1.UX(this.logger, !this.isJson);
383 }
384 if (this.result && !this.result.ux) {
385 this.result.ux = this.ux;
386 }
387 }
388}
389// Set to true to add the "targetusername" flag to this command.
390SfdxCommand.supportsUsername = false;
391// Set to true if this command MUST have a targetusername set, either via
392// a flag or by having a default.
393SfdxCommand.requiresUsername = false;
394// Set to true to add the "targetdevhubusername" flag to this command.
395SfdxCommand.supportsDevhubUsername = false;
396// Set to true if this command MUST have a targetdevhubusername set, either via
397// a flag or by having a default.
398SfdxCommand.requiresDevhubUsername = false;
399// Set to true if this command MUST be run within a SFDX project.
400SfdxCommand.requiresProject = false;
401// Use for full control over command output formating and display, or to override
402// certain pieces of default display behavior.
403SfdxCommand.result = {};
404// Use to enable or configure varargs style (key=value) parameters.
405SfdxCommand.varargs = false;
406exports.SfdxCommand = SfdxCommand;
407//# sourceMappingURL=sfdxCommand.js.map
\No newline at end of file