[--frontend=] [--name=] * Runs setup: diagnose, print the WordPress command that creates an * Application Password for an existing low-privilege user, set the * webhook URL. Outputs a ready-to-paste .env block. Hatch itself never * creates users or Application Passwords. * * wp hatch diagnose [--format=table|json] * Runs the preflight checks and prints results. * * wp hatch info [--format=table|json] * Prints detected plugins + site state. * * wp hatch revalidate [--reason=] * Manually fires the revalidation webhook. * * wp hatch env * Outputs the .env block for an Application Password you already have. * * Loaded only when WP_CLI is defined (see hatch.php bootstrap). * * @package Hatch */ defined( 'ABSPATH' ) || exit; if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) { return; } /** * Hatch CLI command group. */ class Hatch_CLI { /** * Full setup - diagnose, set webhook URL, print .env. Hatch does not create the Application Password; the command prints how to make one with WordPress core. * * ## OPTIONS * * [--frontend=] * : URL of your headless frontend (used for the revalidation webhook). * * [--name=] * : Friendly name to use in the suggested application-password command. * * [--frontend-user=] * : User name of the low-privilege (Subscriber) account the frontend reads as. Defaults to the current user. * * [--skip-diagnose] * : Skip the preflight diagnostic. * * ## EXAMPLES * * # Full setup in one command: * wp hatch setup --frontend=https://mysite.com * * # Name the application password in the suggested command: * wp hatch setup --frontend=https://mysite.com --name="Production frontend" --frontend-user=reader * * @param array $args Positional args. * @param array $assoc_args Flags. * @return void */ public function setup( $args, $assoc_args ) { unset( $args ); $frontend = isset( $assoc_args['frontend'] ) ? esc_url_raw( (string) $assoc_args['frontend'] ) : ''; $name = isset( $assoc_args['name'] ) ? sanitize_text_field( (string) $assoc_args['name'] ) : 'Hatch CLI Frontend'; $skip_dx = ! empty( $assoc_args['skip-diagnose'] ); WP_CLI::log( '' ); WP_CLI::log( WP_CLI::colorize( '%c╭─ Hatch Setup ───────────────────────────────────────%n' ) ); WP_CLI::log( WP_CLI::colorize( '%c│%n' ) ); WP_CLI::log( WP_CLI::colorize( sprintf( '%%c│%%n WordPress URL : %s', home_url() ) ) ); WP_CLI::log( WP_CLI::colorize( sprintf( '%%c│%%n Frontend URL : %s', '' !== (string) $frontend ? $frontend : '(skipping webhook)' ) ) ); WP_CLI::log( WP_CLI::colorize( '%c│%n' ) ); WP_CLI::log( WP_CLI::colorize( '%c╰─────────────────────────────────────────────────────%n' ) ); WP_CLI::log( '' ); // Step 1: Diagnose. if ( ! $skip_dx ) { WP_CLI::log( WP_CLI::colorize( '%y▶ Step 1/4: Running preflight diagnostic…%n' ) ); $report = Hatch_Diagnostic::run(); $this->print_diagnostic_inline( $report ); if ( 'fail' === $report['overall'] ) { WP_CLI::warning( 'Diagnostic failed. Fix blockers above, then re-run setup.' ); WP_CLI::halt( 1 ); } WP_CLI::log( '' ); } // Step 2: Hatch never creates application passwords. Point at WordPress's own command. WP_CLI::log( WP_CLI::colorize( '%y▶ Step 2/4: Application Password (you create it)%n' ) ); $user = $this->frontend_user( $assoc_args ); WP_CLI::log( sprintf( 'Create an Application Password for %s with WordPress itself:', $user->user_login ) ); WP_CLI::log( sprintf( ' wp user application-password create %s "%s" --porcelain', $user->user_login, $name ) ); WP_CLI::log( '' ); // Step 3: Set webhook URL (if provided). if ( '' !== $frontend ) { WP_CLI::log( '' ); WP_CLI::log( WP_CLI::colorize( '%y▶ Step 3/4: Configuring revalidation webhook…%n' ) ); $revalidate_url = rtrim( $frontend, '/' ) . '/api/revalidate'; update_option( 'hatch_revalidate_endpoint', esc_url_raw( $revalidate_url ) ); WP_CLI::success( sprintf( 'Webhook URL set to %s', $revalidate_url ) ); } else { WP_CLI::log( '' ); WP_CLI::log( WP_CLI::colorize( '%y▶ Step 3/4: Webhook URL (skipped - no --frontend flag)%n' ) ); } // Step 4: Print .env. WP_CLI::log( '' ); WP_CLI::log( WP_CLI::colorize( '%y▶ Step 4/4: Copy this into your frontend .env file:%n' ) ); WP_CLI::log( '' ); $webhook_secret = (string) get_option( 'hatch_webhook_secret', '' ); $lines = array( 'HATCH_WP_URL=' . home_url(), 'WORDPRESS_USER=' . $user->user_login, 'WORDPRESS_APP_PASSWORD=', 'HATCH_WEBHOOK_SECRET=' . $webhook_secret, ); WP_CLI::log( WP_CLI::colorize( '%g' . str_repeat( '─', 56 ) . '%n' ) ); foreach ( $lines as $l ) { WP_CLI::log( WP_CLI::colorize( '%g' . $l . '%n' ) ); } WP_CLI::log( WP_CLI::colorize( '%g' . str_repeat( '─', 56 ) . '%n' ) ); WP_CLI::log( '' ); WP_CLI::log( WP_CLI::colorize( '%c⚠ This is the only time the password is shown. Save it now.%n' ) ); WP_CLI::log( '' ); WP_CLI::success( 'Setup complete.' ); } /** * Run preflight diagnostic. * * ## OPTIONS * * [--format=] * : Output format. * --- * default: table * options: * - table * - json * --- * * ## EXAMPLES * * wp hatch diagnose * wp hatch diagnose --format=json * * @param array $args Positional args. * @param array $assoc_args Flags. * @return void */ public function diagnose( $args, $assoc_args ) { unset( $args ); $format = isset( $assoc_args['format'] ) ? (string) $assoc_args['format'] : 'table'; $report = Hatch_Diagnostic::run(); if ( 'json' === $format ) { WP_CLI::log( wp_json_encode( $report, JSON_PRETTY_PRINT ) ); WP_CLI::halt( 'fail' === $report['overall'] ? 1 : 0 ); } $this->print_diagnostic_inline( $report ); if ( 'fail' === $report['overall'] ) { WP_CLI::halt( 1 ); } } /** * Resolve the account the frontend reads as. Hatch never creates it: the * admin passes an existing low-privilege user, and accounts that can edit * or manage the site are refused. * * @param array $assoc_args WP-CLI associative args. * @return WP_User */ private function frontend_user( array $assoc_args ): WP_User { $login = isset( $assoc_args['frontend-user'] ) ? sanitize_user( (string) $assoc_args['frontend-user'], true ) : ''; if ( '' === $login ) { WP_CLI::error( 'Pass --frontend-user=: an existing Subscriber account. Hatch does not create users. Add one with: wp user create reader reader@example.com --role=subscriber' ); } $user = Hatch_Wp_Credential::check_account( $login ); if ( is_wp_error( $user ) ) { WP_CLI::error( $user->get_error_message() ); } return $user; } /** * Print diagnostic results as a nice CLI table. * * @param array $report Report from Hatch_Diagnostic::run(). * @return void */ private function print_diagnostic_inline( array $report ): void { foreach ( $report['checks'] as $c ) { switch ( $c['severity'] ) { case 'pass': $icon = WP_CLI::colorize( '%g✓%n' ); break; case 'warn': $icon = WP_CLI::colorize( '%y!%n' ); break; case 'fail': $icon = WP_CLI::colorize( '%r✕%n' ); break; default: $icon = '·'; } WP_CLI::log( sprintf( ' %s %s - %s', $icon, $c['label'], $c['message'] ) ); if ( ! empty( $c['fix'] ) && 'pass' !== $c['severity'] ) { WP_CLI::log( ' ' . WP_CLI::colorize( '%n→ ' ) . $c['fix'] ); } } WP_CLI::log( '' ); WP_CLI::log( sprintf( ' %s %d pass · %s %d warn · %s %d fail', WP_CLI::colorize( '%g✓%n' ), (int) $report['pass_count'], WP_CLI::colorize( '%y!%n' ), (int) $report['warn_count'], WP_CLI::colorize( '%r✕%n' ), (int) $report['fail_count'] ) ); } /** * Print plugin detection + site info. * * ## OPTIONS * * [--format=] * : Output format. * --- * default: table * options: * - table * - json * --- */ public function info( $args, $assoc_args ) { unset( $args ); $format = isset( $assoc_args['format'] ) ? (string) $assoc_args['format'] : 'table'; $data = array( 'hatch_version' => HATCH_VERSION, 'wp_version' => get_bloginfo( 'version' ), 'php_version' => PHP_VERSION, 'site_url' => home_url(), 'site_name' => get_bloginfo( 'name' ), 'detected' => Hatch_Detector::report(), 'webhook_url' => (string) get_option( 'hatch_revalidate_endpoint', '' ), 'has_secret' => '' !== (string) get_option( 'hatch_webhook_secret', '' ), ); if ( 'json' === $format ) { WP_CLI::log( wp_json_encode( $data, JSON_PRETTY_PRINT ) ); return; } WP_CLI::log( sprintf( 'Hatch %s on WordPress %s (PHP %s)', HATCH_VERSION, $data['wp_version'], PHP_VERSION ) ); WP_CLI::log( sprintf( 'Site: %s', home_url() ) ); WP_CLI::log( sprintf( 'Webhook: %s', ! empty( $data['webhook_url'] ) ? $data['webhook_url'] : '(not configured)' ) ); WP_CLI::log( '' ); WP_CLI::log( sprintf( 'SEO plugin : %s', $data['detected']['seo'] ) ); WP_CLI::log( sprintf( 'Forms : %s', ! empty( $data['detected']['forms'] ) ? implode( ', ', $data['detected']['forms'] ) : 'none' ) ); WP_CLI::log( sprintf( 'Custom fields : %s', $data['detected']['custom_fields'] ) ); WP_CLI::log( sprintf( 'CPT manager : %s', $data['detected']['cpt_manager'] ) ); WP_CLI::log( sprintf( 'i18n : %s', $data['detected']['i18n'] ) ); } /** * Manually trigger the revalidation webhook. * * ## OPTIONS * * [--reason=] * : Reason string sent in the payload (default: "cli-manual"). */ public function revalidate( $args, $assoc_args ) { unset( $args ); $reason = isset( $assoc_args['reason'] ) ? sanitize_text_field( (string) $assoc_args['reason'] ) : 'cli-manual'; $fired = Hatch_Revalidate::trigger( $reason ); if ( ! $fired ) { WP_CLI::error( 'Webhook not configured. Run: wp hatch setup --frontend=https://your-site.com' ); } WP_CLI::success( sprintf( 'Webhook fired (reason: %s).', $reason ) ); } /** * Print the frontend .env block (uses existing webhook secret; does not generate password). */ public function env( $args, $assoc_args ) { unset( $args, $assoc_args ); $user = wp_get_current_user(); if ( ! $user || ! $user->exists() ) { WP_CLI::error( 'Run as a user: wp --user= hatch env' ); } $secret = (string) get_option( 'hatch_webhook_secret', '' ); WP_CLI::log( 'HATCH_WP_URL=' . home_url() ); WP_CLI::log( 'WORDPRESS_USER=' . $user->user_login ); WP_CLI::log( 'WORDPRESS_APP_PASSWORD=' ); WP_CLI::log( 'HATCH_WEBHOOK_SECRET=' . $secret ); } } WP_CLI::add_command( 'hatch', 'Hatch_CLI' );