Programs
Updated Apr 30, 2026
f.program(config?) opens a builder for a CLI tree. Pass commander-level
config, then call .commands({ ... }) with a map of subcommands. The
terminal .commands(...) returns the fully-wired root commander 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}` }))
const deploy = f
.command({ name: "deploy" })
.input(z.object({ env: z.enum(["staging", "prod"]) }))
.output(z.object({ ok: z.boolean() }))
.handler(() => ({ ok: true }))
const cli = f
.program({ name: "myapp", description: "My CLI", version: "1.0.0" })
.commands({ greet, deploy })
cli.parseAsync(process.argv)myapp greet world, myapp deploy --env prod, myapp --help,
myapp --llms all work.
Object key wins as the subcommand name
.commands({ greet: someCmd }) attaches someCmd under the name "greet"
even if the leaf was built with name: "something-else". Decoupling the
program-level routing from each leaf's own naming makes it easy to mount
the same command twice or rename at attach time.
Nesting
Programs and commands are both commander Command instances, so a program
can be a subcommand of another program:
const apiCli = f.program({ name: "api" }).commands({ greet, deploy })
const dbCli = f.program({ name: "db" }).commands({ migrate, seed })
const cli = f.program({ name: "myapp" }).commands({
api: apiCli,
db: dbCli,
status: statusLeaf,
})
// myapp api greet world
// myapp db migrate
// myapp status--llms flattens the tree to space-separated tool names — the inner
greet becomes "api greet" in the manifest. See LLMs mode.
ProgramConfig
Same shape as CommandConfig minus the input-side concerns: arguments
(no input fields at the program level) and jsonOption (programs dispatch —
they don't read JSON input). Otherwise everything in
CommandConfig applies — identity (name,
description, summary, aliases, usage, version), help slots
(helpOption, helpCommand, configureHelp, addHelpText, addHelpCommand,
addHelpOption), behavior toggles, hooks, exit/output config, on
listeners, executableDir, and llmsOption.
enablePositionalOptions defaults to true on programs
Without it, a program-level option like --llms would shadow a subcommand's
--llms (commander would parse --llms against the program no matter where
it appears in argv). With positional options on, options before the
subcommand belong to the program and options after belong to the subcommand
— exactly what you want for tree CLIs. Override by passing
enablePositionalOptions: false if you specifically need the legacy
behavior.
--help and --llms per level
Each level keeps its own:
myapp --help # top-level help, lists subcommands
myapp greet --help # leaf-level help, lists args/options
myapp --llms # entire tree as a flat MCP tools/list
myapp greet --llms # just the greet leafSuppress program-level --llms with llmsOption: false in the program
config; subcommands keep theirs.
Created with ❤ and Livemark