---
name: openapi-to-bruno
description: >
  Convert an OpenAPI specification (YAML/JSON) to a Bruno collection ("collection as code", .bru files).
  Use when the user says /openapi-to-bruno, 'convert openapi to bruno', 'bruno collection from openapi',
  'openapi vers bruno', 'genere une collection bruno', 'importe les specs dans bruno', 'export bruno',
  or needs to create a Bruno collection from an API spec. Bruno is the open-source alternative to Postman.
allowed-tools:
  - Bash
  - Read
  - Glob
  - Grep
  - AskUserQuestion
---

Convert an OpenAPI/Swagger specification to a Bruno collection — a directory of `.bru` files Bruno opens directly (no API import).

Automatically replaces the base URL and recurring path parameters with Bruno environment variables, configures collection-level authentication (OAuth2 authorization-code + PKCE, native to Bruno) from the spec's security schemes, and writes companion environment files under `environments/`.

Two on-disk formats (both fully supported by Bruno 3.x):
- **`yaml`** (default) — OpenCollection YAML, Bruno 3.1+'s native format: `opencollection.yml` + per-request `<Name>.yml` + `folder.yml` + `environments/<Name>.yml`.
- **`bru`** — legacy Bruno DSL: `bruno.json` + `collection.bru` + `<Request>.bru` + `environments/<Name>.bru`. Use for Bruno ≤ 3.0.

## Required tools

- `jq`
- `bruno`

Run `pysae-ai-tools tools require <tools>` before starting; if it exits non-zero, abort and report the install command from stderr to the user.

## Step 1 — Locate the OpenAPI spec

If the user doesn't provide a spec path, check two sources:

1. **Pysae API specs** — resolve them live from the api config:
   ```bash
   pysae-ai-tools project show --project pysae/api --json openapi
   ```
   Build a spec URL as `<base_url[env]><spec_path>` with `{version}`/`{visibility}` substituted (e.g. `https://api.pysae.com/api/docs/v4/internal/openapi.json`). Prefer `internal` visibility (all endpoints). Ask the user which version and environment they want.
2. **Local files** — search the current project:

```bash
find . -maxdepth 3 \( -name "openapi*.yaml" -o -name "openapi*.yml" -o -name "openapi*.json" -o -name "swagger*.yaml" -o -name "swagger*.yml" -o -name "swagger*.json" \) 2>/dev/null | head -10
```

## Step 2 — Convert

The command handles conversion, auth setup, path-parameter variables and multi-environment generation in one shot, writing a Bruno collection directory:

```bash
pysae-ai-tools openapi to-bruno <SPEC_PATH> \
  --output <OUTPUT_DIR> \
  --name <COLLECTION_NAME> \
  --env-name <PRIMARY_ENV_NAME> \
  --env-prefix <PREFIX> \
  --env-default VAR=VALUE \
  --extra-base-url "Name=URL"
```

**Arguments:**
- `<SPEC_PATH>` — Local file or URL
- `--output <dir>` — Output Bruno collection directory (default: `<spec-name>/`)
- `--format <yaml|bru>` — On-disk format (default: `yaml`, Bruno 3.1+ native; use `bru` for Bruno ≤ 3.0)
- `--name <name>` — Collection name (default: derived from spec title)
- `--env-name <name>` — Primary environment name (default: collection name)
- `--env-prefix <prefix>` — Prefix for all environment variable names (e.g. `PYSAE` → `PYSAE_BASE_URL`, `PYSAE_GROUP_ID`)
- `--env-default VAR=VALUE` — Set default values for environment variables, applied to **all** environments (can be repeated)
- `--env-var ENV:VAR=VALUE` — Set a variable for **one environment only**, overriding the global default — e.g. a prod tenant's OAuth2 client id / authorize / token URLs, which differ from dev (can be repeated)
- `--extra-base-url "Name=URL"` — Generate additional environments with different base URLs (append `,oidc=<url>` to give that env its own OIDC discovery URL; can be repeated)
- `--min-occurrences <n>` — Minimum occurrences for a path param to become an env var (default: 2)
- `--auth-type <type>` — Collection auth: `auto`, `oauth2`, `bearer`, `apikey`, `basic`, `none` (default: `auto`)
- `--oidc-url <url>` — Override the OIDC discovery URL for the primary environment
- `--login-endpoint <path>` — Add the auto-login cascade pre-request script (e.g. `/api/v4/login`)
- `--login-token-field <field>` — JSON field in the login response holding the token (default: `session_id`)
- `--system-browser` / `--no-system-browser` — **On by default.** OAuth2 through the **system browser**: sets the collection callback to the `bruno://app/oauth2/callback` deep link and flips Bruno's `useSystemBrowser` preference on (kept consistent). The deep-link callback is already allowed on the Pysae CLI Auth0 client (dev + prod). `--no-system-browser` reverts to the built-in browser with a `localhost` callback.
- `--register` / `--no-register` — **On by default.** After generating, add the collection to Bruno's **active workspace** (`ui-state-snapshot.json` → `activeWorkspacePath`, falling back to the default workspace) so it shows up in the sidebar without a manual *Open Collection*; Bruno reloads live. `--no-register` only writes the files.
- `--workspace <dir>` — Register into a specific workspace directory instead of the active one.

Both integration steps are **best-effort**: if Bruno is not installed, the files are still written and the app steps are skipped with a note.

The command:
1. Installs `openapi-to-postmanv2` CLI if missing (used as the OpenAPI parser), downloads the spec if it's a URL.
2. Extracts the base URL from the spec `servers` field → `{{PREFIX_BASE_URL}}`.
3. Detects security schemes → collection-level `auth` in `collection.bru` (OAuth2 = authorization-code **with `pkce: true`**, native Bruno flow).
4. Detects path parameters used in ≥ 2 endpoints → `{{PREFIX_PARAM_NAME}}` (declared in each request's `params:path`).
5. With `--login-endpoint`, adds a **Bruno-native** cascade pre-request script (`bru`/`req`, not `pm`): API key → login → OAuth2.
6. Writes `bruno.json`, `collection.bru`, the folder/`.bru` request tree, and `environments/<Name>.bru` (one per environment; secrets listed name-only under `vars:secret`).

Output is a directory — there is **no API import**. By default the collection is registered in Bruno's active workspace, so it appears in the sidebar with no further action. With `--no-register`, the user opens it manually: **Bruno → Open Collection → select `<OUTPUT_DIR>`** (the collection directory itself, not a parent folder). Then pick the environment (top-right) and **Get Access Token** on the collection Auth tab for the OAuth2 flow.

A collection only shows up in Bruno once it is declared in the active workspace's `workspace.yml` (`collections:` list) — Bruno does not scan parent folders. Registration writes that entry (idempotent); Bruno watches `workspace.yml` and reloads the sidebar live.

### Auth cascade & OAuth2

The cascade (with `--login-endpoint`) gives three ways to authenticate, first configured wins:
1. `PREFIX_AUTH_API_KEY` → `Authorization: Api-Key <key>` (set the variable; simplest).
2. `PREFIX_AUTH_EMAIL` + `PREFIX_AUTH_PASSWORD` → login at the endpoint, cached Bearer token.
3. otherwise → the collection's OAuth2 (Auth0). Click **Get Access Token** in Bruno.

The collection uses **Authorization Code with PKCE** (`pkce: true`) because the Auth0 app is a public/native client (no secret) — without PKCE Auth0 rejects the token exchange with "access_denied / Unauthorized". `auto_fetch_token` is off so the API-key / login levels never collide with a silently-fetched OAuth2 token. `PREFIX_AUTH_URL` must be the `authorize_url` (it already carries `?audience=…`; Auth0 needs the audience on `/authorize`).

**Callback / browser choice.** By default the callback is `http://localhost:8765/callback` and Bruno's **built-in browser intercepts the redirect internally** (it never navigates to the URL, so no local server is needed) — this only needs the URL in the Auth0 app's Allowed Callback URLs (localhost:8765-8767 already are). With `--system-browser`, the flow opens in the user's real browser and returns via the `bruno://app/oauth2/callback` deep link (custom scheme routed back into Bruno by the OS); the command sets that callback and enables Bruno's `useSystemBrowser` preference. The deep-link callback is registered on the Pysae CLI Auth0 client (dev + prod). Use the built-in browser (default) for zero setup; use `--system-browser` to reuse the user's existing SSO session.

### Example: Pysae API — one Bruno collection per version

Bruno environments live **inside** the collection directory, so generate one collection directory per API version (each gets its own `environments/`). Use the **dev spec** (`api.dev.pysae.com`) as source — it may contain endpoints not yet on prod.

The dev and prod Auth0 tenants are **distinct** (different domain, `client_id` and audience), so the prod environment needs its own OAuth2 variables via `--env-var 'Pysae Prod:…'` — otherwise selecting "Pysae Prod" would still authenticate against dev and the API rejects the dev token with `NO_ROLE`. Review and Local run on the dev tenant, so they inherit the dev defaults.

```bash
CFG=$(pysae-ai-tools pysae api auth oauth-config --env dev)
PROD=$(pysae-ai-tools pysae api auth oauth-config --env prod)
COMMON="--env-prefix PYSAE \
  --env-name 'Pysae Dev' \
  --auth-type oauth2 \
  --login-endpoint /api/v4/login \
  --env-default PYSAE_GROUP_ID=pysae \
  --env-default PYSAE_BASE_URL=$(echo "$CFG" | jq -r .api_base) \
  --env-default PYSAE_AUTH_URL=$(echo "$CFG" | jq -r .authorize_url) \
  --env-default PYSAE_AUTH_TOKEN_URL=$(echo "$CFG" | jq -r .token_endpoint) \
  --env-default PYSAE_AUTH_CLIENT_ID=$(echo "$CFG" | jq -r .client_id) \
  --env-default PYSAE_AUTH_SCOPE=$(echo "$CFG" | jq -r .scope) \
  --extra-base-url 'Pysae Prod=https://api.pysae.com' \
  --extra-base-url 'Pysae Review=https://api.review.pysae.com' \
  --extra-base-url 'Pysae Local=http://127.0.0.1:8000' \
  --env-var Pysae\ Prod:PYSAE_AUTH_URL=$(echo "$PROD" | jq -r .authorize_url) \
  --env-var Pysae\ Prod:PYSAE_AUTH_TOKEN_URL=$(echo "$PROD" | jq -r .token_endpoint) \
  --env-var Pysae\ Prod:PYSAE_AUTH_CLIENT_ID=$(echo "$PROD" | jq -r .client_id) \
  --env-var Pysae\ Prod:PYSAE_AUTH_SCOPE=$(echo "$PROD" | jq -r .scope)"

pysae-ai-tools openapi to-bruno \
  "https://api.dev.pysae.com/api/docs/v4/internal/openapi.json" \
  --output "Pysae Internal API v4" --name "Pysae Internal API v4" $COMMON
```

Repeat for v3 and v5 into their own directories (`--output "Pysae Internal API v3"`, etc.). Only `PYSAE_BASE_URL` differs between Dev / Review / Local (all on the dev tenant); the Prod environment additionally carries its own tenant's OAuth2 vars. `PYSAE_AUTH_API_KEY` / `PYSAE_AUTH_EMAIL` / `PYSAE_AUTH_PASSWORD` are created empty for the user to fill. Switch environments in Bruno's top-right selector; after switching to Prod, re-run **Get Access Token** so Bruno mints a prod-tenant token.

## Step 3 — Report

Tell the user:
- Output collection directory (and its `environments/` files)
- Number of folders and requests generated
- Auth scheme configured and the variable names to fill
- How to open it in Bruno and authenticate
