plugin = $plugin; } /** * Register CLI commands */ public static function register($plugin) { $instance = new self($plugin); WP_CLI::add_command('mls test', array($instance, 'test')); WP_CLI::add_command('mls status', array($instance, 'status')); WP_CLI::add_command('mls sync', array($instance, 'sync')); WP_CLI::add_command('mls stats', array($instance, 'stats')); WP_CLI::add_command('mls cache', array($instance, 'cache')); } /** * Test API connection and authentication. * * ## OPTIONS * * * : Test type: connection or auth * * [--verbose] * : Show detailed information * * ## EXAMPLES * * wp mls test connection * wp mls test auth * wp mls test connection --verbose * * @subcommand test */ public function test($args, $assoc_args) { $type = isset($args[0]) ? $args[0] : 'connection'; $verbose = isset($assoc_args['verbose']); $api_client = $this->plugin->get_api_client(); switch ($type) { case 'connection': WP_CLI::line('Testing connection to MLS Grid API...'); $result = $api_client->test_connection(); if ($result['success']) { WP_CLI::success('Connection successful!'); WP_CLI::line(sprintf('Response time: %dms', $result['response_time'])); if ($verbose && !empty($result['endpoints'])) { WP_CLI::line('Available endpoints:'); foreach ($result['endpoints'] as $endpoint) { WP_CLI::line(' - ' . $endpoint); } } } else { WP_CLI::error('Connection failed: ' . $result['error']); } break; case 'auth': WP_CLI::line('Testing API authentication...'); $options = $this->plugin->get_options(); if (!$options->get_api_token()) { WP_CLI::error('No API token configured. Set MLSGRID_ACCESS_TOKEN in wp-config.php'); } $result = $api_client->test_auth(); if ($result['success']) { WP_CLI::success('Authentication successful!'); WP_CLI::line('Originating System: ' . $result['originating_system']); } else { WP_CLI::error('Authentication failed: ' . $result['error']); } break; default: WP_CLI::error("Unknown test type: {$type}. Use 'connection' or 'auth'."); } } /** * Show sync status and rate limits. * * ## OPTIONS * * [] * : Status type: sync, rate-limits, or all (default) * * ## EXAMPLES * * wp mls status * wp mls status sync * wp mls status rate-limits * * @subcommand status */ public function status($args, $assoc_args) { $type = isset($args[0]) ? $args[0] : 'all'; if ($type === 'all' || $type === 'sync') { $this->show_sync_status(); } if ($type === 'all' || $type === 'rate-limits') { $this->show_rate_limits(); } } /** * Show sync status */ private function show_sync_status() { $sync_engine = $this->plugin->get_sync_engine(); $status = $sync_engine->get_status(); WP_CLI::line(''); WP_CLI::line('=== Sync Status ==='); if ($status['running_sync']) { WP_CLI::warning('Sync currently running'); WP_CLI::line(sprintf( ' Type: %s | Started: %s | Processed: %d', $status['running_sync']->sync_type, $status['running_sync']->started_at, $status['running_sync']->records_processed )); } if ($status['last_sync']) { WP_CLI::line('Last completed sync:'); WP_CLI::line(sprintf( ' Type: %s | Completed: %s', $status['last_sync']->sync_type, $status['last_sync']->completed_at )); WP_CLI::line(sprintf( ' Records: %d processed, %d created, %d updated, %d deleted', $status['last_sync']->records_processed, $status['last_sync']->records_created, $status['last_sync']->records_updated, $status['last_sync']->records_deleted )); } else { WP_CLI::line('No completed syncs found.'); } if ($status['last_failed']) { WP_CLI::warning('Last failed sync:'); WP_CLI::line(' Error: ' . $status['last_failed']->last_error); } WP_CLI::line(''); } /** * Show rate limit status */ private function show_rate_limits() { $rate_limiter = $this->plugin->get_rate_limiter(); $status = $rate_limiter->get_status(); WP_CLI::line('=== Rate Limits ==='); WP_CLI::line(sprintf( 'Hourly: %d / %d requests (%d remaining)', $status['hourly']['used'], $status['hourly']['limit'], $status['hourly']['remaining'] )); WP_CLI::line(sprintf( 'Daily: %d / %d requests (%d remaining)', $status['daily']['used'], $status['daily']['limit'], $status['daily']['remaining'] )); WP_CLI::line(sprintf( 'Data: %s / 4GB this hour', size_format($status['bytes_this_hour']) )); WP_CLI::line(''); } /** * Run property sync. * * ## OPTIONS * * * : Sync type: full, incremental, media, or resume * * [--dry-run] * : Show what would be synced without making changes * * [--limit=] * : Limit number of records to process * * [--id=] * : Sync state ID to resume (for resume command) * * [--force] * : Force re-download of media (for media command) * * ## EXAMPLES * * wp mls sync full * wp mls sync full --dry-run --limit=10 * wp mls sync incremental * wp mls sync media --limit=100 * wp mls sync resume --id=5 * * @subcommand sync */ public function sync($args, $assoc_args) { $type = isset($args[0]) ? $args[0] : 'incremental'; $dry_run = isset($assoc_args['dry-run']); $limit = isset($assoc_args['limit']) ? (int) $assoc_args['limit'] : null; $sync_engine = $this->plugin->get_sync_engine(); // Progress callback for CLI $progress = null; $progress_callback = function($stats) use (&$progress) { if ($progress) { $progress->tick(); } }; switch ($type) { case 'full': WP_CLI::line('Starting full sync...'); if ($dry_run) { WP_CLI::line('DRY RUN - No changes will be made'); } $result = $sync_engine->run_full_sync($dry_run, $limit, $progress_callback); $this->output_sync_result($result); break; case 'incremental': WP_CLI::line('Starting incremental sync...'); if ($dry_run) { WP_CLI::line('DRY RUN - No changes will be made'); } $result = $sync_engine->run_incremental_sync($dry_run, $progress_callback); $this->output_sync_result($result); break; case 'media': WP_CLI::line('Downloading pending media...'); $media_handler = $this->plugin->get_media_handler(); $result = $media_handler->download_pending($limit ?: 100); WP_CLI::line(sprintf( 'Media download complete: %d success, %d failed out of %d total', $result['success'], $result['failed'], $result['total'] )); if ($result['failed'] === 0 && $result['total'] > 0) { WP_CLI::success('All media downloaded successfully!'); } elseif ($result['total'] === 0) { WP_CLI::success('No pending media to download.'); } else { WP_CLI::warning('Some media failed to download.'); } break; case 'resume': $sync_id = isset($assoc_args['id']) ? (int) $assoc_args['id'] : null; if (!$sync_id) { WP_CLI::error('Please specify --id= to resume'); } WP_CLI::line("Resuming sync #{$sync_id}..."); $result = $sync_engine->resume_sync($sync_id, $progress_callback); $this->output_sync_result($result); break; default: WP_CLI::error("Unknown sync type: {$type}. Use 'full', 'incremental', 'media', or 'resume'."); } } /** * Output sync result */ private function output_sync_result($result) { if ($result['success']) { WP_CLI::success('Sync completed successfully!'); } else { WP_CLI::error('Sync failed: ' . $result['error']); } $stats = $result['stats']; WP_CLI::line(sprintf( 'Processed: %d | Created: %d | Updated: %d | Deleted: %d | Errors: %d', $stats['processed'], $stats['created'], $stats['updated'], $stats['deleted'], $stats['errors'] )); } /** * Show database statistics. * * ## EXAMPLES * * wp mls stats * * @subcommand stats */ public function stats($args, $assoc_args) { $db = $this->plugin->get_db(); $stats = $db->get_stats(); WP_CLI::line(''); WP_CLI::line('=== MLS Database Statistics ==='); WP_CLI::line(''); WP_CLI::line('Properties:'); WP_CLI::line(sprintf(' Total: %d', $stats['total_properties'])); WP_CLI::line(sprintf(' Active: %d', $stats['active_properties'])); WP_CLI::line(sprintf(' Pending: %d', $stats['pending_properties'])); WP_CLI::line(sprintf(' Sold: %d', $stats['sold_properties'])); WP_CLI::line(''); WP_CLI::line('Media:'); WP_CLI::line(sprintf(' Total records: %d', $stats['total_media'])); WP_CLI::line(sprintf(' Downloaded: %d', $stats['downloaded_media'])); WP_CLI::line(sprintf(' Pending: %d', $stats['total_media'] - $stats['downloaded_media'])); WP_CLI::line(''); // Show distinct values $query = $this->plugin->get_query(); $cities = $query->get_distinct_cities('Active'); WP_CLI::line('Cities with active listings: ' . count($cities)); if (count($cities) <= 20) { WP_CLI::line(' ' . implode(', ', $cities)); } WP_CLI::line(''); } /** * Manage cache. * * ## OPTIONS * * * : Action: clear, clear-listing, cleanup * * [--confirm] * : Confirm destructive operations * * [--listing=] * : Listing key for clear-listing * * ## EXAMPLES * * wp mls cache clear --confirm * wp mls cache clear-listing --listing=NST123456 * wp mls cache cleanup * * @subcommand cache */ public function cache($args, $assoc_args) { $action = isset($args[0]) ? $args[0] : null; switch ($action) { case 'clear': if (!isset($assoc_args['confirm'])) { WP_CLI::error('This will delete ALL synced data. Add --confirm to proceed.'); } WP_CLI::line('Clearing all MLS data...'); $db = $this->plugin->get_db(); $db->truncate_data(); // Also clear media files $media_handler = $this->plugin->get_media_handler(); $upload_dir = $media_handler->get_upload_dir(); if (is_dir($upload_dir)) { WP_CLI::line('Clearing media files...'); // Note: This is a simplified clear - in production you'd want more careful deletion $this->recursive_delete($upload_dir); wp_mkdir_p($upload_dir); } WP_CLI::success('Cache cleared successfully.'); break; case 'clear-listing': $listing_key = isset($assoc_args['listing']) ? $assoc_args['listing'] : null; if (!$listing_key) { WP_CLI::error('Please specify --listing='); } $media_handler = $this->plugin->get_media_handler(); $media_handler->delete_property_media($listing_key); global $wpdb; $db = $this->plugin->get_db(); $wpdb->delete($db->properties_table(), array('listing_key' => $listing_key)); WP_CLI::success("Listing {$listing_key} cleared."); break; case 'cleanup': WP_CLI::line('Cleaning up orphaned media files...'); $media_handler = $this->plugin->get_media_handler(); $deleted = $media_handler->cleanup_orphaned_files(); WP_CLI::success("Cleaned up {$deleted} orphaned directories."); break; default: WP_CLI::error("Unknown action: {$action}. Use 'clear', 'clear-listing', or 'cleanup'."); } } /** * Recursively delete a directory */ private function recursive_delete($dir) { if (!is_dir($dir)) { return; } $files = array_diff(scandir($dir), array('.', '..')); foreach ($files as $file) { $path = $dir . '/' . $file; if (is_dir($path)) { $this->recursive_delete($path); } else { unlink($path); } } rmdir($dir); } }