Skip to main content
Declare a vars schema on Cli.create() to enable typed variables. Middleware sets them with c.set(), and both middleware and command handlers read them via c.var.

Basic Usage

import { Cli, z } from 'incur'

type User = { id: string; name: string }

const cli = Cli.create('my-cli', {
  description: 'My CLI',
  vars: z.object({
    user: z.custom<User>(),
    requestId: z.string(),
    debug: z.boolean().default(true),
  }),
})

cli.use(async (c, next) => {
  c.set('user', await authenticate())
  c.set('requestId', crypto.randomUUID())
  await next()
})

cli.command('whoami', {
  run(c) {
    return { user: c.var.user, requestId: c.var.requestId, debug: c.var.debug }
  },
})
$ my-cli whoami
# → user:
# →   id: u_123
# →   name: Alice
# → requestId: 550e8400-e29b-41d4-a716-446655440000
# → debug: true

How It Works

  1. Declare the schema — pass vars to Cli.create() with a Zod object schema
  2. Set values in middleware — call c.set(key, value) to populate variables
  3. Read in handlers — access c.var.key in middleware or command handlers

Variable Defaults

Use .default() for variables that don’t need middleware:
const cli = Cli.create('my-cli', {
  description: 'My CLI',
  vars: z.object({
    debug: z.boolean().default(false),
    timeout: z.number().default(30_000),
  }),
})

cli.command('run', {
  run(c) {
    // debug and timeout are available without middleware
    return { debug: c.var.debug, timeout: c.var.timeout }
  },
})

Middleware Example

Authentication

import { Cli, middleware, z } from 'incur'

type User = { id: string; name: string; admin: boolean }

const cli = Cli.create('my-cli', {
  description: 'My CLI',
  vars: z.object({ user: z.custom<User>() }),
})

const requireAuth = middleware<typeof cli.vars>((c, next) => {
  if (!c.var.user) return c.error({ code: 'AUTH', message: 'must be logged in' })
  return next()
})

const requireAdmin = middleware<typeof cli.vars>((c, next) => {
  if (!c.var.user?.admin) throw new Error('admin required')
  return next()
})

cli.command('deploy', {
  middleware: [requireAuth, requireAdmin],
  run() {
    return { deployed: true }
  },
})
$ my-cli deploy
# Error (AUTH): must be logged in

Request Context

const cli = Cli.create('my-cli', {
  description: 'My CLI',
  vars: z.object({
    requestId: z.string(),
    startTime: z.number(),
  }),
})

cli.use(async (c, next) => {
  c.set('requestId', crypto.randomUUID())
  c.set('startTime', Date.now())
  await next()
})

cli.command('run', {
  run(c) {
    const duration = Date.now() - c.var.startTime
    return { requestId: c.var.requestId, duration }
  },
})

Type Inference

The vars schema flows through generics to middleware and command handlers:
const cli = Cli.create('my-cli', {
  vars: z.object({
    user: z.custom<User>(),
    requestId: z.string(),
  }),
})

cli.use((c, next) => {
  c.var.user // Type: User | undefined
  c.var.requestId // Type: string | undefined
  c.set('user', { id: 'u_123', name: 'Alice' })
  c.set('requestId', crypto.randomUUID())
  return next()
})

cli.command('whoami', {
  run(c) {
    c.var.user // Type: User | undefined
    c.var.requestId // Type: string | undefined
    return { user: c.var.user }
  },
})

Middleware Type Hints

For external middleware, use the middleware helper with type parameters:
import { middleware, z } from 'incur'

type User = { id: string; name: string }

const vars = z.object({ user: z.custom<User>() })

const auth = middleware<typeof vars>((c, next) => {
  c.var.user // Type: User | undefined
  c.set('user', await authenticate())
  return next()
})

const cli = Cli.create('my-cli', { vars })
cli.use(auth)

Environment + Variables

Combine CLI-level env with vars for environment variables plus middleware state:
const cli = Cli.create('my-cli', {
  description: 'My CLI',
  env: z.object({
    API_TOKEN: z.string(),
  }),
  vars: z.object({
    user: z.custom<User>(),
  }),
})

cli.use(async (c, next) => {
  // CLI-level env is available in middleware
  const token = c.env.API_TOKEN
  const user = await fetchUser(token)
  c.set('user', user)
  await next()
})

cli.command('whoami', {
  run(c) {
    // Both env and var are available
    return { user: c.var.user, hasToken: !!c.env.API_TOKEN }
  },
})

Typing External Middleware

For middleware defined outside your CLI, pass both vars and env as type parameters:
import { middleware, z } from 'incur'

type User = { id: string; name: string }

const vars = z.object({ user: z.custom<User>() })
const env = z.object({ API_TOKEN: z.string() })

const auth = middleware<typeof vars, typeof env>((c, next) => {
  c.var.user // Type: User | undefined
  c.env.API_TOKEN // Type: string
  return next()
})

Per-Command Middleware

Per-command middleware inherits the CLI’s vars schema:
const cli = Cli.create('my-cli', {
  vars: z.object({ user: z.custom<User>() }),
})

const requireAdmin = middleware<typeof cli.vars>((c, next) => {
  if (!c.var.user?.admin) throw new Error('admin required')
  return next()
})

cli.command('deploy', {
  middleware: [requireAdmin],
  run() {
    return { deployed: true }
  },
})

Best Practices

Do

  • Use vars for state set by middleware (user, session, etc.)
  • Use .default() for static configuration (timeouts, flags)
  • Use z.custom<T>() for complex types
  • Type external middleware with middleware<typeof cli.vars, typeof cli.env>()

Don’t

  • Don’t use vars for arguments or options — use args and options schemas
  • Don’t use vars for environment variables — use env schema
  • Don’t mutate c.var directly — always use c.set()

Build docs developers (and LLMs) love