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,327 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Core\CodeTemplates;
use App\RSpade\Core\Manifest\Manifest;
/**
* Processes stub templates with placeholder replacements
*
* Uses Laravel's {{ placeholder }} syntax for template substitution
*/
class StubProcessor
{
/**
* Process a stub file with given replacements
*
* @param string $stub_name Name of the stub file (without .stub extension)
* @param array $replacements Associative array of placeholder => value
* @return string Processed content
*/
public static function process(string $stub_name, array $replacements): string
{
$stub_path = __DIR__ . "/stubs/{$stub_name}.stub";
if (!file_exists($stub_path)) {
throw new \RuntimeException("Stub file not found: {$stub_path}");
}
$content = file_get_contents($stub_path);
// Replace each placeholder
foreach ($replacements as $key => $value) {
$placeholder = "{{ {$key} }}";
$content = str_replace($placeholder, $value, $content);
}
// Check for any unreplaced placeholders
if (preg_match('/\{\{\s*\w+\s*\}\}/', $content, $matches)) {
throw new \RuntimeException("Unreplaced placeholder found: {$matches[0]}");
}
return $content;
}
/**
* Convert module/feature names to class name format
* Example: "user_profile" becomes "User_Profile"
*
* @param string $name
* @return string
*/
public static function to_class_name(string $name): string
{
$parts = explode('_', $name);
return implode('_', array_map('ucfirst', $parts));
}
/**
* Convert module/feature names to human-readable title
* Example: "user_profile" becomes "User Profile"
*
* @param string $name
* @return string
*/
public static function to_title(string $name): string
{
$parts = explode('_', $name);
return implode(' ', array_map('ucfirst', $parts));
}
/**
* Check if a directory is a submodule
* A submodule has a layout file with @rsx_extends (not a bundle render)
*
* @param string $dir_path
* @return bool
*/
public static function is_submodule(string $dir_path): bool
{
if (!is_dir($dir_path)) {
return false;
}
// Look for layout files
$layout_files = glob($dir_path . '/*_layout.blade.php');
if (empty($layout_files)) {
return false;
}
// Check if the layout uses @rsx_extends
foreach ($layout_files as $layout_file) {
$content = file_get_contents($layout_file);
if (strpos($content, '@rsx_extends') !== false) {
return true;
}
}
return false;
}
/**
* Get the parent layout class for a submodule
*
* @param string $module_path
* @return string
*/
public static function get_parent_layout(string $module_path): string
{
// Look for the module's layout file
$layout_files = glob($module_path . '/*_layout.blade.php');
if (!empty($layout_files)) {
$content = file_get_contents($layout_files[0]);
// Extract the @rsx_id value
if (preg_match("/@rsx_id\(['\"]([^'\"]+)['\"]\)/", $content, $matches)) {
return $matches[1];
}
}
// Return default layout name based on module
$module_name = basename($module_path);
return self::to_class_name($module_name) . '_Layout';
}
/**
* Resolve parent route URL from existing controllers
* Tries to find actual route URL from parent controllers to inherit URL structure
*
* @param string $module_name
* @param string|null $submodule_name
* @param string|null $feature_name
* @return string|null Resolved URL or null if not found
*/
protected static function __resolve_parent_route_url(
string $module_name,
?string $submodule_name = null,
?string $feature_name = null
): ?string {
// Try to find parent controller and extract its route
$controller_attempts = [];
if ($submodule_name && $feature_name) {
// For subfeature: try Module_Submodule_Feature_Controller
$controller_attempts[] = self::to_class_name("{$module_name}_{$submodule_name}_{$feature_name}") . '_Controller';
} elseif ($feature_name) {
// For subfeature (no submodule): try Module_Feature_Controller
$controller_attempts[] = self::to_class_name("{$module_name}_{$feature_name}") . '_Controller';
} elseif ($submodule_name) {
// For submodule feature: try Module_Submodule_Controller with index action
$controller_attempts[] = self::to_class_name("{$module_name}_{$submodule_name}") . '_Index_Controller';
} else {
// For module feature: try Module_Index_Controller
$controller_attempts[] = self::to_class_name($module_name) . '_Index_Controller';
}
// Try each controller class
foreach ($controller_attempts as $controller_class) {
try {
// Find controller file in manifest
$controller_path = Manifest::php_find_class($controller_class);
if (!$controller_path) {
continue;
}
// Parse controller file for #[Route] attribute
$absolute_path = base_path($controller_path);
if (!file_exists($absolute_path)) {
continue;
}
$content = file_get_contents($absolute_path);
// Look for #[Route('...')] attribute on index method
// Match: #[Route('/some/path')] or #[Route('/some/path', ...)]
if (preg_match('/#\[Route\([\'"]([^\'"]+)[\'"]/m', $content, $matches)) {
return $matches[1]; // Return the route path
}
} catch (\Exception $e) {
// Controller not found or error parsing - continue to next attempt
continue;
}
}
return null;
}
/**
* Generate standard replacements for a feature
*
* @param string $module_name
* @param string|null $submodule_name
* @param string $feature_name
* @param string|null $subfeature_name
* @return array
*/
public static function generate_replacements(
string $module_name,
?string $submodule_name = null,
string $feature_name = 'index',
?string $subfeature_name = null
): array {
$replacements = [];
// Basic naming
$replacements['module_name'] = $module_name;
$replacements['module_class'] = self::to_class_name($module_name);
$replacements['module_title'] = self::to_title($module_name);
$replacements['module_path'] = "rsx/app/{$module_name}";
// Determine the full path and naming based on structure
if ($submodule_name) {
$replacements['submodule_name'] = $submodule_name;
$replacements['submodule_class'] = self::to_class_name($submodule_name);
$replacements['submodule_title'] = self::to_title($submodule_name);
$replacements['submodule_css_class'] = self::to_class_name("{$module_name}_{$submodule_name}");
if ($subfeature_name) {
// Module > Submodule > Feature > Subfeature
$controller_prefix = self::to_class_name("{$module_name}_{$submodule_name}_{$feature_name}_{$subfeature_name}");
$file_prefix = "{$module_name}_{$submodule_name}_{$feature_name}_{$subfeature_name}";
// Try to resolve parent route, fallback* to generated path
$parent_url = self::__resolve_parent_route_url($module_name, $submodule_name, $feature_name);
$route_path = $parent_url ? "{$parent_url}/{$subfeature_name}" : "/{$module_name}/{$submodule_name}/{$feature_name}/{$subfeature_name}";
// Subfeature uses parent feature's namespace (not subfeature directory)
$layout_class = self::to_class_name("{$module_name}_{$submodule_name}") . '_Layout';
$bundle_class = self::to_class_name($module_name) . '_Bundle';
$namespace = "Rsx\\App\\" . self::to_class_name($module_name) . "\\" . self::to_class_name($submodule_name) . "\\" . self::to_class_name($feature_name);
$feature_description = "Handle {$subfeature_name} subfeature of {$feature_name} in {$submodule_name}";
} else if ($feature_name !== 'index') {
// Module > Submodule > Feature
$controller_prefix = self::to_class_name("{$module_name}_{$submodule_name}_{$feature_name}");
$file_prefix = "{$module_name}_{$submodule_name}_{$feature_name}";
$route_path = "/{$module_name}/{$submodule_name}/{$feature_name}";
$feature_description = "Handle {$feature_name} feature in {$submodule_name}";
} else {
// Module > Submodule > Index
$controller_prefix = self::to_class_name("{$module_name}_{$submodule_name}") . '_Index';
$file_prefix = "{$module_name}_{$submodule_name}_index";
$route_path = "/{$module_name}/{$submodule_name}";
$feature_description = "Handle index page for {$submodule_name}";
}
$layout_class = self::to_class_name("{$module_name}_{$submodule_name}") . '_Layout';
$bundle_class = self::to_class_name($module_name) . '_Bundle';
$namespace = "Rsx\\App\\" . self::to_class_name($module_name) . "\\" . self::to_class_name($submodule_name);
} else if ($subfeature_name) {
// Module > Feature > Subfeature (no submodule)
$controller_prefix = self::to_class_name("{$module_name}_{$feature_name}_{$subfeature_name}");
$file_prefix = "{$module_name}_{$feature_name}_{$subfeature_name}";
// Try to resolve parent route, fallback* to generated path
$parent_url = self::__resolve_parent_route_url($module_name, null, $feature_name);
$route_path = $parent_url ? "{$parent_url}/{$subfeature_name}" : "/{$module_name}/{$feature_name}/{$subfeature_name}";
$layout_class = self::to_class_name($module_name) . '_Layout';
$bundle_class = self::to_class_name($module_name) . '_Bundle';
$namespace = "Rsx\\App\\" . self::to_class_name($module_name) . "\\" . self::to_class_name($feature_name);
$feature_description = "Handle {$subfeature_name} subfeature of {$feature_name}";
} else if ($feature_name !== 'index') {
// Module > Feature
$controller_prefix = self::to_class_name("{$module_name}_{$feature_name}");
$file_prefix = "{$module_name}_{$feature_name}";
// Try to resolve module's index route, fallback* to generated path
$parent_url = self::__resolve_parent_route_url($module_name, null, null);
$route_path = $parent_url ? "{$parent_url}/{$feature_name}" : "/{$module_name}/{$feature_name}";
$layout_class = self::to_class_name($module_name) . '_Layout';
$bundle_class = self::to_class_name($module_name) . '_Bundle';
$namespace = "Rsx\\App\\" . self::to_class_name($module_name) . "\\" . self::to_class_name($feature_name);
$feature_description = "Handle {$feature_name} feature";
} else {
// Module > Index
$controller_prefix = self::to_class_name($module_name) . '_Index';
$file_prefix = "{$module_name}_index";
// Special case: 'frontend' module goes to root /
$route_path = $module_name === 'frontend' ? '/' : "/{$module_name}";
$layout_class = self::to_class_name($module_name) . '_Layout';
$bundle_class = self::to_class_name($module_name) . '_Bundle';
$namespace = "Rsx\\App\\" . self::to_class_name($module_name);
$feature_description = "Handle index page for {$module_name}";
}
// Common replacements
$replacements['controller_prefix'] = $controller_prefix;
$replacements['controller_class'] = $controller_prefix . '_Controller';
$replacements['view_class'] = $controller_prefix;
$replacements['js_class'] = $controller_prefix;
$replacements['file_prefix'] = $file_prefix;
$replacements['route_path'] = $route_path;
$replacements['layout_class'] = $layout_class;
$replacements['bundle_class'] = $bundle_class;
$replacements['namespace'] = $namespace;
$replacements['feature_description'] = $feature_description;
// View-specific
$replacements['page_title'] = self::to_title($subfeature_name ?? $feature_name);
$replacements['view_path'] = $controller_prefix;
// Determine extends layout
if ($submodule_name) {
$replacements['extends_layout'] = self::to_class_name("{$module_name}_{$submodule_name}") . '_Layout';
$replacements['parent_layout_class'] = self::to_class_name($module_name) . '_Layout';
} else {
$replacements['extends_layout'] = $layout_class;
}
// Section declarations for views
if ($submodule_name) {
$replacements['section_declaration'] = "@section('submodule_content')";
$replacements['section_end'] = "@endsection";
} else {
$replacements['section_declaration'] = "@section('content')";
$replacements['section_end'] = "@endsection";
}
return $replacements;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace {{ namespace }};
use App\RSpade\Core\Bundle\Rsx_Bundle_Abstract;
class {{ bundle_class }} extends Rsx_Bundle_Abstract
{
/**
* Define the bundle configuration
*
* @return array Bundle configuration
*/
public static function define(): array
{
return [
'include' => [
'jquery', // jQuery library (required module)
'lodash', // Lodash utilities (required module)
'bootstrap5_src', // Bootstrap 5 SCSS source bundle
'rsx/theme/variables.scss', // Global SCSS variables
'{{ module_path }}', // Module directory
'rsx/lib', // Shared libraries
'rsx/models', // Models for JS stub generation
'rsx/theme', // Theme assets
],
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace {{ namespace }};
use Illuminate\Http\Request;
use App\RSpade\Core\Controller\Rsx_Controller_Abstract;
use Route;
class {{ controller_class }} extends Rsx_Controller_Abstract
{
// Configure access control: php artisan rsx:man controller
// Use Permission::anybody() or Permission::authenticated()
#[Auth('Permission::anybody()')]
public static function pre_dispatch(Request $request, array $params = [])
{
return null;
}
/**
* {{ feature_description }}
*
* @param Request $request
* @param array $params
* @return mixed
*/
#[Route('{{ route_path }}')]
public static function index(Request $request, array $params = [])
{
$data = [
// Add your data here
];
return rsx_view('{{ view_path }}', $data);
}
}

View File

@@ -0,0 +1,26 @@
class {{ js_class }} {
static init() {
if (!$(".{{ view_class }}").exists()) return;
// Initialize your component here
console.log('{{ js_class }} initialized');
// Example: Handle button clicks
$('.btn-action').on('click', function() {
// Handle action
});
// Example: Initialize tooltips
$('[data-bs-toggle="tooltip"]').tooltip();
}
static on_app_ready() {
{{ js_class }}.init();
}
// static on_jqhtml_ready() {
// // Called after all JQHTML components have loaded and rendered
// // Use this if you need to interact with JQHTML components
// // Otherwise, use on_app_ready() for most initialization
// }
}

View File

@@ -0,0 +1,104 @@
@rsx_id('{{ layout_class }}')
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>@yield('title', '{{ module_title }}') - {{ config('rspade.name', 'RSX') }}</title>
{{-- Bootstrap Icons --}}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
{{-- Bundle includes --}}
{!! {{ bundle_class }}::render() !!}
</head>
<body class="{{ rsx_body_class() }}">
{{-- Header Navigation --}}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top shadow-sm">
<div class="container-fluid">
{{-- Brand --}}
<a class="navbar-brand fw-bold" href="{{ Rsx::Route('{{ module_class }}_Index_Controller')->url() }}">
{{ module_title }}
</a>
{{-- Mobile Toggle --}}
<button type="button" class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarMain">
<span class="navbar-toggler-icon"></span>
</button>
{{-- Main Navigation --}}
<div class="collapse navbar-collapse" id="navbarMain">
{{-- Left Side Navigation --}}
<ul class="navbar-nav me-auto">
{{--
Example navigation items with active state detection:
The active class is added using request()->is() to match URL patterns
--}}
<li class="nav-item">
<a class="nav-link {{ request()->is('{{ module_name }}') || request()->is('{{ module_name }}/index') ? 'active' : '' }}"
href="{{ Rsx::Route('{{ module_class }}_Index_Controller')->url() }}">
<i class="bi bi-house-door"></i> Home
</a>
</li>
{{-- Add more navigation items here following the same pattern --}}
{{--
<li class="nav-item">
<a class="nav-link {{ request()->is('{{ module_name }}/feature*') ? 'active' : '' }}"
href="{{ Rsx::Route('{{ module_class }}_Feature_Controller')->url() }}">
<i class="bi bi-star"></i> Feature
</a>
</li>
--}}
</ul>
{{-- Right Side Navigation --}}
<div class="d-flex align-items-center">
{{-- User Profile Dropdown --}}
@if(\App\RSpade\Core\Session\Session::is_logged_in())
<div class="dropdown">
<button type="button" class="btn btn-dark btn-sm dropdown-toggle" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i>
{{ explode('@', \App\RSpade\Core\Session\Session::get_user()->email)[0] }}
</button>
<ul class="dropdown-menu dropdown-menu-end shadow">
<li><a class="dropdown-item" href="#">
<i class="bi bi-person"></i> Profile
</a></li>
<li><a class="dropdown-item" href="#">
<i class="bi bi-gear"></i> Settings
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ Rsx::Route('Login_Index_Controller', '#logout')->url() }}">
<i class="bi bi-box-arrow-right"></i> Sign Out
</a></li>
</ul>
</div>
@else
<a class="btn btn-outline-light btn-sm" href="{{ Rsx::Route('Login_Index_Controller', '#show_login')->url() }}">
Login
</a>
@endif
</div>
</div>
</div>
</nav>
{{-- Main Content Area --}}
<main class="main-content">
{{-- Flash Messages --}}
@php
$flash_alerts = \App\RSpade\Core\Rsx::render_flash_alerts();
@endphp
@if($flash_alerts)
<div class="container-fluid mt-3">
{!! $flash_alerts !!}
</div>
@endif
@yield('content')
</main>
</body>
</html>

View File

@@ -0,0 +1,16 @@
.{{ view_class }} {
// Component-specific styles
.card {
// Card styles
}
.btn-action {
// Button styles
}
// Responsive adjustments
@media (max-width: 768px) {
// Mobile styles
}
}

View File

@@ -0,0 +1,69 @@
@rsx_id('{{ layout_class }}')
@rsx_extends('{{ parent_layout_class }}')
@section('title')
@yield('submodule_title', '{{ submodule_title }}')
@endsection
@section('content')
<div class="{{ submodule_css_class }}">
<div class="container-fluid">
<div class="row">
{{-- Sidebar --}}
<div class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
<div class="position-sticky pt-3">
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase">
<span>{{ submodule_title }}</span>
</h6>
<ul class="nav flex-column mb-3">
{{--
Example navigation items with active state detection:
The active class is added using request()->is() to match URL patterns
--}}
<li class="nav-item">
<a class="nav-link {{ request()->is('{{ module_name }}/{{ submodule_name }}') || request()->is('{{ module_name }}/{{ submodule_name }}/index') ? 'active' : '' }}"
href="{{ Rsx::Route('{{ controller_prefix }}_Index_Controller')->url() }}">
<i class="bi bi-house"></i> Overview
</a>
</li>
{{-- Add more navigation items here following the same pattern --}}
{{--
<li class="nav-item">
<a class="nav-link {{ request()->is('{{ module_name }}/{{ submodule_name }}/feature*') ? 'active' : '' }}"
href="{{ Rsx::Route('{{ controller_prefix }}_Feature_Controller')->url() }}">
<i class="bi bi-star"></i> Feature
</a>
</li>
--}}
</ul>
@hasSection('sidebar_additional')
<hr class="mx-3">
@yield('sidebar_additional')
@endif
</div>
</div>
{{-- Main Content --}}
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
{{-- Page Header with Breadcrumbs --}}
<div class="row mb-4 mt-3">
<div class="col">
<h1 class="h3 mb-0">@yield('page_title', '{{ submodule_title }}')</h1>
<nav aria-label="breadcrumb" class="mt-2">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ Rsx::Route('{{ module_class }}_Index_Controller')->url() }}">{{ module_title }}</a></li>
<li class="breadcrumb-item">{{ submodule_title }}</li>
@yield('breadcrumb_items')
</ol>
</nav>
</div>
</div>
@yield('submodule_content')
</main>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,53 @@
.{{ submodule_css_class }} {
// Sidebar styles
.sidebar {
min-height: calc(100vh - 56px);
padding: 0;
.sidebar-heading {
font-size: 0.75rem;
text-transform: uppercase;
}
.nav-link {
color: #333;
padding: 0.5rem 1rem;
&.active {
color: #0d6efd;
background-color: rgba(13, 110, 253, 0.1);
border-left: 3px solid #0d6efd;
}
&:hover:not(.active) {
background-color: #f8f9fa;
}
i {
width: 20px;
text-align: center;
margin-right: 0.5rem;
}
}
}
// Main content area
main {
padding-bottom: 2rem;
}
// Card adjustments
.card {
border: 0;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
// Responsive adjustments
@media (max-width: 767px) {
.sidebar {
position: static;
min-height: auto;
padding: 1rem 0;
}
}
}

View File

@@ -0,0 +1,22 @@
@rsx_id('{{ view_class }}')
@rsx_extends('{{ extends_layout }}')
@section('title', '{{ page_title }}')
{{ section_declaration }}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">{{ page_title }}</h5>
</div>
<div class="card-body">
<p>Welcome to {{ page_title }}!</p>
{{-- Add your content here --}}
</div>
</div>
</div>
</div>
</div>
{{ section_end }}