---
title: "CLI"
description: "Every operation in your API becomes a command with typed flags, JSON output, and exit codes, plus login, config, docs, completion, and webhook tooling including the hosted relay."
url: https://typeship.dev/docs/platforms/cli
markdown: https://typeship.dev/docs/platforms/cli.md
section: "Get started"
---
> ## Documentation index
> Fetch the complete documentation index at https://typeship.dev/llms.txt (every page, one line each) or the full text at https://typeship.dev/llms-full.txt.
> Append .md to any docs URL, or send Accept: text/markdown, for the markdown twin of that page.

# CLI

Every operation in your API becomes a command with typed flags, JSON output, and exit codes, plus login, config, docs, completion, and webhook tooling including the hosted relay.

The `cli` platform adds a command-line interface to your TypeScript package. Every operation becomes a command that calls the generated SDK. API commands take flags only, print JSON, and exit with a code, so they run in CI and in scripts without wrappers. The bin is named after your API, so the Acme API ships `acme`.

```bash
acme <resource> <command> [args] [--flags]

acme accounts list --limit 50
acme accounts get acct_123
acme accounts create --name "Operating" --currency usd
```

Path parameters are positional arguments in path order. Everything else is a flag. Subcommands are trimmed of the resource word, so `accounts list` rather than `accounts list-accounts`. The full name stays routable, so nothing you script breaks when the short form appears.

Command names come from the spec's operationIds when they say something the path does not, and are derived otherwise: `list`, `get`, `create`, `update`, `delete`, or the action segment (`charges capture`, `invoices mark-uncollectible`). One item of a sub-collection is singular and the collection plural (`customers create-card`, `customers list-cards`, `customers get-card <customer> <id>`), a singleton is `get`/`update` (`customers get-cash-balance`), and two actions with the same word under one resource are told apart by their parent (`billing deactivate-meters`, `billing deactivate-alerts`), never by a number. An operationId's `ById`/`ByName` tail that just names the positional argument is dropped, so `getPetById` is `pet get <petId>`.

A resource whose name is one of the CLI's own words (`config`, `docs`, `mcp`, `webhooks`, `login`, and the rest listed below) is exposed as `<name>-api`, so `acme config` is always the CLI's config and `acme config-api list` is your API's. Generation reports the rename.

## Flags from the spec

* Body fields, query params, and header params become typed flags named after their wire names: `--name`, `--currency`, `--limit`, `--idempotency-key`, `--cursor`.
* A parameter whose name matches a flag the CLI reads on every API command (`data`, `all`, `select`, `base-url`, `debug`, `validate`, `non-interactive`, `color`, `version`, `help`, and the auth flags such as `token` or `api-key`) is prefixed with where it goes: a body field named `data` is `--body-data`, so `--data` is always the raw body. Two parameters sharing a name across path, query, and body get the same treatment.
* Boolean flags stand alone or take `true`/`false`: `--active`, `--active false`.
* Numbers and booleans are validated. `--limit abc` is a usage error. Enum values are checked against the spec's allowed list, for scalars and for array elements.
* Array fields take a comma list, the flag repeated, or a JSON array, and a single value is always sent as a one-item array: `--tags a,b`, `--tags a --tags b`, `--tags '["a","b"]'`, and `--platforms cli` sends `["cli"]`. Help shows the element type (`string[]`, `available|pending|sold[]`, `object[]`).
* Object fields take JSON: `--metadata '{"team":"core"}'`. Help shows them as `object`; a value that is not a JSON object is a usage error.
* Loosely typed fields (unions, untyped values) show as `json` and accept JSON; a value that is not valid JSON passes through as a plain string.
* `--data '<json>'` supplies a raw JSON body. When the operation also has field flags, `--data` is the base object and field flags are merged on top. Flags win. `--data @body.json` reads the body from a file and `--data -` reads it from stdin, the curl conventions; `--file -` does the same for a binary body.
* For CLIs generated from a GraphQL schema, object-returning operations accept `--select '{ id name }'`, a raw selection set that replaces the default depth-2 selection. Help and `docs` show each command as its root field (`GraphQL query account`), since every call is one POST to the endpoint.

```bash
acme accounts create \
  --data '{"name":"Operating","currency":"usd"}' \
  --name "Ops"
# body sent: { "name": "Ops", "currency": "usd" }
```

Mistyped names get a suggestion. `acme accountz list` answers `Unknown command: accountz. Did you mean 'acme accounts'?` and exits 2. The same applies to subcommands and flags.

Flags for `date` and `date-time` parameters take relative forms as well as ISO 8601: `--created-after -7d` (or `-P7D`, `"7 days ago"`, `today`, `now`) resolves against the clock before the request, so the API sees an absolute value. An unsigned duration (`7d`) is a usage error that names the signed forms. The help table shows `date-time` as the type and names the forms.

## Output and exit codes

Success prints the JSON response on stdout, pretty-printed. Paginated commands print one page plus what fetches the next one: `{ "items": [...], "hasMore": true, "nextPage": { "limit": 2, "cursor": "cur_2" }, "nextCommand": "acme accounts list --limit 2 --cursor cur_2" }`. `nextPage` is the same shape the MCP tool returns; `nextCommand` is ready to run. `--fields id,name,owner.email` keeps only those paths of the result (per item for pages and `--all` streams), which is most of what `jq` gets used for and a token saver for agents.

Every failure is one JSON envelope on stderr whenever stderr is not a terminal (pipes, CI, agents):

```json
{
  "status": "error",
  "issues": [{ "code": "NOT_FOUND", "message": "no such account" }],
  "docs_url": "https://docs.acme.example",
  "next_steps": ["Check the id; list the resource first."],
  "detail": { "status": 404, "error": "NotFoundError", "body": { "code": "not_found", "message": "no such account" } }
}
```

`status` is `error` when the command failed and `action_required` when it stopped on purpose and `next_steps` says what unblocks it (a missing credential, a plan limit, a confirmation). `issues[].code` is stable; branch on it, never on the message. The message is the API's own words when it sent any; `detail` carries the API's body, the SDK error class, and the request id, so nothing is lost.

A person at a terminal gets the same envelope as prose: the message, the next steps, and a dim trailer with the code, the HTTP status, the exit code, and how to get the JSON:

```text
$ acme accounts get acc_404
acme: no such account
  Check the id; list the resource first.
  NOT_FOUND · HTTP 404 · exit 1 · pipe stderr or --mode agent for JSON
```

`--mode human` forces the prose form and `--mode agent` (or `--format json`) the envelope, whatever stderr is.

| Code                                                                   | Exit | When                                                                                 |
| ---------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------ |
| `NO_AUTH`                                                              | 1    | 401 with no credential resolved. `action_required`.                                  |
| `AUTH_INVALID`                                                         | 1    | 401 or 403 with a credential sent.                                                   |
| `PLAN_LIMIT`                                                           | 1    | 402. `action_required`; `next_steps` carries the upgrade URL when the API sends one. |
| `NOT_FOUND`                                                            | 1    | 404.                                                                                 |
| `INVALID_REQUEST`                                                      | 1    | 400, 409, 413, 422: the API rejected the input.                                      |
| `SPEC_INVALID`                                                         | 1    | 422 with typeship's `spec_error` (the typeship CLI).                                 |
| `RATE_LIMITED`                                                         | 1    | 429. `action_required`; `next_steps` says how long to wait.                          |
| `SERVER_ERROR`                                                         | 1    | 5xx.                                                                                 |
| `NETWORK_ERROR`                                                        | 1    | No response: DNS, TLS, refused, timed out.                                           |
| `VALIDATION_FAILED`                                                    | 1    | `--validate` found the body does not match the schema; `detail.violations`.          |
| `CONFIRMATION_REQUIRED`                                                | 2    | A destructive command needs `--force`. `action_required` with the exact command.     |
| `TTY_REQUIRED`                                                         | 2    | A prompt was needed and there is no terminal.                                        |
| `INVALID_USAGE`, `UNKNOWN_COMMAND`, `UNKNOWN_FLAG`, `MISSING_ARGUMENT` | 2    | Wrong flags or arguments.                                                            |

Transport errors still name the request and the deepest cause in the message: `TransportError: GET https://... failed: fetch failed: connect ECONNREFUSED ...`.

Bare invocations (`acme`, `acme accounts`) print help on stderr and exit 2, so a misassembled command in a script still fails and never pollutes stdout. `acme --help`, `acme accounts --help`, `-h`, and the bare word `help` print to stdout and exit 0.

When the spec says an operation is authenticated (`security` at the operation or document level, without an anonymous alternative) and no credential resolves from flags, environment, or `login`, the command stops before sending anything: `NO_AUTH`, `action_required`, `next_steps` naming the env var and `login`. Operations with `security: []` run without one and show `(no auth needed)` in help; `[{}, {...}]` shows `(auth optional)`. A 401 from the API with a credential sent is `AUTH_INVALID`, as before.

## Streaming every page

Paginated commands accept `--all`, which walks every page and streams one item per line (NDJSON) on stdout:

```bash
acme accounts list --all > accounts.ndjson
acme accounts list --all | jq -r '.id'
```

## Help at every level

```bash
acme                          # resources and their commands
acme accounts                 # commands for one resource
acme accounts create --help   # flags, types, required markers, an example
```

Command help is a table sized to its content: the flag, its type (`string`, `number`, `string[]`, `usd|eur`, `object`), a required marker, and the first sentence of the spec's description wrapped to the terminal width; enum lists too long for the type column go on their own line. Pagination and idempotency parameters the engine recognized get a description even when the spec left them blank. Every command ends with an example built from its required inputs. `acme docs accounts create` has the untruncated descriptions.

Root help lists each resource's commands in CRUD order (`list, get, create, update, delete`, then the rest as the spec orders them), wrapped to the terminal; an API with more than 120 operations gets a digest per resource (`list, get, create, … 24 more (acme accounts)`) and the resource's own help has the full list. Root help also lists the auth environment variables, the credentials path, and the setup commands.

## Auth and configuration

Credentials come from environment variables named after your package, with flag overrides. For the `acme` package the prefix is `ACME`:

| Environment variable              | Flag                        | Used when the spec declares               |
| --------------------------------- | --------------------------- | ----------------------------------------- |
| `ACME_TOKEN`                      | `--token`                   | HTTP bearer auth, or OAuth2 access tokens |
| `ACME_API_KEY`                    | `--api-key`                 | A single API key header                   |
| `ACME_USERNAME` / `ACME_PASSWORD` | `--username` / `--password` | HTTP basic auth                           |
| `ACME_BASE_URL`                   | `--base-url`                | Always available                          |

With several API key schemes, each key gets its own variable and flag named after the header or query parameter. Precedence at request time is flags, then environment variables, then credentials saved by `login`.

The base URL resolves as `--base-url`, then `ACME_BASE_URL`, then the stored `base-url`, then the stored `environment`, then the spec's default server. If none exists the command exits 2 with `No base URL. Pass --base-url, set ACME_BASE_URL, or run 'acme config set base-url <url>'.`

## Version

`--version`, `-v`, or the bare word `version` prints the package version, the API version it was generated from, and the generator:

```text
$ acme --version
acme 2.3.0 (Acme API 2.3.0, generated by typeship)
```

## login, logout, whoami

Instead of exporting variables every session, credentials can be stored once. `login` saves them to `~/.config/acme/credentials.json` with mode 600, honoring `XDG_CONFIG_HOME`. `logout` deletes the file.

```bash
acme login                                # hidden prompt on a TTY
acme login --token <value>                # any auth flag works
cat token.txt | acme login --with-token   # stdin, for scripts
```

When the API offers browser approval (an endpoint pair the project configures; typeship's own API does), `acme login` with no flags opens a page where the person approves, and the API mints a key for this machine that `login` stores. Nothing is pasted. Under an agent, or with `--no-browser`, the CLI prints the approval URL (as a JSON event on stderr too) and polls until the person decides; the key never crosses the conversation. The request is PKCE-shaped: the CLI keeps a verifier and the API hands the key only to the poll that proves it.

`login` is otherwise the one command that prompts, and only on a TTY. When the spec declares OAuth2 and the CLI knows a client id (set in the console, passed with `--client-id`, or in `ACME_CLIENT_ID`), `login` runs the RFC 8628 device flow. It prints a code and a URL, polls until you approve in the browser, and refreshes the token automatically a minute before it expires. Scopes and an audience can be configured in the console for authorization servers that gate refresh tokens behind `offline_access` or need an audience for API-valid tokens.

`whoami` calls your API's identity endpoint with whatever credentials would be used, so it answers "which account is this machine acting as". Generation detects endpoints like `GET /me` automatically. Project settings can pin a specific `resource.method`. If the API has no such endpoint, `whoami` reports where credentials would come from without a network call, and exits 1 when there are none.

## config

Stored defaults live next to credentials in `~/.config/acme/config.json`, so the base URL does not need repeating on every command. When the spec declares several servers, environments can be pinned by name:

```bash
acme config set base-url https://api.staging.example.com
acme config set environment sandbox     # names come from the spec's servers
acme config set docs-url https://docs.acme.example
acme config list
acme config get base-url
acme config unset base-url
acme config path
```

Flags always win over stored config, so CI overrides need no cleanup.

## mcp

When the package was generated with the `mcp` platform, the CLI wires the server into agent clients:

```bash
acme mcp                         # print the server entry as JSON, and which clients were detected
acme mcp install --all           # write it into every agent client found on this machine
acme mcp install --claude        # Claude Code (./.mcp.json)
acme mcp install --codex         # Codex CLI (~/.codex/config.toml)
acme mcp install --vscode        # VS Code (./.vscode/mcp.json)
acme mcp install --windsurf | --gemini | --opencode | --zed | --claude-desktop
acme mcp --url https://typeship.dev/mcp/<slug> --claude   # a remote endpoint instead
acme mcp install --claude --read-only                     # a server that cannot write
```

`--read-only` registers the local server with its `--read-only` flag, or points a hosted entry at `<url>/readonly`; either way writes are not callable. Existing entries in the client config are preserved. The entry is the API's hosted MCP endpoint when the project has one (with the auth env var written as a reference, `${ACME_TOKEN}`, never a literal key), else the package's local stdio server, which reads the credentials `login` saves. `--all` skips Cursor until Cursor speaks MCP 2026-07-28; `--cursor` writes it on request with a note. See [MCP server](https://typeship.dev/docs/platforms/mcp).

## init

One command connects a machine, or a repository, to the API:

```bash
acme init --all -k <credential>
```

It stores the credential (or notes the env var already set), installs the API's skills when a repository is configured (`npx skills add owner/skills`), writes the MCP entry into every detected agent client, and upserts a marked `acme` block into `./AGENTS.md` (or `CLAUDE.md` when only that exists, or when running under Claude Code) with the auth rules, the discovery commands, and a compact index of every command. Idempotent: run it again and the block is replaced in place. `--no-skills`, `--no-mcp`, `--no-agents-md` skip a part; the report on stdout says what happened to each.

## agent-guide, auth check, doctor, help --json

For the agent holding only the binary:

```bash
acme agent-guide --format json   # conventions, first command, docs index, hosted MCP, skills, next steps
acme help --json                 # every resource, command, positional, and flag, as data
acme auth check [--live]         # which credential the CLI would send (and, with --live, whether the API accepts it)
acme doctor                      # node, credentials, base URL, docs, MCP client config, with next_steps
```

All four print JSON. `auth check` and `doctor` exit 1 when something is `action_required`.

## webhooks listen

_Available on Pro and Enterprise._

Local webhook development without tunnels. Developers integrating your webhooks need events to reach a handler on their laptop. Tunnels work, but they expose a port, need an account somewhere, and drop events while disconnected. Turn on **webhook relay** under the CLI platform in [project settings](https://typeship.dev/docs/projects#platforms), which needs the CLI on and Pro, and the next generation bakes the relay coordinates into your CLI, which gains a `webhooks listen` command that needs none of that:

```text
$ acme webhooks listen --forward-to localhost:3000/webhooks
Ready! Forwarding relayed events to http://localhost:3000/webhooks
Point this API's webhook endpoint at: https://typeship.dev/relay/rls_... (^C to quit)
200  account.created [1] (43ms)
```

1. `listen` mints a session with its own unguessable ingest URL. Two developers on the same project never see each other's events.
2. The developer registers that URL as a webhook endpoint in your API, the same way they would register any endpoint.
3. Events sent there are buffered and replayed to the local handler with their **original headers and body**. Nothing is re-signed, so signature verification with the generated SDK's `webhooks.unwrap` works unchanged.
4. `--events account.created,account.closed` filters by the payload's type field.

If the connection drops, events keep buffering and replay on reconnect within the same run.

Relayed payloads are transient. Events are deleted 24 hours after they arrive, and sessions are removed after 48 hours idle. Bodies are capped at 1MB, sessions at 5,000 events, and the ingest URL is rate limited per IP. Pair it with `webhooks fake`, below, to cover the whole local loop before any real event exists.

## webhooks fake

When the spec declares webhooks, `webhooks fake <event>` sends (or prints) a signed sample event with Standard Webhooks headers, so handlers are testable before any real event exists:

```bash
acme webhooks fake                        # list the declared events
acme webhooks fake account.updated        # print headers and payload
acme webhooks fake account.updated --forward-to localhost:3000/webhooks
```

The signing key is `--key`, then `ACME_WEBHOOK_KEY`, then a throwaway key. See [Webhooks](https://typeship.dev/docs/guides/webhooks).

## docs

The reference and the guides, in the terminal. `docs` alone prints the API overview. `docs <resource> <command>` is the full operation reference with untruncated descriptions, richer than `--help`.

```bash
acme docs accounts create
acme docs search "idempotency"
acme docs read quickstart
acme docs --web
```

Guides come from your documentation site through the `llms.txt` convention, which most docs hosts publish automatically. Set the site in the console, or with `config set docs-url`. Your spec's `externalDocs` URL is the default. `search` covers both the reference and the guides. `read` prints a guide page. Pages cache locally for an hour, and nothing is fetched unless a docs command runs. The same two layers back the MCP server's `search_docs` and `read_docs` tools.

## completion

Tab completion for resources, commands, every flag, and the values a flag takes (enum values, `true`/`false`, `--color on|off|auto`), plus `config` keys and `mcp install` targets, generated from the same operation table as the CLI:

```bash
# ~/.bashrc
eval "$(acme completion bash)"

# ~/.zshrc
eval "$(acme completion zsh)"

# fish
acme completion fish > ~/.config/fish/completions/acme.fish
```

## upgrade

When the package is published to an npm registry, the CLI can update itself. `upgrade` compares the running version against the registry's latest and installs it with `npm install -g`. `upgrade --check` reports without installing. A custom registry is honored through `npm_config_registry`. If the package is not published, `upgrade` says so. A binary that was installed with pnpm, bun, or yarn, or is running through `npx`, is not upgraded in place: `upgrade` names the right command for that manager instead. Vendored packages update by regeneration, not registries.

By default the CLI never contacts a registry on its own. You can opt into an update notice in the console. Then a once-a-day check caches the latest version and later runs print a one-line hint on stderr. The check has a 1.5 second timeout, and `--non-interactive` silences it.

## feedback

When you configure a support URL in the console, `feedback` opens it in a browser. GitHub issue URLs arrive with the CLI and API version, Node version, and platform prefilled in the body, visible before anything is submitted. Nothing is sent on its own.

## Color

Help text and stderr hints use light color on a TTY. JSON output is never colored, so parsers and agents see identical bytes either way. `--color on|off|auto` overrides detection, and `NO_COLOR` is honored. An explicit `--color on` wins over everything, for terminals that hide their TTY.

## Debug output

`--debug` (or `ACME_DEBUG=1`) prints one line per HTTP attempt to stderr: method, path, status, duration, request id. stdout stays pure JSON, so piping is unaffected. Debug lines never include headers or bodies.

## Runtime validation

`--validate` schema-checks request and response bodies against the spec with the SDK's zero-dependency validator. Violations exit 1 with a `ValidationError` payload listing each JSON path. It is the fastest way to see whether an API has drifted from its published spec.

## Agent mode

The CLI knows when an agent is driving it: `--mode agent`, `ACME_MODE=agent`, or no terminal on stdin and stdout. In agent mode nothing prompts and no browser opens (`docs --web` and `feedback` print the URL instead), and every stop is an `action_required` envelope with `next_steps` the agent can follow. `--mode human` forces the interactive behavior back on.

Destructive commands (every `DELETE`) need `--force` (or `--yes`, `-y`, or `ACME_YES=1`). A person at a terminal is asked; an agent gets `CONFIRMATION_REQUIRED` with the exact command to run once the user has agreed. Nothing is deleted on a guess.

Every request carries a `User-Agent` of `<package>-cli/<version> (typeship; harness=<name>; agent)`: the harness when one is detected (Claude Code, Cursor, Codex, Gemini CLI, and others, by their environment variables), `agent` when a harness is detected or `--mode agent` is set, and `non-interactive` for a bare run without a terminal, so a CI job is not counted as an agent. No secrets ride in it.

`--out <dir>` writes a file-shaped response (an array of `{path, content}` objects) into a directory and prints the rest of the response with a summary, for APIs that return generated or exported files.

## Non-interactive mode

`--non-interactive` (or `ACME_NON_INTERACTIVE=1`) is the explicit switch: `login` skips the device flow and the prompt and fails fast with `TTY_REQUIRED` unless the credential arrives through a flag or `--with-token`, and the update notice stays silent. Agent mode implies everything non-interactive mode does except that `login` still runs the device flow, printing the URL and code as JSON for the agent to relay.

## In CI

Credentials come from the environment, output is JSON, and failures are exit codes. Build the package in the job, then run the compiled bin with a secret:

```yaml title=".github/workflows/report.yml (step excerpt)"
- name: Build the SDK package
  working-directory: vendor/acme
  run: npm install && npm run build

- name: Export account ids
  env:
    ACME_TOKEN: ${{ secrets.ACME_TOKEN }}
  run: |
    node vendor/acme/dist/cli.js accounts list --all \
      | jq -r '.id' > account-ids.txt
```

## Uploads and event streams

Operations with multipart bodies take their binary fields as file paths: `acme uploads avatar --file ./me.png --label "me"` reads the file and sends it as a form part with the other fields alongside. An operation whose whole body is binary takes `--file <path>` for the body. Operations that stream server-sent events print one JSON line per event (`{ "event", "id", "data" }`) until the stream ends, the same shape `--all` uses for pages, so `acme events stream | jq` works. These operations are SDK and CLI only; the MCP server does not expose them as tools.

## Sitemap

[Every page of these docs](https://typeship.dev/llms.txt)
