'Version', 'value' => AIRANKSUITE_VERSION ); $rows[] = array( 'item' => 'Modules booted', 'value' => (string) count( $kernel->moduleIds() ) ); $quarantine = get_option( 'airanksuite_quarantine', array() ); $rows[] = array( 'item' => 'Quarantined modules', 'value' => is_array( $quarantine ) && ! empty( $quarantine ) ? implode( ', ', array_keys( $quarantine ) ) : 'none', ); $i18n = Multilingual::state(); $rows[] = array( 'item' => 'Multilingual', 'value' => $i18n['multilingual'] ? sprintf( '%s (%d languages)', $i18n['label'], count( $i18n['languages'] ) ) : 'no', ); $run = (string) get_option( 'airanksuite_audit_last', '' ); $rows[] = array( 'item' => 'Last audit', 'value' => '' !== $run ? $run : 'never' ); $job = get_option( 'airanksuite_audit_job' ); if ( is_array( $job ) ) { $rows[] = array( 'item' => 'Audit job', 'value' => sprintf( '%s — %d/%d pages, %d findings', (string) $job['state'], (int) $job['done'], (int) $job['total'], (int) $job['issues'] ), ); } WP_CLI\Utils\format_items( 'table', $rows, array( 'item', 'value' ) ); } /** * Run the site audit to completion. * * ## OPTIONS * * [--post-type=] * : Comma-separated post types. Defaults to every public type. * * [--limit=] * : Maximum pages to queue. Default 5000. * * [--format=] * : Summary format. Options: table, json, csv, yaml. Default table. * * ## EXAMPLES * * wp airanksuite audit * wp airanksuite audit --post-type=product --limit=50000 * wp airanksuite audit --format=json > audit.json * * @when after_wp_load * * @param array $args Positional args. * @param array $assoc_args Flags. */ public function audit( array $args, array $assoc_args ): void { $auditor = Kernel::service( Auditor::class ); $types = array(); if ( ! empty( $assoc_args['post-type'] ) ) { $types = array_values( array_filter( array_map( 'trim', explode( ',', (string) $assoc_args['post-type'] ) ) ) ); } $limit = (int) ( $assoc_args['limit'] ?? 5000 ); $status = $auditor->start( $types, $limit ); if ( 'running' !== ( $status['state'] ?? '' ) ) { WP_CLI::error( 'Could not start the audit.' ); } $total = max( 1, (int) $status['total'] ); $progress = WP_CLI\Utils\make_progress_bar( sprintf( 'Auditing %d pages', $total ), $total ); $seen = 0; // The same tick() the browser calls, in a loop. One implementation. while ( 'running' === ( $status['state'] ?? '' ) ) { $status = $auditor->tick(); $done = (int) $status['done']; $progress->tick( max( 0, $done - $seen ) ); $seen = $done; } $progress->finish(); $report = $auditor->report(); WP_CLI::success( sprintf( 'Audit finished: score %d, %d critical, %d warnings, %d notices across %d pages.', (int) $report['score'], (int) $report['totals']['critical'], (int) $report['totals']['warning'], (int) $report['totals']['notice'], (int) $report['checked'] ) ); $rows = array(); foreach ( (array) $report['checks'] as $check ) { $rows[] = array( 'severity' => $check['severity'], 'check' => $check['label'], 'pages' => $check['count'], ); } if ( ! empty( $rows ) ) { WP_CLI\Utils\format_items( (string) ( $assoc_args['format'] ?? 'table' ), $rows, array( 'severity', 'check', 'pages' ) ); } } /** * Print the findings of the last audit. * * ## OPTIONS * * [--severity=] * : critical, warning or notice. * * [--group=] * : site, indexability, duplication, content, geo or commerce. * * [--per-page=] * : How many findings to print. Default 200. * * [--format=] * : table, json, csv or yaml. Default table. * * ## EXAMPLES * * wp airanksuite findings --severity=critical * wp airanksuite findings --group=commerce --format=csv > products.csv * * @when after_wp_load * * @param array $args Positional args. * @param array $assoc_args Flags. */ public function findings( array $args, array $assoc_args ): void { $auditor = Kernel::service( Auditor::class ); $report = $auditor->report( (string) ( $assoc_args['group'] ?? '' ), (string) ( $assoc_args['severity'] ?? '' ), 1, (int) ( $assoc_args['per-page'] ?? 200 ) ); if ( '' === (string) $report['run_id'] ) { WP_CLI::warning( 'No audit has been run yet. Try: wp airanksuite audit' ); return; } if ( empty( $report['items'] ) ) { WP_CLI::success( 'Nothing to fix on that filter.' ); return; } $rows = array(); foreach ( (array) $report['items'] as $item ) { $rows[] = array( 'severity' => $item['severity'], 'issue' => $item['check_label'], 'page' => $item['label'], 'detail' => $item['detail'], 'url' => $item['url'], ); } WP_CLI\Utils\format_items( (string) ( $assoc_args['format'] ?? 'table' ), $rows, array( 'severity', 'issue', 'page', 'detail', 'url' ) ); } /** * Re-analyze content — GEO and SEO scores, keywords, checks. * * ## OPTIONS * * [--post-type=] * : Comma-separated post types. Defaults to every public type. * * [--missing] * : Only pages that have never been analyzed. * * [--limit=] * : Maximum pages. Default 100000. * * ## EXAMPLES * * wp airanksuite analyze * wp airanksuite analyze --post-type=product --missing * * @when after_wp_load * * @param array $args Positional args. * @param array $assoc_args Flags. */ public function analyze( array $args, array $assoc_args ): void { $analyzer = Kernel::service( PostAnalyzer::class ); $types = ! empty( $assoc_args['post-type'] ) ? array_values( array_filter( array_map( 'trim', explode( ',', (string) $assoc_args['post-type'] ) ) ) ) : \AiRankSuite\Support\Inventory::eligible_types(); $query_args = array( 'post_type' => $types, 'post_status' => 'publish', 'posts_per_page' => (int) ( $assoc_args['limit'] ?? 100000 ), 'fields' => 'ids', 'orderby' => 'ID', 'order' => 'ASC', 'no_found_rows' => true, ); if ( isset( $assoc_args['missing'] ) ) { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query $query_args['meta_query'] = array( array( 'key' => '_airanksuite_analysis', 'compare' => 'NOT EXISTS', ), ); } $ids = ( new WP_Query( $query_args ) )->posts; if ( empty( $ids ) ) { WP_CLI::success( 'Nothing to analyze.' ); return; } $progress = WP_CLI\Utils\make_progress_bar( sprintf( 'Analyzing %d pages', count( $ids ) ), count( $ids ) ); $failed = 0; $index = 0; foreach ( $ids as $id ) { ++$index; $post = get_post( (int) $id ); if ( $post ) { try { $analyzer->analyze( $post ); } catch ( \Throwable $e ) { ++$failed; } } $progress->tick(); // A long-running CLI loop accumulates every post it touched in the // object cache and in $wpdb->queries; on 50,000 posts that is the // difference between finishing and an out-of-memory fatal. if ( 0 === $index % 500 ) { wp_cache_flush(); } } $progress->finish(); if ( $failed > 0 ) { WP_CLI::warning( sprintf( '%d pages could not be analyzed.', $failed ) ); } WP_CLI::success( sprintf( 'Analyzed %d pages.', count( $ids ) - $failed ) ); } /** * Scan every link on the site and report the broken ones. * * ## EXAMPLES * * wp airanksuite links * * @when after_wp_load */ public function links(): void { if ( ! class_exists( LinkScanner::class ) ) { \WP_CLI::error( 'The link checker is a Pro module and is not installed in this edition.' ); return; } $scanner = Kernel::service( LinkScanner::class ); $status = $scanner->start(); // The scanner runs in two phases — extract links from posts, then check // each URL — and only knows the URL count once extraction finishes, so // the bar is driven by posts and the URL total is reported at the end. $total = max( 1, (int) ( $status['total_posts'] ?? 1 ) ); $progress = WP_CLI\Utils\make_progress_bar( sprintf( 'Scanning %d pages for links', $total ), $total ); $seen = 0; while ( 'running' === ( $status['state'] ?? '' ) ) { $status = $scanner->tick(); $done = (int) ( $status['done_posts'] ?? 0 ); $progress->tick( max( 0, $done - $seen ) ); $seen = $done; if ( 'check' === ( $status['phase'] ?? '' ) ) { // Checking is network-bound and can take far longer than // extraction; a bar that sat at 100% would read as hung. WP_CLI::debug( sprintf( 'Checked %d of %d URLs', (int) $status['checked'], (int) $status['total_urls'] ) ); } } $progress->finish(); $totals = (array) ( $scanner->report()['totals'] ?? array() ); WP_CLI::success( sprintf( '%d links checked: %d healthy, %d broken, %d redirected, %d unreachable.', (int) ( $totals['total'] ?? 0 ), (int) ( $totals['healthy'] ?? 0 ), (int) ( $totals['broken'] ?? 0 ), (int) ( $totals['redirected'] ?? 0 ), (int) ( $totals['unreachable'] ?? 0 ) ) ); } /** * Apply an industry and market pack. * * ## OPTIONS * * [--pack=] * : Industry id. Omit, or pass "list", to print the available ids. * * [--region=] * : Market id. * * [--city=] * : Used to write local monitoring prompts. * * ## EXAMPLES * * wp airanksuite pack * wp airanksuite pack --pack=medical --region=gcc --city=Dubai * * @when after_wp_load * * @param array $args Positional args. * @param array $assoc_args Flags. */ public function pack( array $args, array $assoc_args ): void { $pack = (string) ( $assoc_args['pack'] ?? '' ); $region = (string) ( $assoc_args['region'] ?? '' ); if ( 'list' === $pack || '' === $pack ) { WP_CLI::line( 'Industries: ' . implode( ', ', Packs::ids() ) ); WP_CLI::line( 'Markets: ' . implode( ', ', Regions::ids() ) ); return; } if ( ! in_array( $pack, Packs::ids(), true ) ) { WP_CLI::error( sprintf( 'Unknown industry "%s". Known: %s', $pack, implode( ', ', Packs::ids() ) ) ); } if ( ! in_array( $region, Regions::ids(), true ) ) { WP_CLI::error( sprintf( 'Unknown market "%s". Known: %s', $region, implode( ', ', Regions::ids() ) ) ); } // Called directly rather than through rest_do_request(): on the command // line there is no logged-in user, so every permission_callback would // refuse. Shell access already outranks any capability check. $request = new \WP_REST_Request( 'POST', '/airanksuite/v1/packs/apply' ); $request->set_param( 'pack', $pack ); $request->set_param( 'region', $region ); $request->set_param( 'city', (string) ( $assoc_args['city'] ?? '' ) ); $module = new \AiRankSuite\Modules\BusinessPacks\BusinessPacksModule(); $module->boot( Kernel::instance()->container() ); $response = $module->apply( $request ); $data = $response->get_data(); foreach ( (array) $data['changed'] as $line ) { WP_CLI::line( ' + ' . $line ); } foreach ( (array) $data['kept'] as $line ) { WP_CLI::line( ' = ' . $line ); } WP_CLI::success( sprintf( 'Applied %s / %s.', $pack, $region ) ); } /** * Regenerate the llms.txt cache and ping the sitemap to search engines. * * ## EXAMPLES * * wp airanksuite flush * * @when after_wp_load */ public function flush(): void { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery $deleted = (int) $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE option_name LIKE %s OR option_name LIKE %s', $wpdb->options, $wpdb->esc_like( '_transient_airanksuite_' ) . '%', $wpdb->esc_like( '_transient_timeout_airanksuite_' ) . '%' ) ); // phpcs:enable flush_rewrite_rules( false ); WP_CLI::success( sprintf( 'Cleared %d cached entries and flushed rewrite rules.', $deleted ) ); } }