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

@bleed-believer/commander

v2.1.0

Published

A lightweight, TypeScript-first library for building type-safe CLI applications. Define your commands with a template string and get fully-inferred types for positional arguments and flags — no decorators, no reflection, no runtime magic.

Readme

@bleed-believer/commander

A lightweight, TypeScript-first library for building type-safe CLI applications. Define your commands with a template string and get fully-inferred types for positional arguments and flags — no decorators, no reflection, no runtime magic.

Installation

npm install @bleed-believer/commander

Core concepts

The library has three layers, each building on the one below:

| Class | Role | |---|---| | Argv | Low-level parser: tokenizes process.argv against a typed schema. | | Command | Wraps Argv with a lifecycle handler (onInit / onDestroy). | | CommandRouter | Groups commands under an optional path prefix; supports nesting. | | Commander | Top-level dispatcher: tries targets in order, re-throws errors. |


Argv — argument parsing

Argv parses raw process.argv against a positionals template and a flags schema. Types are inferred from both at compile time.

import { Argv } from '@bleed-believer/commander';

const argv = new Argv({
    positionals: 'install(i) :packages*',
    flags: {
        saveDev: { type: 'boolean', short: 'D' },
        config:  { type: 'string',  short: 'c' }
    }
});

const result = argv.parse();
// result.positionals.packages  →  string[]
// result.flags.saveDev         →  boolean | undefined
// result.flags.config          →  string  | undefined
// result.tail                  →  string[]

Positionals template syntax

The positionals string is a space-separated list of tokens, matched left-to-right against the arguments after node and the script path.

| Token | Meaning | TypeScript type | |---|---|---| | literal | Must match exactly (e.g. run) | — (no capture) | | name(alias) | Matches the name or any alias (e.g. install(i)) | — (no capture) | | name(a,b,c) | Matches the name or multiple aliases | — | | :name | Required capture | string | | :name? | Optional capture | string \| undefined | | :name* | Variadic, zero-or-more; consumes all remaining positionals | string[] | | :name+ | Variadic, one-or-more; throws if empty | [string, ...string[]] |

Notes:

  • A variadic token (:name* / :name+) must be the last token in the template — it consumes every remaining positional. A template with a token after a variadic throws at construction time.
  • An empty template (positionals: '') captures nothing and matches an invocation with no positionals — use it for a flags-only or default command.

Examples:

// Simple required capture
new Argv({ positionals: 'run :target' })
// argv: ['node', 'script', 'run', './src/index.ts']
// → positionals: { target: './src/index.ts' }

// Optional capture (may be absent)
new Argv({ positionals: 'run :target?' })
// argv: ['node', 'script', 'run']
// → positionals: { target: undefined }

// Variadic zero-or-more
new Argv({ positionals: 'install :packages*' })
// argv: ['node', 'script', 'install', 'a', 'b']
// → positionals: { packages: ['a', 'b'] }

// Alias: matches 'install' OR 'i'
new Argv({ positionals: 'install(i) :packages*' })
// argv: ['node', 'script', 'i', 'typescript']
// → positionals: { packages: ['typescript'] }

Flag options

Flags are defined as a record of camelCase names to FlagOptions:

interface FlagOptions {
    type:      'string' | 'number' | 'boolean';
    short?:    string;   // single-character shorthand (e.g. 'D' → -D)
    array?:    boolean;  // accept multiple values for the same flag
    required?: boolean;  // narrows the inferred type to non-undefined
}

Flag names are automatically converted to --kebab-case on the CLI:

| camelCase name | CLI flag | |---|---| | saveDev | --save-dev | | outputDir | --output-dir |

Value rules:

  • Boolean flags never consume the next token as their value — --save foo with save declared as boolean leaves foo as a positional.
  • A lone - is treated as a positional (the usual stdin convention), not a flag.
  • String flags accept an explicit empty value (--name= or --name '') as the empty string ''. Number flags reject an empty value as a missing value. If a command needs a non-empty string, validate it in your handler.
  • A non-array value flag passed more than once throws FlagParseError; declare array: true to accept multiple values.

Flag input formats

# Long form with space
node script install --output-dir ./dist

# Long form with equals
node script install --output-dir=./dist

# Short form
node script install -o ./dist

# Boolean flag (no value needed)
node script install --save-dev

-- tail separator

Everything after -- is collected into result.tail and not parsed as flags:

node script run ./src/index.ts -- serve --port 8080
#                                 ↑ tail: ['serve', '--port', '8080']

Flag type inference

const argv = new Argv({
    positionals: 'build',
    flags: {
        watch:     { type: 'boolean', short: 'w' },
        port:      { type: 'number',  short: 'p', required: true },
        outDir:    { type: 'string' },
        include:   { type: 'string',  array: true }
    }
});

const { flags } = argv.parse();
flags.watch    // boolean | undefined
flags.port     // number              (required → non-optional)
flags.outDir   // string  | undefined
flags.include  // string[] | undefined

Errors thrown by Argv.parse()

| Error class | When thrown | |---|---| | StaticMismatchError | A literal or alias token in the template did not match the input. | | PositionalMismatchError | A required positional (:name or :name+) was not provided. | | FlagParseError | A flag value was invalid: a required flag was missing, a value flag was given without a value or more than once, or a number flag received a non-numeric/empty value. |


Command — lifecycle-based handler

Command wraps an Argv schema with a factory callback that returns a handler object (CommandTarget). Call run() to attempt a match and execute the handler.

import { Command } from '@bleed-believer/commander';
import type { CommandTarget } from '@bleed-believer/commander';

const installCommand = new Command({
    description: 'Install packages',
    positionals: 'install(i) :packages*',
    flags: {
        saveDev: { type: 'boolean', short: 'D', description: 'Install as dev dependency' }
    },
    callback: context => new class implements CommandTarget {
        async onInit() {
            const { packages } = context.positionals;
            const dev = context.flags.saveDev ?? false;
            console.log(`Installing ${packages.join(', ')} (dev=${dev})`);
        }
    }
});

const result = await installCommand.run();

CommandTarget interface

interface CommandTarget {
    onInit():     void | Promise<void>;
    onDestroy?(): void | Promise<void>;
}

onInit is called first; onDestroy (optional) runs afterwards. onDestroy is guaranteed to run even when onInit throws, so a partially-initialized handler can still release file handles, DB connections, or other resources.

CommandResult return value

run() never throws. It returns a CommandResult object:

interface CommandResult {
    matches: boolean;
    error?:  Error;
}

| Result | Meaning | |---|---| | { matches: false } | Static literal tokens didn't match — this is not the right command. | | { matches: false, error } | Literals matched but a required positional was missing. | | { matches: true, error } | Positionals matched but a flag had an invalid value or the handler threw. | | { matches: true } | Command matched and handler completed without error. |

Inline object handler

Instead of implements CommandTarget, you can return a plain object:

const helpCommand = new Command({
    positionals: 'help(h)',
    callback: _ => ({
        onInit() {
            console.log('Usage: mycli <command> [options]');
        }
    })
});

Testing a command in isolation

Pass a custom argv array to run() to avoid touching process.argv:

const result = await installCommand.run({
    argv: ['node', 'script', 'install', '--save-dev', 'typescript']
});

CommandRouter — prefix-based routing

CommandRouter groups commands under an optional path prefix and tries them in declaration order. Routers can be nested to any depth.

import { CommandRouter } from '@bleed-believer/commander';

const router = new CommandRouter({
    path: 'pkg',
    targets: [installCommand, uninstallCommand]
});

Path prefix

When path is set, those tokens are checked against the start of argv (after node and the script). If they match, the tokens are stripped before the targets are tried. If they don't match, { matches: false } is returned immediately.

  • Path tokens support the same name(alias) alias syntax as positionals.
  • Multi-segment paths are space-separated: 'pkg scripts'.
// Matches: node script pkg install react
// Strips 'pkg', then tries installCommand with: node script install react
new CommandRouter({
    path: 'pkg',
    targets: [installCommand]
})

// Matches: node script pkg scripts run build
// Strips 'pkg scripts', then tries runCommand with: node script run build
new CommandRouter({
    path: 'pkg scripts',
    targets: [runCommand]
})

// Path alias: matches 'pkg' OR 'p'
new CommandRouter({
    path: 'pkg(p)',
    targets: [installCommand]
})

Nested routers

Both Command and CommandRouter satisfy the CommandRouterTarget interface, so they can be freely mixed in a targets list:

const root = new CommandRouter({
    targets: [
        new CommandRouter({
            path: 'pkg',
            targets: [
                new Command({ positionals: 'install(i) :packages*', callback: ... }),
                new Command({ positionals: 'uninstall(u) :packages*', callback: ... })
            ]
        }),
        new Command({ positionals: 'help(h)', callback: ... })
    ]
});

await root.run();

Commander — top-level dispatcher

Commander is the entry point for a CLI application. It iterates targets in order and re-throws any error from the result.

import { Commander, Command, CommandRouter } from '@bleed-believer/commander';

const commander = new Commander([
    new CommandRouter({
        path: 'pkg',
        targets: [installCommand, uninstallCommand]
    }),
    helpCommand,
    versionCommand
]);

await commander.run();

Unlike CommandRouter.run(), Commander.run() throws when a target returns an error. The intent is to surface unhandled errors at the top of the call stack so the process can exit with a non-zero code.

Testing with Commander

Inject a custom process to control argv without patching globals:

const commander = new Commander([myCommand], {
    process: { argv: ['node', 'script', 'install', 'react'] }
});

await commander.run();

Complete example

import { Commander, Command, CommandRouter } from '@bleed-believer/commander';
import type { CommandTarget } from '@bleed-believer/commander';

// --- commands ---

const installCommand = new Command({
    description: 'Install one or more packages',
    positionals: 'install(i) :packages+',
    flags: {
        saveDev:  { type: 'boolean', short: 'D', description: 'Install as dev dependency' },
        registry: { type: 'string',  short: 'r', description: 'Custom npm registry URL' }
    },
    callback: context => new class implements CommandTarget {
        async onInit() {
            const { packages } = context.positionals;
            const { saveDev, registry } = context.flags;
            console.log(`Installing: ${packages.join(' ')}`);
            if (saveDev)   console.log('  → saved as devDependency');
            if (registry)  console.log(`  → using registry: ${registry}`);
        }
    }
});

const uninstallCommand = new Command({
    description: 'Remove one or more packages',
    positionals: 'uninstall(u,remove,rm) :packages+',
    callback: context => ({
        async onInit() {
            console.log(`Removing: ${context.positionals.packages.join(' ')}`);
        }
    })
});

const runCommand = new Command({
    description: 'Run a named script',
    positionals: 'run :script :args*',
    callback: context => ({
        onInit() {
            console.log(`Running script: ${context.positionals.script}`);
        }
    })
});

const helpCommand = new Command({
    description: 'Show help',
    positionals: 'help(h)',
    callback: _ => ({
        onInit() {
            console.log('Usage: mycli <group> <command> [options]');
        }
    })
});

// --- router tree ---

const pkgRouter = new CommandRouter({
    path: 'pkg(p)',
    targets: [installCommand, uninstallCommand]
});

const scriptsRouter = new CommandRouter({
    path: 'pkg scripts',
    targets: [runCommand]
});

// --- entry point ---

const commander = new Commander([
    scriptsRouter,  // more-specific prefix first
    pkgRouter,
    helpCommand
]);

try {
    await commander.run();
} catch (err) {
    console.error(err instanceof Error ? err.message : err);
    process.exit(1);
}

Usage:

node mycli.js pkg install react vue --save-dev
node mycli.js p i typescript -D
node mycli.js pkg uninstall lodash
node mycli.js pkg scripts run build
node mycli.js help

API reference

Argv<P, F>

| Member | Signature | Description | |---|---|---| | constructor | (options: { positionals: P; flags?: F }) | Define the schema. | | parse | (processLike?: { argv: string[] }) → ParsedArgv<P, F> | Parse and return typed result. Throws on mismatch. |

Command<P, F>

| Member | Signature | Description | |---|---|---| | constructor | (options: CommandOptions<P, F>) | Define the command schema and handler factory. | | description | string \| undefined | Human-readable summary (read-only). | | positionals | P | Raw positionals template string (read-only). | | run | (processLike?: { argv: string[] }) → Promise<CommandResult> | Match and execute; never throws. |

CommandRouter

| Member | Signature | Description | |---|---|---| | constructor | (options: CommandRouterOptions) | Provide optional path and ordered targets. | | run | (processLike?: { argv: string[] }) → Promise<CommandResult> | Match and dispatch; never throws. |

Commander

| Member | Signature | Description | |---|---|---| | constructor | (targets: CommanderTarget[], inject?: CommanderInject) | Ordered list of commands/routers. | | run | () → Promise<void> | Dispatch to targets; re-throws errors from results. |

Error classes

| Class | Thrown when | |---|---| | StaticMismatchError | A literal or alias token did not match. Signals "wrong command". | | PositionalMismatchError | A required positional (:name or :name+) was absent. | | FlagParseError | A flag value was invalid: required flag missing, value flag without a value or repeated for a non-array flag, or a number flag given a non-numeric/empty value. |


License

MIT