wp-cli/package-command
Lists, installs, and removes WP-CLI packages, community-maintained projects that extend WP-CLI. Commands include wp package list, wp package install (via Composer), wp package uninstall, and wp package browse to explore available packages.
wp-cli/package-command is not something you install — it ships inside WP-CLI itself and is the wp package namespace. From a Laravel developer's point of view, think of it as the composer subcommand for a second, WP-CLI-owned Composer project that lives outside every one of your application repos.
Minimal path from zero to first value:
# Confirm it's there (bundled with every modern WP-CLI)
wp help package
# What's already installed, and where does it live?
wp package list
wp package path # ~/.wp-cli/packages/
Three places to look first:
wp package list --format=json — the authoritative inventory of WP-CLI extensions on the machine. This is your composer show.~/.wp-cli/packages/composer.json — the real manifest. Every wp package install mutates this file directly. There is no composer.json in your Laravel app that knows about any of this.browse, get, install, is-installed, list, path, update, uninstall).First use case: add a command you actually reach for, then verify it loaded.
wp package install wp-cli/server-command
wp server --help # new command now exists
wp package get wp-cli/server-command --fields=version --format=json
If you're provisioning WordPress sites from a Laravel-side deployment pipeline (Forge, Ploi, Envoyer, or a hand-rolled deployer), the first genuinely useful move is to make package installation part of that provisioning rather than something someone did by hand on a VM six months ago.
Idempotent provisioning. is-installed returns exit code 0/1 and prints nothing — script on the code, not stdout:
for pkg in wp-cli/server-command wp-cli/scaffold-package-command runcommand/hook; do
if ! wp package is-installed "$pkg"; then
wp package install "$pkg" --no-interaction
fi
done
Pinning is Composer syntax, smuggled through the package name. Note the colon:
wp package install wp-cli/server-command:2.0.0 # exact
wp package install wp-cli/server-command:^1.0 # caret
wp package install wp-cli/server-command:~1.2 # tilde
wp package install wp-cli/server-command:@stable # explicit stable
wp package install wp-cli/server-command:dev-main # branch
Treat dev-main as a red flag in anything shared — the README's own list example shows a package stuck on dev-main with an available 2.x-dev, which is exactly the drift you'll accumulate if you don't pin.
Per-project isolation via WP_CLI_PACKAGES_DIR. This is the single most valuable pattern for a Laravel dev, because by default packages are per-Unix-user and therefore shared across every WordPress site on the box. Override it and you get something lockfile-like:
export WP_CLI_PACKAGES_DIR="$(pwd)/.wp-cli-packages"
wp package install wp-cli/server-command --no-interaction
Now .wp-cli-packages/composer.json (and its lock file) is committable, diffable, and reviewable — the same discipline you apply to composer.lock.
Docker: install at build time, run as the runtime user. Installing during the image build gives you layer caching; installing as root and running as www-data gives you permission errors. Set HOME/WP_CLI_PACKAGES_DIR explicitly to a shared path and install as that user.
CI (GitHub Actions shape). Cache the packages dir keyed on the manifest, and skip update checks — they hit the GitHub API:
- uses: actions/cache@v4
with:
path: ~/.wp-cli/packages
key: wp-cli-packages-${{ hashFiles('.wp-cli-packages/composer.json') }}
- run: |
wp package install wp-cli/server-command:^2.0 --no-interaction
wp package list --skip-update-check --format=json
Machine-readable drift detection. Because every listing command supports --format=json|yaml|csv|table|ids and --fields, you can compare environments the way you'd diff composer show:
wp package list --skip-update-check --fields=name,version --format=json \
| jq -S . > /tmp/staging.json
Diff staging against production on every deploy; anything appearing there that isn't in your provisioning script is a hotfix someone forgot to commit.
Local development loop for your own commands. Install by path while iterating — WP-CLI registers a reference, so edits take effect without reinstalling:
wp package install /path/to/my-command --no-interaction
wp package path my-command # cd straight into it
# ... edit, then:
wp package uninstall my-command --no-interaction
This is the fastest way to iterate on a custom command, and wp scaffold package (from the sibling scaffold package) generates the Behat/PHPUnit skeleton. When you're ready to distribute, the same command accepts a git URL, a directory, or a .zip — so an internal, unlisted command package costs you nothing in Packagist setup:
wp package install https://gitlab.com/foo/wp-cli-bar-command.git
wp package install google-sitemap-generator-cli.zip # extracts to local/<package-name>
Call it from Laravel when you must. Shelling out via Symfony\Component\Process\Process with ['wp','package','list','--format=json','--skip-update-check'] is fine for a health-check command or a nightly job; don't shell out on a request path.
~/.wp-cli/packages/. A package upgraded for project A silently changes project B. WP_CLI_PACKAGES_DIR or one container per project is the fix, not discipline.wp invocation. A heavy or broken package slows down all commands and can fatal before wp package uninstall ever runs. Recovery: edit ~/.wp-cli/packages/composer.json by hand, or delete the vendor entry, then run composer install inside that directory.--skip-update-check on list and get; use --fields=version when you only need one value.--insecure is a mitigation, not a default. It retries downloads with TLS validation off after a handshake failure. If you're behind a corporate proxy, fix the CA bundle instead of shipping --insecure into a pipeline.--interaction flag is newer than the rest of the surface. On older bundled versions only the global --no-interaction exists. For scripts, wp package install foo --no-interaction is safe across versions; --interaction alone is not.is-installed communicates via exit code. Comparing stdout to a string will fail silently. if wp package is-installed foo; then ... is the only correct form.wp package uninstall wants the Composer package name, not the zip filename or the extracted directory name. local/<package-name> is where zips land, and the two can disagree.wp package browse is dead weight. The README states plainly that the Package Index is deprecated and frozen. Don't build discovery tooling or docs links on it; use Packagist/GitHub search.wp package update with no arguments updates everything, including package-command if it was installed over the bundled version. That's usually fine, but it means a bad update can remove your ability to roll back via the same command — keep wp package install [email protected]:wp-cli/package-command.git handy.composer config -g github-oauth.github.com <TOKEN> writes to ~/.composer/auth.json. Convenient for private repos, risky to bake into a base image.wp --info before blaming the package.-vvv to wp package install and read the raw Composer output — WP-CLI is mostly relaying it. When the solver fails, going straight into the packages directory and running composer require <pkg> gives you the full, unfiltered error.WP_CLI_CS standard, PHPStan, and a Behat scenario per behavior change. The efficient loop is composer phpcs -- <file>, composer phpstan, then composer behat -- features/<name>.feature:<line> for one scenario — and composer behat-rerun after a failure. Set NO_COLOR=1 and WP_CLI_TEST_QUIET=1 first so output is readable. Note the README is generated via wp scaffold package-readme, so user-facing doc edits belong in the docblock, not the markdown.How can I help you explore Laravel packages today?