Skip to main content
Mount any HTTP server with an OpenAPI specification as typed CLI commands. incur automatically generates subcommands with arguments, options, and descriptions extracted from your spec.

Basic Usage

Pass an OpenAPI spec alongside the fetch property to generate typed subcommands:
import { Cli } from 'incur'
import { app, spec } from './my-hono-openapi-app.js'

Cli.create('my-cli', { description: 'My CLI' })
  .command('api', { fetch: app.fetch, openapi: spec })
  .serve()
$ my-cli api --help
# Commands:
#   listUsers    List users
#   createUser   Create a user
#   getUser      Get a user by ID

Generated Command Structure

incur parses your OpenAPI spec and generates commands with:
  • Operation ID as command nameoperationId becomes the command name
  • Path parameters as arguments — path params like {id} become positional arguments
  • Query parameters as options — query params become --flag options
  • Request body as options — JSON body properties become --flag options
  • Descriptions from specsummary and description fields flow through to help text

Example Commands

# GET /users?limit=5
$ my-cli api listUsers --limit 5
# → users: ...

# GET /users/42
$ my-cli api getUser 42
# → id: 42
# → name: Alice

# POST /users with JSON body {"name":"Bob"}
$ my-cli api createUser --name Bob
# → created: true
# → name: Bob

Without OpenAPI

You can also mount a fetch handler without a spec. All argv tokens are interpreted as path segments and curl-style flags:
import { Cli } from 'incur'
import { Hono } from 'hono'

const app = new Hono()
  .get('/users', (c) => c.json({ users: [{ id: 1, name: 'Alice' }] }))
  .post('/users', async (c) => c.json({ created: true, ...(await c.req.json()) }, 201))

Cli.create('my-cli', {
  description: 'My CLI',
  fetch: app.fetch,
}).serve()
$ my-cli api users
# → users:
# →   - id: 1
# →     name: Alice

$ my-cli api users -X POST -d '{"name":"Bob"}'
# → created: true
# → name: Bob

Curl-style Flags

When using fetch without OpenAPI, incur supports:
FlagDescription
-X, --method <METHOD>HTTP method (default: GET, POST if body present)
-H, --header "Key: Val"Set a request header (repeatable)
-d, --data <json>Request body (implies POST)
--body <json>Request body (implies POST)
--<key> <value>Query string parameter

Mounting as Commands

You can mount fetch handlers onto specific commands instead of the root:
import { Cli } from 'incur'
import { Hono } from 'hono'

const app = new Hono()
  .get('/users', (c) => c.json({ users: [{ id: 1, name: 'Alice' }] }))
  .post('/users', async (c) => c.json({ created: true, ...(await c.req.json()) }, 201))

Cli.create('my-cli', { description: 'My CLI' })
  .command('users', { fetch: app.fetch })
  .serve()
$ my-cli users api users
# → users:
# →   - id: 1
# →     name: Alice

Base Path

Use basePath to prefix all requests:
Cli.create('my-cli', { description: 'My CLI' })
  .command('api', {
    fetch: app.fetch,
    openapi: spec,
    basePath: '/v1',
  })
  .serve()

Framework Compatibility

incur works with any framework that exposes a Web Fetch API handler:
// Hono
fetch: app.fetch

// Bun
fetch: bunApp.fetch

// Deno
fetch: denoApp.fetch

// Elysia
fetch: elysiaApp.fetch

Type Safety

incur resolves all $ref pointers in your OpenAPI spec and converts JSON Schema types to Zod schemas with automatic coercion for path and query parameters (which arrive as strings from argv).
// OpenAPI: path parameter {id} with type: number
// → becomes: z.coerce.number()

// OpenAPI: query parameter ?active with type: boolean
// → becomes: z.coerce.boolean().optional()

Error Handling

HTTP errors are automatically converted to incur error envelopes:
$ my-cli api getUser 999
# Error (HTTP_404): User not found
If the response body contains a message field, it’s used as the error message. Otherwise, the entire body or a generic HTTP {status} message is shown.

Build docs developers (and LLMs) love