Skip to main content

Overview

Commands are registered with .command(), which accepts a name and a definition object containing schemas, metadata, and a run handler.

Registering Commands

Chain .command() calls to register multiple commands:
import { Cli, z } from 'incur'

Cli.create('my-cli', { description: 'My CLI' })
  .command('status', {
    description: 'Show repo status',
    run() {
      return { clean: true }
    },
  })
  .command('install', {
    description: 'Install a package',
    args: z.object({
      package: z.string().describe('Package name'),
    }),
    run(c) {
      return { installed: c.args.package }
    },
  })
  .serve()
$ my-cli status
clean: true

$ my-cli install express
installed: express

Command Definition Structure

A command definition includes schemas for inputs, output, and the handler function.

Required Fields

run
function
required
The command handler. Receives a context object with parsed args, options, env, and helper functions. Can return a value, a Promise, or an AsyncGenerator for streaming.

Input Schemas

args
z.ZodObject<any>
Zod schema for positional arguments. Keys define the order arguments are parsed.
args: z.object({
  source: z.string().describe('Source file'),
  dest: z.string().describe('Destination file'),
})
options
z.ZodObject<any>
Zod schema for named options/flags.
options: z.object({
  force: z.boolean().optional().describe('Overwrite existing files'),
  verbose: z.boolean().default(false).describe('Show detailed output'),
})
env
z.ZodObject<any>
Zod schema for environment variables. Keys are the variable names.
env: z.object({
  DEPLOY_TOKEN: z.string(),
  API_BASE: z.string().default('https://api.example.com'),
})
output
z.ZodType
Zod schema for the return value. Validates the data returned from run().
output: z.object({
  url: z.string(),
  duration: z.number(),
})

Metadata

description
string
A short description of what the command does. Shown in help output.
alias
Record<string, string>
Map of option names to single-character aliases.
alias: { saveDev: 'D', force: 'f' }
Enables -D as shorthand for --saveDev.
examples
Example[]
Usage examples shown in help output.
examples: [
  { args: { env: 'staging' }, description: 'Deploy to staging' },
  { args: { env: 'production' }, options: { force: true }, description: 'Force deploy to production' },
]
hint
string
Plain text hint displayed in help after examples and before global options.

Behavior

format
'toon' | 'json' | 'yaml' | 'md' | 'jsonl'
Default output format for this command. Overrides CLI-level format.
outputPolicy
'all' | 'agent-only'
Controls when output data is displayed. Overrides CLI-level or group-level policy.
middleware
MiddlewareHandler[]
Middleware that runs only for this command, after root and group middleware.

Mounting Sub-CLIs

Mount another CLI instance as a command group by passing it directly to .command():
import { Cli, z } from 'incur'

const cli = Cli.create('my-cli', { description: 'My CLI' })

// Create a sub-group for database commands
const db = Cli.create('db', { description: 'Database commands' })
  .command('migrate', {
    description: 'Run migrations',
    run() {
      return { migrated: true }
    },
  })
  .command('seed', {
    description: 'Seed the database',
    run() {
      return { seeded: true }
    },
  })

cli
  .command('run', {
    description: 'Run a task',
    run() {
      return { ok: true }
    },
  })
  .command(db)  // Mount the db group
  .serve()
$ my-cli db migrate
migrated: true

$ my-cli db seed
seeded: true

Run Handler Context

The run function receives a context object with parsed inputs and helpers:
run(c) {
  c.agent      // boolean: true if stdout is not a TTY (consumed by an agent)
  c.args       // Parsed positional arguments
  c.options    // Parsed named options
  c.env        // Parsed environment variables
  c.var        // Variables set by middleware
  c.name       // CLI name
  c.version    // CLI version string
  
  // Helpers
  c.ok(data, { cta })    // Return success with optional CTAs
  c.error({ code, message, cta })  // Return error with optional CTAs
}

Returning Success

Return a value directly or use c.ok() to attach CTAs:
run(c) {
  return { deployed: true }
}

// With CTAs
run(c) {
  return c.ok(
    { deployed: true },
    {
      cta: {
        commands: [
          { command: 'logs', description: 'View logs' },
          { command: 'status', description: 'Check status' },
        ],
      },
    }
  )
}

Returning Errors

Throw an error or use c.error() for structured errors:
run(c) {
  throw new Error('Something went wrong')
}

// Structured error with code
run(c) {
  return c.error({
    code: 'NOT_AUTHENTICATED',
    message: 'Token not found',
    retryable: false,
  })
}

Async Handlers

Handlers can be async:
run(async c) {
  const response = await fetch('https://api.example.com/data')
  const data = await response.json()
  return data
}

Streaming Handlers

Use async *run to stream chunks incrementally:
cli.command('logs', {
  description: 'Tail logs',
  async *run() {
    yield 'connecting...'
    yield 'streaming logs'
    yield 'done'
  },
})
$ my-cli logs
connecting...
streaming logs
done
With --format jsonl, each chunk becomes {"type":"chunk","data":"..."}. You can also yield objects:
async *run() {
  yield { progress: 50 }
  yield { progress: 100 }
}
Return c.ok() or c.error() from a streaming handler to attach CTAs:
async *run(c) {
  yield { step: 1 }
  yield { step: 2 }
  return c.ok(undefined, {
    cta: {
      commands: [{ command: 'status', description: 'Check status' }],
    },
  })
}

Command Aliases

Use the alias field to define single-character shortcuts for options:
cli.command('deploy', {
  options: z.object({
    force: z.boolean().optional(),
    dryRun: z.boolean().optional(),
  }),
  alias: { force: 'f', dryRun: 'n' },
  run(c) {
    return { deployed: !c.options.dryRun }
  },
})
$ my-cli deploy -f
deployed: true

$ my-cli deploy -fn
deployed: false
Short aliases can be stacked (e.g. -fn) when all but the last are boolean flags.

Agent Detection

The c.agent boolean is true when stdout is not a TTY (piped or consumed by an agent). Use it to tailor behavior:
cli.command('deploy', {
  args: z.object({
    env: z.enum(['staging', 'production']),
  }),
  run(c) {
    if (!c.agent) {
      console.log(`Deploying to ${c.args.env}...`)
    }
    return { url: `https://${c.args.env}.example.com` }
  },
})

Next Steps

Schemas

Explore Zod schema validation and type inference

Output

Learn about output formats and CTAs

Build docs developers (and LLMs) love