wp-environments · git:20261006.3256c65 · 2026-10-06 · sha256 9289dd39082b2ef6

wp-environments git:20261006.3256c65A

Immutable. This exact content never changes and is served at /api/v1/blob/9289dd39082b2ef6.

---
name: wp-environments
description: Local WordPress environment detection and setup — which engine a project runs on (native, Docker, DDEV, Lando, wp-env), the .wp-create.json manifest, WP-CLI wrapper selection, template placeholders, Docker port conflicts, PHP versions and adopt mode. Use when creating, adopting or configuring a local site, or when choosing how to run WP-CLI against one.
user-invocable: false
---

# WordPress Environment Detection & Configuration

This skill teaches how to detect the local development environment, read project configuration, select the correct WP-CLI wrapper, generate config files from templates, and manage PHP versions. It applies whenever a user requests environment setup or when a `.wp-create.json` manifest is present in the project root.

---

## When This Skill Applies

- User requests WordPress environment setup, creation, or configuration
- A `.wp-create.json` file exists in the project root
- User asks about Docker, native server, DDEV, Lando, or wp-env setup
- User asks about PHP version management or web server configuration

---

## 1. Environment Detection

Run the detection script — run it, do not reimplement it. It needs no sudo, prints JSON on
stdout and exits 0; an unknown subcommand prints the usage and exits 1:

```bash
bash -c "${CLAUDE_PLUGIN_ROOT}/bin/wp-env-setup.sh detect"
```

The top-level keys are `os`, `package_manager`, `web_servers` (`nginx`, `apache`, `caddy`),
`php` (`versions`, `active`, `extensions`), `docker` (`installed`, `version`, `compose`),
`tools` (`ddev`, `lando`, `wp-env`, `wp-cli`) and `database` (`mariadb`, `mysql`). A tool that
is absent still carries every key, with an empty version — test `installed`, never the
presence of `version`:

```json
"caddy": { "installed": false, "version": "", "running": false }
```

`php.versions` lists the versions installed under `/etc/php/` (Debian, Ubuntu) or through
Homebrew; anywhere else it holds only the active one.

Use this output to:
- Present available environment options to the user
- Auto-select the best environment type when the user does not specify
- Determine which web servers, PHP versions, and database engines are ready

---

## 2. Project Manifest: `.wp-create.json`

The `.wp-create.json` file is the single source of truth for all commands, agents, and skills. It is generated by `/wp-create` at the project root.

### Full Manifest Structure

```json
{
  "project": {
    "name": "My Project",
    "slug": "my-project",
    "domain": "my-project.local.com",
    "path": "/var/www/html/my-project",
    "created": "2026-03-15"
  },
  "environment": {
    "type": "docker",
    "engine": "docker-compose",
    "web_server": "nginx",
    "php_version": "8.3",
    "container_prefix": "my-project"
  },
  "database": {
    "name": "wp_my_project",
    "user": "root",
    "host": "db"
  },
  "wordpress": {
    "version": "latest",
    "admin_user": "webmaster",
    "admin_email": "webmaster@local.com",
    "url": "https://my-project.local.com",
    "permalink_structure": "/%postname%/"
  },
  "languages": {
    "primary": "en",
    "additional": ["es"],
    "default": "en"
  },
  "plugins": {
    "profile": "starter",
    "installed": [
      "secure-custom-fields",
      "wordpress-seo",
      "wp-fastest-cache"
    ]
  },
  "theme": {
    "slug": "my-project",
    "initialized": false
  },
  "wp_cli": {
    "wrapper": "docker exec my-project-wp wp --allow-root",
    "path_flag": ""
  }
}
```

### Reading the Manifest

Always check for `.wp-create.json` at the project root before executing WP-CLI commands. If it exists, read `wp_cli.wrapper` and use it as the command prefix (referred to as `$WP` throughout this project).

If `.wp-create.json` does not exist, the commands' gate (`wp-config.mjs validate`) exits `3`:
the project was not created by `/wp-create`. Most commands stop there; a site that already
runs and only needs registering goes through `/wp-adopt`, which writes the manifest.

---

## 3. WP-CLI Wrapper Selection

The `wp_cli.wrapper` field determines how WP-CLI commands are executed. Every agent reads this value and prepends it to all `wp` commands.

| Environment | `wp_cli.wrapper` value |
|-------------|----------------------|
| Native | `wp --path=/var/www/html/my-project` |
| Docker | `docker exec my-project-wp wp --allow-root` |
| DDEV | `ddev wp` |
| Lando | `lando wp` |
| wp-env | `npx wp-env run cli wp` |

### Usage Pattern

```bash
# Read wrapper from manifest
WP=$(jq -r '.wp_cli.wrapper' .wp-create.json)

# Use it for all WP-CLI operations
$WP core version
$WP plugin list --format=table
$WP option update blogname "My Site"
```

Never hardcode the execution method. Always read from the manifest.

---

## 4. Template Placeholder Replacement

All template files (`.tpl` extension) use `{{placeholder}}` syntax. No template engine is needed -- simple string replacement.

### Process

1. Read the `.tpl` file
2. Replace every `{{placeholder}}` with the corresponding value from `.wp-create.json` or the detection output
3. Write the result to the final config file path (without the `.tpl` extension)

The templates live in `${CLAUDE_PLUGIN_ROOT}/templates/native/` and
`${CLAUDE_PLUGIN_ROOT}/templates/docker/` (including the dot-files `.wp-env.json.tpl`,
`.lando.yml.tpl` and `.ddev/`). A file written with a `{{…}}` token still in it is broken —
docker compose and wp-env refuse it — so every token below is replaced, and these are all the
tokens the templates use:

| Placeholder | Source | Example |
|-------------|--------|---------|
| `{{domain}}` | `project.domain` | `my-project.local.com` |
| `{{document_root}}` | `project.path` | `/var/www/html/my-project` |
| `{{project_name}}` | `project.slug` | `my-project` |
| `{{theme_slug}}` | `theme.slug` | `my-project` |
| `{{php_version}}` | `environment.php_version` | `8.3` |
| `{{web_server}}` | `environment.web_server` | `nginx` |
| `{{db_name}}` | `database.name` | `wp_my_project` |
| `{{db_user}}` | `database.user` | `root` |
| `{{db_password}}` | **Not a manifest field.** `bash -c "node ${CLAUDE_PLUGIN_ROOT}/bin/wp-config.mjs get '${PROJECT_PATH}' db_password"` | generated per project, never a fixed default |
| `{{ssl_cert}}` | Derived: `/etc/ssl/certs/<domain>.crt` | |
| `{{ssl_key}}` | Derived: `/etc/ssl/private/<domain>.key` | |
| `{{php_fpm_sock}}` | Derived: `/var/run/php/php<php_version>-fpm.sock` | |
| `{{web_user}}` | Derived from the OS: `nginx` (Fedora/RHEL with nginx), `apache` (Fedora/RHEL with Apache), `www-data` (Debian/Ubuntu) | |
| `{{max_upload_size}}` | `1024M` unless the user asks otherwise; nginx templates only | `1024M` |
| `{{http_port}}` | Host port for HTTP: docker compose and wp-env (see section 5) | `80`, wp-env `8888` |
| `{{https_port}}` | Host port for HTTPS, docker compose | `443` |
| `{{tests_port}}` | wp-env's tests site | `8889` |
| `{{phpmyadmin_port}}` | docker compose | `8080` |
| `{{mailpit_port}}` | Mailpit's web UI, docker compose | `8025` |
| `{{mailpit_smtp_port}}` | Mailpit's SMTP, docker compose | `1025` |

The database password is the one value not read from `.wp-create.json`: it lives in the
gitignored `.wp-create.local.json`, and the only supported way to read it is
`wp-config.mjs get '${PROJECT_PATH}' db_password`, which resolves environment → local file →
manifest and warns when the last rung wins. Reading that field out of the manifest yields an
empty value on every project `/wp-create` has written since, and a silent legacy read on
every project it has not. `${PROJECT_PATH}` is not an environment variable the way
`${CLAUDE_PLUGIN_ROOT}` is: it is the directory holding `.wp-create.json`, and you substitute
the real path yourself, because inside the double quotes an unset one expands to nothing. The
command exits `0` with the value, `1` when no rung holds one (or the key is unknown), and `3`
when there is no manifest.

---

## 5. Docker Port Conflict Detection

Before starting containers, check only the host ports the templates publish. The database
is not one of them — `docker-compose.yml.tpl` publishes no port for `db` — so a native
MariaDB on 3306 is not a conflict.

| Port | Placeholder | Template |
|---|---|---|
| 80 | `{{http_port}}` | `docker-compose.yml.tpl` |
| 443 | `{{https_port}}` | `docker-compose.yml.tpl` |
| 8080 | `{{phpmyadmin_port}}` | `docker-compose.yml.tpl` |
| 8025 | `{{mailpit_port}}` | `docker-compose.yml.tpl` |
| 1025 | `{{mailpit_smtp_port}}` | `docker-compose.yml.tpl` |
| 8888 | `{{http_port}}` | `.wp-env.json.tpl` |
| 8889 | `{{tests_port}}` | `.wp-env.json.tpl` |

```bash
# Linux (docker compose; for wp-env check 8888 and 8889 instead)
ss -tlnp | grep -E ':(80|443|8080|8025|1025) '
# macOS or fallback
lsof -i :80 -i :443 -i :8080 -i :8025 -i :1025
```

A port in use gets the next free alternative (8081 for 8080, 8444 for 443), substituted into
its placeholder; stop the service holding it only when the user asks. No manifest field holds
a port: the generated `docker-compose.yml` or `.wp-env.json` is the record, so read the port
back from there.

---

## 6. PHP Version Management

### Native Environments

Run the script; do not type the package names yourself — they differ per OS and the script
already carries them, with the extensions WordPress needs (`cli fpm mysql curl mbstring xml
gd zip intl`):

```bash
bash -c "${CLAUDE_PLUGIN_ROOT}/bin/wp-env-setup.sh php-list"                  # installed versions, no sudo
bash -c "${CLAUDE_PLUGIN_ROOT}/bin/wp-env-setup.sh php-install --version=8.3" # needs sudo
```

`php-list` lists the versions already **installed** (`/etc/php/*/` on Debian and Ubuntu,
Homebrew on macOS); anywhere else, Fedora included, it prints only the active one. It shows
nothing that is merely available to install. `php-install` exits `1` without `--version` and
on an unsupported OS. On Debian and Ubuntu it adds the `ondrej/php` PPA first; on Fedora it
installs `php83` and its `php83-*` packages, falling back to the Remi naming. Switching the
CLI version is not scripted: on Debian and Ubuntu it is
`sudo update-alternatives --set php /usr/bin/php8.3`.

### Docker Environments

For Docker-based environments, PHP version is controlled by the container image tag in the template:

```yaml
# In docker-compose.yml.tpl
services:
  wordpress:
    image: wordpress:php{{php_version}}-fpm
```

No system-level PHP installation is needed. Change the `environment.php_version` value in `.wp-create.json` and regenerate the Docker config from the template.

### DDEV / Lando

These tools manage PHP internally:

- **DDEV:** Set `php_version` in `.ddev/config.yaml` (generated from template)
- **Lando:** Set `php` in `.lando.yml` recipe config (generated from template)

---

## 7. Adopt Mode Detection

Two different things share the word "adopt". `/wp-create`'s Adopt Mode, described here,
**reconfigures** an existing install into a `/wp-create` environment: vhost, SSL, hosts
entry, options. `/wp-adopt` (`bin/wp-config.mjs adopt`) only **registers** a site: it runs a
read-only probe and writes the manifest with `"origin": "adopted"`. Use it for a site that is
already served and only needs auditing, debugging or cloning into.

When targeting an existing WordPress installation, check for `wp-config.php` at the project path to enter "adopt" mode:

```bash
# Check if wp-config.php exists at the target path
if [[ -f "/var/www/html/my-project/wp-config.php" ]]; then
    # Enter adopt mode — skip download, DB creation, and config creation
fi
```

### Adopt Mode Behavior

When `wp-config.php` is found:

1. **Skip:** WordPress download, database creation, config creation, core install
2. **Extract config from the existing install:**
   ```bash
   $WP config list --format=json        # DB credentials, table prefix, debug settings
   $WP core version                      # WordPress version
   $WP plugin list --format=json         # Installed plugins
   $WP theme list --status=active --format=json  # Active theme
   $WP option get siteurl                # Current site URL
   $WP option get home                   # Current home URL
   $WP eval "echo PHP_VERSION;"          # PHP version in use
   ```
3. **Populate the manifest** from extracted values
4. **Still execute:** environment config generation (vhost/Docker), SSL, hosts entry, search-replace if domain changes
5. **Edge cases:**
   - Old WP version (< 5.0): Warn user, offer `$WP core update`
   - Multisite: Detect via `$WP config get MULTISITE`, abort with "Multisite not supported"

---

## Summary Checklist

- [ ] Run `${CLAUDE_PLUGIN_ROOT}/bin/wp-env-setup.sh detect` and parse the JSON output before making environment decisions
- [ ] Read `.wp-create.json` for all project configuration -- never hardcode values
- [ ] Use `wp_cli.wrapper` from the manifest as the WP-CLI command prefix
- [ ] Replace `{{placeholders}}` in `.tpl` files using manifest values
- [ ] Check for Docker port conflicts before starting containers
- [ ] Use OS-appropriate commands for PHP version management
- [ ] Detect `wp-config.php` to determine adopt mode vs fresh install
- [ ] All operations are idempotent -- safe to re-run