بنقرة واحدة
gen-api-cli
// Generate or update CLI commands in cmd/api/ from the OpenAPI spec and Go SDK. Use when the API spec changes or new endpoints are added.
// Generate or update CLI commands in cmd/api/ from the OpenAPI spec and Go SDK. Use when the API spec changes or new endpoints are added.
Write or review UX microcopy for dashboard interfaces. Use when reviewing button labels, errors, empty states, dialogs, helper text, tooltips, toasts, onboarding, or any user-facing product copy.
Structural refactoring pass on changed code. Use after implementing a feature to improve code structure, reduce duplication, and clean up APIs without changing behavior.
Self-review your own work before committing. Fight entropy, ensure quality, leave the codebase better than you found it.
Use this skill when writing, editing, reviewing, or improving documentation in this repository. Activates for tasks involving content in `docs/product/` or `docs/engineering/`, MDX/Markdown files, or any documentation-related request.
Reviews restate handler code in svc/ctrl/worker to find restate client calls (service calls, state access, sleep, etc.) incorrectly placed inside restate.Run, restate.RunVoid, or restate.RunAsync closures. Use when reviewing restate handlers, checking restate.Run usage, or auditing worker service code.
| name | gen-api-cli |
| description | Generate or update CLI commands in cmd/api/ from the OpenAPI spec and Go SDK. Use when the API spec changes or new endpoints are added. |
| disable-model-invocation | true |
| allowed-tools | ["Read","Write","Edit","Glob","Grep","Bash"] |
| argument-hint | [group-name or 'all'] |
You generate and maintain CLI command files in cmd/api/ that wrap the Unkey Go SDK.
Each API endpoint from the OpenAPI spec becomes a CLI subcommand.
Modes (determined by the argument passed after /gen-api-cli):
keys, apis): generate/update that group onlyall or no argument: generate/update all groupscheck: audit mode — do NOT write any files. Instead, compare all existing commands against the OpenAPI spec and report discrepanciesWhen the argument is check:
cmd/api/Before generating anything, update the Go SDK to the latest version:
go get github.com/unkeyed/sdks/api/go/v2@latest
go mod tidy
svc/api/openapi/openapi-generated.yaml — the source of truth for all endpoints.go.mod under github.com/unkeyed/sdks/api/go/v2, then read the SDK source from the Go module cache at ~/go/pkg/mod/github.com/unkeyed/sdks/api/go/v2@<version>/. You MUST read the actual SDK files to verify struct names, field names, and field types. Do not guess.cmd/api/util/ (shared helpers — do NOT modify), cmd/api/root.go, and any existing subpackages to understand the current state.Parse all paths from the OpenAPI spec. They look like /v2/{group}.{action}.
Group them by resource:
apis — endpoints matching apis.*keys — endpoints matching keys.*identities — endpoints matching identities.*permissions — endpoints matching permissions.*ratelimit — endpoints matching ratelimit.*analytics — endpoints matching analytics.*Skip deploy.* (already handled by cmd/deploy) and liveness (GET health check, not useful as CLI).
Each group is a subpackage under cmd/api/. Each command is its own file:
cmd/api/
├── root.go # Cmd, imports and registers group subpackages
├── util/
│ ├── client.go # CreateClient, APIAction
│ ├── output.go # Output
│ ├── errors.go # FormatError
│ └── flags.go # RootKeyFlag, APIURLFlag, ConfigFlag, OutputFlag
├── apis/
│ ├── root.go # Cmd (group command), registers leaf commands
│ ├── create_api.go
│ ├── delete_api.go
│ └── ...
├── keys/
│ ├── root.go
│ ├── create_key.go
│ ├── verify_key.go
│ └── ...
package {group}
import "github.com/unkeyed/unkey/pkg/cli"
var Cmd = &cli.Command{
Name: "{group}",
Usage: "...",
Description: "...",
Commands: []*cli.Command{
{leafCmd1},
{leafCmd2},
},
}
Each leaf command lives in its own file named with the kebab-case action using underscores: create_api.go, verify_key.go, list_overrides.go.
The variable name is unexported: createAPICmd, verifyKeyCmd, listOverridesCmd.
After creating a new group subpackage, import it in cmd/api/root.go and add {group}.Cmd to the Commands slice. Only add entries that don't already exist.
Every group root.go must append util.Disclaimer to its Description:
Description: "Create, read, and delete API namespaces." + util.Disclaimer,
Leaf commands must also append util.Disclaimer to their Description, after the docs link:
Description: `...
For full documentation, see https://www.unkey.com/docs/api-reference/v2/...` + util.Disclaimer,
Command name: Convert the action part of the operationId to kebab-case.
createApi → create-apilistKeys → list-keysverifyKey → verify-keyaddPermissions → add-permissionslimit → limitmultiLimit → multi-limitgetVerifications → get-verificationsFile name: kebab-case action with underscores: create_api.go, verify_key.go
Variable name: camelCase with SDK acronym rules: createAPICmd, verifyKeyCmd
The SDK applies Go naming conventions with acronym uppercasing:
Operation ID {group}.{action} maps to:
apis → Apis, ratelimit → RatelimitcreateApi → CreateAPI, verifyKey → VerifyKeyAcronym rules — these substrings get uppercased when followed by uppercase or end-of-string:
Api → API (but Apis stays Apis)Id → ID (but Identity stays Identity)Url → URLType names:
$ref — e.g., V2ApisCreateApiRequestBody → V2ApisCreateAPIRequestBodyV2ApisCreateAPIResponseBodyCRITICAL: Always verify type and field names by reading the actual SDK source files in the Go module cache. grep for the type name to confirm. If a name is wrong, the code won't compile.
This is extremely important. Descriptions must be copied from the OpenAPI spec as closely as possible.
Usage (short one-liner)Use the first sentence of the OpenAPI path description field.
Description (full help text)Copy the entire OpenAPI path description field verbatim, with these adjustments:
**text** → text, `code` → codeRequired permissions:
- api.*.create_api
- api.<api_id>.create_api
Examples field instead (see below)https://www.unkey.com/docs/api-reference/v2/{group}/{slug} but the slugs don't always match the operation ID (e.g., keys.createKey → /v2/keys/create-api-key). Always verify the URL exists. Format as:
For full documentation, see https://www.unkey.com/docs/api-reference/v2/keys/create-api-key
ExamplesUse the Examples field ([]string) on the Command struct. Each entry is one example invocation.
These are rendered in a separate EXAMPLES section at the bottom of --help output.
Always use --flag=value syntax (not --flag value) in examples for clarity.
Use a short, one-sentence summary of the OpenAPI property description. Since every command links to full docs, flag descriptions should be concise — just enough to know what the flag does. Do NOT copy the full multi-sentence OpenAPI description.
Every leaf command MUST include these four flags first:
util.RootKeyFlag(),
util.APIURLFlag(),
util.ConfigFlag(),
util.OutputFlag(),
Then map OpenAPI request body properties to CLI flags:
| OpenAPI type | SDK Go type | CLI flag | Read value |
|---|---|---|---|
string (required) | string | cli.String("name", "desc", cli.Required()) | cmd.String("name") |
string (optional) | *string | cli.String("name", "desc") | check non-empty, then &v |
integer (required) | int64 | cli.Int64("name", "desc", cli.Required()) | cmd.Int64("name") |
integer (optional) | *int64 | cli.Int64("name", "desc") | check non-zero, then &v |
boolean (optional) | *bool | cli.Bool("name", "desc", cli.Default(X)) | ptr.P(cmd.Bool("name")) — use pkg/ptr for the pointer |
Boolean flag defaults: Look up default: in the OpenAPI spec. Use cli.Default(true) or cli.Default(false) accordingly. For partial-update endpoints where omitting a boolean means "don't change" (no default: in spec), do NOT set a default — use cmd.FlagIsSet("name") to check if the user explicitly passed it:
if cmd.FlagIsSet("enabled") {
req.Enabled = ptr.P(cmd.Bool("enabled"))
}
| array of strings | []string | cli.StringSlice("name", "desc") | cmd.StringSlice("name") |
| object / map / nested | complex | cli.String("name-json", "JSON: ...") | json.Unmarshal |
JSON flags: For complex types exposed as --*-json flags, keep the flag description focused on what the field does. Show the JSON shape in the command's Examples field instead, with realistic values. Every command that has JSON flags MUST include at least one example showing their usage.
Flag naming: Convert camelCase property names to kebab-case: apiId → api-id, externalId → external-id.
Every leaf command uses a plain func(ctx, cmd) error action. No wrappers — each command
explicitly creates the client, times the call, formats errors, and prints output:
Action: func(ctx context.Context, cmd *cli.Command) error {
client, err := util.CreateClient(cmd)
if err != nil {
return err
}
// Build request from flags
req := components.V2ApisCreateAPIRequestBody{
Name: cmd.String("name"), // required: assign directly
}
// Optional string:
if v := cmd.String("prefix"); v != "" {
req.Prefix = &v
}
// Optional int64:
if v := cmd.Int64("limit"); v != 0 {
req.Limit = &v
}
// Optional bool — look up the default in the OpenAPI spec (check `default:` on the property).
// Use cli.Bool with cli.Default and ptr.P from pkg/ptr:
req.Enabled = ptr.P(cmd.Bool("enabled")) // default: true in spec
req.Decrypt = ptr.P(cmd.Bool("decrypt")) // default: false in spec
// For partial-update endpoints where omitting a bool means "don't change":
if cmd.FlagIsSet("enabled") {
req.Enabled = ptr.P(cmd.Bool("enabled"))
}
// String slice:
if v := cmd.StringSlice("permissions"); len(v) > 0 {
req.Permissions = v
}
// JSON field (complex object):
if v := cmd.String("meta-json"); v != "" {
var meta map[string]any
if err := json.Unmarshal([]byte(v), &meta); err != nil {
return fmt.Errorf("invalid JSON for --meta-json: %w", err)
}
req.Meta = meta
}
// Call SDK and handle errors
start := time.Now()
res, err := client.Apis.CreateAPI(ctx, req)
if err != nil {
return fmt.Errorf("%s", util.FormatError(err))
}
return util.Output(cmd, res.V2ApisCreateAPIResponseBody, time.Since(start))
},
See cmd/api/apis/create_api.go for a complete working example. Follow this pattern exactly for all new commands.
The project uses strict linters via bazel nogo. Watch out for:
nil for pointer fields you're not setting:
req := components.V2ApisListKeysRequestBody{
APIID: cmd.String("api-id"),
Limit: nil,
Cursor: nil,
}
Write, Fprintln, Close, etc.For each command, generate a documentation page at docs/product/cli/{group}/{command-name}.mdx.
Each command gets its own .mdx file using Mintlify components:
---
title: "create-api"
description: "Create an API namespace for organizing keys by environment, service, or product"
---
Copy the full description from the OpenAPI spec here (same as the CLI Description,
but keep markdown formatting since this is a docs page).
## Usage
```bash
unkey api apis create-api [flags]
For JSON flags, use an Expandable to show the schema:
Credit and refill configuration. Number of credits remaining. Auto-refill configuration. Refill interval: "daily" or "monthly". Credits to add each interval.Include this exact section on every command doc:
| Flag | Type | Description |
|---|---|---|
--root-key | string | Override root key ($UNKEY_ROOT_KEY) |
--api-url | string | Override API base URL (default: https://api.unkey.com) |
--config | string | Path to config file (default: ~/.unkey/config.toml) |
--output | string | Output format — use json for raw JSON |
<Expandable> with nested <ResponseField> to document the shape of JSON flags. Read the OpenAPI spec's $ref schemas to get the exact field definitions.required attribute on <ParamField>.default attribute on <ParamField>.IMPORTANT: Every new doc file MUST be registered in docs/product/docs.json or it won't appear in the documentation site. After creating doc files, read the current docs.json, find the CLI navigation group, and add any missing pages. The structure should be:
{
"group": "CLI",
"icon": "terminal",
"pages": [
"cli/overview",
{
"group": "apis",
"pages": [
"cli/apis/create-api",
"cli/apis/delete-api",
"cli/apis/get-api",
"cli/apis/list-keys"
]
},
{
"group": "keys",
"pages": [
"cli/keys/create-key",
...
]
}
]
}
When running in check mode, also report:
.mdx file in docs/product/cli/docs.json_test.go fileEach command gets a table-driven test that verifies the exact request body sent to the API.
Each command file {action}.go gets a corresponding {action}_test.go in the same package.
Use util.CaptureRequest[T] which runs the full CLI against a local test server, captures the
request body, unmarshals it into T, and returns it. It fatals on any error.
req := util.CaptureRequest[openapi.V2ApisCreateApiRequestBody](t, Cmd(), "apis create-api --name=test")
Use table-driven tests. Each case is: name, args string, expected struct.
func TestCreateAPI(t *testing.T) {
tests := []struct {
name string
args string
want openapi.V2ApisCreateApiRequestBody
}{
{
name: "basic",
args: "apis create-api --name=payment-service",
want: openapi.V2ApisCreateApiRequestBody{
Name: "payment-service",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := util.CaptureRequest[openapi.V2ApisCreateApiRequestBody](t, Cmd(), tt.args)
require.Equal(t, tt.want, req)
})
}
}
For each command, write test cases covering:
--enabled does NOT send an enabled field (for partial-update commands using FlagIsSet)github.com/unkeyed/unkey/svc/api/openapi (e.g., openapi.V2KeysCreateKeyRequestBody)ptr.P() from github.com/unkeyed/unkey/pkg/ptr for pointer fieldsnullable.NewNullableWithValue() from github.com/oapi-codegen/nullable for nullable fields--flag=value syntax in args (not --flag value)Cmd() not Cmd), so each test gets fresh flag stateAll commands and group roots are defined as functions returning *cli.Command, not package-level
vars. This ensures each test invocation gets fresh flag instances with no stale state from prior
tests. Always call Cmd() in tests, never reference a bare Cmd variable.
See cmd/api/keys/create_key_test.go and cmd/api/keys/update_key_test.go for complete examples.
When running in check mode, also report:
_test.go fileAfter generating, run:
make bazel && make build && make fmt
Fix any errors before finishing. Never use go build directly — always use make build (bazel), as it runs stricter linters.