From 9a3e9279d51106fcfa4590d39377e3e96adee069 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:09:07 +0000 Subject: [PATCH 1/8] Initial plan From 24e960e6506a69769e0047b94004876fe9e68d12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:27:43 +0000 Subject: [PATCH 2/8] Add plugin dependencies support with --with-dependencies flag and install-dependencies command Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- composer.json | 1 + features/plugin-dependencies.feature | 131 +++++++++++++++++++++ src/Plugin_Command.php | 165 ++++++++++++++++++++++++++- 3 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 features/plugin-dependencies.feature diff --git a/composer.json b/composer.json index 4827a65c..6cbe2c7c 100644 --- a/composer.json +++ b/composer.json @@ -49,6 +49,7 @@ "plugin delete", "plugin get", "plugin install", + "plugin install-dependencies", "plugin is-installed", "plugin list", "plugin path", diff --git a/features/plugin-dependencies.feature b/features/plugin-dependencies.feature new file mode 100644 index 00000000..7f0d9f99 --- /dev/null +++ b/features/plugin-dependencies.feature @@ -0,0 +1,131 @@ +Feature: Plugin dependencies support + + Background: + Given an empty cache + + @require-wp-6.5 + Scenario: Install plugin with dependencies using --with-dependencies flag + Given a WP install + + When I run `wp plugin install akismet` + Then STDOUT should contain: + """ + Plugin installed successfully. + """ + + # Create a test plugin with dependencies + And a wp-content/plugins/test-plugin/test-plugin.php file: + """ + install_with_dependencies( $args, $assoc_args ); + } else { + parent::install( $args, $assoc_args ); + } + } + + /** + * Installs plugins with their dependencies. + * + * @param array $args Plugin slugs to install. + * @param array $assoc_args Associative arguments. + */ + private function install_with_dependencies( $args, $assoc_args ) { + $all_to_install = []; + $installed_tracker = []; + + // Remove with-dependencies from assoc_args to avoid infinite recursion + unset( $assoc_args['with-dependencies'] ); + + // Collect all plugins and their dependencies + foreach ( $args as $slug ) { + $this->collect_dependencies( $slug, $all_to_install, $installed_tracker ); + } + + if ( empty( $all_to_install ) ) { + WP_CLI::success( 'No plugins to install.' ); + return; + } + + // Install all collected plugins + parent::install( $all_to_install, $assoc_args ); + } + + /** + * Recursively collects all dependencies for a plugin. + * + * @param string $slug Plugin slug. + * @param array &$all_to_install Reference to array of all plugins to install. + * @param array &$installed_tracker Reference to array tracking what we've already processed. + */ + private function collect_dependencies( $slug, &$all_to_install, &$installed_tracker ) { + // Skip if already processed + if ( isset( $installed_tracker[ $slug ] ) ) { + return; + } + + $installed_tracker[ $slug ] = true; + + // Skip if it's a URL or zip file (can't get dependencies for those) + $is_remote = false !== strpos( $slug, '://' ); + if ( $is_remote || ( pathinfo( $slug, PATHINFO_EXTENSION ) === 'zip' && is_file( $slug ) ) ) { + $all_to_install[] = $slug; + return; + } + + // Get plugin dependencies from WordPress.org API + $dependencies = $this->get_plugin_dependencies( $slug ); + + // Recursively install dependencies first + if ( ! empty( $dependencies ) ) { + foreach ( $dependencies as $dependency_slug ) { + $this->collect_dependencies( $dependency_slug, $all_to_install, $installed_tracker ); + } + } + + // Add this plugin to the install list + $all_to_install[] = $slug; + } + + /** + * Gets the dependencies for a plugin from WordPress.org API. + * + * @param string $slug Plugin slug. + * @return array Array of dependency slugs. + */ + private function get_plugin_dependencies( $slug ) { + $api = plugins_api( 'plugin_information', array( 'slug' => $slug ) ); + + if ( is_wp_error( $api ) ) { + WP_CLI::debug( "Could not fetch information for plugin '$slug': " . $api->get_error_message() ); + return []; + } + + // Check if requires_plugins field exists and is not empty + if ( ! empty( $api->requires_plugins ) && is_array( $api->requires_plugins ) ) { + return $api->requires_plugins; + } + + return []; } /** @@ -1377,6 +1480,66 @@ public function is_active( $args, $assoc_args ) { $this->check_active( $plugin->file, $network_wide ) ? WP_CLI::halt( 0 ) : WP_CLI::halt( 1 ); } + /** + * Installs all dependencies of an installed plugin. + * + * This command is useful when you have a plugin installed that depends on other plugins, + * and you want to install those dependencies without activating the main plugin. + * + * ## OPTIONS + * + * + * : The installed plugin to get dependencies for. + * + * [--activate] + * : If set, dependencies will be activated immediately after install. + * + * [--activate-network] + * : If set, dependencies will be network activated immediately after install. + * + * ## EXAMPLES + * + * # Install all dependencies of an installed plugin + * $ wp plugin install-dependencies my-plugin + * Installing dependency: required-plugin-1 (1.2.3) + * Plugin installed successfully. + * Installing dependency: required-plugin-2 (2.0.0) + * Plugin installed successfully. + * Success: Installed 2 dependencies. + * + * @subcommand install-dependencies + */ + public function install_dependencies( $args, $assoc_args ) { + $plugin = $this->fetcher->get_check( $args[0] ); + $file = $plugin->file; + + // Check if plugin is installed + if ( ! file_exists( WP_PLUGIN_DIR . '/' . $file ) ) { + WP_CLI::error( "Plugin '{$args[0]}' is not installed." ); + } + + // Get dependencies from plugin header + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false ); + $dependencies = []; + + if ( ! empty( $plugin_data['RequiresPlugins'] ) ) { + // Parse the comma-separated list + $dependencies = array_map( 'trim', explode( ',', $plugin_data['RequiresPlugins'] ) ); + } + + if ( empty( $dependencies ) ) { + WP_CLI::success( "Plugin '{$args[0]}' has no dependencies." ); + return; + } + + WP_CLI::log( sprintf( "Installing %d %s for '%s'...", count( $dependencies ), Utils\pluralize( 'dependency', count( $dependencies ) ), $args[0] ) ); + + // Install dependencies + $this->chained_command = true; + $this->install( $dependencies, $assoc_args ); + $this->chained_command = false; + } + /** * Deletes plugin files without deactivating or uninstalling. * From 80278a0c68e76c733ec3c0c19e9406cbdeca1fcb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:31:03 +0000 Subject: [PATCH 3/8] Remove redundant file existence check in install_dependencies Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Plugin_Command.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index 2fc1aaf5..7fde914e 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -1513,11 +1513,6 @@ public function install_dependencies( $args, $assoc_args ) { $plugin = $this->fetcher->get_check( $args[0] ); $file = $plugin->file; - // Check if plugin is installed - if ( ! file_exists( WP_PLUGIN_DIR . '/' . $file ) ) { - WP_CLI::error( "Plugin '{$args[0]}' is not installed." ); - } - // Get dependencies from plugin header $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false ); $dependencies = []; From c2537b098cbdf6eee114c893773447360a583fed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:33:10 +0000 Subject: [PATCH 4/8] Fix potential infinite recursion and improve API error visibility Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Plugin_Command.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index 7fde914e..4fd21ccd 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -1174,7 +1174,7 @@ private function get_plugin_dependencies( $slug ) { $api = plugins_api( 'plugin_information', array( 'slug' => $slug ) ); if ( is_wp_error( $api ) ) { - WP_CLI::debug( "Could not fetch information for plugin '$slug': " . $api->get_error_message() ); + WP_CLI::warning( "Could not fetch information for plugin '$slug': " . $api->get_error_message() ); return []; } @@ -1529,6 +1529,9 @@ public function install_dependencies( $args, $assoc_args ) { WP_CLI::log( sprintf( "Installing %d %s for '%s'...", count( $dependencies ), Utils\pluralize( 'dependency', count( $dependencies ) ), $args[0] ) ); + // Remove with-dependencies flag to avoid recursive dependency resolution + unset( $assoc_args['with-dependencies'] ); + // Install dependencies $this->chained_command = true; $this->install( $dependencies, $assoc_args ); From aef88f05e599c639c19dad938b54907b1f7d6c4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 21:18:02 +0000 Subject: [PATCH 5/8] Fix PHPCS alignment issues for variable assignments Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Plugin_Command.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index 4fd21ccd..4caa62a3 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -1108,7 +1108,7 @@ public function install( $args, $assoc_args ) { * @param array $assoc_args Associative arguments. */ private function install_with_dependencies( $args, $assoc_args ) { - $all_to_install = []; + $all_to_install = []; $installed_tracker = []; // Remove with-dependencies from assoc_args to avoid infinite recursion @@ -1514,7 +1514,7 @@ public function install_dependencies( $args, $assoc_args ) { $file = $plugin->file; // Get dependencies from plugin header - $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false ); + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false ); $dependencies = []; if ( ! empty( $plugin_data['RequiresPlugins'] ) ) { From 7754d6937f4fa57b25439a53b15afa20403b9339 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 21:26:09 +0000 Subject: [PATCH 6/8] Use WP_Plugin_Dependencies class for getting plugin dependencies Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Plugin_Command.php | 56 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index 4caa62a3..6cbfc532 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -1165,12 +1165,40 @@ private function collect_dependencies( $slug, &$all_to_install, &$installed_trac } /** - * Gets the dependencies for a plugin from WordPress.org API. + * Gets the dependencies for a plugin. + * + * Uses WP_Plugin_Dependencies class if available (WordPress 6.5+), + * otherwise falls back to WordPress.org API. * * @param string $slug Plugin slug. * @return array Array of dependency slugs. */ private function get_plugin_dependencies( $slug ) { + // Try to use WP_Plugin_Dependencies class if available (WordPress 6.5+) + if ( class_exists( 'WP_Plugin_Dependencies' ) ) { + // Find the plugin file for this slug + $plugins = get_plugins(); + foreach ( $plugins as $plugin_file => $plugin_data ) { + $plugin_slug = dirname( $plugin_file ); + if ( '.' === $plugin_slug ) { + $plugin_slug = basename( $plugin_file, '.php' ); + } + + if ( $plugin_slug === $slug ) { + // Initialize WP_Plugin_Dependencies if needed + if ( method_exists( 'WP_Plugin_Dependencies', 'initialize' ) ) { + WP_Plugin_Dependencies::initialize(); + } + + // Get dependencies for this plugin file + if ( method_exists( 'WP_Plugin_Dependencies', 'get_dependencies' ) ) { + return WP_Plugin_Dependencies::get_dependencies( $plugin_file ); + } + } + } + } + + // Fallback to WordPress.org API for plugins not yet installed $api = plugins_api( 'plugin_information', array( 'slug' => $slug ) ); if ( is_wp_error( $api ) ) { @@ -1513,13 +1541,27 @@ public function install_dependencies( $args, $assoc_args ) { $plugin = $this->fetcher->get_check( $args[0] ); $file = $plugin->file; - // Get dependencies from plugin header - $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false ); + // Get dependencies using WP_Plugin_Dependencies if available (WordPress 6.5+) $dependencies = []; - - if ( ! empty( $plugin_data['RequiresPlugins'] ) ) { - // Parse the comma-separated list - $dependencies = array_map( 'trim', explode( ',', $plugin_data['RequiresPlugins'] ) ); + + if ( class_exists( 'WP_Plugin_Dependencies' ) ) { + // Initialize WP_Plugin_Dependencies + if ( method_exists( 'WP_Plugin_Dependencies', 'initialize' ) ) { + WP_Plugin_Dependencies::initialize(); + } + + // Get dependencies for this plugin + if ( method_exists( 'WP_Plugin_Dependencies', 'get_dependencies' ) ) { + $dependencies = WP_Plugin_Dependencies::get_dependencies( $file ); + } + } else { + // Fallback: Get dependencies from plugin header manually + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false ); + + if ( ! empty( $plugin_data['RequiresPlugins'] ) ) { + // Parse the comma-separated list + $dependencies = array_map( 'trim', explode( ',', $plugin_data['RequiresPlugins'] ) ); + } } if ( empty( $dependencies ) ) { From 39c63fd37b4e65c4de659e24e7b4bcd54090c5c0 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 2 Nov 2025 10:56:52 +0100 Subject: [PATCH 7/8] Lint fixes --- src/Plugin_Command.php | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index 6cbfc532..9a5511b6 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -1183,17 +1183,10 @@ private function get_plugin_dependencies( $slug ) { if ( '.' === $plugin_slug ) { $plugin_slug = basename( $plugin_file, '.php' ); } - + if ( $plugin_slug === $slug ) { - // Initialize WP_Plugin_Dependencies if needed - if ( method_exists( 'WP_Plugin_Dependencies', 'initialize' ) ) { - WP_Plugin_Dependencies::initialize(); - } - - // Get dependencies for this plugin file - if ( method_exists( 'WP_Plugin_Dependencies', 'get_dependencies' ) ) { - return WP_Plugin_Dependencies::get_dependencies( $plugin_file ); - } + WP_Plugin_Dependencies::initialize(); + return WP_Plugin_Dependencies::get_dependencies( $plugin_file ); } } } @@ -1543,21 +1536,15 @@ public function install_dependencies( $args, $assoc_args ) { // Get dependencies using WP_Plugin_Dependencies if available (WordPress 6.5+) $dependencies = []; - + if ( class_exists( 'WP_Plugin_Dependencies' ) ) { + WP_Plugin_Dependencies::initialize(); // Initialize WP_Plugin_Dependencies - if ( method_exists( 'WP_Plugin_Dependencies', 'initialize' ) ) { - WP_Plugin_Dependencies::initialize(); - } - - // Get dependencies for this plugin - if ( method_exists( 'WP_Plugin_Dependencies', 'get_dependencies' ) ) { - $dependencies = WP_Plugin_Dependencies::get_dependencies( $file ); - } + $dependencies = WP_Plugin_Dependencies::get_dependencies( $file ); } else { // Fallback: Get dependencies from plugin header manually $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false ); - + if ( ! empty( $plugin_data['RequiresPlugins'] ) ) { // Parse the comma-separated list $dependencies = array_map( 'trim', explode( ',', $plugin_data['RequiresPlugins'] ) ); From ac5ed2d6c2d18cd633a4573ec1ff2054590b2664 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 2 Nov 2025 11:36:49 +0100 Subject: [PATCH 8/8] Update tests --- features/plugin-dependencies.feature | 114 +++++++++++++++++++++------ src/Plugin_Command.php | 53 ++++++------- 2 files changed, 111 insertions(+), 56 deletions(-) diff --git a/features/plugin-dependencies.feature b/features/plugin-dependencies.feature index 7f0d9f99..7b4b654b 100644 --- a/features/plugin-dependencies.feature +++ b/features/plugin-dependencies.feature @@ -3,35 +3,52 @@ Feature: Plugin dependencies support Background: Given an empty cache + @less-than-wp-6.5 + Scenario: Install plugin with dependencies using --with-dependencies flag + Given a WP install + + When I try `wp plugin install --with-dependencies bp-classic` + Then STDERR should contain: + """ + Installing plugins with dependencies requires WordPress 6.5 or greater. + """ + @require-wp-6.5 Scenario: Install plugin with dependencies using --with-dependencies flag Given a WP install - When I run `wp plugin install akismet` + When I run `wp plugin install --with-dependencies bp-classic` Then STDOUT should contain: """ - Plugin installed successfully. + Installing BuddyPress + """ + And STDOUT should contain: + """ + Installing BP Classic + """ + And STDOUT should contain: + """ + Success: Installed 2 of 2 plugins. """ - # Create a test plugin with dependencies - And a wp-content/plugins/test-plugin/test-plugin.php file: + When I run `wp plugin list --fields=name,status --format=csv` + Then STDOUT should contain: """ - install_with_dependencies( $args, $assoc_args ); } else { parent::install( $args, $assoc_args ); @@ -1167,27 +1170,21 @@ private function collect_dependencies( $slug, &$all_to_install, &$installed_trac /** * Gets the dependencies for a plugin. * - * Uses WP_Plugin_Dependencies class if available (WordPress 6.5+), - * otherwise falls back to WordPress.org API. - * * @param string $slug Plugin slug. * @return array Array of dependency slugs. */ private function get_plugin_dependencies( $slug ) { - // Try to use WP_Plugin_Dependencies class if available (WordPress 6.5+) - if ( class_exists( 'WP_Plugin_Dependencies' ) ) { - // Find the plugin file for this slug - $plugins = get_plugins(); - foreach ( $plugins as $plugin_file => $plugin_data ) { - $plugin_slug = dirname( $plugin_file ); - if ( '.' === $plugin_slug ) { - $plugin_slug = basename( $plugin_file, '.php' ); - } + // Find the plugin file for this slug + $plugins = get_plugins(); + foreach ( $plugins as $plugin_file => $plugin_data ) { + $plugin_slug = dirname( $plugin_file ); + if ( '.' === $plugin_slug ) { + $plugin_slug = basename( $plugin_file, '.php' ); + } - if ( $plugin_slug === $slug ) { - WP_Plugin_Dependencies::initialize(); - return WP_Plugin_Dependencies::get_dependencies( $plugin_file ); - } + if ( $plugin_slug === $slug ) { + WP_Plugin_Dependencies::initialize(); + return WP_Plugin_Dependencies::get_dependencies( $plugin_file ); } } @@ -1518,6 +1515,10 @@ public function is_active( $args, $assoc_args ) { * [--activate-network] * : If set, dependencies will be network activated immediately after install. * + * [--force] + * : If set, the command will overwrite any installed version of the plugin, without prompting + * for confirmation. + * * ## EXAMPLES * * # Install all dependencies of an installed plugin @@ -1531,25 +1532,17 @@ public function is_active( $args, $assoc_args ) { * @subcommand install-dependencies */ public function install_dependencies( $args, $assoc_args ) { + if ( WP_CLI\Utils\wp_version_compare( '6.5', '<' ) ) { + WP_CLI::error( 'Installing plugin dependencies requires WordPress 6.5 or greater.' ); + } + $plugin = $this->fetcher->get_check( $args[0] ); $file = $plugin->file; - // Get dependencies using WP_Plugin_Dependencies if available (WordPress 6.5+) $dependencies = []; - if ( class_exists( 'WP_Plugin_Dependencies' ) ) { - WP_Plugin_Dependencies::initialize(); - // Initialize WP_Plugin_Dependencies - $dependencies = WP_Plugin_Dependencies::get_dependencies( $file ); - } else { - // Fallback: Get dependencies from plugin header manually - $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false ); - - if ( ! empty( $plugin_data['RequiresPlugins'] ) ) { - // Parse the comma-separated list - $dependencies = array_map( 'trim', explode( ',', $plugin_data['RequiresPlugins'] ) ); - } - } + WP_Plugin_Dependencies::initialize(); + $dependencies = WP_Plugin_Dependencies::get_dependencies( $file ); if ( empty( $dependencies ) ) { WP_CLI::success( "Plugin '{$args[0]}' has no dependencies." );