Register composable before/after hooks with cli.use(). Middleware executes in registration order, onion-style—each calls await next() to proceed to the next middleware or the command handler.
Basic Middleware
Use middleware for cross-cutting concerns like timing, logging, or auth:
import { Cli } from 'incur'
const cli = Cli.create('deploy-cli', { description: 'Deploy tools' })
.use(async (c, next) => {
const start = Date.now()
await next()
console.log(`took ${Date.now() - start}ms`)
})
.command('deploy', {
run() {
return { deployed: true }
},
})
cli.serve()
Output
$ deploy-cli deploy
deployed: true
took 12ms
Middleware Context
Middleware receives a context object with metadata:
import { Cli } from 'incur'
Cli.create('my-cli', { description: 'My CLI' })
.use(async (c, next) => {
console.log(`Running: ${c.command}`)
console.log(`CLI: ${c.name}`)
console.log(`Agent: ${c.agent}`)
await next()
})
.command('deploy', {
run() {
return { deployed: true }
},
})
.serve()
Output
$ my-cli deploy
Running: deploy
CLI: my-cli
Agent: false
deployed: true
Context Properties
| Property | Type | Description |
|---|
c.name | string | The CLI name |
c.command | string | The resolved command path |
c.version | string | undefined | CLI version |
c.agent | boolean | Whether stdout is not a TTY |
c.env | object | Parsed environment variables |
c.var | object | Variables set by upstream middleware |
c.set(key, value) | function | Set a typed variable |
c.error(opts) | function | Return an error result |
Authentication Middleware
Short-circuit the chain by returning early:
import { Cli, middleware, 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>() }),
})
const requireAuth = middleware<typeof cli.vars>((c, next) => {
if (!c.var.user) {
return c.error({
code: 'AUTH',
message: 'must be logged in'
})
}
return next()
})
cli.command('deploy', {
middleware: [requireAuth],
run() {
return { deployed: true }
},
})
cli.serve()
Output
$ my-cli deploy
Error (AUTH): must be logged in
Dependency Injection with Variables
Declare a vars schema to enable typed variables. Middleware sets them with c.set(), and both middleware and command handlers read them via c.var:
import { Cli, z } from 'incur'
type User = { id: string; name: string }
async function authenticate(): Promise<User> {
return { id: 'u_123', name: 'Alice' }
}
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
}
},
})
cli.serve()
Output
$ my-cli whoami
user:
id: u_123
name: Alice
requestId: 550e8400-e29b-41d4-a716-446655440000
debug: true
Default Values
Use .default() for variables that don’t need middleware:
const cli = Cli.create('my-cli', {
vars: z.object({
debug: z.boolean().default(true),
user: z.custom<User>(),
}),
})
cli.command('test', {
run(c) {
// c.var.debug is always available
// c.var.user must be set by middleware
return { debug: c.var.debug }
},
})
Per-Command Middleware
Run middleware only for specific commands:
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],
run() {
return { deployed: true }
},
})
.command('delete', {
middleware: [requireAuth, requireAdmin],
run() {
return { deleted: true }
},
})
.command('status', {
// No auth required
run() {
return { status: 'ok' }
},
})
cli.serve()
Output
$ my-cli deploy
Error (AUTH): must be logged in
$ my-cli status
status: ok
Per-command middleware runs after root and group middleware.
Error Handling
Middleware can handle errors from downstream middleware or commands:
import { Cli } from 'incur'
Cli.create('my-cli', { description: 'My CLI' })
.use(async (c, next) => {
try {
await next()
} catch (error) {
console.error('Caught error:', error)
throw error // Re-throw or handle
}
})
.command('fail', {
run() {
throw new Error('Something went wrong')
},
})
.serve()
Structured Errors
Return structured errors with error codes:
import { Cli, middleware, z } from 'incur'
const requireAuth = middleware((c, next) => {
if (!authenticated()) {
return c.error({
code: 'AUTH',
message: 'must be logged in',
retryable: false,
})
}
return next()
})
Output
$ my-cli deploy
Error (AUTH): must be logged in
Structured errors show up in the output envelope with code and retryable flag.
Throwing Errors
Throwing also works and produces an UNKNOWN error code:
const requireAdmin = middleware<typeof cli.vars>((c, next) => {
if (!c.var.user?.admin) throw new Error('admin required')
return next()
})
Typed Middleware
Use the middleware() helper for full type safety:
import { middleware, z } from 'incur'
type User = { id: string; name: string }
const vars = z.object({
user: z.custom<User>(),
requestId: z.string(),
})
const env = z.object({
API_KEY: z.string(),
})
// Pass vars and env schemas as generics
const myMiddleware = middleware<typeof vars, typeof env>((c, next) => {
// c.var is typed as { user: User, requestId: string }
// c.set() is typed to accept only valid keys
// c.env is typed as { API_KEY: string }
c.set('requestId', crypto.randomUUID())
return next()
})
Environment Variables
Access parsed environment variables in middleware:
import { Cli, middleware, z } from 'incur'
const cli = Cli.create('my-cli', {
description: 'My CLI',
env: z.object({
API_KEY: z.string(),
DEBUG: z.boolean().default(false),
}),
})
const logEnv = middleware<typeof cli.vars, typeof cli.env>((c, next) => {
console.log('API_KEY:', c.env.API_KEY)
console.log('DEBUG:', c.env.DEBUG)
return next()
})
cli.use(logEnv)
Environment variables are validated once at startup before any middleware runs.
Middleware Order
Middleware executes in an onion pattern:
import { Cli } from 'incur'
Cli.create('my-cli', { description: 'My CLI' })
.use(async (c, next) => {
console.log('1: before')
await next()
console.log('1: after')
})
.use(async (c, next) => {
console.log('2: before')
await next()
console.log('2: after')
})
.command('test', {
run() {
console.log('command')
return { ok: true }
},
})
.serve()
Output
$ my-cli test
1: before
2: before
command
2: after
1: after
ok: true
Execution Order
- Root CLI middleware (in registration order)
- Group CLI middleware (if mounted)
- Per-command middleware (if defined)
- Command
run handler
Source Code
The middleware implementation is in src/middleware.ts:
export type Handler<
vars extends z.ZodObject<any> | undefined = undefined,
env extends z.ZodObject<any> | undefined = undefined,
> = (context: Context<vars, env>, next: () => Promise<void>) => Promise<void> | void
export default function middleware<
const vars extends z.ZodObject<any> | undefined = undefined,
const env extends z.ZodObject<any> | undefined = undefined,
>(handler: Handler<vars, env>): Handler<vars, env> {
return handler
}
Use middleware for cross-cutting concerns like auth, logging, and timing. Use variables (c.var and c.set()) for type-safe dependency injection.