Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 76 additions & 71 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,90 +1,95 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Instructions for working in this codebase. See `README.md` for architecture overview.

## What is pv

`pv` is a local development server manager powered by FrankenPHP (Caddy + embedded PHP). It replaces Docker for local dev by managing FrankenPHP instances that serve projects under `.test` domains with HTTPS. Supports multiple PHP versions simultaneously. Written in Go using cobra for CLI commands. See `plan.md` for the full vision.
`pv` is a local dev server manager powered by FrankenPHP. Go + cobra CLI. Manages PHP versions, serves projects under `.test` domains with HTTPS, runs containerized backing services via Colima/Docker.

## Commands
## Build & test

```bash
go build -o pv . # build the binary
go test ./... # run all tests
go test ./internal/registry/ # run tests for one package
go test ./cmd/ -run TestLink # run tests matching a pattern
go test ./... -v # verbose output
go build -o pv . # build
go test ./... # all tests
go test ./internal/registry/ # one package
go test ./cmd/ -run TestLink # pattern match
```

## Architecture
Build version is set via `go build -ldflags "-X github.com/prvious/pv/cmd.version=1.0.0"` — defaults to `"dev"`.

```
main.go # entry point — calls cmd.Execute()
cmd/ # cobra commands
root.go # rootCmd, Execute()
link.go, unlink.go, list.go # project management
start.go, stop.go, restart.go, status.go, log.go # server lifecycle
install.go, update.go # first-time setup and updates
php.go, php_install.go, php_list.go, php_remove.go # PHP version management
use.go # switch global PHP version
internal/
config/ # path helpers for ~/.pv/ directory structure
paths.go # PvDir, PhpDir, PhpVersionDir, PortForVersion, etc.
settings.go # TLD + GlobalPHP settings
registry/ # project registry (JSON in ~/.pv/data/registry.json)
registry.go # Project{Name,Path,Type,PHP}, Registry with CRUD + GroupByPHP
phpenv/ # PHP version management
phpenv.go # InstalledVersions, IsInstalled, SetGlobal, Remove
install.go # Download FrankenPHP from prvious/pv releases + PHP CLI from static-php.dev
resolve.go # ResolveVersion: .pv-php → composer.json → global default
available.go # AvailableVersions from GitHub releases
shim.go # WriteShims: creates ~/.pv/bin/php shim script
caddy/ # Caddyfile generation (multi-version aware)
caddy.go # GenerateSiteConfig(project, globalPHP), GenerateAllConfigs, GenerateVersionCaddyfile
server/ # process management
process.go # Start supervisor (DNS + main FP + secondary FPs), ReconfigureServer
frankenphp.go # StartFrankenPHP, StartVersionFrankenPHP, Reload
dns.go # Embedded DNS server on port 10053
binaries/ # binary download (Mago, Composer)
detection/ # project type detection (laravel, php, static)
setup/ # install prerequisites, resolver, selftest
```
## Command conventions

## Directory layout (~/.pv/)
- **Colon-namespaced**: tool/service/daemon commands use `tool:action` format (e.g., `mago:install`, `service:add`, `daemon:enable`). Core commands (`link`, `start`, `stop`) are plain.
- **All commands register on `rootCmd`** — cobra requires a flat `cmd/` directory. No subdirectories.
- **Always use `RunE`** (not `Run`) so errors propagate.
- **Command files are named `<tool>_<action>.go`** (e.g., `mago_install.go`, `service_add.go`).

```
~/.pv/
├── bin/ # symlinks to active global version + Mago, Composer
├── config/ # Caddyfiles + settings.json
│ ├── Caddyfile # main process
│ ├── php-8.3.Caddyfile # secondary process (if needed)
│ ├── sites/ # per-project configs for main process
│ └── sites-8.3/ # per-project configs for secondary process
├── data/ # registry.json, versions.json, pv.pid
├── logs/ # caddy.log, caddy-8.3.log
└── php/ # per-version binaries
├── 8.3/frankenphp + php
├── 8.4/frankenphp + php
└── 8.5/frankenphp + php
```
## Tool command rules

Every managed tool (php, mago, composer, colima) follows a strict five-command pattern. When adding a new tool, create all five:

| Command | What it does | Where logic lives |
|---------|-------------|-------------------|
| `:download` | Fetches binary to private storage | `internal/binaries/` or `internal/phpenv/` |
| `:path` | Exposes/unexposes from PATH (supports `--remove`) | `internal/tools/` |
| `:install` | Orchestrates `:download` then `tools.Expose()` | `cmd/` — delegates only |
| `:update` | Redownloads, re-exposes if `tools.IsExposed()` | `cmd/` + `internal/` |
| `:uninstall` | Unexposes + removes binary files | `cmd/` + `internal/tools/` |

**Hard rules:**
1. `:install` MUST delegate to `:download` RunE — never inline download logic in `cmd/`.
2. Download logic lives in `internal/binaries/` or `internal/phpenv/`, never in `cmd/`.
3. Exposure logic lives in `internal/tools/` — use `tools.Expose()` / `tools.Unexpose()`.
4. `:update` uses `tools.IsExposed()` (not `AutoExpose`) to decide re-exposure — handles manually-exposed tools correctly.
5. New tools must be registered in `internal/tools/tool.go`'s `All` map with correct `ExposureType` and `AutoExpose`.

## Orchestrator commands

`install`, `update`, and `uninstall` are thin orchestrators. They call per-tool `:install`/`:update`/`:uninstall` RunE functions. They MUST NOT contain download, exposure, or cleanup logic — that belongs in the per-tool commands.

- `pv update` self-updates the pv binary first (via `syscall.Exec` re-exec with `--no-self-update`), then delegates to each tool's `:update`.
- `pv restart` delegates to `daemon:restart` in daemon mode, otherwise reloads config via admin API.

## Binary storage rules

- `~/.pv/bin/` — user PATH. **Only** shims and symlinks go here. Never place real binaries.
- `~/.pv/internal/bin/` — private storage. Real binaries (mago, composer.phar, colima) live here.
- `~/.pv/php/{ver}/` — versioned PHP binaries (php, frankenphp) live here.
- Use `config.InternalBinDir()` for private storage paths, `config.BinDir()` for PATH entries.

## UI rules

All user-facing operations MUST use `internal/ui/` helpers. Never use raw `fmt.Print` for status output.

- **Long operations**: wrap in `ui.Step(label, fn)` — shows spinner, then `✓ result` or `✗ error`.
- **Downloads**: use `ui.StepProgress(label, fn)` — shows progress bar with percentage.
- **Multi-step commands**: use `ui.Header(version)` at start, `ui.Footer(start, msg)` at end.
- **Lists/tables**: use `ui.Table(headers, rows)` or `ui.Tree(items)`.
- **One-liners**: `ui.Success(text)`, `ui.Fail(text)`, `ui.Subtle(text)`.
- All output goes to `os.Stderr` (stdout is reserved for machine-readable output like `pv env`).

## Import cycle: phpenv ↔ tools

`phpenv` and `tools` cannot import each other. This is resolved via callback:
- `phpenv.ExposeFunc` is a `func(name string) error` variable
- `phpenv/shim.go` init() wires it to `tools.Expose()`
- When adding new cross-package dependencies, use the same callback pattern — don't create import cycles.

## Multi-version architecture
## Testing conventions

- **Main FrankenPHP** (global version): serves on :443/:80, handles projects using the global PHP version directly via `php_server`, and proxies non-global projects via `reverse_proxy`.
- **Secondary FrankenPHP** (per non-global version): serves on high port (8830 for 8.3, 8840 for 8.4, etc.), HTTP only, `admin off`. The main process proxies to these.
- **Port scheme**: `8000 + major*100 + minor*10` (e.g., PHP 8.38830).
- FrankenPHP binaries come from `prvious/pv` GitHub releases (format: `frankenphp-{platform}-php{version}`).
- **Filesystem isolation**: always use `t.Setenv("HOME", t.TempDir())` — never touch the real home dir.
- **Cmd tests**: build fresh cobra command trees per test to avoid state leaking.
- **Registry**: in-memory + explicit save. `Load()` → mutate`Save()`.
- **E2E tests**: live in `scripts/e2e/`, run on GitHub Actions (macOS). Source `scripts/e2e/helpers.sh`. Use these for anything needing real binaries, network, DNS, or HTTPS. Add new phases to `.github/workflows/e2e.yml`.

## Testing strategy
## Multi-version PHP

- **Unit tests** (`go test ./...`): Run locally. Use `t.Setenv("HOME", t.TempDir())` for filesystem isolation. Fake binaries (bash scripts) can stand in for real PHP when testing shims.
- **E2E tests** (`.github/workflows/e2e.yml` + `scripts/e2e/`): Run on GitHub Actions (macOS runner) to simulate real end-user flows. These tests use real PHP, real Composer, real FrankenPHP — things we can't easily run locally. **When your feature involves real binary execution, network calls, DNS, HTTPS, or anything that needs a full `pv install` environment, add an e2e script in `scripts/e2e/` and wire it into the workflow.** Each script sources `scripts/e2e/helpers.sh` for `assert_contains`, `assert_fails`, `curl_site`, etc. The workflow phases run sequentially: install → verify → fixtures → link → start → curl → shim → composer → errors → stop → lifecycle → update → verify-final.
- Main FrankenPHP serves on :443/:80, proxies non-global versions via `reverse_proxy`.
- Secondary FrankenPHP per version on high port: `8000 + major*100 + minor*10` (8.3 → 8830).
- Version resolution order: `.pv-php` file → `composer.json` require.php → global default.

## Key patterns
## Services

- **Test isolation**: Tests use `t.Setenv("HOME", t.TempDir())` so filesystem ops go to a temp dir.
- **Cmd tests**: Build fresh cobra command trees per test to avoid state leaking.
- **Registry is in-memory + explicit save**: `Load()` → mutate → `Save()`.
- **Commands use `RunE`** (not `Run`) so errors propagate.
- **Version resolution**: `.pv-php` file → `composer.json` require.php → global default.
- **Caddy site config**: `GenerateSiteConfig(project, globalPHP)` — empty globalPHP = single-version mode.
- Each backing service (mysql, postgres, redis, mail, s3) implements `services.Service` interface.
- Services run as Docker containers via Colima. Container operations go through `container.Engine`.
- Service commands use `service:action` format. New services need: implementation in `internal/services/`, command in `cmd/service_*.go`.
141 changes: 131 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Currently supports PHP projects (Laravel, generic PHP, static sites).
pv install

# Install a PHP version
pv php install 8.4
pv php:install 8.4

# Link a project — it's now live at https://my-app.test
pv link ~/code/my-app
Expand All @@ -41,9 +41,6 @@ pv stop
pv restart
pv log

# Unlink a project
pv unlink my-app

# Unlink a project
pv unlink my-app
```
Expand All @@ -54,20 +51,144 @@ pv unlink my-app

```bash
# Install multiple versions
pv php install 8.3
pv php install 8.4
pv php install 8.5
pv php:install 8.3
pv php:install 8.4
pv php:install 8.5

# Switch the global default
pv use 8.4
pv php:use 8.4

# See what's installed
pv php list
pv php:list

# Remove a version
pv php remove 8.3
pv php:remove 8.3
```

Per-project versions are supported too — drop a `.pv-php` file in your project root or let `pv` read the PHP constraint from `composer.json`. Multiple PHP versions run simultaneously, each project served by its own FrankenPHP process.

`pv link` auto-detects your project type (Laravel, Laravel + Octane, generic PHP, static) and generates the right server configuration automatically.

### Tool management

Each managed tool has a consistent set of commands:

```bash
# Download a tool to private storage
pv mago:download

# Expose/remove from PATH
pv mago:path
pv mago:path --remove

# Install (download + expose)
pv mago:install

# Update to latest version
pv mago:update

# Uninstall (remove binary + PATH entry)
pv mago:uninstall
```

This pattern applies to all tools: `php`, `mago`, `composer`, `colima`.

### Backing services

Containerized services for databases, caching, and more — powered by Colima/Docker:

```bash
# Add a service
pv service:add mysql
pv service:add redis 7

# Manage services
pv service:start mysql
pv service:stop mysql
pv service:status
pv service:list

# Inject credentials into your project's .env
pv service:env my-app

# View logs
pv service:logs mysql

# Remove or destroy
pv service:remove mysql
pv service:destroy mysql
```

Available services: MySQL, PostgreSQL, Redis, Mail (Mailpit), S3 (MinIO).

### Daemon mode

Run pv as a background service that starts on login:

```bash
pv daemon:enable # Install + start daemon
pv daemon:disable # Stop + uninstall daemon
pv daemon:restart # Restart the daemon
```

### Update & uninstall

```bash
pv update # Self-update pv + all tools
pv update --no-self-update # Only update tools
pv uninstall # Complete removal with guided cleanup
```

## Architecture

```
~/.pv/
├── bin/ # User PATH — shims and symlinks only
│ ├── php # Shim (version resolution)
│ ├── composer # Shim (wraps PHAR with PHP)
│ ├── frankenphp # Symlink → ~/.pv/php/{ver}/frankenphp
│ ├── mago # Symlink → ~/.pv/internal/bin/mago
│ └── colima # Symlink → ~/.pv/internal/bin/colima (opt-in)
├── internal/bin/ # Private storage — real binaries
│ ├── colima
│ ├── mago
│ └── composer.phar
├── config/ # Server configuration
│ ├── Caddyfile
│ ├── settings.json
│ ├── sites/ # Per-project Caddyfile includes
│ └── sites-{ver}/ # Per-version site configs
├── data/ # Registry, PID file
├── logs/ # Server logs
└── php/ # Versioned PHP binaries
└── {ver}/frankenphp + php
```

### Multi-version PHP

The main FrankenPHP process (global PHP version) serves on :443/:80. Projects using a different PHP version are proxied to secondary FrankenPHP processes running on high ports (`8000 + major*100 + minor*10`, e.g., PHP 8.3 → port 8830).

Version resolution: `.pv-php` file → `composer.json` `require.php` → global default.

### Source layout

```
main.go # Entry point
cmd/ # CLI commands (flat, colon-namespaced)
internal/
tools/ # Tool abstraction (exposure, shims, symlinks)
config/ # Path helpers, settings
registry/ # Project registry
phpenv/ # PHP version management
caddy/ # Caddyfile generation
server/ # Process management, DNS
daemon/ # macOS launchd integration
binaries/ # Binary download helpers
selfupdate/ # pv self-update
colima/ # Container runtime
container/ # Docker abstraction
services/ # Backing service definitions
detection/ # Project type detection
setup/ # Prerequisites, shell config
ui/ # Terminal UI (lipgloss)
```
Loading