cmd

package
v1.2.1 Latest Latest
Warning

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

Go to latest
Published: Apr 14, 2026 License: MIT Imports: 23 Imported by: 0

README

cmd/ - Command Definitions

This directory contains command files following the ultra-thin command pattern.

Overview

Command files in this directory are intentionally minimal (~20-30 lines) and serve as thin CLI wrappers. All business logic, configuration, and metadata are separated into dedicated packages.

Directory Structure

cmd/
├── README.md              # This file
├── root.go               # Root command (framework file)
├── flags.go              # Flag registration helpers (framework file)
├── helpers.go            # Command creation helpers (framework file)
├── ping.go               # Example ultra-thin command
└── docs.go               # Example ultra-thin command with subcommands

Framework vs User Files

Framework Files (DO NOT EDIT unless modifying the framework)
  • root.go - Root command setup
  • flags.go - Flag registration logic
  • helpers.go - NewCommand() and MustAddToRoot() helpers
User Files (Edit these when adding commands)
  • <command>.go - Individual command files

Creating a New Command

task generate:command name=mycommand

This creates:

  • cmd/mycommand.go - Ultra-thin command file
  • internal/config/commands/mycommand_config.go - Metadata + options
Option 2: Manual Creation
  1. Create command config in internal/config/commands/mycommand_config.go:
package commands

import "github.com/peiman/changie/internal/config"

// MycommandMetadata defines all metadata for the mycommand command
var MycommandMetadata = config.CommandMetadata{
    Use:   "mycommand",
    Short: "Short description",
    Long:  `Long description...`,
    ConfigPrefix: "app.mycommand",
    FlagOverrides: map[string]string{
        "app.mycommand.some_option": "flag-name",
    },
}

func MycommandOptions() []config.ConfigOption {
    return []config.ConfigOption{
        {
            Key:          "app.mycommand.some_option",
            DefaultValue: "default",
            Description:  "Option description",
            Type:         "string",
        },
    }
}

func init() {
    config.RegisterOptionsProvider(MycommandOptions)
}
  1. Create command file in cmd/mycommand.go:
package cmd

import (
    "github.com/peiman/changie/internal/config/commands"
    "github.com/peiman/changie/internal/mycommand"
    "github.com/spf13/cobra"
)

var mycommandCmd = NewCommand(commands.MycommandMetadata, runMycommand)

func init() {
    MustAddToRoot(mycommandCmd)
}

func runMycommand(cmd *cobra.Command, args []string) error {
    cfg := mycommand.Config{
        SomeOption: getConfigValueWithFlags[string](cmd, "flag-name", "app.mycommand.some_option"),
    }
    return mycommand.NewExecutor(cfg, cmd.OutOrStdout()).Execute()
}
  1. Create business logic in internal/mycommand/mycommand.go:
package mycommand

import "io"

// Config holds configuration for mycommand business logic
type Config struct {
    SomeOption string
}

type Executor struct {
    cfg    Config
    writer io.Writer
}

func NewExecutor(cfg Config, writer io.Writer) *Executor {
    return &Executor{cfg: cfg, writer: writer}
}

func (e *Executor) Execute() error {
    // Business logic here
    return nil
}

Ultra-Thin Pattern Rules

✅ DO
  • Use NewCommand() to create commands from metadata
  • Use MustAddToRoot() to register commands
  • Keep command files ~20-30 lines
  • Move all business logic to internal/<command>/
  • Define metadata in internal/config/commands/<command>_config.go
  • Use dependency injection (pass io.Writer, etc.)
  • Add tests for business logic in internal/<command>/<command>_test.go
❌ DON'T
  • Hardcode command metadata (Use, Short, Long) in cmd files
  • Put business logic in cmd files
  • Manually call RegisterFlagsForPrefixWithOverrides() (NewCommand does this)
  • Manually call RootCmd.AddCommand() (MustAddToRoot does this)
  • Set defaults with viper.SetDefault() (use config registry)

Validation

Run the validation script to ensure commands follow the pattern:

task validate:commands
Whitelisting Commands

If you need to deviate from the pattern (e.g., complex command hierarchy), add this comment to the command file:

// ckeletin:allow-custom-command

Examples

  • Simple command: cmd/ping.go (~30 lines)
  • Command with subcommands: cmd/docs.go (~48 lines)

Architecture Benefits

  • Separation of Concerns: CLI wiring separate from business logic
  • Testability: Business logic easily testable without Cobra
  • Consistency: All commands follow same pattern
  • Maintainability: Metadata and options centralized
  • Discoverability: Single source of truth for each command
  • internal/config/command_metadata.go - CommandMetadata struct definition
  • internal/config/command_options.go - ConfigOption struct definition
  • internal/config/commands/ - All command configs (metadata + options)
  • cmd/helpers.go - Framework helpers for creating commands
  • scripts/validate-command-patterns.sh - Validation script

Documentation

Overview

Package cmd implements command line interface commands for the application.

This file contains the implementation of the bump command and its subcommands: - bump major: Bump the major version number (X.y.z -> X+1.0.0) - bump minor: Bump the minor version number (x.Y.z -> x.Y+1.0) - bump patch: Bump the patch version number (x.y.Z -> x.y.Z+1)

These commands manage semantic versioning operations including checking for uncommitted changes, changelog updates, and git tagging.

Index

Constants

View Source
const (
	// ConfigPathModeXDG searches XDG-style config directory (default).
	// On macOS, this means ~/.config/<app> unless XDG_CONFIG_HOME is set.
	ConfigPathModeXDG = "xdg"
	// ConfigPathModeNative searches the OS-native config directory.
	// On macOS, this means ~/Library/Application Support/<app>.
	ConfigPathModeNative = "native"
	// ConfigPathModeBoth searches both XDG and native directories.
	ConfigPathModeBoth = "both"
)

Variables

View Source
var (
	Version = "dev"
	Commit  = ""
	Date    = ""
)
View Source
var RootCmd = &cobra.Command{
	Use:           "",
	Short:         "A production-ready Go CLI application",
	Long:          "",
	SilenceErrors: true,
	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {

		if err := bindFlags(cmd); err != nil {
			return fmt.Errorf("failed to bind flags: %w", err)
		}

		if outputFlag := cmd.Root().PersistentFlags().Lookup("output"); outputFlag != nil && outputFlag.Changed {
			output.SetOutputMode(outputFlag.Value.String())
		}
		output.SetCommandName(cmd.Name())

		if err := initConfig(); err != nil {
			return err
		}

		if err := logger.Init(nil); err != nil {
			return fmt.Errorf("failed to initialize logger: %w", err)
		}

		outputFormat := viper.GetString(config.KeyAppOutputFormat)
		output.SetOutputMode(outputFormat)

		if output.IsJSONMode() {

			zerolog.SetGlobalLevel(zerolog.Disabled)
		}

		if configFileStatus != "" {
			if configFileUsed != "" {
				log.Info().Str("config_file", logger.SanitizePath(configFileUsed)).Msg(configFileStatus)
			} else {
				log.Debug().Msg(configFileStatus)
			}
		}

		return nil
	},
}

Export RootCmd so that tests in other packages can manipulate it without getters/setters.

Functions

func EnvPrefix

func EnvPrefix() string

EnvPrefix returns a sanitized environment variable prefix based on the binary name

func Execute

func Execute() error

func MustAddToRoot added in v1.2.0

func MustAddToRoot(cmd *cobra.Command)

MustAddToRoot adds a command to RootCmd and sets up configuration inheritance.

This is a convenience wrapper that combines two common operations:

  1. Adding the command to the root command
  2. Setting up command configuration to inherit from parent

Usage:

func init() {
    MustAddToRoot(myCmd)
}

This should be called in the init() function of your command file.

func MustNewCommand added in v1.2.0

func MustNewCommand(meta config.CommandMetadata, runE func(*cobra.Command, []string) error) *cobra.Command

MustNewCommand creates a Cobra command and panics on error.

This is a convenience wrapper for NewCommand intended for use in init() functions where there's no way to handle errors gracefully. For testable code or runtime command creation, use NewCommand instead.

Usage in init():

var myCmd = MustNewCommand(config.MyMetadata, runMy)

The runE function signature must be: func(*cobra.Command, []string) error

func NewCommand added in v1.2.0

func NewCommand(meta config.CommandMetadata, runE func(*cobra.Command, []string) error) (*cobra.Command, error)

NewCommand creates a Cobra command from metadata following ckeletin-go patterns.

This helper enforces the ultra-thin command pattern by:

  1. Creating the command from metadata (Use, Short, Long)
  2. Auto-registering flags from the config registry
  3. Applying custom flag overrides from metadata

Returns an error if flag registration fails, allowing callers to handle errors gracefully.

Usage:

cmd, err := NewCommand(config.MyMetadata, runMy)
if err != nil {
    return err
}

For init() functions where you want to panic on error, use MustNewCommand instead.

The runE function signature must be: func(*cobra.Command, []string) error

func RegisterFlagsForPrefixWithOverrides added in v1.2.0

func RegisterFlagsForPrefixWithOverrides(cmd *cobra.Command, prefix string, overrides map[string]string) error

RegisterFlagsForPrefixWithOverrides registers Cobra flags for all configuration options whose keys start with the provided prefix. It binds each flag to Viper using the option's key. Flag names are derived from the key suffix by converting underscores to hyphens, unless an explicit override is provided in the overrides map. Returns an error if flag binding fails.

Types

type ConfigPathInfo added in v1.2.0

type ConfigPathInfo struct {
	// ConfigName is the base config name without extension (e.g. "config")
	// Viper will search for config.yaml, config.yml, config.json, config.toml
	ConfigName string
	// XDGDir is the XDG-style config directory (e.g. "$XDG_CONFIG_HOME/myapp" or "~/.config/myapp")
	XDGDir string
	// NativeDir is the OS-native config directory (e.g. macOS "~/Library/Application Support/myapp").
	NativeDir string
	// Mode controls which user config directory is searched: xdg, native, or both.
	Mode string
	// SearchPaths lists all viper search paths in priority order.
	SearchPaths []string
}

ConfigPaths returns configuration paths for the application.

Config file search order (handled by viper):

  1. --config flag (explicit override)
  2. ./config.{yaml,yml,json,toml} (project-local config)
  3. User config directory based on path mode: - xdg (default): $XDG_CONFIG_HOME/<binaryName> or ~/.config/<binaryName> - native: OS-native config path (macOS: ~/Library/Application Support/<binaryName>) - both: xdg first, then native

Viper automatically detects the file format based on extension.

func ConfigPaths

func ConfigPaths() ConfigPathInfo

Jump to

Keyboard shortcuts

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