Define your CLI declaratively,
write its behavior imperatively.
Validate. Generate. Ship.
Rotini is a spec-driven CLI package for Go, with a codegen tool and a runtime library. You declare your commands, flags and arguments in a spec file (YAML, JSON, JSONC or TOML). Rotini validates it and generates the typed Go, the command tree and a handler stub per command. You write each command's handler, and the runtime parses and validates input before calling it. The loop is: edit the spec, generate, provide/update handler implementations, build.
Quick start
Requires Go 1.27 or later.
1. Initialize
terminal
mkdir helloworld
cd helloworld
go mod init github.com/me/helloworld
go get -tool github.com/go-rotini/rotini/cmd/rotini@latest
go get github.com/go-rotini/rotini@latest
go tool rotini init helloworldwhat init writes
cmd/helloworld/
.rotini.spec.yaml the spec — what the CLI accepts
.rotini.conf.yaml the conf — where generated code goes
.rotini-schema.*.json schemas for editor completion
main.go the entrypoint
internal/cmd/helloworld/
zz_rotini.go generated on every `go generate` — don't edit
helloworld*.go one handler file per command — yours to edit2. Add a command to the spec
cmd/helloworld/.rotini.spec.yaml — under commands:
- name: hello
summary: say hello
arguments:
- name: name
summary: who to greet
schema: { type: string, default: world }
flags:
- name: shout
summary: greet in capitals
identifiers: [-s, --shout]
schema: { type: bool }
- name: help
summary: print help
identifiers: [-h, --help]
schema: { type: bool }3. Generate, and fill in the handler
go generate ./... creates internal/cmd/helloworld/helloworld_hello.go. Replace its TODO with the command’s work, and add "strings" to its imports:
internal/cmd/helloworld/helloworld_hello.go
func (*helloworldHelloHandlers) Run(ctx context.Context, rtx *rotini.Context) {
if argv, err := rotini.ParseArgv[HelloworldHelloInputs](rtx); err == nil {
if argv.Values.HelloworldHello.Flags.Help {
fmt.Fprintln(rtx.Stdout, rtx.Help())
rtx.HaltWithCode(0)
return
}
}
inputs, err := rotini.Collect[HelloworldHelloInputs](rtx)
if err != nil {
rtx.HaltWith(err)
return
}
greeting := "hello, " + inputs.HelloworldHello.Arguments.Name
if inputs.HelloworldHello.Flags.Shout {
greeting = strings.ToUpper(greeting)
}
fmt.Fprintln(rtx.Stdout, greeting)
}4. Build and run
terminal
go generate ./...
go build ./cmd/...
./helloworld hello --shout rotini # HELLO, ROTINI
./helloworld --helpChange the spec, go generate ./..., fill in any new handler, build — that’s the whole loop.
Next
- README.md — using rotini in depth: inputs, handlers, errors, testing, composition
- .rotini.spec.yaml — every spec key
- .rotini.conf.yaml — every conf key
- api.go — what you call from
main.goand your handlers - cli — the
rotinicommand