Fireargs is in technical previewShare feedback

Search

Search documentation

Commands

Updated Oct 21, 2026

f.command(config?) opens a chainable, type-stated builder. Call .input(...), .output(...), optionally .render(...), then .handler(...), in that order. The terminal .handler(...) returns a fully-wired Command, a subclass of commander's Command tagged type: "command".

import { f } from "fireargs"
import { z } from "zod"

const greet = f
  .command({ name: "greet" })
  .input(z.object({ name: z.string() }))
  .output(z.object({ greeting: z.string() }))
  .handler(input => ({ greeting: `hello ${input.name}` }))

await greet.parseAsync(process.argv)

.input(z.object) and .output(schema)

The input must be a z.object(...): its shape determines the positional arguments and --options (see Inputs). The output can be any zod schema — an object, an array, a scalar (see Outputs). It validates whatever the handler returns; mismatches throw at runtime through commander's parseAsync.

.render((output, context) => text)

Optional. Turns the validated output into what a human sees at the shell. It runs only without --json; with it, the output is written as JSON instead. A returned string is written to writeOut with a trailing newline; return nothing to write the output yourself (spinners, colors, tables). context carries input (the parsed input), json and command. Without .render, a run without --json prints nothing.

f.command({ name: "list" })
  .input(z.object({ path: f.argument().string() }))
  .output(z.array(z.string()))
  .render(names => names.map(name => `- ${name}`).join("\n"))
  .handler(input => listNames(input.path))

.handler((input, context) => result)

Receives the parsed, zod-validated input and a context. Must return a value matching the output schema. May be async — fireargs awaits it.

context.json is true when --json was given, with or without a value: a handler that drives spinners or progress output keeps quiet then, so stdout stays parseable. context.command is the running Command.

The handler is wrapped in commander's .action(...). Input that fails the zod schema is reported like a commander parse error — one error: ... line per field on stderr, exit code 1. Other errors propagate via the returned Command's parseAsync(...). A renderError hook takes all of them instead (see JSON mode).

What gets derived from the input schema

For each property of the input object schema:

Zod constructBecomes
z.string() / z.number()--key <value> (mandatory unless wrapped); a number's CLI string is coerced
z.boolean()--key flag (boolean)
f.argument()...positional <key>; [key] when .optional() or .default(v)
z.default(v)forwarded to commander as defaultValue
z.enum([...])cmd.choices([...]) on the option/argument
z.array(T)variadic — <key...> (or [key...]) for positional, --key <value...> option
z.array(z.enum([...]))variadic with choices on the element
.describe("...")shown in commander --help and surfaced as description in --llms JSON Schema

key is shown in kebab-case: headerRows becomes --header-rows and <header-rows>, while the handler and --json keep headerRows. See Flag names.

For per-field commander config that zod doesn't model (short flag, env var, conflicts, hidden, preset, a custom parse, …) wrap the field with f.argument(config) or f.option(config).

CommandConfig

The config passed to f.command(config?) is a thin pass-through to commander methods. All fields are optional.

FieldMaps to
namecmd.name(...)
description, summary, aliases, usage, versionidentity setters
versionOption{ flags?, description? } for the option version adds, e.g. { flags: "-v, --version" }
helpGroupcmd.helpGroup(...) — heading this command is listed under in its parent's help
helpOption, helpCommandcmd.helpOption(...), cmd.helpCommand(...); false to disable
configureHelpcmd.configureHelp(...) — Partial<commander.Help>
addHelpText{ beforeAll?, before?, after?, afterAll? } — each forwarded to cmd.addHelpText(position, text)
addHelpCommand, addHelpOptiondrop a pre-built commander Command / Option instance into help slots
preSubcommand, preAction, postActioneach becomes cmd.hook(event, fn)
allowUnknownOption, allowExcessArguments, enablePositionalOptions, passThroughOptions, combineFlagAndOptionalValuematching boolean toggles
showHelpAfterError, showSuggestionAfterErrormatching setters
exitOverridetrue enables throw mode; pass a (err) => void for a custom handler
configureOutputcmd.configureOutput({...}) — custom writeOut/writeErr/etc.
executableDircmd.executableDir(path)
onArray<{ event, listener }> — each registered via cmd.on(event, listener)
jsonOption, llmsOptioncustomize or disable the built-in --json/--llms flags. See LLMs mode
renderError(error, context) => value — turn a failed run into output. See JSON mode

Anything commander supports that fireargs hasn't surfaced (yet) you can call directly on the returned Command — .handler(...) returns a real commander instance.

Why fixed-order pipeline?

.input then .output then .handler, all required, with .render the one optional step before .handler. This keeps the type-state simple (small interfaces exposing one or two methods) and makes the Command's static type predictable. If you find yourself wanting an inputless command, use z.object({}).

Created with ❤ and Livemark