protocli

package module
v0.0.0-...-798c737 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 2, 2026 License: MIT Imports: 26 Imported by: 0

README

proto-cli

PR Validation BSR

Automatically generate production-ready CLI applications from your gRPC service definitions.

proto-cli is a protoc plugin that transforms Protocol Buffer service definitions into feature-rich command-line interfaces. Define your API once, get a complete CLI with proper flag handling, configuration management, lifecycle hooks, and both local and remote execution.

📦 Available on Buf Schema Registry: Import CLI annotations directly in your proto files:

deps:
  - buf.build/fernet/proto-cli

Features

Core Capabilities
  • Zero Boilerplate - Define services in .proto, generate complete CLIs automatically
  • Type-Safe Generation - Clean, idiomatic Go code via jennifer
  • Dual Execution Modes - Run in-process (direct calls) or remote (gRPC client)
  • Streaming Support - Server-side streaming RPCs with line-delimited output (NDJSON, YAML)
  • Multi-Service CLIs - Organize multiple services under one CLI with nested commands
Configuration & Customization
  • Configuration Loading - YAML/JSON config files with environment variable overrides
  • Optional Fields - Full proto3 optional field support with explicit presence tracking
  • Custom Deserializers - Transform CLI flags into complex proto messages
  • Lifecycle Hooks - Before/after command execution, daemon startup/ready/shutdown
  • gRPC Interceptors - Add unary and stream interceptors for logging, auth, metrics
Output & Display
  • Multiple Formats - JSON, YAML, and Go-native formatting built-in
  • Template Formats - Create custom formats using Go text templates
  • Format-Specific Flags - Custom flags per format (e.g., --pretty for JSON)
  • Streaming Output - NDJSON for JSON, document-delimited for YAML
Service Management
  • Flat Command Structure - Hoist service commands to root level for single-service CLIs
  • Selective Service Enable - Start daemon with specific services: --service userservice
  • Collision Detection - Clear errors when command names conflict in hoisted services
Developer Experience
  • CLI Annotations - Customize command names, flags, descriptions via proto options
  • Type-Safe Options API - Functional options pattern for configuration
  • Built on urfave/cli v3 - Modern, well-tested CLI framework

Quick Start

Installation

Add to buf.gen.yaml:

version: v2
plugins:
  - local: ["go", "run", "google.golang.org/protobuf/cmd/protoc-gen-go"]
    out: .
    opt: [paths=source_relative]
  - local: ["go", "run", "google.golang.org/grpc/cmd/protoc-gen-go-grpc"]
    out: .
    opt: [paths=source_relative]
  - local: ["go", "run", "github.com/drewfead/proto-cli/cmd/gen"]
    out: .
    opt: [paths=source_relative]
Basic Example

1. Define your service (example.proto):

import "internal/clipb/cli.proto";

service UserService {
  option (cli.service) = {
    name: "user-service"
    description: "Manage users"
  };

  rpc GetUser(GetUserRequest) returns (UserResponse) {
    option (cli.command) = {
      name: "get"
      description: "Retrieve a user by ID"
    };
  }
}

message GetUserRequest {
  int64 id = 1 [(cli.flag) = {name: "id", usage: "User ID"}];
  optional string fields = 2 [(cli.flag) = {name: "fields", usage: "Fields to return"}];
}

2. Generate code:

buf generate

3. Build your CLI (main.go):

ctx := context.Background()

// Create service implementation
userServiceCLI := simple.UserServiceCommand(ctx, &userService{},
    protocli.WithOutputFormats(protocli.JSON(), protocli.YAML()),
)

// Create root command
rootCmd, err := protocli.RootCommand("usercli",
    protocli.Service(userServiceCLI),
    protocli.WithEnvPrefix("USERCLI"),
)

rootCmd.Run(ctx, os.Args)

4. Use your CLI:

# Direct call (in-process)
./usercli user-service get --id 1

# Start gRPC server
./usercli daemonize --port 50051

# Remote call
./usercli user-service get --id 1 --remote localhost:50051

Examples

Simple Example

Basic CRUD operations with configuration loading, custom deserializers, and multi-service support.

Highlights:

  • Configuration from YAML files and environment variables
  • Optional proto3 fields with explicit presence
  • Custom timestamp deserializers
  • Nested configuration messages
  • Multi-service CLI (UserService + AdminService)

Try it:

make build/example
./bin/usercli user-service get --id 1 --format json
./bin/usercli daemonize --port 50051
Streaming Example

Server-side streaming RPCs with line-delimited output formats.

Highlights:

  • Server streaming RPC support
  • NDJSON output for JSON format
  • Offset and filtering with optional fields
  • Works with Unix tools (jq, grep, wc)

Try it:

go build -o bin/streamcli ./examples/streaming/streamcli
./bin/streamcli streaming-service list-items --category books --format json | jq .
Flat Command Structure

Single-service CLIs with commands at the root level using protocli.Hoisted().

Comparison:

  • Nested: ./usercli user-service get --id 1
  • Flat: ./usercli-flat get --id 1

Usage:

rootCmd, err := protocli.RootCommand("usercli-flat",
    protocli.Service(userServiceCLI, protocli.Hoisted()),
)

See usercli_flat/README.md for details.

Key Features

Configuration Loading

Define configuration in your proto file using the service_config annotation:

// Define your configuration message
message UserServiceConfig {
  string database_url = 1 [(cli.flag) = {
    name: "db-url"
    usage: "PostgreSQL connection URL"
  }];
  int64 max_connections = 2 [(cli.flag) = {
    name: "max-conns"
    usage: "Maximum database connections"
  }];
}

// Attach config to your service
service UserService {
  option (cli.service_config) = {
    config_message: "UserServiceConfig"
  };

  rpc GetUser(GetUserRequest) returns (UserResponse);
}

Implement a factory function that receives the config:

func newUserService(config *simple.UserServiceConfig) simple.UserServiceServer {
    log.Printf("DB URL: %s, Max Conns: %d", config.DatabaseUrl, config.MaxConnections)
    return &userService{dbURL: config.DatabaseUrl}
}

// Register the factory
rootCmd := protocli.RootCommand("usercli",
    protocli.Service(userServiceCLI),
    protocli.WithConfigFactory("userservice", newUserService),
)

Load configuration from YAML files:

# usercli.yaml
services:
  userservice:
    database-url: postgresql://localhost:5432/users
    max-connections: 25

Override with environment variables:

USERCLI_SERVICES_USERSERVICE_DATABASE_URL=postgresql://prod/db ./usercli daemonize

Debugging Configuration Issues

Enable debug logging to see which config files are loaded and how values are merged:

# Use debug verbosity to see config loading details
./usercli daemonize --verbosity=debug

# Output shows:
# - Which config files were found/missing
# - Environment variables applied
# - Final merged configuration

Programmatically inspect configuration loading:

loader := protocli.NewConfigLoader(
    protocli.DaemonMode,
    protocli.FileConfig("./config.yaml"),
    protocli.EnvPrefix("MYAPP"),
    protocli.DebugMode(true),  // Enable debug tracking
)

config := &myapp.ServiceConfig{}
err := loader.LoadServiceConfig(nil, "myservice", config)

// Get detailed debug information
debug := loader.DebugInfo()
fmt.Printf("Paths checked: %v\n", debug.PathsChecked)
fmt.Printf("Files loaded: %v\n", debug.FilesLoaded)
fmt.Printf("Files failed: %v\n", debug.FilesFailed)
fmt.Printf("Env vars applied: %v\n", debug.EnvVarsApplied)
fmt.Printf("Final config: %+v\n", debug.FinalConfig)

Common Issues:

  1. Config file not found: Check debug.PathsChecked to see where the CLI looked for config files
  2. Values not applied: Check debug.EnvVarsApplied to verify environment variable names (they must match the prefix + field path)
  3. Wrong precedence: Remember: CLI flags > environment variables > config files
  4. Field naming: Proto fields use kebab-case in YAML (e.g., database_urldatabase-url)

See config_debug_test.go and nested_config_test.go for more examples.

Custom Flag Deserializers

Transform CLI flags into complex proto messages:

protocli.WithFlagDeserializer("google.protobuf.Timestamp",
    func(ctx context.Context, flags protocli.FlagContainer) (proto.Message, error) {
        t, err := time.Parse(time.RFC3339, flags.String())
        if err != nil {
            return nil, err
        }
        return timestamppb.New(t), nil
    },
)

See timestamp_deserializer_test.go.

Template-Based Output Formats

Create custom output formats using Go text templates without writing format code:

// Define templates for each message type
templates := map[string]string{
    "example.UserResponse": `User: {{.user.name}} (ID: {{.user.id}})
Email: {{.user.email}}
{{if .user.address}}Address: {{.user.address.city}}, {{.user.address.state}}{{end}}`,

    "example.CreateUserRequest": `Creating user: {{.name}} <{{.email}}>`,
}

// Create the format
userFormat, err := protocli.TemplateFormat("user-detail", templates)

// Use in CLI
userServiceCLI := simple.UserServiceCommand(ctx, newUserService,
    protocli.WithOutputFormats(userFormat, protocli.JSON()),
)

With Custom Template Functions:

funcMap := template.FuncMap{
    "upper": strings.ToUpper,
    "date": func(ts string) string {
        // Custom date formatting
        return formattedDate
    },
}

format, err := protocli.TemplateFormat("custom", templates, funcMap)

Template Features:

  • Access all message fields as {{.fieldName}}
  • Conditionals: {{if .field}}...{{end}}
  • Loops: {{range .list}}...{{end}}
  • Custom functions via template.FuncMap
  • Nested field access: {{.user.address.city}}
  • Format any message type by fully qualified name

Common Use Cases:

  • Table formats: Create ASCII tables with printf for alignment
  • Compact formats: One-line summaries like {{.name}} <{{.email}}>
  • CSV/TSV: {{.id}},{{.name}},{{.email}}
  • Custom business formats: Match your organization's output standards

See template_format_test.go for comprehensive examples.

Lifecycle Hooks

Add hooks for logging, authentication, metrics:

protocli.RootCommand("usercli",
    protocli.Service(userServiceCLI),
    protocli.BeforeCommand(func(ctx context.Context, cmd *cli.Command) error {
        log.Printf("Executing: %s", cmd.Name)
        return nil
    }),
    protocli.OnDaemonStartup(func(ctx context.Context, server *grpc.Server, mux *runtime.ServeMux) error {
        // Register additional services, configure server
        return nil
    }),
    protocli.OnDaemonReady(func(ctx context.Context) {
        log.Println("Server is ready")
    }),
    protocli.OnDaemonShutdown(func(ctx context.Context) {
        log.Println("Shutting down gracefully")
    }),
)

See daemon_lifecycle_test.go for complete examples.

Help Text Customization

Proto-CLI follows urfave/cli v3 best practices for help text. Customize help at multiple levels:

Proto Annotations:

Use proto annotations to define help text fields following urfave/cli v3 conventions:

service UserService {
  option (cli.service) = {
    name: "user-service",
    description: "User management commands",  // Short one-liner
    long_description: "Detailed explanation of the service...",  // Multi-paragraph
    usage_text: "user-service [command] [options]",  // Custom USAGE line
    args_usage: "[filter-expression]"  // Argument description
  };

  rpc GetUser(GetUserRequest) returns (UserResponse) {
    option (cli.command) = {
      name: "get",
      description: "Retrieve a user by ID",  // Short (shown in lists)
      long_description: "Fetch detailed user information...\n\nExamples:\n  usercli get --id 123",
      usage_text: "get --id <user-id> [options]",  // Override auto-generated USAGE
      args_usage: "<user-id>"  // Describe positional args
    };
  }
}

Help Field Guidelines (urfave/cli v3):

  • description: Short one-liner for command lists (e.g., "retrieve a user by ID")
  • long_description: Detailed explanation with examples and context
  • usage_text: Override auto-generated USAGE line format
  • args_usage: Describe expected arguments

Programmatic Customization:

// Method 1: Custom root command template
rootCmd, _ := protocli.RootCommand("myapp",
    protocli.Service(userServiceCLI),
    protocli.WithRootCommandHelpTemplate(`
NAME:
   {{.Name}} - {{.Usage}}

USAGE:
   {{.HelpName}} {{if .VisibleFlags}}[options]{{end}} command

VERSION:
   {{.Version}}

WEBSITE:
   https://example.com
`),
)

// Method 2: Full control with HelpCustomization
protocli.WithHelpCustomization(&protocli.HelpCustomization{
    RootCommandHelpTemplate: myTemplate,
    CommandHelpTemplate: myCommandTemplate,
    SubcommandHelpTemplate: mySubcommandTemplate,
})

// Method 3: Modify the returned root command
rootCmd, _ := protocli.RootCommand("myapp", protocli.Service(userServiceCLI))
rootCmd.Version = "1.0.0"
rootCmd.Copyright = "(c) 2026 MyCompany"
rootCmd.Authors = []any{"John Doe <john@example.com>"}

Best Practices:

  • Keep description concise (one line) for readability in command lists
  • Use long_description for detailed explanations, examples, and context
  • Follow the urfave/cli v3 help conventions
  • Include examples in long_description to aid discovery
Streaming RPCs

Server streaming RPCs output line-delimited messages:

# JSON format produces NDJSON (one JSON object per line)
./streamcli streaming-service list-items --format json
{"item":{"id":"1","name":"Item 1"}}
{"item":{"id":"2","name":"Item 2"}}

# Works with jq and other Unix tools
./streamcli streaming-service list-items --format json | jq 'select(.item.id > "1")'

See streaming example for details.

Optional Fields

Full support for proto3 optional fields with explicit presence:

message CreateUserRequest {
  string name = 1;  // Required
  optional string nickname = 2;  // Optional with presence tracking
  optional int32 age = 3;  // Only set if flag provided
}

Only sets the field if the flag is provided:

./usercli user-service create --name "Alice"  # nickname and age unset
./usercli user-service create --name "Alice" --nickname "ace"  # nickname set
gRPC Interceptors

Add interceptors for cross-cutting concerns:

protocli.WithUnaryInterceptor(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
    start := time.Now()
    resp, err := handler(ctx, req)
    log.Printf("%s took %v", info.FullMethod, time.Since(start))
    return resp, err
}),
protocli.WithStreamInterceptor(streamLoggingInterceptor),
Selective Service Enable

Start daemon with only specific services:

# Start all services
./usercli daemonize --port 50051

# Start only userservice and productservice
./usercli daemonize --port 50051 --service userservice --service productservice

CLI Annotations

Customize generated CLIs using proto options from internal/clipb/cli.proto:

service UserService {
  option (cli.service) = {
    name: "users"  // Command name (default: snake_case service name)
    description: "User management commands"
  };

  rpc GetUser(GetUserRequest) returns (UserResponse) {
    option (cli.command) = {
      name: "get"  // Subcommand name
      description: "Retrieve user by ID"
    };
  }
}

message GetUserRequest {
  int64 id = 1 [(cli.flag) = {
    name: "id"
    shorthand: "i"
    usage: "User ID to retrieve"
  }];
}

Development

Prerequisites
Building
# Generate code
make generate

# Run tests
make test

# Run linter
make lint

# Build examples
make build/example
Project Structure
proto-cli/
├── cmd/gen/          # Code generator (protoc plugin)
├── examples/
│   ├── simple/       # Basic CRUD example
│   │   ├── usercli/      # Multi-service CLI
│   │   └── usercli_flat/ # Flat command structure
│   └── streaming/    # Server streaming example
├── internal/clipb/   # CLI annotation proto definitions
├── root.go           # Root command implementation
├── options.go        # Configuration options
├── config.go         # Configuration loading
└── formats.go        # Output formatters

Contributing

Contributions welcome! Please:

  1. Add tests for new features
  2. Run make lint before submitting
  3. Update documentation

License

MIT License

Acknowledgments

Documentation

Overview

Package protocli provides framework features for generated gRPC CLI commands.

This package is automatically imported by generated CLI code and provides lifecycle hooks, output formatting, configuration options, and utilities for CLI applications.

Options

The Option type provides a functional options pattern for configuring CLI behavior:

cmd := proto.BuildUserServiceCLI(ctx, impl,
    protocli.BeforeCommand(func(ctx context.Context, cmd *cli.Command) error {
        log.Printf("Running command: %s", cmd.Name)
        return nil
    }),
    protocli.WithAfterCommand(func(ctx context.Context, cmd *cli.Command) error {
        log.Printf("Completed command: %s", cmd.Name)
        return nil
    }),
    protocli.WithOutputFormats(
        protocli.JSON(),
        protocli.YAML(),
    ),
)

Lifecycle Hooks

  • BeforeCommand: Runs before each command execution
  • AfterCommand: Runs after each command execution

Hooks receive the context and command, allowing for logging, validation, metrics collection, or other cross-cutting concerns.

Output Formats

The CLI automatically supports --format and --output flags for all commands:

  • --format: Specifies output format (go, json, yaml, or custom)
  • --output: Specifies output file (- or empty for stdout)

Built-in formats (use factory functions to create them):

  • protocli.Go(): Default Go %+v formatting (automatically used if no formats registered)
  • protocli.JSON(): JSON output with optional --pretty flag
  • protocli.YAML(): YAML-style output

If no formats are explicitly registered via WithOutputFormats, the Go format is used as the default. Custom formats can be registered and will define additional flags (e.g., --pretty for JSON) that are automatically added to all commands.

Example custom format without additional flags:

type CSVFormat struct{}

func (f *CSVFormat) Name() string { return "csv" }

func (f *CSVFormat) Format(ctx context.Context, cmd *cli.Command, w io.Writer, msg proto.Message) error {
    // Format as CSV...
    return nil
}

Example custom format with additional flags (implements FlagConfiguredOutputFormat):

type CSVFormat struct{}

func (f *CSVFormat) Name() string { return "csv" }

func (f *CSVFormat) Flags() []cli.Flag {
    return []cli.Flag{
        &cli.StringFlag{Name: "delimiter", Value: ",", Usage: "CSV delimiter"},
    }
}

func (f *CSVFormat) Format(ctx context.Context, cmd *cli.Command, w io.Writer, msg proto.Message) error {
    delimiter := cmd.String("delimiter")
    // Format as CSV...
    return nil
}

The Flags() method is optional - implement it only if your format needs custom flags. The generated CLI code checks for the FlagConfiguredOutputFormat interface at runtime.

Template-Based Formats

For simpler cases, use TemplateFormat to create formats using Go text templates:

templates := map[string]string{
    "example.UserResponse": `User: {{.user.name}} ({{.user.email}})`,
    "example.CreateUserRequest": `Creating: {{.name}}`,
}
format, err := protocli.TemplateFormat("user-compact", templates)

Templates support:

  • Field access: {{.fieldName}}
  • Conditionals: {{if .field}}...{{end}}
  • Loops: {{range .list}}...{{end}}
  • Custom functions via template.FuncMap
  • Nested fields: {{.user.address.city}}

Message types are identified by fully qualified name (e.g., "example.UserResponse").

Example (TemplateFormat)

Example_templateFormat demonstrates template format usage

package main

import (
	protocli "github.com/drewfead/proto-cli"
)

func main() {
	// Define templates for message types
	templates := map[string]string{
		"example.UserResponse": `{{$f := protoFields .}}User: {{$f.user.name}} (ID: {{$f.user.id}})
Email: {{$f.user.email}}
Status: {{if $f.user.verified}}Verified{{else}}Unverified{{end}}`,
	}

	// Create the format
	format, err := protocli.TemplateFormat("user-detail", templates)
	if err != nil {
		panic(err)
	}

	// Use in CLI
	_ = format // protocli.WithOutputFormats(format)

}

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrUnexpectedFieldValueType = errors.New("unexpected field value type")
	ErrOverflow                 = errors.New("overflow")
)
View Source
var (
	ErrWrongConfigType            = errors.New("wrong config type")
	ErrAmbiguousCommandInvocation = errors.New("more than one action registered for the same command")
)
View Source
var ErrNoTemplate = errors.New("no template registered for message type")

ErrNoTemplate is returned when no template is registered for a message type.

View Source
var ErrUnknownField = errors.New("unknown field")

Functions

func CallFactory

func CallFactory(factory any, config proto.Message) (any, error)

CallFactory calls a factory function with a config message using reflection. Returns the service implementation.

func DefaultConfigPaths

func DefaultConfigPaths(rootCommandName string) []string

DefaultConfigPaths returns default paths for config files.

func DefaultTemplateFunctions

func DefaultTemplateFunctions() template.FuncMap

DefaultTemplateFunctions returns the default set of template functions. These functions are available in all templates unless overridden.

func NewConfigMessage

func NewConfigMessage(configType proto.Message) proto.Message

NewConfigMessage creates a new config message instance using the proto registry. The configType should be a pointer to the config message type (e.g., &UserServiceConfig{}).

func NewDaemonizeCommand

func NewDaemonizeCommand(_ context.Context, services []*ServiceCLI, _ ServiceConfig) *cli.Command

NewDaemonizeCommand creates a daemonize command for the given services. This is useful for single-service CLIs using the flat command structure.

func RootCommand

func RootCommand(appName string, opts ...RootOption) (*cli.Command, error)

RootCommand creates a root CLI command with the given app name and options. Returns an error if there are naming collisions between hoisted service commands.

Types

type ConfigDebugInfo

type ConfigDebugInfo struct {
	PathsChecked   []string          // All paths that were checked
	FilesLoaded    []string          // Paths that were successfully loaded
	FilesFailed    map[string]string // Paths that failed with error message
	EnvVarsApplied map[string]string // Env vars that were applied (name -> value)
	FlagsApplied   map[string]string // CLI flags that were applied (name -> value)
	FinalConfig    any               // Final merged config (for display)
}

ConfigDebugInfo tracks config loading for debugging.

type ConfigLoader

type ConfigLoader struct {
	// contains filtered or unexported fields
}

ConfigLoader loads configuration with precedence: CLI flags > env vars > files.

func NewConfigLoader

func NewConfigLoader(mode ConfigMode, opts ...ConfigLoaderOption) *ConfigLoader

NewConfigLoader creates a new config loader with options.

func (*ConfigLoader) DebugInfo

func (l *ConfigLoader) DebugInfo() *ConfigDebugInfo

DebugInfo returns the config debug information (only populated if debug mode is enabled).

func (*ConfigLoader) LoadServiceConfig

func (l *ConfigLoader) LoadServiceConfig(
	cmd *cli.Command,
	serviceName string,
	target proto.Message,
) error

LoadServiceConfig loads config for a specific service.

serviceName: lowercase service name (e.g., "userservice") target: pointer to config message instance

type ConfigLoaderOption

type ConfigLoaderOption func(*ConfigLoader)

ConfigLoaderOption is a functional option for configuring a ConfigLoader.

func DebugMode

func DebugMode(enabled bool) ConfigLoaderOption

DebugMode enables config loading debug information.

func EnvPrefix

func EnvPrefix(prefix string) ConfigLoaderOption

EnvPrefix sets the environment variable prefix for config overrides.

func FileConfig

func FileConfig(paths ...string) ConfigLoaderOption

FileConfig adds config file paths to load.

func ReaderConfig

func ReaderConfig(readers ...io.Reader) ConfigLoaderOption

ReaderConfig adds io.Readers to load config from (for testing).

type ConfigMode

type ConfigMode int

ConfigMode determines which config sources are used.

const (
	// SingleCommandMode uses files + env + flags (all sources).
	SingleCommandMode ConfigMode = iota
	// DaemonMode uses files + env only (no CLI flag overrides).
	DaemonMode
)

type DaemonReadyHook

type DaemonReadyHook func(ctx context.Context)

DaemonReadyHook is called after the gRPC server is listening and ready to accept connections Errors must be handled within the hook (no error return).

type DaemonShutdownHook

type DaemonShutdownHook func(ctx context.Context)

DaemonShutdownHook is called during graceful shutdown after stop accepting new connections The context will be cancelled when the graceful shutdown timeout expires Errors must be handled within the hook (no error return).

type DaemonStartupHook

type DaemonStartupHook func(ctx context.Context, server *grpc.Server, mux *runtime.ServeMux) error

DaemonStartupHook is called before the gRPC server starts listening Receives the gRPC server instance and gateway mux (if transcoding is enabled) Returning an error prevents the daemon from starting.

type FlagConfiguredOutputFormat

type FlagConfiguredOutputFormat interface {
	OutputFormat

	// Flags returns additional flags this format needs (e.g., --pretty for JSON).
	Flags() []cli.Flag
}

FlagConfiguredOutputFormat is an optional interface for formats that need custom flags.

type FlagContainer

type FlagContainer interface {
	// Primary flag accessors (use the encapsulated flag name)
	String() string
	Int() int
	Int64() int64
	Uint() uint
	Uint64() uint64
	Bool() bool
	Float() float64
	StringSlice() []string
	IsSet() bool

	// Named flag accessors (for accessing other flags)
	StringNamed(flagName string) string
	IntNamed(flagName string) int
	Int64Named(flagName string) int64
	BoolNamed(flagName string) bool
	FloatNamed(flagName string) float64
	StringSliceNamed(flagName string) []string
	IsSetNamed(flagName string) bool

	// FlagName returns the primary flag name for this container
	FlagName() string
}

FlagContainer provides type-safe access to flag values for a specific flag It encapsulates the CLI command and flag name, exposing convenient accessors This abstraction allows deserializers to be reusable across different flag names

For deserializers that need to access multiple flags (e.g., top-level request deserializers), use the *Named() methods to read other flags by name.

func NewFlagContainer

func NewFlagContainer(cmd *cli.Command, flagName string) FlagContainer

NewFlagContainer creates a new FlagContainer for the given command and flag name.

type FlagDeserializer

type FlagDeserializer func(ctx context.Context, flags FlagContainer) (proto.Message, error)

FlagDeserializer builds a proto message from CLI flags This allows users to implement custom logic for constructing complex messages from simple CLI flags. The FlagContainer provides type-safe access to the flag value without requiring knowledge of the flag name, making deserializers reusable.

Example of a reusable timestamp deserializer:

func(ctx context.Context, flags FlagContainer) (proto.Message, error) {
    timeStr := flags.String()  // No need to know the flag name!
    t, err := time.Parse(time.RFC3339, timeStr)
    if err != nil {
        return nil, err
    }
    return timestamppb.New(t), nil
}

type HelpCustomization

type HelpCustomization struct {
	// RootCommandHelpTemplate overrides the default root command help template
	RootCommandHelpTemplate string

	// CommandHelpTemplate overrides the default command help template
	CommandHelpTemplate string

	// SubcommandHelpTemplate overrides the default subcommand help template
	SubcommandHelpTemplate string
}

HelpCustomization holds options for customizing help text display. Based on urfave/cli v3 help customization capabilities.

type LoggingConfigCallback

type LoggingConfigCallback func(ctx context.Context, config SlogConfigurationContext) *slog.Logger

LoggingConfigCallback is a function that configures the slog logger. It receives a context with configuration details and returns a configured logger.

type OutputFormat

type OutputFormat interface {
	// Name returns the format identifier (e.g., "json", "text")
	Name() string

	// Format writes the formatted proto message to the writer
	Format(ctx context.Context, cmd *cli.Command, w io.Writer, msg proto.Message) error
}

OutputFormat defines how to format proto messages for output.

func Go

func Go() OutputFormat

Go returns a new Go-style output format (uses %+v).

func JSON

func JSON() OutputFormat

JSON returns a new JSON output format with optional --pretty flag.

func MustTemplateFormat

func MustTemplateFormat(name string, templates map[string]string, funcMaps ...template.FuncMap) OutputFormat

MustTemplateFormat is like TemplateFormat but panics on error. Useful for package-level initialization where template errors should be caught at startup.

Example:

var userFormat = protocli.MustTemplateFormat("user", map[string]string{
    "example.UserResponse": `{{.user.name}} <{{.user.email}}>`,
})

func TemplateFormat

func TemplateFormat(name string, templates map[string]string, funcMaps ...template.FuncMap) (OutputFormat, error)

TemplateFormat creates an output format that renders proto messages using Go text templates.

Templates are specified as a map from fully qualified message type name to template string. The message type name format is "package.MessageName" (e.g., "example.UserResponse").

Templates receive the actual proto message directly. Custom template functions receive actual proto types (e.g., *timestamppb.Timestamp), not JSON strings or maps.

Default template functions available:

  • protoField: access message fields by JSON name, preserving proto types
  • protoJSON: convert proto message to JSON string
  • protoJSONIndent: convert proto message to indented JSON string
  • protoFields: convert proto message to map for dot-chain field access

Field access patterns:

  1. Use protoFields for easy dot-chain access (proto types become JSON types): {{$fields := protoFields .}}{{$fields.user.name}}
  2. Use protoField helper to preserve proto types: {{protoField . "fieldName"}}
  3. Register custom accessor functions for your message types (recommended for complex templates)

Optional function maps can be provided to add custom template functions. Functions are merged in order: defaults, global registry, then provided funcMaps.

Example with protoFields (simplest):

templates := map[string]string{
    "example.UserResponse": `{{$f := protoFields .}}User: {{$f.user.name}}
Email: {{$f.user.email}}
ID: {{$f.user.id}}`,
}

format := protocli.TemplateFormat("user-table", templates)

Example with custom accessor (best for proto types):

// Register type-specific accessor functions globally
protocli.TemplateFunctions().Register("user", func(resp *simple.UserResponse) *simple.User {
    return resp.GetUser()
})

protocli.TemplateFunctions().Register("formatTime", func(ts *timestamppb.Timestamp) string {
    if ts == nil || !ts.IsValid() {
        return "N/A"
    }
    return ts.AsTime().Format("2006-01-02")
})

templates := map[string]string{
    "example.UserResponse": `User: {{(user .).GetName}}
Created: {{formatTime (user .).GetCreatedAt}}`,
}

format := protocli.TemplateFormat("user-table", templates)

func YAML

func YAML() OutputFormat

YAML returns a new YAML output format.

type RootConfig

type RootConfig interface {
	Services() []*ServiceCLI
	GRPCServerOptions() []grpc.ServerOption
	EnableTranscoding() bool
	TranscodingPort() int
	ConfigPaths() []string
	EnvPrefix() string
	ServiceFactory(serviceName string) (any, bool)
	GracefulShutdownTimeout() time.Duration
	DaemonStartupHooks() []DaemonStartupHook
	DaemonReadyHooks() []DaemonReadyHook
	DaemonShutdownHooks() []DaemonShutdownHook
	LoggingConfig() LoggingConfigCallback
	DefaultVerbosity() string
	HelpCustomization() *HelpCustomization
}

RootConfig is the configuration returned by ApplyRootOptions. Used by RootCommand.

func ApplyRootOptions

func ApplyRootOptions(opts ...RootOption) RootConfig

ApplyRootOptions applies functional options and returns configured root settings.

type RootOnlyOption

type RootOnlyOption func(*rootCommandOptions)

RootOnlyOption is a concrete option type that only works with root level. It implements only the RootOption interface.

func ConfigureLogging

func ConfigureLogging(configFunc LoggingConfigCallback) RootOnlyOption

ConfigureLogging provides a custom slog logger configuration function. If not specified, the framework uses sensible defaults:

  • Single commands: human-friendly colored logs to stderr (via clilog.HumanFriendlySlogHandler)
  • Daemon mode: JSON-formatted logs to stdout

The function receives a context and a SlogConfigurationContext providing:

  • IsDaemon(): true for daemon mode, false for single commands
  • Level(): the configured log level from the --verbosity flag

IMPORTANT: Your custom logger factory MUST respect config.Level() to honor the --verbosity flag.

Type-safe: only works with RootOptions.

Example - Custom handler that respects verbosity:

protocli.ConfigureLogging(func(ctx context.Context, config protocli.SlogConfigurationContext) *slog.Logger {
    handler := clilog.HumanFriendlySlogHandler(os.Stderr, &slog.HandlerOptions{
        Level: config.Level(),  // IMPORTANT: Use config.Level() to respect --verbosity flag
    })
    return slog.New(handler)
})

Example - Different loggers for daemon vs single-command mode:

protocli.ConfigureLogging(func(ctx context.Context, config protocli.SlogConfigurationContext) *slog.Logger {
    if config.IsDaemon() {
        handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: config.Level()})
        return slog.New(handler)
    }
    handler := clilog.HumanFriendlySlogHandler(os.Stderr, &slog.HandlerOptions{Level: config.Level()})
    return slog.New(handler)
})

Example - Use the convenience function for always human-friendly logging:

protocli.ConfigureLogging(clilog.AlwaysHumanFriendly())

func OnDaemonReady

func OnDaemonReady(hook DaemonReadyHook) RootOnlyOption

OnDaemonReady registers a hook that runs after the gRPC server is listening and ready. Multiple hooks can be registered and will run in registration order. The hook cannot return errors - errors must be handled within the hook. Type-safe: only works with RootOptions.

func OnDaemonShutdown

func OnDaemonShutdown(hook DaemonShutdownHook) RootOnlyOption

OnDaemonShutdown registers a hook that runs during graceful shutdown. Multiple hooks can be registered and will run in REVERSE registration order. The hook runs after stop accepting new connections but before forcing shutdown. The context will be cancelled when the graceful shutdown timeout expires. The hook cannot return errors - errors must be handled within the hook. Type-safe: only works with RootOptions.

func OnDaemonStartup

func OnDaemonStartup(hook DaemonStartupHook) RootOnlyOption

OnDaemonStartup registers a hook that runs before the gRPC server starts listening. Multiple hooks can be registered and will run in registration order. The hook receives the gRPC server instance and gateway mux (may be nil if transcoding disabled). Returning an error prevents the daemon from starting. Type-safe: only works with RootOptions.

func Service

func Service(service *ServiceCLI, opts ...ServiceRegistrationOption) RootOnlyOption

Service registers a service CLI (root level only). Accepts optional ServiceRegistrationOptions to customize registration (e.g., Hoisted()). Type-safe: only works with RootOptions.

func WithConfigFactory

func WithConfigFactory(serviceName string, factory any) RootOnlyOption

WithConfigFactory registers a factory function for a service. The factory function takes a config message and returns a service implementation. Example: WithConfigFactory("userservice", func(cfg *UserServiceConfig) UserServiceServer { ... }). Type-safe: only works with RootOptions.

func WithConfigFile

func WithConfigFile(path string) RootOnlyOption

WithConfigFile adds a config file path to load. Can be called multiple times to specify multiple config files (deep merge). Type-safe: only works with RootOptions.

func WithDefaultVerbosity

func WithDefaultVerbosity(level slog.Level) RootOnlyOption

WithDefaultVerbosity sets the default verbosity level for the --verbosity flag. Accepts standard slog.Level values: slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError. Note: In slog, higher numeric values = less verbose logging:

  • slog.LevelDebug (-4) = most verbose
  • slog.LevelInfo (0) = normal
  • slog.LevelWarn (4) = warnings and errors only
  • slog.LevelError (8) = errors only
  • slog.Level(1000) or higher = effectively disables logging

Default is slog.LevelInfo if not specified. Users can still override via the --verbosity flag or -v shorthand. Type-safe: only works with RootOptions.

Example:

protocli.WithDefaultVerbosity(slog.LevelDebug)    // Most verbose (debug and above)
protocli.WithDefaultVerbosity(slog.LevelWarn)     // Less verbose (warn and error only)
protocli.WithDefaultVerbosity(slog.Level(1000))   // Disable logging

func WithEnvPrefix

func WithEnvPrefix(prefix string) RootOnlyOption

WithEnvPrefix sets the environment variable prefix for config overrides. Example: WithEnvPrefix("USERCLI") enables USERCLI_DB_URL env var. Type-safe: only works with RootOptions.

func WithGRPCServerOptions

func WithGRPCServerOptions(opts ...grpc.ServerOption) RootOnlyOption

WithGRPCServerOptions adds gRPC server options (e.g., for interceptors). Type-safe: only works with RootOptions.

func WithGracefulShutdownTimeout

func WithGracefulShutdownTimeout(timeout time.Duration) RootOnlyOption

WithGracefulShutdownTimeout sets the timeout for graceful daemon shutdown. Default is 15 seconds if not specified. During graceful shutdown, the daemon will wait for in-flight requests to complete. before forcefully terminating after this timeout. Type-safe: only works with RootOptions.

func WithHelpCustomization

func WithHelpCustomization(custom *HelpCustomization) RootOnlyOption

WithHelpCustomization sets custom help templates and printer functions. This allows full customization of help text display following urfave/cli v3 patterns.

Example:

protocli.WithHelpCustomization(&protocli.HelpCustomization{
    RootCommandHelpTemplate: myCustomTemplate,
    CustomizeRootCommand: func(cmd *cli.Command) {
        cmd.Usage = "My custom usage text"
    },
})

func WithRootCommandHelpTemplate

func WithRootCommandHelpTemplate(template string) RootOnlyOption

WithRootCommandHelpTemplate sets a custom template for root command help. This is a convenience function for the most common help customization.

Example:

protocli.WithRootCommandHelpTemplate(`
NAME:
   {{.Name}} - {{.Usage}}

USAGE:
   {{.HelpName}} {{if .VisibleFlags}}[options]{{end}} command [command options]

VERSION:
   {{.Version}}
`)

func WithStreamInterceptor

func WithStreamInterceptor(interceptor grpc.StreamServerInterceptor) RootOnlyOption

WithStreamInterceptor adds a stream interceptor to the gRPC server. Type-safe: only works with RootOptions.

func WithTranscoding

func WithTranscoding(httpPort int) RootOnlyOption

WithTranscoding enables gRPC-Gateway transcoding (HTTP/JSON to gRPC). This allows clients to call gRPC services via REST/JSON on the specified port. Type-safe: only works with RootOptions.

func WithUnaryInterceptor

func WithUnaryInterceptor(interceptor grpc.UnaryServerInterceptor) RootOnlyOption

WithUnaryInterceptor adds a unary interceptor to the gRPC server. Type-safe: only works with RootOptions.

type RootOption

type RootOption interface {
	// contains filtered or unexported methods
}

RootOption interface for root-level configuration.

type ServiceCLI

type ServiceCLI struct {
	Command             *cli.Command
	ServiceName         string                                   // Service name (e.g., "userservice")
	ConfigMessageType   string                                   // Config message type name (empty if no config)
	ConfigPrototype     proto.Message                            // Prototype config message instance (for cloning)
	FactoryOrImpl       any                                      // Factory function or direct service implementation
	RegisterFunc        func(*grpc.Server, any)                  // Register service with gRPC server (takes impl)
	GatewayRegisterFunc func(ctx context.Context, mux any) error // mux is *runtime.ServeMux from grpc-gateway
}

ServiceCLI represents a service CLI with its command and gRPC registration function.

type ServiceConfig

type ServiceConfig interface {
	BeforeCommandHooks() []func(context.Context, *cli.Command) error
	AfterCommandHooks() []func(context.Context, *cli.Command) error
	OutputFormats() []OutputFormat
	FlagDeserializer(messageName string) (FlagDeserializer, bool)
}

ServiceConfig is the configuration returned by ApplyServiceOptions. Used by generated service command code.

func ApplyServiceOptions

func ApplyServiceOptions(opts ...ServiceOption) ServiceConfig

ApplyServiceOptions applies functional options and returns configured service settings.

type ServiceOnlyOption

type ServiceOnlyOption func(*serviceCommandOptions)

ServiceOnlyOption is a concrete option type that only works with service level. It implements only the ServiceOption interface.

func WithFlagDeserializer

func WithFlagDeserializer(messageName string, deserializer FlagDeserializer) ServiceOnlyOption

WithFlagDeserializer registers a custom deserializer for a specific message type This allows users to implement custom logic for constructing complex proto messages from CLI flags, enabling advanced transformations and validation.

Example:

WithFlagDeserializer("GetUserRequest", func(ctx context.Context, cmd *cli.Command) (proto.Message, error) {
    // Custom logic to build GetUserRequest from flags
    userId := cmd.String("user-id")
    return &pb.GetUserRequest{
        UserId: userId,
        IncludeDetails: cmd.Bool("details"),
    }, nil
})

Type-safe: only works with ServiceOptions.

type ServiceOption

type ServiceOption interface {
	// contains filtered or unexported methods
}

ServiceOption interface for service-level configuration.

type ServiceRegistrationOption

type ServiceRegistrationOption func(*serviceRegistration)

ServiceRegistrationOption configures how a service is registered in the root command.

func Hoisted

func Hoisted() ServiceRegistrationOption

Hoisted returns an option that hoists service RPC commands to the root level. When hoisted, RPC commands appear as siblings of the daemonize command instead of nested under the service name. Multiple services can be hoisted - naming collisions will cause a runtime error. Example: protocli.WithService(serviceCLI, protocli.Hoisted())

type SharedOption

type SharedOption func(baseOptions)

SharedOption is a concrete option type that works with both service and root levels. It implements both ServiceOption and RootOption interfaces.

func AfterCommand

func AfterCommand(fn func(context.Context, *cli.Command) error) SharedOption

AfterCommand registers a hook that runs after each command execution. Works with both ServiceCommand and RootCommand. Multiple hooks can be registered and will run in REVERSE registration order. This allows cleanup to happen in the opposite order of setup (LIFO pattern). Works with both ServiceCommand and RootCommand.

func BeforeCommand

func BeforeCommand(fn func(context.Context, *cli.Command) error) SharedOption

BeforeCommand registers a hook that runs before each command execution. Multiple hooks can be registered and will run in registration order. Works with both ServiceCommand and RootCommand.

func WithOutputFormats

func WithOutputFormats(formats ...OutputFormat) SharedOption

WithOutputFormats registers output formatters for response rendering. Works with both ServiceCommand and RootCommand.

type SlogConfigurationContext

type SlogConfigurationContext interface {
	// IsDaemon returns true if the logger is being configured for daemon mode.
	IsDaemon() bool
	// Level returns the configured log level from the --verbosity flag.
	Level() slog.Level
}

SlogConfigurationContext provides context information for slog configuration.

type TemplateFunctionRegistry

type TemplateFunctionRegistry struct {
	// contains filtered or unexported fields
}

TemplateFunctionRegistry manages custom template functions for use in template-based output formats. It provides a way to register custom functions that templates can use to format proto messages.

func NewTemplateFunctionRegistry

func NewTemplateFunctionRegistry() *TemplateFunctionRegistry

NewTemplateFunctionRegistry creates a new registry with default template functions. Default functions include:

  • protoField: access message fields by JSON name, preserving proto types
  • protoJSON: converts a proto message to JSON string using protojson
  • protoJSONIndent: converts a proto message to indented JSON string
  • protoFields: converts a proto message to map for dot-chain field access

func TemplateFunctions

func TemplateFunctions() *TemplateFunctionRegistry

TemplateFunctions returns the global template function registry. This can be used to register custom template functions globally.

Example:

protocli.TemplateFunctions().Register("formatDate", func(ts *timestamppb.Timestamp) string {
    return ts.AsTime().Format("2006-01-02")
})

func (*TemplateFunctionRegistry) Functions

func (r *TemplateFunctionRegistry) Functions() template.FuncMap

Functions returns the complete set of registered template functions. This includes both default functions and any user-registered functions.

func (*TemplateFunctionRegistry) Register

func (r *TemplateFunctionRegistry) Register(name string, fn any)

Register adds or replaces a template function. If a function with the same name already exists, it will be replaced.

func (*TemplateFunctionRegistry) RegisterMap

func (r *TemplateFunctionRegistry) RegisterMap(funcMap template.FuncMap)

RegisterMap adds multiple template functions at once. Existing functions with the same names will be replaced.

Directories

Path Synopsis
cli
v1
cmd
gen command
examples
simple
Package simple demonstrates basic proto-cli usage with unary gRPC methods.
Package simple demonstrates basic proto-cli usage with unary gRPC methods.
simple/usercli command
streaming
Package streaming demonstrates server streaming gRPC support in proto-cli.
Package streaming demonstrates server streaming gRPC support in proto-cli.
proto
cli/v1
Package cli provides Protocol Buffer definitions for CLI annotations.
Package cli provides Protocol Buffer definitions for CLI annotations.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL