cobra-modularity

Cobra Modular CLI Architecture

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "cobra-modularity" with this command: npx skills add yurifrl/cly/yurifrl-cly-cobra-modularity

Cobra Modular CLI Architecture

Build scalable, maintainable CLI applications using Cobra with modular command registration patterns.

Your Role: CLI Architect

You help structure CLI applications with clean, modular architecture. You:

✅ Design modular command structure - Self-contained command modules ✅ Implement Register pattern - Commands register themselves ✅ Handle flags properly - Persistent vs local, parsing, validation ✅ Structure subcommands - Nested command hierarchies ✅ Apply Cobra idioms - RunE for errors, PreRun hooks, etc. ✅ Follow project patterns - Use existing module conventions

❌ Do NOT centralize commands - Keep modules self-contained ❌ Do NOT modify root unnecessarily - Add via registration only ❌ Do NOT ignore errors - Use RunE, not Run

Core Principles

  1. Modular Command Registration

Commands live in their own packages and register with parent commands.

✅ GOOD - Self-registering module:

// modules/demo/cmd.go package demo

import "github.com/spf13/cobra"

func Register(parent *cobra.Command) { cmd := &cobra.Command{ Use: "demo", Short: "Demo commands", } parent.AddCommand(cmd)

// Register subcommands
spinner.Register(cmd)
list.Register(cmd)

}

❌ BAD - Centralized registration:

// cmd/root.go - DON'T do this func init() { RootCmd.AddCommand(demoCmd) RootCmd.AddCommand(listCmd) RootCmd.AddCommand(spinnerCmd) // Root knows too much }

  1. Command Structure Pattern

Standard module layout:

modules/demo/ ├── cmd.go # Register function ├── spinner/ │ ├── cmd.go # spinner.Register() │ └── spinner.go # Implementation └── list/ ├── cmd.go # list.Register() └── list.go # Implementation

Benefits:

  • Module is self-contained

  • Easy to add/remove commands

  • No changes to root when adding modules

  • Testable in isolation

  1. Error Handling

Use RunE (not Run):

✅ GOOD:

cmd := &cobra.Command{ Use: "fetch", Short: "Fetch data", RunE: run, // Returns error }

func run(cmd *cobra.Command, args []string) error { if err := doWork(); err != nil { return fmt.Errorf("fetch failed: %w", err) } return nil }

❌ BAD:

cmd := &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { doWork() // Error ignored }, }

Cobra Command Structure

Basic Command

package mycommand

import ( "github.com/spf13/cobra" )

func Register(parent *cobra.Command) { cmd := &cobra.Command{ Use: "mycommand", Short: "Short description", Long: Long description with examples, RunE: run, } parent.AddCommand(cmd) }

func run(cmd *cobra.Command, args []string) error { // Implementation return nil }

With Flags

Local flags (command-specific):

var ( flagName string flagCount int flagVerbose bool )

func Register(parent *cobra.Command) { cmd := &cobra.Command{ Use: "process", Short: "Process data", RunE: run, }

// String flag with shorthand
cmd.Flags().StringVarP(&flagName, "name", "n", "", "Name (required)")
cmd.MarkFlagRequired("name")

// Int flag
cmd.Flags().IntVarP(&flagCount, "count", "c", 10, "Count")

// Bool flag
cmd.Flags().BoolVarP(&flagVerbose, "verbose", "v", false, "Verbose output")

parent.AddCommand(cmd)

}

func run(cmd *cobra.Command, args []string) error { // Use flagName, flagCount, flagVerbose return nil }

Persistent flags (inherited by subcommands):

func Register(parent *cobra.Command) { cmd := &cobra.Command{ Use: "server", Short: "Server commands", }

// Available to all subcommands
cmd.PersistentFlags().StringP("config", "c", "", "Config file")

parent.AddCommand(cmd)

// Register subcommands
start.Register(cmd)  // Can access --config
stop.Register(cmd)   // Can access --config

}

With Arguments

Exact args:

cmd := &cobra.Command{ Use: "delete <id>", Short: "Delete item by ID", Args: cobra.ExactArgs(1), // Requires exactly 1 arg RunE: run, }

func run(cmd *cobra.Command, args []string) error { id := args[0] return deleteItem(id) }

Range of args:

Args: cobra.RangeArgs(1, 3), // 1 to 3 args Args: cobra.MinimumNArgs(1), // At least 1 arg Args: cobra.MaximumNArgs(2), // At most 2 args

Custom validation:

Args: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return fmt.Errorf("requires exactly one arg") } if !isValidID(args[0]) { return fmt.Errorf("invalid ID: %s", args[0]) } return nil },

Module Patterns

Demo Module Pattern (CLY)

Parent command - modules/demo/cmd.go :

package demo

import ( "github.com/spf13/cobra" spinner "github.com/yurifrl/cly/modules/demo/spinner" )

var DemoCmd = &cobra.Command{ Use: "demo", Short: "Demo TUI components", }

func Register(parent *cobra.Command) { parent.AddCommand(DemoCmd) }

func init() { // Register all demo subcommands spinner.Register(DemoCmd) // ... more demos }

Subcommand - modules/demo/spinner/cmd.go :

package spinner

import ( tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" )

func Register(parent *cobra.Command) { cmd := &cobra.Command{ Use: "spinner", Short: "Spinner demo", RunE: run, } parent.AddCommand(cmd) }

func run(cmd *cobra.Command, args []string) error { p := tea.NewProgram(initialModel()) if _, err := p.Run(); err != nil { return err } return nil }

Utility Module Pattern (CLY)

Standalone command - modules/uuid/cmd.go :

package uuid

import ( tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" )

func Register(parent *cobra.Command) { cmd := &cobra.Command{ Use: "uuid", Short: "Generate UUIDs", Long: "Interactive UUID generator with history", RunE: run, } parent.AddCommand(cmd) }

func run(cmd *cobra.Command, args []string) error { p := tea.NewProgram(initialModel()) _, err := p.Run() return err }

Registered in root - cmd/root.go :

import ( "github.com/yurifrl/cly/modules/uuid" "github.com/yurifrl/cly/modules/demo" )

func init() { uuid.Register(RootCmd) demo.Register(RootCmd) }

Advanced Patterns

PreRun Hooks

Validate before run:

cmd := &cobra.Command{ Use: "deploy", PreRunE: func(cmd *cobra.Command, args []string) error { // Validation if !fileExists(configFile) { return fmt.Errorf("config not found: %s", configFile) } return nil }, RunE: run, }

Setup before run:

PreRunE: func(cmd *cobra.Command, args []string) error { // Setup database connection db, err = connectDB() return err },

PostRun Hooks

Cleanup after run:

PostRun: func(cmd *cobra.Command, args []string) { // Cleanup db.Close() tempFile.Remove() },

Persistent PreRun (Inherited)

cmd := &cobra.Command{ Use: "api", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { // Runs before ALL subcommands return loadConfig() }, }

Flag Dependencies

Require flag if another present:

cmd.Flags().String("format", "", "Output format") cmd.Flags().String("output", "", "Output file")

cmd.MarkFlagsRequiredTogether("format", "output")

Mutually exclusive flags:

cmd.Flags().Bool("json", false, "JSON output") cmd.Flags().Bool("yaml", false, "YAML output")

cmd.MarkFlagsMutuallyExclusive("json", "yaml")

Subcommand Groups

parent := &cobra.Command{ Use: "api", Short: "API commands", }

// Group 1 parent.AddGroup(&cobra.Group{ ID: "server", Title: "Server Commands:", })

cmd1 := &cobra.Command{ Use: "start", GroupID: "server", }

cmd2 := &cobra.Command{ Use: "stop", GroupID: "server", }

parent.AddCommand(cmd1, cmd2)

Configuration Integration

With Viper

import ( "github.com/spf13/cobra" "github.com/spf13/viper" )

var cfgFile string

func Register(parent *cobra.Command) { cmd := &cobra.Command{ Use: "server", Short: "Run server", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return initConfig() }, RunE: run, }

cmd.PersistentFlags().StringVar(&#x26;cfgFile, "config", "", "Config file")

// Bind flag to viper
viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config"))

parent.AddCommand(cmd)

}

func initConfig() error { if cfgFile != "" { viper.SetConfigFile(cfgFile) } else { viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath(".") viper.AddConfigPath("$HOME/.myapp") }

viper.AutomaticEnv()
return viper.ReadInConfig()

}

func run(cmd *cobra.Command, args []string) error { port := viper.GetInt("server.port") // Use config return nil }

Best Practices

  1. Keep Commands Focused

One responsibility per command:

// ✅ GOOD cly uuid # Generate UUIDs cly demo spinner # Show spinner demo

// ❌ BAD cly utils # Does everything

  1. Use Meaningful Names

// ✅ GOOD Use: "generate", Use: "list-users", Use: "deploy-app",

// ❌ BAD Use: "do", Use: "run", Use: "execute",

  1. Provide Good Help

cmd := &cobra.Command{ Use: "deploy <environment>", Short: "Deploy application", Long: `Deploy the application to specified environment.

Environments: dev, staging, prod

Examples: cly deploy dev cly deploy prod --version v1.2.3, Example: cly deploy dev cly deploy prod --version v1.2.3`, }

  1. Validate Early

PreRunE: func(cmd *cobra.Command, args []string) error { // Validate flags if port < 1 || port > 65535 { return fmt.Errorf("invalid port: %d", port) }

// Check prerequisites
if !commandExists("docker") {
    return fmt.Errorf("docker not found")
}

return nil

},

  1. Handle Interrupts

import ( "context" "os/signal" "syscall" )

func run(cmd *cobra.Command, args []string) error { ctx, stop := signal.NotifyContext( context.Background(), os.Interrupt, syscall.SIGTERM, ) defer stop()

return runWithContext(ctx)

}

Testing Commands

Test Registration

func TestRegister(t *testing.T) { parent := &cobra.Command{Use: "root"} Register(parent)

require.Len(t, parent.Commands(), 1)
require.Equal(t, "mycommand", parent.Commands()[0].Use)

}

Test Command Execution

func TestRun(t *testing.T) { cmd := &cobra.Command{ Use: "test", RunE: run, }

cmd.SetArgs([]string{"arg1", "arg2"})

err := cmd.Execute()
require.NoError(t, err)

}

Test with Flags

func TestRunWithFlags(t *testing.T) { cmd := &cobra.Command{ Use: "test", RunE: run, }

var flagValue string
cmd.Flags().StringVar(&#x26;flagValue, "flag", "", "test flag")

cmd.SetArgs([]string{"--flag", "value"})

err := cmd.Execute()
require.NoError(t, err)
require.Equal(t, "value", flagValue)

}

Common Pitfalls

❌ Using Run instead of RunE:

Run: func(cmd *cobra.Command, args []string) { // Can't return errors! doWork() }

✅ Use RunE:

RunE: func(cmd *cobra.Command, args []string) error { return doWork() }

❌ Not validating args:

RunE: func(cmd *cobra.Command, args []string) error { id := args[0] // Panic if no args! return nil }

✅ Validate with Args:

cmd := &cobra.Command{ Args: cobra.ExactArgs(1), RunE: run, }

❌ Centralizing all commands:

// root.go RootCmd.AddCommand(cmd1) RootCmd.AddCommand(cmd2) RootCmd.AddCommand(cmd3) // Tight coupling

✅ Module registration:

// Each module registers itself module1.Register(RootCmd) module2.Register(RootCmd)

Checklist

  • Command uses RunE (not Run)

  • Register() function for modularity

  • Args validated with Args field

  • Flags bound to variables

  • Required flags marked

  • Good Short description

  • Detailed Long description

  • Examples provided

  • Errors wrapped with context

  • Tests for command execution

Reference

CLY Project Structure:

cmd/ └── root.go # Root command, imports modules

modules/ ├── demo/ │ ├── cmd.go # demo.Register() │ └── spinner/ │ └── cmd.go # spinner.Register() └── uuid/ └── cmd.go # uuid.Register()

Pattern: Each module registers itself, no central registration.

Resources

  • Cobra Docs

  • Cobra User Guide

  • Viper Config

  • CLY examples: modules/demo/ , modules/uuid/

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

cli-config

No summary provided by upstream source.

Repository SourceNeeds Review
General

charm-stack

No summary provided by upstream source.

Repository SourceNeeds Review
General

add-module

No summary provided by upstream source.

Repository SourceNeeds Review
General

go-specialist

No summary provided by upstream source.

Repository SourceNeeds Review