#!/usr/bin/env php 'php-formatter', 'json' => 'json-formatter', // Future formatters can be added here // 'js' => 'js-formatter', // 'scss' => 'style-formatter', // 'css' => 'style-formatter', ]; public function __construct() { $this->formatters_dir = __DIR__ . DIRECTORY_SEPARATOR . 'formatters'; $this->project_root = dirname(__DIR__); $this->cache_file = $this->project_root . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . '.rsx-formatter-cache.json'; // Ensure cache directory exists $cache_dir = dirname($this->cache_file); if (!is_dir($cache_dir)) { mkdir($cache_dir, 0777, true); } } /** * Main entry point */ public function run($argv) { // Check for auto-reformat-periodic mode first if (in_array('--auto-reformat-periodic', $argv)) { $this->auto_reformat_mode = true; $this->handle_auto_reformat_periodic(); return; } // Parse command line arguments $files = []; $formatter_args = []; for ($i = 1; $i < count($argv); $i++) { $arg = $argv[$i]; if ($arg === '--verbose' || $arg === '-v') { $this->verbose = true; $formatter_args[] = $arg; } elseif ($arg === '--help' || $arg === '-h') { $this->show_help(); exit(0); } elseif (substr($arg, 0, 1) !== '-') { $files[] = $arg; } else { $formatter_args[] = $arg; } } // If no files specified, format current directory if (empty($files)) { $files = ['.']; } // Load cache for regular mode too (to update it) $this->load_cache(); $this->route_files($files, $formatter_args); // Save cache if it was modified $this->save_cache(); } /** * Handle --auto-reformat-periodic mode */ private function handle_auto_reformat_periodic() { $rsx_dir = $this->project_root . DIRECTORY_SEPARATOR . 'rsx'; if (!is_dir($rsx_dir)) { $this->warning("RSX directory not found: $rsx_dir"); return; } // Load existing cache $this->load_cache(); if (empty($this->cache_data)) { // First run - build cache without formatting $this->info("Building initial formatter cache..."); $files = $this->scan_directory($rsx_dir); foreach ($files as $file) { $this->cache_data[$file] = [ 'mtime' => filemtime($file), 'size' => filesize($file), 'formatted_at' => time() ]; } $this->cache_dirty = true; $this->save_cache(); $this->success("Cache built with " . count($files) . " files"); return; } // Subsequent run - check for changes $this->info("Checking for file changes..."); $files_to_format = []; $files_to_remove = []; // Check existing cached files foreach ($this->cache_data as $file => $cache_entry) { if (!file_exists($file)) { // File was deleted $files_to_remove[] = $file; $this->info("Removed from cache: $file"); } else { // Check if file changed $current_mtime = filemtime($file); $current_size = filesize($file); if ($current_mtime != $cache_entry['mtime'] || $current_size != $cache_entry['size']) { $files_to_format[] = $file; if ($this->verbose) { $this->info("Changed: $file"); } } } } // Remove deleted files from cache foreach ($files_to_remove as $file) { unset($this->cache_data[$file]); $this->cache_dirty = true; } // Scan for new files $current_files = $this->scan_directory($rsx_dir); foreach ($current_files as $file) { if (!isset($this->cache_data[$file])) { $files_to_format[] = $file; if ($this->verbose) { $this->info("New file: $file"); } } } if (empty($files_to_format)) { $this->success("No files need formatting"); $this->save_cache(); return; } // Format the files that need it $this->info("Formatting " . count($files_to_format) . " files..."); $this->route_files($files_to_format, $this->verbose ? ['-v'] : []); // Save updated cache $this->save_cache(); } /** * Scan directory for formattable files */ private function scan_directory($dir) { $files = []; $iterator = new RecursiveIteratorIterator( new RecursiveCallbackFilterIterator( new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), function ($file, $key, $iterator) { // Skip excluded directories if ($iterator->hasChildren() && $this->is_excluded_dir($file->getFilename())) { return false; } return true; } ), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $file) { if ($file->isFile()) { $path = $file->getPathname(); $extension = $this->get_file_extension($path); if (isset($this->formatters[$extension])) { $files[] = $path; } } } return $files; } /** * Check if directory name should be excluded */ private function is_excluded_dir($dirname) { return in_array($dirname, $this->excluded_dirs); } /** * Check if path contains excluded directories */ private function is_excluded_path($path) { $sep = DIRECTORY_SEPARATOR; foreach ($this->excluded_dirs as $excluded) { if (strpos($path, $sep . $excluded . $sep) !== false) { return true; } } return false; } /** * Check if a path is absolute */ private function is_absolute_path($path) { // Unix absolute path if (substr($path, 0, 1) === '/') { return true; } // Windows absolute path (C:\ or C:/) if (preg_match('/^[a-zA-Z]:[\\\\|\/]/', $path)) { return true; } return false; } /** * Load cache from file */ private function load_cache() { if (file_exists($this->cache_file)) { $content = file_get_contents($this->cache_file); $this->cache_data = json_decode($content, true) ?: []; } } /** * Save cache to file if dirty */ private function save_cache() { if ($this->cache_dirty) { file_put_contents( $this->cache_file, json_encode($this->cache_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ); if ($this->verbose) { $this->info("Cache updated"); } } } /** * Update cache entry for a file */ private function update_cache_entry($file_path) { if (file_exists($file_path)) { $this->cache_data[$file_path] = [ 'mtime' => filemtime($file_path), 'size' => filesize($file_path), 'formatted_at' => time() ]; $this->cache_dirty = true; } } /** * Route files to appropriate formatters */ private function route_files($paths, $formatter_args) { // Group files by formatter $files_by_formatter = []; foreach ($paths as $path) { // Make path absolute if relative (works on both Windows and Unix) if (!$this->is_absolute_path($path)) { $path = getcwd() . DIRECTORY_SEPARATOR . $path; } if (is_file($path)) { // Skip excluded paths if ($this->is_excluded_path($path)) { if ($this->verbose) { $this->info("Skipping excluded: $path"); } continue; } $extension = $this->get_file_extension($path); if (isset($this->formatters[$extension])) { $formatter = $this->formatters[$extension]; if (!isset($files_by_formatter[$formatter])) { $files_by_formatter[$formatter] = []; } $files_by_formatter[$formatter][] = $path; } elseif ($this->verbose) { $this->info("No formatter for: $path"); } } elseif (is_dir($path)) { // For directories, recursively find all files $this->route_directory($path, $files_by_formatter); } else { $this->warning("Path not found: $path"); } } // Execute formatters foreach ($files_by_formatter as $formatter => $files) { $this->execute_formatter($formatter, $files, $formatter_args); } } /** * Route all files in a directory */ private function route_directory($dir_path, &$files_by_formatter) { $iterator = new RecursiveIteratorIterator( new RecursiveCallbackFilterIterator( new RecursiveDirectoryIterator($dir_path, RecursiveDirectoryIterator::SKIP_DOTS), function ($file, $key, $iterator) { // Skip excluded directories if ($iterator->hasChildren() && $this->is_excluded_dir($file->getFilename())) { return false; } return true; } ), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $file) { if ($file->isFile()) { $path = $file->getPathname(); // Double-check path doesn't contain excluded dirs if ($this->is_excluded_path($path)) { continue; } $extension = $this->get_file_extension($path); if (isset($this->formatters[$extension])) { $formatter = $this->formatters[$extension]; if (!isset($files_by_formatter[$formatter])) { $files_by_formatter[$formatter] = []; } $files_by_formatter[$formatter][] = $path; } } } } /** * Execute a formatter with files */ private function execute_formatter($formatter, $files, $args) { $formatter_path = $this->formatters_dir . DIRECTORY_SEPARATOR . $formatter; if (!file_exists($formatter_path)) { $this->error("Formatter not found: $formatter"); return; } // Process files one by one to track success foreach ($files as $file) { // Preserve original modification time to prevent VS Code conflicts $original_mtime = file_exists($file) ? filemtime($file) : null; // Build command for single file // Use 'php' command which works on all platforms $command = 'php ' . escapeshellarg($formatter_path); // Add arguments foreach ($args as $arg) { $command .= ' ' . escapeshellarg($arg); } // Add file $command .= ' ' . escapeshellarg($file); // Execute formatter and capture output ob_start(); passthru($command, $return_code); $output = ob_get_clean(); // Output the formatter's output echo $output; // Update cache if formatting was successful if ($return_code === 0) { // Check if output indicates success (look for success marker) if (strpos($output, '✓') !== false || strpos($output, 'Formatted') !== false) { $this->update_cache_entry($file); // Restore original modification time to prevent VS Code conflicts // This allows VS Code to reload the file seamlessly without detecting external changes if ($original_mtime !== null && file_exists($file)) { touch($file, $original_mtime); } } } elseif ($this->verbose) { $this->warning("Formatter $formatter exited with code $return_code for $file"); } } } /** * Get file extension */ private function get_file_extension($path) { // Handle .formatting.tmp files from VS Code if (substr($path, -15) === '.formatting.tmp') { // Get the actual extension from before .formatting.tmp $actual_path = substr($path, 0, -15); $info = pathinfo($actual_path); return isset($info['extension']) ? strtolower($info['extension']) : ''; } // Handle .blade.php files if (substr($path, -10) === '.blade.php') { return 'blade'; } $info = pathinfo($path); return isset($info['extension']) ? strtolower($info['extension']) : ''; } /** * Show help message */ private function show_help() { $this->output("RSX Code Formatter"); $this->output(""); $this->output("Usage: rsx-format [options] [files...]"); $this->output(""); $this->output("Options:"); $this->output(" -v, --verbose Show detailed output"); $this->output(" -h, --help Show this help message"); $this->output(""); $this->output("Supported file types:"); foreach ($this->formatters as $ext => $formatter) { $this->output(" .$ext - $formatter"); } $this->output(""); $this->output("Examples:"); $this->output(" rsx-format # Format all files in current directory"); $this->output(" rsx-format app/ # Format all files in app directory"); $this->output(" rsx-format file.php file.json # Format specific files"); $this->output(""); $this->output("Note: vendor/, node_modules/, and public/ directories are automatically excluded"); } /** * Output helpers */ private function output($message) { echo $message . PHP_EOL; } private function success($message) { echo "\033[32m" . $message . "\033[0m" . PHP_EOL; } private function info($message) { echo "\033[36m" . $message . "\033[0m" . PHP_EOL; } private function warning($message) { echo "\033[33m" . $message . "\033[0m" . PHP_EOL; } private function error($message) { echo "\033[31m" . $message . "\033[0m" . PHP_EOL; } } // Run the router $router = new FormatterRouter(); $router->run($argv);