|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +Guidance for AI agents (Claude Code, etc.) working in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +sitectl-drupal is a [sitectl](https://github.com/libops/sitectl) plugin for Drupal websites. It adds Drupal-specific commands to sitectl via the plugin SDK, including database/files sync between contexts, drush execution, user login links, and component/extension debugging. |
| 8 | + |
| 9 | +## Build, Test, and Lint Commands |
| 10 | + |
| 11 | +```bash |
| 12 | +# Install dependencies and build |
| 13 | +make build |
| 14 | + |
| 15 | +# Run linter (includes gofmt and golangci-lint) |
| 16 | +make lint |
| 17 | + |
| 18 | +# Run all tests with race detection |
| 19 | +make test |
| 20 | + |
| 21 | +# Run a specific test |
| 22 | +go test -v -race ./pkg/... -run TestSpecificTestName |
| 23 | + |
| 24 | +# Run tests for a single package |
| 25 | +go test -v -race ./cmd |
| 26 | + |
| 27 | +# Use Go workspaces to develop against local sitectl repo |
| 28 | +make work |
| 29 | +``` |
| 30 | + |
| 31 | +## Architecture |
| 32 | + |
| 33 | +### Plugin Structure |
| 34 | + |
| 35 | +This binary is a sitectl plugin. It is invoked by sitectl when discovered in `$PATH` as `sitectl-drupal`. The entry point is `main.go`, which calls `cmd.RegisterCommands(sdk)` to register all commands with the plugin SDK. |
| 36 | + |
| 37 | +### Key Commands (`cmd/`) |
| 38 | + |
| 39 | +- **`drush.go`**: Runs drush commands inside the Drupal container |
| 40 | +- **`uli.go`**: Generates a one-time login link via drush (`user:login`) |
| 41 | +- **`sync.go`**: Syncs database and/or files between contexts (source → target) |
| 42 | +- **`extensions.go`**: Lists/debugs enabled Drupal extensions (modules, themes) |
| 43 | +- **`helpers.go`**: Shared command helpers (e.g., resolving the Drupal container) |
| 44 | + |
| 45 | +### Key Packages (`pkg/`) |
| 46 | + |
| 47 | +**`pkg/jobs`**: Background job definitions registered with the plugin SDK |
| 48 | +- Jobs are long-running operations (sync, import) exposed via `sdk.RegisterJob` |
| 49 | + |
| 50 | +### Plugin SDK Usage |
| 51 | + |
| 52 | +Always use the sitectl SDK functions rather than re-implementing: |
| 53 | + |
| 54 | +- **`docker.ExecCapture()`**: Capture stdout from a container exec — do not write your own wrapper |
| 55 | +- **`job.ConfirmDatabaseReplacement()`**: Prompt before destructive DB imports — do not copy locally |
| 56 | +- **`job.ResolveRecentArtifact()`**: Resolve or produce a dated artifact (today/yesterday reuse) |
| 57 | +- **`job.StageArtifactBetweenContexts()`**: Download from source, upload to target |
| 58 | +- **`job.DownloadContextFile()`** / **`job.EnsurePathAbsentOnContext()`** / **`job.EnsureDirOnContext()`**: File transfer helpers |
| 59 | +- **`plugin.debugui`**: Use `debugui.RenderPanel`, `debugui.FormatRows` — do not copy locally |
| 60 | +- **`helpers.FirstNonEmpty()`**: Returns first non-empty string from a variadic list |
| 61 | +- **`helpers.GetContextFromArgs()`**: Extracts `--context` from `DisableFlagParsing` commands |
| 62 | + |
| 63 | +## Go Coding Conventions |
| 64 | + |
| 65 | +### Core Principles |
| 66 | + |
| 67 | +- **Simplicity First:** Favor simple, readable code over clever solutions |
| 68 | +- **Idiomatic Go:** Follow standard Go conventions and community practices |
| 69 | +- **Standard Library:** Prefer the Go standard library over third-party dependencies |
| 70 | + |
| 71 | +### Code Style |
| 72 | + |
| 73 | +- Follow all conventions outlined in [Effective Go](https://go.dev/doc/effective_go) |
| 74 | +- Use `gofmt` to format all code before committing |
| 75 | +- Keep functions small and focused on a single responsibility |
| 76 | +- Create utility functions for any behavior that repeats more than twice |
| 77 | +- Name variables clearly; avoid abbreviations unless universally understood (e.g., `i` for index) |
| 78 | + |
| 79 | +### Naming Conventions |
| 80 | + |
| 81 | +- **Packages:** Short, concise, lowercase, single-word names |
| 82 | +- **Interfaces:** Use `-er` suffix for single-method interfaces (e.g., `Reader`, `Writer`) |
| 83 | +- **Getters:** Omit `Get` prefix (use `Name()`, not `GetName()`) |
| 84 | +- **Acronyms:** Keep consistent case (e.g., `userID`, `HTTPServer`, not `userId`, `HttpServer`) |
| 85 | + |
| 86 | +### Dependency Management |
| 87 | + |
| 88 | +- Default to the standard library; only introduce external dependencies when necessary |
| 89 | +- Prefer `net/http` for routing (Go 1.22+ has built-in advanced routing) |
| 90 | +- Document why any external dependency is required |
| 91 | + |
| 92 | +### Error Handling |
| 93 | + |
| 94 | +- Always check and handle errors explicitly |
| 95 | +- Use `RunE` (not `Run`) for Cobra commands so errors propagate correctly |
| 96 | +- Return errors rather than calling `os.Exit` or `log.Fatal` outside of `main`/`Execute` |
| 97 | +- Wrap errors with context: `fmt.Errorf("context: %w", err)` |
| 98 | +- Don't ignore errors with `_` without a clear reason |
| 99 | +- Output to `cmd.OutOrStdout()` / `cmd.ErrOrStderr()`, not `fmt.Println` / `os.Stdout` |
| 100 | + |
| 101 | +```go |
| 102 | +// Good |
| 103 | +if err := doSomething(); err != nil { |
| 104 | + return fmt.Errorf("failed to do something: %w", err) |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +### Plugin/SDK Reuse |
| 109 | + |
| 110 | +- Before writing a helper, check `pkg/docker`, `pkg/job`, `pkg/helpers`, and `pkg/plugin/debugui` in sitectl |
| 111 | +- Do not copy debug panel styles or render functions locally — use `debugui` |
| 112 | +- Do not copy `ExecCapture` — use `docker.ExecCapture` |
| 113 | +- Do not copy `ConfirmDatabaseReplacement` — use `job.ConfirmDatabaseReplacement` |
| 114 | +- Follow the Go idiom: a little copying is better than a little abstraction, but we already have the dependency |
| 115 | + |
| 116 | +### Go Workspaces |
| 117 | + |
| 118 | +- **Never use `replace` directives** in `go.mod` for local development |
| 119 | +- Use `make work` (runs `scripts/use-go-work.sh`) to create a `go.work` file instead |
| 120 | + |
| 121 | +### Concurrency |
| 122 | + |
| 123 | +- Prefer channels for communication between goroutines |
| 124 | +- Avoid shared mutable state; use `sync.Mutex` when necessary |
| 125 | +- Always run tests with `-race`: `go test -race ./...` |
| 126 | +- Use `context.Context` for cancellation and timeout control |
| 127 | + |
| 128 | +### Logging |
| 129 | + |
| 130 | +- Use `log/slog` for all structured logging |
| 131 | +- Log levels: Debug (diagnostics), Info (general), Warn (potentially harmful), Error (failures) |
| 132 | +- Never log sensitive data (passwords, tokens, secrets) |
| 133 | + |
| 134 | +```go |
| 135 | +slog.Info("user authenticated", "user_id", userID, "ip_address", ipAddr) |
| 136 | +``` |
| 137 | + |
| 138 | +### Testing |
| 139 | + |
| 140 | +- Write unit tests for all new features and bug fixes |
| 141 | +- Use table-driven tests for multiple scenarios |
| 142 | +- Use `t.Helper()` in test helper functions |
| 143 | +- Run with race detection: `go test -race ./...` |
| 144 | + |
| 145 | +```go |
| 146 | +func TestCalculate(t *testing.T) { |
| 147 | + tests := []struct { |
| 148 | + name string |
| 149 | + input int |
| 150 | + want int |
| 151 | + wantErr bool |
| 152 | + }{ |
| 153 | + {"positive number", 5, 25, false}, |
| 154 | + {"zero", 0, 0, false}, |
| 155 | + {"negative number", -5, 0, true}, |
| 156 | + } |
| 157 | + for _, tt := range tests { |
| 158 | + t.Run(tt.name, func(t *testing.T) { |
| 159 | + got, err := Calculate(tt.input) |
| 160 | + if (err != nil) != tt.wantErr { |
| 161 | + t.Errorf("error = %v, wantErr %v", err, tt.wantErr) |
| 162 | + return |
| 163 | + } |
| 164 | + if got != tt.want { |
| 165 | + t.Errorf("got %v, want %v", got, tt.want) |
| 166 | + } |
| 167 | + }) |
| 168 | + } |
| 169 | +} |
| 170 | +``` |
| 171 | + |
| 172 | +### Documentation |
| 173 | + |
| 174 | +- Every exported function, type, method, and package must have a comment |
| 175 | +- Start comments with the name of the element being documented |
| 176 | +- Comment the **why**, not the **what** for internal logic |
| 177 | + |
| 178 | +```go |
| 179 | +// UserService handles all user-related operations. |
| 180 | +type UserService struct{} |
| 181 | + |
| 182 | +// GetUser retrieves a user by their ID. |
| 183 | +func (s *UserService) GetUser(id string) (*User, error) {} |
| 184 | +``` |
| 185 | + |
| 186 | +### Linting |
| 187 | + |
| 188 | +- Use `golangci-lint` for all linting checks |
| 189 | +- Fix all linting issues before committing |
| 190 | +- Run `make lint` before pushing |
| 191 | + |
| 192 | +## Development Notes |
| 193 | + |
| 194 | +- The plugin binary must be named `sitectl-drupal` and placed in `$PATH` for sitectl to discover it |
| 195 | +- Uses the plugin SDK's `RegisterCommands` pattern — all commands registered in `cmd/root.go` |
| 196 | +- The `--yolo` flag bypasses `ConfirmDatabaseReplacement` prompts for scripted use |
| 197 | +- Logging via `slog` with level controlled by `LOG_LEVEL` env var or `--log-level` flag |
0 commit comments