add-module

Automates creation of new modules in the CLY project following established patterns.

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 "add-module" with this command: npx skills add yurifrl/cly/yurifrl-cly-add-module

Add Module Skill

Automates creation of new modules in the CLY project following established patterns.

Module Isolation Principle (CRITICAL)

The Portability Test: Before creating a module, ask: "If I copy this module folder to a new repo, how hard would it be to make it work?"

The answer should be: trivially easy.

Requirements for Isolation

  • Self-contained: All module logic lives within its directory

  • Minimal dependencies: Only depend on stdlib, Bubbletea/Bubbles/Lipgloss, and Cobra

  • No cross-module imports: Modules NEVER import from other modules

  • Single registration point: Only touch parent's cmd.go for registration

  • Own types: Define types locally, don't reach into other packages

What a Module Can Import

✓ Standard library (fmt, strings, etc.) ✓ github.com/charmbracelet/bubbletea ✓ github.com/charmbracelet/bubbles/* ✓ github.com/charmbracelet/lipgloss ✓ github.com/spf13/cobra ✓ github.com/yurifrl/cly/pkg/* (shared utilities - see below) ✗ github.com/yurifrl/cly/modules/* (NEVER)

Avoiding Duplication (The Balance)

Isolation doesn't mean blind copy-paste. Use pkg/ for genuinely shared code:

Location Purpose Example

pkg/style/

Shared Lipgloss styles Colors, borders, common styles

pkg/keys/

Common keybindings Quit keys, navigation patterns

pkg/tui/

TUI utilities Screen helpers, common components

Rule of Three: Only extract to pkg/ when 3+ modules need the same code.

Duplication is OK when:

  • Variations exist between modules

  • Extraction would create tight coupling

Extract to pkg/ when:

  • Exact same code in 3+ places

  • Code is substantial and stable

  • Changes should propagate everywhere

Module Directory = Complete Unit

modules/demo/spinner/ ├── cmd.go # Registration only ├── spinner.go # All logic here └── (optional) # Helpers if needed, but keep in same package

Copy this folder → paste in new project → change import path → works.

When to Use This Skill

  • User wants to add a new demo module showcasing a Bubbletea component

  • User wants to create a new utility command

  • User mentions "create a module", "add a command", "new demo"

Module Types

Demo Modules (modules/demo/<name>/ )

Purpose: Showcase Charm UI components and patterns Examples: chat, spinner, table, list-simple (48 total) Parent: Registered under demo namespace

Utility Modules (modules/<name>/ )

Purpose: Provide real functionality Examples: uuid (UUID generator) Parent: Registered directly under root command

Step-by-Step Workflow

Determine Module Type

Ask user if unclear:

  • "Is this a demo (showcase component) or utility (real functionality)?"

Find Reference (for demos)

  • Check if component exists in references/bubbletea/examples/<name>/

  • Read the reference implementation

  • Note initialization code in main() function

Create Directory Structure

For demo:

mkdir -p modules/demo/<name>

For utility:

mkdir -p modules/<name>

Create cmd.go (Command Registration)

Template for demos:

package <packagename>

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

func Register(parent *cobra.Command) { cmd := &cobra.Command{ Use: "<name>", Short: "<description>", 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 }

Add tea.Program options if needed:

  • tea.WithAltScreen()

  • For fullscreen demos

  • tea.WithMouseAllMotion()

  • For mouse tracking

  • tea.WithReportFocus()

  • For focus/blur events

Create Implementation File

Extract from reference:

  • Copy type definitions (model struct, custom types)

  • Copy Init(), Update(), View() methods

  • Create initialModel() from main() function's initialization code

  • Remove unused imports (fmt, os, log often unused after main() removal)

Template:

package <packagename>

import ( tea "github.com/charmbracelet/bubbletea" // Component imports as needed )

type model struct { // State fields }

func initialModel() model { // Initialization from reference's main() return model{} }

func (m model) Init() tea.Cmd { return nil }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c": return m, tea.Quit } } return m, nil }

func (m model) View() string { return "Your UI\n" }

Register Module

For demos - Edit modules/demo/cmd.go :

import ( yourmodule "github.com/yurifrl/cly/modules/demo/your-module" )

func init() { // ... existing registrations yourmodule.Register(DemoCmd) }

For utilities - Edit cmd/root.go :

import ( "github.com/yurifrl/cly/modules/yourutil" )

func init() { // ... existing registrations yourutil.Register(RootCmd) }

Validation Checklist

  • Compiles: go build

  • Shows in help: go run main.go --help or go run main.go demo --help

  • Runs: go run main.go <command> (or go run main.go demo <name> )

  • Quits cleanly with 'q' or Ctrl+C

  • No unused imports

Common Patterns

Package Naming

  • Directory with hyphens: list-simple/

  • Package name with underscores: package list_simple

  • Import alias: listsimple "github.com/yurifrl/cly/modules/demo/list-simple"

Extracting initialModel()

In reference main():

func main() { s := spinner.New() s.Spinner = spinner.Dot m := model{spinner: s} tea.NewProgram(m).Run() }

Extract to:

func initialModel() model { s := spinner.New() s.Spinner = spinner.Dot return model{spinner: s} }

Helper Functions

If reference has helpers (like getPackages(), filter(), etc.), copy them to the implementation file.

Examples to Reference

Simple: modules/demo/spinner/

  • Basic component Complex: modules/demo/list-fancy/
  • Multiple files (delegate.go, randomitems.go) Utility: modules/uuid/
  • Real functionality with list UI Advanced: modules/demo/chat/
  • Multiple components (textarea + viewport)

Quick Reference Commands

Test compilation

go build

View help

go run main.go --help go run main.go demo --help

Run demo

go run main.go demo <name>

Run utility

go run main.go <name>

Clean dependencies

go mod tidy

Best Practices

Start from reference - All 48 Bubbletea examples available in references/ Copy existing module - Fastest way to get structure right Test incrementally - Build and run after each file Clean imports early - Remove fmt/os/log before testing Follow naming - Hyphens in names, underscores in packages

Troubleshooting

Build Errors

  • "undefined: initialModel" → Function not created or private (make sure it's initialModel , not InitialModel )

  • "unused import" → Remove it from imports

  • "package name mismatch" → Check hyphens vs underscores

Runtime Errors

  • "could not open TTY" → Normal in non-interactive shells, try in terminal

  • Component not responding → Check Update() delegates to component's Update()

  • Can't quit → Verify KeyMsg handling for "q" and "ctrl+c"

Module Template

Use this template to add new commands quickly.

Module Categories

Demo Modules (UI Component Showcases)

Location: modules/demo/<name>/

Purpose: Demonstrate Charm components and patterns Examples: chat , spinner , table , list-simple

When to use: Showcasing UI components, TUI patterns, Bubbletea features

Utility Modules (Real Functionality)

Location: modules/<name>/

Purpose: Provide actual utility commands Examples: uuid (UUID generator)

When to use: Commands users will actually use for work

Quick Steps

For Demo Modules

  • Find reference: references/bubbletea/examples/<component>/

  • Copy pattern: cp -r modules/demo/spinner modules/demo/<newname>

  • Adapt implementation from reference

  • Register in modules/demo/cmd.go init()

  • Test

For Utility Modules

  • Copy pattern: cp -r modules/uuid modules/<newname>

  • Implement functionality

  • Register in cmd/root.go init()

  • Test

Demo Module Template

File: modules/demo/<name>/cmd.go

package <packagename> // Use underscores for hyphens: list_simple for list-simple

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

func Register(parent *cobra.Command) { cmd := &cobra.Command{ Use: "<name>", Short: "<short description>", Long: "<detailed description>", 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 }

File: modules/demo/<name>/<name>.go

package <packagename>

import ( tea "github.com/charmbracelet/bubbletea" // Add component imports as needed: // "github.com/charmbracelet/bubbles/spinner" // "github.com/charmbracelet/bubbles/list" // "github.com/charmbracelet/bubbles/table" // "github.com/charmbracelet/lipgloss" )

type model struct { // Component state quitting bool err error }

func initialModel() model { // Initialize your model here // Extract this from reference example's main() function return model{} }

func (m model) Init() tea.Cmd { return nil // Or return component's Init: spinner.Tick, textarea.Blink, etc. }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c": m.quitting = true return m, tea.Quit } }

return m, nil

}

func (m model) View() string { if m.quitting { return "Goodbye!\n" }

return "Your UI here\nPress q to quit\n"

}

Registration Patterns

Demo Module Registration

File: modules/demo/cmd.go

import ( // ... yourmodule "github.com/yurifrl/cly/modules/demo/your-module" )

func init() { // ... yourmodule.Register(DemoCmd) }

Utility Module Registration

File: cmd/root.go

import ( // ... "github.com/yurifrl/cly/modules/yourutil" )

func init() { uuid.Register(RootCmd) demo.Register(RootCmd) yourutil.Register(RootCmd) // Add here }

Bubbletea Program Options

Some demos require special options when creating the tea.Program:

AltScreen (Fullscreen Mode)

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

Use when: Demo should use alternate screen buffer (fullscreen, eyes, cellbuffer) Examples: modules/demo/fullscreen/ , modules/demo/eyes/

Mouse Support

p := tea.NewProgram(initialModel(), tea.WithMouseAllMotion())

Use when: Demo needs mouse tracking Example: modules/demo/mouse/

Focus Reporting

p := tea.NewProgram(initialModel(), tea.WithReportFocus())

Use when: Demo needs to know when terminal gains/loses focus Example: modules/demo/focus-blur/

Input Filtering

p := tea.NewProgram(initialModel(), tea.WithFilter(filterFunc))

Use when: Need to intercept/modify messages before Update() Example: modules/demo/prevent-quit/

Reference Examples (48 Available)

All 48 Bubbletea examples are in references/bubbletea/examples/ and modules/demo/ :

Core Components

Demo Shows Reference

spinner

Animated loading references/bubbletea/examples/spinner

list-simple

Selection lists references/bubbletea/examples/list-simple

table

Data tables references/bubbletea/examples/table

textinput

Single-line input references/bubbletea/examples/textinput

textarea

Multi-line input references/bubbletea/examples/textarea

progress-static

Progress bars references/bubbletea/examples/progress-static

Advanced

Demo Shows Reference

chat

Textarea + Viewport references/bubbletea/examples/chat

file-picker

File selection references/bubbletea/examples/file-picker

credit-card-form

Complex forms references/bubbletea/examples/credit-card-form

split-editors

Multiple panes references/bubbletea/examples/split-editors

All 48 examples are available - explore modules/demo/ for implementations.

Adapting Reference Examples

Step-by-Step Process

Find reference: references/bubbletea/examples/<component>/main.go

Read main() function: This has initialization code Extract to initialModel(): Move setup from main() to initialModel() Copy Model implementation: Copy type definitions, Init(), Update(), View() Clean imports: Remove fmt , os , log if unused Create cmd.go: Use template above with Register() function

Example: Adapting Spinner

Reference: references/bubbletea/examples/spinner/main.go

Extract this from main():

s := spinner.New() s.Spinner = spinner.Dot s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) return model{spinner: s}

Becomes initialModel():

func initialModel() model { s := spinner.New() s.Spinner = spinner.Dot s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) return model{spinner: s} }

Naming Conventions

Command Names

  • Lowercase only

  • Hyphens for multi-word: list-simple , credit-card-form , altscreen-toggle

Package Names

  • Lowercase, no hyphens

  • Use underscores: list_simple , credit_card_form , altscreen_toggle

  • Go converts hyphens automatically during import

File Names

  • cmd.go

  • Always this name (command registration)

  • <name>.go

  • Main implementation (e.g., spinner.go , list-simple.go )

  • Additional files: delegate.go , helpers.go , types.go (if needed)

Checklist

Before Implementation

  • Decided: demo or utility module?

  • Found reference example (if demo)

  • Command name chosen (lowercase, hyphens if multi-word)

During Implementation

  • Created directory in correct location

  • Created cmd.go with Register() function

  • Created implementation file with initialModel()

  • Package name matches conventions

  • Imports are clean (no unused)

After Implementation

  • Registered in parent cmd.go init()

  • Import added to parent cmd.go

  • Compiles: go build

  • Appears in help: go run main.go --help or go run main.go demo --help

  • Runs: go run main.go <command>

  • Quits cleanly with 'q' or Ctrl+C

Tips

Start with existing demos - 48 working examples to learn from Copy working code - Don't reinvent, adapt from references Test frequently - Build and run after each change Keep it simple - Single file until complexity demands splitting Use shared styles - Import pkg/style for consistent theming Follow the pattern - Look at 3-4 similar modules before starting

Troubleshooting

"undefined: initialModel"

  • Make sure initialModel() function exists in implementation file

  • Check it's exported (lowercase 'i' makes it package-private)

"package name mismatch"

  • Directory name with hyphens → package name with underscores

  • Example: list-simple/ → package list_simple

"unused import"

  • Remove fmt , os , log if not actually used

  • Check your View() and Update() functions

  • Common after removing main() function

"command not showing in help"

  • Verify Register() called in parent's init()

  • Check import path is correct

  • Run go mod tidy

Advanced Patterns

Multiple Files (Complex Modules)

modules/demo/list-fancy/ ├── cmd.go # Command registration ├── list-fancy.go # Model and main logic ├── delegate.go # Custom item delegate └── randomitems.go # Helper functions

With Flags

var demoType string

func Register(parent *cobra.Command) { cmd := &cobra.Command{ Use: "demo", Short: "Demo with flag", RunE: run, }

cmd.Flags().StringVarP(&#x26;demoType, "type", "t", "default", "Demo type")
parent.AddCommand(cmd)

}

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

With Required Args

func Register(parent *cobra.Command) { cmd := &cobra.Command{ Use: "download <url>", Short: "Download with progress", Args: cobra.ExactArgs(1), // Require 1 argument RunE: run, } parent.AddCommand(cmd) }

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

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.

General

charm-stack

No summary provided by upstream source.

Repository SourceNeeds Review
General

cobra-modularity

No summary provided by upstream source.

Repository SourceNeeds Review
General

go-specialist

No summary provided by upstream source.

Repository SourceNeeds Review