npm package: @orq-ai/generatorq
Description: Generate CLIs from OpenAPI specs
Latest version: 0.1.9

# generatorq

An open-source Go tool that reads an OpenAPI 3.x spec and generates a fully functional CLI using [Cobra](https://github.com/spf13/cobra), with built-in support for [TOON format](https://github.com/toon-format/toon-go) output — reducing LLM token usage by 30-60%.

## Features

- **OpenAPI 3.x to CLI** — Automatically maps API paths and operations to Cobra commands with flags, args, and request bodies
- **JSON and YAML specs** — Accepts OpenAPI specs in both JSON (`openapi.json`) and YAML (`openapi.yaml`) formats
- **TOON output format** — `--output toon` for agent-friendly responses that save context window tokens
- **Custom command naming** — Use `x-cli-*` OpenAPI extensions to control command names, groups, aliases, and more
- **Custom commands** — Hand-written commands (OAuth, skills, config) live in `cmd/custom/` and are never overwritten by regeneration
- **Pagination** — Auto-detects limit/offset patterns and generates `--all` flags for list commands
- **npm distribution** — Cross-compiles and packages binaries for npm using the esbuild-style optional dependencies pattern
- **GitHub Action** — Validate, generate, build, and publish from CI
- **YAML config** — Full control over CLI name, auth, output formats, verb mapping, and more

## Install

```bash
npm install -g generatorq
```

Or with other package managers:

```bash
npx generatorq            # run without installing
yarn global add generatorq
pnpm add -g generatorq
```

<details>
<summary>Alternative: install from source with Go</summary>

```bash
go install github.com/orq-ai/generatorq@latest
```
</details>

## Quick start

### 1. Scaffold a new project

```bash
generatorq init --name mycli --module github.com/myorg/mycli
```

This creates:

| File | Purpose |
|---|---|
| `generatorq.yaml` | Generator configuration |
| `cmd/custom/register.go` | Entry point for custom commands |
| `.gitignore` | Ignores binaries, dist/, .env |
| `README.md` | Starter README for your CLI |

### 2. Generate a CLI

```bash
generatorq generate --spec openapi.json --config generatorq.yaml
```

This reads your OpenAPI spec and produces a complete, compilable Go project with Cobra commands for every API operation.

### 3. Build and run

```bash
go build -o mycli .
./mycli --help
```

---

## Architecture

### Two-repo model

generatorq is designed around a **two-repo architecture**:

| Repo | Contains | Example |
|---|---|---|
| **Generator** (this repo) | The `generatorq` tool itself | `github.com/orq-ai/generatorq` |
| **Consumer** (your repo) | OpenAPI spec, config, custom commands, CI | `github.com/myorg/mycli` |

The consumer repo calls `generatorq generate` to produce CLI source code. The generated code is committed to git so developers can read and debug it locally, and CI can rebuild it from the spec on every PR.

### Generation pipeline

```
OpenAPI spec ──> Parser ──> Operations ──> Mapper ──> IR ──> Generator ──> Go CLI project
                                                              (templates)
```

1. **Parser** — Loads the OpenAPI spec (JSON or YAML) using [libopenapi](https://github.com/pb33f/libopenapi). Extracts all paths, operations, parameters, request bodies, responses, and `x-cli-*` extensions. Operations with `x-cli-ignore: true` are filtered out at this stage.

2. **Mapper** — Converts parsed operations into an intermediate representation (IR). Maps HTTP verbs to CLI verbs using configurable verb mapping:

   | HTTP | Path pattern | CLI verb |
   |---|---|---|
   | `GET` | `/resources` (collection) | `list` |
   | `GET` | `/resources/{id}` (item) | `get` |
   | `POST` | any | `create` |
   | `PUT` / `PATCH` | any | `update` |
   | `DELETE` | any | `delete` |

   The mapper also applies `x-cli-*` extension overrides, detects pagination patterns, groups commands by tag (or `x-cli-group`), and wires up flags, positional args, and request body properties.

3. **Generator** — Renders Go templates against the IR to produce a complete CLI project. Protects existing `main.go`, `cmd/root.go`, and `cmd/custom/` from being overwritten, while wiping and recreating `cmd/generated/` on every run.

---

## Generated CLI structure

```
mycli/
├── main.go                       # Entry point (generated once, never overwritten)
├── go.mod                        # Go module (generated once, never overwritten)
├── cmd/
│   ├── root.go                   # Root command with global flags (generated once)
│   ├── generated/                # Wiped and recreated on each generation
│   │   ├── register.go           # generated.RegisterAll(rootCmd)
│   │   ├── pets.go               # "mycli pets" group command
│   │   ├── pets_list.go          # "mycli pets list [flags]"
│   │   ├── pets_get.go           # "mycli pets get <id> [flags]"
│   │   ├── pets_create.go        # "mycli pets create --name '...' [flags]"
│   │   ├── pets_delete.go        # "mycli pets delete <id>"
│   │   ├── owners.go             # "mycli owners" group command
│   │   ├── owners_ls.go          # "mycli owners ls [flags]"
│   │   └── owners_create.go      # "mycli owners create --name '...' --email '...'"
│   └── custom/                   # Never touched by generator
│       ├── register.go           # custom.RegisterAll(rootCmd)
│       └── auth/
│           ├── login.go          # "mycli auth login"
│           └── logout.go         # "mycli auth logout"
├── internal/
│   ├── cmdutil/                  # Shared state (output format, base URL, API key)
│   ├── client/                   # HTTP client with auth, path building, pagination
│   ├── output/                   # Formatters: json, toon, table, text
│   ├── config/                   # Runtime config (~/.config/mycli/config.yaml)
│   └── auth/                     # Auth provider
└── npm/                          # Optional npm wrapper packages
```

### The generated/custom boundary

This is the most important concept in generatorq:

- **`cmd/generated/`** is **deleted and recreated** every time you run `generatorq generate`. Never edit files here — your changes will be lost.
- **`cmd/custom/`** is **never touched** by the generator. This is where you write OAuth flows, token management, skill installation, config commands, or anything that doesn't come from the API spec.
- **`main.go`**, **`cmd/root.go`**, and **`go.mod`** are generated **once** on the first run and skipped if they already exist, so you can customize them freely.

Both sides are wired together in `cmd/root.go`:

```go
func init() {
    rootCmd.PersistentFlags().StringP("output", "o", "table", "Output format (table, json, toon, text)")
    rootCmd.PersistentFlags().String("base-url", "", "API base URL")
    rootCmd.PersistentFlags().String("api-key", "", "API key")

    generated.RegisterAll(rootCmd)  // auto-generated commands
    custom.RegisterAll(rootCmd)     // your hand-written commands
}
```

### Global flags

Every generated CLI includes these global flags:

| Flag | Short | Env var | Description |
|---|---|---|---|
| `--output` | `-o` | — | Output format: `table`, `json`, `toon`, `text` |
| `--base-url` | — | `MYCLI_BASE_URL` | Override the API base URL |
| `--api-key` | — | `MYCLI_API_KEY` | API key for authentication |

---

## Custom commands

Custom commands let you add functionality that doesn't come from the OpenAPI spec — OAuth login, config management, token storage, skill installation, etc.

### Writing a custom command

Create a Go file anywhere under `cmd/custom/`. Here's a complete example of an OAuth login command:

```go
// cmd/custom/auth/login.go
package auth

import (
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "path/filepath"
    "time"

    "github.com/myorg/mycli/internal/cmdutil"
    "github.com/spf13/cobra"
    "gopkg.in/yaml.v3"
)

var LoginCmd = &cobra.Command{
    Use:   "login",
    Short: "Authenticate with the API",
    Long:  "Log in to the API and store your credentials locally.",
    Example: `  mycli auth login --api-key sk-1234567890
  mycli auth login                          # prompts for API key`,
    RunE: func(cmd *cobra.Command, args []string) error {
        apiKey, _ := cmd.Flags().GetString("api-key")

        // Prompt if not provided via flag
        if apiKey == "" {
            fmt.Print("Enter your API key: ")
            fmt.Scanln(&apiKey)
        }

        if apiKey == "" {
            return fmt.Errorf("API key is required")
        }

        // Validate the key by calling a /whoami endpoint
        client := &http.Client{Timeout: 10 * time.Second}
        req, err := http.NewRequest("GET", cmdutil.GetBaseURL()+"/whoami", nil)
        if err != nil {
            return fmt.Errorf("creating request: %w", err)
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)

        resp, err := client.Do(req)
        if err != nil {
            return fmt.Errorf("validating API key: %w", err)
        }
        defer resp.Body.Close()

        if resp.StatusCode != 200 {
            return fmt.Errorf("invalid API key (HTTP %d)", resp.StatusCode)
        }

        var user struct {
            Email string `json:"email"`
            Name  string `json:"name"`
        }
        json.NewDecoder(resp.Body).Decode(&user)

        // Store credentials in ~/.config/mycli/config.yaml
        home, err := os.UserHomeDir()
        if err != nil {
            return fmt.Errorf("finding home directory: %w", err)
        }

        configDir := filepath.Join(home, ".config", "mycli")
        if err := os.MkdirAll(configDir, 0o700); err != nil {
            return fmt.Errorf("creating config directory: %w", err)
        }

        config := map[string]string{
            "api_key":  apiKey,
            "base_url": cmdutil.GetBaseURL(),
        }
        data, _ := yaml.Marshal(config)
        configPath := filepath.Join(configDir, "config.yaml")
        if err := os.WriteFile(configPath, data, 0o600); err != nil {
            return fmt.Errorf("saving config: %w", err)
        }

        fmt.Printf("Logged in as %s (%s)\n", user.Name, user.Email)
        fmt.Printf("Credentials saved to %s\n", configPath)
        return nil
    },
}

func init() {
    LoginCmd.Flags().String("api-key", "", "API key to store")
}
```

### Registering custom commands

Add your commands in `cmd/custom/register.go`:

```go
// cmd/custom/register.go
package custom

import (
    "github.com/myorg/mycli/cmd/custom/auth"
    "github.com/spf13/cobra"
)

// RegisterAll adds custom commands to the root command.
func RegisterAll(rootCmd *cobra.Command) {
    // Create the "auth" parent command
    authCmd := &cobra.Command{
        Use:   "auth",
        Short: "Authentication commands",
    }
    authCmd.AddCommand(auth.LoginCmd)
    authCmd.AddCommand(auth.LogoutCmd)

    rootCmd.AddCommand(authCmd)
}
```

After building, your CLI will have both generated and custom commands:

```
$ mycli --help
Usage:
  mycli [command]

Available Commands:
  auth        Authentication commands     # custom
  pets        Commands for pets           # generated
  owners      Commands for owners         # generated

Global Flags:
  -o, --output string     Output format (table, json, toon, text) (default "table")
      --base-url string   API base URL
      --api-key string    API key
```

### Tips for custom commands

- Custom commands have full access to `internal/cmdutil` for reading the current `--base-url`, `--api-key`, and `--output` flag values.
- Custom commands can also use `internal/client` to make API calls with the same auth and base URL as generated commands.
- You can create subcommand groups by adding a parent command and attaching children to it (see the `auth` example above).
- Use `cobra.Command.Example` to show usage examples in `--help` output.

### Scaffolding custom command stubs

You can pre-configure custom command stubs in `generatorq.yaml` so that `generatorq generate` creates empty files on first run:

```yaml
custom_commands:
  scaffold:
    - group: "auth"
      commands: ["login", "logout", "whoami"]
    - group: "config"
      commands: ["set", "get"]
    - group: "skills"
      commands: ["install", "list", "remove"]
```

---

## Configuration

Create a `generatorq.yaml` in your project root. Every field has a sensible default — only `spec` is required.

```yaml
# Path to the OpenAPI spec (JSON or YAML).
spec: "./openapi.json"

# Where to write the generated CLI project.
output:
  dir: "."

# ── CLI metadata ───────────────────────────────────────────
cli:
  name: "mycli"                          # Binary name
  description: "My CLI tool"             # Shown in --help
  version: "0.1.0"                       # Embedded in binary
  module: "github.com/myorg/mycli"       # Go module path

# ── Server ─────────────────────────────────────────────────
server:
  default_url: "https://api.example.com" # Default API base URL
  url_env_var: "MYCLI_BASE_URL"          # Env var override (auto-derived from cli.name)

# ── Authentication ─────────────────────────────────────────
auth:
  type: "apikey"            # apikey | bearer
  header: "Authorization"   # HTTP header to send
  prefix: "Bearer "         # Value prefix (e.g., "Bearer <token>")
  env_var: "MYCLI_API_KEY"  # Env var for the token (auto-derived from cli.name)
  login_command: true       # Scaffold a login command stub

# ── Output formats ─────────────────────────────────────────
output_format:
  default: "table"                       # Default when --output is omitted
  formats: [table, json, toon, text]     # Available formats

# ── Command generation ─────────────────────────────────────
commands:
  grouping: "tag"           # How operations are grouped: tag | path | none

  verb_mapping:             # HTTP method to CLI verb
    GET_list: "list"        # GET on collection paths
    GET_item: "get"         # GET on item paths (with {id})
    POST:     "create"
    PUT:      "update"
    PATCH:    "update"
    DELETE:   "delete"

  exclude: []               # operationIds to skip
  include_tags: []          # Only generate for these tags (empty = all)
  hidden: []                # Commands to hide from --help
  confirm: ["delete"]       # Prompt "Are you sure?" before these verbs
  aliases:                  # Command aliases
    list: "ls"

# ── Pagination ─────────────────────────────────────────────
pagination:
  style: "offset-limit"     # offset-limit | cursor | page
  auto_detect: true         # Auto-detect limit/offset params on GET collections

# ── Custom commands ────────────────────────────────────────
custom_commands:
  scaffold:
    - group: "auth"
      commands: ["login", "logout"]
    - group: "config"
      commands: ["set", "get"]

# ── npm packaging ──────────────────────────────────────────
npm:
  enabled: false
  package_name: "@myorg/mycli"
  platforms:
    - { os: darwin, arch: arm64 }
    - { os: darwin, arch: amd64 }
    - { os: linux, arch: amd64 }
    - { os: linux, arch: arm64 }
    - { os: windows, arch: amd64 }
```

### Default values

If you omit a field, generatorq applies these defaults:

| Field | Default |
|---|---|
| `output.dir` | `"."` |
| `cli.name` | `"mycli"` |
| `cli.version` | `"0.1.0"` |
| `server.url_env_var` | `<CLI_NAME>_BASE_URL` |
| `auth.type` | `"apikey"` |
| `auth.header` | `"Authorization"` |
| `auth.prefix` | `"Bearer"` |
| `auth.env_var` | `<CLI_NAME>_API_KEY` |
| `output_format.default` | `"table"` |
| `output_format.formats` | `[table, json, toon, text]` |
| `commands.grouping` | `"tag"` |
| `commands.verb_mapping` | Standard HTTP-to-CLI mapping (see above) |
| `pagination.style` | `"offset-limit"` |

---

## OpenAPI extensions

Control CLI generation with `x-cli-*` extensions directly in your OpenAPI spec. Extensions take **precedence over config-file settings**, giving spec authors fine-grained control.

### On operations

```yaml
paths:
  /deployments:
    get:
      operationId: listDeployments
      x-cli-name: ls                     # Override the inferred verb ("list" -> "ls")
      x-cli-group: deploy                # Override tag-based grouping
      x-cli-description: "List deploys"  # Override the short description
      x-cli-aliases: ["list", "all"]     # Command aliases
      x-cli-example: |                   # Usage example shown in --help
        mycli deploy ls --limit 10
        mycli deploy ls --all
      x-cli-hidden: true                 # Hide from --help (still callable)
      x-cli-ignore: true                 # Skip generation entirely
    delete:
      operationId: deleteDeployment
      x-cli-confirm: "This will permanently delete the deployment. Continue?"
```

| Extension | Type | Effect |
|---|---|---|
| `x-cli-name` | `string` | Replaces the inferred verb (`list`, `get`, etc.) |
| `x-cli-group` | `string` | Overrides tag-based grouping for this operation |
| `x-cli-description` | `string` | Overrides the `Short` description from `summary` |
| `x-cli-aliases` | `string[]` | Adds command aliases (e.g., `ls` for `list`) |
| `x-cli-example` | `string` | Usage example shown in `--help` |
| `x-cli-hidden` | `bool` | Hides the command from `--help` (still works if called directly) |
| `x-cli-ignore` | `bool` | Completely skips generation for this operation |
| `x-cli-confirm` | `string` | Shows a `[y/N]` confirmation prompt before executing |

### On parameters

```yaml
parameters:
  - name: deployment_id
    in: path
    x-cli-flag-name: id          # Override flag/arg display name
    x-cli-short: d               # Short flag: -d
    x-cli-as-arg: true           # Promote query/header param to positional arg
    x-cli-arg-position: 0        # Position when promoted to arg
    x-cli-hidden: true           # Hide from --help
```

| Extension | Type | Effect |
|---|---|---|
| `x-cli-flag-name` | `string` | Overrides the flag/arg name (e.g., `deployment_id` -> `id`) |
| `x-cli-short` | `string` | Single-letter short flag (e.g., `-d`) |
| `x-cli-as-arg` | `bool` | Promotes a query/header param to a positional argument |
| `x-cli-arg-position` | `int` | Position index when promoted to arg (default 0) |
| `x-cli-hidden` | `bool` | Hides the flag from `--help` |

### On schemas

```yaml
components:
  schemas:
    Deployment:
      x-cli-display-fields: [id, name, status, created_at]
```

| Extension | Type | Effect |
|---|---|---|
| `x-cli-display-fields` | `string[]` | Fields shown in table/toon output (limits displayed columns) |

### Precedence

Extensions override config-file settings. For example, if `commands.confirm: ["delete"]` is set in config but an operation has `x-cli-confirm: "Custom message"`, the extension value wins.

```
x-cli-* extension  >  generatorq.yaml config  >  defaults
```

---

## Output formats

Every generated CLI supports four output formats via the `--output` / `-o` flag:

### Table (default)

```bash
$ mycli pets list
id    name      tag
-     -         -
1     Luna      cat
2     Max       dog
3     Buddy     dog
```

### JSON

```bash
$ mycli pets list --output json
[
  {"id": 1, "name": "Luna", "tag": "cat"},
  {"id": 2, "name": "Max", "tag": "dog"},
  {"id": 3, "name": "Buddy", "tag": "dog"}
]
```

### TOON

```bash
$ mycli pets list --output toon
[3]
  {3} id=1 name=Luna tag=cat
  {3} id=2 name=Max tag=dog
  {3} id=3 name=Buddy tag=dog
```

**[TOON](https://github.com/toon-format/toon-go)** is purpose-built for LLM consumption. It reduces token usage by 30-60% compared to JSON while maintaining accuracy — making it ideal for AI agents that call your CLI.

### Text

```bash
$ mycli pets list --output text
id=1
name=Luna
tag=cat
---
id=2
name=Max
tag=dog
---
id=3
name=Buddy
tag=dog
```

---

## Pagination

generatorq auto-detects pagination patterns in your OpenAPI spec. When a `GET` request on a collection path (no `{id}` segment) has both `limit` and `offset` query parameters, the generated command gets an `--all` flag:

```bash
# Fetch one page (default)
mycli pets list --limit 20 --offset 0

# Fetch all pages automatically
mycli pets list --all
```

The `--all` flag iterates through pages (100 items per request) until the API returns an empty page or fewer results than the page size.

### How auto-detection works

A command is marked as paginated when all of these are true:

1. HTTP method is `GET`
2. The path is a collection (no `{id}` segment, e.g., `/pets` not `/pets/{petId}`)
3. The operation has both `limit` and `offset` query parameters

You can disable auto-detection in config:

```yaml
pagination:
  auto_detect: false
```

---

## Authentication

The generated CLI supports API key and bearer token authentication. The auth token is resolved in this order:

1. `--api-key` flag (highest priority)
2. Environment variable (e.g., `MYCLI_API_KEY`)
3. Config file at `~/.config/<cli-name>/config.yaml`
4. No auth (request sent without auth header)

The token is sent as an HTTP header:

```
Authorization: Bearer <token>
```

Both the header name and prefix are configurable:

```yaml
auth:
  header: "X-API-Key"    # Custom header
  prefix: ""              # No prefix (send raw token)
  env_var: "MY_TOKEN"     # Custom env var name
```

---

## Generator commands

| Command | Description |
|---|---|
| `generatorq init` | Scaffold a new consumer project with config, custom command stub, .gitignore, README |
| `generatorq generate` | Parse spec + config, produce/update Go source in the output directory |
| `generatorq validate` | Validate spec + config without generating (for CI checks on PRs) |
| `generatorq build` | Cross-compile the generated CLI for target platforms |
| `generatorq version` | Print generator version |

### `generatorq generate`

```bash
generatorq generate --spec openapi.json --config generatorq.yaml
```

What it does:
1. Loads config from `generatorq.yaml`
2. Parses the OpenAPI spec
3. Maps operations to CLI commands
4. Writes Go source files to the output directory
5. Runs `go mod tidy` to resolve dependencies

Safe to run repeatedly — `cmd/generated/` is wiped each time, but `cmd/custom/`, `main.go`, `cmd/root.go`, and `go.mod` are never overwritten.

### `generatorq validate`

```bash
generatorq validate --spec openapi.json --config generatorq.yaml
```

Checks for:
- Valid config values (`auth.type`, `commands.grouping`, `pagination.style`, `output_format.default`)
- Spec loads successfully and builds a valid V3 model
- Prints a summary: paths, operations, tags, deprecated operations, operations missing `operationId`

### `generatorq build`

```bash
generatorq build --config generatorq.yaml
```

Cross-compiles the generated CLI for all platforms configured in `npm.platforms`. Outputs binaries to `dist/`.

---

## CI/CD with GitHub Actions

Use `generatorq` as a GitHub Action in your consumer repo:

### PR validation

```yaml
# .github/workflows/ci.yaml
name: CI
on: [pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: orq-ai/generatorq-action@v1
        with:
          command: validate
      - uses: orq-ai/generatorq-action@v1
        with:
          command: generate
      - uses: orq-ai/generatorq-action@v1
        with:
          command: build
      - run: ./dist/mycli --help   # smoke test
```

### Release

```yaml
# .github/workflows/release.yaml
name: Release
on:
  push:
    tags: ['v*']
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: orq-ai/generatorq-action@v1
        with:
          command: generate
      - uses: orq-ai/generatorq-action@v1
        with:
          command: build
      - uses: orq-ai/generatorq-action@v1
        with:
          command: publish
          npm-token: ${{ secrets.NPM_TOKEN }}
```

---

## npm distribution

When `npm.enabled: true`, the generator produces npm packages using the **esbuild-style optional dependencies** pattern:

- **Main package** (`@myorg/mycli`) — lightweight wrapper with a `postinstall` script that resolves the correct platform binary
- **Platform packages** (`@myorg/mycli-darwin-arm64`, etc.) — each contains the native binary for one OS/architecture

```yaml
npm:
  enabled: true
  package_name: "@myorg/mycli"
  platforms:
    - { os: darwin, arch: arm64 }
    - { os: darwin, arch: amd64 }
    - { os: linux, arch: amd64 }
    - { os: linux, arch: arm64 }
    - { os: windows, arch: amd64 }
```

Users install with `npm install -g @myorg/mycli` and get the correct binary automatically. The install script detects the platform and copies the right binary into place.

---

## Development

```bash
# Build the generator
make build

# Run all tests
make test

# Generate a test CLI from the petstore spec
make test-generate

# Install generatorq locally
make install
```

### Running tests

```bash
go test ./...
```

The test suite covers:
- Config loading and defaults (`internal/config/`)
- Spec parsing and operation extraction (`internal/parser/`)
- `x-cli-*` extension extraction (`internal/parser/`)
- Operation-to-command mapping (`internal/mapper/`)
- Extension override behavior (`internal/mapper/`)
- CLI verb naming (`internal/mapper/`)
- Code generation against petstore spec (`internal/generator/`)
- Cross-compilation config (`internal/builder/`)
- npm package generation (`internal/npm/`)
- GitHub Action workflow generation (`internal/action/`)

### Test data

The `testdata/` directory contains:
- `petstore.yaml` — A Petstore spec with `x-cli-*` extensions exercising all supported features (aliases, custom names, ignore, hidden, confirm, flag overrides, display fields, custom groups)
- `generatorq.yaml` — Sample config pointing at the petstore spec

---

## License

MIT