npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pastweb/plugable

v3.0.0

Published

utility library for plugin manager creation

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 runCommand to select the command to execute.
  • Ordering controls — Use enforce: 'pre' | 'post' to control plugin hook order.
  • Command-scoped hooks — Use apply to 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/plugable

Documentation 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

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:

  1. Define commands with createCommand.
  2. Expose them from a plugin with createCommands(commandsInput, definitions).
  3. Parse argv with getCommandsInput.
  4. Load plugins with loadPlugins.
  5. Run the selected command with runCommand.
  6. 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:

  1. loadPlugins calls each plugin commands(commandsInput, ...args) factory.
  2. runCommand executes 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 --opt2
const 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, and final.
    • The private names name, apply, and enforce cannot 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

  • args can 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 commands factory receives commands(commandsInput, 'sharedArg')
  • pluginA receives commands(commandsInput, 'sharedArg', 'pluginArg')
  • pluginB receives commands(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.
  • pluginOrder: PluginsOrder
    • Grouped plugin hooks returned by loadPlugins.
  • 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, or post.

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 as build/website.
  • commands: (commandsInput: CommandsInput, ...args: any[]) => Commands (optional)
    • Registers commands during loadPlugins.
  • 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

  • apply filters plugin hooks, not command registration.
  • commands registration is independent from enforce.
  • 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 commands factories and apply functions.
  • 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 by pre, default, and post.

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: {},
}); // true

Commands

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 --static
const 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-like key:value pairs.
  • Numeric values are parsed only when the complete value is numeric. For example, 123 becomes a number, while 123abc remains 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 none command 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

  • createCommand does not change the raw Command shape.
  • createCommands returns regular Commands, so loadPlugins and runCommand keep working as before.
  • run(context) receives a typed CommandContext.
  • aliases registers runtime aliases. If build has alias b, then b --watch runs the same command and reads options from the b command segment.
  • Option defaults are applied to context.options when the parsed input does not provide a value.
  • option.string, option.number, option.boolean, option.array, and option.object create typed option definitions.
  • Option helpers support required, default, alias, aliases, and description.
  • 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: true in createCommands options to preserve unknown parsed options.
  • Validation throws a structured CommandError and does not print, so callers decide how to report errors.
  • Command help metadata is generated from name, description, aliases, examples, options, and args.
  • 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 --watch

Validation 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 plugin commands hook.
    • 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.
  • commandsInput: CommandsInput
    • Parsed command input used to select the first command.

Returns

  • Promise<boolean | void>
    • true when the command returns undefined.
    • The command return value when it returns true or false.
  • false when 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 as build <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. Use summary, details, args, options, and examples instead. 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.
  • printInfo defaults to cyan output.
  • printWarning defaults to yellow output.
  • printError defaults to red output.
  • printError exits the process with code 1 for non-object errors.

License

MIT License (c) 2026 Domenico Pasto