@clibuilder/plugins
@clibuilder/plugins adds plugin support to a clibuilder CLI. It loads plugin packages, and adds
the plugins list and plugins search commands. A CLI opts in with one call to
.use(). A CLI that does not call it has no plugin system to
learn.
@clibuilder/plugins 0.2.0 is published to npm. It needs clibuilder 12.0.0 or later. 0.1.0 needs
clibuilder 11.5.0 or later.
Install
Section titled “Install”Install it with:
npm install @clibuilder/plugins clibuilderclibuilder ^11.5.0 || ^12 is the peer dependency of 0.1.0. 0.2.0 requires
clibuilder ^12. Node.js 20.19 or newer is required.
0.1.0 publishes ESM and CJS. 0.2.0 is ESM-only; require('@clibuilder/plugins') still
works through Node’s require(esm).
import { cli } from 'clibuilder'import { config } from 'clibuilder/config'import { plugins } from '@clibuilder/plugins'
cli({ name: 'my-cli', version: '1.0.0' }) .use(config()) .use(plugins({ keywords: ['my-cli-plugin'] })) .parse(process.argv)plugins() returns an extension for cli().use(). When it runs, it adds the plugins command
group and loads plugins from up to three sources.
Options
Section titled “Options”| Option | Default | Meaning |
|---|---|---|
keywords |
[name] of the CLI |
The npm keywords that plugins list and plugins search look for, and that the dependencies source matches |
include |
[] |
Plugin packages the CLI ships as its own dependencies |
dependencies |
false |
Load the project’s direct dependencies that carry one of keywords |
config |
true |
Read the plugins key from the CLI’s config |
command |
true |
Add the plugins list / plugins search group |
See Choosing a keywords namespace for
why <cli-name>-plugin is a better keyword than the default.
Plugin sources
Section titled “Plugin sources”Plugins load from these sources, in this order:
include: packages the CLI author lists in code. The user needs no config.dependencies: when turned on, the packages underdependenciesanddevDependenciesin the nearestpackage.jsonthat carry one ofkeywords, inpackage.jsonorder. Packages installed by other packages are never checked. The option is off by default, because a dependency that adds the keyword in a later version would start adding commands without the user doing anything.config: thepluginsarray in the CLI’s config file, as inclibuilder11. An entry that starts with!excludes that package from the other sources.
{ "plugins": ["my-cli-plugin-git", "!my-cli-plugin-deploy"]}A package named by more than one source loads once, at its first position. A package that fails to
import, or exports no activate, is skipped with a warning.
find-installed-packages and search-packages are imported only when plugins list or
plugins search runs, so a CLI that does not run them does not load them.
The commands
Section titled “The commands”With output: 'axi', the default, each command returns its answer, in the global --format:
| Command | Answer |
|---|---|
plugins |
The installed plugins, followed by the group’s own commands. A host that adds a sub-command to the group gets it listed too |
plugins list (ls) |
The installed plugins |
plugins search |
The packages on npm that carry one of keywords. --fields keywords adds the keywords each one matched |
$ my-cli pluginsplugins[1]: my-cli-plugin-gitcommands[2]: list,searchA list longer than 20 entries is cut to the first 20, with count: 20 of 47 total and a next step
that adds --full, which shows them all. No plugins, or no packages found, is an empty state that
names the keywords. Each next step is a complete command line, offered only when the command it
names is in the group. A registry search that fails is an error with code registry-search-failed,
exit code 1.
With output: 'console', the commands log their report through the CLI’s ui as in 0.2.0, and a
bare plugins prints its help and exits with code 2.
Exports
Section titled “Exports”| Export | Purpose |
|---|---|
plugins(options?) |
The extension for cli().use() |
pluginsCommand({ keywords }) |
The plugins command group alone, for a CLI that loads plugins its own way |
loadPlugins({ cwd, ui, registry, host }, names) |
Imports and activates the named plugins, and returns the commands they add |
PluginActivationContext |
The type of the context a plugin’s activate receives. clibuilder 12 keeps a deprecated copy and 13 removes it |
pluginsCommand adds the commands without loading anything:
import { cli } from 'clibuilder'import { pluginsCommand } from '@clibuilder/plugins'
cli({ name: 'my-cli', version: '1.0.0' }) .command(pluginsCommand({ keywords: ['my-cli-plugin'] })) .parse(process.argv)Writing a plugin
Section titled “Writing a plugin”A plugin exports activate(context). The context has the same shape as in clibuilder 11, so an
existing plugin loads unchanged. Import the type from @clibuilder/plugins:
import type { PluginActivationContext } from '@clibuilder/plugins'
export function activate({ addCommand }: PluginActivationContext) { addCommand({ name: 'miku', description: 'vocaloid commands', run() {} })}The Plugins guide covers the rest: the registry, publishing, naming and testing.
Moving from the plugin support in clibuilder 11
Section titled “Moving from the plugin support in clibuilder 11”In clibuilder 11, setting config or keywords loads the plugins named in config and adds the
plugins group. That keeps working through every 11.x release.
When a CLI calls .use(plugins()), clibuilder 11.5.0 and later skip their own config-driven
plugin loading and their own plugins group, so nothing loads twice. That is clibuilder 11
behavior, and it applies to @clibuilder/plugins 0.1.0. You can switch a CLI before v12:
cli({ name: 'my-cli', version: '1.0.0', config: true, keywords: ['my-cli-plugin'] })cli({ name: 'my-cli', version: '1.0.0', config: true }) .use(plugins({ keywords: ['my-cli-plugin'] }))Since 12.0.0, clibuilder removes its own plugin loading, the keywords option,
this.keywords and the automatic plugins group, so there is nothing to skip. plugins() is the
only way to load plugins, and it adds the plugins group unless you pass command: false. If the
config lists plugins and the CLI loads none, clibuilder 12 warns once and names this package. The
plugin registry (defineKey, defineCollectionKey and this.registry) stays in core. See the
v12 roadmap and
section 9 of the slim-dependencies RFC.
Known gaps
Section titled “Known gaps”Under Yarn PnP, the dependencies source cannot read a dependency’s package.json. plugins list
works under Yarn PnP (#575).