diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cdb5969 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,96 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Guidelines + +**Documentation Updates**: When making code changes, ALWAYS update this documentation file to reflect: +- New architectural patterns or changes to existing ones +- Bug fixes with explanations of the root cause and solution +- New development patterns or conventions +- Changes to key interfaces or components +- Updates to build processes or commands + +This ensures the documentation stays current and helps future developers understand the codebase evolution. + +## Commands + +### Build and Development +- `make` - Build the launchr binary to `bin/launchr` +- `make DEBUG=1` - Build with debug symbols for use with dlv debugger +- `make deps` - Fetch go dependencies +- `make test` - Run all tests +- `make lint` - Run golangci-lint with fixes +- `make install` - Install globally to `$GOPATH/bin` +- `go generate ./...` - Generate code (runs as part of build) + +### Usage +- `bin/launchr --help` - Show help +- `bin/launchr --version` - Show version +- `bin/launchr build` - Build custom launchr with plugins + +## Architecture Overview + +Launchr is a CLI action runner that executes tasks defined in YAML files across multiple runtimes (containers, shell, plugins). The architecture is built around several core patterns: + +### Core Systems + +**Plugin Architecture**: Weight-based plugin system where plugins register via `init()` functions and implement lifecycle interfaces like `OnAppInitPlugin`, `CobraPlugin`, `DiscoveryPlugin`. Plugins are registered globally through `launchr.RegisterPlugin()`. + +**Plugin Hierarchies**: Plugins can have sub-plugins (module subpaths). During the build process, when checking for module replacements, the system must distinguish between a plugin and its sub-plugins. The fix ensures that exact path matches (`p.Path == repl`) are not skipped, only true subpath relationships (`p.Path != repl && strings.HasPrefix(p.Path, repl)`). + +**Service-Oriented Design**: Core services (Config, Manager, PluginManager) are registered and retrieved through dependency injection via `App.AddService()` and `App.GetService()`. All services implement the `Service` interface. + +**Runtime Strategy Pattern**: Multiple runtime implementations (shell, container, plugin) that implement the `Runtime` interface with `Init()`, `Execute()`, `Close()`, `Clone()` methods. + +### Key Components + +- **Action System** (`pkg/action/`): Core action entity with manager handling lifecycle, discovery, validation, and execution +- **Runtime System**: Shell, Container (Docker/K8s), and Plugin runtime implementations +- **Discovery System**: YAML and embedded filesystem action discovery with extensible discovery plugins +- **Configuration System**: YAML-based config with dot-notation access and reflection-based caching +- **Plugin System** (`plugins/`): Core plugins for naming, CLI integration, discovery, value processing, and verbosity + +### Important Interfaces + +- `App`: Global application state management +- `Plugin`: Base plugin interface with `PluginInfo()` and lifecycle hooks +- `Service`: Dependency injection with `ServiceInfo()` +- `Runtime`: Action execution environment abstraction +- `Manager`: Action management and orchestration + +### Key Files + +- `app.go`: Main application implementation with plugin and service management +- `types.go`: Type aliases to reduce external dependencies +- `pkg/action/manager.go`: Action lifecycle management +- `pkg/action/action.go`: Core action entity +- `internal/launchr/config.go`: Configuration system +- `plugins/default.go`: Plugin registration + +### Development Patterns + +- Type aliases in `types.go` for clean interfaces +- Error handling with custom types and `errors.Is()` support +- Go template integration for dynamic action configuration +- Mutex-protected operations for concurrency safety +- `fs.FS` interface for filesystem abstraction +- JSON Schema validation for inputs and configuration +- **Plugin Replacement Logic**: In `plugins/builder/environment.go`, the system handles Go module replacements: + - When ensuring modules are required, the system checks if a module is explicitly replaced (exact match) or if a plugin is a subpath of any replaced module (`p.Path != repl && strings.HasPrefix(p.Path, repl)`) to skip downloading its dependencies. This logic is inlined for direct use. + - `ensureModuleRequired(ctx, modulePath, modReplace)`: This method ensures that a module is correctly added to `go.mod`, using a placeholder version if the module is replaced. + + This approach ensures that exact module replacements are handled correctly, while sub-plugins of replaced modules are properly skipped during dependency resolution, preventing unnecessary downloads and maintaining module integrity. + +### Execution Flow + +1. Plugin registration and service initialization +2. Action discovery through registered discovery plugins +3. Cobra command generation from discovered actions +4. Multi-stage input validation (runtime flags, persistent flags, action parameters) +5. Runtime-specific execution with cleanup +6. Support for async action execution with status tracking + +### Environment Variables + +- `LAUNCHR_ACTIONS_PATH`: Path for action discovery (default: current directory) diff --git a/docs/DEVELOPER_GUIDELINES.md b/docs/DEVELOPER_GUIDELINES.md new file mode 100644 index 0000000..9dc397b --- /dev/null +++ b/docs/DEVELOPER_GUIDELINES.md @@ -0,0 +1,590 @@ +# Developer Guidelines + +This document provides comprehensive guidelines for developers working on the Launchr project. + +## Table of Contents + +1. [Getting Started](#getting-started) +2. [Code Style and Conventions](#code-style-and-conventions) +3. [Architecture Guidelines](#architecture-guidelines) +4. [Logging Guidelines](#logging-guidelines) +5. [Plugin Development](#plugin-development) +6. [Service Development](#service-development) +7. [Testing Guidelines](#testing-guidelines) +8. [Error Handling](#error-handling) +9. [Performance Considerations](#performance-considerations) +10. [Contributing Guidelines](#contributing-guidelines) + +## Getting Started + +### Prerequisites +- Go 1.24 or later +- Make +- Docker (for container runtime testing) +- Kubernetes (optional, for k8s runtime testing) + +### Development Setup +```bash +# Clone the repository +git clone +cd launchr + +# Install dependencies +make deps + +# Build the project +make + +# Run tests +make test + +# Run linter +make lint +``` + +### Development Environment +```bash +# Build with debug symbols for debugging +make DEBUG=1 + +# Run with verbose logging +LAUNCHR_LOG_LEVEL=DEBUG bin/launchr --help +``` + +## Code Style and Conventions + +### Go Code Style +Follow standard Go conventions as defined by `gofmt` and `golangci-lint`: + +```go +// Good: Clear, descriptive function names +func NewActionManager(config Config) *ActionManager { + return &ActionManager{ + config: config, + actions: make(map[string]*Action), + } +} + +// Bad: Unclear, abbreviated names +func NewActMgr(cfg Cfg) *ActMgr { + return &ActMgr{cfg: cfg, acts: make(map[string]*Act)} +} +``` + +### File Organization +``` +pkg/ +├── action/ # Action-related functionality +├── driver/ # Runtime drivers (docker, k8s) +├── jsonschema/ # JSON schema utilities +└── archive/ # Archive utilities + +internal/ +└── launchr/ # Internal application code + +plugins/ +├── actionnaming/ # Action naming plugin +├── actionscobra/ # Cobra integration plugin +└── ... + +cmd/ +└── launchr/ # Command-line entry point +``` + +### Import Organization +```go +package example + +import ( + // Standard library + "context" + "fmt" + "io" + + // Third-party dependencies + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + + // Local imports + "github.com/launchrctl/launchr/internal/launchr" + "github.com/launchrctl/launchr/pkg/action" +) +``` + +### Naming Conventions +- **Packages**: Short, single-word, lowercase (e.g., `action`, `driver`) +- **Interfaces**: Noun or adjective ending in -er (e.g., `Manager`, `Runner`) +- **Functions**: Descriptive verbs (e.g., `CreateAction`, `ValidateInput`) +- **Constants**: CamelCase with descriptive names +- **Variables**: CamelCase, descriptive but concise + +## Architecture Guidelines + +### Plugin Development Rules + +1. **Single Responsibility**: Each plugin should have one clear purpose +2. **Minimal Dependencies**: Avoid tight coupling between plugins +3. **Weight Selection**: Choose weights thoughtfully for proper ordering +4. **Interface Implementation**: Only implement interfaces you actually use + +```go +// Good: Focused plugin with single responsibility +type ActionNamingPlugin struct { + transformer NameTransformer +} + +func (p *ActionNamingPlugin) PluginInfo() launchr.PluginInfo { + return launchr.PluginInfo{ + Name: "action-naming", + Weight: 100, + } +} + +// Bad: Plugin trying to do everything +type MegaPlugin struct{} +func (p *MegaPlugin) OnAppInit(...) error { /* complex logic */ } +func (p *MegaPlugin) CobraAddCommands(...) error { /* more logic */ } +func (p *MegaPlugin) DiscoverActions(...) error { /* even more logic */ } +``` + +### Service Design Rules + +1. **Interface Segregation**: Keep interfaces focused and small +2. **Dependency Injection**: Use constructor injection for dependencies +3. **Service Lifecycle**: Consider service startup and shutdown +4. **Error Handling**: Return errors, don't panic + +```go +// Good: Small, focused interface +type ActionReader interface { + Get(id string) (*Action, bool) + All() map[string]*Action +} + +type ActionWriter interface { + Add(*Action) error + Delete(id string) error +} + +// Bad: Large interface with many responsibilities +type ActionEverything interface { + Get(id string) (*Action, bool) + Add(*Action) error + Delete(id string) error + Validate(*Action) error + Execute(*Action) error + // ... 10 more methods +} +``` + +## Logging Guidelines + +### Use the Right Logger + +#### Internal Logging (`Log()`) - For Developers +```go +// Good: Debug information for developers +Log().Debug("initialising application", "config_dir", app.cfgDir) +Log().Error("error on plugin init", "plugin", p.Name, "err", err) + +// Bad: User-facing messages in internal log +Log().Info("Build completed successfully") // Users won't see this +``` + +#### Terminal Logging (`Term()`) - For Users +```go +// Good: User-facing status messages +Term().Info().Printfln("Starting to build %s", buildName) +Term().Warning().Printfln("Configuration file not found, using defaults") +Term().Success().Println("Build completed successfully") + +// Bad: Debug information in terminal output +Term().Info().Printf("Debug: variable state = %+v", internalState) +``` + +### Structured Logging Best Practices +```go +// Good: Structured with context +Log().Error("failed to execute action", + "action_id", action.ID(), + "runtime", action.Runtime().Type(), + "error", err) + +// Bad: String concatenation +Log().Error("failed to execute action " + action.ID() + ": " + err.Error()) +``` + +## Plugin Development + +### Plugin Lifecycle + +1. **Registration**: Plugins register themselves in `init()` +2. **Initialization**: Implement required interfaces +3. **Execution**: Plugin methods called by the system + +### Plugin Template +```go +package myplugin + +import "github.com/launchrctl/launchr" + +type MyPlugin struct { + name string +} + +func (p *MyPlugin) PluginInfo() launchr.PluginInfo { + return launchr.PluginInfo{ + Name: p.name, + Weight: 100, // Choose appropriate weight + } +} + +func (p *MyPlugin) OnAppInit(app launchr.App) error { + // Get required services + var config launchr.Config + app.GetService(&config) + + // Initialize plugin + return p.initialize(config) +} + +func (p *MyPlugin) initialize(config launchr.Config) error { + // Plugin-specific initialization + return nil +} + +func init() { + launchr.RegisterPlugin(&MyPlugin{name: "my-plugin"}) +} +``` + +### Plugin Testing +```go +func TestMyPlugin(t *testing.T) { + plugin := &MyPlugin{name: "test-plugin"} + + // Test plugin info + info := plugin.PluginInfo() + assert.Equal(t, "test-plugin", info.Name) + + // Test initialization + app := createTestApp() + err := plugin.OnAppInit(app) + assert.NoError(t, err) +} +``` + +## Service Development + +### Service Implementation +```go +type MyService struct { + config Config + logger *Logger +} + +func (s *MyService) ServiceInfo() launchr.ServiceInfo { + return launchr.ServiceInfo{ + Name: "my-service", + Description: "Example service for demonstration", + } +} + +func (s *MyService) Initialize() error { + // Service initialization logic + return nil +} + +func NewMyService(config Config, logger *Logger) *MyService { + return &MyService{ + config: config, + logger: logger, + } +} +``` + +### Service Testing +```go +func TestMyService(t *testing.T) { + config := createTestConfig() + logger := createTestLogger() + + service := NewMyService(config, logger) + + err := service.Initialize() + assert.NoError(t, err) + + info := service.ServiceInfo() + assert.Equal(t, "my-service", info.Name) +} +``` + +## Testing Guidelines + +### Test Structure +```go +func TestFunctionName(t *testing.T) { + // Arrange + input := createTestInput() + expected := expectedResult() + + // Act + result, err := FunctionUnderTest(input) + + // Assert + assert.NoError(t, err) + assert.Equal(t, expected, result) +} +``` + +### Test Helpers +```go +func createTestApp() launchr.App { + app := &testApp{ + services: make(map[launchr.ServiceInfo]launchr.Service), + } + return app +} + +func createTestConfig() launchr.Config { + return &testConfig{ + data: make(map[string]interface{}), + } +} +``` + +### Table-Driven Tests +```go +func TestActionValidation(t *testing.T) { + tests := []struct { + name string + action *Action + wantErr bool + }{ + { + name: "valid action", + action: createValidAction(), + wantErr: false, + }, + { + name: "invalid action - missing ID", + action: createActionWithoutID(), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateAction(tt.action) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} +``` + +## Error Handling + +### Error Creation +```go +// Good: Descriptive error with context +func (m *Manager) GetAction(id string) (*Action, error) { + action, exists := m.actions[id] + if !exists { + return nil, fmt.Errorf("action %q not found", id) + } + return action, nil +} + +// Bad: Generic error +func (m *Manager) GetAction(id string) (*Action, error) { + action, exists := m.actions[id] + if !exists { + return nil, errors.New("not found") + } + return action, nil +} +``` + +### Error Wrapping +```go +func (r *Runtime) Execute(ctx context.Context, action *Action) error { + if err := r.prepare(ctx, action); err != nil { + return fmt.Errorf("failed to prepare runtime: %w", err) + } + + if err := r.run(ctx, action); err != nil { + return fmt.Errorf("failed to execute action: %w", err) + } + + return nil +} +``` + +### Avoid Panics +```go +// Good: Return error +func RegisterPlugin(p Plugin) error { + info := p.PluginInfo() + if _, exists := registeredPlugins[info]; exists { + return fmt.Errorf("plugin %q already registered", info.Name) + } + registeredPlugins[info] = p + return nil +} + +// Bad: Panic (current implementation - should be changed) +func RegisterPlugin(p Plugin) { + info := p.PluginInfo() + if _, exists := registeredPlugins[info]; exists { + panic(fmt.Errorf("plugin %q already registered", info.Name)) + } + registeredPlugins[info] = p +} +``` + +## Performance Considerations + +### Minimize Reflection +```go +// Good: Direct type assertion +func GetService[T Service](container ServiceContainer) (T, error) { + var zero T + service, exists := container.GetByType(reflect.TypeOf(zero)) + if !exists { + return zero, fmt.Errorf("service %T not found", zero) + } + return service.(T), nil +} + +// Current: Heavy reflection usage +func (app *appImpl) GetService(v any) { + // Complex reflection logic... +} +``` + +### Efficient Logging +```go +// Good: Check log level before expensive operations +if Log().Level() >= LogLevelDebug { + Log().Debug("complex operation result", "data", expensiveSerialize(data)) +} + +// Bad: Always compute expensive data +Log().Debug("complex operation result", "data", expensiveSerialize(data)) +``` + +### Resource Management +```go +// Good: Proper cleanup with defer +func (r *Runtime) Execute(ctx context.Context) error { + resource, err := r.acquireResource() + if err != nil { + return err + } + defer func() { + if cleanupErr := resource.Close(); cleanupErr != nil { + Log().Error("failed to cleanup resource", "error", cleanupErr) + } + }() + + return r.doWork(resource) +} +``` + +## Contributing Guidelines + +### Before Making Changes + +1. **Read Architecture Documentation**: Understand the system design +2. **Check Existing Issues**: Look for related work or discussions +3. **Write Tests**: Ensure your changes are well-tested +4. **Run Linter**: `make lint` must pass +5. **Update Documentation**: Keep docs in sync with code changes + +### Pull Request Process + +1. **Feature Branch**: Create a descriptive branch name +2. **Small Commits**: Make logical, focused commits +3. **Clear Messages**: Write descriptive commit messages +4. **Update Tests**: Add or modify tests as needed +5. **Documentation**: Update relevant documentation + +### Code Review Checklist + +- [ ] Code follows style guidelines +- [ ] All tests pass +- [ ] Linter passes without warnings +- [ ] Changes are well-documented +- [ ] No breaking changes without version bump +- [ ] Error handling is appropriate +- [ ] Logging follows guidelines +- [ ] Performance impact considered + +### Release Process + +1. **Version Bump**: Update version in appropriate files +2. **Changelog**: Update CHANGELOG.md with changes +3. **Tag Release**: Create annotated git tag +4. **GitHub Release**: Create release with binaries + +## Best Practices Summary + +### DO ✅ +- Use structured logging with context +- Implement focused interfaces +- Handle errors gracefully +- Write comprehensive tests +- Document public APIs +- Follow Go conventions +- Use appropriate logging system +- Minimize reflection usage +- Clean up resources properly + +### DON'T ❌ +- Use panics for recoverable errors +- Create large, monolithic interfaces +- Mix logging systems inappropriately +- Ignore test coverage +- Break existing APIs without versioning +- Use magic numbers without constants +- Leak resources or goroutines +- Write untestable code + +## Getting Help + +- **Architecture Questions**: Review `docs/architecture/` +- **Plugin Development**: See `docs/development/plugin.md` +- **Service Development**: See `docs/development/service.md` +- **General Questions**: Create a GitHub issue +- **Discussions**: Use GitHub Discussions for broader topics + +## Useful Commands + +```bash +# Development +make deps # Install dependencies +make # Build project +make DEBUG=1 # Build with debug symbols +make test # Run tests +make lint # Run linter + +# Testing +go test ./... # Run all tests +go test -v ./pkg/action/... # Run specific package tests +go test -race ./... # Run tests with race detection +go test -cover ./... # Run tests with coverage + +# Debugging +dlv debug ./cmd/launchr # Debug with Delve +LAUNCHR_LOG_LEVEL=DEBUG ./bin/launchr # Verbose logging + +# Building +make install # Install globally +make clean # Clean build artifacts +``` + +Remember: The goal is to maintain high code quality while preserving the excellent architectural foundation that already exists in Launchr. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 95b3290..d60ea95 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,10 +1,21 @@ -# Launchr documentation +# Launchr Documentation -1. [Built-in functionality](#built-in-functionality) -2. [Actions](actions.md) -3. [Actions Schema](actions.schema.md) -4. [Global configuration](config.md) -5. [Development](development) +## User Documentation +1. [Actions](actions.md) - Action definition and usage +2. [Actions Schema](actions.schema.md) - Schema validation for actions +3. [Global Configuration](config.md) - Application configuration +4. [Built-in functionality](#built-in-functionality) + +## Developer Documentation +5. **[Developer Guidelines](DEVELOPER_GUIDELINES.md)** - Comprehensive development guide +6. **[Architecture](architecture/)** - System architecture documentation + - [Architectural Patterns](architecture/ARCHITECTURAL_PATTERNS.md) + - [Logging Architecture](architecture/LOGGING_ARCHITECTURE.md) + - [Plugin System](architecture/PLUGIN_SYSTEM.md) + - [Service System](architecture/SERVICE_SYSTEM.md) +7. [Development](development) - Specific development topics + - [Plugin Development](development/plugin.md) + - [Service Development](development/service.md) ## Build plugin diff --git a/docs/architecture/ARCHITECTURAL_PATTERNS.md b/docs/architecture/ARCHITECTURAL_PATTERNS.md new file mode 100644 index 0000000..278cb47 --- /dev/null +++ b/docs/architecture/ARCHITECTURAL_PATTERNS.md @@ -0,0 +1,341 @@ +# Architectural Patterns Analysis + +## Overview +This document analyzes the architectural patterns used in the Launchr Go project and provides suggestions for improvements. + +## Patterns Currently Used + +### 1. **Factory Pattern** ✅ +**Location**: `pkg/driver/factory.go`, `pkg/action/runtime.container.go` +**Implementation**: Clean factory methods for creating drivers and runtimes +```go +func New(t Type) (ContainerRunner, error) { + switch t { + case Docker: return NewDockerRuntime() + case Kubernetes: return NewKubernetesRuntime() + default: panic(fmt.Sprintf("container runtime %q is not implemented", t)) + } +} +``` + +### 2. **Plugin Architecture** ✅ +**Location**: `internal/launchr/types.go`, `plugins/` +**Implementation**: Weight-based plugin system with lifecycle hooks +- Excellent extensibility through interface-based design +- Multiple plugin types: `OnAppInitPlugin`, `CobraPlugin`, `DiscoveryPlugin` + +### 3. **Strategy Pattern** ✅ +**Location**: `pkg/action/discover.go`, `pkg/action/process.go` +**Implementation**: Pluggable algorithms for discovery and value processing +- `DiscoveryStrategy` interface for different file discovery methods +- `ValueProcessor` interface for different value processing strategies + +### 4. **Decorator Pattern** ✅ +**Location**: `pkg/action/manager.go` +**Implementation**: Action decoration system using functional decorators +```go +func WithDefaultRuntime(cfg Config) DecorateWithFn +func WithContainerRuntimeConfig(cfg Config, prefix string) DecorateWithFn +``` + +### 5. **Template Method Pattern** ✅ +**Location**: `pkg/action/runtime.go` +**Implementation**: Consistent lifecycle management across all runtimes +- `Init()`, `Execute()`, `Close()`, `Clone()` methods + +### 6. **Observer Pattern** ✅ +**Location**: `app.go` +**Implementation**: Event-driven plugin hooks for application lifecycle events + +### 7. **Repository Pattern** ✅ +**Location**: `pkg/action/manager.go` +**Implementation**: Action management with CRUD operations +- `All()`, `Get()`, `Add()`, `Delete()` methods + +### 8. **Dependency Injection** ✅ +**Location**: `app.go` +**Implementation**: Reflection-based service container +- Type-safe service registration and retrieval + +### 9. **Builder Pattern** ✅ +**Location**: `pkg/action/runtime.container.go` +**Implementation**: Container definition building with conditional logic + +### 10. **Composition Pattern** ✅ +**Location**: Throughout codebase +**Implementation**: Mixin-style composition (`WithLogger`, `WithTerm`, `WithFlagsGroup`) + +## Anti-Patterns and Issues + +### 1. **Panic-Driven Error Handling** ❌ +**Problem**: Multiple locations use `panic()` for recoverable errors +**Impact**: Reduces application stability and error handling flexibility + +### 2. **Heavy Reflection Usage** ⚠️ +**Problem**: Service container and configuration rely heavily on reflection +**Impact**: Runtime performance overhead and reduced compile-time safety + +### 3. **Global State** ⚠️ +**Problem**: Global plugin registry and other global variables +**Impact**: Reduces testability and increases coupling + +### 4. **Large Interface Problem** ❌ +**Problem**: `Manager` interface has 12+ methods +**Impact**: Violates Single Responsibility Principle + +### 5. **Temporal Coupling** ❌ +**Problem**: Hidden dependencies on operation order +**Impact**: Fragile code that breaks when usage order changes + +## Suggested Improvements + +### 1. **Replace Panics with Proper Error Handling** +**Priority**: High +**Benefits**: +- Improved application stability +- Better error recovery +- More predictable behavior + +**Implementation**: +```go +// Instead of: +func RegisterPlugin(p Plugin) { + if _, ok := registeredPlugins[info]; ok { + panic(fmt.Errorf("plugin %q already registered", info)) + } +} + +// Use: +func RegisterPlugin(p Plugin) error { + if _, ok := registeredPlugins[info]; ok { + return fmt.Errorf("plugin %q already registered", info) + } + return nil +} +``` + +### 2. **Implement Dependency Graph for Plugins** +**Priority**: High +**Benefits**: +- Proper dependency resolution +- Better plugin ordering +- Reduced configuration complexity + +**Implementation**: +```go +type PluginDependency struct { + Name string + Dependencies []string + Optional []string +} + +func ResolveDependencies(plugins []PluginDependency) ([]string, error) { + // Topological sort implementation +} +``` + +### 3. **Interface Segregation for Manager** +**Priority**: Medium +**Benefits**: +- Better separation of concerns +- Easier testing +- Reduced coupling + +**Implementation**: +```go +type ActionReader interface { + All() map[string]*Action + Get(id string) (*Action, bool) +} + +type ActionWriter interface { + Add(*Action) error + Delete(id string) +} + +type ActionDiscoverer interface { + Discover(ctx context.Context) ([]*Action, error) +} +``` + +### 4. **Reduce Reflection Usage in Services** +**Priority**: Medium +**Benefits**: +- Better performance +- Compile-time safety +- Clearer dependencies + +**Implementation**: +```go +type ServiceContainer struct { + config Config + manager Manager + pluginMgr PluginManager +} + +func NewServiceContainer(config Config, manager Manager, pluginMgr PluginManager) *ServiceContainer { + return &ServiceContainer{config, manager, pluginMgr} +} + +func (sc *ServiceContainer) Config() Config { return sc.config } +func (sc *ServiceContainer) Manager() Manager { return sc.manager } +``` + +### 5. **Add Configuration Schema Validation** +**Priority**: Medium +**Benefits**: +- Better error messages +- Early validation +- Improved user experience + +**Implementation**: +```go +type ConfigValidator interface { + Validate(config map[string]interface{}) error +} + +func NewSchemaValidator(schemaPath string) ConfigValidator { + // JSON Schema validation implementation +} +``` + +### 6. **Implement Error Context Enhancement** +**Priority**: Low +**Benefits**: +- Better debugging +- Improved error messages +- Easier troubleshooting + +**Implementation**: +```go +type ContextError struct { + Op string + Context map[string]interface{} + Err error +} + +func (e *ContextError) Error() string { + return fmt.Sprintf("%s: %v (context: %+v)", e.Op, e.Err, e.Context) +} +``` + +### 7. **Add Circuit Breaker Pattern for External Services** +**Priority**: Low +**Benefits**: +- Improved resilience +- Better failure handling +- Reduced cascade failures + +**Implementation**: +```go +type CircuitBreaker interface { + Execute(operation func() error) error + State() CircuitState +} + +func NewCircuitBreaker(config CircuitBreakerConfig) CircuitBreaker { + // Circuit breaker implementation +} +``` + +### 8. **Implement Command Pattern for Actions** +**Priority**: Low +**Benefits**: +- Better action queuing +- Undo/redo capabilities +- Audit logging + +**Implementation**: +```go +type Command interface { + Execute(ctx context.Context) error + Undo(ctx context.Context) error + Description() string +} + +type ActionCommand struct { + action *Action + runtime Runtime +} +``` + +### 9. **Add Caching Layer with Cache-Aside Pattern** +**Priority**: Low +**Benefits**: +- Improved performance +- Reduced resource usage +- Better scalability + +**Implementation**: +```go +type Cache interface { + Get(key string) (interface{}, bool) + Set(key string, value interface{}, ttl time.Duration) + Delete(key string) +} + +type CachedActionManager struct { + manager Manager + cache Cache +} +``` + +### 10. **Implement Retry Pattern with Exponential Backoff** +**Priority**: Low +**Benefits**: +- Better resilience +- Reduced transient failures +- Improved reliability + +**Implementation**: +```go +type RetryConfig struct { + MaxAttempts int + BaseDelay time.Duration + MaxDelay time.Duration + Multiplier float64 +} + +func RetryWithBackoff(config RetryConfig, operation func() error) error { + // Exponential backoff implementation +} +``` + +## Implementation Priority + +### High Priority (Immediate Impact) +1. Replace panics with proper error handling +2. Implement plugin dependency graph +3. Add configuration schema validation + +### Medium Priority (Quality Improvements) +1. Interface segregation for Manager +2. Reduce reflection usage in services +3. Enhance error context + +### Low Priority (Advanced Features) +1. Circuit breaker pattern +2. Command pattern for actions +3. Caching layer implementation +4. Retry pattern with backoff + +## Testing Recommendations + +### 1. **Add Architectural Tests** +- Test plugin dependency resolution +- Validate interface segregation +- Test error handling paths + +### 2. **Performance Tests** +- Benchmark reflection usage +- Test concurrent action execution +- Measure memory usage patterns + +### 3. **Integration Tests** +- Test plugin lifecycle +- Validate service container behavior +- Test configuration loading + +## Conclusion + +The Launchr project demonstrates excellent architectural design with sophisticated use of design patterns. The suggested improvements focus on increasing stability, reducing complexity, and enhancing maintainability while preserving the excellent extensibility provided by the current plugin architecture. \ No newline at end of file diff --git a/docs/architecture/LOGGING_ARCHITECTURE.md b/docs/architecture/LOGGING_ARCHITECTURE.md new file mode 100644 index 0000000..35bf6f0 --- /dev/null +++ b/docs/architecture/LOGGING_ARCHITECTURE.md @@ -0,0 +1,161 @@ +# Dual Logging Architecture Analysis + +## Overview + +Launchr implements a sophisticated **dual logging architecture** that separates internal debugging/diagnostics from user-facing terminal output. This is an advanced architectural pattern that provides excellent separation of concerns between developer logging and user communication. + +## The Two Logging Systems + +### 1. Internal Logging System: `Log()` + +**Location**: `internal/launchr/log.go` +**Purpose**: Developer-focused structured logging for debugging and diagnostics +**Technology**: Go's structured logger (`slog`) with pterm integration + +#### Key Features: +- **Structured Logging**: Uses `slog` for key-value structured logs +- **Multiple Handlers**: Console, TextHandler, JSON Handler support +- **Configurable Levels**: Disabled, Debug, Info, Warn, Error +- **Thread-Safe**: Uses atomic pointer for default logger +- **Default Behavior**: Outputs to `io.Discard` (silent by default) +- **Runtime Configuration**: Log level and output can be changed at runtime + +#### Implementation Details: +```go +// Global atomic logger for thread safety +var defaultLogger atomic.Pointer[Logger] + +// Logger wraps slog with additional options +type Logger struct { + *Slog // Go's structured logger + LogOptions +} + +// Usage pattern +Log().Debug("shutdown cleanup") +Log().Error("error on OnAppInit", "plugin", p.K.String(), "err", err) +``` + +#### Usage Statistics: +- **18 instances** across the codebase +- Primarily used in: + - Application lifecycle (`app.go`) + - Action management (`pkg/action/`) + - Driver implementations (`pkg/driver/`) + - Plugin systems (`plugins/`) + +### 2. Terminal/User Logging System: `Term()` + +**Location**: `internal/launchr/term.go` +**Purpose**: User-facing formatted terminal output with styling and colors +**Technology**: pterm (Pretty Terminal) library + +#### Key Features: +- **Styled Output**: Colored, formatted terminal output with prefixes +- **Multiple Printers**: Basic, Info, Warning, Success, Error +- **User-Focused**: Designed for end-user communication +- **Global Control**: Can be enabled/disabled globally +- **Reflection-Based**: Uses reflection for pterm WithWriter method calls + +#### Implementation Details: +```go +// Terminal with styled printers +type Terminal struct { + w io.Writer // Target writer + p []TextPrinter // Styled printers array + enabled bool // Global enable/disable +} + +// Usage patterns +Term().Info().Printfln("Starting to build %s", b.PkgName) +Term().Warning().Printfln("Error on application shutdown cleanup:\n %s", err) +Term().Error().Println(err) +``` + +#### Usage Statistics: +- **7 instances** across the codebase +- Primarily used in: + - Application error handling (`app.go`) + - Build system (`plugins/builder/`) + - User-facing operations + +## Best Practices for Usage + +### When to Use Internal Logging (`Log()`) +- Debug information for developers +- Error diagnostics with context +- Performance metrics and traces +- Internal state changes +- Plugin lifecycle events + +```go +// Good examples +Log().Debug("initialising application") +Log().Error("error on OnAppInit", "plugin", p.K.String(), "err", err) +Log().Debug("executing shell", "cmd", cmd) +``` + +### When to Use Terminal Logging (`Term()`) +- User-facing status messages +- Error messages users need to see +- Build progress information +- Success/completion notifications +- Warnings about user actions + +```go +// Good examples +Term().Info().Printfln("Starting to build %s", name) +Term().Warning().Printfln("Error on application shutdown cleanup:\n %s", err) +Term().Success().Println("Build completed successfully") +``` + +### Anti-Patterns to Avoid +```go +// DON'T: Use internal logging for user messages +Log().Info("Build started") // Users won't see this + +// DON'T: Use terminal logging for debug info +Term().Info().Printf("Debug: variable x = %v", x) // Clutters user output + +// DON'T: Mix logging systems inconsistently +Log().Error("Failed to start") +Term().Error().Println("Failed to start") // Pick one based on audience +``` + +## Configuration + +### Internal Logging Configuration: +```go +// Runtime level changes +Log().SetLevel(LogLevelDebug) +Log().SetOutput(os.Stderr) + +// Different handler types +NewConsoleLogger(w) // Pretty console output +NewTextHandlerLogger(w) // Plain text output +NewJSONHandlerLogger(w) // JSON structured output +``` + +### Terminal Configuration: +```go +// Global enable/disable +Term().EnableOutput() +Term().DisableOutput() + +// Output redirection +Term().SetOutput(writer) + +// Individual printer access +Term().Info().Println("message") +Term().Warning().Printf("format %s", arg) +``` + +## Thread Safety + +- **Internal Logging**: ✅ Thread-safe using `atomic.Pointer[Logger]` +- **Terminal Logging**: ⚠️ Global instance without explicit synchronization + +## Performance Considerations + +- **Internal Logging**: Optimized with `io.Discard` by default +- **Terminal Logging**: Uses reflection in `SetOutput()` which has overhead \ No newline at end of file diff --git a/docs/architecture/PLUGIN_SYSTEM.md b/docs/architecture/PLUGIN_SYSTEM.md new file mode 100644 index 0000000..5b1c104 --- /dev/null +++ b/docs/architecture/PLUGIN_SYSTEM.md @@ -0,0 +1,269 @@ +# Plugin System Architecture + +## Overview + +Launchr implements a sophisticated plugin architecture that allows for extensible functionality through a weight-based plugin system with lifecycle hooks and interface-based design. + +## Core Plugin Architecture + +### Plugin Registration + +Plugins register themselves globally during package initialization: + +```go +// Global plugin registry +var registeredPlugins = make(PluginsMap) + +func RegisterPlugin(p Plugin) { + info := p.PluginInfo() + InitPluginInfo(&info, p) + if _, ok := registeredPlugins[info]; ok { + panic(fmt.Errorf("plugin %q already registered", info)) + } + registeredPlugins[info] = p +} +``` + +### Plugin Interface Hierarchy + +#### Base Plugin Interface +```go +type Plugin interface { + PluginInfo() PluginInfo +} + +type PluginInfo struct { + Name string + Weight int // Used for ordering plugins +} +``` + +#### Specialized Plugin Interfaces + +1. **OnAppInitPlugin** - Application initialization hooks +```go +type OnAppInitPlugin interface { + Plugin + OnAppInit(app App) error +} +``` + +2. **CobraPlugin** - Command-line interface integration +```go +type CobraPlugin interface { + Plugin + CobraAddCommands(rootCmd *Command) error +} +``` + +3. **PersistentPreRunPlugin** - Pre-execution hooks +```go +type PersistentPreRunPlugin interface { + Plugin + PersistentPreRun(cmd *Command, args []string) error +} +``` + +4. **DiscoveryPlugin** - Action discovery hooks +```go +type DiscoveryPlugin interface { + Plugin + DiscoverActions(ctx context.Context, manager Manager) ([]*Action, error) +} +``` + +5. **GeneratePlugin** - Code generation hooks +```go +type GeneratePlugin interface { + Plugin + Generate(config GenerateConfig) error +} +``` + +## Core Plugins + +### 1. Action Naming Plugin (`plugins/actionnaming/`) +- **Purpose**: Configurable action ID transformation +- **Functionality**: Provides naming strategies for discovered actions + +### 2. Actions Cobra Plugin (`plugins/actionscobra/`) +- **Purpose**: Cobra CLI integration for actions +- **Functionality**: Converts discovered actions into Cobra commands + +### 3. YAML Discovery Plugin (`plugins/yamldiscovery/`) +- **Purpose**: YAML file discovery in filesystem +- **Functionality**: Discovers `action.yaml` files and loads action definitions + +### 4. Built-in Processors Plugin (`plugins/builtinprocessors/`) +- **Purpose**: Value processing for action parameters +- **Functionality**: Provides processors for different parameter types + +### 5. Builder Plugin (`plugins/builder/`) +- **Purpose**: Code generation and template functionality +- **Functionality**: Builds custom launchr binaries with embedded plugins + +### 6. Verbosity Plugin (`plugins/verbosity/`) +- **Purpose**: Logging level management +- **Functionality**: Handles verbosity flags and log level configuration + +## Plugin Lifecycle + +### 1. Registration Phase +```go +func init() { + launchr.RegisterPlugin(&myPlugin{}) +} +``` + +### 2. Discovery Phase +```go +func (app *appImpl) init() error { + // Get plugins by type + plugins := launchr.GetPluginByType[OnAppInitPlugin](app.pluginMngr) + + // Execute in weight order + for _, p := range plugins { + if err = p.V.OnAppInit(app); err != nil { + return err + } + } +} +``` + +### 3. Execution Phase +```go +func (app *appImpl) exec() error { + // Add commands from plugins + plugins := launchr.GetPluginByType[CobraPlugin](app.pluginMngr) + for _, p := range plugins { + if err := p.V.CobraAddCommands(app.cmd); err != nil { + return err + } + } +} +``` + +## Plugin Weight System + +Plugins are ordered by weight for execution: +- **Lower weight** = **Higher priority** (executed first) +- **Default weight**: Usually 0 or positive integers +- **System plugins**: Often use negative weights for high priority + +## Plugin Implementation Example + +```go +type MyPlugin struct { + name string +} + +func (p *MyPlugin) PluginInfo() launchr.PluginInfo { + return launchr.PluginInfo{ + Name: p.name, + Weight: 100, + } +} + +func (p *MyPlugin) OnAppInit(app launchr.App) error { + // Plugin initialization logic + return nil +} + +func init() { + launchr.RegisterPlugin(&MyPlugin{name: "my-plugin"}) +} +``` + +## Plugin Service Integration + +Plugins can access application services through dependency injection: + +```go +func (p *MyPlugin) OnAppInit(app launchr.App) error { + var config launchr.Config + app.GetService(&config) + + var manager action.Manager + app.GetService(&manager) + + // Use services... + return nil +} +``` + +## Best Practices + +### Plugin Design +1. **Single Responsibility**: Each plugin should have one clear purpose +2. **Minimal Dependencies**: Avoid tight coupling between plugins +3. **Error Handling**: Always return meaningful errors +4. **Weight Selection**: Choose weights thoughtfully for proper ordering + +### Plugin Registration +```go +// Good: Clear registration in init() +func init() { + launchr.RegisterPlugin(&wellNamedPlugin{}) +} + +// Bad: Registration outside init() +func RegisterMyPlugin() { + launchr.RegisterPlugin(&myPlugin{}) +} +``` + +### Plugin Interface Implementation +```go +// Good: Implement only needed interfaces +type MyDiscoveryPlugin struct{} + +func (p *MyDiscoveryPlugin) PluginInfo() launchr.PluginInfo { ... } +func (p *MyDiscoveryPlugin) DiscoverActions(...) ([]*Action, error) { ... } + +// Bad: Implementing unnecessary interfaces +type MyPlugin struct{} +func (p *MyPlugin) OnAppInit(...) error { return nil } // Empty implementation +``` + +## Advanced Features + +### Plugin Composition +Plugins can implement multiple interfaces: + +```go +type MultiInterfacePlugin struct{} + +func (p *MultiInterfacePlugin) PluginInfo() launchr.PluginInfo { ... } +func (p *MultiInterfacePlugin) OnAppInit(app launchr.App) error { ... } +func (p *MultiInterfacePlugin) CobraAddCommands(cmd *Command) error { ... } +``` + +### Plugin Dependencies +While not explicitly supported, plugins can coordinate through: +- **Weight ordering**: Lower weight plugins run first +- **Service sharing**: Common services accessed through app +- **Configuration**: Shared configuration through Config service + +## Limitations and Considerations + +### Current Limitations +1. **Weight-based ordering**: Primitive dependency resolution +2. **Global registration**: All plugins registered globally +3. **No unloading**: Plugins cannot be dynamically unloaded +4. **Panic on conflicts**: Duplicate plugin names cause panics + +### Improvement Opportunities +1. **Dependency graphs**: Explicit plugin dependencies +2. **Plugin discovery**: Dynamic plugin loading from files +3. **Plugin isolation**: Namespace isolation between plugins +4. **Hot reloading**: Dynamic plugin reloading support + +## Plugin Development Workflow + +1. **Define Purpose**: Clear single responsibility +2. **Choose Interfaces**: Implement only necessary plugin interfaces +3. **Select Weight**: Choose appropriate execution order +4. **Implement Logic**: Core plugin functionality +5. **Register**: Add registration in `init()` +6. **Test**: Ensure plugin works in isolation and with others +7. **Document**: Provide clear documentation and examples \ No newline at end of file diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..03410e3 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,103 @@ +# Architecture Documentation + +This directory contains comprehensive architectural documentation for the Launchr project. + +## Documents Overview + +### [Architectural Patterns](ARCHITECTURAL_PATTERNS.md) +Comprehensive analysis of all architectural patterns used in Launchr, including: +- **Design Patterns**: Factory, Strategy, Decorator, Observer, Repository, etc. +- **Anti-Patterns**: Identified issues and code smells +- **Improvement Suggestions**: Detailed recommendations with priorities +- **Implementation Examples**: Code samples showing pattern usage + +### [Logging Architecture](LOGGING_ARCHITECTURE.md) +Deep dive into Launchr's sophisticated dual logging system: +- **Internal Logging (`Log()`)**: Developer-focused structured logging +- **Terminal Logging (`Term()`)**: User-facing styled output +- **Best Practices**: When to use each system +- **Configuration**: Runtime setup and customization + +### [Plugin System](PLUGIN_SYSTEM.md) +Complete guide to Launchr's extensible plugin architecture: +- **Plugin Interfaces**: Base and specialized plugin types +- **Registration System**: Weight-based plugin ordering +- **Core Plugins**: Built-in plugin functionality +- **Development Guide**: How to create custom plugins + +### [Service System](SERVICE_SYSTEM.md) +Documentation of the dependency injection and service management: +- **Service Container**: Type-safe service registration and retrieval +- **Core Services**: Config, Manager, PluginManager +- **Service Lifecycle**: Creation, registration, and usage +- **Best Practices**: Service design and implementation + +## Architecture Overview + +Launchr is built around several key architectural principles: + +### 1. **Plugin-Based Extensibility** +- Weight-based plugin system with lifecycle hooks +- Interface-driven design for maximum flexibility +- Clear separation between core and plugin functionality + +### 2. **Service-Oriented Architecture** +- Dependency injection through service container +- Type-safe service resolution using reflection +- Clean separation of concerns between services + +### 3. **Dual Logging System** +- Internal structured logging for developers (`Log()`) +- Styled terminal output for users (`Term()`) +- Runtime configuration and level management + +### 4. **Strategy Pattern for Runtimes** +- Multiple execution environments (shell, container, plugin) +- Pluggable runtime implementations +- Consistent lifecycle management across runtimes + +### 5. **Repository Pattern for Actions** +- Centralized action management and discovery +- CRUD operations with validation +- Multiple discovery strategies (filesystem, embedded) + +## Key Strengths + +✅ **Excellent Extensibility**: Plugin architecture allows easy feature addition +✅ **Clean Separation**: Clear boundaries between internal and user-facing code +✅ **Type Safety**: Reflection-based but type-safe service resolution +✅ **User Experience**: Sophisticated terminal output with styling +✅ **Developer Experience**: Structured logging with contextual information + +## Areas for Improvement + +⚠️ **Error Handling**: Replace panics with proper error returns +⚠️ **Thread Safety**: Add synchronization to terminal logging +⚠️ **Reflection Usage**: Consider compile-time alternatives +⚠️ **Interface Segregation**: Split large interfaces into focused ones +⚠️ **Plugin Dependencies**: Implement proper dependency resolution + +## Reading Guide + +1. **New Developers**: Start with [Plugin System](PLUGIN_SYSTEM.md) and [Service System](SERVICE_SYSTEM.md) +2. **Architecture Review**: Focus on [Architectural Patterns](ARCHITECTURAL_PATTERNS.md) +3. **Logging Implementation**: See [Logging Architecture](LOGGING_ARCHITECTURE.md) +4. **Contributing**: Review all documents for complete understanding + +## Relationship to Other Documentation + +This architecture documentation complements: +- **[../development/](../development/)**: Development guides and practices +- **[../actions.md](../actions.md)**: Action definition and usage +- **[../config.md](../config.md)**: Configuration management +- **[CLAUDE.md](../../CLAUDE.md)**: AI assistant guidance + +## Maintenance + +These documents should be updated when: +- New architectural patterns are introduced +- Existing patterns are significantly modified +- Major refactoring affects system architecture +- New services or plugins are added to the core system + +For questions or suggestions about the architecture, please create an issue or discussion in the project repository. \ No newline at end of file diff --git a/docs/architecture/SERVICE_SYSTEM.md b/docs/architecture/SERVICE_SYSTEM.md new file mode 100644 index 0000000..1677c4e --- /dev/null +++ b/docs/architecture/SERVICE_SYSTEM.md @@ -0,0 +1,356 @@ +# Service System Architecture + +## Overview + +Launchr implements a service-oriented architecture using dependency injection pattern. The system provides a type-safe way to register and retrieve services throughout the application. + +## Core Service Architecture + +### Service Interface +```go +type Service interface { + ServiceInfo() ServiceInfo +} + +type ServiceInfo struct { + Name string + Description string +} +``` + +### Service Container +The application acts as a service container: + +```go +type appImpl struct { + services map[ServiceInfo]Service + // ... other fields +} + +func (app *appImpl) AddService(s Service) { + info := s.ServiceInfo() + launchr.InitServiceInfo(&info, s) + if _, ok := app.services[info]; ok { + panic(fmt.Errorf("service %s already exists", info)) + } + app.services[info] = s +} + +func (app *appImpl) GetService(v any) { + // Reflection-based service retrieval + for _, srv := range app.services { + st := reflect.TypeOf(srv) + if st.AssignableTo(stype) { + reflect.ValueOf(v).Elem().Set(reflect.ValueOf(srv)) + return + } + } + panic(fmt.Sprintf("service %q does not exist", stype)) +} +``` + +## Core Services + +### 1. Configuration Service +**Interface**: `Config` +**Implementation**: `launchr.ConfigFromFS` +**Purpose**: YAML-based configuration management + +```go +type Config interface { + Exists(key string) bool + Get(key string, v any) error + Path(parts ...string) string +} +``` + +**Usage**: +```go +var config launchr.Config +app.GetService(&config) + +var timeout time.Duration +config.Get("action.timeout", &timeout) +``` + +### 2. Action Manager Service +**Interface**: `Manager` +**Implementation**: `action.NewManager` +**Purpose**: Action lifecycle management + +```go +type Manager interface { + All() map[string]*Action + Get(id string) (*Action, bool) + Add(*Action) error + Delete(id string) + Discover(ctx context.Context) ([]*Action, error) + // ... other methods +} +``` + +**Usage**: +```go +var manager action.Manager +app.GetService(&manager) + +actions, err := manager.Discover(ctx) +``` + +### 3. Plugin Manager Service +**Interface**: `PluginManager` +**Implementation**: `launchr.NewPluginManagerWithRegistered` +**Purpose**: Plugin management and discovery + +```go +type PluginManager interface { + GetPlugins() PluginsMap + GetPluginByType(pluginType reflect.Type) []PluginMapItem +} +``` + +**Usage**: +```go +var pluginMgr launchr.PluginManager +app.GetService(&pluginMgr) + +plugins := launchr.GetPluginByType[OnAppInitPlugin](pluginMgr) +``` + +## Service Registration + +Services are registered during application initialization: + +```go +func (app *appImpl) init() error { + // Create services + config := launchr.ConfigFromFS(os.DirFS(app.cfgDir)) + actionMngr := action.NewManager( + action.WithDefaultRuntime(config), + action.WithContainerRuntimeConfig(config, name+"_"), + ) + + // Register services + app.AddService(actionMngr) + app.AddService(app.pluginMngr) + app.AddService(config) + + return nil +} +``` + +## Service Usage Patterns + +### In Plugins +```go +func (p *MyPlugin) OnAppInit(app launchr.App) error { + // Get required services + var config launchr.Config + app.GetService(&config) + + var manager action.Manager + app.GetService(&manager) + + // Use services + return p.initializeWithServices(config, manager) +} +``` + +### In Application Code +```go +func (app *appImpl) someMethod() error { + var manager action.Manager + app.GetService(&manager) + + return manager.SomeOperation() +} +``` + +## Service Lifecycle + +### 1. Creation Phase +Services are created during app initialization: +```go +config := launchr.ConfigFromFS(os.DirFS(app.cfgDir)) +actionMngr := action.NewManager(options...) +``` + +### 2. Registration Phase +Services are registered with the container: +```go +app.AddService(config) +app.AddService(actionMngr) +``` + +### 3. Usage Phase +Services are retrieved when needed: +```go +var config launchr.Config +app.GetService(&config) +``` + +## Service Implementation Example + +```go +type MyService struct { + name string + config Config +} + +func (s *MyService) ServiceInfo() launchr.ServiceInfo { + return launchr.ServiceInfo{ + Name: "my-service", + Description: "Example service implementation", + } +} + +func (s *MyService) DoSomething() error { + // Service logic + return nil +} + +func NewMyService(config launchr.Config) *MyService { + return &MyService{ + name: "my-service", + config: config, + } +} +``` + +## Service Dependencies + +Services can depend on other services through constructor injection: + +```go +func NewActionManager(config Config, pluginMgr PluginManager) Manager { + return &actionManager{ + config: config, + pluginMgr: pluginMgr, + } +} +``` + +## Best Practices + +### Service Design +1. **Single Responsibility**: Each service should have one clear purpose +2. **Interface Segregation**: Define focused interfaces +3. **Dependency Injection**: Use constructor injection for dependencies +4. **Immutable State**: Prefer immutable service configuration + +### Service Registration +```go +// Good: Register services in logical order +app.AddService(config) // Base service +app.AddService(pluginMgr) // Core service +app.AddService(actionMgr) // Dependent service + +// Bad: Random registration order +app.AddService(actionMgr) // Depends on config +app.AddService(config) // Should be registered first +``` + +### Service Retrieval +```go +// Good: Check for service existence +var config launchr.Config +app.GetService(&config) +if config == nil { + return errors.New("config service not available") +} + +// Bad: Assume service exists (will panic if missing) +var config launchr.Config +app.GetService(&config) +config.Get("key", &value) // Potential panic +``` + +## Strengths + +1. **Type Safety**: Reflection-based but type-safe service retrieval +2. **Dependency Injection**: Clean separation of concerns +3. **Service Discovery**: Easy access to registered services +4. **Interface-Based**: Services defined by contracts, not implementations + +## Limitations + +1. **Reflection Overhead**: Runtime reflection for service retrieval +2. **Panic on Missing**: Missing services cause panics +3. **No Lifecycle Management**: Services don't have explicit lifecycle hooks +4. **Single Instance**: Each service type can only have one instance + +## Improvement Opportunities + +### 1. Service Lifecycle Management +```go +type ServiceLifecycle interface { + Start(ctx context.Context) error + Stop(ctx context.Context) error + Health() error +} +``` + +### 2. Service Scoping +```go +type ServiceScope string + +const ( + ScopeSingleton ServiceScope = "singleton" + ScopeTransient ServiceScope = "transient" + ScopeScoped ServiceScope = "scoped" +) +``` + +### 3. Service Factories +```go +type ServiceFactory interface { + Create(container ServiceContainer) (Service, error) + Scope() ServiceScope +} +``` + +### 4. Graceful Error Handling +```go +func (app *appImpl) GetService(v any) error { + // Return error instead of panic + if service, exists := app.findService(v); exists { + setValue(v, service) + return nil + } + return fmt.Errorf("service %T not found", v) +} +``` + +## Advanced Patterns + +### Service Composition +```go +type CompositeService struct { + config Config + manager Manager + logger Logger +} + +func (s *CompositeService) ServiceInfo() launchr.ServiceInfo { + return launchr.ServiceInfo{ + Name: "composite-service", + } +} +``` + +### Service Delegation +```go +type DelegatingService struct { + delegate Service +} + +func (s *DelegatingService) DoWork() error { + // Add cross-cutting concerns + log.Debug("starting work") + defer log.Debug("finished work") + + return s.delegate.DoWork() +} +``` + +The service system provides a clean, type-safe way to manage dependencies and share functionality across the application while maintaining loose coupling between components. \ No newline at end of file diff --git a/plugins/builder/environment.go b/plugins/builder/environment.go index a9a2937..f98f7cd 100644 --- a/plugins/builder/environment.go +++ b/plugins/builder/environment.go @@ -80,6 +80,37 @@ func newBuildEnvironment(streams launchr.Streams) (*buildEnvironment, error) { }, nil } +// ensureModuleRequired adds a module to go.mod if it's not already there or if it's replaced. +// It uses a placeholder version for replaced modules. +func (env *buildEnvironment) ensureModuleRequired(ctx context.Context, modulePath string, modReplace map[string]string) error { + // Check if the module is replaced (exact match). + replaced := false + for replPath := range modReplace { + if modulePath == replPath { + replaced = true + break + } + } + + pkgStr := modulePath + if replaced { + // If module is replaced, use a placeholder version for `go mod edit -require`. + // Ensure it has a version, even if it's a placeholder. + if !strings.Contains(pkgStr, "@") { + pkgStr += "@v0.0.0" + } + } + + // Use `go mod edit -require` to ensure the module is in go.mod. + // This command handles cases where the module is already required or needs to be added. + // If it's a replaced module, it will ensure the replacement is respected. + err := env.execGoMod(ctx, "edit", "-require", pkgStr) + if err != nil { + return err + } + return nil +} + func (env *buildEnvironment) CreateModFile(ctx context.Context, opts *BuildOptions) error { var err error // Create go.mod. @@ -88,7 +119,7 @@ func (env *buildEnvironment) CreateModFile(ctx context.Context, opts *BuildOptio return err } - // Replace requested modules. + // Apply requested module replacements. for o, n := range opts.ModReplace { err = env.execGoMod(ctx, "edit", "-replace", o+"="+n) if err != nil { @@ -96,48 +127,52 @@ func (env *buildEnvironment) CreateModFile(ctx context.Context, opts *BuildOptio } } - // Download the requested dependencies directly. + // Download dependencies. if opts.NoCache { + // Set GONOSUMDB and GONOPROXY for modules that should not be cached or verified. + // This is typically used for local development or specific build scenarios. domains := make([]string, len(opts.Plugins)) - for i := 0; i < len(domains); i++ { - domains[i] = opts.Plugins[i].Path + for i, p := range opts.Plugins { + domains[i] = p.Path + } + // Add core package path to the list if it's not already there + if !strings.Contains(strings.Join(domains, ","), opts.CorePkg.Path) { + domains = append(domains, opts.CorePkg.Path) } noproxy := strings.Join(domains, ",") env.env = append(env.env, "GONOSUMDB="+noproxy, "GONOPROXY="+noproxy) } - // Download core. - var coreRepl bool - for repl := range opts.ModReplace { - if strings.HasPrefix(opts.CorePkg.Path, repl) { - coreRepl = true - break - } - } - if !coreRepl { - err = env.execGoGet(ctx, opts.CorePkg.String()) - if err != nil { - return err - } + // Ensure core package is required. + err = env.ensureModuleRequired(ctx, opts.CorePkg.String(), opts.ModReplace) + if err != nil { + return err } - // Download plugins. -nextPlugin: + // Ensure plugins are required. for _, p := range opts.Plugins { - // Do not get plugins of module subpath. + // Skip plugins that are subpaths of replaced modules. + isSubpath := false for repl := range opts.ModReplace { - if strings.HasPrefix(p.Path, repl) { - continue nextPlugin + if p.Path != repl && strings.HasPrefix(p.Path, repl) { + isSubpath = true + break } } - err = env.execGoGet(ctx, p.String()) + if isSubpath { + continue + } + + // Ensure the plugin module is required in go.mod. + err = env.ensureModuleRequired(ctx, p.String(), opts.ModReplace) if err != nil { return err } } + // @todo update all but with fixed versions if requested - return err + return nil } func (env *buildEnvironment) Filepath(s string) string {