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' );
+ }
+}