From ce264e71e65f8c4e1f7d811f5937febf2f60743e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:55:23 +0000 Subject: [PATCH 01/10] Initial plan From 783e8991103626d1c87a32a27c931ebcef828712 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:12:53 +0000 Subject: [PATCH 02/10] Add Ability and Ability Category commands with tests Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- composer.json | 9 + entity-command.php | 23 ++ features/ability-category.feature | 113 ++++++++++ features/ability.feature | 240 ++++++++++++++++++++ src/Ability_Category_Command.php | 227 +++++++++++++++++++ src/Ability_Command.php | 356 ++++++++++++++++++++++++++++++ 6 files changed, 968 insertions(+) create mode 100644 features/ability-category.feature create mode 100644 features/ability.feature create mode 100644 src/Ability_Category_Command.php create mode 100644 src/Ability_Command.php diff --git a/composer.json b/composer.json index 21d853c5..6474665c 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,15 @@ }, "bundled": true, "commands": [ + "ability", + "ability category", + "ability category exists", + "ability category get", + "ability category list", + "ability execute", + "ability exists", + "ability get", + "ability list", "comment", "comment approve", "comment count", diff --git a/entity-command.php b/entity-command.php index 6cb54ff3..2dfe9513 100644 --- a/entity-command.php +++ b/entity-command.php @@ -92,3 +92,26 @@ }, ) ); + +WP_CLI::add_command( + 'ability', + 'Ability_Command', + array( + 'before_invoke' => function () { + if ( Utils\wp_version_compare( '6.9', '<' ) ) { + WP_CLI::error( 'Requires WordPress 6.9 or greater.' ); + } + }, + ) +); +WP_CLI::add_command( + 'ability category', + 'Ability_Category_Command', + array( + 'before_invoke' => function () { + if ( Utils\wp_version_compare( '6.9', '<' ) ) { + WP_CLI::error( 'Requires WordPress 6.9 or greater.' ); + } + }, + ) +); diff --git a/features/ability-category.feature b/features/ability-category.feature new file mode 100644 index 00000000..375d6978 --- /dev/null +++ b/features/ability-category.feature @@ -0,0 +1,113 @@ +Feature: Manage WordPress ability categories + + Background: + Given a WP install + + @require-wp-6.9 + Scenario: Ability category commands require WordPress 6.9+ + When I try `wp core download --version=6.8 --force` + And I run `wp core version` + Then STDOUT should contain: + """ + 6.8 + """ + + When I try `wp ability category list` + Then STDERR should contain: + """ + Error: Requires WordPress 6.9 or greater. + """ + And the return code should be 1 + + @require-wp-6.9 + Scenario: List ability categories + Given a wp-content/mu-plugins/test-ability-categories.php file: + """ + 'First test category', + ) ); + + wp_register_ability_category( 'test_category_2', array( + 'description' => 'Second test category', + ) ); + } ); + """ + + When I run `wp ability category list --format=count` + Then STDOUT should contain: + """ + 2 + """ + + When I run `wp ability category list --fields=name,description --format=csv` + Then STDOUT should contain: + """ + test_category_1,"First test category" + """ + And STDOUT should contain: + """ + test_category_2,"Second test category" + """ + + @require-wp-6.9 + Scenario: Get a specific ability category + Given a wp-content/mu-plugins/test-ability-categories.php file: + """ + 'Content operations category', + ) ); + } ); + """ + + When I try `wp ability category get invalid_category` + Then STDERR should contain: + """ + Error: Ability category invalid_category doesn't exist. + """ + And the return code should be 1 + + When I run `wp ability category get content_ops --field=description` + Then STDOUT should be: + """ + Content operations category + """ + + When I run `wp ability category get content_ops --field=name` + Then STDOUT should be: + """ + content_ops + """ + + @require-wp-6.9 + Scenario: Check if an ability category exists + Given a wp-content/mu-plugins/test-ability-categories.php file: + """ + 'This category exists', + ) ); + } ); + """ + + When I try `wp ability category exists existing_category` + Then the return code should be 0 + + When I try `wp ability category exists non_existent_category` + Then the return code should be 1 diff --git a/features/ability.feature b/features/ability.feature new file mode 100644 index 00000000..b0e43fcf --- /dev/null +++ b/features/ability.feature @@ -0,0 +1,240 @@ +Feature: Manage WordPress abilities + + Background: + Given a WP install + + @require-wp-6.9 + Scenario: Ability commands require WordPress 6.9+ + When I try `wp core download --version=6.8 --force` + And I run `wp core version` + Then STDOUT should contain: + """ + 6.8 + """ + + When I try `wp ability list` + Then STDERR should contain: + """ + Error: Requires WordPress 6.9 or greater. + """ + And the return code should be 1 + + @require-wp-6.9 + Scenario: List abilities + Given a wp-content/mu-plugins/test-abilities.php file: + """ + 'content', + 'description' => 'Test ability one', + 'callback' => function( $input ) { + return array( 'result' => 'success', 'input' => $input ); + }, + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( 'type' => 'integer' ), + ), + ), + 'output_schema' => array( + 'type' => 'object', + ), + ) ); + + wp_register_ability( 'test_ability_2', array( + 'category' => 'users', + 'description' => 'Test ability two', + 'callback' => function( $input ) { + return array( 'result' => 'done' ); + }, + 'input_schema' => array( 'type' => 'object' ), + 'output_schema' => array( 'type' => 'object' ), + ) ); + } ); + """ + + When I run `wp ability list --format=count` + Then STDOUT should contain: + """ + 2 + """ + + When I run `wp ability list --fields=name,category,description --format=csv` + Then STDOUT should contain: + """ + test_ability_1,content,"Test ability one" + """ + And STDOUT should contain: + """ + test_ability_2,users,"Test ability two" + """ + + When I run `wp ability list --category=content --format=count` + Then STDOUT should contain: + """ + 1 + """ + + @require-wp-6.9 + Scenario: Get a specific ability + Given a wp-content/mu-plugins/test-abilities.php file: + """ + 'content', + 'description' => 'Gets a test post', + 'callback' => function( $input ) { + return array( 'id' => $input['id'], 'title' => 'Test Post' ); + }, + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( 'type' => 'integer' ), + ), + ), + 'output_schema' => array( + 'type' => 'object', + ), + ) ); + } ); + """ + + When I try `wp ability get invalid_ability` + Then STDERR should contain: + """ + Error: Ability invalid_ability doesn't exist. + """ + And the return code should be 1 + + When I run `wp ability get get_test_post --field=category` + Then STDOUT should be: + """ + content + """ + + When I run `wp ability get get_test_post --field=description` + Then STDOUT should be: + """ + Gets a test post + """ + + @require-wp-6.9 + Scenario: Check if an ability exists + Given a wp-content/mu-plugins/test-abilities.php file: + """ + 'content', + 'description' => 'Test exists', + 'callback' => function( $input ) { + return array( 'result' => 'ok' ); + }, + 'input_schema' => array( 'type' => 'object' ), + 'output_schema' => array( 'type' => 'object' ), + ) ); + } ); + """ + + When I try `wp ability exists test_exists` + Then the return code should be 0 + + When I try `wp ability exists non_existent_ability` + Then the return code should be 1 + + @require-wp-6.9 + Scenario: Execute an ability with JSON input + Given a wp-content/mu-plugins/test-abilities.php file: + """ + 'testing', + 'description' => 'Echoes input', + 'callback' => function( $input ) { + return array( 'echoed' => $input ); + }, + 'input_schema' => array( 'type' => 'object' ), + 'output_schema' => array( 'type' => 'object' ), + ) ); + } ); + """ + + When I try `wp ability execute non_existent_ability '{"test": "data"}'` + Then STDERR should contain: + """ + Error: Ability non_existent_ability doesn't exist. + """ + And the return code should be 1 + + When I run `wp ability execute echo_input '{"message": "hello"}'` + Then STDOUT should contain: + """ + "echoed" + """ + And STDOUT should contain: + """ + "message" + """ + And STDOUT should contain: + """ + "hello" + """ + And STDOUT should contain: + """ + Success: Ability executed successfully. + """ + + @require-wp-6.9 + Scenario: Execute an ability with input from STDIN + Given a wp-content/mu-plugins/test-abilities.php file: + """ + 'testing', + 'description' => 'Processes input', + 'callback' => function( $input ) { + return array( 'processed' => true, 'data' => $input ); + }, + 'input_schema' => array( 'type' => 'object' ), + 'output_schema' => array( 'type' => 'object' ), + ) ); + } ); + """ + + When I run `echo '{"value": 42}' | wp ability execute process_input` + Then STDOUT should contain: + """ + "processed": true + """ + And STDOUT should contain: + """ + "value": 42 + """ + And STDOUT should contain: + """ + Success: Ability executed successfully. + """ diff --git a/src/Ability_Category_Command.php b/src/Ability_Category_Command.php new file mode 100644 index 00000000..797b0a79 --- /dev/null +++ b/src/Ability_Category_Command.php @@ -0,0 +1,227 @@ +=] + * : Filter by one or more fields. + * + * [--field=] + * : Prints the value of a single field for each category. + * + * [--fields=] + * : Limit the output to specific category fields. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - json + * - count + * - yaml + * --- + * + * ## AVAILABLE FIELDS + * + * These fields will be displayed by default for each category: + * + * * name + * * description + * + * ## EXAMPLES + * + * # List all registered ability categories + * $ wp ability category list --format=csv + * name,description + * content,Content operations + * + * @subcommand list + */ + public function list_( $args, $assoc_args ) { + $formatter = $this->get_formatter( $assoc_args ); + + $categories = wp_get_ability_categories(); + + if ( empty( $categories ) ) { + $categories = array(); + } + + $items = array(); + foreach ( $categories as $category ) { + $items[] = $this->format_category_for_output( $category ); + } + + // Apply filters from $assoc_args + $filter_keys = array_diff( array_keys( $assoc_args ), array( 'fields', 'field', 'format' ) ); + if ( ! empty( $filter_keys ) ) { + $items = array_filter( + $items, + function ( $item ) use ( $assoc_args, $filter_keys ) { + foreach ( $filter_keys as $key ) { + if ( isset( $assoc_args[ $key ] ) && isset( $item[ $key ] ) ) { + if ( $item[ $key ] !== $assoc_args[ $key ] ) { + return false; + } + } + } + return true; + } + ); + } + + $formatter->display_items( $items ); + } + + /** + * Gets details about a registered ability category. + * + * ## OPTIONS + * + * + * : Category name. + * + * [--field=] + * : Instead of returning the whole category, returns the value of a single field. + * + * [--fields=] + * : Limit the output to specific fields. Defaults to all fields. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - json + * - yaml + * --- + * + * ## AVAILABLE FIELDS + * + * These fields will be displayed by default for the specified category: + * + * * name + * * description + * + * ## EXAMPLES + * + * # Get details of a specific ability category + * $ wp ability category get content --fields=name,description + * +-------------+----------+ + * | Field | Value | + * +-------------+----------+ + * | name | content | + * | description | Content operations | + * +-------------+----------+ + * + * # Get the description of a category + * $ wp ability category get content --field=description + * Content operations + */ + public function get( $args, $assoc_args ) { + $category = wp_get_ability_category( $args[0] ); + + if ( ! $category ) { + WP_CLI::error( "Ability category {$args[0]} doesn't exist." ); + } + + if ( empty( $assoc_args['fields'] ) ) { + $assoc_args['fields'] = $this->fields; + } + + $formatter = $this->get_formatter( $assoc_args ); + + $data = $this->format_category_for_output( $category ); + + $formatter->display_item( $data ); + } + + /** + * Checks whether an ability category exists. + * + * ## OPTIONS + * + * + * : Category name. + * + * ## EXAMPLES + * + * # Check whether a category exists + * $ wp ability category exists content + * $ echo $? + * 0 + * + * # Check whether a non-existent category exists + * $ wp ability category exists fake_category + * $ echo $? + * 1 + */ + public function exists( $args ) { + if ( wp_has_ability_category( $args[0] ) ) { + exit( 0 ); + } else { + exit( 1 ); + } + } + + /** + * Formats an ability category object for output. + * + * @param WP_Ability_Category $category The category object. + * @return array Formatted category data. + */ + private function format_category_for_output( $category ) { + $data = array( + 'name' => $category->name, + 'description' => $category->description, + ); + + return $data; + } + + private function get_formatter( &$assoc_args ) { + return new Formatter( $assoc_args, $this->fields, 'ability-category' ); + } +} diff --git a/src/Ability_Command.php b/src/Ability_Command.php new file mode 100644 index 00000000..db88878d --- /dev/null +++ b/src/Ability_Command.php @@ -0,0 +1,356 @@ +=] + * : Filter by one or more fields. + * + * [--field=] + * : Prints the value of a single field for each ability. + * + * [--fields=] + * : Limit the output to specific ability fields. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - json + * - count + * - yaml + * --- + * + * ## AVAILABLE FIELDS + * + * These fields will be displayed by default for each ability: + * + * * name + * * category + * * description + * + * These fields are optionally available: + * + * * callback + * * input_schema + * * output_schema + * + * ## EXAMPLES + * + * # List all registered abilities + * $ wp ability list --format=csv + * name,category,description + * get_post,content,Gets a post + * + * # List abilities in a specific category + * $ wp ability list --category=content --fields=name,description + * +----------+-------------+ + * | name | description | + * +----------+-------------+ + * | get_post | Gets a post | + * +----------+-------------+ + * + * @subcommand list + */ + public function list_( $args, $assoc_args ) { + $formatter = $this->get_formatter( $assoc_args ); + + $abilities = wp_get_abilities(); + + if ( empty( $abilities ) ) { + $abilities = array(); + } + + $items = array(); + foreach ( $abilities as $ability ) { + $items[] = $this->format_ability_for_output( $ability ); + } + + // Apply filters from $assoc_args + $filter_keys = array_diff( array_keys( $assoc_args ), array( 'fields', 'field', 'format' ) ); + if ( ! empty( $filter_keys ) ) { + $items = array_filter( + $items, + function ( $item ) use ( $assoc_args, $filter_keys ) { + foreach ( $filter_keys as $key ) { + if ( isset( $assoc_args[ $key ] ) && isset( $item[ $key ] ) ) { + if ( $item[ $key ] !== $assoc_args[ $key ] ) { + return false; + } + } + } + return true; + } + ); + } + + $formatter->display_items( $items ); + } + + /** + * Gets details about a registered ability. + * + * ## OPTIONS + * + * + * : Ability name. + * + * [--field=] + * : Instead of returning the whole ability, returns the value of a single field. + * + * [--fields=] + * : Limit the output to specific fields. Defaults to all fields. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - json + * - yaml + * --- + * + * ## AVAILABLE FIELDS + * + * These fields will be displayed by default for the specified ability: + * + * * name + * * category + * * description + * * callback + * * input_schema + * * output_schema + * + * ## EXAMPLES + * + * # Get details of a specific ability + * $ wp ability get get_post --fields=name,category,description + * +-------------+----------+ + * | Field | Value | + * +-------------+----------+ + * | name | get_post | + * | category | content | + * | description | Gets a post | + * +-------------+----------+ + * + * # Get the category of an ability + * $ wp ability get get_post --field=category + * content + */ + public function get( $args, $assoc_args ) { + $ability = wp_get_ability( $args[0] ); + + if ( ! $ability ) { + WP_CLI::error( "Ability {$args[0]} doesn't exist." ); + } + + if ( empty( $assoc_args['fields'] ) ) { + $default_fields = array_merge( + $this->fields, + array( + 'callback', + 'input_schema', + 'output_schema', + ) + ); + + $assoc_args['fields'] = $default_fields; + } + + $formatter = $this->get_formatter( $assoc_args ); + + $data = $this->format_ability_for_output( $ability ); + + $formatter->display_item( $data ); + } + + /** + * Checks whether an ability exists. + * + * ## OPTIONS + * + * + * : Ability name. + * + * ## EXAMPLES + * + * # Check whether an ability exists + * $ wp ability exists get_post + * $ echo $? + * 0 + * + * # Check whether a non-existent ability exists + * $ wp ability exists fake_ability + * $ echo $? + * 1 + */ + public function exists( $args ) { + if ( wp_has_ability( $args[0] ) ) { + exit( 0 ); + } else { + exit( 1 ); + } + } + + /** + * Executes an ability with the provided input. + * + * ## OPTIONS + * + * + * : Ability name. + * + * [] + * : JSON input for the ability. If not provided, reads from STDIN. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: json + * options: + * - json + * - yaml + * --- + * + * ## EXAMPLES + * + * # Execute an ability with inline JSON input + * $ wp ability execute get_post '{"id": 1}' + * + * # Execute an ability with input from STDIN + * $ echo '{"id": 1}' | wp ability execute get_post + * + * # Execute an ability and get YAML output + * $ wp ability execute get_post '{"id": 1}' --format=yaml + */ + public function execute( $args, $assoc_args ) { + $ability_name = $args[0]; + + if ( ! wp_has_ability( $ability_name ) ) { + WP_CLI::error( "Ability {$ability_name} doesn't exist." ); + } + + // Get input from argument or STDIN + $input_json = isset( $args[1] ) ? $args[1] : file_get_contents( 'php://stdin' ); + + if ( empty( $input_json ) ) { + $input = array(); + } else { + $input = json_decode( $input_json, true ); + if ( json_last_error() !== JSON_ERROR_NONE ) { + WP_CLI::error( 'Invalid JSON input: ' . json_last_error_msg() ); + } + } + + // Execute the ability + $result = wp_execute_ability( $ability_name, $input ); + + if ( is_wp_error( $result ) ) { + WP_CLI::error( $result->get_error_message() ); + } + + // Output the result + $format = isset( $assoc_args['format'] ) ? $assoc_args['format'] : 'json'; + + if ( 'json' === $format ) { + WP_CLI::line( wp_json_encode( $result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); + } elseif ( 'yaml' === $format ) { + WP_CLI::line( WP_CLI\Utils\mustache_render( '{{#.}}{{key}}: {{value}}{{/.}}', $result ) ); + } + + WP_CLI::success( 'Ability executed successfully.' ); + } + + /** + * Formats an ability object for output. + * + * @param WP_Ability $ability The ability object. + * @return array Formatted ability data. + */ + private function format_ability_for_output( $ability ) { + $data = array( + 'name' => $ability->name, + 'category' => $ability->category, + 'description' => $ability->description, + 'callback' => $this->format_callback( $ability->callback ), + 'input_schema' => wp_json_encode( $ability->input_schema ), + 'output_schema' => wp_json_encode( $ability->output_schema ), + ); + + return $data; + } + + /** + * Formats a callback for display. + * + * @param callable $callback The callback to format. + * @return string Formatted callback string. + */ + private function format_callback( $callback ) { + if ( is_string( $callback ) ) { + return $callback; + } elseif ( is_array( $callback ) && count( $callback ) === 2 ) { + if ( is_object( $callback[0] ) ) { + return get_class( $callback[0] ) . '::' . $callback[1]; + } else { + return $callback[0] . '::' . $callback[1]; + } + } elseif ( $callback instanceof Closure ) { + return '{closure}'; + } else { + return '{callable}'; + } + } + + private function get_formatter( &$assoc_args ) { + return new Formatter( $assoc_args, $this->fields, 'ability' ); + } +} From 1f2a6dcd3918f531d2a34b14af8f7a353b02bb0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:15:22 +0000 Subject: [PATCH 03/10] Improve robustness and fix test scenarios Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/ability-category.feature | 9 +-------- features/ability.feature | 9 +-------- src/Ability_Category_Command.php | 10 +++++++++- src/Ability_Command.php | 21 +++++++++++++++++++-- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/features/ability-category.feature b/features/ability-category.feature index 375d6978..853713ca 100644 --- a/features/ability-category.feature +++ b/features/ability-category.feature @@ -3,15 +3,8 @@ Feature: Manage WordPress ability categories Background: Given a WP install - @require-wp-6.9 + @less-than-wp-6.9 Scenario: Ability category commands require WordPress 6.9+ - When I try `wp core download --version=6.8 --force` - And I run `wp core version` - Then STDOUT should contain: - """ - 6.8 - """ - When I try `wp ability category list` Then STDERR should contain: """ diff --git a/features/ability.feature b/features/ability.feature index b0e43fcf..b769e05b 100644 --- a/features/ability.feature +++ b/features/ability.feature @@ -3,15 +3,8 @@ Feature: Manage WordPress abilities Background: Given a WP install - @require-wp-6.9 + @less-than-wp-6.9 Scenario: Ability commands require WordPress 6.9+ - When I try `wp core download --version=6.8 --force` - And I run `wp core version` - Then STDOUT should contain: - """ - 6.8 - """ - When I try `wp ability list` Then STDERR should contain: """ diff --git a/src/Ability_Category_Command.php b/src/Ability_Category_Command.php index 797b0a79..95d2af91 100644 --- a/src/Ability_Category_Command.php +++ b/src/Ability_Category_Command.php @@ -81,7 +81,15 @@ class Ability_Category_Command extends WP_CLI_Command { public function list_( $args, $assoc_args ) { $formatter = $this->get_formatter( $assoc_args ); - $categories = wp_get_ability_categories(); + // Get all registered ability categories + if ( function_exists( 'wp_get_ability_categories' ) ) { + $categories = wp_get_ability_categories(); + } elseif ( function_exists( 'wp_ability_categories' ) ) { + $registry = wp_ability_categories(); + $categories = $registry->get_all(); + } else { + $categories = array(); + } if ( empty( $categories ) ) { $categories = array(); diff --git a/src/Ability_Command.php b/src/Ability_Command.php index db88878d..b7b894ab 100644 --- a/src/Ability_Command.php +++ b/src/Ability_Command.php @@ -101,7 +101,15 @@ class Ability_Command extends WP_CLI_Command { public function list_( $args, $assoc_args ) { $formatter = $this->get_formatter( $assoc_args ); - $abilities = wp_get_abilities(); + // Get all registered abilities + if ( function_exists( 'wp_get_abilities' ) ) { + $abilities = wp_get_abilities(); + } elseif ( function_exists( 'wp_abilities' ) ) { + $registry = wp_abilities(); + $abilities = $registry->get_all(); + } else { + $abilities = array(); + } if ( empty( $abilities ) ) { $abilities = array(); @@ -291,7 +299,16 @@ public function execute( $args, $assoc_args ) { } // Execute the ability - $result = wp_execute_ability( $ability_name, $input ); + // First try wp_execute_ability if it exists, otherwise call the callback directly + if ( function_exists( 'wp_execute_ability' ) ) { + $result = wp_execute_ability( $ability_name, $input ); + } else { + $ability = wp_get_ability( $ability_name ); + if ( ! $ability || ! is_callable( $ability->callback ) ) { + WP_CLI::error( "Cannot execute ability {$ability_name}." ); + } + $result = call_user_func( $ability->callback, $input ); + } if ( is_wp_error( $result ) ) { WP_CLI::error( $result->get_error_message() ); From 5c8bd84c407ddc4bc53010e30562c5c47986146a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:19:18 +0000 Subject: [PATCH 04/10] Improve YAML output formatting for execute command Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Ability_Command.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Ability_Command.php b/src/Ability_Command.php index b7b894ab..76d90195 100644 --- a/src/Ability_Command.php +++ b/src/Ability_Command.php @@ -320,7 +320,14 @@ public function execute( $args, $assoc_args ) { if ( 'json' === $format ) { WP_CLI::line( wp_json_encode( $result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } elseif ( 'yaml' === $format ) { - WP_CLI::line( WP_CLI\Utils\mustache_render( '{{#.}}{{key}}: {{value}}{{/.}}', $result ) ); + // Convert to YAML-like output + foreach ( $result as $key => $value ) { + if ( is_array( $value ) || is_object( $value ) ) { + WP_CLI::line( $key . ': ' . wp_json_encode( $value ) ); + } else { + WP_CLI::line( $key . ': ' . $value ); + } + } } WP_CLI::success( 'Ability executed successfully.' ); From 05b7c16be6ff7afed397c73fdbe869a8f1ce8905 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 10 Nov 2025 12:52:07 +0100 Subject: [PATCH 05/10] Lint fixes --- phpcs.xml.dist | 1 + src/Ability_Category_Command.php | 2 +- src/Ability_Command.php | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 2df6f26d..91583809 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -69,6 +69,7 @@ */src/Site(_Meta|_Option)?_Command\.php$ */src/Term(_Meta)?_Command\.php$ */src/User(_Application_Password|_Meta|_Session|_Term)?_Command\.php$ + */src/Ability(_Category)?_Command\.php$ diff --git a/src/Ability_Category_Command.php b/src/Ability_Category_Command.php index 95d2af91..c0492065 100644 --- a/src/Ability_Category_Command.php +++ b/src/Ability_Category_Command.php @@ -85,7 +85,7 @@ public function list_( $args, $assoc_args ) { if ( function_exists( 'wp_get_ability_categories' ) ) { $categories = wp_get_ability_categories(); } elseif ( function_exists( 'wp_ability_categories' ) ) { - $registry = wp_ability_categories(); + $registry = wp_ability_categories(); $categories = $registry->get_all(); } else { $categories = array(); diff --git a/src/Ability_Command.php b/src/Ability_Command.php index 76d90195..c3475be5 100644 --- a/src/Ability_Command.php +++ b/src/Ability_Command.php @@ -105,7 +105,7 @@ public function list_( $args, $assoc_args ) { if ( function_exists( 'wp_get_abilities' ) ) { $abilities = wp_get_abilities(); } elseif ( function_exists( 'wp_abilities' ) ) { - $registry = wp_abilities(); + $registry = wp_abilities(); $abilities = $registry->get_all(); } else { $abilities = array(); From 8473bbbe8fb4a90f5d362ccdccebe77eba3a3d73 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 10 Nov 2025 12:53:09 +0100 Subject: [PATCH 06/10] Fix conditional --- entity-command.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/entity-command.php b/entity-command.php index 2dfe9513..7577b605 100644 --- a/entity-command.php +++ b/entity-command.php @@ -98,7 +98,7 @@ 'Ability_Command', array( 'before_invoke' => function () { - if ( Utils\wp_version_compare( '6.9', '<' ) ) { + if ( Utils\wp_version_compare( '6.8', '<=' ) ) { WP_CLI::error( 'Requires WordPress 6.9 or greater.' ); } }, @@ -109,7 +109,7 @@ 'Ability_Category_Command', array( 'before_invoke' => function () { - if ( Utils\wp_version_compare( '6.9', '<' ) ) { + if ( Utils\wp_version_compare( '6.8', '<=' ) ) { WP_CLI::error( 'Requires WordPress 6.9 or greater.' ); } }, From bac8b0f4b283126c9d9a6f42e7e1e8dc50997ea7 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 10 Nov 2025 13:56:54 +0100 Subject: [PATCH 07/10] Fix tests --- features/ability-category.feature | 57 +++++++------- features/ability.feature | 123 +++++++++++++----------------- src/Ability_Category_Command.php | 21 ++--- src/Ability_Command.php | 54 +++---------- 4 files changed, 97 insertions(+), 158 deletions(-) diff --git a/features/ability-category.feature b/features/ability-category.feature index 853713ca..9d9df912 100644 --- a/features/ability-category.feature +++ b/features/ability-category.feature @@ -14,38 +14,39 @@ Feature: Manage WordPress ability categories @require-wp-6.9 Scenario: List ability categories + When I run `wp ability category list --format=count` + Then save STDOUT as {COUNT} + Given a wp-content/mu-plugins/test-ability-categories.php file: """ 'First test category', 'description' => 'First test category', ) ); - - wp_register_ability_category( 'test_category_2', array( + + wp_register_ability_category( 'test-category-2', array( + 'label' => 'Second test category', 'description' => 'Second test category', ) ); } ); """ When I run `wp ability category list --format=count` - Then STDOUT should contain: + Then STDOUT should not contain: """ - 2 + {COUNT} """ - When I run `wp ability category list --fields=name,description --format=csv` + When I run `wp ability category list --fields=slug,description --format=csv` Then STDOUT should contain: """ - test_category_1,"First test category" + test-category-1,"First test category" """ And STDOUT should contain: """ - test_category_2,"Second test category" + test-category-2,"Second test category" """ @require-wp-6.9 @@ -53,13 +54,10 @@ Feature: Manage WordPress ability categories Given a wp-content/mu-plugins/test-ability-categories.php file: """ 'Content operations category', + add_action( 'wp_abilities_api_categories_init', function() { + wp_register_ability_category( 'content', array( + 'label' => 'Content category', + 'description' => 'Content category', ) ); } ); """ @@ -71,16 +69,16 @@ Feature: Manage WordPress ability categories """ And the return code should be 1 - When I run `wp ability category get content_ops --field=description` + When I run `wp ability category get content --field=description` Then STDOUT should be: """ - Content operations category + Content category """ - When I run `wp ability category get content_ops --field=name` + When I run `wp ability category get content --field=slug` Then STDOUT should be: """ - content_ops + content """ @require-wp-6.9 @@ -88,18 +86,15 @@ Feature: Manage WordPress ability categories Given a wp-content/mu-plugins/test-ability-categories.php file: """ 'This category exists', 'description' => 'This category exists', ) ); } ); """ - When I try `wp ability category exists existing_category` + When I try `wp ability category exists existing-category` Then the return code should be 0 When I try `wp ability category exists non_existent_category` diff --git a/features/ability.feature b/features/ability.feature index b769e05b..3c1b9216 100644 --- a/features/ability.feature +++ b/features/ability.feature @@ -14,18 +14,19 @@ Feature: Manage WordPress abilities @require-wp-6.9 Scenario: List abilities + When I run `wp ability list --format=count` + Then save STDOUT as {ABILITIES_COUNT} + Given a wp-content/mu-plugins/test-abilities.php file: """ 'content', + add_action( 'wp_abilities_api_init', function() { + wp_register_ability( 'my-plugin/test-ability-1', array( + 'label' => 'Test Ability 1', + 'category' => 'site', 'description' => 'Test ability one', - 'callback' => function( $input ) { + 'permission_callback' => '__return_true', + 'execute_callback' => function( $input ) { return array( 'result' => 'success', 'input' => $input ); }, 'input_schema' => array( @@ -38,11 +39,13 @@ Feature: Manage WordPress abilities 'type' => 'object', ), ) ); - - wp_register_ability( 'test_ability_2', array( - 'category' => 'users', + + wp_register_ability( 'my-plugin/test-ability-2', array( + 'label' => 'Test Ability 2', + 'category' => 'user', 'description' => 'Test ability two', - 'callback' => function( $input ) { + 'permission_callback' => '__return_true', + 'execute_callback' => function( $input ) { return array( 'result' => 'done' ); }, 'input_schema' => array( 'type' => 'object' ), @@ -52,25 +55,19 @@ Feature: Manage WordPress abilities """ When I run `wp ability list --format=count` - Then STDOUT should contain: + Then STDOUT should not contain: """ - 2 + {ABILITIES_COUNT} """ When I run `wp ability list --fields=name,category,description --format=csv` Then STDOUT should contain: """ - test_ability_1,content,"Test ability one" + my-plugin/test-ability-1,site,"Test ability one" """ And STDOUT should contain: """ - test_ability_2,users,"Test ability two" - """ - - When I run `wp ability list --category=content --format=count` - Then STDOUT should contain: - """ - 1 + my-plugin/test-ability-2,user,"Test ability two" """ @require-wp-6.9 @@ -78,15 +75,13 @@ Feature: Manage WordPress abilities Given a wp-content/mu-plugins/test-abilities.php file: """ 'content', + add_action( 'wp_abilities_api_init', function() { + wp_register_ability( 'my-plugin/get-test-post', array( + 'label' => 'Get Test Post', + 'category' => 'site', 'description' => 'Gets a test post', - 'callback' => function( $input ) { + 'permission_callback' => '__return_true', + 'execute_callback' => function( $input ) { return array( 'id' => $input['id'], 'title' => 'Test Post' ); }, 'input_schema' => array( @@ -109,13 +104,13 @@ Feature: Manage WordPress abilities """ And the return code should be 1 - When I run `wp ability get get_test_post --field=category` + When I run `wp ability get my-plugin/get-test-post --field=category` Then STDOUT should be: """ - content + site """ - When I run `wp ability get get_test_post --field=description` + When I run `wp ability get my-plugin/get-test-post --field=description` Then STDOUT should be: """ Gets a test post @@ -126,15 +121,13 @@ Feature: Manage WordPress abilities Given a wp-content/mu-plugins/test-abilities.php file: """ 'content', + add_action( 'wp_abilities_api_init', function() { + wp_register_ability( 'my-plugin/test-exists', array( + 'label' => 'Test Exists', + 'category' => 'site', 'description' => 'Test exists', - 'callback' => function( $input ) { + 'permission_callback' => '__return_true', + 'execute_callback' => function( $input ) { return array( 'result' => 'ok' ); }, 'input_schema' => array( 'type' => 'object' ), @@ -143,10 +136,10 @@ Feature: Manage WordPress abilities } ); """ - When I try `wp ability exists test_exists` + When I try `wp ability exists my-plugin/test-exists` Then the return code should be 0 - When I try `wp ability exists non_existent_ability` + When I try `wp ability exists non-existent-ability` Then the return code should be 1 @require-wp-6.9 @@ -154,15 +147,13 @@ Feature: Manage WordPress abilities Given a wp-content/mu-plugins/test-abilities.php file: """ 'testing', + add_action( 'wp_abilities_api_init', function() { + wp_register_ability( 'my-plugin/echo-input', array( + 'label' => 'Echo Input', + 'category' => 'site', 'description' => 'Echoes input', - 'callback' => function( $input ) { + 'permission_callback' => '__return_true', + 'execute_callback' => function( $input ) { return array( 'echoed' => $input ); }, 'input_schema' => array( 'type' => 'object' ), @@ -171,14 +162,14 @@ Feature: Manage WordPress abilities } ); """ - When I try `wp ability execute non_existent_ability '{"test": "data"}'` + When I try `wp ability execute non-existent-ability '{"test": "data"}'` Then STDERR should contain: """ - Error: Ability non_existent_ability doesn't exist. + Error: Ability non-existent-ability doesn't exist. """ And the return code should be 1 - When I run `wp ability execute echo_input '{"message": "hello"}'` + When I run `wp ability execute my-plugin/echo-input '{"message": "hello"}'` Then STDOUT should contain: """ "echoed" @@ -191,25 +182,19 @@ Feature: Manage WordPress abilities """ "hello" """ - And STDOUT should contain: - """ - Success: Ability executed successfully. - """ @require-wp-6.9 Scenario: Execute an ability with input from STDIN Given a wp-content/mu-plugins/test-abilities.php file: """ 'testing', + add_action( 'wp_abilities_api_init', function() { + wp_register_ability( 'my-plugin/process-input', array( + 'label' => 'Process Input', + 'category' => 'site', 'description' => 'Processes input', - 'callback' => function( $input ) { + 'permission_callback' => '__return_true', + 'execute_callback' => function( $input ) { return array( 'processed' => true, 'data' => $input ); }, 'input_schema' => array( 'type' => 'object' ), @@ -218,7 +203,7 @@ Feature: Manage WordPress abilities } ); """ - When I run `echo '{"value": 42}' | wp ability execute process_input` + When I run `echo '{"value": 42}' | wp ability execute my-plugin/process-input` Then STDOUT should contain: """ "processed": true @@ -227,7 +212,3 @@ Feature: Manage WordPress abilities """ "value": 42 """ - And STDOUT should contain: - """ - Success: Ability executed successfully. - """ diff --git a/src/Ability_Category_Command.php b/src/Ability_Category_Command.php index c0492065..e033fe33 100644 --- a/src/Ability_Category_Command.php +++ b/src/Ability_Category_Command.php @@ -13,14 +13,14 @@ * # List all registered ability categories * $ wp ability category list --format=table * +----------+-------------+ - * | name | description | + * | slug | description | * +----------+-------------+ * | content | Content operations | * +----------+-------------+ * * # Get details about a specific category * $ wp ability category get content --format=json - * {"name":"content","description":"Content operations"} + * {"slug":"content","description":"Content operations"} * * # Check if a category exists * $ wp ability category exists content @@ -32,7 +32,8 @@ class Ability_Category_Command extends WP_CLI_Command { private $fields = array( - 'name', + 'slug', + 'label', 'description', ); @@ -82,14 +83,7 @@ public function list_( $args, $assoc_args ) { $formatter = $this->get_formatter( $assoc_args ); // Get all registered ability categories - if ( function_exists( 'wp_get_ability_categories' ) ) { - $categories = wp_get_ability_categories(); - } elseif ( function_exists( 'wp_ability_categories' ) ) { - $registry = wp_ability_categories(); - $categories = $registry->get_all(); - } else { - $categories = array(); - } + $categories = wp_get_ability_categories(); if ( empty( $categories ) ) { $categories = array(); @@ -222,8 +216,9 @@ public function exists( $args ) { */ private function format_category_for_output( $category ) { $data = array( - 'name' => $category->name, - 'description' => $category->description, + 'slug' => $category->get_slug(), + 'label' => $category->get_label(), + 'description' => $category->get_description(), ); return $data; diff --git a/src/Ability_Command.php b/src/Ability_Command.php index c3475be5..20c51d96 100644 --- a/src/Ability_Command.php +++ b/src/Ability_Command.php @@ -171,9 +171,9 @@ function ( $item ) use ( $assoc_args, $filter_keys ) { * These fields will be displayed by default for the specified ability: * * * name - * * category + * * label * * description - * * callback + * * category * * input_schema * * output_schema * @@ -298,17 +298,9 @@ public function execute( $args, $assoc_args ) { } } - // Execute the ability - // First try wp_execute_ability if it exists, otherwise call the callback directly - if ( function_exists( 'wp_execute_ability' ) ) { - $result = wp_execute_ability( $ability_name, $input ); - } else { - $ability = wp_get_ability( $ability_name ); - if ( ! $ability || ! is_callable( $ability->callback ) ) { - WP_CLI::error( "Cannot execute ability {$ability_name}." ); - } - $result = call_user_func( $ability->callback, $input ); - } + $ability = wp_get_ability( $ability_name ); + + $result = $ability->execute( $input ); if ( is_wp_error( $result ) ) { WP_CLI::error( $result->get_error_message() ); @@ -329,8 +321,6 @@ public function execute( $args, $assoc_args ) { } } } - - WP_CLI::success( 'Ability executed successfully.' ); } /** @@ -341,39 +331,17 @@ public function execute( $args, $assoc_args ) { */ private function format_ability_for_output( $ability ) { $data = array( - 'name' => $ability->name, - 'category' => $ability->category, - 'description' => $ability->description, - 'callback' => $this->format_callback( $ability->callback ), - 'input_schema' => wp_json_encode( $ability->input_schema ), - 'output_schema' => wp_json_encode( $ability->output_schema ), + 'name' => $ability->get_name(), + 'label' => $ability->get_label(), + 'description' => $ability->get_description(), + 'category' => $ability->get_category(), + 'input_schema' => wp_json_encode( $ability->get_input_schema() ), + 'output_schema' => wp_json_encode( $ability->get_output_schema() ), ); return $data; } - /** - * Formats a callback for display. - * - * @param callable $callback The callback to format. - * @return string Formatted callback string. - */ - private function format_callback( $callback ) { - if ( is_string( $callback ) ) { - return $callback; - } elseif ( is_array( $callback ) && count( $callback ) === 2 ) { - if ( is_object( $callback[0] ) ) { - return get_class( $callback[0] ) . '::' . $callback[1]; - } else { - return $callback[0] . '::' . $callback[1]; - } - } elseif ( $callback instanceof Closure ) { - return '{closure}'; - } else { - return '{callable}'; - } - } - private function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->fields, 'ability' ); } From e7568c16c5b83a23c5f0ed2d04d05d2e26b5b016 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 10 Nov 2025 13:59:52 +0100 Subject: [PATCH 08/10] Fix indentation --- features/ability.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/ability.feature b/features/ability.feature index 3c1b9216..54917486 100644 --- a/features/ability.feature +++ b/features/ability.feature @@ -15,7 +15,7 @@ Feature: Manage WordPress abilities @require-wp-6.9 Scenario: List abilities When I run `wp ability list --format=count` - Then save STDOUT as {ABILITIES_COUNT} + Then save STDOUT as {ABILITIES_COUNT} Given a wp-content/mu-plugins/test-abilities.php file: """ From ab792ae4e7f747bcf85f59482fbc387353b4f9e0 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 10 Nov 2025 16:08:00 +0100 Subject: [PATCH 09/10] Adjust condition again --- entity-command.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/entity-command.php b/entity-command.php index 7577b605..a2801a7e 100644 --- a/entity-command.php +++ b/entity-command.php @@ -98,7 +98,7 @@ 'Ability_Command', array( 'before_invoke' => function () { - if ( Utils\wp_version_compare( '6.8', '<=' ) ) { + if ( Utils\wp_version_compare( '6.9-beta1', '<' ) ) { WP_CLI::error( 'Requires WordPress 6.9 or greater.' ); } }, @@ -109,7 +109,7 @@ 'Ability_Category_Command', array( 'before_invoke' => function () { - if ( Utils\wp_version_compare( '6.8', '<=' ) ) { + if ( Utils\wp_version_compare( '6.9-beta1', '<' ) ) { WP_CLI::error( 'Requires WordPress 6.9 or greater.' ); } }, From fba102f6699ffdac72c2edd30923480633887d6e Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 7 Dec 2025 10:27:38 +0100 Subject: [PATCH 10/10] PHPStan fixes --- src/Ability_Command.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Ability_Command.php b/src/Ability_Command.php index 20c51d96..7ecf4a6d 100644 --- a/src/Ability_Command.php +++ b/src/Ability_Command.php @@ -298,6 +298,11 @@ public function execute( $args, $assoc_args ) { } } + /** + * Existence is checked above with wp_has_ability(). + * + * @var \WP_Ability $ability + */ $ability = wp_get_ability( $ability_name ); $result = $ability->execute( $input ); @@ -310,10 +315,10 @@ public function execute( $args, $assoc_args ) { $format = isset( $assoc_args['format'] ) ? $assoc_args['format'] : 'json'; if ( 'json' === $format ) { - WP_CLI::line( wp_json_encode( $result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); + WP_CLI::line( (string) wp_json_encode( $result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } elseif ( 'yaml' === $format ) { // Convert to YAML-like output - foreach ( $result as $key => $value ) { + foreach ( (array) $result as $key => $value ) { if ( is_array( $value ) || is_object( $value ) ) { WP_CLI::line( $key . ': ' . wp_json_encode( $value ) ); } else {