Introducing Bombshell Router
A new category of CLI input and option parser
Today we’re introducing @bomb.sh/router, a small library that parses command-line input into valid, statically typed entry points. It’s an exciting addition to the Bombshell toolbox that’s built to meet the needs of any CLI application or framework. From the tiniest app with a single command to the biggest SDKs that potentially ship hundreds of them, it’s a snap to integrate and get started.
Parse intent, not options
Historically, we think of a CLI’s first task as “parsing the options” before considering how to proceed. Usually, this involves mapping input values into a (potentially hierarchically nested) property bag which is then passed to a handler. As commands multiply, it becomes harder to see which values belong to which command and which are inherited from its parents.
@bomb.sh/router reframes this as parsing an intent to enter the application at a specific place and with specific data. Each intent matches a route (where we want to enter the application) and a method (what we want to do there). Then, if the method is EXECUTE, it will also carry a validated model (the values that the entry point needs to do its thing).
Let’s imagine a pared-down version of the aws CLI built with @bomb.sh/router. Here’s an invocation of the cp command:
$ aws --profile production --region us-east-1 \ s3 --max-concurrent-requests 30 \ cp ./report.csv s3://reports/report.csvIn Bombshell Router terminology, this request to copy ./report.csv up to s3://reports/report.csv looks like this:
EXECUTE /s3/cp{ "/": { "profile": "production", "region": "us-east-1" }, "/s3": { "maxConcurrentRequests": 30 }, "/s3/cp": { "source": "./report.csv", "target": "s3://reports/report.csv" }}Here the method is EXECUTE and the route is /s3/cp. The models object keeps configuration scoped to each route along the way. Notice how the parent routes, / and /s3, each have their own model. Profile and region belong to /, concurrency belongs to /s3, and source and target belong to /s3/cp.
If we ask for --help at the same route, we get a different intent:
$ aws s3 cp --helpHELP /s3/cpStandard Schema
In order for Bombshell Router to parse an execution intent, its models must be fully validated. It uses the Standard Schema interface everywhere, so you can use schemas from libraries like Zod, ArkType, and Valibot interchangeably. The schemas you already use in your application can describe its entry points, too.
You can validate individual options and arguments, or validate and transform them into the model your application actually needs. That includes relationships between values, defaults, and transformations whose output types carry through to the intent. Your application always receives a model it can work with directly, with runtime and TypeScript agreeing on its shape.
Dynamic Discovery
Sometimes you need to parse part of a command-line invocation before you know what the rest means. For example, a --config option might point to a file that lists plugins, and those plugins might supply new options, new routes, or both! In other words, the invocation itself contains the information needed to discover the routes it can enter.
Bombshell Router solves this seeming chicken-and-egg dilemma with support for incremental parsing. At every declared dynamic section, parsing pauses with a validated model and a continuation. Your application loads what it needs, then resumes parsing with the result. The remaining input is preserved, and parsing can continue through newly discovered options and routes, pausing again wherever necessary.
This works for help, too. A request for help can follow discovery all the way to a command that wasn’t known when parsing began. The routing graph can grow as the intent is parsed, while your application stays in charge of every bit of I/O.
Value Sources
An entry point needs values, and command-line arguments are only one place to get them. Bombshell Router can also bind environment variables and JavaScript values, including configuration you’ve loaded from a file, into the same models.
A port parameter at the root, /, automatically binds to the PORT environment variable; the same parameter on /serve binds to SERVE_PORT. Values follow a consistent precedence: CLI arguments, then environment variables, then supplied JavaScript values, then schema defaults. Whatever the source, the selected value goes through the same schema validation.
That gives you one definition of what an entry point accepts, whether someone invokes it at a terminal, configures it in CI, or supplies a configuration file. Your command code receives the resulting model either way.
Not a framework
Despite doing so much, Bombshell Router is tiny, has no runtime dependencies, and performs no I/O. Parsing is fully synchronous, including each step of dynamic discovery. It returns a data structure describing the user’s intent, leaving your application in charge of handlers, effects, output, and process lifetime. Even the help and error formatters return strings for you to display however you like.
Its architecture composes seamlessly, so you can start small with a single root command in the classic parseArgs() style and grow organically into hundreds of entry points nested as deeply as your application requires.
Get Started
Bombshell Router is not just a better mousetrap for argument parsing. It’s a new way of thinking about how inputs relate to the surface of your application. We hope you’ll give it a try!
npm install @bomb.sh/routerCheck out the README and get in touch with us on Discord to ask any questions or show us what you’re building. We’re pretty excited about the features this will unlock and every bit as excited to hear how it works out for you.