UNPKG

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