Contributing to lazycap

January 23, 2026 · View on GitHub

Thank you for your interest in contributing to lazycap! This guide will help you get started with development, understand the codebase, and submit high-quality contributions.

Table of Contents

Code of Conduct

By participating in this project, you agree to maintain a respectful and inclusive environment. Be kind, constructive, and professional in all interactions.

Getting Started

Prerequisites

  • Go 1.21+Download Go
  • Make — Optional but recommended
  • A Capacitor project — For testing functionality
  • Git — For version control

Quick Start

# 1. Fork and clone
git clone https://github.com/YOUR_USERNAME/lazycap.git
cd lazycap

# 2. Install dependencies
go mod download

# 3. Build
make build

# 4. Run (from a Capacitor project directory)
./bin/lazycap

Development Setup

Building

# Standard build
make build

# Build with version info
make build VERSION=1.0.0

# Install to $GOPATH/bin
make install

Running During Development

# Run directly
go run .

# Run with hot reload (requires air)
make dev

# Run tests
make test

# Run linter
make lint

Development with Hot Reload

Install air for live reloading:

go install github.com/cosmtrek/air@latest
make dev

Architecture Overview

lazycap/
├── cmd/lazycap/          # CLI entry point and commands
├── internal/
│   ├── cap/              # Capacitor CLI integration
│   ├── debug/            # Debug tools and actions
│   ├── device/           # Device discovery and management
│   ├── plugin/           # Plugin system core
│   │   ├── plugin.go     # Plugin interface and registry
│   │   ├── context.go    # Plugin context interface
│   │   ├── manager.go    # Plugin lifecycle management
│   │   └── context_impl.go # Context implementation
│   ├── plugins/          # Built-in plugins
│   │   ├── mcp/          # MCP Server plugin
│   │   └── firebase/     # Firebase Emulator plugin
│   ├── preflight/        # Environment validation
│   ├── settings/         # User settings management
│   └── ui/               # Bubble Tea TUI
│       ├── model.go      # Main app model
│       ├── process.go    # Process management
│       └── styles.go     # UI styling
├── assets/               # Images and static files
└── main.go               # Application entry

Key Components

PackageDescription
cmd/lazycapCobra CLI commands and app initialization
internal/uiBubble Tea model, views, and keyboard handling
internal/capCapacitor CLI wrapper and project detection
internal/deviceiOS/Android device and emulator discovery
internal/pluginPlugin system core with interface and manager
internal/plugins/*Built-in plugin implementations
internal/settingsUser preferences and configuration
internal/debugDebug tools and cleanup actions

Data Flow

User Input → Bubble Tea Model → Update Function → Commands
     ↑                                   ↓
     └─────── View Function ←── State Update

              Plugin Events (via EventBus)

Creating Plugins

lazycap features an extensible plugin system. Plugins can interact with all aspects of the application.

Plugin Interface

type Plugin interface {
    // Metadata
    ID() string          // Unique identifier (e.g., "my-plugin")
    Name() string        // Display name
    Description() string // Short description
    Version() string     // Semantic version
    Author() string      // Author name

    // Lifecycle
    Init(ctx Context) error  // Initialize with context
    Start() error            // Start the plugin
    Stop() error             // Stop the plugin
    IsRunning() bool         // Check if running

    // Configuration
    GetSettings() []Setting                    // Declare settings
    OnSettingChange(key string, value interface{}) // Handle changes
    GetStatusLine() string                     // Status bar text
    GetCommands() []Command                    // Keyboard commands
}

Plugin Context

Plugins receive a Context interface providing access to:

type Context interface {
    // Project
    GetProject() *cap.Project

    // Devices
    GetDevices() []device.Device
    GetSelectedDevice() *device.Device
    RefreshDevices() error

    // Actions
    RunOnDevice(deviceID string, liveReload bool) error
    RunWeb() error
    Sync(platform string) error
    Build() error
    OpenIDE(platform string) error

    // Processes
    GetProcesses() []ProcessInfo
    GetProcessLogs(processID string) []string
    KillProcess(processID string) error

    // Settings
    GetSettings() *settings.Settings
    GetSetting(key string) interface{}
    SetSetting(key string, value interface{}) error
    GetPluginSetting(pluginID, key string) interface{}
    SetPluginSetting(pluginID, key string, value interface{}) error

    // Events
    Subscribe(event EventType, handler EventHandler) UnsubscribeFunc
    Emit(event EventType, data interface{})

    // Logging
    Log(pluginID string, message string)
    LogError(pluginID string, err error)
}

Example Plugin

package myplugin

import "github.com/icarus-itcs/lazycap/internal/plugin"

const PluginID = "my-plugin"

type MyPlugin struct {
    ctx     plugin.Context
    running bool
}

func New() *MyPlugin {
    return &MyPlugin{}
}

func Register() error {
    return plugin.Register(New())
}

func (p *MyPlugin) ID() string          { return PluginID }
func (p *MyPlugin) Name() string        { return "My Plugin" }
func (p *MyPlugin) Description() string { return "Does something cool" }
func (p *MyPlugin) Version() string     { return "1.0.0" }
func (p *MyPlugin) Author() string      { return "Your Name" }
func (p *MyPlugin) IsRunning() bool     { return p.running }

func (p *MyPlugin) Init(ctx plugin.Context) error {
    p.ctx = ctx
    return nil
}

func (p *MyPlugin) Start() error {
    p.running = true
    p.ctx.Log(PluginID, "Plugin started")
    return nil
}

func (p *MyPlugin) Stop() error {
    p.running = false
    p.ctx.Log(PluginID, "Plugin stopped")
    return nil
}

func (p *MyPlugin) GetSettings() []plugin.Setting {
    return []plugin.Setting{
        {
            Key:         "autoStart",
            Name:        "Auto Start",
            Description: "Start automatically",
            Type:        "bool",
            Default:     false,
        },
    }
}

func (p *MyPlugin) OnSettingChange(key string, value interface{}) {
    // Handle setting changes
}

func (p *MyPlugin) GetStatusLine() string {
    if p.running {
        return "MyPlugin active"
    }
    return ""
}

func (p *MyPlugin) GetCommands() []plugin.Command {
    return []plugin.Command{
        {
            Key:         "M",
            Name:        "MyAction",
            Description: "Do my action",
            Handler: func() error {
                // Handle the command
                return nil
            },
        },
    }
}

Registering Your Plugin

Add to internal/plugins/plugins.go:

import "github.com/icarus-itcs/lazycap/internal/plugins/myplugin"

func RegisterAll() error {
    // ... existing plugins
    if err := myplugin.Register(); err != nil {
        return err
    }
    return nil
}

Code Style

  • Formatting: Run go fmt before committing
  • Linting: Run make lint to check for issues
  • Naming: Follow Go conventions (camelCase, exported = PascalCase)
  • Comments: Document exported functions and types
  • Error handling: Return errors, don't panic

Testing

# Run all tests
make test

# Run with coverage
make coverage

# Run specific package tests
go test ./internal/cap/...

Pull Request Process

  1. Fork the repository

  2. Create a feature branch:

    git checkout -b feature/my-feature
    
  3. Make changes with clear, atomic commits

  4. Test your changes thoroughly

  5. Push and open a PR:

    git push origin feature/my-feature
    
  6. Fill out the PR template

  7. Wait for review and address feedback

PR Checklist

  • Code follows project style guidelines
  • Tests added/updated for changes
  • Documentation updated if needed
  • Commit messages follow convention
  • PR description explains the "why"

Commit Convention

We follow Conventional Commits. This is important because commit messages determine automatic version bumps.

PrefixDescriptionVersion Bump
feat:New featureMinor (0.X.0)
fix:Bug fixPatch (0.0.X)
docs:Documentation onlyPatch
style:Formatting, no code changePatch
refactor:Code restructuringPatch
test:Adding/updating testsPatch
chore:Maintenance tasksPatch
perf:Performance improvementPatch

Breaking Changes

For breaking changes, use ! after the type or include BREAKING CHANGE: in the commit body:

# Using ! suffix (bumps major version)
feat!: redesign plugin API

# Using BREAKING CHANGE in body (bumps major version)
refactor: change configuration format

BREAKING CHANGE: The config file format has changed from JSON to YAML.

Manual Version Override

Include [major], [minor], or [patch] in your commit message to force a specific version bump:

git commit -m "chore: important update [minor]"

Examples

feat: add Firebase Emulator plugin
fix: correct device detection on M1 Macs
docs: update plugin development guide
refactor: extract device discovery into separate package
feat!: redesign settings API
fix: critical bug [major]

See RELEASING.md for complete documentation on versioning and releases.

Release Process

lazycap uses automated releases. When your PR is merged to main:

  1. GitHub Actions analyzes your commit messages
  2. A new version tag is created automatically
  3. Binaries are built for all platforms
  4. A GitHub Release is published
  5. The Homebrew formula is updated

You don't need to do anything special — just follow the commit convention above, and the automation handles the rest.

For details on how versioning works, see RELEASING.md.

Issue Guidelines

Bug Reports

Include:

  • lazycap version (lazycap version)
  • Operating system and version
  • Steps to reproduce
  • Expected vs actual behavior
  • Debug logs if applicable (/tmp/lazycap-debug.log)

Feature Requests

Include:

  • Clear description of the feature
  • Use case / motivation
  • Potential implementation approach (optional)

Questions

For general questions, open a Discussion instead of an Issue.


Need Help?

Thank you for contributing to lazycap!