wp-cli-extending
Add custom WP-CLI commands to a WordPress plugin via `WP_CLI::add_command( $name, $callable, $args )`. Covers the class-based command pattern with PHPDoc-driven synopsis, positional vs `--flag` args, I/O helpers (`success` / `log` / `warning` / `error` / `confirm` / `debug`), for
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/wordpress/wp-cli-extending
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
WP-CLI: Extending with Custom Commands
WP-CLI is the maintenance / automation interface to a WordPress install. Almost no plugin ships CLI commands — even though wp myplugin import, wp myplugin clear-cache, wp myplugin run-sync are exactly what ops people want. The API is WP_CLI::add_command() and a class with PHPDoc-annotated methods.
When to use this skill
Trigger when ANY of the following is true:
- A plugin needs a CLI surface — bulk import / export, data migration, queue dispatch, debug introspection, scheduled-job force-run, cache clear, license activate.
- Code references
WP_CLI::add_command,WP_CLI::log,WP_CLI::success,WP_CLI::warning,WP_CLI::error,WP_CLI::confirm,WP_CLI::runcommand,WP_CLI::add_hook,WP_CLI\Utils\format_items,WP_CLI\Utils\make_progress_bar,WP_CLI\Utils\get_flag_value. - The user has a long-running admin task (
update_optionloop over 50k rows, AJAX-timing-out import) and wants to run it from terminal. - Code is checking
if ( defined( 'WP_CLI' ) && WP_CLI )and the body is empty / wrong.
The bootstrap — guard, then register
// In the plugin's main file or a dedicated CLI bootstrap.
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'myplugin', MyPlugin\CLI\Commands::class );
}
The class is instantiated lazily when WP-CLI dispatches a myplugin ... invocation. PHPDoc on each method is what drives synopsis / help. Public methods become subcommands. Method names are used as-is, so import_licenses registers wp myplugin import_licenses unless you add @subcommand import-licenses. Use @subcommand for normal hyphenated command names.
Full worked example (import + list with progress bar, dry-run, format args) lives in reference.md. The skeleton:
namespace MyPlugin\CLI;
final class Commands {
/**
* One-line description.
*
* ## OPTIONS
*
* <file>
* : Positional, required.
*
* [--dry-run]
* : Optional flag.
*
* @subcommand import-licenses
* @when after_wp_load
*/
public function import_licenses( array $args, array $assoc_args ): void {
[ $file ] = $args;
$dry = (bool) \WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run', false );
// ... work, with \WP_CLI::log() / success() / error() ...
}
}
PHPDoc drives the synopsis — get the format right
WP-CLI parses the docblock for command help / arg validation. The strict format:
| Line | Means |
|---|---|
| First paragraph | One-line short description (shown in wp help myplugin) |
## OPTIONS |
Begins the args section |
<name> |
Positional arg, required |
[<name>] |
Positional arg, optional |
[<name>...] |
Variadic positional |
--flag |
Boolean flag |
[--flag] |
Optional boolean flag |
[--key=<value>] |
Associative arg |
[--key=<value>...] |
Repeatable associative arg |
: description |
The next line is the description for the arg above |
## EXAMPLES |
Begins examples block |
@when before_wp_load / after_wp_load |
Controls when WP boots relative to the command (default: after_wp_load) |
@subcommand name-with-hyphen |
Exposes a PHP method under a CLI-safe command name |
@alias |
Alternate name for the command |
The arg pattern lines are NOT freeform — they MUST match WP-CLI's parser. Run wp help myplugin import-licenses to see what the parser made of your docblock; if the command is missing, check the method name / @subcommand tag first. If the ## OPTIONS section looks wrong there, the docblock is malformed.
I/O helpers — pick the right severity
Verified at WP_CLI class methods (reflection-confirmed):
\WP_CLI::line( $msg ); // plain stdout; no log prefix
\WP_CLI::log( $msg ); // also plain stdout but suppressed by --quiet
\WP_CLI::success( $msg ); // green "Success: ..."
\WP_CLI::warning( $msg ); // yellow "Warning: ..."
\WP_CLI::error( $msg, true ); // red "Error: ..." AND exit non-zero (default)
\WP_CLI::error( $msg, false ); // print error but continue
\WP_CLI::debug( $msg ); // only printed with --debug flag
\WP_CLI::confirm( $question, $assoc_args ); // y/n prompt; auto-yes when $assoc_args contains --yes
\WP_CLI::halt( $code ); // exit with custom code
error exits by default — use it for "abort the command". warning does not exit.
Formatted output — match the --format convention
Every WP-CLI command that lists data accepts --format=<table|csv|json|yaml|count>. Build your commands the same way. WP_CLI\Utils\format_items( $format, $items, $fields ) does the work (signature verified via reflection).
$format = \WP_CLI\Utils\get_flag_value( $assoc_args, 'format', 'table' );
\WP_CLI\Utils\format_items( $format, $items, array( 'id', 'name', 'expires_at' ) );
Declare the format enum in the PHPDoc with the --- YAML-block syntax so WP-CLI validates user input — full example in reference.md:
* [--format=<format>]
* : Output format.
* ---
* default: table
* options:
* - table
* - csv
* - json
* - yaml
* - count
* ---
Invoking other commands — runcommand
Reflection-confirmed signature: WP_CLI::runcommand( $command, $options = [] ). Options:
| Option | Meaning |
|---|---|
launch |
true (default for after_wp_load boundary crossing): run in subprocess. false: run in current process (faster but shares state) |
return |
true: return the output as a string. 'stdout' / 'stderr' / 'return_code': return one piece. false (default): echo |
parse |
'json': JSON-decode the captured output |
exit_error |
false: don't exit on non-zero return from the inner command |
// Run another command and capture its JSON output.
$users = \WP_CLI::runcommand( 'user list --role=customer --format=json', array(
'return' => true,
'parse' => 'json',
) );
// Fire-and-forget; let it print to terminal.
\WP_CLI::runcommand( 'cache flush' );
Lifecycle hooks — WP_CLI::add_hook
Useful for command lifecycle logic. An ordinary active plugin is loaded during
WordPress bootstrap, so it cannot register code early enough to run before
WordPress loads; before_wp_load is for commands registered by WP-CLI packages
or an earlier bootstrap file.
| Hook | When |
|---|---|
before_wp_load |
Before any WP file is loaded |
after_wp_load |
After WP bootstrap, before commands run |
before_invoke:<command> |
Right before <command>; parent command hooks also fire for subcommands |
after_invoke:<command> |
Right after <command> |
before_run_command |
Before every command, after dispatch |
after_run_command |
After every command |
WP_CLI::add_hook( 'before_invoke:myplugin import-licenses', static function () {
\WP_CLI::log( 'Disabling Action Scheduler runners for the import...' );
remove_action( 'action_scheduler_run_queue', 'ActionScheduler::run_queue' );
} );
Long-running commands and progress bars
WP_CLI\Utils\make_progress_bar( $message, $count, $interval = 100 ) returns a progress bar (\cli\progress\Bar from the wp-cli/php-cli-tools lib). Call ->tick() (with optional increment) per item, ->finish() at the end.
$bar = \WP_CLI\Utils\make_progress_bar( 'Migrating', $total );
foreach ( $rows as $row ) {
migrate_one( $row );
$bar->tick();
}
$bar->finish();
For commands that take >30 seconds, flush WP object cache between batches and wp_get_db_schema()-style commands — the persistent $wpdb accumulates query log + cached results. Common pattern:
foreach ( array_chunk( $ids, 500 ) as $chunk ) {
process_chunk( $chunk );
\WP_CLI\Utils\wp_clear_object_cache(); // utility function, reflection-confirmed
}
Critical rules
- Always guard registration with
defined( 'WP_CLI' ) && WP_CLI. Otherwise theWP_CLI::add_commandcall fatals on every web request — the class doesn't exist outside CLI runs. - Method visibility matters. Only
publicmethods are exposed as subcommands.protected/privatehelpers don't leak — use them freely. - Underscores are not automatically converted to hyphens. A method named
import_licensesregisters asimport_licenses; add@subcommand import-licenseswhen the CLI command should be hyphenated. - Register a class name, not an already-instantiated object, when you want lazy loading. With a class string, WP-CLI reflects PHPDoc during registration/help and only constructs the class when the command is invoked. Constructors should still stay cheap because every real command run pays for them.
@when after_wp_loadis the default and the normal plugin mode. A command declared by an active plugin is registered only while WP is loading, too late for a realbefore_wp_loadcommand. Put WP-independent early commands in a WP-CLI package/bootstrap loaded before core.WP_CLI::error()exits with non-zero by default. Passfalseas the 2nd arg only when you genuinely want to print "Error:" but continue.- Format your output via
WP_CLI\Utils\format_items(), not byecho. Users expect--format=jsonto work; rolling your own table breaks pipelines and scripts. - Don't bypass
WP_CLI::logwith rawecho.echodoesn't respect--quiet; logs always do. - Pass
$assoc_argstoWP_CLI::confirm()when you want--yessupport. Callingconfirm( $question )prompts even if the user supplied--yes. - Don't query the database from
before_wp_load.$wpdbis not initialized yet. Useafter_wp_load(the default) for anything touching WP state. - Don't
wp_die()inside a CLI command — it bypasses WP-CLI's error formatting and produces ugly stack traces. UseWP_CLI::error().
Common AI mistakes
See reference.md for before/after snippets covering: missing defined('WP_CLI') guard, hand-rolled synopsis logic instead of PHPDoc, raw echo instead of WP_CLI::log/success/error, hand-built tables that break --format=json, and wp_die() inside a CLI command.
Cross-references
- See
wp-plugin-cronwhen the CLI command's job is to dispatch background work — Action Scheduler / cron is the right destination, not "do it all in the CLI invocation". - See
wp-rest-apiwhen the same logic also needs a REST surface — extract the core into a service class, expose via both CLI command and REST route. - See
wp-locale-and-datesforwp_date()output in command tables — locale-aware display matters in--format=table.
What this skill does NOT cover
- Distributing the command as a standalone WP-CLI package (
composer.json,wp package install). Plugin-shipped commands cover 95% of plugin needs. - Writing your own custom output formatter (subclassing
WP_CLI\Formatter). Almost never needed —format_itemscovers everything. - The package authoring conventions (
wp-cli/dotenv-commandstyle). Out of scope.
References
WP_CLI::add_command()— reflection-verified signatureadd_command( $name, $callable, $args = [] ).WP_CLI\Utils\format_items()— reflection-verifiedformat_items( $format, $items, $fields ).WP_CLI\Utils\make_progress_bar()— reflection-verifiedmake_progress_bar( $message, $count, $interval = 100 ).WP_CLI\Utils\get_flag_value()—get_flag_value( $assoc_args, $flag, $default = null ).- WP-CLI handbook (canonical docs): https://make.wordpress.org/cli/handbook/references/internal-api/
- WP-CLI hook system: https://make.wordpress.org/cli/handbook/guides/hook-system/
- Official documentation: https://make.wordpress.org/cli/handbook/references/internal-api/wp-cli-add-command/
- Official documentation: https://make.wordpress.org/cli/handbook/guides/commands-cookbook/
Files (wp-agent-skills)
-
reference.md 4.3 KB
# wp-cli-extending — Reference: full examples Long-form examples kept out of `SKILL.md` for brevity. The main skill is the contract + critical rules; this file is the worked code. ## Full command class — import + list with all the right idioms ```php namespace MyPlugin\CLI; final class Commands { /** * Imports licenses from a CSV file. * * ## OPTIONS * * <file> * : Path to CSV file. * * [--dry-run] * : Parse and report counts without writing. * * [--batch-size=<n>] * : Rows per chunk. Default: 100. * * ## EXAMPLES * * wp myplugin import-licenses ./batch.csv --batch-size=500 * wp myplugin import-licenses ./batch.csv --dry-run * * @subcommand import-licenses * @when after_wp_load */ public function import_licenses( array $args, array $assoc_args ): void { [ $file ] = $args; $dry_run = (bool) \WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run', false ); $batch_size = (int) \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ); if ( ! is_readable( $file ) ) { \WP_CLI::error( "Cannot read file: $file" ); } $rows = \MyPlugin\License\Importer::parse( $file ); $total = count( $rows ); \WP_CLI::log( sprintf( 'Parsed %d rows.', $total ) ); if ( $dry_run ) { \WP_CLI::success( 'Dry run complete. No changes written.' ); return; } $progress = \WP_CLI\Utils\make_progress_bar( 'Importing', $total ); foreach ( array_chunk( $rows, $batch_size ) as $chunk ) { \MyPlugin\License\Importer::write_chunk( $chunk ); $progress->tick( count( $chunk ) ); } $progress->finish(); \WP_CLI::success( sprintf( 'Imported %d licenses.', $total ) ); } /** * Lists active licenses. * * ## OPTIONS * * [--format=<format>] * : Output format. * --- * default: table * options: * - table * - csv * - json * - yaml * - count * --- * * [--fields=<fields>] * : Limit output to specific fields (comma-separated). * * @subcommand list-licenses */ public function list_licenses( array $args, array $assoc_args ): void { $items = \MyPlugin\License\Repo::all_active(); $fields = ! empty( $assoc_args['fields'] ) ? array_map( 'trim', explode( ',', $assoc_args['fields'] ) ) : array( 'id', 'key', 'product', 'expires_at' ); $format = \WP_CLI\Utils\get_flag_value( $assoc_args, 'format', 'table' ); \WP_CLI\Utils\format_items( $format, $items, $fields ); } } ``` ## Common before/after AI mistakes ```php // WRONG — fatals on every web request (WP_CLI class doesn't exist there) WP_CLI::add_command( 'myplugin', MyClass::class ); // RIGHT — guard if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'myplugin', MyClass::class ); } ``` ```php // WRONG — invents synopsis in code; WP-CLI parser ignores it, help is wrong public function import( $args, $assoc_args ) { if ( empty( $assoc_args['file'] ) ) { echo "Usage: wp myplugin import --file=<path>\n"; return; } } // RIGHT — declare in PHPDoc; WP-CLI shows usage and validates /** * ## OPTIONS * * --file=<path> * : The CSV file. */ public function import( $args, $assoc_args ) { /* ... */ } ``` ```php // WRONG — raw echo doesn't respect --quiet, gets no colour, no exit code echo "Done.\n"; echo "ERROR: something went wrong\n"; // RIGHT — use WP-CLI helpers \WP_CLI::success( 'Done.' ); \WP_CLI::error( 'Something went wrong.' ); // also exits non-zero ``` ```php // WRONG — hand-built table; --format=json now broken for scripts foreach ( $items as $row ) { printf( "%-10s %s\n", $row['id'], $row['name'] ); } // RIGHT — let WP-CLI format it; users get table/csv/json/yaml for free \WP_CLI\Utils\format_items( \WP_CLI\Utils\get_flag_value( $assoc_args, 'format', 'table' ), $items, array( 'id', 'name' ) ); ``` ```php // WRONG — wp_die in a CLI command produces an HTML / fatal-looking output if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'Not allowed.' ); } // RIGHT — WP-CLI error if ( ! current_user_can( 'manage_options' ) ) { \WP_CLI::error( 'Not allowed.' ); } ``` -
SKILL.md 12.6 KB
--- name: wp-cli-extending description: Add custom WP-CLI commands to a WordPress plugin via `WP_CLI::add_command( $name, $callable, $args )`. Covers the class-based command pattern with PHPDoc-driven synopsis, positional vs `--flag` args, I/O helpers (`success` / `log` / `warning` / `error` / `confirm` / `debug`), formatted output via `WP_CLI\Utils\format_items()` + `--format=table|csv|json|yaml|count`, progress bars with `WP_CLI\Utils\make_progress_bar()`, `WP_CLI::runcommand()` for invoking other commands, lifecycle hooks (`before_wp_load`, `before_invoke:{cmd}`, `after_invoke:{cmd}`), and the `defined( 'WP_CLI' ) && WP_CLI` registration guard. Use for plugin bulk import, data migration, queue dispatch, debug introspection, or any CLI surface. metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "wordpress" wp-skills-plugin-version-tested: "7.1" wp-skills-wp-version-tested: "7.1" wp-skills-toolchain-tested: "WP-CLI 2.10 - 2.11" wp-skills-php-min: "7.4" wp-skills-last-updated: "2026-08-20" --- # WP-CLI: Extending with Custom Commands WP-CLI is the maintenance / automation interface to a WordPress install. Almost no plugin ships CLI commands — even though `wp myplugin import`, `wp myplugin clear-cache`, `wp myplugin run-sync` are exactly what ops people want. The API is `WP_CLI::add_command()` and a class with PHPDoc-annotated methods. ## When to use this skill Trigger when ANY of the following is true: - A plugin needs a CLI surface — bulk import / export, data migration, queue dispatch, debug introspection, scheduled-job force-run, cache clear, license activate. - Code references `WP_CLI::add_command`, `WP_CLI::log`, `WP_CLI::success`, `WP_CLI::warning`, `WP_CLI::error`, `WP_CLI::confirm`, `WP_CLI::runcommand`, `WP_CLI::add_hook`, `WP_CLI\Utils\format_items`, `WP_CLI\Utils\make_progress_bar`, `WP_CLI\Utils\get_flag_value`. - The user has a long-running admin task (`update_option` loop over 50k rows, AJAX-timing-out import) and wants to run it from terminal. - Code is checking `if ( defined( 'WP_CLI' ) && WP_CLI )` and the body is empty / wrong. ## The bootstrap — guard, then register ```php // In the plugin's main file or a dedicated CLI bootstrap. if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'myplugin', MyPlugin\CLI\Commands::class ); } ``` The class is instantiated **lazily** when WP-CLI dispatches a `myplugin ...` invocation. PHPDoc on each method is what drives synopsis / help. Public methods become subcommands. Method names are used as-is, so `import_licenses` registers `wp myplugin import_licenses` unless you add `@subcommand import-licenses`. Use `@subcommand` for normal hyphenated command names. Full worked example (import + list with progress bar, dry-run, format args) lives in `reference.md`. The skeleton: ```php namespace MyPlugin\CLI; final class Commands { /** * One-line description. * * ## OPTIONS * * <file> * : Positional, required. * * [--dry-run] * : Optional flag. * * @subcommand import-licenses * @when after_wp_load */ public function import_licenses( array $args, array $assoc_args ): void { [ $file ] = $args; $dry = (bool) \WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run', false ); // ... work, with \WP_CLI::log() / success() / error() ... } } ``` ## PHPDoc drives the synopsis — get the format right WP-CLI parses the docblock for command help / arg validation. The strict format: | Line | Means | |---|---| | First paragraph | One-line short description (shown in `wp help myplugin`) | | `## OPTIONS` | Begins the args section | | `<name>` | Positional arg, required | | `[<name>]` | Positional arg, optional | | `[<name>...]` | Variadic positional | | `--flag` | Boolean flag | | `[--flag]` | Optional boolean flag | | `[--key=<value>]` | Associative arg | | `[--key=<value>...]` | Repeatable associative arg | | `: description` | The next line is the description for the arg above | | `## EXAMPLES` | Begins examples block | | `@when before_wp_load` / `after_wp_load` | Controls when WP boots relative to the command (default: `after_wp_load`) | | `@subcommand name-with-hyphen` | Exposes a PHP method under a CLI-safe command name | | `@alias` | Alternate name for the command | The arg pattern lines are NOT freeform — they MUST match WP-CLI's parser. Run `wp help myplugin import-licenses` to see what the parser made of your docblock; if the command is missing, check the method name / `@subcommand` tag first. If the `## OPTIONS` section looks wrong there, the docblock is malformed. ## I/O helpers — pick the right severity Verified at `WP_CLI` class methods (reflection-confirmed): ```php \WP_CLI::line( $msg ); // plain stdout; no log prefix \WP_CLI::log( $msg ); // also plain stdout but suppressed by --quiet \WP_CLI::success( $msg ); // green "Success: ..." \WP_CLI::warning( $msg ); // yellow "Warning: ..." \WP_CLI::error( $msg, true ); // red "Error: ..." AND exit non-zero (default) \WP_CLI::error( $msg, false ); // print error but continue \WP_CLI::debug( $msg ); // only printed with --debug flag \WP_CLI::confirm( $question, $assoc_args ); // y/n prompt; auto-yes when $assoc_args contains --yes \WP_CLI::halt( $code ); // exit with custom code ``` `error` exits by default — use it for "abort the command". `warning` does not exit. ## Formatted output — match the `--format` convention Every WP-CLI command that lists data accepts `--format=<table|csv|json|yaml|count>`. Build your commands the same way. `WP_CLI\Utils\format_items( $format, $items, $fields )` does the work (signature verified via reflection). ```php $format = \WP_CLI\Utils\get_flag_value( $assoc_args, 'format', 'table' ); \WP_CLI\Utils\format_items( $format, $items, array( 'id', 'name', 'expires_at' ) ); ``` Declare the format enum in the PHPDoc with the `---` YAML-block syntax so WP-CLI validates user input — full example in `reference.md`: ``` * [--format=<format>] * : Output format. * --- * default: table * options: * - table * - csv * - json * - yaml * - count * --- ``` ## Invoking other commands — `runcommand` Reflection-confirmed signature: `WP_CLI::runcommand( $command, $options = [] )`. Options: | Option | Meaning | |---|---| | `launch` | `true` (default for `after_wp_load` boundary crossing): run in subprocess. `false`: run in current process (faster but shares state) | | `return` | `true`: return the output as a string. `'stdout'` / `'stderr'` / `'return_code'`: return one piece. `false` (default): echo | | `parse` | `'json'`: JSON-decode the captured output | | `exit_error` | `false`: don't exit on non-zero return from the inner command | ```php // Run another command and capture its JSON output. $users = \WP_CLI::runcommand( 'user list --role=customer --format=json', array( 'return' => true, 'parse' => 'json', ) ); // Fire-and-forget; let it print to terminal. \WP_CLI::runcommand( 'cache flush' ); ``` ## Lifecycle hooks — `WP_CLI::add_hook` Useful for command lifecycle logic. An ordinary active plugin is loaded during WordPress bootstrap, so it cannot register code early enough to run before WordPress loads; `before_wp_load` is for commands registered by WP-CLI packages or an earlier bootstrap file. | Hook | When | |---|---| | `before_wp_load` | Before any WP file is loaded | | `after_wp_load` | After WP bootstrap, before commands run | | `before_invoke:<command>` | Right before `<command>`; parent command hooks also fire for subcommands | | `after_invoke:<command>` | Right after `<command>` | | `before_run_command` | Before every command, after dispatch | | `after_run_command` | After every command | ```php WP_CLI::add_hook( 'before_invoke:myplugin import-licenses', static function () { \WP_CLI::log( 'Disabling Action Scheduler runners for the import...' ); remove_action( 'action_scheduler_run_queue', 'ActionScheduler::run_queue' ); } ); ``` ## Long-running commands and progress bars `WP_CLI\Utils\make_progress_bar( $message, $count, $interval = 100 )` returns a progress bar (`\cli\progress\Bar` from the `wp-cli/php-cli-tools` lib). Call `->tick()` (with optional increment) per item, `->finish()` at the end. ```php $bar = \WP_CLI\Utils\make_progress_bar( 'Migrating', $total ); foreach ( $rows as $row ) { migrate_one( $row ); $bar->tick(); } $bar->finish(); ``` For commands that take >30 seconds, flush WP object cache between batches and `wp_get_db_schema()`-style commands — the persistent `$wpdb` accumulates query log + cached results. Common pattern: ```php foreach ( array_chunk( $ids, 500 ) as $chunk ) { process_chunk( $chunk ); \WP_CLI\Utils\wp_clear_object_cache(); // utility function, reflection-confirmed } ``` ## Critical rules - **Always guard registration with `defined( 'WP_CLI' ) && WP_CLI`**. Otherwise the `WP_CLI::add_command` call fatals on every web request — the class doesn't exist outside CLI runs. - **Method visibility matters**. Only `public` methods are exposed as subcommands. `protected` / `private` helpers don't leak — use them freely. - **Underscores are not automatically converted to hyphens**. A method named `import_licenses` registers as `import_licenses`; add `@subcommand import-licenses` when the CLI command should be hyphenated. - **Register a class name, not an already-instantiated object, when you want lazy loading**. With a class string, WP-CLI reflects PHPDoc during registration/help and only constructs the class when the command is invoked. Constructors should still stay cheap because every real command run pays for them. - **`@when after_wp_load` is the default and the normal plugin mode**. A command declared by an active plugin is registered only while WP is loading, too late for a real `before_wp_load` command. Put WP-independent early commands in a WP-CLI package/bootstrap loaded before core. - **`WP_CLI::error()` exits with non-zero by default**. Pass `false` as the 2nd arg only when you genuinely want to print "Error:" but continue. - **Format your output via `WP_CLI\Utils\format_items()`, not by `echo`**. Users expect `--format=json` to work; rolling your own table breaks pipelines and scripts. - **Don't bypass `WP_CLI::log` with raw `echo`**. `echo` doesn't respect `--quiet`; logs always do. - **Pass `$assoc_args` to `WP_CLI::confirm()` when you want `--yes` support**. Calling `confirm( $question )` prompts even if the user supplied `--yes`. - **Don't query the database from `before_wp_load`**. `$wpdb` is not initialized yet. Use `after_wp_load` (the default) for anything touching WP state. - **Don't `wp_die()` inside a CLI command** — it bypasses WP-CLI's error formatting and produces ugly stack traces. Use `WP_CLI::error()`. ## Common AI mistakes See `reference.md` for before/after snippets covering: missing `defined('WP_CLI')` guard, hand-rolled synopsis logic instead of PHPDoc, raw `echo` instead of `WP_CLI::log/success/error`, hand-built tables that break `--format=json`, and `wp_die()` inside a CLI command. ## Cross-references - See **`wp-plugin-cron`** when the CLI command's job is to dispatch background work — Action Scheduler / cron is the right destination, not "do it all in the CLI invocation". - See **`wp-rest-api`** when the same logic also needs a REST surface — extract the core into a service class, expose via both CLI command and REST route. - See **`wp-locale-and-dates`** for `wp_date()` output in command tables — locale-aware display matters in `--format=table`. ## What this skill does NOT cover - Distributing the command as a standalone WP-CLI package (`composer.json`, `wp package install`). Plugin-shipped commands cover 95% of plugin needs. - Writing your own custom output formatter (subclassing `WP_CLI\Formatter`). Almost never needed — `format_items` covers everything. - The package authoring conventions (`wp-cli/dotenv-command` style). Out of scope. ## References - `WP_CLI::add_command()` — reflection-verified signature `add_command( $name, $callable, $args = [] )`. - `WP_CLI\Utils\format_items()` — reflection-verified `format_items( $format, $items, $fields )`. - `WP_CLI\Utils\make_progress_bar()` — reflection-verified `make_progress_bar( $message, $count, $interval = 100 )`. - `WP_CLI\Utils\get_flag_value()` — `get_flag_value( $assoc_args, $flag, $default = null )`. - WP-CLI handbook (canonical docs): https://make.wordpress.org/cli/handbook/references/internal-api/ - WP-CLI hook system: https://make.wordpress.org/cli/handbook/guides/hook-system/ - Official documentation: <https://make.wordpress.org/cli/handbook/references/internal-api/wp-cli-add-command/> - Official documentation: <https://make.wordpress.org/cli/handbook/guides/commands-cookbook/>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.