Skip to content

Overview

@bomb.sh/router is a statically typed entry-point router for command-line applications. An argument parser tells you what the user typed. The router tells you where and how they intend to enter your program, and binds a validated, statically typed model for that entry point.

The router matches input to an intent: a method at an application-relative route.

HELP /
VERSION /
EXECUTE /pr/list
HELP /pr/create
EXECUTE /pr/create

There are three methods: help, version, and execute. Every route supports help. Version and execute exist only on the routes where you add them.

Every reachable intent appears in the result type of parse(). Narrow method and route, and TypeScript knows the exact model for that entry point:

import
var process: NodeJS.Process
process
from "node:process";
import {
function command<const N extends string, const E extends readonly Input[]>(start: Definition<N>, ...elements: E & Validate<CommandZero<N>, E>): Materialize<Fold<CommandZero<N>, Normalize<E>>>
command
,
function name<N extends string>(name: N): Definition<N>
name
,
function option<const N extends string, const E extends readonly Unary[]>(named: Definition<N>, ...elements: E & Check<Zero<N>, E>): ElementOf<...>
option
,
function parse<const R extends AnyRoute>(route: R, input: Input): Parse<R>
parse
,
function route<const N extends string, const E extends readonly Input[]>(start: Definition<N>, ...elements: E & Validate<RouteZero<N>, E>): Materialize<Fold<RouteZero<N>, Normalize<E>>>
route
,
function schema<S extends Schema>(schema: S): TransformElement<SchemaTransform<OutputOf<S>>>
schema
,
} from "@bomb.sh/router";
import * as
import z
z
from "zod";
const
const app: Route<"gh", "help" | "execute", readonly [Done<{}, readonly [Route<"pr", "help", readonly [Done<{}, readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>]>]>]>
app
=
command<"gh", readonly [Route<"pr", "help", readonly [Done<{}, readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>]>]>(start: Definition<"gh">, elements_0: Route<...>): Route<...>
command
(
name<"gh">(name: "gh"): Definition<"gh">
name
("gh"),
route<"pr", readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>(start: Definition<"pr">, elements_0: Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>): Route<...>
route
(
name<"pr">(name: "pr"): Definition<"pr">
name
("pr"),
command<"list", readonly [ModelElement<ParamModel<"limit", number>>]>(start: Definition<"list">, elements_0: ModelElement<ParamModel<"limit", number>>): Route<...>
command
(
name<"list">(name: "list"): Definition<"list">
name
("list"),
option<"limit", readonly [TransformElement<SchemaTransform<number>>]>(named: Definition<"limit">, elements_0: TransformElement<SchemaTransform<number>>): ModelElement<...>
option
(
name<"limit">(name: "limit"): Definition<"limit">
name
("limit"),
schema<z.ZodDefault<z.ZodNumber>>(schema: z.ZodDefault<z.ZodNumber>): TransformElement<SchemaTransform<number>>
schema
(
import z
z
.
function number(params?: z.RawCreateParams & {
coerce?: boolean;
}): z.ZodNumber
export number
number
().
ZodType<number, ZodNumberDef, number>.default(def: number): z.ZodDefault<z.ZodNumber> (+1 overload)
default
(30))),
),
),
);
const
const result: Parse<Route<"gh", "help" | "execute", readonly [Done<{}, readonly [Route<"pr", "help", readonly [Done<{}, readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>]>]>]>>
result
=
parse<Route<"gh", "help" | "execute", readonly [Done<{}, readonly [Route<"pr", "help", readonly [Done<{}, readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>]>]>]>>(route: Route<...>, input: Input): Parse<...>
parse
(
const app: Route<"gh", "help" | "execute", readonly [Done<{}, readonly [Route<"pr", "help", readonly [Done<{}, readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>]>]>]>
app
, {
argv: string[]
argv
:
var process: NodeJS.Process
process
.
NodeJS.Process.argv: string[]

The process.argv property returns an array containing the command-line arguments passed when the Node.js process was launched. The first element will be

execPath

. See process.argv0 if access to the original value of argv[0] is needed. The second element will be the path to the JavaScript file being executed. The remaining elements will be any additional command-line arguments.

For example, assuming the following script for process-args.js:

import { argv } from 'node:process';
// print process.argv
argv.forEach((val, index) => {
console.log(`${index}: ${val}`);
});

Launching the Node.js process as:

Terminal window
node process-args.js one two=three four

Would generate the output:

0: /usr/local/bin/node
1: /Users/mjr/work/node/process-args.js
2: one
3: two=three
4: four

@since ― v0.1.27

argv
.
Array<string>.slice(start?: number, end?: number): string[]

Returns a copy of a section of an array. For both start and end, a negative index can be used to indicate an offset from the end of the array. For example, -2 refers to the second to last element of the array.

@param ― start The beginning index of the specified portion of the array. If start is undefined, then the slice begins at index 0.

@param ― end The end index of the specified portion of the array. This is exclusive of the element at the index 'end'. If end is undefined, then the slice extends to the end of the array.

slice
(2) });
if (
const result: Parse<Route<"gh", "help" | "execute", readonly [Done<{}, readonly [Route<"pr", "help", readonly [Done<{}, readonly [Route<"list", "help" | "execute", readonly [Done<{
limit: number;
}, []>]>]>]>]>]>>
result
.
ok: boolean
ok
&&
const result: Help<"/"> | Execute<"/", {
"/": {};
}> | Help<"/pr"> | Help<"/pr/list"> | Execute<"/pr/list", {
"/": {};
"/pr": {};
"/pr/list": {
limit: number;
};
}>
result
.
Intent<M extends Method, P extends RoutePath>.method: "help" | "execute"
method
=== "execute" &&
const result: Execute<"/", {
"/": {};
}> | Execute<"/pr/list", {
"/": {};
"/pr": {};
"/pr/list": {
limit: number;
};
}>
result
.
Intent<M extends Method, P extends RoutePath>.route: "/" | "/pr/list"
route
=== "/pr/list"
) {
const result: Execute<"/pr/list", {
"/": {};
"/pr": {};
"/pr/list": {
limit: number;
};
}>
result
.
Execute<"/pr/list", { "/": {}; "/pr": {}; "/pr/list": { limit: number; }; }>.model: {
limit: number;
}
model
.
limit: number
limit
; // number
}

@bomb.sh/router is not a CLI framework. It does not own handlers, effects, output, or process lifetime. It is not a flag parser whose product is a bag of flags either. Its product is a typed intent, and your application decides what that intent does.

  • Typed intents: parse() returns a discriminated union of every reachable entry point, so a switch on method and route is fully type-checked.
  • Route-scoped options: Parent and child routes can declare the same option name without ambiguity, because each token binds to the route segment that owns it.
  • Multiple sources: Bind CLI arguments, environment variables, and JavaScript values onto the same model, with an explicit precedence order.
  • Standard Schema validation: Zod, ArkType, Valibot, and other Standard Schema libraries work without adapters. Invalid data never reaches your application.
  • No I/O: The parser is synchronous. It reads only the argv, values, and environment records you pass to it.
  • Runs everywhere: No runtime-specific APIs, so it runs on Node.js, Deno, and Bun.

Head to Installation to add @bomb.sh/router to your project.