This document provides guidelines for AI coding agents working on the go-kef-w2 project.
go-kef-w2 is a Go CLI tool, library, and planned apps for controlling KEF's W2 platform speakers over the network. The project uses Cobra for CLI commands, Viper for configuration, and provides both a CLI tool (kefw2) and a reusable library package (kefw2).
This project uses Task as its build tool. Install with brew install go-task or go install github.com/go-task/task/v3/cmd/task@latest.
Available tasks:
task buildortask b- Build the kefw2 binary tobin/kefw2task runortask r- Run kefw2 directly (pass args:task r -- status)task completeortask c- Generate shell completionstask allortask a- Build all (build + completions)task cleanortask x- Clean build artifactstask release-test- Test release build with goreleaser
See Taskfile.yaml for full task definitions and dependencies.
Build:
# Build with version info (recommended)
task build
# Simple build
go build -o bin/kefw2 cmd/kefw2/kefw2.goRun:
# Run directly
go run cmd/kefw2/kefw2.go [args]
# Or with task
task run -- [args]Tests:
# Run all tests
go test ./...
# Run tests in a specific package
go test ./kefw2
# Run a single test
go test ./kefw2 -run TestAirableClient_GetRows
# Run tests with verbose output
go test -v ./kefw2
# Run tests with coverage
go test -cover ./kefw2Lint:
# Format code
go fmt ./...
gofmt -s -w .
# Vet code
go vet ./...
# If golangci-lint is installed
golangci-lint run.
├── cmd/kefw2/ # CLI application entry point
│ ├── kefw2.go # Main entry point
│ └── cmd/ # Cobra command implementations
├── kefw2/ # Core library package
│ ├── kefw2.go # Speaker struct and main API
│ ├── http.go # HTTP client methods
│ ├── events.go # Event streaming/subscription
│ ├── airable.go # Airable service integration
│ └── *_test.go # Unit tests
├── examples/ # Example usage
├── docs/ # Documentation
└── Taskfile.yaml # Task build definitions
Order: Standard library, external packages, then local packages. Group with blank lines.
import (
"encoding/json"
"fmt"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/hilli/go-kef-w2/kefw2"
)Use aliased imports for logrus:
log "github.com/sirupsen/logrus"- Always run
go fmtbefore committing - Use tabs for indentation (Go standard)
- Keep lines under 120 characters where reasonable
- Use
gofmt -sfor simplification
Struct tags: Use mapstructure, json, and yaml tags for config structs:
type KEFSpeaker struct {
IPAddress string `mapstructure:"ip_address" json:"ip_address" yaml:"ip_address"`
Name string `mapstructure:"name" json:"name" yaml:"name"`
Model string `mapstructure:"model" json:"model" yaml:"model"`
}Exported vs unexported:
- Public API: PascalCase (e.g.,
KEFSpeaker,NewSpeaker) - Internal helpers: camelCase (e.g.,
getData,getMACAddress)
Variables:
- Short, descriptive names for local variables:
err,resp,data - Descriptive names for package-level variables:
DefaultEventSubscriptions
Functions/Methods:
- Receiver methods:
(s KEFSpeaker) GetVolume()or(s *KEFSpeaker) UpdateInfo() - Use pointer receivers when modifying the struct
- Use value receivers for read-only operations
- Constructors:
NewSpeaker(ipAddress string)
Constants:
- Use PascalCase for exported:
SourceWiFi,SpeakerStatusOn - Use camelCase or UPPER_CASE for unexported
Always check errors explicitly:
data, err := s.getData(path)
if err != nil {
return err
}Wrap errors with context using fmt.Errorf with %w:
err = s.getId()
if err != nil {
return fmt.Errorf("failed to get speaker IDs: %w", err)
}Return early on errors:
if IPAddress == "" {
return KEFSpeaker{}, fmt.Errorf("KEF Speaker IP is empty")
}Handle connection errors with user-friendly messages:
func (s KEFSpeaker) handleConnectionError(err error) error {
if err == nil {
return nil
}
if strings.Contains(err.Error(), "connection refused") {
return fmt.Errorf("Unable to connect to speaker at %s...", s.IPAddress)
}
return fmt.Errorf("Connection error: %v", err)
}Timeouts: Always set client timeouts (currently 1 second):
client := &http.Client{}
client.Timeout = 1.0 * time.SecondDefer close: Always defer body close after error check:
resp, err := client.Do(req)
if err != nil {
return nil, s.handleConnectionError(err)
}
defer resp.Body.Close()Command structure:
var someCmd = &cobra.Command{
Use: "command",
Aliases: []string{"cmd"},
Short: "Brief description",
Long: `Longer description`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// Implementation
},
}Use custom color printers for output (defined in cmd/kefw2/cmd/color_print.go):
headerPrinter.Print("Volume: ")
contentPrinter.Printf("%d\n", volume)
errorPrinter.Println("Error message")
taskConpletedPrinter.Printf("Success message\n")Test file naming: *_test.go in the same package
Test function naming:
func TestFunctionName(t *testing.T) { }
func TestStructName_MethodName(t *testing.T) { }Use httptest for mocking:
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Mock response
json.NewEncoder(w).Encode(response)
}))
defer mockServer.Close()- Go version: 1.24.0 (see go.mod)
- License: MIT (include license header in new files)
- Configuration: Uses Viper with YAML config at
~/.config/kefw2/kefw2.yaml - Environment variables: Prefix with
KEFW2_(e.g.,KEFW2_SPEAKER) - Main package imports:
github.com/hilli/go-kef-w2/kefw2for library usage