docs: improve AGENTS.md with accurate build tags, commands, and style guidelines (#42)
- Fix Go version from 1.24+ to 1.25 (matching go.mod) - Add missing make lint/test commands - Add critical -tags webkit2_41 requirement to all test commands - Add warning about generated files (sqlcgen/, *_templ.go) - Add unexported sentinel error convention, logger.WithGroup() pattern - Add SQLite WAL mode and SetMaxOpenConns(1) detail - Add frontend type checking command (tsc --noEmit) - Add missing backend directories to structure - Consolidate and tighten sections to reduce line count
This commit is contained in:
@@ -5,7 +5,7 @@ Guidelines for AI coding agents working in this repository.
|
|||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
YellowJacket is a cross-platform desktop music player built with:
|
YellowJacket is a cross-platform desktop music player built with:
|
||||||
- **Backend**: Go 1.24+ with Wails v2 framework
|
- **Backend**: Go 1.25 with Wails v2 framework
|
||||||
- **Frontend**: TypeScript with Lit Web Components
|
- **Frontend**: TypeScript with Lit Web Components
|
||||||
- **Database**: SQLite (pure-Go driver via `modernc.org/sqlite`)
|
- **Database**: SQLite (pure-Go driver via `modernc.org/sqlite`)
|
||||||
- **Build Tools**: Make, Wails CLI, Vite, pnpm
|
- **Build Tools**: Make, Wails CLI, Vite, pnpm
|
||||||
@@ -18,191 +18,165 @@ make build-dev # Debug build
|
|||||||
make build-prod # Production build (obfuscated + UPX compressed)
|
make build-prod # Production build (obfuscated + UPX compressed)
|
||||||
make generate # Run all code generators (sqlc, templ)
|
make generate # Run all code generators (sqlc, templ)
|
||||||
make clean # Clean frontend build artifacts
|
make clean # Clean frontend build artifacts
|
||||||
|
make lint # Run golangci-lint
|
||||||
|
make test # Run all Go tests (race detector, no cache, 2min timeout)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frontend Only
|
### Frontend Only
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend && pnpm install # Install dependencies
|
||||||
pnpm install # Install dependencies
|
cd frontend && pnpm dev # Vite dev server
|
||||||
pnpm dev # Vite dev server
|
cd frontend && pnpm build # Production build
|
||||||
pnpm build # Production build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
**Important**: Tests require the `-tags webkit2_41` build tag.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go test ./... # Run all tests
|
make test # All tests (preferred)
|
||||||
go test ./backend/player/ # Run tests in a specific package
|
go test -tags webkit2_41 ./... # All tests manually
|
||||||
go test -run TestFunctionName ./backend/player/ # Run a single test by name
|
go test -tags webkit2_41 ./backend/player/ # Single package
|
||||||
go test -v -run TestFunctionName ./backend/player/ # Verbose output
|
go test -tags webkit2_41 -run TestFunctionName ./backend/player/ # Single test
|
||||||
|
go test -tags webkit2_41 -v -run TestFunctionName ./backend/player/ # Verbose single test
|
||||||
```
|
```
|
||||||
|
|
||||||
Test files are located alongside source files as `*_test.go`. Test fixtures live in `test_data/`.
|
Test files are colocated with source as `*_test.go`. Test fixtures live in `test_data/`. Some tests skip in CI when they require hardware (audio device, Wails runtime).
|
||||||
|
|
||||||
## Linting
|
## Linting
|
||||||
|
|
||||||
The project uses golangci-lint (v2 config) with strict rules. Key enabled linters:
|
golangci-lint v2 config (`.golangci.yml`) with strict rules. Key linters:
|
||||||
- `gocritic`, `errorlint`, `err113`, `godot`, `revive`, `sloglint`, `nlreturn`, `wsl`
|
- `gocritic`, `errorlint`, `err113`, `godot`, `revive`, `sloglint`, `nlreturn`, `wsl`
|
||||||
- Formatters: `gci`, `gofmt`, `gofumpt`, `goimports`, `golines`
|
- Formatters: `gci`, `gofmt`, `gofumpt`, `goimports`, `golines`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
golangci-lint run
|
make lint # Lint all Go code
|
||||||
|
golangci-lint run --build-tags webkit2_41 ./... # With build tags explicitly
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Frontend type checking: `cd frontend && pnpm exec tsc --noEmit`
|
||||||
|
|
||||||
## Code Generation
|
## Code Generation
|
||||||
|
|
||||||
`go:generate` directives live in:
|
`go:generate` directives live in `backend/app.go` (templ) and `backend/database/database.go` (sqlc). After modifying `.templ` files or SQL in `backend/database/sql/`, run `make generate`. **Never edit files in `backend/database/sql/sqlcgen/` or `*_templ.go` — they are generated.**
|
||||||
- `backend/app.go` — templ: generates `*_templ.go` from `.templ` files
|
|
||||||
- `backend/database/database.go` — sqlc: generates type-safe DB code from SQL
|
|
||||||
|
|
||||||
After modifying `.templ` files or SQL in `backend/database/sql/`, run `make generate`.
|
## Go Code Style
|
||||||
|
|
||||||
## Code Style Guidelines
|
### Package Documentation
|
||||||
|
Every package must have a doc comment ending with a period:
|
||||||
### Go Code Style
|
|
||||||
|
|
||||||
#### Package Documentation
|
|
||||||
Every package must have a doc comment:
|
|
||||||
```go
|
```go
|
||||||
// Package player provides audio playback functionality.
|
// Package player provides audio playback functionality.
|
||||||
package player
|
package player
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Import Organization
|
### Import Organization
|
||||||
Imports are grouped and ordered by gci/goimports (three groups separated by blank lines):
|
Three groups separated by blank lines (enforced by `gci`): stdlib, third-party, internal.
|
||||||
1. Standard library 2. Third-party packages 3. Internal packages (`yellowjacket/...`)
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
|
||||||
"yellowjacket/backend/events"
|
"yellowjacket/backend/events"
|
||||||
"yellowjacket/backend/metadata"
|
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Error Handling
|
### Error Handling
|
||||||
- Always wrap errors with context: `fmt.Errorf("failed to open file: %w", err)`
|
- Wrap errors with context: `fmt.Errorf("failed to open file: %w", err)`
|
||||||
- Define sentinel errors as package-level vars (enforced by `err113`):
|
- Sentinel errors as package-level vars (enforced by `err113`):
|
||||||
```go
|
```go
|
||||||
var ErrUnsupportedFileType = errors.New("unsupported file type")
|
var ErrUnsupportedFileType = errors.New("unsupported file type")
|
||||||
```
|
```
|
||||||
|
- Unexported sentinels for internal use: `var errNotDirectory = errors.New("not a directory")`
|
||||||
- Use `errors.Join()` for accumulating multiple errors
|
- Use `errors.Join()` for accumulating multiple errors
|
||||||
- Return early on errors; blank line required after early returns (`nlreturn`)
|
- Return early on errors; blank line required after early returns (`nlreturn`)
|
||||||
|
|
||||||
#### Naming Conventions
|
### Naming Conventions
|
||||||
- Structs: `PascalCase` (e.g., `Player`, `AudioFile`)
|
- Structs/exported: `PascalCase` — Unexported: `camelCase`
|
||||||
- Exported methods: `PascalCase`
|
|
||||||
- Unexported methods/fields: `camelCase`
|
|
||||||
- Constants: `PascalCase` for exported, grouped with `const (...)`
|
- Constants: `PascalCase` for exported, grouped with `const (...)`
|
||||||
- Custom domain types: `type PlayerState string`, `type UserVolume int`
|
- Custom domain types: `type PlayerState string`, `type UserVolume int`, `type AudioFileExtension string`
|
||||||
|
|
||||||
#### Logging
|
### Logging
|
||||||
Use `log/slog` with structured key-value pairs. Logger instances are injected via constructors:
|
`log/slog` with structured key-value pairs. Logger injected via constructors, scoped with `logger.WithGroup("player")`:
|
||||||
```go
|
```go
|
||||||
p.logger.Info("File loaded", "file", filePath)
|
p.logger.Info("File loaded", "file", filePath)
|
||||||
p.logger.Error("Failed to decode", "path", filePath, "err", err)
|
p.logger.Error("Failed to decode", "path", filePath, "err", err)
|
||||||
```
|
```
|
||||||
Logger groups via `logger.WithGroup("player")` for component-scoped logging.
|
|
||||||
|
|
||||||
#### Constructor Pattern
|
### Comments & Formatting
|
||||||
|
- Doc comments on all exported functions/types, ending with periods (enforced by `godot`)
|
||||||
|
- Blank line after early returns (enforced by `nlreturn`)
|
||||||
|
|
||||||
|
### Constructor Pattern
|
||||||
```go
|
```go
|
||||||
func NewPlayer(ctx context.Context, logger *slog.Logger, db *database.DB) (*Player, error) {
|
func NewPlayer(ctx context.Context, logger *slog.Logger, db *database.DB) (*Player, error) {
|
||||||
player := &Player{ctx: ctx, logger: logger, state: Stopped}
|
player := &Player{ctx: ctx, logger: logger.WithGroup("player"), state: Stopped}
|
||||||
// initialization...
|
|
||||||
return player, nil
|
return player, nil
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### SetContext Pattern (Two-Phase Initialization)
|
### SetContext Pattern (Two-Phase Initialization)
|
||||||
Backend components that need the Wails runtime use a two-phase pattern because Wails runtime features (events, dialogs) are unavailable until `OnStartup`:
|
Components needing Wails runtime use two phases (runtime unavailable until `OnStartup`):
|
||||||
1. Constructor (`New*`) — created before Wails runtime is available
|
1. `New*()` constructor — created before Wails runtime is available
|
||||||
2. `SetContext(ctx context.Context)` — called after Wails runtime starts; registers event handlers
|
2. `SetContext(ctx context.Context)` — called after runtime starts; registers event handlers, restores state
|
||||||
|
|
||||||
#### Comments
|
### Build Tags
|
||||||
- Doc comments on all exported functions/types
|
Dev/prod detection via `internal/dev/`: `//go:build dev` → `IsDev = true`, `//go:build !dev` → `IsDev = false`.
|
||||||
- End sentences with periods (enforced by `godot`)
|
|
||||||
- Blank line after early returns (enforced by `nlreturn`)
|
|
||||||
|
|
||||||
#### Build Tags
|
## TypeScript/Lit Code Style
|
||||||
Dev/prod detection uses build tags in `internal/dev/`:
|
|
||||||
- `//go:build dev` → `IsDev = true` (used by `make dev`)
|
|
||||||
- `//go:build !dev` → `IsDev = false` (production builds)
|
|
||||||
|
|
||||||
### TypeScript/Lit Code Style
|
### Import Organization
|
||||||
|
Use path aliases from `tsconfig.json`. Use `import type` for type-only imports (`verbatimModuleSyntax`).
|
||||||
#### Import Organization
|
|
||||||
Use path aliases defined in `tsconfig.json`:
|
|
||||||
```typescript
|
```typescript
|
||||||
import { EventsOn, EventsEmit } from '@runtime/runtime';
|
import { EventsOn, EventsEmit } from '@runtime/runtime';
|
||||||
import type { TrackInfo } from '@store/player-store';
|
import type { TrackInfo } from '@store/player-store';
|
||||||
```
|
```
|
||||||
Available aliases: `@go/*`, `@components/*`, `@store/*`, `@runtime/*`, `@utils/*`, `@assets/*`, `@pages/*`
|
Aliases: `@go/*`, `@components/*`, `@store/*`, `@runtime/*`, `@utils/*`, `@assets/*`, `@pages/*`
|
||||||
|
|
||||||
#### Lit Component Pattern
|
### Lit Component Pattern
|
||||||
```typescript
|
```typescript
|
||||||
@customElement('component-name')
|
@customElement('component-name')
|
||||||
export class ComponentName extends LitElement {
|
export class ComponentName extends LitElement {
|
||||||
@state() private someState: Type = initialValue;
|
@state() private someState: Type = initialValue;
|
||||||
|
static override styles = css`...`;
|
||||||
override connectedCallback() { super.connectedCallback(); }
|
override connectedCallback() { super.connectedCallback(); }
|
||||||
|
override disconnectedCallback() { super.disconnectedCallback(); }
|
||||||
override render() { return html`...`; }
|
override render() { return html`...`; }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
- `override` keyword required (`noImplicitOverride: true`)
|
||||||
#### TypeScript Strictness
|
- Private event handlers as arrow functions: `private handleClick = () => { ... }`
|
||||||
- `strict: true` enabled
|
- `strict: true`, `noUncheckedIndexedAccess: true`, `verbatimModuleSyntax: true`, `experimentalDecorators: true`, `noUnusedLocals: true`, `noUnusedParameters: true`
|
||||||
- `noUncheckedIndexedAccess: true` — check array/object access
|
- Singleton stores in `frontend/src/store/` (backend is source of truth). `ReactiveController` pattern connects Lit components to stores — subscribe in `hostConnected()`, unsubscribe in `hostDisconnected()`.
|
||||||
- `noImplicitOverride: true` — must use `override` keyword
|
|
||||||
- `verbatimModuleSyntax: true` — use `import type` for type-only imports
|
|
||||||
- `experimentalDecorators: true` — required for Lit decorators
|
|
||||||
|
|
||||||
## Frontend-Backend Communication
|
## Frontend-Backend Communication
|
||||||
|
|
||||||
### Event System
|
### Event System
|
||||||
Events are the primary communication mechanism. **Event names must match exactly** in both files:
|
Events are the primary communication mechanism. **Event names must match exactly** in both files:
|
||||||
- Go: `backend/events/events.go`
|
- Go: `backend/events/events.go` — TypeScript: `frontend/src/events.ts`
|
||||||
- TypeScript: `frontend/src/events.ts`
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
runtime.EventsEmit(p.ctx, events.TrackChanged, trackInfo)
|
runtime.EventsEmit(p.ctx, events.TrackChanged, trackInfo)
|
||||||
runtime.EventsOn(p.ctx, events.RequestPlay, func(_ ...any) { p.Play() })
|
runtime.EventsOn(p.ctx, events.RequestPlay, func(_ ...any) { p.Play() })
|
||||||
```
|
```
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
EventsEmit(Events.RequestPlay);
|
EventsEmit(Events.RequestPlay);
|
||||||
EventsOn(Events.TrackChanged, (trackInfo: TrackInfo) => { ... });
|
EventsOn(Events.TrackChanged, (trackInfo: TrackInfo) => { ... });
|
||||||
```
|
```
|
||||||
|
|
||||||
### State Management
|
|
||||||
- Singleton stores in `frontend/src/store/` — backend is source of truth
|
|
||||||
- ReactiveController pattern (`PlayerController`) connects Lit components to stores
|
|
||||||
|
|
||||||
### HTMX
|
### HTMX
|
||||||
The config page uses HTMX for HTML fragment loading. Backend serves HTML fragments via templ templates (`backend/config/config-form.templ`, `backend/library/config.templ`). Config is a separate entry point (`src/pages/config/`).
|
The config page uses HTMX for HTML fragment loading. Backend serves fragments via templ templates (`backend/config/config-form.templ`, `backend/library/config.templ`). Config has a separate entry point (`src/pages/config/`).
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
SQLite with sqlc for type-safe queries. Schemas in `backend/database/sql/schemas/`, queries in `backend/database/sql/queries/`, generated code in `backend/database/sql/sqlcgen/`. After modifying SQL files, run `make generate`.
|
SQLite with sqlc for type-safe queries. Schemas in `backend/database/sql/schemas/`, queries in `backend/database/sql/queries/`, generated code in `backend/database/sql/sqlcgen/`. SQLite opened with WAL mode and `SetMaxOpenConns(1)` (single-writer). After modifying SQL files, run `make generate`.
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
- `backend/` — Go backend: `config/`, `database/`, `events/`, `library/`, `metadata/`, `player/`, `system/`
|
- `backend/` — Go: `config/`, `database/`, `events/`, `library/`, `metadata/`, `models/`, `player/`, `queue/`, `system/`, `logging/`, `frontendutil/`, `assets/`
|
||||||
- `frontend/src/` — TypeScript/Lit: `components/`, `pages/`, `store/`, `utils/`
|
- `frontend/src/` — TypeScript/Lit: `components/`, `pages/`, `store/`, `utils/`
|
||||||
- `frontend/wailsjs/` — Generated Wails bindings
|
- `frontend/wailsjs/` — Auto-generated Wails bindings (do not edit)
|
||||||
- `internal/dev/` — Build-tag-based dev/prod detection
|
- `internal/dev/` — Build-tag-based dev/prod detection
|
||||||
- `pkg/templcomp/` — Shared templ component utilities
|
- `pkg/templcomp/` — Shared templ component utilities
|
||||||
- `test_data/` — Audio test fixtures
|
- `test_data/` — Audio test fixtures
|
||||||
|
|
||||||
## Key Dependencies
|
|
||||||
|
|
||||||
- **Wails v2**: Desktop app framework bridging Go and web frontend
|
|
||||||
- **beep**: Audio playback library (custom fork `TheCodeOfCaleb/beep`)
|
|
||||||
- **sqlc**: Type-safe SQL code generation
|
|
||||||
- **templ**: Go HTML templating
|
|
||||||
- **Lit**: Web component framework
|
|
||||||
- **Web Awesome**: Web component UI library (`@awesome.me/webawesome`)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user