Skip to main content
Single-command CLIs are perfect for utilities that perform one specific task. Instead of registering subcommands, you pass the run handler directly to Cli.create().

When to Use Single-Command Pattern

Use this pattern when your CLI:
  • Performs a single, focused task
  • Doesn’t need subcommands or command groups
  • Has a simple input/output flow
Examples: greet, convert, validate, ping

Basic Single-Command CLI

1
Import incur
2
Start by importing Cli and z from incur:
3
import { Cli, z } from 'incur'
4
Define the CLI with run
5
Pass the run handler directly to Cli.create():
6
Cli.create('greet', {
  description: 'A greeting CLI',
  args: z.object({
    name: z.string().describe('Name to greet'),
  }),
  run(c) {
    return { message: `hello ${c.args.name}` }
  },
}).serve()
7
Run the CLI
8
greet world
# → message: hello world

Adding Arguments

Define positional arguments using a Zod schema. Arguments are parsed in the order they appear in the schema:
Cli.create('add', {
  description: 'Add two numbers',
  args: z.object({
    a: z.coerce.number().describe('First number'),
    b: z.coerce.number().describe('Second number'),
  }),
  run(c) {
    return { result: c.args.a + c.args.b }
  },
}).serve()
add 5 10
# → result: 15
Arguments are always required unless marked with .optional(). Use z.coerce.number() or z.coerce.boolean() to parse string input from the command line.

Adding Options

Options are named flags that users pass with --flag syntax:
Cli.create('greet', {
  description: 'A greeting CLI',
  args: z.object({
    name: z.string().describe('Name to greet'),
  }),
  options: z.object({
    loud: z.boolean().default(false).describe('Shout the greeting'),
    prefix: z.string().optional().describe('Add a prefix'),
  }),
  run(c) {
    let message = `hello ${c.args.name}`
    if (c.options.prefix) message = `${c.options.prefix} ${message}`
    if (c.options.loud) message = message.toUpperCase()
    return { message }
  },
}).serve()
greet alice --loud
# → message: HELLO ALICE

greet bob --prefix "Hey there:" --loud
# → message: HEY THERE: HELLO BOB

Option Aliases

Create short aliases for options:
Cli.create('greet', {
  args: z.object({
    name: z.string(),
  }),
  options: z.object({
    loud: z.boolean().default(false),
  }),
  alias: { loud: 'L' },
  run(c) {
    const message = c.options.loud ? `HELLO ${c.args.name}` : `hello ${c.args.name}`
    return { message }
  },
}).serve()
greet alice -L
# → message: HELLO ALICE

Output Handling

The return value from run is automatically formatted and displayed to the user.

Default Output (TOON)

By default, incur uses TOON format - a compact, token-efficient format:
Cli.create('status', {
  run() {
    return {
      clean: true,
      branch: 'main',
      files: ['src/index.ts', 'README.md'],
    }
  },
}).serve()
status
# → clean: true
# → branch: main
# → files[2]:
# →   src/index.ts
# →   README.md

Async Handlers

Handlers can be async:
Cli.create('fetch', {
  args: z.object({
    url: z.string().describe('URL to fetch'),
  }),
  async run(c) {
    const response = await fetch(c.args.url)
    const data = await response.json()
    return { data }
  },
}).serve()

Error Handling

Use c.error() for structured errors:
Cli.create('divide', {
  args: z.object({
    a: z.coerce.number(),
    b: z.coerce.number(),
  }),
  run(c) {
    if (c.args.b === 0) {
      return c.error({
        code: 'DIVISION_BY_ZERO',
        message: 'Cannot divide by zero',
      })
    }
    return { result: c.args.a / c.args.b }
  },
}).serve()
Or throw an IncurError:
import { Cli, Errors, z } from 'incur'

Cli.create('auth', {
  run() {
    const token = process.env.API_TOKEN
    if (!token) {
      throw new Errors.IncurError({
        code: 'NOT_AUTHENTICATED',
        message: 'API_TOKEN environment variable not set',
        retryable: false,
      })
    }
    return { authenticated: true }
  },
}).serve()

Context Properties

The run handler receives a context object with:
  • c.args - Parsed positional arguments
  • c.options - Parsed option flags
  • c.name - The CLI name (useful for help messages)
  • c.agent - true if called by an agent (stdout is not a TTY)
  • c.env - Parsed environment variables (if env schema is defined)
  • c.var - Variables set by middleware
  • c.ok(data, meta?) - Return success with optional CTAs
  • c.error(options) - Return an error
Cli.create('whoami', {
  run(c) {
    if (!c.agent) {
      console.log('Running in terminal mode...')
    }
    return { name: c.name, agent: c.agent }
  },
}).serve()

Built-in Help

Help is generated automatically from your schemas:
greet --help
# greet – A greeting CLI
#
# Usage: greet <name>
#
# Arguments:
#   name  Name to greet
#
# Built-in Commands:
#   completions  Generate shell completion script
#   mcp add      Register as an MCP server
#   skills add   Sync skill files to your agent
#
# Global Options:
#   --format <toon|json|yaml|md|jsonl>  Output format
#   --help                              Show help
#   --llms                              Print LLM-readable manifest
#   --mcp                               Start as MCP stdio server
#   --verbose                           Show full output envelope
#   --version                           Show version

Complete Example

Here’s a complete single-command CLI with all features:
import { Cli, z } from 'incur'

Cli.create('calc', {
  description: 'Simple calculator',
  version: '1.0.0',
  args: z.object({
    operation: z.enum(['add', 'subtract', 'multiply', 'divide']).describe('Operation to perform'),
    a: z.coerce.number().describe('First number'),
    b: z.coerce.number().describe('Second number'),
  }),
  options: z.object({
    precision: z.coerce.number().default(2).describe('Decimal precision'),
  }),
  run(c) {
    let result: number
    
    switch (c.args.operation) {
      case 'add':
        result = c.args.a + c.args.b
        break
      case 'subtract':
        result = c.args.a - c.args.b
        break
      case 'multiply':
        result = c.args.a * c.args.b
        break
      case 'divide':
        if (c.args.b === 0) {
          return c.error({
            code: 'DIVISION_BY_ZERO',
            message: 'Cannot divide by zero',
          })
        }
        result = c.args.a / c.args.b
        break
    }
    
    return {
      operation: c.args.operation,
      result: Number(result.toFixed(c.options.precision)),
    }
  },
}).serve()
calc multiply 5 7
# → operation: multiply
# → result: 35

calc divide 10 3 --precision 4
# → operation: divide
# → result: 3.3333
If you need subcommands, use the multi-command pattern instead. Don’t pass both run and .command() calls - they have different purposes.

Build docs developers (and LLMs) love