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

@shieldsbetter/sbopts

v0.1.0

Published

Opinionated ESM command-line parser: --long/-s flags, boolean stacking, repeated-as-array values, inherited subcommand flags, and termiflo-rendered --help.

Readme

sbopts

CI Coverage

Command line option parsing to my particular tastes.

The tastes:

  • Long flags are --foo, short flags are -f.
  • Boolean short flags stack: -abc means -a -b -c.
  • Lists by repeated entries: -f bar -f bazz --> ['bar', 'bazz'].
  • Subcommands are the norm and parent flags are inherited by its children. app -C dir build and app build -C dir mean the same thing.
  • -- ends parsing.
  • --help is generated for you.

Pure ESM, Node ≥ 18.

Install

npm install @shieldsbetter/sbopts
import { command } from '@shieldsbetter/sbopts';

A complete CLI

import { command } from '@shieldsbetter/sbopts';

const cli = command('demo', {
    summary: 'A toy version-control-shaped CLI.',
    description: 'Demonstrates the whole surface of sbopts.',
    flags: {
        dir: {
            short: 'C',
            type: 'string',
            summary: 'Run as if started in <dir>.',
        },
        verbose: { short: 'v', type: 'boolean', summary: 'Print more.' },
    },
    commands: {
        commit: {
            summary: 'Record changes to the repository.',
            flags: {
                message: {
                    short: 'm',
                    type: 'string',
                    summary: 'Commit message.',
                },
                all: { short: 'a', type: 'boolean', summary: 'Stage all.' },
            },
            run: ({ flags, positionals, rest }) => {
                // your handler
            },
        },
    },
});

cli.run(); // parses process.argv, dispatches, prints help / errors, exits
$ demo commit -a -m 'first'        # -m takes a value, so it stands on its own
$ demo commit -av                  # -a and -v are booleans: a stack (-a -v)
$ demo -C /tmp commit -m hi        # -C is demo's flag, deferred past `commit`
$ demo commit --help              # auto-generated, typeset help

Defining commands

command(name, spec) builds a command (usually the root). A command may carry flags, positional-argument metadata, a run handler, and nested commands.

command('app', {
    summary: 'One-liner shown in the parent command list and atop --help.',
    description: 'Longer prose for the help body. Falls back to `summary`.',
    flags: {
        /* … */
    },
    args: [{ name: 'file', required: true, summary: 'Input to read.' }],
    commands: {
        /* name: spec, … */
    },
    run: (ctx) => {
        /* … */
    },
});

args is metadata for the usage line and help only — positionals are always collected and handed to your handler as positionals; sbopts doesn't bind them to names. A leaf command (no subcommands) collects bare words as positionals; a command with subcommands requires the next bare word to name one of them.

Defining flags

Flags are a map keyed by the --long name. Everything but the key is optional:

flags: {
    output:  { short: 'o', type: 'string', summary: 'Where to write.' },
    jobs:    { short: 'j', type: 'number', default: 1 },
    verbose: { short: 'v', type: 'boolean' },
    include: { short: 'I', type: 'string', array: true },
    level:   { type: 'string', choices: ['low', 'high'], required: true },
}

| field | meaning | | ----------- | ------------------------------------------------------- | | short | single-letter -x alias | | type | 'boolean' (default), 'string', or 'number' | | array | always collect into an array (even a single occurrence) | | default | value when the flag is absent | | required | error if never supplied (never with default) | | choices | restrict the (coerced) value to a set | | negatable | for booleans, allow --no-<long> (default true) | | summary | one-line help text |

Booleans are presence-only (-vtrue); the rest take a value. Defaults when absent: false for booleans, [] for arrays, undefined otherwise (or your default).

required and default together are a DefinitionError. A flag that must always be supplied can never fall back to a default, so one of the two would silently do nothing; sbopts makes you say which you meant.

The grammar, precisely

| input | meaning | | ------------------------- | ----------------------------------------------- | | --foo | boolean footrue | | --foo bar / --foo=bar | value flag foo'bar' | | --no-foo | negatable boolean foofalse | | -f | boolean ftrue | | -o bar / -o=bar | value flag o'bar' | | -abc | booleans a, b, ctrue (== -a -b -c) | | -abo (o takes a value) | error — a value flag can't be stacked | | --tag a --tag b | repeated → ['a', 'b'] | | -- | stop parsing; the remainder is rest | | - | an ordinary positional (the stdin idiom) |

There is no attached-value shorthand for shorts: -obar is the four-flag stack -o -b -a -r, not -o=bar. A multi-character short token is therefore always a pure boolean stack, and a value-taking short flag must stand on its own (-o bar or -o=bar) — which is exactly "stack only booleans".

A flag declared array: true is always an array. A scalar flag supplied more than once is also promoted to an array (the "repeats become arrays" opinion); a scalar supplied once stays scalar.

Inheritance & deferral

A parent's flags are in scope for every descendant. The parser learns a child's own flags only once it has descended into that child, but the parent's flags stay available throughout — so they can appear before or after the subcommand name:

cli.parse(['-C', 'dir', 'commit']); // flags.dir === 'dir'
cli.parse(['commit', '-C', 'dir']); // flags.dir === 'dir'  (deferred)

Redefining an inherited long name or reusing an inherited short letter is a DefinitionError (a bug in your CLI, thrown when the tree is built). The implicit --help is the exception: it is added by sbopts rather than declared by you, so taking that spelling is an override, not a collision.

Parsing vs. running

command(...).parse(argv) is pure: it returns a result and throws on bad input. Use it when you want to drive control flow yourself.

const { command, path, flags, positionals, rest, terminated, help } = cli.parse(
    process.argv.slice(2),
);
  • command — the resolved (deepest) command
  • path — its names from the root, e.g. ['app', 'remote', 'add']
  • flags — resolved values, defaults filled in
  • positionals — bare arguments
  • rest — everything after --
  • terminated — whether a -- was seen
  • help — whether --help / -h was requested

command(...).run(argv?, io?) is the batteries-included path: it parses, prints help on --help (exit 0), prints usage errors to stderr (exit 1), dispatches to the resolved command's run handler, and returns the handler's (awaited) result. The handler receives { flags, positionals, rest, path, command }.

The io hooks are injectable, which is what makes run() testable:

await cli.run(argv, {
    stdout: (s) => out.push(s),
    stderr: (s) => err.push(s),
    exit: (code) => {
        capturedCode = code;
    },
    width: 72, // help width; defaults to the terminal's, capped at 80
});

Errors

  • DefinitionError — the CLI was declared wrong (duplicate short, unknown type, inherited-flag collision). A programming bug; thrown while building.
  • UsageError — the end user typed something invalid (unknown flag, missing value, failed coercion, unknown subcommand). parse() throws it; run() catches it, prints a hint, and exits non-zero. It carries the command that was in scope when parsing failed.

Help

--help / -h short-circuits parsing and reports help for whichever command was in scope. command.help({ width }) returns the same text as a string.

Declaring a flag named help, or one using -h, takes the spelling over — at any level, not just the root. Your flag is then an ordinary flag: it takes a value if you say so, it does not short-circuit, and parse() reports help: false. Descendants inherit the override; siblings and ancestors keep their own --help. The layout — usage line, description, Arguments, Commands, Options, and Global options (inherited flags) — is typeset with termiflo, so it wraps to the terminal width and stays aligned.

An option's description ends with Required. or Default: <value> where either applies, since neither is visible in the invocation itself. Defaults are shown JSON-quoted, so an empty string reads as "" rather than vanishing. An implicit default (false, []) is not announced — only one you declared.

Usage: app build [options] [target]

Build the project.

Arguments:
  target   What to build.

Options:
  -f, --force             Overwrite outputs.
  -D, --define <string>   Set a key=value definition; may be repeated.
  -j, --jobs <number>     Parallel jobs.
  -h, --help              Show this help and exit.

Global options:
  -C, --dir <string>   Run as if started in <dir>.
  -v, --verbose        Be chatty.

License

ISC, in full at LICENSE.