Fix bin/publish: use correct .env path for rspade_system Fix bin/publish script: prevent grep exit code 1 from terminating script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
536 lines
17 KiB
PHP
Executable File
536 lines
17 KiB
PHP
Executable File
#!/usr/bin/env php
|
|
<?php
|
|
/**
|
|
* RSX Code Formatter Router
|
|
*
|
|
* Routes formatting requests to the appropriate formatter based on file type
|
|
*
|
|
* Hidden feature: --auto-reformat-periodic
|
|
* Performs intelligent reformatting of ./rsx directory only:
|
|
* - First run: Builds cache without formatting
|
|
* - Subsequent runs: Only formats changed/new files
|
|
* - Automatically excludes vendor/node_modules/public
|
|
* - Updates cache with file state after formatting
|
|
* Note: Intentionally not shown in --help output
|
|
*/
|
|
|
|
// Add error reporting for debugging
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', 1);
|
|
|
|
class FormatterRouter
|
|
{
|
|
private $formatters_dir;
|
|
private $verbose = false;
|
|
private $cache_file;
|
|
private $cache_data = [];
|
|
private $cache_dirty = false;
|
|
private $auto_reformat_mode = false;
|
|
private $project_root;
|
|
|
|
/**
|
|
* Directories to exclude from formatting
|
|
*/
|
|
private $excluded_dirs = ['vendor', 'node_modules', 'public'];
|
|
|
|
/**
|
|
* Map of file extensions to formatter scripts
|
|
*/
|
|
private $formatters = [
|
|
'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); |