Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions fern/products/cli-generator/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,60 @@ export MY_TOKEN=tok-456
my-cli users list
```

## Named profiles

For APIs where every call carries a tenant identifier (an account SID, an org slug, a workspace), a profile stores it once instead of it being typed on every command. A profile is a named bundle of request context resolved once per invocation: a credential, parameter defaults, [server URL variables](/learn/cli-generator/get-started/features#server-url-variables), an optional base URL, a [retry limit](/learn/cli-generator/get-started/features#retries-with-backoff), and a default output format. Nothing else is accepted; an unknown key is rejected at write time rather than stored and ignored.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 [vale] <FernStyles.Acronyms> reported by reviewdog 🐶
'SID' has no definition.


Profiles are off by default. Enable them with the [`profiles` config option](/learn/cli-generator/get-started/configuration#config-options) in `generators.yml`, which adds a `profiles` command group and a global `--profile` / `-p` flag to the generated CLI. A CLI generated without the option is unchanged.

```yaml title="generators.yml"
config:
binaryName: acme
profiles:
enabled: true
```

### Manage profiles

```bash
acme profiles create prod --set AccountSid=AC11… --with-token # token read from stdin, stored in the OS keychain
acme profiles create sub --parent prod --set AccountSid=AC99… # subaccount that shares prod's credential
acme profiles set prod ACME_REGION=us1 ACME_RETRIES=3 # add or change values by env var name
acme profiles use sub # make it the active profile

acme messages list # no --account-sid needed
acme messages list -p prod # one command against another tenant; the active profile is unchanged

acme profiles list # every profile, with the active one marked
acme profiles current # the profile in effect and how it was selected
acme profiles show staging # one profile's resolved config without selecting it
acme profiles remove sub # also deletes that profile's stored credential
```

`profiles create` accepts `--set <name>=<value>` for API parameters (validated against the spec, so a typo is rejected), `--server-var <name>=<value>` or `--<name> <value>` for server URL variables, `--base-url`, `--retries <N>`, `--default-format`, `--parent <name>`, and `--with-token` to read a credential from stdin. When several auth schemes are declared, `--scheme` names the one the credential belongs to.

`profiles set <name> KEY=VALUE ...` takes any environment variable the CLI reads (a credential such as `ACME_AUTH_TOKEN`, or a setting such as `ACME_RETRIES`, `ACME_BASE_URL`, `ACME_OUTPUT`, or `ACME_<SERVER_VAR>`) or an API parameter by its spec name, and creates the profile if it doesn't exist. Keys are validated before anything is written, and an unrecognized key fails with a suggestion. `auth login --from-env` captures the credential from the CLI's own environment variables into the active profile.

### Select a profile

A profile is selected by, in order, `--profile` / `-p`, the `<PREFIX>_PROFILE` environment variable, then the profile marked active by `profiles use`, where `<PREFIX>` is the binary name uppercased with `-` replaced by `_`. With no profile selected, the CLI behaves as if the feature were absent. A named profile that doesn't exist is an error rather than a fallthrough to environment credentials, so a command never silently runs against the wrong tenant. The `profiles` group itself always runs unprofiled, so a stale active pointer can be repaired.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 [vale] <FernStyles.Adverbs> reported by reviewdog 🐶
Remove 'silently' if it's not important to the meaning of the statement.

Suggested change
A profile is selected by, in order, `--profile` / `-p`, the `<PREFIX>_PROFILE` environment variable, then the profile marked active by `profiles use`, where `<PREFIX>` is the binary name uppercased with `-` replaced by `_`. With no profile selected, the CLI behaves as if the feature were absent. A named profile that doesn't exist is an error rather than a fallthrough to environment credentials, so a command never silently runs against the wrong tenant. The `profiles` group itself always runs unprofiled, so a stale active pointer can be repaired.
A profile is selected by, in order, `--profile` / `-p`, the `<PREFIX>_PROFILE` environment variable, then the profile marked active by `profiles use`, where `<PREFIX>` is the binary name uppercased with `-` replaced by `_`. With no profile selected, the CLI behaves as if the feature were absent. A named profile that doesn't exist is an error rather than a fallthrough to environment credentials, so a command never runs against the wrong tenant. The `profiles` group itself always runs unprofiled, so a stale active pointer can be repaired.


Scripts and agents should pass `-p` per invocation: it mutates no global state, so parallel invocations can't race.

```bash
for tenant in prod sub; do acme messages list -p "$tenant" --format json; done
```

### Precedence

Each value resolves as `explicit flag → environment variable → profile → spec default`. Environment variables sit above the active profile, so a CI pipeline that exports `ACME_API_KEY` or `ACME_BASE_URL` is never overridden by a profile a developer stored on the same machine. A profile named explicitly with `-p` is itself an explicit choice for that invocation, so its values outrank the environment. Retries resolve as `--retries` → `<PREFIX>_RETRIES` → profile → `x-fern-retries`, and `--retries 0` is equivalent to `--no-retry`. `profiles current` reports when an environment variable is overriding a stored credential.

### Storage and inheritance

Non-secret settings live in `~/.config/<bin>/profiles.toml`. Secrets are never written to that file: credentials go to the OS keychain under a profile-scoped account, the same store `auth login` uses.

`--parent <name>` sets single-level inheritance. A child inherits its parent's credential, base URL, server variables, parameter defaults (child wins per key), and retries. It doesn't inherit the default output format, since output shape belongs to the invocation rather than the tenant. Because the parent is resolved at read time, editing it propagates to its children.

## Help output

Every generated CLI includes a dynamically rendered `Authentication:` section in its `--help` output listing every scheme, the expected env var or flag, and whether a credential is detected.
26 changes: 26 additions & 0 deletions fern/products/cli-generator/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,32 @@ config:
```
</ParamField>

<ParamField path="profiles" type="object" toc={true} required={false}>
Adds [named profiles](/learn/cli-generator/get-started/authentication#named-profiles) to the generated CLI: a top-level `profiles` subcommand group, a global `--profile` / `-p` flag, and a `<binaryName>_PROFILE` environment variable. A profile stores a tenant's credential, parameter defaults, server URL variables, base URL, retry limit, and default output format once, so users of multi-tenant APIs don't pass an account identifier on every command.

Off by default. A CLI generated without this block is byte-identical to one generated before the option existed, and a `profiles` block with `enabled` unset or `false` is a no-op.

```yaml title="generators.yml"
config:
binaryName: acme
profiles:
enabled: true
```
</ParamField>
<Indent>
<ParamField path="profiles.enabled" type="boolean" default="false" required={false}>
Emits the profiles surface. Enabling it adds a top-level subcommand group and a global flag to an existing CLI, which is a surface change for its users.
</ParamField>

<ParamField path="profiles.commandName" type="string" default="profiles" required={false}>
Name of the top-level subcommand group. Must start with a lowercase letter and contain only `[a-z0-9-]`, and can't be one of the built-in groups (`auth`, `completion`, `man`, `errors`, `generate-skills`, `help`). Rename it (for example, to `tenants` or `accounts`) when that reads better for your API. If your spec already declares a `profiles` resource, the built-in subcommands are folded into that group rather than colliding with it, so renaming is optional.
</ParamField>

<ParamField path="profiles.revokeOperation" type="string" required={false}>
Dotted command path of an operation that revokes a profile's remote credential, such as `iam.keys.remove`. When set, `profiles remove` gains a `--revoke` flag that calls the operation before deleting the profile locally. The operation is invoked with the profile's stored parameters as its arguments, so any parameter it requires must be set on the profile. When omitted, the flag isn't registered.
</ParamField>
</Indent>

<ParamField path="packageIdentity" type="object" toc={true} required={false}>
Sets the Cargo `[package]` metadata of the generated crate. Use `packageIdentity` to publish the crate under your own name, license, repository, and authors instead of Fern's.

Expand Down
6 changes: 4 additions & 2 deletions fern/products/cli-generator/features.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Failed requests are retried automatically with exponential backoff on status cod
| Backoff factor | 2x |
| Jitter | 10% |

GET, HEAD, OPTIONS, DELETE, and PUT retry by default. POST and PATCH retry only when the operation declares server-side idempotency support with [`x-fern-idempotent: true`](/learn/api-definitions/openapi/extensions/idempotency), which also exposes an `--idempotency-key` flag on that command, or when the caller passes `--idempotency-key` explicitly. Pass `--no-retry` to disable retries for a single invocation.
GET, HEAD, OPTIONS, DELETE, and PUT retry by default. POST and PATCH retry only when the operation declares server-side idempotency support with [`x-fern-idempotent: true`](/learn/api-definitions/openapi/extensions/idempotency), which also exposes an `--idempotency-key` flag on that command, or when the caller passes `--idempotency-key` explicitly. Pass `--no-retry` to disable retries for a single invocation, or `--retries <N>` to set the number of retries after the first attempt (`--retries 0` equals `--no-retry`). The limit resolves as `--retries` → `<PREFIX>_RETRIES` → [profile](/learn/cli-generator/get-started/authentication#named-profiles) → `x-fern-retries`.

An `Idempotency-Key` header is generated automatically for POST, PUT, and PATCH requests unless the invocation carries its own `--idempotency-key` or the operation opts out with `x-fern-cli-idempotency: false`. The same generated key is sent on every attempt of a request, but the key alone doesn't make a request retry-eligible.

Expand Down Expand Up @@ -151,7 +151,7 @@ contoso plants list --schema
}
```

`input.properties` is keyed by the spec's parameter names, and each property's `flag` gives the exact flag to pass. Properties without a `flag` are passed through `--params` or `--json`. Operations that support [`--page-all`](#pagination), return binary bodies, or stream also report `paginable`, `binaryResponse`, or `streaming`.
`input.properties` is keyed by the spec's parameter names, and each property's `flag` gives the exact flag to pass. Properties without a `flag` are passed through `--params` or `--json`. Operations that support [`--page-all`](#pagination), return binary bodies, or stream also report `paginable`, `binaryResponse`, or `streaming`. The root schema lists the CLI's built-in command groups (`auth`, `completion`, `man`, and `profiles` when [enabled](/learn/cli-generator/get-started/authentication#named-profiles)) under `builtinCommands`, separate from the spec-derived operations.

## Server URL variables

Expand All @@ -166,6 +166,8 @@ export BIGCOMMERCE_STORE_HASH=abc123
bigcommerce v3 customers list
```

The environment variable is `<PREFIX>_<VARIABLE>`, where `<PREFIX>` is the binary name uppercased with `-` replaced by `_`. Variables declared with `x-fern-sdk-variables` use their bare name instead (`gardenId` reads `GARDEN_ID`). A [named profile](/learn/cli-generator/get-started/authentication#named-profiles) can also store the variable; the flag wins over the environment variable, which wins over the profile.

## File uploads and downloads

For endpoints with `format: binary` request bodies, pass a file path as the `--file` argument. For binary responses, use `--output <PATH>` to save the response body to a file.
Expand Down
9 changes: 5 additions & 4 deletions fern/products/cli-generator/state-of-the-world.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Fern generates a command-line interface from the same API definition that alread

- Naming, nesting, auth, output shape, errors, and exit codes come from one runtime, so consistency holds by construction.
- Bearer, API key, HTTP Basic, and OAuth: client credentials plus interactive `auth login` with Proof Key for Code Exchange (PKCE) and device-code flows, tokens stored in the OS keyring, and `auth status` to show which source is active.
- Named profiles, enabled with `profiles.enabled: true`: a `profiles` command group and a global `-p` flag store a tenant's credential, parameter defaults, server variables, base URL, retries, and default output format once. Secrets stay in the OS keyring; environment variables still outrank the active profile, so CI is never overridden by a stored profile.
- `--debug` dumps the fully built request and the response for generated commands and for custom commands that call the co-generated SDK.
- Retries with backoff and jitter: always on 408 and 429, and on 5xx when the request is safe to replay; automatic idempotency keys on mutating requests; `--dry-run` prints the exact request without sending it; inputs validated before any request is built.

Expand Down Expand Up @@ -95,10 +96,10 @@ Fern generates a command-line interface from the same API definition that alread
| In active development | Next | Later |
| --- | --- | --- |
| **Richer error display**: 401 and 422 hints, links into docs | **`serve`**: the same surface over MCP | **Grouped interactive prompts** for missing parameters |
| **Agent-readiness scoring** of generated CLIs | **Multiple credential profiles** | **Interactive explore view** for nested responses |
| | **CLI snippets** in the docs site and API Explorer | **Infrastructure-as-code mode**: state file, plan/diff, apply |
| | **`docs` command**: documentation from inside the CLI | **Per-endpoint default output format** |
| | **Multi-scheme auth refinements** | **Webhook listener**: `webhooks listen` for local development |
| **Agent-readiness scoring** of generated CLIs | **CLI snippets** in the docs site and API Explorer | **Interactive explore view** for nested responses |
| | **`docs` command**: documentation from inside the CLI | **Infrastructure-as-code mode**: state file, plan/diff, apply |
| | **Multi-scheme auth refinements** | **Per-endpoint default output format** |
| | | **Webhook listener**: `webhooks listen` for local development |
| | | **GraphQL generation** on the existing runtime support |

## How generation runs
Expand Down
Loading