environment('production')) { throw new \RuntimeException('FATAL: rsx:debug command is not available in production environment. This is a development-only debugging tool.'); } // Check if --examples was requested if ($this->option('examples')) { $this->showExamples(); return 0; } // Get the URL to debug $url = $this->argument('url'); // If no URL provided, show help if (!$url) { $this->error('No URL provided. Use --examples to see usage examples.'); return 1; } // Check if console_debug is disabled globally and user didn't override $console_debug_enabled = config('rsx.console_debug.enabled', false) || env('CONSOLE_DEBUG_ENABLED') === 'true'; $console_debug_override = $this->option('console') || $this->option('console-log') || $this->option('console-list') || $this->option('console-debug-all') || $this->option('console-debug-filter') || $this->option('console-debug-benchmark') || env('CONSOLE_DEBUG_FILTER') || env('CONSOLE_DEBUG_BENCHMARK'); $console_debug_disabled = $this->option('console-debug-disable') || env('CONSOLE_DEBUG_ENABLED') === 'false'; // If console_debug is disabled and not overridden, show a single line message if (!$console_debug_enabled && !$console_debug_override && !$console_debug_disabled) { $this->line('console_debug is disabled. Run `php artisan rsx:man console_debug` for more information on its usage.'); // Don't return early - still run the test, just with the message } // Ensure URL starts with / if (!str_starts_with($url, '/')) { $url = '/' . $url; } // Get user ID from options (accepts ID or email) $user_id = $this->option('user'); if ($user_id !== null) { $user_id = $this->resolve_user($user_id); if ($user_id === null) { return 1; // Error already displayed } } // Get log flag $show_log = $this->option('log'); // Get no-body flag $no_body = $this->option('no-body'); // Get follow-redirects flag $follow_redirects = $this->option('follow-redirects'); // Get headers flag $headers = $this->option('headers'); // Get console flag (--console or --console-log alias) $console_log = $this->option('console') || $this->option('console-log'); // Get xhr-dump flag $xhr_dump = $this->option('xhr-dump'); // Get input-elements flag $input_elements = $this->option('input-elements'); // Get POST data $post_data = $this->option('post'); // Get cookies flag $cookies = $this->option('cookies'); // Get wait-for selector $wait_for = $this->option('wait-for'); // Get all-logs flag $all_logs = $this->option('all-logs'); // Get new feature flags $expect_element = $this->option('expect-element'); $dump_element = $this->option('dump-element'); $storage = $this->option('storage'); $xhr_list = $this->option('xhr-list'); $full = $this->option('full'); $eval_code = $this->option('eval'); // Get timeout option and validate $timeout = $this->option('timeout'); if ($timeout !== null) { $timeout = intval($timeout); if ($timeout < 30000) { $this->error('❌ Timeout value is in milliseconds and must be no less than 30000 milliseconds (30 seconds)'); return 1; } } else { $timeout = 30000; // Default 30 seconds } // Get screenshot options $screenshot_width = $this->option('screenshot-width'); $screenshot_path = $this->option('screenshot-path'); // Get dump-dimensions option $dump_dimensions = $this->option('dump-dimensions'); // Get console debug options (with environment variable fallbacks) $console_debug_filter = $this->option('console-debug-filter') ?: env('CONSOLE_DEBUG_FILTER'); $console_debug_benchmark = $this->option('console-debug-benchmark') ?: env('CONSOLE_DEBUG_BENCHMARK', false); $console_debug_all = $this->option('console-debug-all') ?: (env('CONSOLE_DEBUG_FILTER') === 'ALL'); $console_debug_disable = $this->option('console-debug-disable') ?: (env('CONSOLE_DEBUG_ENABLED') === 'false'); // Auto-enable console-log and console_debug when console-debug-filter is set if ($console_debug_filter && !$console_debug_disable) { $console_log = true; // Enable console log output // console_debug is enabled via the filter itself } // Auto-enable console_debug when CONSOLE_DEBUG_ENABLED=true if (env('CONSOLE_DEBUG_ENABLED') === 'true' && !$console_debug_disable) { // Enable console output if not explicitly disabled if (!$console_debug_filter && !$console_debug_all) { $console_debug_all = true; // Show all channels if no specific filter } } $console_list = $this->option('console-list'); // console-list is an alias for console-log if ($console_list) { $console_log = true; } // Rotate logs before test to ensure clean slate Debugger::logrotate(); // Check if Playwright script exists $playwright_script = base_path('bin/route-debug.js'); if (!file_exists($playwright_script)) { $this->error("❌ Playwright script not found: {$playwright_script}"); $this->error('Please create the script or check your installation'); return 1; } // Check if node/npm is available $node_check = new Process(['node', '--version']); $node_check->run(); if (!$node_check->isSuccessful()) { $this->error('❌ Node.js is not installed or not in PATH'); return 1; } // Check if playwright is installed $playwright_check = new Process(['node', '-e', "require('playwright')"], base_path()); $playwright_check->run(); if (!$playwright_check->isSuccessful()) { $this->warn('⚠️ Playwright not installed. Installing now...'); $npm_install = new Process(['npm', 'install', 'playwright'], base_path()); $npm_install->run(function ($type, $buffer) { echo $buffer; }); if (!$npm_install->isSuccessful()) { $this->error('❌ Failed to install Playwright'); return 1; } $this->info('✅ Playwright installed'); $this->info(''); } // Check if chromium browser is installed and up to date $browser_check_script = "const {chromium} = require('playwright'); chromium.launch({headless:true}).then(b => {b.close(); process.exit(0);}).catch(e => {console.error(e.message); process.exit(1);});"; $browser_check = new Process(['node', '-e', $browser_check_script], base_path(), $_ENV, null, 10); $browser_check->run(); if (!$browser_check->isSuccessful()) { $error_output = $browser_check->getErrorOutput() . $browser_check->getOutput(); // Check if it's a browser not installed or out of date error if (str_contains($error_output, "Executable doesn't exist") || str_contains($error_output, "browserType.launch") || str_contains($error_output, "Playwright was just installed or updated")) { $this->info('Installing/updating Chromium browser...'); $browser_install = new Process(['npx', 'playwright', 'install', 'chromium'], base_path()); $browser_install->setTimeout(300); // 5 minute timeout for download $browser_install->run(function ($type, $buffer) { // Silent - downloads can be verbose }); if (!$browser_install->isSuccessful()) { $this->error('❌ Failed to install Chromium browser'); $this->error('Run manually: npx playwright install chromium'); return 1; } $this->info('✅ Chromium browser installed/updated'); $this->info(''); } else { $this->error('❌ Browser check failed: ' . trim($error_output)); return 1; } } // Generate signed request token for user/site context // This prevents unauthorized requests from hijacking sessions via headers $dev_auth_token = null; if ($user_id) { $dev_auth_token = $this->generate_dev_auth_token($url, $user_id); } // Build command arguments $command_args = ['node', $playwright_script, $url]; if ($user_id) { $command_args[] = "--user={$user_id}"; } if ($dev_auth_token) { $command_args[] = "--dev-auth-token={$dev_auth_token}"; } if ($show_log) { $command_args[] = '--log'; } if ($no_body) { $command_args[] = '--no-body'; } if ($follow_redirects) { $command_args[] = '--follow-redirects'; } if ($headers) { $command_args[] = '--headers'; } if ($console_log) { $command_args[] = '--console-log'; } if ($xhr_dump) { $command_args[] = '--xhr-dump'; } if ($input_elements) { $command_args[] = '--input-elements'; } if ($post_data) { $command_args[] = "--post={$post_data}"; } if ($cookies) { $command_args[] = '--cookies'; } if ($wait_for) { $command_args[] = "--wait-for={$wait_for}"; } if ($all_logs) { $command_args[] = '--all-logs'; } if ($expect_element) { $command_args[] = "--expect-element={$expect_element}"; } if ($dump_element) { $command_args[] = "--dump-element={$dump_element}"; } if ($storage) { $command_args[] = '--storage'; } if ($xhr_list) { $command_args[] = '--xhr-list'; } if ($full) { $command_args[] = '--full'; } if ($eval_code) { // Don't use escapeshellarg here as it adds extra quotes // Just pass the eval code directly since Process class handles escaping $command_args[] = "--eval={$eval_code}"; } if ($timeout) { $command_args[] = "--timeout={$timeout}"; } if ($console_debug_filter) { $command_args[] = "--console-debug-filter={$console_debug_filter}"; } if ($console_debug_benchmark) { $command_args[] = "--console-debug-benchmark"; } if ($console_debug_all) { $command_args[] = "--console-debug-all"; } if ($console_debug_disable) { $command_args[] = "--console-debug-disable"; } if ($screenshot_width) { $command_args[] = "--screenshot-width={$screenshot_width}"; } if ($screenshot_path) { $command_args[] = "--screenshot-path={$screenshot_path}"; } if ($dump_dimensions) { $command_args[] = "--dump-dimensions={$dump_dimensions}"; } // Pass Laravel log path as environment variable $laravel_log_path = storage_path('logs/laravel.log'); $env = array_merge($_ENV, [ 'LARAVEL_LOG_PATH' => $laravel_log_path ]); // Add console debug filter to environment if provided if ($console_debug_filter) { $env['CONSOLE_DEBUG_FILTER'] = $console_debug_filter; $env['CONSOLE_DEBUG_ENABLED'] = 'true'; // Enable console_debug when filter is set } // Convert timeout from milliseconds to seconds for Process timeout // Add 10 seconds buffer to the Process timeout to allow Playwright to timeout first $process_timeout = ($timeout / 1000) + 10; // Release the application lock before running Playwright to prevent lock contention // The artisan command holds a WRITE lock which would block the web request's READ lock \App\RSpade\Core\Bootstrap\RsxBootstrap::temporarily_release_lock(); $process = new Process( $command_args, base_path(), $env, null, $process_timeout ); $process->run(function ($type, $buffer) { // Output directly to console echo $buffer; }); // Rotate logs after test to clean slate for next run Debugger::logrotate(); return $process->isSuccessful() ? 0 : 1; } /** * Show comprehensive usage examples */ protected function showExamples() { $this->info('RSX Debug Command - Comprehensive Usage Examples'); $this->line('================================================='); $this->line(''); $this->comment('BASIC USAGE:'); $this->line(' php artisan rsx:debug /dashboard # Test a URL'); $this->line(' php artisan rsx:debug /api/users --no-body # Headers only'); $this->line(' php artisan rsx:debug /login --full # All information'); $this->line(''); $this->comment('AUTHENTICATION:'); $this->line(' php artisan rsx:debug /admin --user=1 # Test as user ID 1'); $this->line(' php artisan rsx:debug /admin --user=admin@example.com # Test as user by email'); $this->line(''); $this->comment('TESTING RSX JAVASCRIPT:'); $this->line(' php artisan rsx:debug /demo --eval="Rsx.Route(\'Demo_Controller\').url()" --no-body'); $this->line(' php artisan rsx:debug /demo --eval="JSON.stringify(Rsx._routes)" --no-body'); $this->line(' php artisan rsx:debug /demo --eval="Rsx.is_dev()" --no-body'); $this->line(''); $this->comment('POST-LOAD INTERACTIONS (click buttons, test modals, etc):'); $this->line(' php artisan rsx:debug /page --user=1 --eval="$(\'[data-sid=btn_edit]\').click(); await new Promise(r => setTimeout(r, 2000));"'); $this->line(' # Click button, wait 2s for modal'); $this->line(' php artisan rsx:debug /form --eval="$(\'#submit\').click(); await new Promise(r => setTimeout(r, 1000));"'); $this->line(' # Submit form and capture result'); $this->line(''); $this->comment('DEBUGGING OUTPUT:'); $this->line(' php artisan rsx:debug / --console # All console output'); $this->line(' php artisan rsx:debug / --console-log # Alias for --console'); $this->line(' php artisan rsx:debug / --console-debug-filter=AUTH # Filter console_debug'); $this->line(' php artisan rsx:debug / --console-debug-all # Show all console_debug channels'); $this->line(' php artisan rsx:debug / --console-debug-benchmark # With timing'); $this->line(' php artisan rsx:debug / --console-debug-disable # Disable console_debug'); $this->line(' php artisan rsx:debug / --log # Display Laravel error log'); $this->line(' php artisan rsx:debug / --all-logs # Show all log files'); $this->line(''); $this->comment('XHR/AJAX MONITORING:'); $this->line(' php artisan rsx:debug /api --xhr-list # Simple XHR list'); $this->line(' php artisan rsx:debug /api --xhr-dump # Full XHR details'); $this->line(''); $this->comment('DOM INSPECTION:'); $this->line(' php artisan rsx:debug /form --expect-element="#submit" # Verify element exists'); $this->line(' php artisan rsx:debug /page --dump-element=".content" # Extract element HTML'); $this->line(' php artisan rsx:debug /form --input-elements # List form inputs'); $this->line(' php artisan rsx:debug /slow --wait-for=".loaded" # Wait for element'); $this->line(''); $this->comment('HTTP TESTING:'); $this->line(' php artisan rsx:debug /api --post=\'{"key":"value"}\' # POST JSON data'); $this->line(' php artisan rsx:debug /auth --follow-redirects # Track redirects'); $this->line(' php artisan rsx:debug /api --headers # Display all headers'); $this->line(' php artisan rsx:debug /app --cookies # Show all cookies'); $this->line(''); $this->comment('BROWSER STATE:'); $this->line(' php artisan rsx:debug /app --storage # View localStorage/sessionStorage'); $this->line(' php artisan rsx:debug /slow --timeout=60000 # 60 second timeout'); $this->line(''); $this->comment('SCREENSHOTS:'); $this->line(' php artisan rsx:debug /page --screenshot-path=/tmp/screenshot.png'); $this->line(' # Screenshot at 1920px (default)'); $this->line(' php artisan rsx:debug /page --screenshot-width=mobile --screenshot-path=/tmp/mobile.png'); $this->line(' # Mobile device (412px)'); $this->line(' php artisan rsx:debug /page --screenshot-width=tablet --screenshot-path=/tmp/tablet.png'); $this->line(' # Tablet device (768px)'); $this->line(' php artisan rsx:debug /page --screenshot-width=1024 --screenshot-path=/tmp/custom.png'); $this->line(' # Custom width (1024px)'); $this->line(' # Available presets: mobile (412px), iphone-mobile (390px), tablet (768px),'); $this->line(' # desktop-small (1366px), desktop-medium (1920px), desktop-large (2560px)'); $this->line(''); $this->comment('LAYOUT DEBUGGING:'); $this->line(' php artisan rsx:debug /page --dump-dimensions=".card"'); $this->line(' # Add data-dimensions to .card elements'); $this->line(' php artisan rsx:debug /page --dump-dimensions=".sidebar,.main"'); $this->line(' # Multiple selectors'); $this->line(' # Output in DOM: data-dimensions=\'{"x":0,"y":60,"w":250,"h":800,"margin":0,"padding":"20 15 20 15"}\''); $this->line(''); $this->comment('IMPORTANT NOTES:'); $this->line(' • When using rsx:debug with grep and no output appears, re-run without grep'); $this->line(' to see the full context and any errors that may have occurred'); $this->line(' • Use rsx_dump_die() in your code for temporary debugging output'); $this->line(' • This command is development-only and disabled in production'); $this->line(' • For more details on console_debug: php artisan rsx:man console_debug'); $this->line(' • For config options: php artisan rsx:man config_rsx'); } /** * Resolve user identifier to user ID * * Accepts either a numeric user ID or an email address. * Validates that the user exists in the database. * * @param string $user_input User ID or email address * @return int|null User ID or null if not found (error already displayed) */ protected function resolve_user(string $user_input): ?int { // Check if input is an email address if (str_contains($user_input, '@')) { $login_user = Login_User_Model::find_by_email($user_input); if (!$login_user) { $this->error("User not found: {$user_input}"); return null; } return $login_user->id; } // Input is a user ID - validate it exists if (!ctype_digit($user_input)) { $this->error("Invalid user identifier: {$user_input} (must be numeric ID or email address)"); return null; } $user_id = (int) $user_input; $login_user = Login_User_Model::find($user_id); if (!$login_user) { $this->error("User ID not found: {$user_id}"); return null; } return $user_id; } /** * Generate a signed dev auth token for Playwright requests * * The token is an HMAC signature of the request parameters using APP_KEY. * This ensures that only requests originating from rsx:debug (which has * access to APP_KEY) can authenticate as different users. * * @param string $url The URL being tested * @param int $user_id The user ID to authenticate as * @return string The signed token */ protected function generate_dev_auth_token(string $url, int $user_id): string { $app_key = config('app.key'); if (!$app_key) { $this->error("APP_KEY not configured - cannot generate dev auth token"); exit(1); } // Create payload with request parameters $payload = json_encode([ 'url' => $url, 'user_id' => $user_id, ]); // Sign with HMAC-SHA256 return hash_hmac('sha256', $payload, $app_key); } }