for all mutations. */ final class Cli { private function authority(): void { if ( ! current_user_can( 'manage_options' ) ) { \WP_CLI::error( 'An administrator is required. Pass --user=.' ); } } /** * Start a local scan and queue its first bounded batch. * * ## EXAMPLES * wp antivirus-ai scan --user=admin */ public function scan( array $args, array $assoc ): void { $this->authority(); try { $id = ( new Scanner( new Store() ) )->start(); Scheduler::scheduleTick( $id ); \WP_CLI::success( 'Scan queued: ' . $id ); } catch ( \Throwable $error ) { \WP_CLI::error( $error->getMessage() ); } } /** * Advance the active scan within a bounded number of batches. * * ## OPTIONS * [--id=] * : Server-issued scan ID. Defaults to the latest scan. * [--max-batches=] * : 1 to 20 batches. Defaults to 1; repeat from system cron until complete. */ public function run( array $args, array $assoc ): void { $this->authority(); try { $store = new Store(); $scanner = new Scanner( $store ); $scan = $scanner->latest(); $id = isset( $assoc['id'] ) ? (string) $assoc['id'] : ( $scan['_id'] ?? '' ); if ( ! preg_match( '/^[a-z0-9_]{1,64}$/D', $id ) ) { throw new \RuntimeException( 'Start a scan first or supply a valid server-issued ID.' ); } $limit = max( 1, min( 20, (int) ( $assoc['max-batches'] ?? 1 ) ) ); for ( $batch = 0; $batch < $limit; $batch++ ) { $before = $store->get( $id ); if ( ! $before || 'scan' !== ( $before['_kind'] ?? '' ) ) { throw new \RuntimeException( 'This saved scan is unavailable. Supply a retained scan ID.' ); } $result = Scheduler::batch( $id ); $scan = $store->get( $id ); if ( ! $scan || 'scan' !== ( $scan['_kind'] ?? '' ) ) { throw new \RuntimeException( 'This saved scan is unavailable after its bounded batch.' ); } if ( in_array( $result['state'] ?? '', array( 'waiting', 'stopped' ), true ) || ! in_array( $scan['_status'] ?? $scan['status'], array( 'queued', 'running' ), true ) ) { break; } } \WP_CLI::line( wp_json_encode( $scan, JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE ) ); } catch ( \Throwable $error ) { \WP_CLI::error( $error->getMessage() ); } } /** * Run bounded local and consented PRO background jobs from a host scheduler. * * ## OPTIONS * [--max-batches=] * : 1 to 20 local scan batches; defaults to 1. PRO queues advance one bounded batch each. * * ## EXAMPLES * wp antivirus-ai work --user=admin --max-batches=1 */ public function work( array $args, array $assoc ): void { $this->authority(); try { $result = Scheduler::work( max( 1, min( 20, (int) ( $assoc['max-batches'] ?? 1 ) ) ) ); \WP_CLI::line( wp_json_encode( $result, JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE ) ); } catch ( \Throwable $error ) { \WP_CLI::error( $error->getMessage() ); } } /** Print latest scan status as JSON. */ public function status( array $args, array $assoc ): void { $this->authority(); \WP_CLI::line( wp_json_encode( ( new Scanner( new Store() ) )->latest(), JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE ) ); } /** Cancel the active scan without deleting retained findings. */ public function cancel( array $args, array $assoc ): void { $this->authority(); try { $store = new Store(); $scanner = new Scanner( $store ); $scan = $scanner->latest(); if ( ! $scan ) { throw new \RuntimeException( 'There is no scan to cancel.' ); } $result = Scheduler::requestCancel( $scan['_id'] ); $after = $store->get( $scan['_id'] ); if ( ! $after || 'scan' !== ( $after['_kind'] ?? '' ) ) { throw new \RuntimeException( 'The saved scan is unavailable after its stop request.' ); } if ( ! empty( $result['cancel_pending'] ) || in_array( $after['_status'], array( 'queued', 'running' ), true ) ) { \WP_CLI::success( 'Stop request saved. The current batch can finish; remaining queued work will then stop. Evidence is retained.' ); } elseif ( 'cancelled' === $after['_status'] ) { \WP_CLI::success( 'Scan cancelled; evidence is retained.' ); } else { \WP_CLI::success( 'The scan is already terminal. Its saved evidence and result are unchanged.' ); } } catch ( \Throwable $error ) { \WP_CLI::error( $error->getMessage() ); } } /** Export retained findings as JSON to standard output; protect the destination. */ public function export( array $args, array $assoc ): void { $this->authority(); Admin::streamExport( new Store() ); } }