Skip to content

Code Your Plugin

Juliet Shackell edited this page Jul 1, 2024 · 20 revisions

This section provides reference information for customizing your plugin.

Generate Stuff

We provide a number of commands in the plugin-dev plugin to generate your own plugin, commands, flags, and more. See the Get Started section for examples.

First install the plugin:

sf plugins install @salesforce/plugin-dev

Here's the full list of commands:

  • sf dev generate plugin: Generate the files for a plugin. Includes a sample hello world command.
  • sf dev generate command: Generate the initial files for a new command.
  • sf dev generate flag: Update existing command files with code for a new flag.
  • sf dev generate library: Generate initial files for a library that multiple CLI commands can call.

Run a command with --help to see more information.

Yarn Scripts

If you use sf dev generate plugin to generate your initial plugin, we include a number of yarn scripts that help with developing and releasing your plugin.

wireit

The templates and the CLI team's plugins use wireit by default. It simplifies running scripts in parallel, understandings script dependencies, and uses cached results based on file changes.

If you want to use different scripts, or add to the wireit configuration, you'll want to modify sfdevrc.json and not package.json; otherwise yarn install scripts will keep changing it back to the default. The properties under wireit in sfdevrc.json will overwrite the matching property in package.json#wireit during yarn install. Example

We encourage you to use wireit dependencies over npm-style hooks (ex: pretest) for better performance.

You can always yarn clean-all to delete all the cache materials if you suspect wrong results due to caching.

Scripts

Script Description
yarn / yarn install Install the plugin's dependencies.
yarn clean Delete transient directories and files (such as docs/, tmp/, *.log).
yarn clean-all Run yarn clean and remove node_modules.
yarn clean:lib Delete the compiled source code (lib/).
yarn compile Compile source code into lib/.
yarn docs Generate documentation for your plug-in. Requires that you add a typedoc.json configuration which isn't included in the generated plugin.
yarn format Prettify your source code. This script runs automatically in the husky pre-commit hook.
yarn lint Lint your source code.
yarn build Run yarn clean, yarn compile and yarn lint.
yarn postpack Delete the oclif.manifest.json.
yarn prepack Runs yarn build and generates oclif.manifest.json file.
yarn test:only Run unit tests (files that match *.test.ts pattern).
yarn test Unit tests, test compile/lint, and several checks to prevent breaking changes and documentation bugs
yarn test:nuts Run NUT tests (files that match *.nut.ts pattern).
yarn version Update README with latest commands.

Use Libraries

We encourage plugin developers to use existing npm libraries that already have the functionality you need; there's no reason to reinvent the wheel.

Salesforce Libraries

Salesforce owns and maintains these npm libraries to implement common and useful functionality in your plugin.

@salesforce/core

The @salesforce/core library provides client-side management of Salesforce DX projects, org authentication, connections to Salesforce APIs, and other utilities. Much of the core functionality that powers the Salesforce CLI plugins comes from this library. You can use this functionality in your plugins too.

  • AuthInfo, Org, and Connection classes to interact with Salesforce orgs.
  • Messages class to work with messages in Markdown, JSON, or JavaScript.
  • ConfigFile class to work with configuration files.
  • SfError class to throw errors from your plugin.
  • SfProject class to work with Salesforce project directories.
  • testSetup utility to write unit tests.
  • See API docs for details.

@salesforce/sf-plugins-core

  • SfCommand class, the base class for every sf command.
  • Salesforce specific command flags, such as salesforceId, requiredOrg, requiredHub, and optionalOrg.
  • See API Docs for details.

@salesforce/kit

  • A collection of commonly needed utilities. It includes high level support for parsing and working with JSON data, interacting with environment variables, a minimal lodash replacement, and support for common design patterns.
  • See API docs for details.

@salesforce/source-deploy-retrieve

  • Functionality for working with Salesforce metadata.
  • See API docs for details.

@salesforce/ts-types

  • A collection of commonly used types and type-narrowing convenience functions for writing concise type guards.
  • See API docs for details.

@salesforce/cli-plugins-testkit

  • Testing library that provides utilities to write NUTs (non-unit-tests), such as integration, smoke, and e2e style testing. For example, you could write tests to ensure your plugin commands execute properly using an isolated Salesforce project, scratch org, and different Salesforce CLI executables
  • See docs and examples for details.

@salesforce/ts-sinon

  • Library for creating test stubs with sinon.

@oclif/core

  • The underlying framework of the entire Salesforce CLI and all its plugins.
  • You don’t need to know much about this library in order to develop your plugin. But in case you're curious, here are the docs.

Third-Party Libraries

We encourage you to use libraries written by other developers in the npm community. Be sure, however, that you do your due diligence to ensure that you're using libraries that are well-maintained by trustworthy developers.

Here are a few libraries that we recommend:

  • got to make HTTP requests.
  • graceful-fs for resilient file system operations.
  • chalk to colorize output.
  • open to open URLs in a web browser.
  • change-case to convert strings between different cases, such as camelCase to TitleCase.
  • sinon, mocha, and chai to test your plugin.

Common Coding Patterns

The following sections describe common coding patterns that you'll likely use in your plugin, along with a code sample. Where possible, we also provide a link to one of our plugins as an additional example.

Throw an Error

To throw an error from your command, first add the error message to your messages Markdown file. Use a H1 header for the error name. We suggest you follow the Salesforce CLI style convention of prefacing error names with error and warnings with warning. For example:

# error.InvalidUsername

Invalid Username: %s. 

Load the message into your command with the Messages.loadMessages method and throw it using message.createError. This code example builds on the sample hello world command.

import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-awesome', 'hello.world');

export type HelloWorldResult = {
  name: string;
  time: string;
};

export default class World extends SfCommand<HelloWorldResult> {
  public static readonly summary = messages.getMessage('summary');
  public static readonly description = messages.getMessage('description');
  public static readonly examples = messages.getMessages('examples');

  public static flags = {
    username: Flags.string({
      char: 'u',
      description: messages.getMessage('flags.username.summary'),
      default: 'World',
    }),
  };

  public async run(): Promise<HelloWorldResult> {
    const { flags } = await this.parse(World);
    // throw an error that's created using the error message defined in the messages file and provide the username to insert into the message.
    throw messages.createError('error.InvalidUsername', [flags.username]);
  }
}

When a user runs the command and runs into the error, it's automatically prefaced with Error: , such as:

Error: Invalid Username: doesnt@work.org

The Messages class also contains the createWarning and createInfo methods for warnings and informational output.

Error Codes

When a CLI command encounters an error, it usually returns an exit code of 1. If you don't include any error handling code in your command, oclif handles the error and its default exit code is 1. Similarly, a successful command execution returns a 0 exit code by default.

You can use a different exit code if you want. You must, however, use only those codes that aren't currently being used by Salesforce CLI, or are reserved for its future use. This table shows these error codes.

Error Code Description
0 The command executed successfully.
1 The command didn't execute successfully.
2 oclif detected errors, typically issues with flags.
3 - 9 Reserved for future use by Salesforce CLI.
10 TypeErrors, which are typically problems in client code.
11 - 19 Reserved for future use by Salesforce CLI.
20 GACKs, which are problems in Salesforce server code.
21 - 29 Reserved for future use by Salesforce CLI.
68 Partial success, such as deploying only some requested metadata.
69 Request still in progress, or the request timed out.
130 The command received a termination signal, such as the user pressing Ctrl+C.

Prompt the User

SfCommand contains a prompt method that encapsulates the inquirer library. See the inquirer's documentation for different types of questions you can construct.

This code example show how to change the run() method in the sample hello world command to ask the user a question and change the output based on the answer.

import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-awesome', 'hello.world');

export type HelloWorldResult = {
  name: string;
};

export default class World extends SfCommand<HelloWorldResult> {
  public static readonly summary = messages.getMessage('summary');
  public static readonly description = messages.getMessage('description');
  public static readonly examples = messages.getMessages('examples');

  public static flags = {
    name: Flags.string({
      char: 'n',
      description: messages.getMessage('flags.name.summary'),
      default: 'World',
    }),
  };

  public async run(): Promise<HelloWorldResult> {
    const { flags } = await this.parse(World);
    const answers = await this.prompt<{ confirm: boolean }>({
      type: 'confirm',
      name: 'confirm',
      message: `Hello ${flags.name}! Is that your real name?`,
    });
    const message = answers.confirm ? `Hello ${flags.name}` : 'Hello ???';
    this.log(message);
    return { name: flags.name };
  }
}

Spinners

SfCommand exposes a spinner class that you can use to put spinners on the terminal if your command takes a while to complete. These spinners are automatically suppressed if the --json flag is present.

This code example show how to change the run() method in the sample hello world command to sleep for a short time, but display the word Loading... and a spinner while it sleeps.

import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import { sleep } from '@salesforce/kit';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-awesome', 'hello.world');

export type HelloWorldResult = {
  name: string;
};

export default class World extends SfCommand<HelloWorldResult> {
  public static readonly summary = messages.getMessage('summary');
  public static readonly description = messages.getMessage('description');
  public static readonly examples = messages.getMessages('examples');

  public static flags = {
    name: Flags.string({
      char: 'n',
      description: messages.getMessage('flags.name.summary'),
      default: 'World',
    }),
  };

  public async run(): Promise<HelloWorldResult> {
    const { flags } = await this.parse(World);
    this.spinner.start('Loading...');
    await sleep(5000);
    this.spinner.stop();
    return { name: flags.name };
  }
}

Progress Bars

SfCommand exposes a progress class that you can use to put progress bars on the terminal. These progress bars are automatically suppressed if the --json flag is present.

This code example show how to change the run() method in the sample hello world command to sleep for a short time, but display the words Hello World Progress and a progress bar while it sleeps.

import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import { sleep } from '@salesforce/kit';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-awesome', 'hello.world');

export type HelloWorldResult = {
  name: string;
};

export default class World extends SfCommand<HelloWorldResult> {
  public static readonly summary = messages.getMessage('summary');
  public static readonly description = messages.getMessage('description');
  public static readonly examples = messages.getMessages('examples');

  public static flags = {
    name: Flags.string({
      char: 'n',
      description: messages.getMessage('flags.name.summary'),
      default: 'World',
    }),
  };

  public async run(): Promise<HelloWorldResult> {
    const { flags } = await this.parse(World);
    this.progress.start(0, {}, { title: 'Hello World Progress' });
    this.progress.setTotal(100);
    for (let i = 0; i < 100; i++) {
      await sleep(10);
      this.progress.update(i);
    }

    this.progress.finish();

    return { name: flags.name };
  }
}

Use a Configuration File

You can easily create and use configuration files using the ConfigFile class from @salesforce/core. The configuration file is located in the global .sfdx directory if isGlobal equals true. Otherwise it's located in your local project directory.

This code sample shows how to create a global configuration file called myConfigFilename.json, located in the global .sfdx directory. The example then shows how to set a key called myKey to the value myvalue.

import { ConfigFile } from '@salesforce/core';

class MyConfig extends ConfigFile {
  public static getFileName(): string {
    return 'myConfigFilename.json';
  }
}
const myConfig = await MyConfig.create({
  isGlobal: true
});

myConfig.set('mykey', 'myvalue');
await myConfig.write();

Add a Configuration Variable

sf uses configuration variables to set CLI defaults, such as your default org (target-org) or the API version you want the CLI to use (org-api-version). You set and get configuration variables with the sf config set|get commands. You can define your own custom configuration variable that is also managed by the sf config commands.

This example adds a Boolean configuration variable called do-awesome-things. First create a file in your plugin called configMeta.ts with code similar to this:

import { ConfigValue } from '@salesforce/core';

export enum ConfigVars {
  DO_AWESOME_THINGS = 'do-awesome-things',
}

export default [
  {
    key: ConfigVars.DO_AWESOME_THINGS,
    description: 'do awesome things',
    input: {
      validator: (value: ConfigValue): boolean => value != null && ['true', 'false'].includes(value.toString()),
      failedMessage: 'Must provide a boolean value.',
    },
  },
];

Then update the package.json file in the top-level directory of your plugin and add a configMeta property to the oclif section, like this:

{
  "oclif": {
    "configMeta": "./lib/configMeta",
  }
}

You can then set the new configuration variable like this:

sf config set do-awesome-things=true

Use Our Linter Rules to Improve Your Code

Linters help you write awesome code by flagging programming errors, bugs, stylistic errors and suspicious constructs. If you generate a plugin using our plugin generator (sf dev generate plugin), you'll also get our recommended linter rules to help you develop the best possible commands for your users.

The rules are open-source and the list is maintained here along with setup instructions

We recommend that you install the eslint plugin in VS Code so that you get feedback in real time as you're developing.

Command Properties

If you extend the base SfCommand class, you get lots of out-of-the-box functionality to develop highly usable commands. Our command generator (sf dev generate command) creates a Typescript class for your new command that automatically extends SfCommand.

Here's a brief overview of the various class properties you can set to alter the behavior of your command. Check out SfCommand class and the oclif docs to dig a little deeper. To see many of these properties in action, take a look at the DeployMetadata class, which implements the sf deploy metadata core Salesforce CLI command.

  • summary - String that briefly describes the purpose of the command. Displayed with the --help | -h flags.
  • description - String that provides a more in-depth explanation of the command. Displayed with the --help flag.
  • examples - Array of strings that provide examples of how to use the command. Displayed with the --help flag. Pro tip: Rather than "hard code" the CLI command name in the corresponding messages Markdown file, such as sf hello world, use the string <%= config.bin %> <%= command.id %> instead. The CLI framework automatically inserts the command.
  • aliases - Array of strings that describe the different names that this command can be invoked by.
  • state - Set to beta if you want your command to warn users that it's still in development.
  • hidden - Set to true if you want to hide your command from users.
  • configurationVariablesSection - HelpSection that describes the configuration variables that can be used with this command. Only used for displaying help with the --help flag.
  • envVariablesSection - HelpSection that describes the environment variables that can be used with this command. Only used for displaying help with the --help flag.
  • errorCodes - HelpSection that describes the error codes that could be returned by the command. Only used for displaying help with the --help flag.
  • requiresProject - Set to true if you want the command to throw an error if the user isn't running the command from inside a Salesforce project directory.
  • enableJsonFlag - Set to false to disable the --json flag for your command.

Command Flags

Add flags to your commands to allow user input to modify the behavior of the command.

To get started quickly with your new flag, run this interactive command in your plugin directory:

sf dev generate flag

Global Flags

All commands automatically have the --help and -h flags for displaying long and short command help, respectively.

If your command extends the base SfCommand class, it also has these flags by default:

  • --json : Format output as JSON.
  • --flags-dir : Import flag values from a file.

Built-In Flags

If you pick one of these flag types when you run dev generate flag, you don't need to write any code at all to make it work!

  • optionalHub (corresponds to the standard --target-dev-hub flag)
  • requiredlHub (corresponds to the standard --target-dev-hub flag)
  • optionalOrg (corresponds to the standard --target-org flag)
  • requiredOrg (corresponds to the standard --target-org flag)
  • orgApiVersion (corresponds to the standard --api-version flag)

Instead, you can simply use the existing Salesforce CLI code to make the flag work the same way as it does in other CLI commands, and even use the standard definition (flag long and short name, description, default value). The dev generate flag command prompts you for all the information.

Flag Properties

Below are the most common properties when defining a new flag. See oclif docs for the full list and @salesforce/sf-plugins-core for Specialty Flags.

  • summary - Brief overview of the flag's purpose. Displayed with the --help | -h flags.
  • description - More in-depth explanation of the flag. Displayed only with the --help flag.
  • char - Short character that the user can use instead of the full flag name, such as -p instead of --package-name.
  • multiple - Set to true if the user can provide the flag multiple times in the same command execution, e.g. sf do awesome things -i 1 -i 2 -i 3
  • parse - A function to modify or validate input. The value returned from this function will be the new flag value.
  • dependsOn - Flags that must be passed in order to use this flag.
  • exclusive - This flag cannot be specified alongside these other flags.
  • exactlyOne - Exactly one of these flags must be provided.
  • required - Set to true if the user is required to provide this flag on every command execution.
  • default - Provide the default value for a flag if it is not provided by the user.
  • hidden - Set to true if you want to hide the flag from the user.

Flag Types

Choose a flag type based on the behavior you want that flag to cause. Using a specific flag type helps you validate the format of the flag value that your user supplies.

This section lists the types you can choose for your new flag. Each section includes a code snippet that shows an example of declaring the flag in your command Typescript file; see the summary for a description of the type.

See DeployMetadata class for the flags defined for the sf deploy metadata core Salesforce CLI command.

boolean

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

class MyCommand extends SfCommand {
  public static flags = {
    'my-boolean-flag': Flags.boolean({
      summary: 'a flag that expects a true/false value', 
    }),
  }
}

The flag doesn't accept an actual value; simply specifying it at the command line sets it to true. Sample user input:

--my-boolean-flag

directory

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

class MyCommand extends SfCommand {
  public static flags = {
    'my-dir-flag': Flags.directory({
      summary: 'a flag that expects a string that points to a directory', 
      exists: true, // optionally require the directory to exist
    }),
  }
}

Sample user input:

--my-dir-flag /Users/romeo/sfdx-projects

duration

This flag takes the input value and converts it to a Duration.

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

class MyCommand extends SfCommand {
  public static flags = {
    'my-duration-flag': Flags.duration({
      summary: 'a flag that expects a string that can be converted to a Duration', 
      unit: 'minutes',
    }),
  }
}

Sample user input:

--my-duration-flag 33

enum

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

enum MyEnum {
  'A' = 'A',
  'B' = 'B',
  'C' = 'C',
}

class MyCommand extends SfCommand {
  public static flags = {
    'my-enum-flag': Flags.enum<MyEnum>({
      summary: 'a flag that expects a specific value defined by an enum',
      options: Object.values(MyEnum),
    }),
  }
}

Sample user input:

--my-enum-flag B

file

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

class MyCommand extends SfCommand {
  public static flags = {
    'my-file-flag': Flags.file({
      summary: 'a flag that expects a string that points to a file', 
      exists: true, // optionally require the file to exist
    }),
  }
}

Sample user input:

--my-file-flag /Users/romeo/sfdx-projects/list.json

integer

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

class MyCommand extends SfCommand {
  public static flags = {
    'my-integer-flag': Flags.integer({
      summary: 'a flag that expects a number', 
      min: 0, // optionally set the minimum acceptable number
      max: 100 // optionally set the maximum acceptable number
    }),
  }
}

Sample user input:

--my-integer-flag 42

string

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

class MyCommand extends SfCommand {
  public static flags = {
    'my-string-flag': Flags.string({
      summary: 'a flag that expects a string value', 
    }),
  }
}

Sample user input:

--my-string-flag "awesome string value"

orgApiVersion

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

class MyCommand extends SfCommand {
  public static flags = {
    'my-apiversion-flag': Flags.orgApiVersion({
      summary: 'a flag that expects a valid Salesforce API version', 
    }),
  }
}

Sample user input:

--my-apiversion-flag 56.0

requiredHub

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

class MyCommand extends SfCommand {
  public static flags = {
    'my-devhub-flag': Flags.requiredOrg({
      summary: 'a flag that expects a username of a devhub org that you have authorized', 
    }),
  }
}

Sample user input:

--my-devhub-flag devhub@example.com

requiredOrg

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

class MyCommand extends SfCommand {
  public static flags = {
    'my-username-flag': Flags.requiredOrg({
      summary: 'a flag that expects a username of an org that you have authorized', 
    }),
  }
}

Sample user input:

--my-username-flag test-wvkpnfm5z113@example.com

optionalOrg

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

class MyCommand extends SfCommand {
  public static flags = {
    'my-optional-username-flag': Flags.requiredOrg({
      summary: 'a flag that expects a username of an org that you may or may not have authorized', 
    }),
  }
}

Sample user input:

--my-optional-username-flag test-wvkpnfm5z113@example.com

salesforceId

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

class MyCommand extends SfCommand {
  public static flags = {
    'my-sfid-flag': Flags.salesforceId({
      summary: 'a flag that expects a Salesforce ID', 
      length: 18, // optionally set the length of the id
      startsWith: '00D', // optionally set the string that the id must start with
    }),
  }
}

Sample user input:

--my-sfid-flag 04t001122334455ABC

url

This flag takes the input value and converts it to a URL

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

class MyCommand extends SfCommand {
  public static flags = {
    'my-url-flag': Flags.url({
      summary: 'a flag that expects a url that can be parsed by node's URL class', 
    }),
  }
}

Sample user input:

--my-url-flag https://developer.salesforce.com/docs

Add Logging

Add logging to your plugin with the Logger class from @salesforce/core.

Add Logging to a Command

Let's show how to add logging to the initial hello world command that was generated by sf dev generate plugin.

  1. Open src/commands/hello/world.ts and update the import from @salesforce/core to include the Logger class:

    import { Messages, Logger } from '@salesforce/core';
  2. Now create child logger instance. The Logger class creates a root logger with some defaults. Other code, like a command or library, then creates a child logger using the Logger.child() method. To try it out, replace the run method with this code:

      public async run(): Promise<HelloWorldResult> {
        const log = await Logger.child(this.ctor.name);
    
        const { flags } = await this.parse(World);
        const time = new Date().toDateString();
        const message = `Hello ${flags.name} at ${time}`;
    
        log.debug(`Time: ${time} | Name: ${flags.name}`);
    
        this.log(message);
        return {
          name: flags.name,
          time,
        };
      }

    The example shows how you pass the name of the child logger as the first argument of Logger.child(). In the example it's set it to this.ctor.name which points to the name of the command class (World in our case), but it can be anything you want.

  3. Let's now make the logger print records to the terminal. First set the DEBUG environment variable, either to * to see all log records or sf:_LoggerName_ to filter records by logger.

    For now, let's print logs for only the hello world command:

    For bash/zsh:

    export DEBUG='sf:World'

    For PowerShell:

    $Env:DEBUG = 'sf:World'
  4. Run the hello world command using bin/dev as usual:

    ./bin/dev hello world --name Astro
    

    You should see log and command output similar to this:

      sf:World DEBUG Time: Wed Sep 7 2022 | Name: Astro +0ms
    Hello Astro at Wed Sep 7 2022
    
  5. Remember to unset the DEBUG environment variable when you're done:

    For bash/zsh:

    unset DEBUG

    For PowerShell:

    $Env:DEBUG = ''

Logging Levels

The Logger class wraps pino, which already sets log levels. See logger.levels for more information.

Log Storage

The CLI saves logs to a file in the global .sf folder; you can disable this behavior by setting the SFDX_DISABLE_LOG_FILE environment variable to true. A new log file is started for each day, formatted like sf-YYYY-MM-DD.log. They'll be cleaned up eventually if more than 7 days old.

Log records are saved to a log file only if the log level method used is the same level or higher than the level set in the logger instance.

Let's see how this works. By default, the root logger sets the log level to warn. This example shows calls to debug, warn, and error methods, but only the warn and error records are saved to the log file:

log.debug('test1'); // log level: 20, below 40 so don't save it.
log.warn('test2');  // log level: 40, same level as the logger instance so record goes to file.
log.error('test3'); // log level: 50, above `warn` so it's saved to a file too.

The user can set the log level via environment variables (SF_LOG_LEVEL=debug or SF_LOG_LEVEL=trace) so it's best not to rely on code to manage your child Logger. Let the user control the log level.

Hooks

A hook is a piece of code that runs at a specific lifecycle event of a CLI command. Think of a hook as a pause in the CLI command execution. The command executes as usual until it encounters a hook. It pauses while the hook code executes, and then picks up again when the hook code execution is completed. Salesforce CLI supports all the Open CLI Framework (oclif) hooks.

For example, let's say you've configured the oclif hook init in your plugin. When you run any command in the plugin, the init hook fires after the CLI is initialized but before the command is found. If you've also configured a prerun hook, then that one fires right after the init hook but before the command itself is run. If you've configured a postrun hook ... You get the idea.

Create a hook by adding TypeScript or JavaScript code and configuration information to your custom Salesforce CLI plugin. You can create a plugin that contains only hooks, or add the hook code and configuration to a plugin that contains all your business logic; it's up to you!

When a user installs the plugin that contains the hooks into their CLI, the hook fires at the appropriate lifecycle event. The hook continues to fire until the user explicitly uninstalls the custom plugin. If you want the hooks to fire in a continuous integration (CI) job, install the custom plugin before you run any Salesforce CLI commands.

Create a Salesforce CLI Hook

The best way to show how to create a hook in your custom plugin is to show how Salesforce CLI itself uses hooks. We'll use the the top-level Salesforce CLI npm package @salesforce/cli as an example.

  1. Code the hook in TypeScript or JavaScript. For example, this code is for a prerun hook that Salesforce CLI uses to check the version of the plugin that contains the command that the user is executing. The hook alerts the user if their installed plugin version is different from the version bundled with the CLI.

  2. Update your plugin's package.json file and add a hooks object inside the oclif object. The hooks object specifies the type of oclif hook, such as prerun, and the location of your compiled source. For example:

    {
    ...
      "oclif": {
         "commands": "./lib/commands",
         "bin": "sf",
         "hooks": {
             "prerun": "./lib/hooks/prerun"
        }
    ...
    }
    

    See the @salesforce/cli package.json for another example.

  3. After you release your plugin, users install it in their CLI as usual.

    sf plugins install my-plugin-with-hooks

    From this point on, or until the user uninstalls the plugin, the configured hooks fire when the user runs any Salesforce CLI command.

Clone this wiki locally