Skip to main content

Overview

Incur uses Zod schemas to validate inputs and outputs. Schemas enable:
  • Automatic validation — inputs are parsed before run() executes
  • Type inference — TypeScript infers types from schemas with zero manual annotations
  • Error reporting — validation errors include field-level details
  • Schema composition — reuse schemas across commands

Importing Zod

Incur re-exports Zod, so you can import z directly from incur:
import { Cli, z } from 'incur'

Arguments Schema

The args schema defines positional arguments. Keys define the order arguments are parsed from the command line.
cli.command('copy', {
  args: z.object({
    source: z.string().describe('Source file'),
    dest: z.string().describe('Destination file'),
  }),
  run(c) {
    // c.args.source and c.args.dest are typed as string
    return { copied: `${c.args.source}${c.args.dest}` }
  },
})
$ my-cli copy file.txt backup.txt
copied: file.txt backup.txt

Optional Arguments

Use .optional() to make an argument optional:
args: z.object({
  package: z.string().optional().describe('Package name'),
})

Default Values

Use .default() to provide a fallback value:
args: z.object({
  env: z.enum(['staging', 'production']).default('staging'),
})

Options Schema

The options schema defines named flags. Options are parsed from --name value or --name (for booleans).
cli.command('deploy', {
  args: z.object({
    env: z.enum(['staging', 'production']),
  }),
  options: z.object({
    force: z.boolean().optional().describe('Overwrite existing deployment'),
    dryRun: z.boolean().default(false).describe('Preview changes without deploying'),
  }),
  run(c) {
    // c.options.force: boolean | undefined
    // c.options.dryRun: boolean
    return { deployed: !c.options.dryRun }
  },
})
$ my-cli deploy staging --force
deployed: true

$ my-cli deploy production --dry-run
deployed: false

Boolean Options

Boolean options are flags that don’t require a value:
options: z.object({
  verbose: z.boolean().default(false),
})
$ my-cli run --verbose
Use --no- prefix to negate:
$ my-cli run --no-verbose

String Options

String options require a value:
options: z.object({
  output: z.string().describe('Output file path'),
})
$ my-cli build --output dist/bundle.js

Number Options

Number options are automatically coerced:
options: z.object({
  port: z.number().default(3000).describe('Server port'),
})
$ my-cli serve --port 8080

Enum Options

Use z.enum() for a fixed set of values:
options: z.object({
  format: z.enum(['json', 'yaml', 'toml']).default('json'),
})
$ my-cli export --format yaml

Array Options

Use z.array() to collect multiple values:
options: z.object({
  include: z.array(z.string()).describe('Files to include'),
})
$ my-cli build --include src/a.ts --include src/b.ts

Kebab-Case to camelCase

Incur automatically maps kebab-case flags to camelCase schema keys:
options: z.object({
  saveDev: z.boolean().optional(),
})
$ my-cli install --save-dev
# Parsed as c.options.saveDev = true

Environment Variables Schema

The env schema validates environment variables. Keys are the variable names (e.g. NPM_TOKEN).
cli.command('deploy', {
  args: z.object({
    env: z.enum(['staging', 'production']),
  }),
  env: z.object({
    DEPLOY_TOKEN: z.string(),
    API_BASE: z.string().default('https://api.example.com'),
  }),
  run(c) {
    // c.env.DEPLOY_TOKEN: string
    // c.env.API_BASE: string
    return { url: c.env.API_BASE, token: c.env.DEPLOY_TOKEN }
  },
})
If DEPLOY_TOKEN is not set, incur raises a validation error before run() executes.

Output Schema

The output schema validates the return value from run(). This ensures the command always returns data in the expected shape.
cli.command('deploy', {
  args: z.object({
    env: z.enum(['staging', 'production']),
  }),
  output: z.object({
    url: z.string(),
    duration: z.number(),
  }),
  run(c) {
    // Return value must match output schema
    return {
      url: `https://${c.args.env}.example.com`,
      duration: 3.2,
    }
  },
})
If the return value doesn’t match, incur raises a validation error.

Type Inference

Incur infers types from schemas automatically. No manual type annotations are required.
cli.command('greet', {
  args: z.object({
    name: z.string(),
  }),
  options: z.object({
    loud: z.boolean().default(false),
  }),
  output: z.object({
    message: z.string(),
  }),
  run(c) {
    c.args.name      // inferred as: string
    c.options.loud   // inferred as: boolean
    
    return {
      message: `hello ${c.args.name}`,  // inferred as: string
    }
  },
})
Type errors are caught at compile time:
run(c) {
  return { message: 123 }  // ❌ Type error: expected string
}

Schema Composition

Reuse schemas across commands:
import { Cli, z } from 'incur'

// Shared schemas
const envArg = z.object({
  env: z.enum(['staging', 'production']).describe('Target environment'),
})

const deployEnv = z.object({
  DEPLOY_TOKEN: z.string(),
})

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

cli
  .command('deploy', {
    args: envArg,
    env: deployEnv,
    run(c) {
      return { deployed: c.args.env }
    },
  })
  .command('rollback', {
    args: envArg,
    env: deployEnv,
    run(c) {
      return { rolledBack: c.args.env }
    },
  })
  .serve()

Schema Extensions

Extend schemas with .merge() or .extend():
const baseOptions = z.object({
  verbose: z.boolean().default(false),
})

const deployOptions = baseOptions.extend({
  force: z.boolean().optional(),
})

cli.command('deploy', {
  options: deployOptions,
  run(c) {
    // c.options.verbose: boolean
    // c.options.force: boolean | undefined
    return { deployed: true }
  },
})

Validation Errors

When validation fails, incur provides detailed field-level errors:
$ my-cli deploy
Error: missing required argument <env>
See below for usage.

my-cli deploy Deploy to an environment

Usage: my-cli deploy <env>

Arguments:
  env  Target environment
In non-TTY mode (agents), errors include machine-readable field details:
{
  "ok": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Required",
    "fieldErrors": [
      {
        "path": "env",
        "expected": "string",
        "received": "undefined",
        "message": "Required"
      }
    ]
  }
}

Descriptions

Use .describe() to add human-readable descriptions shown in help output:
args: z.object({
  name: z.string().describe('Name to greet'),
}),
options: z.object({
  loud: z.boolean().default(false).describe('Use uppercase'),
})
$ my-cli greet --help

Usage: my-cli greet <name>

Arguments:
  name  Name to greet

Options:
  --loud  Use uppercase

Deprecated Options

Mark options as deprecated with .meta({ deprecated: true }):
options: z.object({
  zone: z.string().optional().describe('Availability zone').meta({ deprecated: true }),
  region: z.string().optional().describe('Target region'),
})
Deprecated flags show [deprecated] in --help and emit a warning when used:
$ my-cli deploy --zone us-east-1
Warning: --zone is deprecated

Next Steps

Output

Learn about output formats and CTAs

Zod Documentation

Explore Zod’s full schema API

Build docs developers (and LLMs) love