<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>wp-cli package Archives - WP-CLI Mastery</title>
	<atom:link href="https://wpclimastery.com/blog/tag/wp-cli-package/feed/" rel="self" type="application/rss+xml" />
	<link>https://wpclimastery.com/blog/tag/wp-cli-package/</link>
	<description>Automate WordPress Like a DevOps Pro.</description>
	<lastBuildDate>Mon, 24 Nov 2025 11:16:47 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://wpclimastery.com/wp-content/uploads/2025/11/cropped-favicon-32x32.webp</url>
	<title>wp-cli package Archives - WP-CLI Mastery</title>
	<link>https://wpclimastery.com/blog/tag/wp-cli-package/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>How to Create Custom WP-CLI Commands: Developer&#8217;s Guide</title>
		<link>https://wpclimastery.com/blog/how-to-create-custom-wp-cli-commands-developers-guide/</link>
		
		<dc:creator><![CDATA[Krasen]]></dc:creator>
		<pubDate>Tue, 25 Nov 2025 09:00:00 +0000</pubDate>
				<category><![CDATA[Advanced WP-CLI Techniques]]></category>
		<category><![CDATA[custom wp-cli commands]]></category>
		<category><![CDATA[extend wp-cli]]></category>
		<category><![CDATA[wordpress cli development]]></category>
		<category><![CDATA[wp-cli package]]></category>
		<category><![CDATA[wpcli development]]></category>
		<guid isPermaLink="false">https://wpclimastery.com/?p=13</guid>

					<description><![CDATA[<p>WP-CLI comes with hundreds of built-in commands for managing WordPress. But what if you need a command specific to your workflow, plugin, or client requirements? Creating custom WP-CLI commands lets...</p>
<p>The post <a href="https://wpclimastery.com/blog/how-to-create-custom-wp-cli-commands-developers-guide/">How to Create Custom WP-CLI Commands: Developer&#8217;s Guide</a> appeared first on <a href="https://wpclimastery.com">WP-CLI Mastery</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>WP-CLI comes with hundreds of built-in commands for managing WordPress. But what if you need a command specific to your workflow, plugin, or client requirements? Creating custom WP-CLI commands lets you extend the CLI with your own functionality.</p>



<p>In this advanced guide, you&#8217;ll learn to create production-ready custom WP-CLI commands from scratch. You&#8217;ll understand command structure, parameter handling, the WP_CLI API, and how to package your commands for distribution.</p>



<p>By the end, you&#8217;ll have the skills to build professional CLI tools that make WordPress management even more powerful.</p>



<h3 class="wp-block-heading" id="why-create-custom-wp-cli-commands-why-custom-commands">Why Create Custom WP-CLI Commands?</h3>



<h4 class="wp-block-heading" id="use-cases-for-custom-commands">Use Cases for Custom Commands</h4>



<p><strong>Plugin-Specific Operations</strong></p>



<ul class="wp-block-list">
<li>Flush custom caches</li>



<li>Run migrations</li>



<li>Import/export plugin data</li>



<li>Administrative tasks</li>
</ul>



<p><strong>Agency/Enterprise Workflows</strong></p>



<ul class="wp-block-list">
<li>Client onboarding automation</li>



<li>Standardized site audits</li>



<li>Custom deployment tasks</li>



<li>Compliance reporting</li>
</ul>



<p><strong>Development Tools</strong></p>



<ul class="wp-block-list">
<li>Generate boilerplate code</li>



<li>Database seeding</li>



<li>Testing helpers</li>



<li>Performance profiling</li>
</ul>



<h4 class="wp-block-heading" id="benefits-over-manual-scripts">Benefits Over Manual Scripts</h4>



<p><strong>Integration with WP-CLI Ecosystem</strong></p>



<ul class="wp-block-list">
<li>Automatic WordPress context loading</li>



<li>Access to all WordPress functions</li>



<li>Works with WP-CLI packages</li>



<li>Follows CLI conventions</li>
</ul>



<p><strong>Professional API</strong></p>



<ul class="wp-block-list">
<li><code>WP_CLI::success()</code>,&nbsp;<code>WP_CLI::error()</code>,&nbsp;<code>WP_CLI::warning()</code></li>



<li>Built-in progress bars</li>



<li>Table formatting</li>



<li>Color output</li>
</ul>



<p><strong>Distribution</strong></p>



<ul class="wp-block-list">
<li>Share via Composer packages</li>



<li>Install with&nbsp;<code>wp package install</code></li>



<li>Version management</li>
</ul>



<h3 class="wp-block-heading" id="prerequisites-prerequisites">Prerequisites</h3>



<p>Before creating custom commands, you should have:</p>



<ul class="wp-block-list">
<li><strong>WP-CLI installed</strong>&nbsp;&#8211;&nbsp;<a href="#">Installation guide</a></li>



<li><strong>PHP knowledge</strong>&nbsp;&#8211; Object-oriented PHP basics</li>



<li><strong>WordPress familiarity</strong>&nbsp;&#8211; Understanding of WordPress hooks and functions</li>



<li><strong>Bash fundamentals</strong>&nbsp;&#8211;&nbsp;<a href="#">Bash scripting guide</a></li>
</ul>



<h3 class="wp-block-heading" id="command-structure-basics-command-structure">Command Structure Basics</h3>



<h4 class="wp-block-heading" id="command-anatomy">Command Anatomy</h4>



<p>WP-CLI commands follow this structure:</p>



<pre class="wp-block-code"><code>wp &lt;command&gt; &lt;subcommand&gt; &lt;arguments&gt; --&lt;flags&gt;
</code></pre>



<p><strong>Examples:</strong></p>



<pre class="wp-block-code"><code>wp plugin install woocommerce
<em>#  ^^^^^^ ^^^^^^^ ^^^^^^^^^^^</em>
<em>#  cmd    subcmd  argument</em>

wp post list --post_type=page --format=table
<em>#  ^^^^ ^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^</em>
<em>#  cmd  sub  flag=value        flag=value</em>
</code></pre>



<h4 class="wp-block-heading" id="command-registration">Command Registration</h4>



<p>Commands are registered using&nbsp;<code>WP_CLI::add_command()</code>:</p>



<pre class="wp-block-code"><code>WP_CLI::add_command('hello', 'My_Hello_Command');
</code></pre>



<p>This creates:&nbsp;<code>wp hello</code></p>



<h4 class="wp-block-heading" id="class-based-vs-function-based">Class-Based vs Function-Based</h4>



<p><strong>Class-based (recommended):</strong></p>



<pre class="wp-block-code"><code>class My_Command {
		public function greet($args, $assoc_args) {
				WP_CLI::success("Hello!");
		}
}

WP_CLI::add_command('mycommand greet', 'My_Command');
</code></pre>



<p><strong>Function-based:</strong></p>



<pre class="wp-block-code"><code>function my_greet_command($args, $assoc_args) {
		WP_CLI::success("Hello!");
}

WP_CLI::add_command('greet', 'my_greet_command');
</code></pre>



<p><strong>Best practice</strong>: Use classes for better organization and multiple subcommands.</p>



<h3 class="wp-block-heading" id="your-first-custom-command-first-command">Your First Custom Commandnd}</h3>



<p>Let&#8217;s create a simple custom command step-by-step.</p>



<h4 class="wp-block-heading" id="step-1-create-plugin-file">Step 1: Create Plugin File</h4>



<p>Create a plugin to hold your command:</p>



<pre class="wp-block-code"><code>&lt;?php
<em>/**
 * Plugin Name: My WP-CLI Commands
 * Description: Custom WP-CLI commands
 * Version: 1.0.0
 */</em>

<em>// Don't execute in non-CLI context</em>
if (!defined('WP_CLI')) {
		return;
}

<em>/**
 * Hello World command
 */</em>
class Hello_Command {

		<em>/**
		 * Prints a greeting
		 *
		 * ## EXAMPLES
		 *
		 *     wp hello greet
		 *     wp hello greet --name=John
		 */</em>
		public function greet($args, $assoc_args) {
				$name = isset($assoc_args&#91;'name']) ? $assoc_args&#91;'name'] : 'World';

				WP_CLI::success("Hello, {$name}!");
		}
}

<em>// Register the command</em>
WP_CLI::add_command('hello', 'Hello_Command');
</code></pre>



<h4 class="wp-block-heading" id="step-2-install-plugin">Step 2: Install Plugin</h4>



<p>Save this as&nbsp;<code>wp-content/plugins/my-cli-commands/my-cli-commands.php</code></p>



<p>Activate it:</p>



<pre class="wp-block-code"><code>wp plugin activate my-cli-commands
</code></pre>



<h4 class="wp-block-heading" id="step-3-test-your-command">Step 3: Test Your Command</h4>



<pre class="wp-block-code"><code>wp hello greet
<em># Success: Hello, World!</em>

wp hello greet --name=John
<em># Success: Hello, John!</em>
</code></pre>



<p><strong>Congratulations!</strong>&nbsp;You&#8217;ve created your first custom WP-CLI command.</p>



<h4 class="wp-block-heading" id="step-4-view-help">Step 4: View Help</h4>



<p>WP-CLI automatically generates help documentation:</p>



<pre class="wp-block-code"><code>wp help hello greet
</code></pre>



<h3 class="wp-block-heading" id="using-the-wp_cli-class-wpcli-class">Using the WP_CLI Class</h3>



<p>The&nbsp;<code>WP_CLI</code>&nbsp;class provides powerful methods for CLI interaction.</p>



<h4 class="wp-block-heading" id="output-methods">Output Methods</h4>



<pre class="wp-block-code"><code><em>// Success message (green)</em>
WP_CLI::success("Operation completed!");

<em>// Error message (red) - exits script</em>
WP_CLI::error("Something went wrong!");

<em>// Warning message (yellow)</em>
WP_CLI::warning("Proceeding with caution...");

<em>// Info message (no color)</em>
WP_CLI::log("Processing item 5 of 10");

<em>// Debug message (only shown with --debug)</em>
WP_CLI::debug("Variable value: " . $value);
</code></pre>



<p><strong>Example:</strong></p>



<pre class="wp-block-code"><code>public function process() {
		WP_CLI::log("Starting processing...");

		if (!file_exists($file)) {
				WP_CLI::warning("File not found, using defaults");
		}

		if ($error) {
				WP_CLI::error("Processing failed!");  <em>// Exits here</em>
		}

		WP_CLI::success("Processing complete!");
}
</code></pre>



<h4 class="wp-block-heading" id="progress-bars">Progress Bars</h4>



<p>For long-running operations:</p>



<pre class="wp-block-code"><code>public function import_posts($args) {
		$posts = range(1, 1000);

		$progress = \WP_CLI\Utils\make_progress_bar('Importing posts', count($posts));

		foreach ($posts as $post) {
				<em>// Do import work</em>
				sleep(0.01);  <em>// Simulate work</em>

				$progress-&gt;tick();
		}

		$progress-&gt;finish();
		WP_CLI::success("Imported " . count($posts) . " posts");
}
</code></pre>



<p><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Importing posts  500/1000 &#91;==============&gt;              ] 50%
</code></pre>



<h4 class="wp-block-heading" id="confirmation-prompts">Confirmation Prompts</h4>



<p>Confirm destructive operations:</p>



<pre class="wp-block-code"><code>public function delete_all_posts() {
		WP_CLI::confirm("Are you sure you want to delete ALL posts?");

		<em>// User must type 'y' or 'yes' to continue</em>
		<em>// Otherwise, script exits</em>

		<em>// Proceed with deletion</em>
		WP_CLI::success("All posts deleted");
}
</code></pre>



<p><strong>Usage:</strong></p>



<pre class="wp-block-code"><code>wp mycommand delete-all-posts
<em># Are you sure you want to delete ALL posts? &#91;y/n]</em>
</code></pre>



<p><strong>Skip confirmation in scripts:</strong></p>



<pre class="wp-block-code"><code>wp mycommand delete-all-posts --yes
</code></pre>



<p>Learn more in the&nbsp;<a href="https://make.wordpress.org/cli/handbook/references/internal-api/">official WP_CLI class documentation</a>.</p>



<h3 class="wp-block-heading" id="parameter-handling-with-synopsis-parameters">Parameter Handling with Synopsis</h3>



<p>Synopsis defines your command&#8217;s parameters and flags.</p>



<h4 class="wp-block-heading" id="basic-synopsis">Basic Synopsis</h4>



<pre class="wp-block-code"><code><em>/**
 * Import data from file
 *
 * ## OPTIONS
 *
 * &lt;file&gt;
 * : Path to import file
 *
 * &#91;--format=&lt;format&gt;]
 * : Import format (json, csv)
 * ---
 * default: json
 * options:
 *   - json
 *   - csv
 * ---
 *
 * ## EXAMPLES
 *
 *     wp mycommand import data.json
 *     wp mycommand import data.csv --format=csv
 *
 * @when after_wp_load
 */</em>
public function import($args, $assoc_args) {
		list($file) = $args;  <em>// Required positional argument</em>

		$format = $assoc_args&#91;'format'];  <em>// Has default value</em>

		WP_CLI::log("Importing {$file} as {$format}");
}
</code></pre>



<h4 class="wp-block-heading" id="parameter-types">Parameter Types</h4>



<p><strong>Required positional argument:</strong></p>



<pre class="wp-block-code"><code><em>/**
 * &lt;file&gt;
 * : Path to file
 */</em>
</code></pre>



<p>Usage:&nbsp;<code>wp cmd import file.json</code></p>



<p><strong>Optional positional argument:</strong></p>



<pre class="wp-block-code"><code><em>/**
 * &#91;&lt;file&gt;]
 * : Path to file (optional)
 */</em>
</code></pre>



<p><strong>Required flag:</strong></p>



<pre class="wp-block-code"><code><em>/**
 * --user=&lt;id&gt;
 * : User ID (required)
 */</em>
</code></pre>



<p>Usage:&nbsp;<code>wp cmd process --user=1</code></p>



<p><strong>Optional flag with default:</strong></p>



<pre class="wp-block-code"><code><em>/**
 * &#91;--format=&lt;format&gt;]
 * : Output format
 * ---
 * default: table
 * options:
 *   - table
 *   - json
 *   - csv
 * ---
 */</em>
</code></pre>



<p><strong>Boolean flag:</strong></p>



<pre class="wp-block-code"><code><em>/**
 * &#91;--dry-run]
 * : Run without making changes
 */</em>
</code></pre>



<p>Usage:&nbsp;<code>wp cmd process --dry-run</code></p>



<p>Check in code:</p>



<pre class="wp-block-code"><code>$dry_run = isset($assoc_args&#91;'dry-run']) &amp;&amp; $assoc_args&#91;'dry-run'];
</code></pre>



<h4 class="wp-block-heading" id="accessing-parameters">Accessing Parameters</h4>



<pre class="wp-block-code"><code>public function command($args, $assoc_args) {
		<em>// Positional arguments (ordered)</em>
		$first_arg = isset($args&#91;0]) ? $args&#91;0] : '';
		$second_arg = isset($args&#91;1]) ? $args&#91;1] : '';

		<em>// Or use list()</em>
		list($file, $action) = $args;

		<em>// Associative arguments (flags)</em>
		$format = $assoc_args&#91;'format'];  <em>// No default, may not exist</em>
		$format = isset($assoc_args&#91;'format']) ? $assoc_args&#91;'format'] : 'table';

		<em>// Get with default helper</em>
		$format = \WP_CLI\Utils\get_flag_value($assoc_args, 'format', 'table');
}
</code></pre>



<h3 class="wp-block-heading" id="output-formatting-output-formatting">Output Formatting</h3>



<h4 class="wp-block-heading" id="table-format">Table Format</h4>



<p>Display data as tables:</p>



<pre class="wp-block-code"><code>public function list_users() {
		$users = get_users(array('number' =&gt; 10));

		$items = array();
		foreach ($users as $user) {
				$items&#91;] = array(
						'ID' =&gt; $user-&gt;ID,
						'Username' =&gt; $user-&gt;user_login,
						'Email' =&gt; $user-&gt;user_email,
						'Role' =&gt; implode(', ', $user-&gt;roles)
				);
		}

		\WP_CLI\Utils\format_items('table', $items, array('ID', 'Username', 'Email', 'Role'));
}
</code></pre>



<p><strong>Output:</strong></p>



<pre class="wp-block-code"><code>+----+----------+------------------+-------------+
| ID | Username | Email            | Role        |
+----+----------+------------------+-------------+
| 1  | admin    | admin@site.com   | administrator |
| 2  | editor   | editor@site.com  | editor      |
+----+----------+------------------+-------------+
</code></pre>



<h4 class="wp-block-heading" id="json-format">JSON Format</h4>



<pre class="wp-block-code"><code>\WP_CLI\Utils\format_items('json', $items, array('ID', 'Username', 'Email'));
</code></pre>



<p><strong>Output:</strong></p>



<pre class="wp-block-code"><code>&#91;
	{"ID":"1","Username":"admin","Email":"admin@site.com"},
	{"ID":"2","Username":"editor","Email":"editor@site.com"}
]
</code></pre>



<h4 class="wp-block-heading" id="csv-format">CSV Format</h4>



<pre class="wp-block-code"><code>\WP_CLI\Utils\format_items('csv', $items, array('ID', 'Username', 'Email'));
</code></pre>



<p><strong>Output:</strong></p>



<pre class="wp-block-code"><code>ID,Username,Email
1,admin,admin@site.com
2,editor,editor@site.com
</code></pre>



<h4 class="wp-block-heading" id="supporting-multiple-formats">Supporting Multiple Formats</h4>



<pre class="wp-block-code"><code><em>/**
 * &#91;--format=&lt;format&gt;]
 * : Output format
 * ---
 * default: table
 * options:
 *   - table
 *   - json
 *   - csv
 *   - count
 * ---
 */</em>
public function list($args, $assoc_args) {
		$format = \WP_CLI\Utils\get_flag_value($assoc_args, 'format', 'table');

		$items = $this-&gt;get_items();

		if ('count' === $format) {
				WP_CLI::log(count($items));
				return;
		}

		\WP_CLI\Utils\format_items($format, $items, array('ID', 'Title', 'Status'));
}
</code></pre>



<h3 class="wp-block-heading" id="error-handling-error-handling">Error Handling</h3>



<h4 class="wp-block-heading" id="validation">Validation</h4>



<p>Always validate parameters:</p>



<pre class="wp-block-code"><code>public function import($args, $assoc_args) {
		list($file) = $args;

		<em>// Check file exists</em>
		if (!file_exists($file)) {
				WP_CLI::error("File not found: {$file}");
		}

		<em>// Check file is readable</em>
		if (!is_readable($file)) {
				WP_CLI::error("File is not readable: {$file}");
		}

		<em>// Validate format</em>
		$format = $assoc_args&#91;'format'];
		$allowed = array('json', 'csv');
		if (!in_array($format, $allowed)) {
				WP_CLI::error("Invalid format: {$format}. Must be: " . implode(', ', $allowed));
		}

		<em>// Proceed with import...</em>
}
</code></pre>



<h4 class="wp-block-heading" id="try-catch">Try-Catch</h4>



<p>Handle exceptions gracefully:</p>



<pre class="wp-block-code"><code>public function process() {
		try {
				$this-&gt;do_risky_operation();
				WP_CLI::success("Operation completed");
		} catch (Exception $e) {
				WP_CLI::error("Operation failed: " . $e-&gt;getMessage());
		}
}
</code></pre>



<h4 class="wp-block-heading" id="exit-codes">Exit Codes</h4>



<p>Commands should return appropriate exit codes:</p>



<ul class="wp-block-list">
<li><code>0</code>&nbsp;&#8211; Success</li>



<li><code>1</code>&nbsp;&#8211; Error (automatic with&nbsp;<code>WP_CLI::error()</code>)</li>
</ul>



<h3 class="wp-block-heading" id="complete-example-site-audit-command-complete-example">Complete Example: Site Audit Command</h3>



<p>Let&#8217;s build a real-world command that audits a WordPress site.</p>



<pre class="wp-block-code"><code>&lt;?php
<em>/**
 * Plugin Name: Site Audit Command
 * Description: Custom WP-CLI command for site auditing
 * Version: 1.0.0
 */</em>

if (!defined('WP_CLI')) {
		return;
}

<em>/**
 * Performs comprehensive site audits
 */</em>
class Site_Audit_Command {

		<em>/**
		 * Run a complete site audit
		 *
		 * ## OPTIONS
		 *
		 * &#91;--format=&lt;format&gt;]
		 * : Output format
		 * ---
		 * default: table
		 * options:
		 *   - table
		 *   - json
		 * ---
		 *
		 * &#91;--save=&lt;file&gt;]
		 * : Save results to file
		 *
		 * ## EXAMPLES
		 *
		 *     wp site-audit run
		 *     wp site-audit run --format=json
		 *     wp site-audit run --save=audit-report.json
		 *
		 * @when after_wp_load
		 */</em>
		public function run($args, $assoc_args) {
				WP_CLI::log("Running site audit...");

				$results = array();

				<em>// WordPress version</em>
				$results&#91;] = $this-&gt;check_wordpress_version();

				<em>// Plugin updates</em>
				$results&#91;] = $this-&gt;check_plugin_updates();

				<em>// Theme updates</em>
				$results&#91;] = $this-&gt;check_theme_updates();

				<em>// Database size</em>
				$results&#91;] = $this-&gt;check_database_size();

				<em>// Upload directory size</em>
				$results&#91;] = $this-&gt;check_uploads_size();

				<em>// Orphaned data</em>
				$results&#91;] = $this-&gt;check_orphaned_data();

				<em>// Get format</em>
				$format = \WP_CLI\Utils\get_flag_value($assoc_args, 'format', 'table');

				<em>// Output results</em>
				if ('json' === $format) {
						WP_CLI::log(json_encode($results, JSON_PRETTY_PRINT));
				} else {
						\WP_CLI\Utils\format_items('table', $results, array('Check', 'Status', 'Details'));
				}

				<em>// Save to file if requested</em>
				if (isset($assoc_args&#91;'save'])) {
						file_put_contents($assoc_args&#91;'save'], json_encode($results, JSON_PRETTY_PRINT));
						WP_CLI::success("Audit saved to: " . $assoc_args&#91;'save']);
				}

				WP_CLI::success("Audit complete!");
		}

		private function check_wordpress_version() {
				global $wp_version;

				$updates = get_core_updates();
				$has_update = isset($updates&#91;0]) &amp;&amp; $updates&#91;0]-&gt;response === 'upgrade';

				return array(
						'Check' =&gt; 'WordPress Version',
						'Status' =&gt; $has_update ? '⚠ Update Available' : '✓ Up to Date',
						'Details' =&gt; 'Current: ' . $wp_version
				);
		}

		private function check_plugin_updates() {
				$updates = get_plugin_updates();
				$count = count($updates);

				return array(
						'Check' =&gt; 'Plugin Updates',
						'Status' =&gt; $count &gt; 0 ? "⚠ {$count} available" : '✓ All updated',
						'Details' =&gt; $count &gt; 0 ? implode(', ', array_keys($updates)) : 'None'
				);
		}

		private function check_theme_updates() {
				$updates = get_theme_updates();
				$count = count($updates);

				return array(
						'Check' =&gt; 'Theme Updates',
						'Status' =&gt; $count &gt; 0 ? "⚠ {$count} available" : '✓ All updated',
						'Details' =&gt; $count &gt; 0 ? implode(', ', array_keys($updates)) : 'None'
				);
		}

		private function check_database_size() {
				global $wpdb;

				$size = $wpdb-&gt;get_var("
						SELECT SUM(data_length + index_length) / 1024 / 1024
						FROM information_schema.TABLES
						WHERE table_schema = '{$wpdb-&gt;dbname}'
				");

				$size_mb = round($size, 2);
				$status = $size_mb &gt; 500 ? '⚠ Large' : '✓ Normal';

				return array(
						'Check' =&gt; 'Database Size',
						'Status' =&gt; $status,
						'Details' =&gt; "{$size_mb} MB"
				);
		}

		private function check_uploads_size() {
				$upload_dir = wp_upload_dir();
				$path = $upload_dir&#91;'basedir'];

				$size = $this-&gt;get_directory_size($path);
				$size_mb = round($size / 1024 / 1024, 2);
				$status = $size_mb &gt; 5000 ? '⚠ Large' : '✓ Normal';

				return array(
						'Check' =&gt; 'Uploads Directory',
						'Status' =&gt; $status,
						'Details' =&gt; "{$size_mb} MB"
				);
		}

		private function check_orphaned_data() {
				global $wpdb;

				<em>// Count orphaned post meta</em>
				$orphaned = $wpdb-&gt;get_var("
						SELECT COUNT(*)
						FROM {$wpdb-&gt;postmeta} pm
						LEFT JOIN {$wpdb-&gt;posts} p ON p.ID = pm.post_id
						WHERE p.ID IS NULL
				");

				$status = $orphaned &gt; 0 ? "⚠ {$orphaned} found" : '✓ None';

				return array(
						'Check' =&gt; 'Orphaned Post Meta',
						'Status' =&gt; $status,
						'Details' =&gt; $orphaned &gt; 0 ? "Run cleanup recommended" : 'Clean'
				);
		}

		private function get_directory_size($path) {
				$size = 0;

				foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)) as $file) {
						if ($file-&gt;isFile()) {
								$size += $file-&gt;getSize();
						}
				}

				return $size;
		}
}

WP_CLI::add_command('site-audit', 'Site_Audit_Command');
</code></pre>



<h4 class="wp-block-heading" id="usage">Usage</h4>



<pre class="wp-block-code"><code><em># Run audit</em>
wp site-audit run

<em># JSON output</em>
wp site-audit run --format=json

<em># Save to file</em>
wp site-audit run --save=audit-2025.json
</code></pre>



<p><strong>Output:</strong></p>



<pre class="wp-block-code"><code>+---------------------+-------------------+------------------+
| Check               | Status            | Details          |
+---------------------+-------------------+------------------+
| WordPress Version   | ✓ Up to Date      | Current: 6.4.2   |
| Plugin Updates      | ⚠ 3 available     | woocommerce, ... |
| Theme Updates       | ✓ All updated     | None             |
| Database Size       | ✓ Normal          | 45.23 MB         |
| Uploads Directory   | ⚠ Large           | 5432.12 MB       |
| Orphaned Post Meta  | ⚠ 156 found       | Run cleanup...   |
+---------------------+-------------------+------------------+
Success: Audit complete!
</code></pre>



<h3 class="wp-block-heading" id="packaging-commands-packaging">Packaging Commands</h3>



<h4 class="wp-block-heading" id="create-composer-package">Create Composer Package</h4>



<p>Structure your command as a Composer package:</p>



<pre class="wp-block-code"><code>my-wp-cli-command/
├── composer.json
├── command.php
└── README.md
</code></pre>



<p><strong>composer.json:</strong></p>



<pre class="wp-block-code"><code>{
		"name": "vendor/my-wpcli-command",
		"description": "Custom WP-CLI command for site auditing",
		"type": "wp-cli-package",
		"require": {
				"php": "&gt;=7.4"
		},
		"autoload": {
				"files": &#91;"command.php"]
		},
		"extra": {
				"commands": &#91;
						"site-audit"
				]
		}
}
</code></pre>



<h4 class="wp-block-heading" id="install-via-composer">Install via Composer</h4>



<p>Users can install your command:</p>



<pre class="wp-block-code"><code>wp package install vendor/my-wpcli-command
</code></pre>



<p>Or via&nbsp;<code>composer.json</code>:</p>



<pre class="wp-block-code"><code>{
		"require": {
				"vendor/my-wpcli-command": "^1.0"
		}
}
</code></pre>



<p>Learn more:&nbsp;<a href="https://wp-cli.org/package-index/">WP-CLI Package Index</a></p>



<h3 class="wp-block-heading" id="testing-custom-commands-testing">Testing Custom Commands</h3>



<h4 class="wp-block-heading" id="manual-testing">Manual Testing</h4>



<pre class="wp-block-code"><code><em># Test basic functionality</em>
wp site-audit run

<em># Test with flags</em>
wp site-audit run --format=json

<em># Test error handling (invalid flag)</em>
wp site-audit run --format=invalid
</code></pre>



<h4 class="wp-block-heading" id="automated-testing-with-behat">Automated Testing with Behat</h4>



<p>For serious projects, use WP-CLI&#8217;s testing framework:</p>



<pre class="wp-block-code"><code><em># Install testing framework</em>
composer require --dev wp-cli/wp-cli-tests
</code></pre>



<p><strong>Example test:</strong></p>



<pre class="wp-block-code"><code>Feature: Site audit command

	Scenario: Run basic audit
		Given a WP install

		When I run `wp site-audit run`
		Then STDOUT should contain "Audit complete"
		And the return code should be 0

	Scenario: Save audit to file
		Given a WP install

		When I run `wp site-audit run --save=test-audit.json`
		Then the test-audit.json file should exist
		And STDOUT should contain "Audit saved"
</code></pre>



<p>Learn more:&nbsp;<a href="https://make.wordpress.org/cli/handbook/misc/plugin-unit-tests/">WP-CLI Testing Documentation</a></p>



<h3 class="wp-block-heading" id="distribution-distribution">Distribution</h3>



<h4 class="wp-block-heading" id="github-repository">GitHub Repository</h4>



<p>Host your command on GitHub:</p>



<ol class="wp-block-list">
<li>Create repo:&nbsp;<code>my-wpcli-command</code></li>



<li>Push code with&nbsp;<code>composer.json</code></li>



<li>Tag releases:&nbsp;<code>v1.0.0</code>,&nbsp;<code>v1.1.0</code>, etc.</li>
</ol>



<h4 class="wp-block-heading" id="wp-cli-package-index">WP-CLI Package Index</h4>



<p>Submit to&nbsp;<a href="https://wp-cli.org/package-index/">WP-CLI Package Index</a>:</p>



<ol class="wp-block-list">
<li>Follow package guidelines</li>



<li>Submit pull request to package index</li>



<li>Wait for review and approval</li>
</ol>



<h4 class="wp-block-heading" id="wordpress-plugin-directory">WordPress Plugin Directory</h4>



<p>Optionally distribute as WordPress plugin:</p>



<ul class="wp-block-list">
<li>Works automatically when plugin is activated</li>



<li>Discoverable via WordPress.org</li>



<li>Easier for non-technical users</li>
</ul>



<h3 class="wp-block-heading" id="real-world-examples-examples">Real-World Examples</h3>



<h4 class="wp-block-heading" id="official-wp-cli-packages">Official WP-CLI Packages</h4>



<p>Study these for inspiration:</p>



<ul class="wp-block-list">
<li><a href="https://github.com/wp-cli/doctor-command">wp-cli/doctor-command</a>&nbsp;&#8211; Health checks</li>



<li><a href="https://github.com/wp-cli/profile-command">wp-cli/profile-command</a>&nbsp;&#8211; Performance profiling</li>



<li><a href="https://github.com/wp-cli/scaffold-command">wp-cli/scaffold-command</a>&nbsp;&#8211; Code generation</li>
</ul>



<h4 class="wp-block-heading" id="popular-third-party-commands">Popular Third-Party Commands</h4>



<ul class="wp-block-list">
<li><strong>WooCommerce CLI</strong>&nbsp;&#8211; WooCommerce management</li>



<li><strong>ACF CLI</strong>&nbsp;&#8211; Advanced Custom Fields operations</li>



<li><strong>Yoast SEO CLI</strong>&nbsp;&#8211; SEO management</li>
</ul>



<p>Browse more:&nbsp;<a href="https://wp-cli.org/package-index/">WP-CLI Package Index</a></p>



<h3 class="wp-block-heading" id="troubleshooting-troubleshooting">Troubleshooting</h3>



<h4 class="wp-block-heading" id="command-not-found">Command Not Found</h4>



<p><strong>Problem</strong>:&nbsp;<code>wp mycommand</code>&nbsp;returns &#8220;Error: &#8216;mycommand&#8217; is not a registered wp command.&#8221;</p>



<p><strong>Solutions</strong>:</p>



<ol class="wp-block-list">
<li>Check plugin is activated:</li>
</ol>



<pre class="wp-block-code"><code>wp plugin list
</code></pre>



<ol start="2" class="wp-block-list">
<li>Verify&nbsp;<code>WP_CLI</code>&nbsp;is defined:</li>
</ol>



<pre class="wp-block-code"><code>if (!defined('WP_CLI')) {
		return; <em>// Command won't register</em>
}
</code></pre>



<ol start="3" class="wp-block-list">
<li>Check command registration:</li>
</ol>



<pre class="wp-block-code"><code>WP_CLI::add_command('mycommand', 'My_Command');
</code></pre>



<h4 class="wp-block-heading" id="parameters-not-working">Parameters Not Working</h4>



<p><strong>Problem</strong>: Flags are ignored or cause errors.</p>



<p><strong>Solutions</strong>:</p>



<ol class="wp-block-list">
<li>Check synopsis syntax in docblock</li>



<li>Verify parameter names match:</li>
</ol>



<pre class="wp-block-code"><code>$format = $assoc_args&#91;'format'];  <em>// Must match --format</em>
</code></pre>



<ol start="3" class="wp-block-list">
<li>Provide defaults for optional parameters:</li>
</ol>



<pre class="wp-block-code"><code>$format = \WP_CLI\Utils\get_flag_value($assoc_args, 'format', 'table');
</code></pre>



<h4 class="wp-block-heading" id="memory-limit-errors">Memory Limit Errors</h4>



<p><strong>Problem</strong>: Command fails with &#8220;Allowed memory size exhausted.&#8221;</p>



<p><strong>Solution</strong>: Process items in batches:</p>



<pre class="wp-block-code"><code>public function process_all() {
		$per_page = 100;
		$page = 1;

		do {
				$items = get_posts(array(
						'posts_per_page' =&gt; $per_page,
						'paged' =&gt; $page
				));

				foreach ($items as $item) {
						<em>// Process item</em>
				}

				$page++;

		} while (count($items) === $per_page);
}
</code></pre>



<h3 class="wp-block-heading" id="next-steps-next-steps">Next Steps</h3>



<p>You now know how to create production-ready custom WP-CLI commands!</p>



<h4 class="wp-block-heading" id="continue-learning">Continue Learning</h4>



<ol class="wp-block-list">
<li><strong><a href="#">Bash Functions for WordPress</a></strong>&nbsp;&#8211; Combine with shell scripts</li>



<li><strong><a href="#">WordPress CI/CD with GitHub Actions</a></strong>&nbsp;&#8211; Use commands in pipelines</li>



<li><strong><a href="#">WP-CLI Config Files</a></strong>&nbsp;&#8211; Advanced configuration</li>
</ol>



<h4 class="wp-block-heading" id="build-these-commands">Build These Commands</h4>



<p><strong>Practice projects:</strong></p>



<ul class="wp-block-list">
<li>Content migration command</li>



<li>Security audit command</li>



<li>Performance optimization command</li>



<li>Database cleanup command</li>



<li>Multi-site sync command</li>
</ul>



<h4 class="wp-block-heading" id="master-wordpress-automation">Master WordPress Automation</h4>



<p>Want to build advanced automation systems with custom commands, APIs, and deployment pipelines?</p>



<p><strong><a href="/#get-started">Join the WPCLI Mastery waitlist</a></strong>&nbsp;and get:</p>



<ul class="wp-block-list">
<li>Advanced command development patterns</li>



<li>Real-world command examples library</li>



<li>Package creation templates</li>



<li>Early bird course pricing ($99 vs $199)</li>
</ul>



<h3 class="wp-block-heading" id="conclusion">Conclusion</h3>



<p>Creating custom WP-CLI commands lets you extend WordPress automation with your specific workflow needs. Whether you&#8217;re building internal tools, distributing plugins, or streamlining client management, custom commands are essential for professional WordPress development.</p>



<p><strong>Key takeaways:</strong></p>



<ul class="wp-block-list">
<li>Use classes for organization and multiple subcommands</li>



<li>Leverage&nbsp;<code>WP_CLI</code>&nbsp;class methods for professional output</li>



<li>Define parameters with synopsis for clear documentation</li>



<li>Support multiple output formats (table, JSON, CSV)</li>



<li>Package with Composer for easy distribution</li>



<li>Test thoroughly before releasing</li>
</ul>



<p>The custom commands you build today become reusable tools that save time forever.</p>



<p><strong>Ready to build?</strong>&nbsp;Start with the site audit example and adapt it to your needs.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><strong>Questions about custom WP-CLI commands?</strong>&nbsp;Drop a comment below!</p>



<p><strong>Found this helpful?</strong>&nbsp;Share with other WordPress developers building CLI tools.</p>



<p><strong>Next:</strong>&nbsp;Learn how to&nbsp;<a href="#">integrate external APIs with WordPress</a>&nbsp;using WP-CLI commands.</p>
<p>The post <a href="https://wpclimastery.com/blog/how-to-create-custom-wp-cli-commands-developers-guide/">How to Create Custom WP-CLI Commands: Developer&#8217;s Guide</a> appeared first on <a href="https://wpclimastery.com">WP-CLI Mastery</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
