Search
Search documentation
Introduction
Updated Oct 21, 2026
Define each command with typed input and output schemas. Get composability, structured JSON I/O, and LLM-ready manifests baked in — for humans, agents, and scripts alike.
Installation
pnpm add fireargs
# or
npm install fireargsA first command
import { f } from "fireargs"
import { z } from "zod"
// Composable arguments and options
const name = f.argument().schema(z.string().describe("The user's name."))
const times = f.option().schema(z.number().min(1).default(1).describe("Repeat it n times."))
const shout = f.option({ short: "s" }).schema(z.boolean().default(false).describe("Use capitals."))
// Composable groups of them
const greetInput = z.object({ name, times, shout })
// Typed output, validated on every run
const greetOutput = z.object({ greeting: z.string() })
// A commander command, typed from the schemas
const greet = f
.command({ name: "greet", description: "Greet someone politely." })
.input(greetInput)
.output(greetOutput)
// Output for humans; --json prints the data instead
.render(output => output.greeting)
.handler(input => {
const greeting = Array(input.times).fill(`hello ${input.name}`).join(" ")
return { greeting: input.shout ? greeting.toUpperCase() : greeting }
})
// Use as a normal commander command, e.g. configure or apply plugins
greet.parseAsync(process.argv)That gets you:
$ node greet.ts world --times 2 -s # CLI input, rendered output
HELLO WORLD HELLO WORLD
$ node greet.ts world --json # CLI input, JSON output
{
"greeting": "hello world"
}
$ node greet.ts --json '{"name":"world","times":2}' # JSON input, JSON output
{
"greeting": "hello world hello world"
}
$ node greet.ts world --times 0
error: option '--times <value>' argument '0' is invalid. Too small: expected number to be >=1--help comes from commander:
Usage: greet [options] <name>
Greet someone politely.
Arguments:
name The user's name.
Options:
--json [value] write output as JSON; pass a JSON string to also drive input
--llms print this command's schema (name, input, output) as JSON for
LLM tool-use
--times <value> Repeat it n times. (default: 1)
-s, --shout Use capitals. (default: false)
-h, --help display help for commandA first program
Group leaves with f.program(...).router({ ... }):
// Composable commands: a router of leaves and nested programs
const cli = f
.program({ name: "myapp", description: "My CLI", version: "1.0.0" })
.router({ greet, deploy, db: dbCli })
// Use as a normal commander command, e.g. configure or apply plugins
cli.parseAsync(process.argv)That gives:
$ myapp --help
Usage: myapp [options] [command]
My CLI
Options:
-V, --version output the version number
--llms print the program manifest (subcommand schemas) as
JSON for LLM tool-use
-h, --help display help for command
Commands:
greet [options] <name> Greet someone politely.
deploy [options] <env> Deploy the current build.
db [options] Database tasks.
$ myapp db migrate --json '{}'
{
"applied": 0
}Programs nest — router accepts other programs the same way it accepts
leaves. myapp db migrate routes through both levels.
JSON mode
Every command takes --json. Bare, it switches the output to JSON; given a value, that value is
the input, checked against the input schema, so scripts never assemble argv:
$ node greet.ts world --times 2 --json # CLI input, JSON output
{
"greeting": "hello world hello world"
}
$ node greet.ts --json '{"name":"world","times":3,"shout":true}' # JSON input, JSON output
{
"greeting": "HELLO WORLD HELLO WORLD HELLO WORLD"
}
$ node greet.ts world --json | jq -r .greeting # pipe it into other tools
hello world
$ node greet.ts --json '{"times":2}' # input errors go to stderr, exit code 1
error: invalid --json input at 'name': Invalid input: expected string, received undefined
$ node greet.ts --json '{"name":"world","times":"2"}'
error: invalid --json input at 'times': Invalid input: expected number, received stringstdout only ever carries the data, so a script can parse it without checking for noise.
LLMs mode
Every command and program ships with --llms, which prints an
MCP-tools/list-shaped manifest. For greet.ts above:
{
"tools": [
{
"name": "help",
"description": "Documentation tool, not callable. Each `tools[].name` is the space-separated path beneath this binary. Invoke a tool with `<binary> <name> --json '<value>'` matching that tool's `inputSchema`; output is JSON on stdout matching `outputSchema`.",
"inputSchema": {
"type": "object",
"properties": {}
},
"outputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "greet",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The user's name."
},
"times": {
"default": 1,
"description": "Repeat it n times.",
"type": "number",
"minimum": 1
},
"shout": {
"default": false,
"description": "Use capitals.",
"type": "boolean"
}
},
"required": ["name"]
},
"outputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"greeting": {
"type": "string"
}
},
"required": ["greeting"],
"additionalProperties": false
},
"description": "Greet someone politely."
}
]
}An LLM agent reads the manifest, picks a tool, and invokes it with
<binary> <name> --json '<value>'. Output comes back as JSON on stdout.
Documentation
- Commands — the
f.command(...)chainable builder. - Inputs —
f.argument()/f.option()and per-field commander config. - Outputs — the output schema, serialization, and JSON Schema in
--llms. - Programs — subcommand trees, nesting, and program-level config.
- JSON mode —
--jsonfor programmatic and pipeline use. - LLMs mode —
--llmsand the MCP-shaped manifest. - Composability — share common params across commands by composing zod schemas.
- Contributing — local setup, conventions, release flow.
License
MIT.
Created with ❤ and Livemark