Generating Cobra CLI Boilerplate with OpenCLI Specs
Building a CLI means writing the same boilerplate over and over: parse flags, validate arguments, wire up help text, handle errors, repeat for every command. It’s tedious work even when frameworks help automate things that distracts you from writing the actual logic behind each action.
What if you could define your CLI interface once in a declarative spec and generate all that scaffolding deterministically? That’s exactly what OpenCLI Spec enables.
An OpenCLI Spec (OCS) document defines a CLI’s capabilities the same way an OpenAPI Spec defines an API, as a machine-readable contract. From that single source of truth, you can generate documentation, validate your design, and produce ready-to-use boilerplate code for popular frameworks like Cobra, urfave/cli, and Yargs.
In this post, we’ll walk through generating a complete Cobra-based CLI from an OpenCLI document and end up with a working, framework-agnostic application where your business logic is cleanly separated from generated scaffolding.
The 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:
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.
Notice how declarative this is; every command, argument, flag, and choice lives in one file. This document becomes your single source of truth for the CLI interface.
Generating the Code
Now comes the powerful part: turning this declarative spec into working code. We’ll generate a Cobra-based CLI, but the same process works for urfave/cli and Yargs as well.
The entire workflow is powered by ocli - the OpenCLI command-line tool that validates specs, generates documentation, and produces framework-specific boilerplate.
Step 1: Install the ocli CLI
Install it using go or download the binary from GitHub.
go install github.com/bcdxn/opencli/cmd/ocli@latest
Verify the installation:
$ ocli --help
# A CLI for working with OpenCLI Specs. ocli is designed to make working with
# OpenCLI Spec documents (https://github.com/bcdxn/opencli) easier, providing:
# • validation of OpenCLI Spec documents
# • documentation generation from specs
# • framework-specific code generation
#
# USAGE:
# ocli {command} <arguments> [flags]
#
# AVAILABLE COMMANDS:
# check Check an OpenCLI Spec document for errors
# gen Generate code/docs from an OpenCLI Spec document
Want to add support for your favorite CLI framework? Open an issue or submit a pull request.
Step 2: Initialize the Project
Set up a fresh Go module and grab the petstore spec:
mkdir petstore && cd petstore
go mod init petstore
Pull in the example OCS document:
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.
Step 3: Generate Boilerplate Code
With the spec in hand, a single ocli gen cli command produces all the scaffolding:
ocli gen cli \
--framework cobra \
--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:
go mod tidy
Now let’s look at what ocli created. Everything is encapsulated in the gencli package. Each command gets its own file, plus supporting files for bootstrapping, error handling, and I/O management:
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 anActionsInterface. The interface creates a contract that maps one-to-one with every command in your spec. Your job is simply to implement it. Let’s see how this works.
Each generated command file exposes a function that returns a Cobra command, receiving the ActionsInterface as a dependency:
// cmd_petstore_pet_add.gen.go
func NewCmdPetstorePetAdd(a ActionsInterface) *cobra.Command {
// ...
}
Inside, each command’s RunE handler delegates to the corresponding method on ActionsInterface:
// cmd_petstore_pet_add.gen.go
command := &cobra.Command{
Use: "add",
// ...
RunE: func(c *cobra.Command, args []string) error {
// ... parse flags, validate args ...
return a.PetstorePetAdd(c.Context(), cmdArgs, cmdFlags)
},
}
This means the generated code handles all flag parsing, argument validation, and help text, and then calls your implementation. The ActionsInterface itself looks like this:
// actions.gen.go
// Code generated by github.com/bcdxn/opencli@v1.0.0-alpha.14 DO NOT EDIT.
package gencli
import (
"context"
"github.com/bcdxn/opencli/spec"
)
// ActionsInterface defines all actions the petstore CLI supports.
type ActionsInterface interface {
PetstoreList(ctx context.Context) error
PetstorePetAdd(ctx context.Context, args PetstorePetAddArgs, flags PetstorePetAddFlags) error
PetstorePetUpdate(ctx context.Context, args PetstorePetUpdateArgs, flags PetstorePetUpdateFlags) error
PetstorePetFindByStatus(ctx context.Context, flags PetstorePetFindByStatusFlags) error
PetstorePetFindByTags(ctx context.Context, flags PetstorePetFindByTagsFlags) error
PetstorePetGet(ctx context.Context, flags PetstorePetGetFlags) error
PetstorePetUpdateForm(ctx context.Context, flags PetstorePetUpdateFormFlags) error
PetstorePetDelete(ctx context.Context, flags PetstorePetDeleteFlags) error
PetstorePetUploadImage(ctx context.Context, args PetstorePetUploadImageArgs, flags PetstorePetUploadImageFlags) error
PetstoreStoreInventory(ctx context.Context) error
PetstoreStoreOrderPlace(ctx context.Context, args PetstoreStoreOrderPlaceArgs, flags PetstoreStoreOrderPlaceFlags) error
PetstoreStoreOrderGet(ctx context.Context, flags PetstoreStoreOrderGetFlags) error
PetstoreStoreOrderDelete(ctx context.Context, args PetstoreStoreOrderDeleteArgs) error
PetstoreUserCreate(ctx context.Context, args PetstoreUserCreateArgs, flags PetstoreUserCreateFlags) error
PetstoreUserCreateWithList(ctx context.Context, args PetstoreUserCreateWithListArgs) error
PetstoreUserLogin(ctx context.Context, flags PetstoreUserLoginFlags) error
PetstoreUserLogout(ctx context.Context) error
PetstoreUserGet(ctx context.Context, flags PetstoreUserGetFlags) error
PetstoreUserUpdate(ctx context.Context, flags PetstoreUserUpdateFlags) error
PetstoreUserDelete(ctx context.Context, flags PetstoreUserDeleteFlags) error
HelpFunc(cmd *spec.CommandItem)
UsageFunc(cmd *spec.CommandItem) error
IOStreams() IOStreams
Version() string
}
Every method corresponds to a command defined in the spec. 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. A few helper methods (HelpFunc, UsageFunc, IOStreams, Version) round out the interface with sensible defaults.
Step 4: Implement the Actions Interface
This is where you write your actual business logic. Create a type that satisfies ActionsInterface — and because it’s just a Go interface, the pattern feels familiar if you’ve used oapi-codegen with OpenAPI specs.
The benefits of this contract-first approach are significant:
- Your spec is your contract — define the CLI surface first, then implement against a generated interface
- Framework agnostic business logic — your
Actionstype has zero dependencies on Cobra or any CLI framework - Always in sync — documentation, and implementation all stay in sync with the OpenCLI Spec document as the source of truth
Start by creating a new package for your implementation:
mkdir -p ./internal/cliapp
touch ./internal/cliapp/actions.go
Define your Actions type:
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:
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 ...
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):
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
}
Step 5: Wire Up the Entry Point
The final piece is a minimal main.go that bootstraps everything:
mkdir -p cmd/petstore
touch cmd/petstore/main.go
And we add the code:
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)
}
We need just three lines of substance, and critically, no dependency on Cobra in your ‘user-land’ code:
Try it Out
That’s the entire application. Let’s run it:
$ 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
$ 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.
Benefits of OpenCLI
Building CLIs traditionally means writing repetitive boilerplate: flag definitions, argument parsing, help text, error handling, version commands; multiply that by every subcommand and it gets tedious, fast. OpenCLI eliminates that tedium while giving you something better than manual authoring:
- Single source of truth - your spec drives the code, the docs, and the validation. Change one file and regenerate.
- Contract-first development - design your CLI interface before writing implementation logic, the same way OpenAPI enables contract-first API design.
- Framework-agnostic architecture - swap Cobra for urfave/cli by changing a flag in
ocli gen cli. Your business logic doesn’t change. (Other CLI frameworks/languages are also supported) - AI-friendly - an OpenCLI document lets AI tools understand your CLI surface without scanning hundreds of lines of code, saving tokens and improving accuracy.
- Deterministic generation - run
ocli gen clitoday or next month and get the same result. (oh, and it won’t burn your precious tokens generating the scaffolding)
Get Started
Try OpenCLI spec and ocli on your next CLI project:
- Read the OpenCLI Specification to understand the schema
- Experiment with the live editor to see docs generated in real time
- Install
ocliand generate your first codebase:go install github.com/bcdxn/opencli/cmd/ocli@latest - Explore examples and contribute at github.com/bcdxn/opencli
OpenCLI is in alpha — the core patterns are proven, and community feedback now shapes what comes next.