git:20261006.3256c65 to git:20261006.ec80a72

77 added, 178 removed. Audit A to A.

---
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.
+ description: Detects the local tooling with the bundled wp-env-setup.sh, reads the .wp-create.json manifest through wp-config.mjs (environment.type and environment.engine, the wp_cli.wrapper every WP-CLI call runs through, the database password kept in .wp-create.local.json), fills the placeholders in the native and Docker .tpl templates, and resolves port and PHP-version conflicts for a local WordPress site on native, docker-compose, DDEV, Lando or wp-env. Use when /wp-create or /wp-adopt sets up a local WordPress site, when choosing or reading the WP-CLI wrapper for one, when a WordPress docker compose or wp-env port is already taken, or when listing or installing PHP versions for a local WordPress site. Not for deploying to a server, or for containerizing an app that is not WordPress.
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
-
- ---
+ How a local WordPress site's environment is detected, recorded in `.wp-create.json`, and
+ turned into config files — and how every later command reaches WP-CLI through it.
## 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
+ ### The other subcommands
- ---
+ The script does the root-level writes too. Every subcommand except `detect` and `php-list`
+ needs sudo, and each one prints its usage line and exits `1` when a required flag is missing:
- ## 2. Project Manifest: `.wp-create.json`
+ | Subcommand | Flags | Does |
+ |---|---|---|
+ | `detect` | | The JSON above |
+ | `php-list` | `[--json]` | Installed PHP versions (section 6) |
+ | `php-install` | `--version=8.3` | Installs a PHP version and its extensions (section 6) |
+ | `native-setup` | `--domain=`, `--document-root=`, `--web-server=nginx\|apache\|caddy`, `[--vhost-src=/tmp/x.conf]`, `[--no-ssl]` | Certificate, hosts entry, vhost, reload |
+ | `native-remove` | `--domain=`, `[--web-server=]` | Removes the certificate, hosts entry and vhost |
+ | `vhost-install` | `--src=`, `--dest=` | Installs a generated vhost with its mode, owner and SELinux context |
+ | `ssl-generate` | `--domain=` | A self-signed certificate |
+ | `hosts-add`, `hosts-remove` | `--domain=` | The `/etc/hosts` entry |
+ | `permissions` | `--path=`, `[--web-user=]` | WordPress file and directory permissions |
+ | `service-reload` | `--service=` | Reloads the web server |
- 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.
+ **A generated vhost goes in through `native-setup --vhost-src` or `vhost-install`, never
+ `sudo mv` or `sudo cp`.** On an SELinux host a file moved out of `/tmp` keeps its `user_tmp_t`
+ label, nginx and Apache cannot read it, and the reload fails with `(13: Permission denied)`;
+ `vhost-install` runs `restorecon`. Caddy has no `--vhost-src`: its block is appended to the
+ Caddyfile by hand.
- ### Full Manifest Structure
+ ## 2. Project Manifest: `.wp-create.json`
+ `/wp-create` writes `.wp-create.json` at the project root (its Step 5 holds the full shape),
+ and `bin/wp-config.mjs` is the single definition of a valid one. The fields this skill reads:
+
```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": ""
- }
+ "project": { "slug": "my-project", "domain": "my-project.local.com", "path": "/var/www/html/my-project" },
+ "environment": { "type": "docker", "engine": "docker-compose", "web_server": "nginx", "php_version": "8.3" },
+ "database": { "name": "wp_my_project", "user": "root", "host": "db" },
+ "theme": { "slug": "my-project" },
+ "wp_cli": { "wrapper": "docker exec my-project-wp wp --allow-root", "path_flag": "" }
}
```
- ### Reading the Manifest
+ - `environment.type` is `native` or `docker` — `docker` for every container engine.
+ - `environment.engine` names the engine: `native`, `docker-compose`, `ddev`, `lando` or
+ `wp-env`. Commands branch on this one, not on `type`: store profiles (`/wp-create` with a
+ `woo-*` profile, `/wp-woo-setup`, `bin/store-kit-sync.sh`) need `native`, and stop on
+ anything else.
- 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).
+ Read a value through the validator, not with `jq`:
+ ```bash
+ bash -c "node ${CLAUDE_PLUGIN_ROOT}/bin/wp-config.mjs get '${PROJECT_PATH}' wp_cli.wrapper"
+ bash -c "node ${CLAUDE_PLUGIN_ROOT}/bin/wp-config.mjs validate '${PROJECT_PATH}'"
+ ```
+
+ `${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. `get` exits `0` with the value, `1`
+ for an unknown key or a missing value, and `3` when there is no manifest.
+
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` |
+ ## 3. The WP-CLI Wrapper
- ### Usage Pattern
+ `wp_cli.wrapper` is how WP-CLI runs against this site: `wp --path=…` on a native install,
+ `docker exec <slug>-wp wp --allow-root`, `ddev wp`, `lando wp` or `npx wp-env run cli wp` in
+ a container. `/wp-create` chooses it and writes it; everything else reads it with `get`
+ above and prefixes every `wp` command with it (`$WP` throughout this plugin):
```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.
-
- ---
+ Never hardcode the execution method. Always read it from the manifest.
## 4. Template Placeholder Replacement
- All template files (`.tpl` extension) use `{{placeholder}}` syntax. No template engine is needed -- simple string replacement.
-
- ### Process
+ All template files (`.tpl` extension) use `{{placeholder}}` syntax, replaced by plain string
+ substitution:
- 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)
+ 1. Read the `.tpl` file.
+ 2. Replace every `{{placeholder}}` with its value from the table below.
+ 3. Write the result to the final config 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.
-
- ---
+ manifest and warns when the last rung wins. Read straight out of the manifest it comes back
+ empty, or as a stale legacy value on a project that kept one there.
## 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
+ ### Native
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:
+ ### Containers
- 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"
+ No system PHP is involved. The version comes from `environment.php_version` through
+ `{{php_version}}`: the `wordpress:php{{php_version}}-fpm` image tag in
+ `docker-compose.yml.tpl`, `php_version` in `.ddev/config.yaml`, `php` in `.lando.yml`,
+ `phpVersion` in `.wp-env.json`. To change it, change the manifest value and regenerate the
+ file from its template.
- ---
+ ## 7. Adopt Mode
- ## Summary Checklist
+ Two different things share the word "adopt". `/wp-create`'s Adopt Mode **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.
- - [ ] 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
+ `/wp-create` enters Adopt Mode when the target path already holds a `wp-config.php`. The
+ procedure — what it skips, what it reads from the existing install, the multisite refusal —
+ is `commands/wp-create.md`'s "Adopt Mode" section; follow it there.