UNPKG

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