From 67d99422eeb6eef7bd4004d8f0a9a6eddfd67dbb Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 27 Mar 2024 11:21:04 -0600 Subject: [PATCH 01/13] feat: update hooks.md --- docs/hooks.md | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/hooks.md b/docs/hooks.md index 931792b1..c6b85832 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -20,27 +20,27 @@ export default hook The hook must also be declared with the event's name and hook's file path under oclif's settings in `package.json`: -```js - "oclif": { - "commands": "./lib/commands", - "hooks": { - "init": "./lib/hooks/init/example" - } +```json +"oclif": { + "commands": "./lib/commands", + "hooks": { + "init": "./lib/hooks/init/example" } +} ``` Multiple hooks of the same event type can be declared with an array. -```js - "oclif": { - "commands": "./lib/commands", - "hooks": { - "init": [ - "./lib/hooks/init/example", - "./lib/hooks/init/another_hook" - ] - } +```json +"oclif": { + "commands": "./lib/commands", + "hooks": { + "init": [ + "./lib/hooks/init/example", + "./lib/hooks/init/another_hook" + ] } +} ``` You can create hooks with `oclif generate hook myhook --event=init`. @@ -49,8 +49,11 @@ You can create hooks with `oclif generate hook myhook --event=init`. * `init` - runs when the CLI is initialized before a command is found to run * `prerun` - runs after `init` and after the command is found, but just before running the command itself -* `postrun` - runs after the command only if the command finishes with no error * `command_not_found` - runs if a command is not found before the error is displayed +* `command_incomplete` - runs if a command is not found but it is a partial of an existing command. Useful for instances where you'd like to present a prompt with all the matching commands. See Salesforce CLI's [implementation](https://github.com/salesforcecli/cli/blob/main/src/hooks/incomplete.ts). +* `jit_plugin_not_installed` - runs if a command from a [JIT plugin](./jit_plugins.md) is executed but the plugin isn't installed yet. See Salesforce CLI's [implementation](https://github.com/salesforcecli/plugin-trust/blob/main/src/hooks/jitPluginInstall.ts). +* `preparse` - runs before flags and args are parsed and validated. Useful if you need to manipulate the input. See Salesforce CLI's [implementation](https://github.com/salesforcecli/cli/blob/main/src/hooks/preparse.ts). +* `postrun` - runs after the command only if the command finishes with no error ## Custom Events @@ -89,4 +92,4 @@ export class extends Command { } ``` -If you need to exit during a hook, use `this.error()` or `this.exit()`. Otherwise the hook will just emit a warning. This is to prevent an issue such as a plugin failing in `init` causing the entire CLI to not function. +If you need to exit during a hook, use `options.context.error()` or `options.context.exit()`. Throwing an `Error` will not cause the CLI to exit - this is to prevent an issues such a single plugin's `init` hook causing a CLI to immediately fail on every command execution. From 42024ac73fa734e362cede6ae12e75b7323dabc5 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 27 Mar 2024 12:44:35 -0600 Subject: [PATCH 02/13] feat: add doc on Performance class --- docs/performance.md | 196 ++++++++++++++++++++++++++++++++++++++++++ website/sidebars.json | 3 +- 2 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 docs/performance.md diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 00000000..ef0723de --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,196 @@ +--- +title: Performance +--- + +oclif supports performance tracking out of the box - both for oclif and you own code. You can enable it in one of two ways depending on how you've implemented your bin scripts. + +If you're using the bin scripts that come from the generator, you can simply set `performanceEnabled` on the `settings` module: + +## How to Enable + +```typescript +#!/usr/bin/env node + +import {execute, settings} from '@oclif/core' +settings.performanceEnabled = true +await execute({dir: import.meta.url}) +``` + +You can also enable it on the `Config` class directly if that works better for your CLI: + +```typescript +import {Config, run} from '@oclif/core' +import {fileUrlToPath} from 'node:url' + +const config = await Config.load({ + root: resolve(fileURLToPath(import.meta.url), '..');, + enablePerf: true +}) + +await run(config) +``` + +## Accessing oclif-specific performance metrics + +Once performance is enabled, you can see the in the debug output under the `oclif-perf` scope. + +``` +❯ DEBUG=oclif-perf sf version +@salesforce/cli/2.35.6 darwin-arm64 node-v20.11.0 + oclif-perf Process Uptime: 747.6823ms +0ms + oclif-perf Oclif Time: 302.1286ms +0ms + oclif-perf Init Time: 37.7735ms +0ms + oclif-perf Config Load Time: 294.5321ms +0ms + oclif-perf • Root Plugin Load Time: 11.2781ms +0ms + oclif-perf • Plugins Load Time: 274.6006ms +0ms + oclif-perf • Commands Load Time: 6.7736ms +0ms + oclif-perf Core Plugin Load Time: 20.9403ms +0ms + oclif-perf User Plugin Load Time: 0.0000ms +0ms + oclif-perf Linked Plugin Load Time: 2.3124ms +0ms + oclif-perf Plugin Load Times: +0ms + oclif-perf oclif-hello-world: 239.1951ms (no manifest!) +0ms + oclif-perf @oclif/plugin-update: 18.8549ms +0ms + oclif-perf @oclif/plugin-autocomplete: 17.5277ms +0ms + oclif-perf @oclif/plugin-commands: 16.4917ms +0ms + oclif-perf @oclif/plugin-not-found: 16.3310ms +0ms + oclif-perf @oclif/plugin-search: 15.8846ms +0ms + oclif-perf @oclif/plugin-version: 14.8063ms +0ms + oclif-perf @salesforce/plugin-org: 14.5657ms +0ms + oclif-perf @salesforce/plugin-data: 14.3991ms +0ms + oclif-perf @oclif/plugin-warn-if-update-available: 14.3733ms +0ms + oclif-perf @oclif/plugin-which: 14.1629ms +0ms + oclif-perf @salesforce/plugin-apex: 13.9885ms +0ms + oclif-perf @salesforce/plugin-auth: 13.6895ms +0ms + oclif-perf @salesforce/plugin-deploy-retrieve: 13.6353ms +0ms + oclif-perf @salesforce/plugin-limits: 13.1123ms +0ms + oclif-perf @salesforce/plugin-packaging: 12.9777ms +0ms + oclif-perf @salesforce/plugin-info: 12.7787ms +0ms + oclif-perf @salesforce/plugin-marketplace: 12.1542ms +0ms + oclif-perf @salesforce/plugin-source: 11.9480ms +0ms + oclif-perf @salesforce/plugin-schema: 11.7775ms +0ms + oclif-perf @salesforce/plugin-settings: 11.4785ms +0ms + oclif-perf @salesforce/plugin-templates: 11.4225ms +0ms + oclif-perf @salesforce/plugin-sobject: 11.3670ms +0ms + oclif-perf root: 11.2781ms +0ms + oclif-perf @salesforce/plugin-user: 10.8521ms +0ms + oclif-perf @salesforce/plugin-telemetry: 10.5724ms +1ms + oclif-perf @salesforce/plugin-trust: 10.4463ms +0ms + oclif-perf @oclif/plugin-plugins: 2.0135ms +0ms + oclif-perf @oclif/plugin-help: 1.9039ms +0ms + oclif-perf Hook Run Times: +0ms + oclif-perf init: +0ms + oclif-perf total: 37.4624ms +0ms + oclif-perf oclif-hello-world(./dist/hooks/init/init): 7.9818ms +0ms + oclif-perf @oclif/plugin-warn-if-update-available(./lib/hooks/init/check-update): 37.1145ms +0ms + oclif-perf @salesforce/plugin-settings(./lib/hooks/init/load_config_meta): 29.3073ms +0ms + oclif-perf @oclif/plugin-update(./dist/hooks/init.js): 33.1767ms +0ms + oclif-perf prerun: +0ms + oclif-perf total: 260.5702ms +0ms + oclif-perf @salesforce/cli(./dist/hooks/prerun): 2.3582ms +0ms + oclif-perf @salesforce/plugin-telemetry(./lib/hooks/telemetryPrerun.js): 260.2634ms +0ms + oclif-perf preparse: +0ms + oclif-perf total: 0.5351ms +0ms + oclif-perf @salesforce/cli(/dist/hooks/preparse): 0.5045ms +0ms + oclif-perf postrun: +0ms + oclif-perf total: 0.3507ms +0ms + oclif-perf Command Load Time: 0.7478ms +0ms + oclif-perf Command Run Time: 264.2587ms +0ms +``` + +You can also access these metrics programmatically like so: + +```typescript +const {Performance, flush, handle, run, settings} = await import('@oclif/core') +settings.performanceEnabled = true +await run(process.argv.slice(2)) + .catch(async (error) => handle(error)) + .finally(async () => { + console.log('Performance', Performance.oclifPerf) + await flush() + }) +``` + +## Using `Performance` for your code + +You can also use the `Performance` class to capture performance metrics on your own code base. + +To capture performance metrics for a block of code, you need to create a new `marker` using `Performance.mark`. You then need to call the `.stop` method the `marker` to finish timing that block of code. + +Here's a brief example: + +```typescript +import {Performance} from '@oclif/core' +// Create a new marker. +// First argument is the owner of the marker (This allows Performance to be able to distinguish the origin of each marker) +// Second argument is the name of the maker. Naming convention that oclif uses internally is .#scope. You are free, however, to name these however you like. +const marker = Performance.mark('my-plugin', 'hello.run') + +// do things that take a while. + +// Add details to the marker that you might want to access later +marker?.addDetails({from: flags.from, person: args.person}) +// Stop the marker. This will capture the amount of time between the creation of the marker and the stopping of the marker. +marker?.stop() +``` + +And a more thorough example using the `hello` command from the [hello-world](https://github.com/oclif/hello-world) template: + +```typescript +// ./src/commands/hello/index.ts +import {Args, Command, Flags, Performance} from '@oclif/core' + +export default class Hello extends Command { + static args = { + person: Args.string({description: 'Person to say hello to', required: true}), + } + + static flags = { + from: Flags.string({char: 'f', description: 'Who is saying hello', required: true}), + } + + async run(): Promise { + const {args, flags} = await this.parse(Hello) + const marker = Performance.mark('my-plugin', 'hello.run') + await new Promise((resolve) => { + setTimeout(resolve, 1000) + }) + marker?.addDetails({from: flags.from, person: args.person}) + marker?.stop() + this.log(`hello ${args.person} from ${flags.from}! (./src/commands/hello/index.ts)`) + } +} +``` + +All the markers you create will be accessible on the static `results` property on `Performance`: + +```typescript +// bin/run.js +const {Performance, flush, handle, run, settings} = await import('@oclif/core') +settings.performanceEnabled = true +await run(process.argv.slice(2)) + .catch(async (error) => handle(error)) + .finally(async () => { + console.log('Results', Performance.results) + await flush() + }) +``` + +``` +❯ bin/run.js hello reader --from oclif +hello reader from oclif! (./src/commands/hello/index.ts) +Results Map(1) { + 'my-plugin' => [ + { + details: { + from: oclif, + person: reader + }, + duration: 1003.4971249999999, + method: 'run', + module: 'hello', + name: 'hello.run', + scope: undefined + } + ] +} +``` diff --git a/website/sidebars.json b/website/sidebars.json index 71fceca9..fba23fa7 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -38,7 +38,8 @@ "global_flags", "single_command_cli", "esm", - "themes" + "themes", + "performance" ], "Also See": [ "examples", From 5caad35c0a8f4e01ca3c47a9e823bc89f0f5e3eb Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 27 Mar 2024 13:22:04 -0600 Subject: [PATCH 03/13] fix: move perf page --- website/sidebars.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/sidebars.json b/website/sidebars.json index fba23fa7..4013033b 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -19,7 +19,8 @@ "plugins", "help_classes", "error_handling", - "json" + "json", + "performance" ], "How to": [ "releasing", @@ -38,8 +39,7 @@ "global_flags", "single_command_cli", "esm", - "themes", - "performance" + "themes" ], "Also See": [ "examples", From 9543d71235c904e657ddc2b39deb7fbcf0553c1a Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 27 Mar 2024 13:33:09 -0600 Subject: [PATCH 04/13] fix: update help class examples --- docs/help_classes.md | 128 ++++++++----------------------------------- 1 file changed, 24 insertions(+), 104 deletions(-) diff --git a/docs/help_classes.md b/docs/help_classes.md index 49d4f9c3..e74ad621 100644 --- a/docs/help_classes.md +++ b/docs/help_classes.md @@ -16,22 +16,15 @@ $ my-cli help ## Custom Help -``` -$ yarn add @oclif/core --latest -``` - To get started, first define the filepath to your help class in oclif's config in package.json. This is a relative path to the help class, without a file extension. For this example, the help class will be created in a file at "[project root]/src/help.ts". -``` +```json { - // ... "oclif": { "helpClass": "./dist/help" - // ... } - // ... } ``` @@ -50,7 +43,7 @@ export default class CustomHelp extends HelpBase { console.log('This will be displayed in multi-command CLIs') } - showCommandHelp(command: Command) { + showCommandHelp(command: Command.Loadable) { console.log('This will be displayed in single-command CLIs') } } @@ -68,27 +61,23 @@ To see an example of what is possible take a look at the source code for the [de The default `Help` class provides many method “hooks” that make it easy to override the particular parts of help's output you want to customize. ```TypeScript -import {Command, Help, Topic} from '@oclif/core'; +import {Command, Help, Interfaces} from '@oclif/core' export default class MyHelpClass extends Help { // acts as a "router" // and based on the args it receives // calls one of showRootHelp, showTopicHelp, - // or showCommandHelp - showHelp(args: string[]): void { - } + // the formatting for an individual command + formatCommand(command: Command.Loadable): string {} - // display the root help of a CLI - showRootHelp(): void { - } + // the formatting for a list of commands + formatCommands(commands: Command.Loadable[]): string {} - // display help for a topic - showTopicHelp(topic: Topic): void { - } + // displayed for the root help + formatRoot(): string {} - // display help for a command - showCommandHelp(command: Command): void { - } + // the formatting for an individual topic + formatTopic(topic: Interfaces.Topic): string {} // the default implementations of showRootHelp // showTopicHelp and showCommandHelp @@ -98,26 +87,22 @@ export default class MyHelpClass extends Help { // these can be overwritten as well // the formatting responsible for the header - // displayed for the root help - formatRoot(): string { - } + // the formatting for a list of topics + protected formatTopics(topics: Interfaces.Topic[]): string {} - // the formatting for an individual topic - formatTopic(topic: Config.Topic): string { - } + // display help for a command + showCommandHelp(command: Command.Loadable): void {} - // the formatting for a list of topics - protected formatTopics(topics: Config.Topic[]): string { - } + // or showCommandHelp + showHelp(args: string[]): void {} - // the formatting for a list of commands - formatCommands(commands: Config.Command[]): string { - } + // display the root help of a CLI + showRootHelp(): void {} - // the formatting for an individual command - formatCommand(command: Config.Command): string { - } + // display help for a topic + showTopicHelp(topic: Interfaces.Topic): void {} } + ``` To see the default implementation of these methods take a look at the [default `Help` class exported from @oclif/core](https://github.com/oclif/core/blob/main/src/help/index.ts). @@ -126,10 +111,10 @@ To start experimenting, define `showCommandHelp` in your custom help class and c ```TypeScript -import {Command, Help, Topic} from '@oclif/core'; +import {Command, Help} from '@oclif/core'; export default class MyHelpClass extends Help { - public showCommandHelp(command: Config.Command) { + public showCommandHelp(command: Command.Loadable) { console.log('Display my custom command help!') } } @@ -141,68 +126,3 @@ Then run help for any command. $ my-cli login --help Display my custom command help! ``` - - -## Building custom help classes in JavaScript projects - -These examples above followed a TypeScript project. For JavaScript project with an example help file at "[project root]/src/help.js" would have a package.json with the `helpClass` defined: - -``` -{ - // ... - "oclif": { - "helpClass": "./src/help" - // ... - } - // ... -} -``` - -The imports are handled slightly different for JavaScript projects but the rest of the help class mimic the TypeScript examples above, except without type annotations. - -```js -const {HelpBase} = require('@oclif/core'); - -module.exports = class MyHelpClass extends HelpBase { - showHelp(args) { - console.log('This will be displayed in multi-command CLIs') - } - - showCommandHelp(command) { - console.log('This will be displayed for a single command') - } -} -``` - -```js -const {Help} = require('@oclif/core'); - -module.exports = class MyHelpClass extends Help { - showHelp(args) { - } - - showRootHelp() { - } - - showTopicHelp(topic) { - } - - showCommandHelp(command) { - } - - formatRoot() { - } - - formatTopic(topic) { - } - - formatTopics(topics) { - } - - formatCommands(commands) { - } - - formatCommand(command) { - } -} -``` From 067d589a1b14f92d721f71fb7de6640d9f372302 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 27 Mar 2024 13:51:42 -0600 Subject: [PATCH 05/13] fix: error handling --- docs/error_handling.md | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/docs/error_handling.md b/docs/error_handling.md index 5940cdac..97693f59 100644 --- a/docs/error_handling.md +++ b/docs/error_handling.md @@ -2,7 +2,7 @@ title: Error Handling --- -oclif handles intentionally - and unintentionally - thrown errors in two places. First in the `Command.catch` method and then, finally, in the bin/run `catch` handler where the Error is printed and the CLI exits. This error flow makes it possible for you to control and respond to errors that occur in your CLI as you see fit. +oclif handles intentionally - and unintentionally - thrown errors in two places. First in the `Command.catch` method and then, finally, in the `bin/run.js` `catch` handler where the Error is printed and the CLI exits. This error flow makes it possible for you to control and respond to errors that occur in your CLI as you see fit. ## Error Handling in the `catch` method @@ -20,24 +20,41 @@ export default class Hello extends Command { } ``` -If this type of error handling is being implemented across multiple commands consider using a Custom Base Class (https://oclif.io/docs/base_class#docsNav) for your commands and overriding the `catch` method. +If this type of error handling is being implemented across multiple commands consider using a [Custom Base Class](./base_class.md) for your commands and overriding the `catch` method. ## bin/run.js `catch` handler -Every oclif CLI has a ./bin/run.js file that is the entry point of command invocation. Errors that occur in the CLI, including re-thrown errors from a Command, are caught here in the bin/run.js `catch` handler. +Every oclif CLI has a `./bin/run.js` file that is the entry point of command invocation. Errors that occur in the CLI, including re-thrown errors from a Command, are caught and handled by oclif's `handle` function, which displays the error to the user. -```js -.catch(require('@oclif/core/handle')) +If you generated your CLI using `oclif generate`, then you will see an `execute` function that's responsible for running the CLI and catching any errors. You can, however, implement this yourself if you need to customize the error handling. + +Here's the generic `bin/run.js` that comes with `oclif generate`: + +```javascript +#!/usr/bin/env node + +import {execute} from '@oclif/core' + +await execute({dir: import.meta.url}) ``` -This catch handler uses the `@oclif/errors/handle` function to display (and cleanup, if necessary) the error to the user. This handler can be swapped for any function that receives an error argument. +To customize error handling, you'll want to use oclif's `run` function instead of `execute`: + +```javascript +#!/usr/bin/env node + +import {run, handle, flush} from '@oclif/core' + +await run(process.argv.slice(2), import.meta.url) + .catch(async (error) => handle(error)) + .finally(async () => flush()) +``` -If you chose to implement your own handler here, we still recommend you delegate finally to the `@oclif/core/handle` function for clean-up and exiting logic. +The `catch` handler can be swapped for any function that receives an error argument. If you chose to implement your own handler here, we still recommend you delegate finally to the `handle` function for clean-up and exiting logic. ```js .catch((error) => { - const oclifHandler = require('@oclif/core/handle'); // do any extra work with error - return oclifHandler(error); + return handle(error); }) ``` From 8f41ae55ef134881b2ff7e96ac5150443f84cf24 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 27 Mar 2024 13:52:37 -0600 Subject: [PATCH 06/13] fix: json docs --- docs/json.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/json.md b/docs/json.md index ca4ecb2b..44d2bea7 100644 --- a/docs/json.md +++ b/docs/json.md @@ -11,7 +11,7 @@ import {Command} from '@oclif/core' export class HelloCommand extends Command { public static enableJsonFlag = true public async run(): Promise<{ message: string }> { - console.log('hello, world!') + this.log('hello, world!') return { message: 'hello, world!' } } } From 72348faefa6df01d242a1655e01d6be1d98ea7df Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 27 Mar 2024 14:03:28 -0600 Subject: [PATCH 07/13] fix: running programmatically --- docs/running_programmatically.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/running_programmatically.md b/docs/running_programmatically.md index 1496c4e7..974448d8 100644 --- a/docs/running_programmatically.md +++ b/docs/running_programmatically.md @@ -8,7 +8,7 @@ First, it is generally a bad idea to run a command directly as the command expor ## Sharing code with modules -For example, if we use `sf config list` as an example, we could have a command that outputs the config vars of an app to the screen like this: +For example, suppose you have a `config list` command that outputs config vars of an app to the terminal. **./src/commands/config/list.ts** @@ -28,7 +28,11 @@ export class ConfigList extends Command { } ``` -If we had another command such as `sf config update` that would do some logic then display the config variables using the same logic, we should create a new module that we could call directly: +Then suppose you have another command, `config update`, that updates the app's configuration and finally displays the new config vars to the terminal. + +Since both `config list` and `config update` need to display the config vars in the exact same way, you should create a new module, function, or class that's responsible for creating the display. + +For example: **./src/commands/config/update.ts** @@ -65,7 +69,7 @@ This is the recommended way to share code. This can be extended further by putti Still, if you _really_ want to call a command directly, it's easy to do. You have a couple of options. -If you know that the command you want to run is installed in the CLI, you can use `this.config.runCommand`. For this, we could write our `sf config update` command like so: +If you know that the command you want to run is installed in the CLI, you can use `this.config.runCommand`. For this, we could write our `config update` command like so: **./src/commands/config/update.ts** @@ -83,7 +87,7 @@ export class ConfigUpdate extends Command { } ``` -Or you could import the command directly and execute it directly like so: +The second option is to import the command directly and execute it directly like so: **./src/commands/config/update.ts** @@ -104,3 +108,5 @@ export class ConfigUpdate extends Command { ``` This works because commands have a static `.run()` [method on them](https://github.com/oclif/core/blob/main/src/command.ts) that can be used to instantiate the command and run the instance `.run()` method. It takes in the argv as input to the command. + +But, again, we **strongly encourage** you to avoid these options. It's far better to extract any shared logic out of a `Command` class. From c56b14ea2efeeda26c2d7c2eb47ac63743aaa80c Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 27 Mar 2024 14:06:33 -0600 Subject: [PATCH 08/13] fix: jit docs --- docs/jit_plugins.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/jit_plugins.md b/docs/jit_plugins.md index 12f731e7..e14af632 100644 --- a/docs/jit_plugins.md +++ b/docs/jit_plugins.md @@ -21,23 +21,23 @@ To use this feature you need to: 3. Implement the `jit_plugin_not_installed` hook. -`@oclif/core` attempts to be UX-agnostic, meaning that we don't want to impose any particular user experience on you. Any time a user experience is required we utilize hooks so that you can design the exact user experience you want your users to have. +oclif attempts to be UX-agnostic, meaning that we don't want to impose any particular user experience on you. Any time a user experience is required we utilize [hooks](./hooks.md) so that you can design the exact user experience you want your users to have. In the case of JIT plugin installation, there are many possible user experiences that you might want - maybe you want to prompt the user for confirmation first, or maybe you want to log a specific message, etc... Here's an example of how you might implement the hook, ```typescript -import { Hook, CLIError, ux } from '@oclif/core'; +import { Hook, Errors, ux } from '@oclif/core'; -const hook: Hook<'jit_plugin_not_installed'> = async function (opts) { +const hook: Hook.JitPluginNotInstalled = async function (opts) { try { const answer = await ux.confirm(`${opts.command.pluginName} not installed. Would you like to install?`) if (answer === 'y') { await opts.config.runCommand('plugins:install', [`${opts.command.pluginName}@${opts.pluginVersion}`]); } } catch (error) { - throw new CLIError(`Could not install ${opts.command.pluginName}`, 'JitPluginInstallError'); + throw new Errors.CLIError(`Could not install ${opts.command.pluginName}`, 'JitPluginInstallError'); } }; From 2095c0288557aff7c4e7eb20e3542af4301fe571 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 27 Mar 2024 14:09:24 -0600 Subject: [PATCH 09/13] fix: base_class.md --- docs/base_class.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/base_class.md b/docs/base_class.md index 79df82f6..8b00a43a 100644 --- a/docs/base_class.md +++ b/docs/base_class.md @@ -10,13 +10,6 @@ For large CLIs with multiple plugins, it's useful to put this base class into it // src/baseCommand.ts import {Command, Flags, Interfaces} from '@oclif/core' -enum LogLevel { - debug = 'debug', - info = 'info', - warn = 'warn', - error = 'error', -} - export type Flags = Interfaces.InferredFlags export type Args = Interfaces.InferredArgs @@ -26,10 +19,11 @@ export abstract class BaseCommand extends Command { // define flags that can be inherited by any command that extends BaseCommand static baseFlags = { - 'log-level': Flags.custom({ - summary: 'Specify level for logging.', - options: Object.values(LogLevel), + 'log-level': Flags.option({ + default: 'info', helpGroup: 'GLOBAL', + options: ['debug', 'warn', 'error', 'info', 'trace'] as const, + summary: 'Specify level for logging.', })(), } From 31d95d339d98b030cacc2eb185aeba6991a81580 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 27 Mar 2024 14:15:09 -0600 Subject: [PATCH 10/13] feat: flexible_taxonomy.md --- docs/flexible_taxonomy.md | 26 +++++++++++--------------- docs/hooks.md | 2 +- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/docs/flexible_taxonomy.md b/docs/flexible_taxonomy.md index 7a238fa2..09ae44c4 100644 --- a/docs/flexible_taxonomy.md +++ b/docs/flexible_taxonomy.md @@ -17,35 +17,31 @@ If you'd like for your customers to execute commands without adhereing to the de There are two main benefits to enabling flexible taxonomy: -1. It makes your CLI more user-friendly. For example, you might have a command, `my-cli foobars:list`. If a user mistakenly enters `my-cli list:foobars` then oclif will automatically know that it should execute `foobars:list` instead of throwing an error. +1. It makes your CLI more user-friendly. For example, you might have a command, `my-cli foobars list`. If a user mistakenly enters `my-cli list foobars` then oclif will automatically know that it should execute `foobars list` instead of throwing an error. 2. It gives you the opportunity to prompt a user for the right command if they only provide part of a command. This makes individual commands more discoverable, especially if you have a large number of commands. See [Hook Implementation](#hook-implementation) for more details. ### Hook Implementation -When `flexibleTaxonomy` is enabled, oclif will run the `command_incomplete` hook anytime a user enters an incomplete command (e.g. the command is `one:two:three` but they only entered `two`). This hook gives you the opportunity to create an interactive user experience. +When `flexibleTaxonomy` is enabled, oclif will run the `command_incomplete` hook anytime a user enters an incomplete command (e.g. the command is `one two three` but they only entered `two`). This hook gives you the opportunity to create an interactive user experience. -This example shows how you can use the [inquirer](#https://www.npmjs.com/package/inquirer) package to prompt the user for which command they would like to run: +This example shows how you can use the [inquirer](https://www.npmjs.com/package/inquirer) package to prompt the user for which command they would like to run: ```typescript -import { Hook, toConfiguredId, toStandardizedId } from "@oclif/core"; -import { prompt } from "inquirer"; +import { Hook, toConfiguredId, toStandardizedId } from '@oclif/core'; +import { select } from '@inquirer/prompts'; const hook: Hook.CommandIncomplete = async function ({ config, matches, argv, }) { - const { command } = await prompt<{ command: string }>([ - { - name: "command", - type: "list", - message: "Which of these commands would you like to run?", - choices: matches.map((p) => toConfiguredId(p.id, config)), - }, - ]); + const command = await select({ + message: 'Which of these commands would you like to run?', + choices: matches.map((p) => toConfiguredId(p.id, config)), + }); - if (argv.includes("--help") || argv.includes("-h")) { - return config.runCommand("help", [toStandardizedId(command, config)]); + if (argv.includes('--help') || argv.includes('-h')) { + return config.runCommand('help', [toStandardizedId(command, config)]); } return config.runCommand(toStandardizedId(command, config), argv); diff --git a/docs/hooks.md b/docs/hooks.md index c6b85832..1d5249c8 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -50,7 +50,7 @@ You can create hooks with `oclif generate hook myhook --event=init`. * `init` - runs when the CLI is initialized before a command is found to run * `prerun` - runs after `init` and after the command is found, but just before running the command itself * `command_not_found` - runs if a command is not found before the error is displayed -* `command_incomplete` - runs if a command is not found but it is a partial of an existing command. Useful for instances where you'd like to present a prompt with all the matching commands. See Salesforce CLI's [implementation](https://github.com/salesforcecli/cli/blob/main/src/hooks/incomplete.ts). +* `command_incomplete` - runs if a command is not found but it is a partial of an existing command. Only works if [flexible taxonomy](./flexible_taxonomy.md) is enabled. Useful for instances where you'd like to present a prompt with all the matching commands. See Salesforce CLI's [implementation](https://github.com/salesforcecli/cli/blob/main/src/hooks/incomplete.ts). * `jit_plugin_not_installed` - runs if a command from a [JIT plugin](./jit_plugins.md) is executed but the plugin isn't installed yet. See Salesforce CLI's [implementation](https://github.com/salesforcecli/plugin-trust/blob/main/src/hooks/jitPluginInstall.ts). * `preparse` - runs before flags and args are parsed and validated. Useful if you need to manipulate the input. See Salesforce CLI's [implementation](https://github.com/salesforcecli/cli/blob/main/src/hooks/preparse.ts). * `postrun` - runs after the command only if the command finishes with no error From a1ec685525d49f98cb1efb333e1b221fb89b766e Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 27 Mar 2024 14:22:30 -0600 Subject: [PATCH 11/13] feat: flag inheritance --- docs/flag_inheritance.md | 64 ++++++++++++++++++++++++++++++++++++++++ docs/global_flags.md | 21 ------------- website/sidebars.json | 2 +- 3 files changed, 65 insertions(+), 22 deletions(-) create mode 100644 docs/flag_inheritance.md delete mode 100644 docs/global_flags.md diff --git a/docs/flag_inheritance.md b/docs/flag_inheritance.md new file mode 100644 index 00000000..e9a1ab99 --- /dev/null +++ b/docs/flag_inheritance.md @@ -0,0 +1,64 @@ +--- +title: Flag Inheritance +--- + +There are some instances where you might want to define a flag once for all of your commands. In this case you can add a base flag to an abstract base `Command` class. For example, + + +```typescript +import { Command, Flags } from '@oclif/core'; + +export abstract class BaseCommand extends Command { + static baseFlags = { + interactive: Flags.boolean({ + char: 'i', + description: 'Run command in interactive mode', + // Show this flag under a separate GLOBAL section in help. + helpGroup: 'GLOBAL', + }), + }; +} +``` + +Any command that extends `BaseCommand` will now have an `--interactive` flag on it. + +If you are going to stack multiple layers of abstract `Command` classes, then you must spread the `baseFlags` to ensure that the flags are properly inherited. For example, + +```typescript +import { Command, Flags } from '@oclif/core'; + +export abstract class FirstBaseCommand extends Command { + static baseFlags = { + interactive: Flags.boolean({ + char: 'i', + description: 'Run command in interactive mode', + // Show this flag under a separate GLOBAL section in help. + helpGroup: 'GLOBAL', + }), + }; +} + +export abstract class SecondBaseCommand extends FirstBaseCommand { + static baseFlags = { + ...FirstBaseCommand.baseFlags, + 'log-level': Flags.option({ + default: 'info', + description: 'Specify log level', + helpGroup: 'GLOBAL', + options: ['debug', 'warn', 'error', 'info', 'trace'] as const, + summary: 'Specify level for logging.', + char: 'l', + })(), + }; +} + +export abstract class ThirdBaseCommand extends SecondBaseCommand { + static baseFlags = { + ...SecondBaseCommand.baseFlags, + verbose: Flags.boolean({ + description: 'Show verbose output.', + char: 'v' + }) + }; +} +``` diff --git a/docs/global_flags.md b/docs/global_flags.md deleted file mode 100644 index 83ccb721..00000000 --- a/docs/global_flags.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Global Flags ---- - -There are some instances where you might want to define a flag once for all of your commands. In this case you can add a global flag to an abstract base `Command` class. For example, - - -```typescript -import { Command, Flags } from '@oclif/core'; - -export abstract class BaseCommand extends Command { - static baseFlags = { - interactive: Flags.boolean({ - char: 'i', - description: 'Run command in interactive mode', - }), - }; -} -``` - -Any command that extends `BaseCommand` will now have an `--interactive` flag on it. diff --git a/website/sidebars.json b/website/sidebars.json index 4013033b..b81bf5a1 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -36,7 +36,7 @@ "notifications", "debugging", "flexible_taxonomy", - "global_flags", + "flag_inheritance", "single_command_cli", "esm", "themes" From bb1f49278ee3e57af05a6c05575fb275ef95ced3 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 27 Mar 2024 14:23:41 -0600 Subject: [PATCH 12/13] fix: single command CLI --- docs/single_command_cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/single_command_cli.md b/docs/single_command_cli.md index 77de2f61..26faefd8 100644 --- a/docs/single_command_cli.md +++ b/docs/single_command_cli.md @@ -17,4 +17,4 @@ To support this, you will need to put your command logic into `src/index.ts` and } ``` -See [Command Discovery Strategies](./command_discovery_strategies) for more details. +where `./dist/index.js` is a file that exports a `Command` class. See [Command Discovery Strategies](./command_discovery_strategies) for more details. From b549f38f846da7ffb8b9dbceace2081c363131bc Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 27 Mar 2024 14:26:37 -0600 Subject: [PATCH 13/13] fix: esm.md --- docs/esm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/esm.md b/docs/esm.md index b2d68b72..be4cdc75 100644 --- a/docs/esm.md +++ b/docs/esm.md @@ -2,7 +2,7 @@ title: ESM --- -Version 3.0.0 of `@oclif/core` officially supports ESM plugin development and CJS/ESM interoperability, meaning that you can have a root plugin written with CJS and your bundled plugins written in ESM or vice versa. +Version 3.0.0 of `@oclif/core` officially supports ESM plugin development and CJS/ESM interoperability, meaning that you can have a root plugin written with CJS and your plugins written in ESM or vice versa. - [Interoperability Overview](#interoperability-overview) - [ESM Root plugin](#esm-root-plugin)