Skip to content

@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 it with:

Terminal window
npm install @clibuilder/plugins clibuilder

clibuilder ^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.

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.

Plugins load from these sources, in this order:

  1. include: packages the CLI author lists in code. The user needs no config.
  2. dependencies: when turned on, the packages under dependencies and devDependencies in the nearest package.json that carry one of keywords, in package.json order. 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.
  3. config: the plugins array in the CLI’s config file, as in clibuilder 11. 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.

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
Terminal window
$ my-cli plugins
plugins[1]: my-cli-plugin-git
commands[2]: list,search

A 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.

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)

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.

Under Yarn PnP, the dependencies source cannot read a dependency’s package.json. plugins list works under Yarn PnP (#575).