This document provides guidelines for AI agents working on the phvm (PHP Version Manager) codebase.
phvm is a cross-platform PHP version manager written in Go 1.27.1. The required Go version is declared in go.mod and used by CI and release builds. It installs PHP from source, manages multiple versions via symlinks, and handles extensions and php.ini configuration. The CLI is built with cobra.
# Build
make build # Build the binary
make release-package-check # Build and check six release archives without publishing
make release-check # Preflight, snapshot packaging and artifact smoke; no commit/tag/push
# Run without building
make dev ARGS="install 8.3" # Run with go run
# Install
make install # Install to GOBIN
make install-local # Install to ~/.phvm/bin
# Clean
make clean # Remove build artifacts
# Dependencies
make deps # Download and tidy dependencies
make update-deps # Update all dependenciesmake lint # Run pinned golangci-lint via go run
make fmt # Format code with go fmt
make vet # Run go vet
make govulncheck # Check known Go and dependency vulnerabilities
make gosec # Check source security issues
make security # Run govulncheck and gosec
make security-baseline # Keep all gosec findings; reject new identities beyond reviewed baseline
# Direct golangci-lint (if installed)
golangci-lint run ./...Enabled linters: errcheck, govet, ineffassign, staticcheck (includes former gosimple checks), unused, misspell, unconvert, unparam, revive. Formatters: gofmt, goimports. The configuration uses golangci-lint v2.
# Run all tests
make test # Runs: go test -v -race -cover ./...
make test-release # Isolated Git release lifecycle
make test-installers # Installer integrity and publication
make test-checks # Security baseline and release archive regressions
make test-scenarios # Required fixture lifecycles; skipped/missing tests fail
make test-runtime-scenarios # Real PHP with synthetic PHAR/native module fixtures
make test-native-scenarios # Official Composer/PECL; network and Linux PHP SDK required
make test-native-build # Real PHP source build; supplied archive/version/SHA256
# Run tests with coverage report
make test-coverage # Generates coverage.html
# Run a single test
go test -v -run TestFunctionName ./internal/package/
# Run specific test case in table-driven test
go test -v -run TestFunctionName/test_case_name ./internal/package/
# Examples
go test -v -run TestParseVersion ./internal/core/
go test -v -run TestAtomicWriteFile ./internal/fsutil/
go test -v -run TestConfDManager/Create ./internal/ini/Scenario definitions and states are in scripts/scenarios.json; the coverage
matrix and prerequisites are in docs/scenario-matrix.md. Execution evidence is
written to ignored reports/scenarios-*.json and .jsonl. Fixture results,
real runtime fixtures, upstream smoke and cross-compilation are distinct proof
levels. Ordinary make test may skip optional native tests; the scenario runner
rejects skips within its selected required profile.
Order imports in three groups separated by blank lines:
- Standard library
- Third-party packages
- Local packages (
github.com/hightemp/phvm/...)
import (
"context"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/hightemp/phvm/internal/core"
"github.com/hightemp/phvm/internal/log"
)Rules:
- No dot imports (enforced by revive)
- Use
goimportsfor automatic ordering
- Use
gofmtwith simplify enabled - Tabs for indentation (Go standard)
- No trailing whitespace
| Element | Convention | Example |
|---|---|---|
| Packages | lowercase, short, descriptive | cli, core, fsutil, remote |
| Exported types | PascalCase | Client, Paths, Version |
| Unexported types | camelCase | clientOptions |
| Exported functions | PascalCase, verb prefix | NewClient(), ParseVersion() |
| Unexported functions | camelCase | extractPriority(), normalizeIniName() |
| Constants (exported) | PascalCase | LevelDebug, SpecialVersionAliases |
| Constants (unexported) | camelCase | disabledSuffix |
| Interfaces | PascalCase, -er suffix when applicable |
io.Reader, Locker |
| Receivers | short, 1-2 letters | (c *Client), (p *Paths), (v *Version) |
Wrap errors with context:
if err != nil {
return fmt.Errorf("create request: %w", err)
}Return early, avoid deep nesting:
func doSomething() error {
if err := step1(); err != nil {
return fmt.Errorf("step1: %w", err)
}
if err := step2(); err != nil {
return fmt.Errorf("step2: %w", err)
}
return nil
}CLI error handling pattern:
func runCommand(cmd *cobra.Command, args []string) {
if err := doWork(); err != nil {
log.Error("Failed to do work: %v", err)
os.Exit(1)
}
log.Success("Work completed")
}Package comments (required):
// Package core provides core functionality for phvm.
package coreExported function comments:
// ParseVersion parses a version string into a Version struct.
func ParseVersion(s string) (*Version, error) {Exported type comments:
// Client is an HTTP client with retry support.
type Client struct {Table-driven tests with subtests:
func TestParseVersion(t *testing.T) {
tests := []struct {
input string
expected *Version
wantErr bool
}{
{"8.3.30", &Version{Major: 8, Minor: 3, Patch: 30}, false},
{"", nil, true},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
v, err := ParseVersion(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseVersion(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
// ... assertions
})
}
}Temp directory cleanup:
tmpDir, err := os.MkdirTemp("", "phvm-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)phvm/
├── cmd/phvm/main.go # Entry point
├── internal/
│ ├── cli/ # Cobra commands (root.go, install.go, etc.)
│ ├── core/ # Core types: Paths, Version, Config, Alias
│ ├── build/ # PHP build logic and profiles
│ ├── remote/ # HTTP client, php.net API, PECL
│ ├── fsutil/ # Filesystem utilities (atomic writes, symlinks)
│ ├── ini/ # php.ini management, conf.d
│ ├── ext/ # Extension installation
│ ├── shell/ # Shell init scripts
│ ├── composer/ # Composer installation
│ ├── doctor/ # Dependency checking
│ ├── toolchain/ # Shared build environment and command parsing
│ └── log/ # Structured logging
├── scripts/ # Install scripts (bash, PowerShell)
├── Makefile # Build automation
├── .golangci.yml # Linter configuration
└── .goreleaser.yml # Release configuration
Key dependencies (see go.mod):
github.com/spf13/cobra- CLI frameworkgithub.com/Masterminds/semver/v3- Semantic versioninggithub.com/fatih/color- Terminal colorsgithub.com/hashicorp/go-retryablehttp- HTTP client with retriesgithub.com/pelletier/go-toml/v2- TOML config parsinggithub.com/schollz/progressbar/v3- Progress bars
Manager pattern for domain logic:
type InstalledManager struct {
paths *Paths
}
func NewInstalledManager(paths *Paths) *InstalledManager {
return &InstalledManager{paths: paths}
}
func (m *InstalledManager) IsInstalled(version string) bool { ... }Options pattern for configuration:
type ClientOptions struct {
UserAgent string
Timeout time.Duration
Retries int
}
func DefaultClientOptions() ClientOptions { ... }
func NewClient(opts ClientOptions) *Client { ... }Atomic file operations: Use fsutil.AtomicWriteFile() for safe file writes.