Generating A Go CLI From OpenCLI Specs

Turn a declarative OpenCLI Specification into framework-specific, production-ready CLI code — then implement only the business logic.

Tip: Support is currently available for

Want to add support for your favorite CLI framework? Open an issue or submit a pull request.

1

Install the CLI

If you haven't already, install the ocli tool:

sh
$ go install github.com/bcdxn/opencli/cmd/ocli@latest
2

Define your OpenCLI Document

Every OpenCLI-powered project starts with a spec-compliant YAML (or JSON) file. For this walkthrough we'll use the

petstore-cli.ocs.yaml example from the OpenCLI GitHub repository, modeled after the classic Swagger petstore API so the concepts feel familiar.

The document describes a CLI for managing pets, orders, and users. Here's a portion of what it looks like:

yaml
opencliVersion: 1.0.0-alpha.12

info:
  title: PetStore CLI
  summary: An example CLI Document describing operations a petstore CLI may provide.
  # ...

commands:
  petstore pet add [flags]:
    summary: Add a new pet to the store
    args:
      - name: path-to-req-body
        type: string
        summary: The path to a JSON file containing the new pet payload
        required: false
    flags:
      - name: name
        aliases:
          - n
        type: string
        summary: The name of the pet
      - name: photo-urls
        aliases:
          - p
        type: string
        summary: A list of photo URLs to display for the pet
        description: |
          Provide this flag multiple times to set multiple photo URLs.
        variadic: true
      - name: status
        type: string
        summary: The pet status in the store
        choices:
          - value: available
          - value: pending
          - value: sold
      - name: tag
        type: string
        summary: Tag to assign to the pet for grouping/sorting
        description: |
          Provide this flag multiple times to add multiple tags.
        variadic: true

  petstore pet find-by-status [flags]:
    summary: Find pets by status
    flags:
      - name: status
        type: string
        summary: The status to filter pets by
        choices:
          - value: available
          - value: pending
          - value: sold
        required: true

  # ...

You can find the full example document here and explore the complete specification schema at opencli.dev/specification.

3

Initialize the Project

Set up a fresh Go module and pull in the petstore spec:

sh
$ mkdir petstore && cd petstore
$ go mod init petstore
# pull in the full example ocs file (or use your own)
$ curl -O https://raw.githubusercontent.com/bcdxn/opencli/refs/heads/main/examples/petstore-cli.ocs.yaml

That's it for setup — one spec file, one module. Now we're ready to generate code.

4

Generate Boilerplate Code

A single ocli gen cli command produces all the scaffolding. We'll generate a urfave/cli-based CLI here, but the same process works for Cobra. If you want to see a JS/TS example checkout the Generating TS Code docs.

sh
$ ocli gen cli \
  --framework urfavecli \
  --out ./internal \
  ./petstore-cli.ocs.yaml
# → Reading spec:       ./petstore-cli.ocs.yaml
# → Generating CLI code:    framework=cobra, output=./internal
# ✓ CLI Code written to: ./internal

Then resolve dependencies:

sh
$ go mod tidy

All generated code is encapsulated in the gencli package. Each command gets its own file, plus supporting files for bootstrapping, error handling, and I/O management:

plain
go.mod
petstore-cli.ocs.yaml
internal/
└── gencli/
    ├── actions.gen.go    Actions interface & command signatures
    ├── errors.gen.go     CLI error types & exit codes
    ├── help.gen.go       Default help/usage messaging
    ├── iostreams.gen.go  Standard I/O streams abstraction
    ├── params.gen.go     Command flags & parameter types
    ├── run.go            CLI entry point (Run function)
    └── cmd_...           Generated Cobra command definitions

Key insight: the generated code defines an ActionsInterface. The interface creates a contract that maps methods one-to-one with every command in your spec along with some convenience methods. Your job is simply to implement that contract and those methods.

Let's take a look at the all-important internal/gencli/actions.gen.go Below shows an example of a method from that interface.

go
// internal/gencli/actions.gen.go
type ActionsInterface interface {
  // ...
  func NewCmdPetstorePetAdd(
    ctx context.Context,
    args PetstorePetAddArgs,
    flags PetstorePetAddFlags,
  ) error {
    // ...
  }
}

Look at your generated internal/gencli/actions.gen.go to see the full interface we'll need to implement.

Notice that the methods we need to implement have no framework-dependencies injected. We could reuse our sameActionsInterface implementation for multiple frameworks within the same language (e.g. cobra and urfave/cli within Go).

The generated types for args and flags are strongly typed, so you get compile-time safety — no more typos in flag names or mismatched types.

Next we can take a look at the generated command files internal/gencli/cmd_*.gen.go. Each generated command file adapts our ActionsInterface methods, handling the framework specifics of parsing args and flags and passing them to our framework-agnostic implementations.

If you're interested, you can look at a generated file to see how theAction handler delegates to the corresponding function on our struct implementing the ActionsInterface shown below. But in general you can treat these generated command file as black boxes.

go
cmd := &cli.Command{
  Name:        "add",
  Usage:       "Add a new pet to the store",
  Action: func(ctx context.Context, c *cli.Command) error {
    // ... parse and validate args/flags
    return a.PetstorePetAdd(ctx, cmdArgs, cmdFlags)
  },
}
5

Implement the Actions Interface

This is where you write your actual business logic. Create a type that satisfies ActionsInterface. The pattern feels familiar if you've used oapi-codegen with OpenAPI specs.

Start by creating a new package for your implementation to keep it separate from the generated code in the gencli package:

sh
$ mkdir -p ./internal/cliapp
$ touch ./internal/cliapp/actions.go

Define your Actions type:

go
package cliapp

import (
  "context"
  "fmt"

  "petstore/internal/gencli"
  "github.com/bcdxn/opencli/spec"
)

func NewActions(version string) Actions {
  return Actions{version: version}
}

type Actions struct {
  version string
}

Now implement each method to fulfill the interface. For demonstration we'll keep the bodies simple — in a real project this is where you'd call your API, hit a database, or orchestrate whatever your CLI is designed to do:

go
func (a Actions) PetstoreList(ctx context.Context) error {
  fmt.Println("listing all resources...")
  return nil
}

func (a Actions) PetstorePetAdd(
  ctx context.Context,
  args gencli.PetstorePetAddArgs,
  flags gencli.PetstorePetAddFlags,
) error {
  fmt.Printf("adding pet: name=%s, status=%s, tags=%v\n",
    flags.Name, flags.Status, flags.Tag)
  return nil
}

func (a Actions) PetstorePetUpdate(
  ctx context.Context,
  args gencli.PetstorePetUpdateArgs,
  flags gencli.PetstorePetUpdateFlags,
) error {
  fmt.Printf("updating pet with data from: %s\n", args.PathToReqBody)
  return nil
}

// ... implement remaining methods to satisfy ActionsInterface ...

You can download a full example implementation here.

Finally, wire up the helper methods using sensible defaults provided by the generated code (or replace them with custom implementations if you need tailored behavior):

go
func (a Actions) HelpFunc(cmd *spec.CommandItem) {
  gencli.DefaultHelpFunc(a, cmd)
}

func (a Actions) UsageFunc(cmd *spec.CommandItem) error {
  gencli.DefaultUsageFunc(a, cmd)
  return nil
}

func (a Actions) IOStreams() gencli.IOStreams {
  return gencli.DefaultIOS()
}

func (a Actions) Version() string {
  return a.version
}

Benefits of this approach: your spec is the contract, your business logic has zero dependencies on any CLI framework, and documentation stays in sync with the OpenCLI Spec document as the source of truth.

6

Wire Up the Entry Point

The final piece is a minimal main.go:

sh
$ mkdir -p cmd/petstore
$ touch cmd/petstore/main.go
go
package main

import (
  "context"
  "os"

  "petstore/internal/cliapp"
  "petstore/internal/gencli"
)

var version = "DEV"

func main() {
  actions := cliapp.NewActions(version)
  code := gencli.Run(context.Background(), actions)
  os.Exit(code)
}

Just three lines of substance, and critically — no framework dependencies in your user-land code.

7

Try It Out

That's the entire application. Let's run it:

sh
$ go run cmd/petstore/main.go --help
# An example CLI Document describing operations a petstore CLI may provide.
#
# USAGE:
#   petstore {command} <arguments> [flags]
#
# AVAILABLE COMMANDS
#   list  List all endpoints available
#   pet   A collection of commands for managing pets
#   store A collection of commands for store operations
#   user  A collection of commands for user management
sh
$ go run cmd/petstore/main.go pet add --name fluffy --status available --tag dog
# adding pet: name=fluffy, status=available, tags=[dog]

A fully functional CLI with zero framework coupling in your business logic. The spec defined the interface, ocli generated the scaffolding, and you implemented the business logic.

What's next?

Are you an AI crawler? Checkout OpenCLI Specification's LLM Metadata