diff --git a/composer.json b/composer.json index 2ce784e..149ea6b 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "ai", "ai check", "ai generate", + "ai is-supported", "ai status", "connectors", "connectors get", diff --git a/features/generate.feature b/features/generate.feature index f9d2a42..8bbb1ae 100644 --- a/features/generate.feature +++ b/features/generate.feature @@ -7,6 +7,9 @@ Feature: Generate AI content providerMetadata(), + $this->metadata(), + [ 'provider' => 'wp-cli-mock-provider' ] + ); + } + } class WP_CLI_Mock_Provider implements ProviderInterface { @@ -144,12 +185,10 @@ Feature: Generate AI content } } - WP_CLI::add_hook( - 'ai_client_init', + WP_CLI::add_wp_hook( + 'init', static function () { AiClient::defaultRegistry()->registerProvider( WP_CLI_Mock_Provider::class ); - - ( new API_Credentials_Manager() )->initialize(); } ); """ @@ -196,3 +235,42 @@ Feature: Generate AI content """ Top-k must be a positive integer """ + + @require-wp-7.0 + Scenario: Generate fails when AI is disabled + Given a wp-content/mu-plugins/disable-ai.php file: + """ + get_error_message() ); + } + if ( isset( $assoc_args['provider'] ) ) { $builder = $builder->using_provider( $assoc_args['provider'] ); } @@ -185,6 +195,32 @@ public function generate( $args, $assoc_args ) { } } + /** + * Checks whether AI features are supported in the current environment. + * + * Exits with code 0 if AI features are supported, or code 1 if they are not. + * + * ## EXAMPLES + * + * # Check if AI is supported + * $ wp ai is-supported + * Success: AI features are supported. + * + * @subcommand is-supported + * + * @param string[] $args Positional arguments. Unused. + * @param string[] $assoc_args Associative arguments. Unused. + * @return void + */ + public function is_supported( $args, $assoc_args ) { + // @phpstan-ignore function.notFound + if ( wp_supports_ai() ) { + WP_CLI::halt( 0 ); + } else { + WP_CLI::halt( 1 ); + } + } + /** * Checks if a prompt is supported for generation. * @@ -217,10 +253,19 @@ public function check( $args, $assoc_args ) { list( $prompt ) = $args; $type = $assoc_args['type'] ?? 'text'; + // @phpstan-ignore function.notFound + if ( ! wp_supports_ai() ) { + WP_CLI::error( 'AI features are not supported in this environment.' ); + } + try { // @phpstan-ignore function.notFound $builder = wp_ai_client_prompt( $prompt ); + if ( is_wp_error( $builder ) ) { + WP_CLI::error( $builder->get_error_message() ); + } + if ( 'text' === $type ) { $supported = $builder->is_supported_for_text_generation(); if ( $supported ) { @@ -267,7 +312,7 @@ public function check( $args, $assoc_args ) { * # Check AI status * $ wp ai status * +------------------+-----------+ - * | Capability | Supported | + * | capability | supported | * +------------------+-----------+ * | Text Generation | Yes | * | Image Generation | No | @@ -282,6 +327,10 @@ public function status( $args, $assoc_args ) { // @phpstan-ignore function.notFound $builder = wp_ai_client_prompt(); + if ( is_wp_error( $builder ) ) { + WP_CLI::error( $builder->get_error_message() ); + } + // Check each capability $capabilities = array( array( @@ -292,6 +341,26 @@ public function status( $args, $assoc_args ) { 'capability' => 'Image Generation', 'supported' => $builder->is_supported_for_image_generation() ? 'Yes' : 'No', ), + array( + 'capability' => 'Text to Speech Generation', + 'supported' => $builder->is_supported_for_text_to_speech_conversion() ? 'Yes' : 'No', + ), + array( + 'capability' => 'Video Generation', + 'supported' => $builder->is_supported_for_video_generation() ? 'Yes' : 'No', + ), + array( + 'capability' => 'Speech Generation', + 'supported' => $builder->is_supported_for_speech_generation() ? 'Yes' : 'No', + ), + array( + 'capability' => 'Music Generation', + 'supported' => $builder->is_supported_for_music_generation() ? 'Yes' : 'No', + ), + array( + 'capability' => 'Embedding Generation', + 'supported' => $builder->is_supported_for_embedding_generation() ? 'Yes' : 'No', + ), ); $format = $assoc_args['format'] ?? 'table'; @@ -321,6 +390,10 @@ private function generate_text( $builder, $assoc_args ) { // @phpstan-ignore class.notFound $text = $builder->generate_text_result(); + if ( is_wp_error( $text ) ) { + WP_CLI::error( $text ); + } + if ( 'json' === $format ) { $json = json_encode( array( 'text' => $text->toText() ) ); if ( false === $json ) { @@ -380,6 +453,10 @@ private function generate_image( $builder, $assoc_args ) { // @phpstan-ignore class.notFound $image_file = $builder->generate_image(); + if ( is_wp_error( $image_file ) ) { + WP_CLI::error( $image_file ); + } + if ( isset( $assoc_args['destination-file'] ) ) { $output_path = $assoc_args['destination-file']; $output_path = realpath( dirname( $output_path ) ) . DIRECTORY_SEPARATOR . basename( $output_path ); @@ -406,7 +483,7 @@ private function generate_image( $builder, $assoc_args ) { } WP_CLI::success( 'Image saved to ' . $output_path ); - } elseif ( $assoc_args['stdout'] ) { + } elseif ( Utils\get_flag_value( $assoc_args, 'stdout', false ) ) { $data_uri = $image_file->getDataUri(); $data_parts = $data_uri ? explode( ',', $data_uri, 2 ) : []; @@ -424,7 +501,6 @@ private function generate_image( $builder, $assoc_args ) { WP_CLI::log( $image_data ); } else { - WP_CLI::success( 'Image generated:' ); WP_CLI::line( (string) $image_file->getDataUri() ); } }