❯ rotini --documentation█
rotini

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 helloworld
what 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 edit

2. 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 --help

Change the spec, go generate ./..., fill in any new handler, build — that’s the whole loop.


Next