# InsurUp CLI

A comprehensive, automation-friendly command-line interface for the
[InsurUp](https://insurup.com) insurance platform, built on
[`@insurup/sdk`](https://www.npmjs.com/package/@insurup/sdk) and
[Bun](https://bun.sh).

- **Two auth flows** — machine-to-machine (client credentials) and browser login
  (authorization code + PKCE).
- **Secure by default** — tokens and the M2M secret are stored in the OS keychain
  (`Bun.secrets`: macOS Keychain, libsecret, Windows Credential Manager).
- **All 18 SDK modules** — customers, vehicles, properties, policies, proposals,
  cases, agents, agent users/branches/roles/setup, OAuth clients, insurance,
  coverage, templates, languages, files, webhooks.
- **Scriptable** — `--json` on every command, stable exit codes, env-var auth,
  reads request bodies from flags / files / stdin.

## Install

The CLI is distributed three ways:

```bash
# 1. Run via Bun (no install) — requires Bun >= 1.3
bunx @insurup/cli --help

# 2. Standalone binary (no runtime required) — from GitHub Releases
#    macOS/Linux/Windows builds are attached to each release.
curl -L https://github.com/InsurUp/cli/releases/latest/download/insurup-darwin-arm64 -o insurup
chmod +x insurup && ./insurup --version

# 3. Homebrew
brew install InsurUp/tap/cli
```

> The npm package runs on **Bun** (it uses `Bun.secrets`/`Bun.serve`). For
> environments without Bun, use the standalone binary or Homebrew.

## Quick start

```bash
# Browser login (authorization code + PKCE) — uses the configured public client
insurup auth login

# …or machine-to-machine (great for CI)
insurup auth login --m2m --client-id <id> --client-secret <secret> --save

# Who am I?
insurup auth whoami

# Do things
insurup customers list --json
insurup customers get <id>
insurup insurance companies
```

## Authentication

### Browser (interactive)

```bash
insurup auth login
```

Uses InsurUp's public `cli` client by default — no client secret required. Opens
your browser via a Pushed Authorization Request (RFC 9126), captures the redirect
on a local loopback server, and stores the resulting tokens (with refresh) in the
OS keychain. Use `--no-browser` on a headless box to print the URL instead.

To use a different public OAuth client for browser login:

```bash
insurup config set browser-client-id <public-client-id>
```

### Machine-to-machine (automation)

```bash
# Interactive once: prompts for the secret, caches it in the keychain, and
# writes the client id + settings to the profile (see --save below):
insurup auth login --m2m --client-id <id> --save

# Fully stateless (CI) — nothing is stored, login happens per invocation:
export INSURUP_CLIENT_ID=...
export INSURUP_CLIENT_SECRET=...
insurup customers list --json
```

`--save` is **M2M-only**: it caches the client secret in the keychain and writes
the client id, auth server, and scopes to the active profile, so later logins
(and on-demand M2M auto-login) need no flags. It does not apply to browser login
(passing it there is rejected) — browser tokens are always stored in the keychain
regardless. Configure the public browser client separately with
`config set browser-client-id`.

### Commands

| Command | Description |
| --- | --- |
| `auth login [--m2m] [--no-browser] [--save]` | Log in (browser or M2M) |
| `auth status` | Show session status (no network) |
| `auth whoami` | Show the authenticated identity |
| `auth token` | Print a valid access token (`$(insurup auth token)`) |
| `auth logout [--forget-secret]` | Clear the stored session |
| `config show` | Show effective local CLI config |
| `config set base-url <url>` | Persist an API base URL to the active profile |
| `config set auth-server <url>` | Persist an authorization server URL to the active profile |
| `config set browser-client-id <id>` | Persist the public browser OAuth client id to the active profile |
| `config unset base-url` | Remove the profile API base URL |
| `config unset auth-server` | Remove the profile authorization server URL |
| `config unset browser-client-id` | Remove the profile public browser client id |

### Configuration & profiles

Settings resolve with precedence **flags → environment → config file → defaults**.
Use `--profile <name>` (or `INSURUP_PROFILE`) to keep multiple environments side
by side. Non-secret settings live in `~/.config/insurup/config.json`; secrets only
ever live in the OS keychain. Environment variables can also be supplied via a
`.env` file in the working directory — the Bun runtime auto-loads it.

```bash
insurup config set base-url https://api.insurup.com
insurup config set auth-server https://auth.insurup.com
insurup config set browser-client-id cli
insurup config get base-url
insurup config get auth-server
insurup config get browser-client-id
```

| Env var | Meaning |
| --- | --- |
| `INSURUP_CLIENT_ID` | M2M (confidential) client id |
| `INSURUP_BROWSER_CLIENT_ID` | Public browser-login client id (default `cli`) |
| `INSURUP_CLIENT_SECRET` | M2M client secret |
| `INSURUP_AUTH_SERVER` | Authorization server (default `https://auth.insurup.com`) |
| `INSURUP_API_URL` | API base URL |
| `INSURUP_SCOPES` | Space/comma-separated scopes |
| `INSURUP_PROFILE` | Active profile |
| `INSURUP_TOKEN_ENDPOINT` / `INSURUP_AUTHORIZATION_ENDPOINT` | Explicit endpoints (skip OIDC discovery) |

## Usage

Commands follow `insurup <module> <verb> [args] [flags]`. Reads map to SDK
getters; writes take a body via `--data`:

```bash
# Inline JSON, a file, or stdin
insurup customers create --data '{"type":"INDIVIDUAL","fullName":"Jane Doe"}'
insurup customers create --data @customer.json
echo '{"type":"INDIVIDUAL"}' | insurup customers create --data -

# Live proposal updates over SignalR (until Ctrl-C)
insurup proposals watch <proposal-id> --json
```

### Listing, search & pagination

GraphQL-backed lists are cursor-paginated, searchable, and render each item as a
key/value block (use `--json` for machine output):

```bash
# Page size (default 20, max 100). Interactive in a TTY: prints a page, then
# asks "Show more?" and follows the cursor until you decline.
insurup customers list --first 50

# Free-text search across the entity's searchable fields (server-side).
insurup customers list --search "jane"

# Non-interactive / piped / --json prints one page; the next cursor is hinted
# on stderr so scripts can page manually with --after.
insurup policies list --json
insurup policies list --first 20 --after <cursor> --json
```

GraphQL lists with pagination + `--search`: `customers list` · `policies list` ·
`proposals list` · `cases list` · `agent-users list` · `policies transfers list` ·
`policies file-transfers list` · `webhooks deliveries list`. Other modules'
`list` verbs (e.g. `languages`, `webhooks`, `agent-branches`) return plain
collections without search/pagination.

> `--search` matches across each entity's searchable fields (server-side).
> Verified end-to-end against `@insurup/sdk` ≥ 0.1.31 (earlier 0.1.30 returned
> HTTP 400 on four of these lists and errored on `cases` search — fixed upstream).

### Modules

`customers` · `vehicles` · `properties` · `policies` · `proposals` · `cases` ·
`agents` · `agent-branches` · `agent-roles` · `agent-setup` · `agent-users` ·
`oauth-clients` · `insurance` · `coverage` · `templates` · `languages` · `files` ·
`webhooks`

Run `insurup <module> --help` to see each module's verbs.

## Automation

- `--json` — machine-readable output on stdout (human status/errors go to stderr).
- `--quiet` — suppress non-essential output. `--no-color` / `NO_COLOR` disable color.
- **Exit codes:** `0` ok · `1` generic · `2` usage/validation · `3` auth ·
  `4` not found · `5` API/server error.

```bash
if insurup customers get "$ID" --json > customer.json; then
  jq .fullName customer.json
else
  case $? in 3) echo "login first";; 4) echo "no such customer";; esac
fi
```

## Development

```bash
bun install
bun run dev -- --help        # run from source
bun run typecheck
bun run lint
bun test                     # unit + integration
bun test --coverage          # enforces 90% line/function floors (bunfig.toml)
bun run compile              # standalone binary -> dist/insurup
```

Tests use Bun's runner with an in-memory keychain and a mock OAuth + API server
(`Bun.serve`), so nothing touches the real keychain or production API.

## Deployment

Releases are **draft-driven** (via [Release Drafter](https://github.com/release-drafter/release-drafter)):

1. **CI** (`ci.yml`) runs lint, typecheck, coverage, and a compile smoke check on every push/PR.
2. **Release Drafter** (`release-drafter.yml`) keeps a **draft GitHub Release** up to
   date as PRs merge — the changelog and next version are derived from PR **labels**
   (`feature`/`enhancement` → minor, `bug`/`fix`/`chore`/… → patch, `breaking` → major).
3. **Publishing the draft** (in the GitHub Releases UI) creates the tag and triggers
   `publish.yml`, which:
   - bumps `package.json` to the release version and commits it back to `main`,
   - **publishes `@insurup/cli` to npm** (`NPM_TOKEN`),
   - cross-compiles macOS/Linux/Windows binaries + `SHA256SUMS` and attaches them, and
   - updates the **`InsurUp/homebrew-tap`** tap formula via
     `packaging/homebrew/update-tap.sh` (when `HOMEBREW_TAP_TOKEN` is set).

To cut a release: review the draft at **Releases → Draft**, then click **Publish**.

> Required secret: `NPM_TOKEN`. Optional: `HOMEBREW_TAP_TOKEN` (a PAT with push
> access to the tap). macOS binaries are unsigned by default; add
> codesign/notarization to `publish.yml` to avoid Gatekeeper prompts.

## License

[MIT](LICENSE)
