A scaffold generator for Go CLI applications built with peterbourgon/ff/v4.
Climax generates the boilerplate for a structured, idiomatic CLI: one package per command, a shared root Config that threads stdin/stdout/stderr through the whole tree, signal-safe shutdown, and a dispatcher that routes arguments to the matching command. New commands can be added at any time — climax uses AST analysis to register them correctly even if you've edited the generated files or renamed the dispatcher.
go install github.com/StevenACoffman/climax@latest# Create a new module
mkdir myapp && cd myapp
go mod init github.com/yourname/myapp
# Scaffold the application
climax init
# Run it immediately
go run . --help
# Add commands
climax add serve
climax add config
climax add create --parent config # nested under config
# Build
go build -o myapp .Generates a complete CLI application skeleton at path (default: current directory). The path must be inside an existing Go module.
Generated files:
myapp/
main.go # entry point with signal-safe shutdown
cmd/
cmd.go # dispatcher: routes args to commands
root/
root.go # shared Config (Stdin, Stdout, Stderr, ff.Command)
version/
version.go # version command (skip with --no-version)
Example output:
initialized climax app at /home/user/myapp (import: github.com/yourname/myapp)
created main.go
created cmd/cmd.go
created cmd/root/root.go
created cmd/version/version.go
Flags:
| Flag | Default | Description |
|---|---|---|
--name |
last import path segment | CLI name used in usage strings (allows hyphens) |
--short |
"TODO: describe <name> here" |
ShortHelp for the root command |
--long |
(omitted) | LongHelp for the root command |
--root-pkg |
root |
Go package name for the root config package |
--no-version |
false | Skip generating cmd/version/version.go |
Adds a new command package at cmd/<name>/<name>.go and registers it in the dispatcher. The path must be the root of an application created by climax init.
Registration uses AST analysis to locate the correct insertion point, so it works reliably even if you've removed the generated marker comments, restructured the dispatcher, or renamed it from cmd.go to command.go.
Example output:
added command "serve"
created cmd/serve/serve.go
modified cmd/cmd.go
Flags:
| Flag | Default | Description |
|---|---|---|
--name |
same as <name> |
ff.Command.Name in the generated file (allows hyphens) |
--short |
"<name> command" |
ShortHelp for the generated command |
--long |
"<Name> is a new command." |
LongHelp for the generated command |
-p, --parent |
root package | Go package name of the parent command |
Nesting commands:
Use the Go package name (not a variable name) as the --parent value:
climax add config
climax add create --parent config # creates cmd/create/create.go under configPrints build and version information for the climax binary, read from the module's embedded build info:
GitVersion: v0.3.1
GitCommit: a1b2c3d4e5f6...
GitTreeState: clean
BuildDate: 2025-11-01T12:00:00
BuiltBy: unknown
GoVersion: go1.24.0
Compiler: gc
ModuleSum: h1:...
Platform: darwin/arm64
Use --json for machine-readable output:
climax version --json{
"gitVersion": "v0.3.1",
"gitCommit": "a1b2c3d4e5f6...",
"gitTreeState": "clean",
"buildDate": "2025-11-01T12:00:00",
"builtBy": "unknown",
"goVersion": "go1.24.0",
"compiler": "gc",
"moduleChecksum": "h1:...",
"platform": "darwin/arm64"
}Checks a climax-based application for structural drift from the current scaffold templates. The path defaults to the current directory, which must be the root of an application created by climax init.
Each issue is shown as a focused unified diff:
── cmd/cmd.go: stdin io.Reader parameter in Run, stdin forwarded to root.New
--- a/cmd/cmd.go
+++ b/cmd/cmd.go (expected per climax template)
@@ structural pattern @@
-func Run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
- r := root.New(stdout, stderr)
+func Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
+ r := root.New(stdin, stdout, stderr)
Expected patterns are derived directly from the embedded template files in pkg/scaffold/templates/, so the lint checks automatically stay in sync whenever templates are updated.
Exits with status 1 when any issues are found, making it suitable for CI checks on generated apps.
Detects structural drift between climax's own source files and the scaffold template files in pkg/scaffold/templates/. This is a climax development tool — run it after changing a structural pattern in main.go, cmd/cmd.go, or cmd/root/root.go to check whether the templates need updating.
Drift detected: 3 item(s) (3 fixable with --apply)
✗ main signal.NotifyContext
✗ main run() separation
✗ cmd stdin io.Reader parameter in Run
Without --apply it reports drift and exits non-zero (useful in CI). With --apply it patches the template files in place for each fixable item. Items where the template has a property the source doesn't are flagged for manual review.
After climax init followed by climax add serve, climax add config, and climax add create --parent config:
myapp/
main.go
cmd/
cmd.go
root/
root.go
version/
version.go
serve/
serve.go
config/
config.go
create/
create.go
Run any command:
go run . serve
go run . config
go run . config create
go run . help config createmain.go uses signal.NotifyContext so Ctrl-C cancels the context cleanly:
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
run(ctx)
}run is intentionally separated from main to allow test harnesses to call it directly.
stdin, stdout, and stderr are passed explicitly from main through the dispatcher down to every command's Config. Nothing uses os.Stdout directly after main.go. This makes commands testable without capturing global state:
// cmd/cmd.go
func Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
r := root.New(stdin, stdout, stderr)
...
}
// cmd/root/root.go
type Config struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
Flags *ff.FlagSet
Command *ff.Command
}Each command lives in its own package and embeds *root.Config to inherit the shared I/O:
// cmd/serve/serve.go
type Config struct {
*root.Config
Port int
Flags *ff.FlagSet
Command *ff.Command
}
func New(parent *root.Config) *Config { ... }
func (cfg *Config) exec(ctx context.Context, args []string) error { ... }A child command embeds its parent's Config instead of *root.Config, giving it access to both the shared I/O and any flags the parent defines:
// cmd/create/create.go
type Config struct {
*config.Config // embeds the config command's Config
Flags *ff.FlagSet
Command *ff.Command
}root.ExitError lets a command exit with a specific non-zero code without printing an error: ... line. The dispatcher skips help-text printing for ExitError, and run() in main.go calls os.Exit directly:
// In any command's exec function:
if noIssues {
return nil // exit 0
}
return root.ExitError(1) // exit 1, no "error:" printedThis is used by climax lint and climax update to signal failure to CI without redundant error output.
The generated cmd/version/version.go reads the module version automatically from the Go toolchain's embedded build info. When a binary is installed via go install or built from a tagged release, the version is set without any extra build flags:
go install github.com/yourname/myapp@v1.2.3
myapp version
# v1.2.3For local or untagged builds, the version defaults to "dev". Override it at link time if needed:
go build -ldflags "-X 'github.com/yourname/myapp/cmd/version.Version=v1.2.3'" -o myapp .