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..a2801a7e 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-beta1', '<' ) ) { + 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-beta1', '<' ) ) { + 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..9d9df912 --- /dev/null +++ b/features/ability-category.feature @@ -0,0 +1,101 @@ +Feature: Manage WordPress ability categories + + Background: + Given a WP install + + @less-than-wp-6.9 + Scenario: Ability category commands require WordPress 6.9+ + 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 + 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( + 'label' => 'Second test category', + 'description' => 'Second test category', + ) ); + } ); + """ + + When I run `wp ability category list --format=count` + Then STDOUT should not contain: + """ + {COUNT} + """ + + When I run `wp ability category list --fields=slug,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 category', + 'description' => 'Content 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 --field=description` + Then STDOUT should be: + """ + Content category + """ + + When I run `wp ability category get content --field=slug` + Then STDOUT should be: + """ + content + """ + + @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', + 'description' => '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..54917486 --- /dev/null +++ b/features/ability.feature @@ -0,0 +1,214 @@ +Feature: Manage WordPress abilities + + Background: + Given a WP install + + @less-than-wp-6.9 + Scenario: Ability commands require WordPress 6.9+ + 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 + When I run `wp ability list --format=count` + Then save STDOUT as {ABILITIES_COUNT} + + Given a wp-content/mu-plugins/test-abilities.php file: + """ + 'Test Ability 1', + 'category' => 'site', + 'description' => 'Test ability one', + 'permission_callback' => '__return_true', + 'execute_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( 'my-plugin/test-ability-2', array( + 'label' => 'Test Ability 2', + 'category' => 'user', + 'description' => 'Test ability two', + 'permission_callback' => '__return_true', + 'execute_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 not contain: + """ + {ABILITIES_COUNT} + """ + + When I run `wp ability list --fields=name,category,description --format=csv` + Then STDOUT should contain: + """ + my-plugin/test-ability-1,site,"Test ability one" + """ + And STDOUT should contain: + """ + my-plugin/test-ability-2,user,"Test ability two" + """ + + @require-wp-6.9 + Scenario: Get a specific ability + Given a wp-content/mu-plugins/test-abilities.php file: + """ + 'Get Test Post', + 'category' => 'site', + 'description' => 'Gets a test post', + 'permission_callback' => '__return_true', + 'execute_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 my-plugin/get-test-post --field=category` + Then STDOUT should be: + """ + site + """ + + When I run `wp ability get my-plugin/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: + """ + 'Test Exists', + 'category' => 'site', + 'description' => 'Test exists', + 'permission_callback' => '__return_true', + 'execute_callback' => function( $input ) { + return array( 'result' => 'ok' ); + }, + 'input_schema' => array( 'type' => 'object' ), + 'output_schema' => array( 'type' => 'object' ), + ) ); + } ); + """ + + 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` + 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: + """ + 'Echo Input', + 'category' => 'site', + 'description' => 'Echoes input', + 'permission_callback' => '__return_true', + 'execute_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 my-plugin/echo-input '{"message": "hello"}'` + Then STDOUT should contain: + """ + "echoed" + """ + And STDOUT should contain: + """ + "message" + """ + And STDOUT should contain: + """ + "hello" + """ + + @require-wp-6.9 + Scenario: Execute an ability with input from STDIN + Given a wp-content/mu-plugins/test-abilities.php file: + """ + 'Process Input', + 'category' => 'site', + 'description' => 'Processes input', + 'permission_callback' => '__return_true', + 'execute_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 my-plugin/process-input` + Then STDOUT should contain: + """ + "processed": true + """ + And STDOUT should contain: + """ + "value": 42 + """ 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 new file mode 100644 index 00000000..e033fe33 --- /dev/null +++ b/src/Ability_Category_Command.php @@ -0,0 +1,230 @@ +=] + * : 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 ); + + // Get all registered ability categories + $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( + 'slug' => $category->get_slug(), + 'label' => $category->get_label(), + 'description' => $category->get_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..20c51d96 --- /dev/null +++ b/src/Ability_Command.php @@ -0,0 +1,348 @@ +=] + * : 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 ); + + // 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(); + } + + $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 + * * label + * * description + * * category + * * 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() ); + } + } + + $ability = wp_get_ability( $ability_name ); + + $result = $ability->execute( $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 ) { + // 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 ); + } + } + } + } + + /** + * 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->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; + } + + private function get_formatter( &$assoc_args ) { + return new Formatter( $assoc_args, $this->fields, 'ability' ); + } +}