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

View File

@@ -0,0 +1,757 @@
<?php
/**
* IDE Service Handler
*
* Handles all IDE service requests bypassing Laravel for performance.
* Uses file-based authentication with SHA1 message signing.
*
* Authentication Flow:
* 1. VS Code generates session, client_key, server_key and writes to auth.json
* 2. Client signs requests with SHA1(body + client_key)
* 3. Server validates client signature
* 4. Server signs response with SHA1(body + server_key)
* 5. Client validates server signature
*/
// Error reporting for development
error_reporting(E_ALL);
ini_set('display_errors', 0);
// Base path - framework is in system/ subdirectory
$system_path = realpath(__DIR__ . '/../../../..');
define('IDE_BASE_PATH', realpath($system_path . '/..')); // Project root
define('IDE_SYSTEM_PATH', $system_path); // Framework root
// Helper to get framework paths
function ide_framework_path($relative_path) {
return IDE_SYSTEM_PATH . '/' . ltrim($relative_path, '/');
}
// Load exec_safe() function
require_once ide_framework_path('app/RSpade/helpers.php');
// JSON response helper
function json_response($data, $code = 200) {
http_response_code($code);
header('Content-Type: application/json');
$json = json_encode($data);
// Sign response if we have auth
global $auth_data;
if (!empty($auth_data['server_key'])) {
$signature = sha1($json . $auth_data['server_key']);
header('X-Signature: ' . $signature);
}
echo $json;
exit;
}
// Error response helper
function error_response($message, $code = 400) {
json_response(['success' => false, 'error' => $message], $code);
}
// Check if IDE services are enabled
$env_file = IDE_BASE_PATH . '/.env';
if (file_exists($env_file)) {
$env_content = file_get_contents($env_file);
// Check if production
if (preg_match('/^APP_ENV=production$/m', $env_content)) {
// Check if explicitly enabled in production
if (!preg_match('/^RSX_IDE_SERVICES_ENABLED=true$/m', $env_content)) {
error_response('IDE services disabled in production', 403);
}
}
}
// Parse request URI to get service
$request_uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$service_path = str_replace('/_ide/service', '', $request_uri);
$service_path = trim($service_path, '/');
// Get request body
$request_body = file_get_contents('php://input');
$request_data = json_decode($request_body, true);
// Handle authentication
$auth_data = [];
$session_header = $_SERVER['HTTP_X_SESSION'] ?? null;
$signature_header = $_SERVER['HTTP_X_SIGNATURE'] ?? null;
if ($session_header && $signature_header) {
// Load auth data for session
$auth_file = ide_framework_path('storage/rsx-ide-bridge/auth-' . $session_header . '.json');
if (file_exists($auth_file)) {
$auth_data = json_decode(file_get_contents($auth_file), true);
// Validate signature
$expected_signature = sha1($request_body . $auth_data['client_key']);
if ($signature_header !== $expected_signature) {
error_response('Invalid signature', 401);
}
} else {
// Session not found - provide recovery hint
http_response_code(401);
header('Content-Type: application/json');
echo json_encode([
'error' => 'Session not found',
'code' => 401,
'recoverable' => true,
'recovery' => 'Create new session via POST /_ide/service/auth/create'
]);
exit;
}
} else {
// No auth provided - check if this is an auth creation request
if ($service_path === 'auth/create' && $_SERVER['REQUEST_METHOD'] === 'POST') {
// Generate new auth session
$session = bin2hex(random_bytes(20));
$client_key = bin2hex(random_bytes(20));
$server_key = bin2hex(random_bytes(20));
$auth_data = [
'session' => $session,
'client_key' => $client_key,
'server_key' => $server_key,
'created' => time()
];
// Save auth file
$bridge_dir = ide_framework_path('storage/rsx-ide-bridge');
if (!is_dir($bridge_dir)) {
mkdir($bridge_dir, 0755, true);
}
$auth_file = $bridge_dir . '/auth-' . $session . '.json';
file_put_contents($auth_file, json_encode($auth_data, JSON_PRETTY_PRINT));
// Return auth data (unsigned response since client doesn't have keys yet)
http_response_code(200);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'session' => $session,
'client_key' => $client_key,
'server_key' => $server_key
]);
exit;
} else {
error_response('Authentication required', 401);
}
}
// Route to appropriate service handler
switch ($service_path) {
case 'format':
handle_format_service($request_data);
break;
case 'definition':
handle_definition_service($request_data);
break;
case 'complete':
handle_complete_service($request_data);
break;
case 'exec':
handle_exec_service($request_data);
break;
case 'git':
handle_git_service($request_data);
break;
case 'git/diff':
handle_git_diff_service($request_data);
break;
case 'command':
handle_command_service($request_data);
break;
case 'health':
json_response(['success' => true, 'service' => 'ide', 'version' => '1.0.0']);
break;
default:
error_response('Unknown service: ' . $service_path, 404);
}
/**
* Handle format service - formats PHP files
*/
function handle_format_service($data) {
$file = $data['file'] ?? null;
if (!$file) {
error_response('No file provided');
}
// Normalize path separators (Windows sends backslashes, convert to forward slashes)
$file = str_replace('\\', '/', $file);
// Build full path
$full_path = IDE_BASE_PATH . '/' . ltrim($file, '/');
// Normalize path without requiring file to exist (for .formatting.tmp files)
// Use realpath on the directory, then append the filename
$dir = dirname($full_path);
$filename = basename($full_path);
// Get real directory path
if (is_dir($dir)) {
$real_dir = realpath($dir);
if ($real_dir) {
$full_path = $real_dir . '/' . $filename;
}
}
// Security check - path must be within project
if (strpos($full_path, IDE_BASE_PATH) !== 0) {
error_response('Invalid file path - must be within project');
}
if (!file_exists($full_path)) {
error_response('File not found', 404);
}
// Execute formatter
$formatter_path = ide_framework_path('bin/rsx-format');
if (!file_exists($formatter_path)) {
error_response('Formatter not found', 500);
}
// Log formatting request
$log_file = ide_framework_path('storage/logs/ide-formatter.log');
$log_dir = dirname($log_file);
if (!is_dir($log_dir)) {
mkdir($log_dir, 0755, true);
}
$log_entry = sprintf(
"[%s] Formatting file: %s\n Full path: %s\n CWD before: %s\n File exists: %s\n File size: %d bytes\n",
date('Y-m-d H:i:s'),
$file,
$full_path,
getcwd(),
file_exists($full_path) ? 'yes' : 'no',
file_exists($full_path) ? filesize($full_path) : 0
);
file_put_contents($log_file, $log_entry, FILE_APPEND);
// Change to project root so formatter can find config files
$old_cwd = getcwd();
chdir(IDE_BASE_PATH);
$command = sprintf('php %s %s 2>&1',
escapeshellarg($formatter_path),
escapeshellarg($full_path)
);
file_put_contents($log_file, " CWD after chdir: " . getcwd() . "\n Command: $command\n", FILE_APPEND);
$output = [];
$return_var = 0;
\exec_safe($command, $output, $return_var);
// Restore working directory
chdir($old_cwd);
// Log result
file_put_contents($log_file, sprintf(
" Return code: %d\n Output lines: %d\n Output: %s\n File size after: %d bytes\n\n",
$return_var,
count($output),
implode("\n ", $output),
file_exists($full_path) ? filesize($full_path) : 0
), FILE_APPEND);
if ($return_var === 0) {
// Read formatted content if requested
$content = null;
if (!empty($data['return_content'])) {
$content = file_get_contents($full_path);
}
json_response([
'success' => true,
'message' => 'Formatting completed',
'content' => $content
]);
} else {
error_response('Formatting failed: ' . implode("\n", $output), 500);
}
}
/**
* Handle definition service - find symbol definitions
* Supports all the types from the VS Code definition provider
*/
function handle_definition_service($data) {
$identifier = $data['identifier'] ?? $data['symbol'] ?? null;
$method = $data['method'] ?? null;
$type = $data['type'] ?? null;
if (!$identifier) {
error_response('No identifier provided');
}
// Load manifest data
$manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php');
if (!file_exists($manifest_file)) {
error_response('Manifest not found - run php artisan rsx:manifest:build', 500);
}
$manifest_raw = include $manifest_file;
// The manifest structure has the data under 'data' key
$manifest = $manifest_raw['data'] ?? $manifest_raw;
// Search based on type
$result = null;
switch ($type) {
case 'class':
// Search PHP classes with optional method
if (isset($manifest['php']['classes'][$identifier])) {
$class_data = $manifest['php']['classes'][$identifier];
$file = $class_data['file'];
$line = $class_data['line'] ?? 1;
// If method specified, try to find method line number
if ($method && isset($class_data['methods'][$method])) {
$line = $class_data['methods'][$method]['line'] ?? $line;
}
$result = [
'found' => true,
'file' => $file,
'line' => $line
];
}
break;
case 'view':
// Search Blade views by name or rsx_id
foreach ($manifest['blade']['views'] ?? [] as $view) {
if ($view['name'] === $identifier || $view['rsx_id'] === $identifier) {
$result = [
'found' => true,
'file' => $view['file'],
'line' => 1
];
break;
}
}
break;
case 'bundle_alias':
// Search bundle aliases defined in config/rsx.php
$config_file = ide_framework_path('config/rsx.php');
if (file_exists($config_file)) {
$config = include $config_file;
if (isset($config['bundle_aliases'][$identifier])) {
$bundle_class = $config['bundle_aliases'][$identifier];
// Extract class name from namespace
$class_name = str_replace('\\', '_', str_replace('App\\RSpade\\', '', $bundle_class));
// Find the bundle class file
if (isset($manifest['php']['classes'][$class_name])) {
$result = [
'found' => true,
'file' => $manifest['php']['classes'][$class_name]['file'],
'line' => $manifest['php']['classes'][$class_name]['line'] ?? 1
];
}
}
}
break;
case 'jqhtml_template':
// Search for .jqhtml template files
foreach ($manifest['jqhtml']['components'] ?? [] as $component) {
if ($component['name'] === $identifier) {
$result = [
'found' => true,
'file' => $component['template_file'],
'line' => 1
];
break;
}
}
break;
case 'jqhtml_class':
// Search for JavaScript class extending Jqhtml_Component
foreach ($manifest['jqhtml']['components'] ?? [] as $component) {
if ($component['name'] === $identifier && isset($component['js_file'])) {
$result = [
'found' => true,
'file' => $component['js_file'],
'line' => $component['js_line'] ?? 1
];
break;
}
}
break;
case 'jqhtml_class_method':
// Search for method in jqhtml component JavaScript class
foreach ($manifest['jqhtml']['components'] ?? [] as $component) {
if ($component['name'] === $identifier && isset($component['js_file'])) {
$js_file = IDE_BASE_PATH . '/' . $component['js_file'];
$line = 1;
// Try to find the method line number
if ($method && file_exists($js_file)) {
$content = file_get_contents($js_file);
$lines = explode("\n", $content);
foreach ($lines as $index => $line_content) {
// Look for method definition patterns
if (preg_match('/^\s*' . preg_quote($method) . '\s*\(/', $line_content) ||
preg_match('/^\s*async\s+' . preg_quote($method) . '\s*\(/', $line_content) ||
preg_match('/^\s*static\s+' . preg_quote($method) . '\s*\(/', $line_content)) {
$line = $index + 1;
break;
}
}
}
$result = [
'found' => true,
'file' => $component['js_file'],
'line' => $line
];
break;
}
}
break;
case 'route':
// Search routes for controller::method
foreach ($manifest['php']['routes'] ?? [] as $route) {
if ($route['class'] . '::' . $route['method'] === $identifier) {
$result = [
'found' => true,
'file' => $route['file'],
'line' => $route['line'] ?? 1
];
break;
}
}
break;
default:
// Try to auto-detect type
// First try as class
if (isset($manifest['php']['classes'][$identifier])) {
$class_data = $manifest['php']['classes'][$identifier];
$result = [
'found' => true,
'file' => $class_data['file'],
'line' => $class_data['line'] ?? 1
];
}
// Try as view
if (!$result) {
foreach ($manifest['blade']['views'] ?? [] as $view) {
if ($view['name'] === $identifier || $view['rsx_id'] === $identifier) {
$result = [
'found' => true,
'file' => $view['file'],
'line' => 1
];
break;
}
}
}
// Try as jqhtml component
if (!$result) {
foreach ($manifest['jqhtml']['components'] ?? [] as $component) {
if ($component['name'] === $identifier) {
$result = [
'found' => true,
'file' => $component['template_file'] ?? $component['js_file'],
'line' => 1
];
break;
}
}
}
}
if ($result) {
json_response($result);
} else {
json_response(['found' => false]);
}
}
/**
* Handle complete service - provide autocomplete suggestions
*/
function handle_complete_service($data) {
$prefix = $data['prefix'] ?? '';
$context = $data['context'] ?? null;
// Load manifest data
$manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php');
if (!file_exists($manifest_file)) {
error_response('Manifest not found', 500);
}
$manifest = include $manifest_file;
$suggestions = [];
// Search PHP classes
foreach ($manifest['php']['classes'] ?? [] as $class_name => $class_data) {
if (stripos($class_name, $prefix) === 0) {
$suggestions[] = [
'label' => $class_name,
'kind' => 'class',
'detail' => $class_data['file']
];
}
}
// Limit results
$suggestions = array_slice($suggestions, 0, 100);
json_response([
'success' => true,
'suggestions' => $suggestions
]);
}
/**
* Handle exec service - execute artisan commands
*/
function handle_exec_service($data) {
$command = $data['command'] ?? null;
if (!$command) {
error_response('No command provided');
}
// Whitelist of allowed commands
$allowed_commands = [
'rsx:check',
'rsx:manifest:build',
'rsx:clean',
'rsx:bundle:compile',
'rsx:routes'
];
// Extract base command
$parts = explode(' ', $command);
$base_command = $parts[0];
if (!in_array($base_command, $allowed_commands)) {
error_response('Command not allowed: ' . $base_command, 403);
}
// Build full artisan command
$artisan_path = IDE_BASE_PATH . '/artisan';
$full_command = sprintf('php %s %s 2>&1',
escapeshellarg($artisan_path),
$command
);
$output = [];
$return_var = 0;
\exec_safe($full_command, $output, $return_var);
json_response([
'success' => $return_var === 0,
'output' => implode("\n", $output),
'exit_code' => $return_var
]);
}
/**
* Handle git service - get git status
*/
function handle_git_service($data) {
$output = [];
$return_var = 0;
\exec_safe('cd ' . escapeshellarg(IDE_BASE_PATH) . ' && git status --porcelain 2>&1', $output, $return_var);
if ($return_var !== 0) {
error_response('Git command failed: ' . implode("\n", $output), 500);
}
$modified = [];
$added = [];
$conflicts = [];
foreach ($output as $line) {
if (strlen($line) < 4) continue;
$status = substr($line, 0, 2);
$file = trim(substr($line, 3));
// Parse git status codes
if (str_contains($status, 'U') || ($status[0] === 'A' && $status[1] === 'A')) {
$conflicts[] = $file;
} elseif ($status[0] === 'A' || $status[1] === 'A' || $status === '??') {
$added[] = $file;
} elseif ($status[0] === 'M' || $status[1] === 'M' || $status[0] === 'R') {
$modified[] = $file;
}
}
json_response([
'success' => true,
'modified' => $modified,
'added' => $added,
'conflicts' => $conflicts
]);
}
/**
* Handle git diff service - get line-level changes for a file
*/
function handle_git_diff_service($data) {
$file = $data['file'] ?? null;
if (!$file) {
error_response('No file provided');
}
// Normalize path separators (Windows sends backslashes, convert to forward slashes)
$file = str_replace('\\', '/', $file);
// Security check - file must be within project
$full_path = IDE_BASE_PATH . '/' . ltrim($file, '/');
// Normalize path without requiring file to exist (handles deleted files in git)
$dir = dirname($full_path);
$filename = basename($full_path);
if (is_dir($dir)) {
$real_dir = realpath($dir);
if ($real_dir) {
$full_path = $real_dir . '/' . $filename;
}
}
// Validate path is within project
if (strpos($full_path, IDE_BASE_PATH) !== 0) {
error_response('Invalid file path');
}
// Note: File existence not required for git diff (might be deleted)
$output = [];
$return_var = 0;
// Get diff with line numbers, no context
\exec_safe('cd ' . escapeshellarg(IDE_BASE_PATH) . ' && git diff HEAD --unified=0 ' . escapeshellarg($file) . ' 2>&1', $output, $return_var);
if ($return_var !== 0) {
error_response('Git diff command failed: ' . implode("\n", $output), 500);
}
$added = [];
$modified = [];
$deleted = [];
foreach ($output as $line) {
// Parse unified diff format: @@ -old_start,old_count +new_start,new_count @@
if (preg_match('/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/', $line, $matches)) {
$old_start = (int)$matches[1];
$old_count = isset($matches[2]) ? (int)$matches[2] : 1;
$new_start = (int)$matches[3];
$new_count = isset($matches[4]) ? (int)$matches[4] : 1;
// Deletion: old_count > 0, new_count = 0
if ($old_count > 0 && $new_count === 0) {
$deleted[] = [$new_start, $new_start];
}
// Addition: old_count = 0, new_count > 0
elseif ($old_count === 0 && $new_count > 0) {
$added[] = [$new_start, $new_start + $new_count - 1];
}
// Modification: both > 0
else {
$modified[] = [$new_start, $new_start + $new_count - 1];
}
}
}
json_response([
'success' => true,
'added' => $added,
'modified' => $modified,
'deleted' => $deleted
]);
}
/**
* Handle command service - execute artisan commands with output capture
*/
function handle_command_service($data) {
$command = $data['command'] ?? null;
$arguments = $data['arguments'] ?? [];
if (!$command) {
error_response('No command provided');
}
// Whitelist of allowed commands
$allowed_commands = [
'rsx:check',
'rsx:manifest:build',
'rsx:clean',
'rsx:bundle:compile',
'rsx:routes',
'rsx:refactor:rename_php_class',
'rsx:refactor:rename_php_class_function',
'rsx:refactor:sort_php_class_functions'
];
if (!in_array($command, $allowed_commands)) {
error_response('Command not allowed: ' . $command, 403);
}
// Build artisan command with arguments
$artisan_path = IDE_BASE_PATH . '/artisan';
$command_parts = [
'php',
escapeshellarg($artisan_path),
$command
];
// Add arguments
foreach ($arguments as $arg) {
$command_parts[] = escapeshellarg($arg);
}
$full_command = implode(' ', $command_parts) . ' 2>&1';
// Change to project root
$old_cwd = getcwd();
chdir(IDE_BASE_PATH);
// Capture output using ob_start
ob_start();
$output = [];
$return_var = 0;
\exec_safe($full_command, $output, $return_var);
$ob_output = ob_get_clean();
// Restore working directory
chdir($old_cwd);
$combined_output = trim($ob_output . "\n" . implode("\n", $output));
json_response([
'success' => $return_var === 0,
'output' => $combined_output,
'exit_code' => $return_var
]);
}