#!/bin/bash # RSpade Framework Update Script # Pulls updates from rspade_upstream remote while preserving ./rsx in project mode # This script runs BEFORE Laravel loads to update framework code independently set -e # Parse arguments NO_REBUILD=false SHOW_DIFF=false STASH_CHANGES=false for arg in "$@"; do if [ "$arg" = "--no-rebuild" ]; then NO_REBUILD=true elif [ "$arg" = "--diff" ]; then SHOW_DIFF=true elif [ "$arg" = "--stash" ]; then STASH_CHANGES=true fi done # Configure git to ignore file mode changes git config core.fileMode false # Determine project mode: Check for .rspade-dev-environment or rsx/.git IS_PROJECT_MODE=false if [ -f ".rspade-dev-environment" ] || [ -d "rsx/.git" ]; then IS_PROJECT_MODE=true fi # In project mode, rsx/.git MUST exist if [ "$IS_PROJECT_MODE" = true ] && [ ! -d "rsx/.git" ]; then echo "ERROR: ./rsx is not a git repository." echo "" echo "The development environment appears corrupted." echo "Expected ./rsx to be an independent git repository." exit 1 fi # Check if framework is forked if [ -f ".rspade-forked-framework" ]; then echo "ERROR: Framework is in forked mode." echo "" echo "You have taken full ownership of the RSpade framework codebase." echo "Automatic updates are disabled to prevent overwriting your changes." echo "" echo "To manually update from upstream:" echo " 1. git fetch rspade_upstream" echo " 2. git diff rspade_upstream/rspade_selfupdate" echo " 3. Manually merge desired changes" echo " 4. Test thoroughly" echo "" echo "For detailed procedures: php artisan rsx:man framework_fork" exit 1 fi # Check .env for IS_FRAMEWORK_DEVELOPER=true if [ -f ".env" ]; then if grep -q "^IS_FRAMEWORK_DEVELOPER=true" .env; then echo "ERROR: This command is disabled for framework developers." echo "" echo "This command is for application developers to pull framework updates." echo "Framework developers should use standard git commands to manage the monorepo." echo "" echo "To use this command, set IS_FRAMEWORK_DEVELOPER=false in .env" exit 1 fi fi # Check APP_ENV for production if [ -f ".env" ]; then if grep -q "^APP_ENV=production" .env; then echo "ERROR: Framework updates are disabled in production mode." echo "" echo "Framework updates should be incorporated into and tested in a" echo "development environment before deploying to production." echo "" echo "Update in development, test thoroughly, then deploy to production." exit 1 fi fi # Verify rspade_upstream remote exists, auto-configure for project mode if ! git remote get-url rspade_upstream >/dev/null 2>&1; then # Auto-configure remote for project mode (when system/ is a submodule) if [ "$IS_PROJECT_MODE" = false ] && [ -f "../.gitmodules" ] && grep -q "path = system" ../.gitmodules 2>/dev/null; then echo "→ Configuring rspade_upstream remote (first-time setup)..." UPSTREAM_URL="ssh://git@privategit.hanson.xyz:3322/brianhansonxyz/rspade_system.git" if git remote add rspade_upstream "$UPSTREAM_URL" 2>&1; then echo " ✓ Remote configured: $UPSTREAM_URL" echo "" else echo "ERROR: Failed to configure rspade_upstream remote." exit 1 fi else # Framework dev mode or can't auto-detect - require manual configuration echo "ERROR: Remote \"rspade_upstream\" not configured." echo "" echo "Configure the upstream remote:" echo " git remote add rspade_upstream " exit 1 fi fi echo "" echo "=== Pull RSpade Framework Updates ===" echo "" # Detect current branch and set upstream branch CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>&1) # Handle detached HEAD state (common in git submodules) if [ "$CURRENT_BRANCH" = "HEAD" ]; then echo "⚠ Detected detached HEAD state (git submodule)" echo " Checking out master branch..." if ! git checkout master 2>&1; then echo "ERROR: Failed to checkout master branch" exit 1 fi CURRENT_BRANCH="master" echo " ✓ Now on master branch" echo "" fi UPSTREAM_BRANCH="rspade_upstream/${CURRENT_BRANCH}" if [ "$IS_PROJECT_MODE" = true ]; then echo "Mode: Project (./rsx will be preserved)" else echo "Mode: Framework development (./rsx will be updated)" fi echo "Branch: $CURRENT_BRANCH → $UPSTREAM_BRANCH" echo "" # Remove read-only attribute from CLAUDE.dist.md before update if [ -f "docs/CLAUDE.dist.md" ]; then echo "→ Removing read-only attribute from docs/CLAUDE.dist.md..." chmod u+w docs/CLAUDE.dist.md 2>/dev/null || true fi # Fetch updates first (before verifying branch exists) echo "Fetching updates from rspade_upstream..." if ! git fetch rspade_upstream 2>&1; then echo "ERROR: Failed to fetch updates" exit 1 fi echo " ✓ Updates fetched" # Verify upstream branch exists (after fetch) if ! git rev-parse "$UPSTREAM_BRANCH" >/dev/null 2>&1; then echo "ERROR: Upstream branch $UPSTREAM_BRANCH not found" echo "" echo "Current branch: $CURRENT_BRANCH" echo "Expected upstream: $UPSTREAM_BRANCH" echo "" echo "Available upstream branches:" git branch -r | grep rspade_upstream/ | sed 's/^/ /' exit 1 fi # Check if already up-to-date CURRENT_HEAD=$(git rev-parse HEAD 2>&1) UPSTREAM_HEAD=$(git rev-parse "$UPSTREAM_BRANCH" 2>&1) ALREADY_UP_TO_DATE=false if [ "$CURRENT_HEAD" = "$UPSTREAM_HEAD" ]; then echo "" echo "✓ Already up to date" echo "" ALREADY_UP_TO_DATE=true fi # Handle --diff option: show diff and exit if [ "$SHOW_DIFF" = true ]; then echo "" echo "=== Git Diff: Local Changes vs Upstream ===" echo "" git diff HEAD "$UPSTREAM_BRANCH" exit 0 fi # ============================================================================= # STEP: Clean up class override artifacts before checking for uncommitted changes # ============================================================================= # The manifest's class override system renames framework files to .upstream when # an rsx/ override exists. Before updating, we need to: # 1. Restore any deleted files (git checkout) # 2. Delete any .upstream files (the originals will be restored by git) # This ensures git sees a clean state, and the next manifest build will # re-apply any overrides with the updated framework files. # ============================================================================= echo "→ Cleaning up class override artifacts..." # Step 1: Restore deleted files DELETED_FILES=$(git status --porcelain 2>&1 | grep "^ D " | sed 's/^ D //' || true) if [ -n "$DELETED_FILES" ]; then echo " Restoring deleted files..." echo "$DELETED_FILES" | while read -r file; do if [ -n "$file" ]; then git checkout HEAD -- "$file" 2>/dev/null && echo " ✓ Restored: $file" || true fi done fi # Step 2: Delete .upstream files (framework files renamed by class override system) UPSTREAM_FILES=$(find . -name "*.upstream" -type f 2>/dev/null | grep -v "./rsx/" || true) if [ -n "$UPSTREAM_FILES" ]; then echo " Removing .upstream override markers..." echo "$UPSTREAM_FILES" | while read -r upstream_file; do if [ -n "$upstream_file" ] && [ -f "$upstream_file" ]; then rm -f "$upstream_file" && echo " ✓ Removed: $upstream_file" || true fi done fi # Step 3: Restore files with ONLY use statement changes # When classes are overridden in rsx/, Php_Fixer updates use statements in system/ files # to point to the Rsx\ namespace. These changes should be reverted before framework update. # We only auto-revert files where ALL changes are use statement modifications. echo " Checking for auto-fixable use statement changes..." MODIFIED_SYSTEM_FILES=$(git diff --name-only -- . ":(exclude)rsx" 2>/dev/null | grep "\.php$" || true) USE_STMT_REVERTED=0 if [ -n "$MODIFIED_SYSTEM_FILES" ]; then while IFS= read -r modified_file; do if [ -z "$modified_file" ] || [ ! -f "$modified_file" ]; then continue fi # Get the diff for this file, check if ALL changed lines are use statements # Changed lines start with + or - (excluding the diff header lines +++ and ---) DIFF_CONTENT=$(git diff -- "$modified_file" 2>/dev/null || true) if [ -z "$DIFF_CONTENT" ]; then continue fi # Extract changed lines (+ or - at start, not +++ or ---) # Then check if all of them are use statements or empty ALL_USE_STATEMENTS=true while IFS= read -r line; do # Skip diff metadata lines case "$line" in "+++"*|"---"*|"@@"*|"diff "*|"index "*) continue ;; esac # Check lines that start with + or - (actual changes) case "$line" in "+"*|"-"*) # Remove the leading +/- and trim whitespace content="${line#[+-]}" content=$(echo "$content" | sed 's/^[[:space:]]*//') # Empty lines are OK if [ -z "$content" ]; then continue fi # Use statements are OK (with or without leading backslash) if echo "$content" | grep -qE "^use[[:space:]]+(\\\\)?[A-Za-z]"; then continue fi # Any other change means this file has non-use-statement modifications ALL_USE_STATEMENTS=false break ;; esac done <<< "$DIFF_CONTENT" # If all changes were use statements, revert the file if [ "$ALL_USE_STATEMENTS" = true ]; then if git checkout HEAD -- "$modified_file" 2>/dev/null; then echo " ✓ Reverted use statements: $modified_file" USE_STMT_REVERTED=$((USE_STMT_REVERTED + 1)) fi fi done <<< "$MODIFIED_SYSTEM_FILES" fi if [ "$USE_STMT_REVERTED" -gt 0 ]; then echo " ✓ Reverted $USE_STMT_REVERTED file(s) with use statement changes" fi echo " ✓ Override cleanup complete" echo "" # In project mode, check for uncommitted changes outside ./rsx if [ "$IS_PROJECT_MODE" = true ]; then # Get uncommitted changes (modified, staged, deleted) excluding ./rsx UNCOMMITTED_CHANGES=$(git diff --name-only -- . ":(exclude)rsx" 2>&1) STAGED_CHANGES=$(git diff --cached --name-only -- . ":(exclude)rsx" 2>&1) # Combine and deduplicate (|| true prevents grep exit code 1 from killing script) ALL_CHANGES=$(echo -e "${UNCOMMITTED_CHANGES}\n${STAGED_CHANGES}" | sort -u | grep -v "^$" || true) if [ -n "$ALL_CHANGES" ]; then if [ "$STASH_CHANGES" = false ]; then echo "" echo "════════════════════════════════════════════════════════════════════════════════" echo "❌ ERROR: Uncommitted framework changes detected" echo "════════════════════════════════════════════════════════════════════════════════" echo "" echo "You are in PROJECT MODE where ./rsx is your application repository." echo "Framework files outside ./rsx should NEVER be modified in project mode." echo "" echo "The following framework files have uncommitted changes:" echo "" echo "$ALL_CHANGES" | sed 's/^/ /' echo "" echo "These changes must be resolved before updating the framework." echo "" echo "OPTIONS:" echo "" echo " 1. VIEW DIFFERENCES" echo " See what changed between your local files and upstream:" echo "" echo " php artisan rsx:framework:pull --diff" echo "" echo " 2. STASH CHANGES AND UPDATE (recommended)" echo " Your changes will be stashed automatically, then the framework" echo " will be updated. You can review or restore the stashed changes after:" echo "" echo " php artisan rsx:framework:pull --stash" echo "" echo " After update, review your stashed changes:" echo " git stash list" echo " git stash show" echo " git stash pop (to restore)" echo "" echo " 3. DISCARD FRAMEWORK CHANGES" echo " If these were accidental edits, reset them:" echo "" echo " git checkout HEAD -- [file paths]" echo "" echo " Or reset all framework changes:" echo " git checkout HEAD -- . ':!rsx'" echo "" echo " 4. COMMIT THEN UPDATE (framework developers only)" echo " If you're intentionally modifying the framework:" echo "" echo " git add -A" echo " git commit -m \"Framework modifications\"" echo " php artisan rsx:framework:pull" echo "" echo " 5. FORK THE FRAMEWORK" echo " If you need to preserve framework modifications permanently:" echo "" echo " php artisan rsx:man framework_fork" echo "" echo " This documents taking full control of the framework codebase." echo "" echo "════════════════════════════════════════════════════════════════════════════════" echo "Note to LLM agents:" echo "════════════════════════════════════════════════════════════════════════════════" echo "" echo "You have attempted to update the framework with uncommitted changes present." echo "" echo "REQUIRED ACTIONS:" echo "" echo " 1. Show the developer the full list of uncommitted files above" echo " 2. Run 'git diff' to show what changed in those files" echo " 3. Ask the developer explicitly: 'These framework files were modified." echo " Do you want me to discard these changes and update the framework," echo " or stash them and proceed with the update?'" echo "" echo "You MUST NOT proceed until the developer has made a decision." echo "You MUST NOT automatically use --stash without explicit permission." echo "" echo "════════════════════════════════════════════════════════════════════════════════" exit 1 else # Stash changes with descriptive message echo "" echo "⚠️ Stashing uncommitted framework changes..." echo "" STASH_MESSAGE="Auto-stashed by framework update with --stash on $(date '+%Y-%m-%d %H:%M:%S')" if ! git stash push -u -m "$STASH_MESSAGE" -- . ":(exclude)rsx" 2>&1; then echo "ERROR: Failed to stash changes" exit 1 fi echo " ✓ Changes stashed" echo "" echo "Your changes have been saved. After update completes, you can review them:" echo " git stash list" echo " git stash show" echo " git stash pop (to restore changes)" echo "" fi fi fi # In framework mode, check for uncommitted changes if [ "$IS_PROJECT_MODE" = false ]; then # Get uncommitted changes (modified, staged, deleted) UNCOMMITTED_CHANGES=$(git diff --name-only 2>&1) STAGED_CHANGES=$(git diff --cached --name-only 2>&1) # Combine and deduplicate (|| true prevents grep exit code 1 from killing script) ALL_CHANGES=$(echo -e "${UNCOMMITTED_CHANGES}\n${STAGED_CHANGES}" | sort -u | grep -v "^$" || true) if [ -n "$ALL_CHANGES" ]; then if [ "$STASH_CHANGES" = false ]; then echo "" echo "════════════════════════════════════════════════════════════════════════════════" echo "❌ ERROR: Uncommitted changes detected" echo "════════════════════════════════════════════════════════════════════════════════" echo "" echo "You are in FRAMEWORK MODE where this repository is the framework itself." echo "The following files have uncommitted changes:" echo "" echo "$ALL_CHANGES" | sed 's/^/ /' echo "" echo "These changes must be resolved before updating the framework." echo "" echo "OPTIONS:" echo "" echo " 1. VIEW DIFFERENCES" echo " See what changed between your local files and upstream:" echo "" echo " php artisan rsx:framework:pull --diff" echo "" echo " 2. STASH CHANGES AND UPDATE (recommended)" echo " Your changes will be stashed automatically, then the framework" echo " will be updated. You can review or restore the stashed changes after:" echo "" echo " php artisan rsx:framework:pull --stash" echo "" echo " After update, review your stashed changes:" echo " git stash list" echo " git stash show" echo " git stash pop (to restore)" echo "" echo " 3. DISCARD CHANGES" echo " If these were accidental edits, reset them:" echo "" echo " git reset --hard HEAD" echo "" echo " 4. COMMIT THEN UPDATE" echo " If you're intentionally modifying the framework:" echo "" echo " git add -A" echo " git commit -m \"Framework modifications\"" echo " php artisan rsx:framework:pull" echo "" echo "════════════════════════════════════════════════════════════════════════════════" exit 1 else # Stash changes with descriptive message echo "" echo "⚠️ Stashing uncommitted changes..." echo "" STASH_MESSAGE="Auto-stashed by framework update with --stash on $(date '+%Y-%m-%d %H:%M:%S')" if ! git stash push -u -m "$STASH_MESSAGE" 2>&1; then echo "ERROR: Failed to stash changes" exit 1 fi echo " ✓ Changes stashed" echo "" echo "Your changes have been saved. After update completes, you can review them:" echo " git stash list" echo " git stash show" echo " git stash pop (to restore changes)" echo "" fi fi fi # Apply framework updates (skip if already up to date) if [ "$ALREADY_UP_TO_DATE" = false ]; then # Extract and display changelog echo "" echo "⚠ RSpade framework has upstream changes" echo "" # Get commit subjects, filter out Claude Code attribution and blank lines CHANGELOG=$(git log --pretty=format:"%s" HEAD.."$UPSTREAM_BRANCH" 2>&1 | \ grep -v "Claude Code" | \ grep -v "Co-Authored-By:" | \ grep -v "^$" | \ sed 's/^/ • /') if [ -n "$CHANGELOG" ]; then echo "Changelog:" echo "" echo "$CHANGELOG" echo "" fi # Show file diff stats (exclude ./rsx if in project mode) if [ "$IS_PROJECT_MODE" = true ]; then DIFF_STATS=$(git diff --stat HEAD.."$UPSTREAM_BRANCH" -- . ":(exclude)rsx" 2>&1) else DIFF_STATS=$(git diff --stat HEAD.."$UPSTREAM_BRANCH" 2>&1) fi if [ -n "$DIFF_STATS" ]; then echo "Files that will be updated:" echo "" echo "$DIFF_STATS" | sed 's/^/ /' echo "" fi # Apply updates based on mode echo "Applying framework updates..." if [ "$IS_PROJECT_MODE" = true ]; then # Project mode: merge with --no-ff (preserves ./rsx via .gitattributes) if ! git merge --no-ff "$UPSTREAM_BRANCH" 2>&1; then echo "ERROR: Framework update failed." echo "" # Show current git state GIT_STATUS=$(git status --short 2>&1) if [ -n "$GIT_STATUS" ]; then echo "Current git state:" echo "" echo "$GIT_STATUS" | sed 's/^/ /' echo "" fi echo "Check status: php artisan rsx:framework:status" exit 1 fi echo " ✓ Framework updated (./rsx preserved by .gitattributes)" # Remove files that were deleted upstream (git merge doesn't auto-delete) echo " → Cleaning up deleted files..." DELETED_FILES=$(git diff --name-only --diff-filter=D HEAD~1 HEAD -- rsx/ 2>/dev/null || true) if [ -n "$DELETED_FILES" ]; then echo "$DELETED_FILES" | while read -r file; do if [ -f "$file" ]; then rm -f "$file" echo " Removed: $file" fi done fi else # Framework mode: fast-forward only (updates everything including ./rsx) MERGE_OUTPUT=$(git merge --ff-only "$UPSTREAM_BRANCH" 2>&1) || { if echo "$MERGE_OUTPUT" | grep -q "up to date\|up-to-date"; then echo " ✓ Already up to date" else echo "ERROR: Framework update failed." echo "" echo "ERROR: Cannot fast-forward: you have local modifications" echo "" # Show modified files GIT_STATUS=$(git status --short 2>&1) if [ -n "$GIT_STATUS" ]; then echo "Modified files blocking update:" echo "" echo "$GIT_STATUS" | sed 's/^/ /' echo "" fi echo "To resolve:" echo "" echo " 1. VIEW DIFFERENCES" echo " php artisan rsx:framework:pull --diff" echo "" echo " 2. STASH CHANGES AND UPDATE (recommended)" echo " php artisan rsx:framework:pull --stash" echo "" echo " 3. DISCARD MODIFICATIONS" echo " git reset --hard $UPSTREAM_BRANCH" echo "" echo " 4. COMMIT THEN UPDATE" echo " git add -A" echo " git commit -m \"Framework modifications\"" echo " php artisan rsx:framework:pull" echo "" echo " 5. CHECK STATUS" echo " php artisan rsx:framework:status" exit 1 fi } echo " ✓ Framework updated" fi fi echo "" if [ "$ALREADY_UP_TO_DATE" = true ]; then echo "→ Running framework maintenance (migrations and cache rebuild)..." else echo "✓ Framework updated successfully" fi echo "" if [ "$IS_PROJECT_MODE" = true ] && [ "$ALREADY_UP_TO_DATE" = false ]; then echo "Your application code in ./rsx was preserved." echo "" fi # Rebuild caches after framework update if [ "$NO_REBUILD" = true ]; then echo "⚠️ Cleaning caches only (--no-rebuild specified)" echo "" if ! php artisan rsx:clean --silent; then echo "ERROR: Cache clean failed" exit 1 fi echo " ✓ Caches cleaned" echo "" echo "⚠ Manifest and bundles NOT rebuilt - you must run manually:" echo " php artisan rsx:manifest:build" echo " php artisan rsx:bundle:compile" echo "" else echo "⚠️ Regenerating framework caches" echo "" echo " This rebuild is only required because the framework itself was updated." echo " Never run these commands manually unless there are catastrophic errors." echo "" # Step 1: Clean caches echo "Step 1/4: Cleaning caches..." if ! php artisan rsx:clean --silent; then echo "ERROR: Cache clean failed" exit 1 fi echo " ✓ Caches cleaned" echo "" # Step 2: Rebuild manifest echo "Step 2/4: Rebuilding manifest..." if ! php artisan rsx:manifest:build; then echo "ERROR: Manifest rebuild failed" echo " Run manually: php artisan rsx:manifest:build" exit 1 fi echo " ✓ Manifest rebuilt" echo "" # Step 3: Run framework migrations (before bundle compile) echo "Step 3/4: Running framework migrations..." if ! php artisan migrate --framework-only --force; then echo "ERROR: Framework migrations failed" echo " Check errors above and fix migration issues" echo "" echo " Bundle compilation skipped due to migration failure" exit 1 fi echo " ✓ Framework migrations completed" echo "" # Step 4: Compile bundles (only if migrations succeeded) echo "Step 4/4: Compiling all bundles..." if ! php artisan rsx:bundle:compile; then echo "ERROR: Bundle compilation failed" echo " Check errors above and fix bundle issues" exit 1 fi echo " ✓ All bundles compiled" echo "" fi # Set read-only attribute on CLAUDE.dist.md after successful update if [ -f "docs/CLAUDE.dist.md" ]; then echo "→ Setting read-only attribute on docs/CLAUDE.dist.md..." chmod u-w docs/CLAUDE.dist.md 2>/dev/null || true echo " ✓ Framework documentation marked read-only" echo "" fi echo "✅ Framework update complete" echo "" exit 0