WP-CLI-first principle — use WP-CLI instead of generating PHP code whenever possible, with command reference, ACF seeding patterns, and environment-aware execution
Scanned 5/27/2026
Install via CLI
openskills install yojahny55/claude-wp-builder---
name: wp-cli-patterns
description: WP-CLI-first principle — use WP-CLI instead of generating PHP code whenever possible, with command reference, ACF seeding patterns, and environment-aware execution
user-invocable: false
trigger: auto-invoke when .wp-create.json exists in project root
---
# WP-CLI Patterns — Best Practices for All Agents
This skill teaches the **WP-CLI-first principle**: use WP-CLI commands instead of generating PHP code whenever possible. WP-CLI saves tokens, reduces errors, and executes faster than writing throwaway PHP files.
---
## Core Rule: WP-CLI Over PHP Generation
**Always prefer a single WP-CLI command over generating PHP code.**
```
# BAD (costs tokens): Generate PHP file with update_option()
# GOOD (1 line): $WP option update my_option 'value'
# BAD: Generate PHP with wp_insert_post()
# GOOD: $WP post create --post_type=page --post_title='About' --post_status=publish
# BAD: Generate PHP with wp_create_nav_menu()
# GOOD: $WP menu create "Primary EN" && $WP menu item add-post primary-en 5
# BAD: Generate PHP to activate a plugin
# GOOD: $WP plugin activate secure-custom-fields
# BAD: Generate PHP to set permalink structure
# GOOD: $WP rewrite structure '/%postname%/'
```
Use `wp eval` only when no dedicated WP-CLI subcommand exists for the operation (e.g., calling ACF's `update_field()` API).
---
## The `$WP` Convention
Throughout all commands, agents, and skills, **`$WP`** is shorthand for the value of `wp_cli.wrapper` from `.wp-create.json`. Agents read this value and substitute it into all WP-CLI commands.
For example, if the environment is Docker:
```bash
# $WP expands to:
docker exec my-project-wp wp --allow-root
# So this command:
$WP option update blogname "My Site"
# Becomes:
docker exec my-project-wp wp --allow-root option update blogname "My Site"
```
**How to read `$WP`:** Parse `.wp-create.json` at the project root and extract the `wp_cli.wrapper` value. Every WP-CLI command in this skill assumes `$WP` is set to that value.
---
## Environment-Aware Execution
Agents read `wp_cli.wrapper` from `.wp-create.json` and prepend it to all WP-CLI commands. The wrapper value depends on the environment type:
| Environment | `wp_cli.wrapper` value | Notes |
|-------------|----------------------|-------|
| Native | `wp --path=/var/www/html/my-project` | Direct CLI, requires WP-CLI installed on host |
| Docker | `docker exec my-project-wp wp --allow-root` | Executes inside WordPress container |
| DDEV | `ddev wp` | DDEV proxies to the web container |
| Lando | `lando wp` | Lando proxies to the appserver container |
| wp-env | `npx wp-env run cli wp` | wp-env proxies to its CLI container |
**Important:** Never hardcode the execution method. Always read the wrapper from the manifest so commands work across all environments.
---
## Environment Detection: Reading `.wp-create.json`
The `.wp-create.json` manifest is generated by `/wp-create` at the project root. It is the single source of truth for all commands, agents, and skills.
```bash
# Check if manifest exists
if [ -f .wp-create.json ]; then
# Extract the WP-CLI wrapper
WP=$(jq -r '.wp_cli.wrapper' .wp-create.json)
# Extract other useful values
PROJECT_SLUG=$(jq -r '.project.slug' .wp-create.json)
PRIMARY_LANG=$(jq -r '.languages.primary' .wp-create.json)
ADDITIONAL_LANGS=$(jq -r '.languages.additional[]' .wp-create.json)
ENV_TYPE=$(jq -r '.environment.type' .wp-create.json)
fi
```
When `.wp-create.json` does **not** exist, WP-CLI features are unavailable. Fall back to file-only operations (the pre-existing behavior).
---
## WP-CLI Command Reference
All 16 domains agents should know. Every command below is prefixed with `$WP` in practice.
| Domain | Commands |
|--------|----------|
| Database | `wp db create`, `wp db import`, `wp db export`, `wp db check`, `wp db query` |
| Content | `wp post create`, `wp post update`, `wp post delete`, `wp post meta update` |
| Media | `wp media import <url>`, `wp media regenerate` |
| Options | `wp option get`, `wp option update`, `wp option delete` |
| Menus | `wp menu create`, `wp menu item add-post`, `wp menu item add-custom`, `wp menu location assign` |
| Plugins | `wp plugin install`, `wp plugin activate`, `wp plugin deactivate`, `wp plugin list` |
| Theme | `wp theme activate`, `wp theme list` |
| Config | `wp config set`, `wp config get`, `wp config list` |
| Rewrite | `wp rewrite structure`, `wp rewrite flush` |
| Cache | `wp cache flush`, `wp transient delete --all` |
| Cron | `wp cron event list`, `wp cron event run` |
| Search | `wp search-replace 'old' 'new'` |
| Scaffold | `wp scaffold child-theme`, `wp scaffold plugin` |
| Export/Import | `wp export`, `wp import` |
| User | `wp user create`, `wp user update` |
| Eval | `wp eval 'php_code();'` |
### Useful Flags
- `--porcelain` — return only the ID (useful for capturing post/attachment IDs)
- `--format=json` — machine-readable output for parsing
- `--format=table` — human-readable output for display
- `--allow-root` — required inside Docker containers running as root
- `--force` — skip confirmation prompts (e.g., `wp post delete 1 --force`)
---
## ACF Field Seeding Patterns
### Preferred: `update_field()` via `wp eval`
Use ACF's own API for field operations. This is storage-format-agnostic and handles field key registration, serialization, and caching correctly.
```bash
# Simple field on options page
$WP eval "update_field('hero_title', 'Building Digital Excellence', 'option');"
# Image field (import first, use attachment ID)
ID=$($WP media import 'https://images.unsplash.com/photo-xxx' --title='Hero Background' --porcelain)
$WP eval "update_field('hero_image', $ID, 'option');"
# Repeater field
$WP eval "
\$rows = array(
array('title' => 'Web Design', 'description' => 'Custom websites...', 'icon' => 43),
array('title' => 'SEO', 'description' => 'Search optimization...', 'icon' => 44),
);
update_field('services_cards', \$rows, 'option');
"
# Page post meta (field on a specific page)
$WP eval "update_field('about_hero_title', 'Our Story', <post_id>);"
```
### Alternative: Direct `wp_options` for Bulk Operations
Faster for bulk seeding but coupled to ACF internals. Use only when ACF API is unavailable or for bulk performance.
ACF stores options page fields in `wp_options` with an `options_` prefix (e.g., field `hero_title` is stored as `options_hero_title`).
```bash
# Simple field
$WP option update options_hero_title "Building Digital Excellence"
$WP option update options_hero_image 42
# Repeater fields (indexed subfields + count)
$WP option update options_services_cards_0_title "Web Design"
$WP option update options_services_cards_0_icon 43
$WP option update options_services_cards_1_title "SEO"
$WP option update options_services_cards_1_icon 44
$WP option update options_services_cards 2 # total row count
```
### After Seeding: Always Flush Cache
```bash
$WP cache flush
```
---
## Bilingual Naming Convention
This convention matches the i18n helper system defined in the `wp-bilingual` skill.
- **Primary language fields use no suffix:** `hero_title`, `hero_description`, `cta_text`
- **Secondary language fields append `_<lang>`:** `hero_title_es`, `hero_description_es`, `cta_text_es`
### Seeding Bilingual Content
```bash
# Primary language (no suffix)
$WP eval "update_field('hero_title', 'Building Digital Excellence', 'option');"
# Secondary language (append _<lang>)
$WP eval "update_field('hero_title_es', 'Construyendo Excelencia Digital', 'option');"
$WP eval "update_field('hero_subtitle_es', 'Creamos sitios web que funcionan', 'option');"
# Bilingual repeater subfields
$WP eval "
\$rows = get_field('services_cards', 'option');
\$rows[0]['title_es'] = 'Diseno Web';
\$rows[1]['title_es'] = 'SEO';
update_field('services_cards', \$rows, 'option');
"
```
### Rules
- Non-translatable fields (images, URLs, numbers, booleans) do NOT get language variants
- Read `.wp-create.json` field `languages.additional` to know which suffixes to generate
- If `languages.additional` is empty, skip all `_<lang>` field operations
---
## Common Patterns
### Create Pages and Set Front Page
```bash
HOME_ID=$($WP post create --post_type=page --post_title='Home' --post_status=publish --porcelain)
ABOUT_ID=$($WP post create --post_type=page --post_title='About' --post_status=publish --porcelain)
$WP option update show_on_front 'page'
$WP option update page_on_front $HOME_ID
```
### Create and Assign Menus
```bash
$WP menu create "Primary EN"
$WP menu create "Primary ES"
$WP menu item add-post primary-en $HOME_ID --title="Home"
$WP menu item add-post primary-en $ABOUT_ID --title="About"
$WP menu item add-post primary-es $HOME_ID --title="Inicio"
$WP menu item add-post primary-es $ABOUT_ID --title="Acerca"
$WP menu location assign "Primary EN" primary_en
$WP menu location assign "Primary ES" primary_es
```
### Import Media and Use Attachment ID
```bash
ID=$($WP media import 'https://example.com/photo.jpg' --title='Hero Image' --porcelain)
$WP eval "update_field('hero_image', $ID, 'option');"
```
### Verify Operations
```bash
# Verify a field was seeded
$WP eval "echo get_field('hero_title', 'option') ? 'OK' : 'EMPTY';"
# Verify a page exists with correct template
$WP eval "echo get_page_template_slug($PAGE_ID);"
# Verify plugin is active
$WP plugin list --status=active --format=table
# Verify menus are assigned
$WP menu location list --format=table
```
### Final Cleanup After Seeding
```bash
$WP rewrite flush
$WP cache flush
```
No comments yet. Be the first to comment!