Build CLIs. TypeScript does the rest.
npm i @truyman/cliimport { command, run } from "@truyman/cli";
const greet = command({
name: "greet",
args: [{ name: "name", type: "string" }],
handler: ([name]) => console.log(`Hello, ${name}!`),
});
run(greet, process.argv.slice(2));$ bun greet.ts World
Hello, World!That's it. Your args are typed. Your handler knows what it's getting.
- Type-safe everything - Args and options flow into your handler with full type inference
- Subcommands - Nest commands infinitely:
cli foo bar baz - Built-in help -
-hand--helpjust work - Graceful errors -
run()catches known errors and prints them pretty - Short & long flags -
-vand--verbose, the way nature intended
import { command, run } from "@truyman/cli";
const greet = command({
name: "greet",
description: "A friendly greeting CLI",
version: "1.0.0",
args: [
{ name: "name", type: "string", description: "Who to greet" },
],
options: {
shout: {
type: "boolean",
long: "shout",
short: "s",
description: "LOUD MODE",
},
times: {
type: "number",
long: "times",
short: "n",
description: "Repeat N times",
},
},
handler: ([name], { shout, times }) => {
let msg = `Hello, ${name}!`;
if (shout) msg = msg.toUpperCase();
for (let i = 0; i < (times || 1); i++) {
console.log(msg);
}
},
});
run(greet, process.argv.slice(2));$ bun greet.ts Ada --shout -n 3
HELLO, ADA!
HELLO, ADA!
HELLO, ADA!| Property | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Command name |
description |
string |
No | Shown in help |
version |
string |
No | Version string |
args |
PositionalArg[] |
No | Positional arguments |
options |
Options |
No | Flag options |
inherits |
Options |
No | Options inherited from parents |
handler |
(args, options) => void |
* | Your code goes here |
subcommands |
Command[] |
* | Nested commands |
groups |
CommandGroups |
No | Group subcommands in help |
examples |
Examples |
No | Usage examples in help |
* A command has either handler OR subcommands, never both.
// options.ts
import type { Options } from "@truyman/cli";
export const GlobalOptions = {
verbose: { type: "boolean", long: "verbose", short: "v" },
} as const satisfies Options;// commands/add.ts
import { command } from "@truyman/cli";
import { GlobalOptions } from "../options";
export const add = command({
name: "add",
inherits: GlobalOptions,
args: [{ name: "url", type: "string" }] as const,
handler: ([url], { verbose }) => {
if (verbose) console.log("[verbose] Adding remote...");
console.log(`Added ${url}`);
},
});// index.ts
import { command, run } from "@truyman/cli";
import { GlobalOptions } from "./options";
import { add } from "./commands/add";
const remote = command({
name: "remote",
options: GlobalOptions,
subcommands: [add],
});
const git = command({
name: "git",
subcommands: [remote],
});
run(git, process.argv.slice(2));$ git remote add https://github.com/... --verboseThe inherits property tells the leaf command which parent options it should parse and receive in its handler. This enables full type inference for inherited options.
Organize subcommands into groups for cleaner help output:
const cli = command({
name: "my-cli",
groups: {
"Project": ["init", "build", "test"],
"Development": ["serve", "watch"],
},
subcommands: [init, build, test, serve, watch, help],
});$ my-cli --help
Usage:
my-cli [options] <command> [args...]
Project:
init Initialize a new project
build Build the project
test Run tests
Development:
serve Start development server
watch Watch for changes
help Show help
Options:
-h, --help Show help
-V, --version Show version
Groups appear in definition order. Commands not assigned to any group appear last without a header. This is optional—omit groups for a flat command list.
Add usage examples to help output:
const cli = command({
name: "my-cli",
description: "A deployment tool",
examples: [
"my-cli deploy",
"my-cli deploy --env staging",
{ command: "my-cli deploy --env prod", description: "Deploy to production" },
],
handler: () => {},
});$ my-cli --help
A deployment tool
Examples:
my-cli deploy
my-cli deploy --env staging
my-cli deploy --env prod Deploy to production
Usage:
my-cli [options]
Options:
-h, --help Show help
-V, --version Show version
Examples can be simple strings or objects with { command, description } for annotated examples. Descriptions are shown dimmed and aligned.
| Property | Type | Description |
|---|---|---|
name |
string |
Argument name shown in help |
type |
string |
"string", "number", or "boolean" |
description |
string |
Shown in help output |
optional |
boolean |
Shows as [name] instead of <name> |
variadic |
boolean |
Collect remaining args into array (must be last) |
const rm = command({
name: "rm",
args: [{ name: "files", type: "string", variadic: true }] as const,
handler: ([files]) => files.forEach(f => console.log(`Removing ${f}`)),
});
// rm file1.txt file2.txt file3.txt → files = ["file1.txt", "file2.txt", "file3.txt"]| Property | Type | Description |
|---|---|---|
type |
string |
"string", "number", or "boolean" |
long |
string |
Long flag name (defaults to key name) |
short |
string |
Single-character short flag |
description |
string |
Shown in help output |
default |
any |
Default value when not provided |
required |
boolean |
Throw error if not provided |
multiple |
boolean |
Collect repeated flags into array |
negatable |
boolean |
Allow --no-<flag> syntax (boolean only) |
placeholder |
string |
Custom placeholder in help (e.g., "path") |
port: { type: "number", default: 3000 }
// --port 8080 → 8080, (omitted) → 3000config: { type: "string", required: true }
// Missing --config throws MissingOptionErrortag: { type: "string", multiple: true }
// --tag foo --tag bar → ["foo", "bar"]color: { type: "boolean", negatable: true }
// --color → true, --no-color → falseHandlers can be async. run() awaits completion:
handler: async ([url]) => {
const res = await fetch(url);
console.log(await res.text());
}run() catches errors and displays helpful messages. For custom handling, call command.run() directly:
import { MissingArgumentError } from "@truyman/cli";
try {
myCommand.run(argv);
} catch (err) {
if (err instanceof MissingArgumentError) {
// Custom handling
}
}| Error | Cause |
|---|---|
MissingArgumentError |
Required positional argument not provided |
InvalidArgumentError |
Argument value doesn't match expected type |
MissingOptionError |
Required option not provided |
InvalidOptionError |
Option value doesn't match expected type |
UnknownOptionError |
Unknown flag provided |
MissingSubcommandError |
Parent command invoked without subcommand |
UnknownSubcommandError |
Unknown subcommand name (shows suggestions) |
Use as const on args for precise type inference:
// ✓ Handler receives [string, number | undefined]
args: [
{ name: "file", type: "string" },
{ name: "count", type: "number", optional: true },
] as const,
// ✗ Without as const: handler receives unknown[]For reusable options, use as const satisfies Options:
const GlobalOptions = {
verbose: { type: "boolean", short: "v" },
} as const satisfies Options;Runs the command. Handles -h/--help automatically. Missing args? Shows help. Bad option? Red error + usage.
Returns the auto-generated help string. For when you need it manually.
MIT