Extending Featurevisor
Plugins
A plugin adds a command to the Featurevisor CLI. The built in commands use the same API, so a plugin can read every definition, whatever format or datasource the project uses.
Write a plugin#
A plugin is an object with a command, a handler and examples. Plugins suit reports your team keeps asking for. This one lists every rule that is partly rolled out, which a weekly release review could start from:
module.exports = { command: 'rollouts', description: 'List rules that are partly rolled out', options: { below: { type: 'number', description: 'Only list rules below this percentage', }, }, handler: async function ({ datasource, parsed }) { const below = parsed.below ?? 100 for (const key of await datasource.listFeatures()) { const feature = await datasource.readFeature(key) const lists = Array.isArray(feature.rules) ? { '': feature.rules } : feature.rules for (const [environment, rules] of Object.entries(lists)) { for (const rule of rules) { if (rule.percentage > 0 && rule.percentage < below) { const where = environment ? ` (${environment})` : '' console.log(`${key}${where}: ${rule.key} at ${rule.percentage}%`) } } } } }, examples: [ { command: 'rollouts', description: 'list every partial rollout', }, { command: 'rollouts --below=50', description: 'list rollouts under 50%', }, ],}Register it in the configuration:
module.exports = { tags: ['web', 'mobile'], plugins: [require('./plugins/rollouts')],}Run it like any other command:
npx featurevisor rolloutscheckout: everyone at 25%description appears next to the command in npx featurevisor --help. Without it, the first example's description is used.
Options#
Declare options and the CLI checks every option before your handler runs, as it does for built in commands. A typo stops the command with a suggestion:
Unknown argument: belo. Did you mean "--below"?Each option has a type: string, number, boolean or array, the last for an option given more than once. It can also have an alias, a list of choices, a description for the help, demandOption: true when it is required, and hidden: true to leave it out of the help.
A plugin without options accepts any argument, so a typo reaches the handler unchecked. Declare them in any plugin others will use.
Handler#
The handler receives:
| Property | Contains |
|---|---|
rootDirectoryPath | The project's root directory |
projectConfig | The resolved configuration, with every default filled in |
parsed | The parsed command line arguments, such as { below: 50 } |
datasource | Reads and writes definitions, described below |
cliVersion | The CLI's version |
Return false to make the command exit with an error.
Reading and writing definitions#
Use datasource rather than the file system. It follows the project's parser, directories and adapter, so the plugin works unchanged in a JSON or TOML project. Every kind of definition has the same methods:
const keys = await datasource.listFeatures()const exists = await datasource.featureExists('checkout')const checkout = await datasource.readFeature('checkout')await datasource.writeFeature('checkout', checkout)await datasource.deleteFeature('checkout')Segments, attributes, global variables, schemas, targets and scenarios have the same five, and tests have all but an exists check. There are also methods for the revision and state, built datafiles and Git history. See Datasource.
In a project with sets, choose a set first:
const storefront = datasource.forSet('storefront')const keys = await storefront.listFeatures()TypeScript#
The Plugin type from @featurevisor/core checks a plugin's shape:
import type { Plugin } from '@featurevisor/core'const rollouts: Plugin = { command: 'rollouts', handler: async function ({ datasource, parsed }) { // ... }, examples: [],}export default rolloutsShare a plugin#
Publish a plugin to npm to use it in several projects. Export a function that takes options and returns the plugin, so each project can configure it:
module.exports = function rollouts({ below = 100 } = {}) { return { command: 'rollouts', handler: async function ({ datasource, parsed }) { const threshold = parsed.below ?? below // ... }, examples: [], }}module.exports = { plugins: [require('featurevisor-plugin-rollouts')({ below: 50 })],}Command names must be unique. Registering a command that a built in command or another plugin already has fails at once, so choose a name unlikely to clash with commands Featurevisor may add later.

