@pastweb/plugable
v3.0.0
Published
utility library for plugin manager creation
Maintainers
Readme
@pastweb/plugable
A platform-agnostic, TypeScript-first utility for building plugin managers, hook runners, and plugin-driven command systems.
plugable is inspired by the Vite plugin model, but it stays framework-neutral and small enough to use in CLIs, build tools, browser workflows, server workflows, or any system that needs public plugin contracts.
Features
- Plugin manager primitives — Load plugins, group hooks by order, and run them consistently.
- Hook execution modes — Supports sequential, parallel, and waterfall hooks.
- Command registration — Plugins can expose command factories for CLI-like tools.
- Command lifecycle clarity — Command input is parsed once, passed to plugin command factories, and then used by
runCommandto select the command to execute. - Ordering controls — Use
enforce: 'pre' | 'post'to control plugin hook order. - Command-scoped hooks — Use
applyto run plugin hooks only for matching command input. - Platform-neutral — No dependency on Node-specific APIs in the plugin contract.
- TypeScript-first — Public types are exported for plugins, hooks, commands, command input, and plugin order.
Installation
npm i -S @pastweb/plugable
# or
pnpm i -S @pastweb/plugable
# or
yarn add -S @pastweb/plugableDocumentation Overview
The documentation is organized around the main pieces needed to build a plugin manager.
- Basic usage — Parse command input, load plugins, run a command, and then run hooks.
- Command lifecycle — Understand the two-step command flow: plugin command factory first, command function later.
- Hooks — Define hook behavior and choose sequential, parallel, or waterfall execution.
- Plugins — Author plugin objects with hook functions,
apply,enforce, and optional command factories. - Commands — Parse command input and register command definitions from plugins.
- API reference — Function signatures, parameters, returns, and examples for each exported primitive.
This project is distributed under the MIT license.
Import note: command and hook helpers are intentionally not exported from the package root. Import command helpers from @pastweb/plugable/command and hook helpers from @pastweb/plugable/hooks.
Summary
- Basic Usage
- Complete CLI Example
- Command Lifecycle
- When To Use Plugable
- Hooks
- Plugins
- Commands
- Printing Helpers
- License
Basic Usage
Use getCommandsInput to parse CLI input, loadPlugins to collect commands and hooks, runCommand to execute the selected command, and runPluginsHooks to run plugin hooks when the command allows it.
import { argv } from 'node:process';
import { loadPlugins } from '@pastweb/plugable';
import { getCommandsInput, runCommand } from '@pastweb/plugable/command';
import { createHooks, runPluginsHooks } from '@pastweb/plugable/hooks';
const commandsInput = getCommandsInput(argv.slice(2));
const hooks = createHooks({
buildStart: {
type: 'sequential',
},
});
const { commands, pluginsOrder } = await loadPlugins([
// plugins
], { hooks, commandsInput });
if (await runCommand(commands, commandInput)) {
await runPluginsHooks(hooks, pluginsOrder);
}When command support is not needed, skip runCommand and run hooks directly.
import { loadPlugins } from '@pastweb/plugable';
import { createHooks, runPluginsHooks } from '@pastweb/plugable/hooks';
const hooks = createHooks({
setup: {
type: 'parallel',
},
});
const { pluginsOrder } = await loadPlugins([
// plugins
], { hooks });
await runPluginsHooks(hooks, pluginsOrder);Complete CLI Example
This example shows the recommended command authoring flow:
- Define commands with
createCommand. - Expose them from a plugin with
createCommands(commandsInput, definitions). - Parse argv with
getCommandsInput. - Load plugins with
loadPlugins. - Run the selected command with
runCommand. - Run hooks only when the command allows it.
import { argv } from 'node:process';
import { CommandError, createCommand, createCommands, getCommandsInput, option, runCommand } from '@pastweb/plugable/command';
import { loadPlugins } from '@pastweb/plugable';
import { createHooks, runPluginsHooks } from '@pastweb/plugable/hooks';
const buildCommand = createCommand({
name: 'build',
description: 'Build the project.',
aliases: ['b'],
examples: ['pastweb build --config=./config.js --watch'],
options: {
config: option.string({
alias: '-c',
default: './config.js',
description: 'Path to the config file.',
}),
watch: option.boolean({
description: 'Keep the build running.',
}),
},
run(context) {
const { options } = context;
const { config, watch } = options;
// run the build using config and watch
return true;
},
});
const buildPlugin = {
name: 'build-plugin',
commands: (commandsInput, ...args) => createCommands(commandsInput, [ buildCommand ], { args }),
buildStart() {
// run before or after the command, depending on your CLI flow
},
};
const commandsInput = getCommandsInput(argv.slice(2));
const hooks = createHooks({
buildStart: {
type: 'sequential',
},
});
try {
const { commands, pluginsOrder } = await loadPlugins([ buildPlugin ], { hooks, commandsInput });
if (await runCommand(commands, commandInput)) {
await runPluginsHooks(hooks, pluginsOrder);
}
} catch (error) {
if (error instanceof CommandError) {
console.error(error.message);
process.exitCode = 1;
} else {
throw error;
}
}Command Lifecycle
Commands use a two-step lifecycle:
loadPluginscalls each plugincommands(commandsInput, ...args)factory.runCommandexecutes the selected command function returned by that factory.
This means commandsInput is available while commands are registered, not as a runtime argument passed to the returned command function. If a command needs parsed options, read them in the plugin commands factory and close over them in the returned command function.
const plugin = {
name: 'example',
commands(commandsInput) {
const buildOptions = commandsInput.build || {};
return {
build: {
command() {
if (buildOptions.watch) {
// run build in watch mode
}
// run build
},
},
};
},
};The command factory receives the complete parsed input, so a plugin can inspect the selected command and any sibling command segments before it returns command definitions.
Command input is semantic and segment-based. Every non-option token starts a new command segment, and options belong to the nearest command segment before them.
pastweb build --opt1 website --opt2const commandsInput = getCommandsInput('build --opt1 website --opt2');
// {
// build: {
// opt1: true,
// },
// website: {
// opt2: true,
// },
// }In this model, website is not a positional argument of build. It is a
semantic command segment with its own options. If a future helper adds
positional arguments, it should do that through command metadata without
changing this raw parser behavior.
const plugin = {
name: 'build-plugin',
commands(commandsInput) {
const isBuild = Boolean(commandsInput.build);
const buildOptions = commandsInput.build || {};
return {
build: {
command() {
if (!isBuild) return false;
if (buildOptions.config) {
// use buildOptions.config
}
},
},
};
},
};Lifecycle steps:
| Step | Function | Purpose |
|------|----------|---------|
| 1 | getCommandsInput(argv) | Parses CLI input into a CommandsInput object. |
| 2 | loadPlugins(plugins, { hooks, commandsInput }) | Registers plugin commands and groups plugin hooks. |
| 3 | commands(commandsInput, ...args) | Lets each plugin inspect parsed input and return command definitions. |
| 4 | runCommand(commands, commandsInput) | Selects the first parsed command and runs its command() function. |
| 5 | runPluginsHooks(hooks, pluginsOrder) | Runs plugin hooks when the command returns true or undefined. |
runCommand uses commandsInput only to find the command name to execute. It does not pass commandsInput to command().
type CommandFactory = (commandsInput: CommandsInput, ...args: any[]) => Commands;
type CommandFunction = () => boolean | void | Promise<boolean | void>;const commands = {
build: {
command() {
// Receives no arguments.
return true;
},
},
};
await runCommand(commands, getCommandsInput('build --watch'));Returning false from a command function tells the caller to stop before running plugin hooks.
if (await runCommand(commands, commandsInput)) {
await runPluginsHooks(hooks, pluginsOrder);
}Command registration and plugin hook filtering are separate. The plugin commands factory registers commands; the plugin apply property filters plugin hooks for the current command input.
const plugin = {
name: 'docs-plugin',
apply: 'build',
commands() {
return {
docs: {
command() {
// This command is still registered even when the current input is not "build".
},
},
};
},
buildStart() {
// This hook only runs when apply matches the current command input.
},
};When To Use Plugable
Use plugable directly when you are building a plugin contract, hook runner, build workflow, or CLI-like tool where commands are only one part of a larger extension system.
Use a full CLI framework or an adapter around one when you need batteries-included CLI features such as generated shell completion scripts, interactive prompts, deeply nested positional parsing, terminal UI, automatic command discovery, or mature help formatting out of the box.
plugable stays small on purpose: it gives you plugin loading, hook ordering, command registration, typed command helpers, and parser primitives without owning your whole CLI architecture.
Hooks
Hooks describe named extension points that plugins can implement.
| Type | Execution | Behavior |
|------|-----------|----------|
| sequential | sync or async | Runs each plugin hook in order with the configured args. |
| parallel | async | Runs all plugin hooks at the same time with the configured args. |
| waterfall | sync or async | Runs hooks in order and passes each return value into the next hook. |
createHooks
Normalizes hook definitions and fills default callbacks.
Syntax
function createHooks(hooks: Hooks): Hooks;Parameters
hooks:Hooks- Object keyed by hook name.
- Each value can define
type,args,callback, andfinal. - The private names
name,apply, andenforcecannot be used as hook names.
Returns
Hooks- The normalized hook definition object.
Example
import { createHooks } from '@pastweb/plugable/hooks';
const hooks = createHooks({
buildStart: {
type: 'sequential',
args: ['app'],
},
resolveConfig: {
type: 'waterfall',
args: () => ({ root: process.cwd() }),
},
});Hook args
argscan be a single value, an array of values, or a function that returns either shape.- Array values are spread into hook functions.
- Args functions are evaluated before the hook function runs.
- Waterfall hooks receive only the first normalized arg as the initial value.
The special commands hook config can pass extra args to plugin command factories.
const hooks = createHooks({
commands: {
args: 'sharedArg',
pluginA: ['pluginArg'],
pluginB: () => ['lazyPluginArg'],
},
});In this example:
- every plugin
commandsfactory receivescommands(commandsInput, 'sharedArg') pluginAreceivescommands(commandsInput, 'sharedArg', 'pluginArg')pluginBreceivescommands(commandsInput, 'sharedArg', 'lazyPluginArg')
runPluginsHooks
Runs grouped plugin hooks by order: pre, default, then post.
Syntax
async function runPluginsHooks(
hooks: Hooks,
pluginOrder: PluginsOrder,
includeHook?: string | string[] | null,
skipOrder?: string | string[] | null,
): Promise<void>;Parameters
hooks:Hooks- Hook definitions returned by
createHooks.
- Hook definitions returned by
pluginOrder:PluginsOrder- Grouped plugin hooks returned by
loadPlugins.
- Grouped plugin hooks returned by
includeHook:string | string[] | null(optional)- Run only specific hook names. Runs all hooks when omitted.
skipOrder:string | string[] | null(optional)- Skip one or more plugin order groups:
pre,default, orpost.
- Skip one or more plugin order groups:
Returns
Promise<void>
Example
await runPluginsHooks(hooks, pluginsOrder);
await runPluginsHooks(hooks, pluginsOrder, ['buildStart']);
await runPluginsHooks(hooks, pluginsOrder, null, ['post']);Plugins
Plugins are plain objects with a required name and optional hook functions, apply, enforce, and commands.
Plugin Object
type Plugin = {
name: string;
apply?: string | string[] | ((commandsInput: CommandsInput) => boolean);
commands?: (commandsInput: CommandsInput, ...args: any[]) => Commands;
enforce?: 'pre' | 'post';
[hookName: string]: unknown;
};Properties
name:string- Unique plugin name.
apply:string | string[] | ((commandsInput: CommandsInput) => boolean)(optional)- Filters plugin hooks for specific command input.
- String values can use
/for nested command matching, such asbuild/website.
commands:(commandsInput: CommandsInput, ...args: any[]) => Commands(optional)- Registers commands during
loadPlugins.
- Registers commands during
enforce:'pre' | 'post'(optional)- Places hook functions before or after default-order plugins.
Example
const plugin = {
name: 'docs',
apply: 'build/docs',
enforce: 'post',
commands(commandsInput) {
const docsOptions = commandsInput.docs || {};
return {
docs: {
command() {
if (docsOptions.help) return false;
},
},
};
},
buildStart(root) {
// plugin hook
},
};Notes
applyfilters plugin hooks, not command registration.commandsregistration is independent fromenforce.- If two plugins register the same command name, the later duplicate is registered as
<pluginName>:<commandName>. - Multi-stage plugins can be represented as an array of plugin objects.
Authoring Plugin Commands
Prefer createCommand and createCommands when a plugin exposes commands.
createCommand defines a reusable command description. createCommands belongs
inside the plugin commands(commandsInput, ...args) factory and converts command
definitions into the raw Commands object expected by loadPlugins.
Single-command plugin
import { createCommand, createCommands, option } from '@pastweb/plugable/command';
const buildCommand = createCommand({
name: 'build',
description: 'Build the project.',
options: {
config: option.string({
alias: '-c',
default: './config.js',
description: 'Path to the config file.',
}),
watch: option.boolean({
description: 'Keep the build running.',
}),
},
run(context) {
const { options } = context;
options.config;
options.watch;
},
});
export const buildPlugin = {
name: 'build-plugin',
commands(commandsInput) {
return createCommands(commandsInput, [
buildCommand,
]);
},
};Multi-command plugin
const previewCommand = createCommand({
name: 'preview',
description: 'Preview the project.',
options: {
port: option.number({
default: 3000,
description: 'Preview server port.',
}),
},
run(context) {
const { options } = context;
options.port;
},
});
export const projectPlugin = {
name: 'project-plugin',
commands(commandsInput) {
return createCommands(commandsInput, [
buildCommand,
previewCommand,
]);
},
};Plugin-specific injected command args
const hooks = createHooks({
commands: {
args: ['workspace'],
'build-plugin': ['build-cache'],
},
});
export const buildPlugin = {
name: 'build-plugin',
commands(commandsInput, ...args) {
return createCommands(commandsInput, [
buildCommand,
], {
args,
});
},
};Inside run(context), injected values are available as context.args.
const buildCommand = createCommand({
name: 'build',
run(context) {
const { args } = context;
const [workspace, cache] = args;
},
});Migration from raw command objects
// Before
export const plugin = {
name: 'build-plugin',
commands(commandsInput) {
const buildOptions = commandsInput.build || {};
return {
build: {
help: {
commandName: 'build',
summary: 'Build the project.',
},
command() {
if (buildOptions.watch) {
// run in watch mode
}
},
},
};
},
};
// After
const buildCommand = createCommand({
name: 'build',
description: 'Build the project.',
options: {
watch: option.boolean(),
},
run(context) {
const { options } = context;
if (options.watch) {
// run in watch mode
}
},
});
export const plugin = {
name: 'build-plugin',
commands(commandsInput) {
return createCommands(commandsInput, [
buildCommand,
]);
},
};Raw command objects remain supported. Use them when you need full manual control,
and use createCommand when you want typed options, generated help metadata, and
validation.
Command registration currently receives commandsInput plus loose injected args.
That keeps the plugin contract stable and platform-neutral. A richer plugin
registration context can be considered later, but it should be additive rather
than replacing the existing factory signature.
loadPlugins
Loads plugin commands and groups plugin hooks by order.
Syntax
async function loadPlugins<T extends Plugin>(
plugins: (T | T[])[],
options?: {
hooks?: Hooks;
commandsInput?: CommandsInput;
includePluginCommands?: string | string[];
},
): Promise<{
commands: Commands;
pluginsOrder: PluginsOrder;
}>;Parameters
plugins:(T | T[])[]- Mandatory plugin objects or arrays of plugin objects.
options.hooks:Hooks(optional)- Hook definitions.
options.commandsInput:CommandsInput(optional)- Parsed command input passed to plugin
commandsfactories andapplyfunctions.
- Parsed command input passed to plugin
options.includePluginCommands:string | string[](optional)- Include command factories only from matching plugin names.
Returns
Promise<{ commands: Commands; pluginsOrder: PluginsOrder }>commands: collected command definitions.pluginsOrder: hook functions grouped bypre,default, andpost.
Example
const { commands, pluginsOrder } = await loadPlugins(
[plugin],
{
hooks,
commandsInput: commandInput,
},
);groupPlugins
Groups plugins by enforce order without loading command definitions.
Syntax
function groupPlugins<T extends Plugin>(
plugins: T[],
commandsInput?: CommandsInput,
): PluginGroups<T>;Returns
PluginGroups<T>{ pre: T[]; default: T[]; post: T[] }
applyPlugin
Checks whether a plugin should apply to the current command input.
Syntax
function applyPlugin(
apply: string | string[] | undefined | ((commandsInput: CommandsInput) => boolean),
commandsInput: CommandsInput,
): boolean;Example
applyPlugin('build/site', {
build: {},
site: {},
}); // trueCommands
Commands are registered by plugin commands factories and executed by runCommand.
The recommended API is createCommand plus createCommands. It keeps command
authoring typed, generates help metadata, validates options, and still returns
the raw Commands object used by the rest of the library.
Use raw command objects only when you need full manual control over command registration and execution.
getCommandsInput
Parses a command-line string or argv array into a semantic CommandsInput object.
Each non-option token creates a command segment, and each option is attached to
the closest previous command segment.
Syntax
function getCommandsInput(argv?: string | string[]): CommandsInput;Parameters
argv:string | string[](optional) (default:[])- Command-line input as a string or token array.
Returns
CommandsInput- Object keyed by command name, with parsed options as values.
Example
myCli build --config=./config.js --env=[ true, mpa, 1 ] website --staticconst commandInput = getCommandsInput(
'build --config=./config.js --env=[ true, mpa, 1 ] website --static',
);
// {
// build: {
// config: './config.js',
// env: [true, 'mpa', 1],
// },
// website: {
// static: true,
// },
// }Option ownership example
const commandInput = getCommandsInput('build --opt1 website --opt2');
// {
// build: {
// opt1: true,
// },
// website: {
// opt2: true,
// },
// }Parsing rules
- Options start with
--or-. - Options without values resolve to
true. - Every non-option token starts a new command segment.
- Options belong to the nearest command segment before them.
- Positional arguments are intentionally not represented by the raw parser.
- Quoted string values can contain spaces.
- Values after
=can be strings, numbers, booleans, arrays, or object-likekey:valuepairs. - Numeric values are parsed only when the complete value is numeric. For example,
123becomes a number, while123abcremains a string. - Values can contain additional
=characters after the first separator. - Arrays and objects are not nestable.
- Unclosed arrays, unclosed objects, and malformed object entries throw parse errors.
- Repeated options are overwritten by the latest value.
- If the first input item is an option, options are stored under the
nonecommand key.
createCommand / createCommands
Defines typed commands and binds them to the raw command lifecycle.
Use createCommand to describe a command and createCommands inside a plugin
commands(commandsInput, ...args) factory to turn those definitions into the raw
Commands object consumed by runCommand.
Syntax
function createCommand<TName extends string, TOptions extends CommandOptionsDefinition>(
definition: CreateCommandDefinition<TName, TOptions>,
): CreatedCommand<TName, TOptions>;
function createCommands(
commandsInput: CommandsInput,
commandDefinitions: AnyCreatedCommand[],
options?: {
args?: unknown[];
allowUnknownOptions?: boolean;
},
): Commands;Example
import { createCommand, createCommands, option } from '@pastweb/plugable/command';
const buildCommand = createCommand({
name: 'build',
description: 'Build the project.',
aliases: ['b'],
examples: ['pastweb build --watch'],
options: {
config: option.string({
alias: '-c',
default: './config.js',
description: 'Path to the config file.',
}),
watch: option.boolean({
description: 'Keep the build running.',
}),
},
run(context) {
const { options } = context;
options.config;
options.watch;
},
});
const plugin = {
name: 'build-plugin',
commands(commandsInput, ...args) {
return createCommands(commandsInput, [
buildCommand,
], {
args,
});
},
};Notes
createCommanddoes not change the rawCommandshape.createCommandsreturns regularCommands, soloadPluginsandrunCommandkeep working as before.run(context)receives a typedCommandContext.aliasesregisters runtime aliases. Ifbuildhas aliasb, thenb --watchruns the same command and reads options from thebcommand segment.- Option defaults are applied to
context.optionswhen the parsed input does not provide a value. option.string,option.number,option.boolean,option.array, andoption.objectcreate typed option definitions.- Option helpers support
required,default,alias,aliases, anddescription. - Option defaults are validated against their declared type.
- Required options and option value types are validated before
run(context)executes. - Unknown options are rejected by default when a command has an explicit option schema.
- Set
allowUnknownOptions: trueincreateCommandsoptions to preserve unknown parsed options. - Validation throws a structured
CommandErrorand does not print, so callers decide how to report errors. - Command help metadata is generated from
name,description,aliases,examples,options, andargs. - Raw command objects are still supported for backward compatibility.
Runtime alias example
const buildCommand = createCommand({
name: 'build',
aliases: ['b'],
options: {
watch: option.boolean(),
},
run(context) {
const { options } = context;
// For `pastweb b --watch`, context.commandName is "b".
// options.watch is true.
},
});
const commandsInput = getCommandsInput('b --watch');
const commands = createCommands(commandsInput, [
buildCommand,
]);
await runCommand(commands, commandsInput);Generated help metadata
const commands = createCommands(getCommandsInput('build --help'), [
buildCommand,
]);
commands.build.help;
// {
// commandName: 'build',
// summary: 'Build the project.',
// aliases: ['b'],
// options: [
// {
// name: '--config',
// aliases: ['-c'],
// description: 'Path to the config file.',
// default: './config.js',
// },
// {
// name: '--watch',
// description: 'Keep the build running.',
// },
// ],
// examples: ['pastweb build --watch'],
// }Printed help output
import { printHelp } from '@pastweb/plugable';
printHelp(commands.build.help); - build
Build the project.
Aliases: b
Options:
--config, -c (default: ./config.js) - Path to the config file.
--watch - Keep the build running.
Examples:
pastweb build --watchValidation example
import { CommandError, createCommands } from '@pastweb/plugable/command';
try {
const commands = createCommands(commandsInput, [
buildCommand,
]);
await runCommand(commands, commandsInput);
} catch (error) {
if (error instanceof CommandError) {
console.error(error.message);
}
}createCommandContext
Builds a typed command registration context from parsed command input.
Use this inside a plugin commands(commandsInput, ...args) factory when a command
helper needs the selected command name, selected options, the full parsed input,
injected command args, and command metadata together.
Syntax
function createCommandContext<TOptions = CommandOptions, TMetadata extends Help = Help>(
commandsInput: CommandsInput,
options?: {
commandName?: string;
args?: unknown[];
metadata?: TMetadata;
},
): CommandContext<TOptions, TMetadata>;Returns
CommandContext<TOptions, TMetadata>commandName: selected semantic command segment.options: options for the selected command segment.commandsInput: full parsed command input.args: values injected through the plugincommandshook.metadata: optional command help metadata.
Example
import { createCommandContext } from '@pastweb/plugable/command';
const plugin = {
name: 'build-plugin',
commands(commandsInput, ...args) {
const context = createCommandContext(commandsInput, {
commandName: 'build',
args,
metadata: {
commandName: 'build',
summary: 'Build the project.',
},
});
return {
build: {
help: context.metadata,
command() {
const { options } = context;
if (options.watch) {
// run in watch mode
}
},
},
};
},
};The raw parser does not represent positional arguments. The args field is used
for values injected by the plugin commands hook today and leaves room for
future helper-level positional metadata without changing parser behavior.
createCli
Runs the common parse, load, command, and hook flow in one optional helper.
Use this when you want a small batteries-included runner. Keep using
getCommandsInput, loadPlugins, runCommand, and runPluginsHooks directly
when your CLI needs custom ordering, error handling, or lifecycle decisions.
Syntax
async function createCli<TPlugin extends Plugin = Plugin>(
options: {
hooks: Hooks;
plugins: (TPlugin | TPlugin[])[];
argv?: string | string[];
includePluginCommands?: string | string[];
runHooks?: boolean;
includeHook?: string | string[] | null;
skipOrder?: string | string[] | null;
},
): Promise<{
commandsInput: CommandsInput;
commands: Commands;
pluginsOrder: PluginsOrder;
commandResult: boolean | void;
}>;Example
import { argv } from 'node:process';
import { createCli } from '@pastweb/plugable/command';
const result = await createCli({
hooks,
plugins: [
buildPlugin,
],
argv: argv.slice(2),
});
if (result.commandResult) {
// Command succeeded and hooks already ran.
}Set runHooks: false when you want createCli to load plugins and execute the
command but leave hook execution to your application.
createCommandManifest
Creates a machine-readable command manifest from createCommand definitions.
Use this optional helper for documentation generation, static docs pages, schema
exports, or tests that need a serializable view of command definitions. The
manifest does not include run functions.
Syntax
function createCommandManifest(
commandDefinitions: AnyCreatedCommand[],
): CommandManifest;Example
import { createCommandManifest } from '@pastweb/plugable/command';
const manifest = createCommandManifest([
buildCommand,
]);
// {
// commands: [
// {
// name: 'build',
// description: 'Build the project.',
// aliases: ['b'],
// options: [
// {
// name: 'watch',
// flag: '--watch',
// type: 'boolean',
// },
// ],
// },
// ],
// }createCompletionMetadata
Creates shell-agnostic completion metadata from createCommand definitions.
This helper intentionally does not generate Bash, Zsh, Fish, or PowerShell scripts. It returns structured metadata that an adapter can turn into the shell format your CLI supports.
Syntax
function createCompletionMetadata(
commandDefinitions: AnyCreatedCommand[],
): CompletionMetadata;Example
import { createCompletionMetadata } from '@pastweb/plugable/command';
const completion = createCompletionMetadata([
buildCommand,
]);
// {
// commands: [
// {
// name: 'build',
// aliases: ['b'],
// options: [
// {
// name: '--watch',
// type: 'boolean',
// },
// ],
// },
// ],
// }runCommand
Runs the first command found in commandsInput.
Syntax
async function runCommand(
commands: Commands,
commandsInput: CommandsInput,
): Promise<boolean | void>;Parameters
commands:Commands- Command definitions returned by
loadPlugins.
- Command definitions returned by
commandsInput:CommandsInput- Parsed command input used to select the first command.
Returns
Promise<boolean | void>truewhen the command returnsundefined.- The command return value when it returns
trueorfalse.
falsewhen no command is found.
Example
const shouldRunHooks = await runCommand(commands, commandInput);
if (shouldRunHooks) {
await runPluginsHooks(hooks, pluginsOrder);
}Advanced Raw Commands
Raw command objects are still supported. They are useful when a plugin wants to own parsing, validation, help metadata, or command execution manually.
const plugin = {
name: 'raw-build-plugin',
commands(commandsInput) {
const buildOptions = commandsInput.build || {};
return {
build: {
help: {
commandName: 'build',
summary: 'Build the project.',
},
command() {
if (buildOptions.watch) {
// run in watch mode
}
return true;
},
},
};
},
};Raw command callbacks receive no arguments. Read parsed input in the plugin
commands(commandsInput, ...args) factory and close over the values you need.
Raw commands do not get createCommand option validation or generated option
help. Prefer createCommand for new plugin commands unless you need this lower
level control.
Printing Helpers
plugable includes small CLI-oriented printing helpers for common command output.
printHelp
Prints command help metadata.
Syntax
function printHelp(
help: Help,
colors?: { param?: string; text?: string } | null,
header?: string,
): void;Example
printHelp({
commandName: 'build <target>',
summary: 'Build a project target.',
aliases: ['b'],
args: [
{
name: '<target>',
description: 'Target to build.',
required: true,
},
],
options: [
{
name: '--config <path>',
aliases: ['-c'],
description: 'Path to the config file.',
default: './config.js',
},
{
name: '--watch',
description: 'Keep the build running.',
},
],
examples: [
{
command: 'pastweb build website --watch',
description: 'Build the website target in watch mode.',
},
],
});Help metadata
commandName: command usage line, such asbuild <target>.summary: short command description.details: longer description as a string or string array.aliases: command aliases.args: documented positional arguments for generated help output.options: documented command options for generated help output.examples: example command usages.description: deprecated legacy description field. Usesummary,details,args,options, andexamplesinstead. It is still rendered for backward compatibility during the metadata migration.
printInfo / printWarning / printError
Prints formatted messages for info, warning, and error output.
Syntax
function printInfo(info: InfoMessage, color?: string | null, header?: string): void;
function printWarning(warning: WarningMessage, color?: string | null, header?: string): void;
function printError(error: ErrorMessage, color?: string | null, header?: string): void;Notes
- Object and array messages are rendered with
prettyjson. printInfodefaults to cyan output.printWarningdefaults to yellow output.printErrordefaults to red output.printErrorexits the process with code1for non-object errors.
License
MIT License (c) 2026 Domenico Pasto
