Fix bin/publish: copy docs.dist from project root

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>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

536
bin/rsx-format Executable file
View File

@@ -0,0 +1,536 @@
#!/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);