add-module
Original:🇺🇸 English
Translated
Create new demo or utility modules following CLY project patterns. Use when adding TUI demonstration modules to modules/demo/ or utility modules to modules/, following Bubbletea/Bubbles conventions with proper Cobra CLI integration.
6installs
Sourceyurifrl/cly
Added on
NPX Install
npx skill4agent add yurifrl/cly add-moduleTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →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 for registration
cmd.go - 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 for genuinely shared code:
pkg/| Location | Purpose | Example |
|---|---|---|
| Shared Lipgloss styles | Colors, borders, common styles |
| Common keybindings | Quit keys, navigation patterns |
| TUI utilities | Screen helpers, common components |
Rule of Three: Only extract to when 3+ modules need the same code.
pkg/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 packageCopy 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>/
)
modules/demo/<name>/Purpose: Showcase Charm UI components and patterns
Examples: chat, spinner, table, list-simple (48 total)
Parent: Registered under namespace
demoUtility Modules (modules/<name>/
)
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
bash
# For demo:
mkdir -p modules/demo/<name>
# For utility:
mkdir -p modules/<name>Create cmd.go (Command Registration)
Template for demos:
go
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:
- - For fullscreen demos
tea.WithAltScreen() - - For mouse tracking
tea.WithMouseAllMotion() - - For focus/blur events
tea.WithReportFocus()
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:
go
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.gogo
import (
yourmodule "github.com/yurifrl/cly/modules/demo/your-module"
)
func init() {
// ... existing registrations
yourmodule.Register(DemoCmd)
}For utilities - Edit :
cmd/root.gogo
import (
"github.com/yurifrl/cly/modules/yourutil"
)
func init() {
// ... existing registrations
yourutil.Register(RootCmd)
}Validation Checklist
- Compiles:
go build - Shows in help: or
go run main.go --helpgo run main.go demo --help - Runs: (or
go run main.go <command>)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():
go
func main() {
s := spinner.New()
s.Spinner = spinner.Dot
m := model{spinner: s}
tea.NewProgram(m).Run()
}Extract to:
go
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: - Basic component
Complex: - Multiple files (delegate.go, randomitems.go)
Utility: - Real functionality with list UI
Advanced: - Multiple components (textarea + viewport)
modules/demo/spinner/modules/demo/list-fancy/modules/uuid/modules/demo/chat/Quick Reference Commands
bash
# 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 tidyBest 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 , not
initialModel)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:
Purpose: Demonstrate Charm components and patterns
Examples: , , ,
modules/demo/<name>/chatspinnertablelist-simpleWhen to use: Showcasing UI components, TUI patterns, Bubbletea features
Utility Modules (Real Functionality)
Location:
Purpose: Provide actual utility commands
Examples: (UUID generator)
modules/<name>/uuidWhen 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 init()
modules/demo/cmd.go - Test
For Utility Modules
- Copy pattern:
cp -r modules/uuid modules/<newname> - Implement functionality
- Register in init()
cmd/root.go - Test
Demo Module Template
File: modules/demo/<name>/cmd.go
modules/demo/<name>/cmd.gogo
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
modules/demo/<name>/<name>.gogo
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.gogo
import (
// ...
yourmodule "github.com/yurifrl/cly/modules/demo/your-module"
)
func init() {
// ...
yourmodule.Register(DemoCmd)
}Utility Module Registration
File:
cmd/root.gogo
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)
go
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
go
p := tea.NewProgram(initialModel(), tea.WithMouseAllMotion())Use when: Demo needs mouse tracking
Example:
modules/demo/mouse/Focus Reporting
go
p := tea.NewProgram(initialModel(), tea.WithReportFocus())Use when: Demo needs to know when terminal gains/loses focus
Example:
modules/demo/focus-blur/Input Filtering
go
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 and :
references/bubbletea/examples/modules/demo/Core Components
| Demo | Shows | Reference |
|---|---|---|
| Animated loading | |
| Selection lists | |
| Data tables | |
| Single-line input | |
| Multi-line input | |
| Progress bars | |
Advanced
| Demo | Shows | Reference |
|---|---|---|
| Textarea + Viewport | |
| File selection | |
| Complex forms | |
| Multiple panes | |
All 48 examples are available - explore for implementations.
modules/demo/Adapting Reference Examples
Step-by-Step Process
Find reference:
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 , , if unused
Create cmd.go: Use template above with Register() function
references/bubbletea/examples/<component>/main.gofmtoslogExample: Adapting Spinner
Reference:
references/bubbletea/examples/spinner/main.goExtract this from main():
go
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
return model{spinner: s}Becomes initialModel():
go
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-formaltscreen-toggle
Package Names
- Lowercase, no hyphens
- Use underscores: ,
list_simple,credit_card_formaltscreen_toggle - Go converts hyphens automatically during import
File Names
- - Always this name (command registration)
cmd.go - - Main implementation (e.g.,
<name>.go,spinner.go)list-simple.go - Additional files: ,
delegate.go,helpers.go(if needed)types.go
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: or
go run main.go --helpgo 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 for consistent theming
Follow the pattern - Look at 3-4 similar modules before starting
pkg/styleTroubleshooting
"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,osif not actually usedlog - 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 functionsWith Flags
go
var demoType string
func Register(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "demo",
Short: "Demo with flag",
RunE: run,
}
cmd.Flags().StringVarP(&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
go
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
}