#!/bin/bash # # PC2 Safe Update / Recovery Script # # Use this when: # - the GUI auto-updater is stuck or has failed # - you're on a v1.2.0 / v1.2.1 / v1.2.2 node and the auto-updater # can't carry you forward (older UpdateService.ts had bugs that # this script works around) # - you suspect the install is half-finished and want a forced clean # re-sync to whatever's on origin/main # # Run from inside the pc2.net repo: # cd ~/pc2.net && bash scripts/update.sh # # Or one-line from anywhere: # curl -fsSL https://raw.githubusercontent.com/Elacity/pc2.net/main/scripts/update.sh | bash # set -e # Source nvm if installed so that node/npm/pm2 (commonly installed via # `npm i -g pm2` under nvm-managed Node) are on PATH. Without this, the # script's bare bash environment doesn't see ~/.nvm/versions/node/*/bin # and `pm2 stop` fails with "command not found", breaking step 1 even # though pm2 is installed and working in the user's interactive shell. # v1.2.4's update.sh shipped without this and broke for users on a # nvm-managed pm2 install (reported by 4HM3D, Apr 30 2026). export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" if [[ -s "$NVM_DIR/nvm.sh" ]]; then # shellcheck source=/dev/null \. "$NVM_DIR/nvm.sh" fi # Last-ditch: probe the standard nvm-managed pm2 location and add it to # PATH if the symlink chain is intact. This catches setups where nvm.sh # isn't installed but pm2 is at a known location. if ! command -v pm2 >/dev/null 2>&1; then for npm_bin_dir in "$HOME"/.nvm/versions/node/*/bin /usr/local/bin /opt/homebrew/bin; do if [[ -x "$npm_bin_dir/pm2" ]]; then export PATH="$npm_bin_dir:$PATH" break fi done fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PC2_DIR="$(dirname "$SCRIPT_DIR")" PC2_NODE_DIR="$PC2_DIR/pc2-node" # Allow running via `curl | bash` outside the repo: try common install # locations and fall back to a clear error. if [[ ! -d "$PC2_NODE_DIR" ]]; then for candidate in "$HOME/pc2.net" "$HOME/.pc2"; do if [[ -d "$candidate/pc2-node" ]]; then PC2_DIR="$candidate" PC2_NODE_DIR="$candidate/pc2-node" SCRIPT_DIR="$candidate/scripts" break fi done fi if [[ ! -d "$PC2_NODE_DIR" ]]; then echo "❌ Cannot find pc2-node directory." echo " Looked in: $PC2_DIR/pc2-node, ~/pc2.net/pc2-node, ~/.pc2/pc2-node" echo " Run this script from inside the pc2.net repo, or clone it first:" echo " git clone https://github.com/Elacity/pc2.net ~/pc2.net && bash ~/pc2.net/scripts/update.sh" exit 1 fi # ───────────────────────────────────────────────────────────────────── # Safety: refuse to obliterate UNCOMMITTED USER CODE. Step 4 below does # `git reset --hard origin/main`, which is exactly what a developer # WOULDN'T want if they accidentally ran this script from inside their # dev tree. # # Production nodes routinely have benign drift in BUILD ARTIFACTS: # - package.json / package-lock.json at every level (npm normalisation # across node versions: lockfileVersion bumps, hoisting metadata, # trailing newline differences, dependency dedup, etc.) # - frontend/ (regenerated by `npm run build:frontend`) # - dist/ (regenerated by `tsc`) # - particle-auth/assets/ (regenerated by particle-auth build) # # These are NOT "user code changes" and the safety guard should NOT # block on them. v1.2.5 update.sh treated them as user changes and # refused to run, requiring users to manually `git checkout` or set # PC2_UPDATE_FORCE=1. v1.2.6 auto-discards build artifacts and only # blocks on real source code drift. v1.2.7.1 widens the auto-discard # list to include ALL package.json / package-lock.json files because # WSL operators were hitting a `git pull` abort on `package.json`, # `packages/particle-auth/package.json`, etc. — those files normalise # differently across npm versions and operators never legitimately # edit them on production nodes. # # Override with `PC2_UPDATE_FORCE=1` if you actually do want to nuke # all local changes (e.g. corrupted tree on a production node). # ───────────────────────────────────────────────────────────────────── cd "$PC2_DIR" if [[ -z "$PC2_UPDATE_FORCE" ]]; then # Auto-reset known-safe build artifacts before checking for real drift. # `git reset --hard origin/main` will pull the upstream versions back # anyway, so resetting them here is purely cosmetic — it just makes the # safety check below see a clean tree. # # v1.2.7.1: include the four files commonly seen drifting on update # (root package.json + lockfile, particle-auth package.json, # pc2-node lockfile). Glob fallback below catches any future additions. BUILD_ARTIFACTS=( "package.json" "package-lock.json" "packages/particle-auth/package.json" "pc2-node/package.json" "pc2-node/package-lock.json" ) for f in "${BUILD_ARTIFACTS[@]}"; do if [[ -f "$f" ]] && ! git diff --quiet -- "$f" 2>/dev/null; then git checkout -- "$f" 2>/dev/null || true fi done # Belt-and-braces glob fallback: catch any package.json / package-lock.json # under packages/ or pc2-node/ that gets added in future without # someone remembering to update BUILD_ARTIFACTS above. `git ls-files` # only matches tracked files, so this can't accidentally touch # operator-created files outside the repo. while IFS= read -r f; do [[ -z "$f" ]] && continue if ! git diff --quiet -- "$f" 2>/dev/null; then git checkout -- "$f" 2>/dev/null || true fi done < <(git ls-files 'packages/**/package.json' 'packages/**/package-lock.json' 'pc2-node/package.json' 'pc2-node/package-lock.json' 2>/dev/null) # Frontend dirs are regenerated by build:frontend / build:gui — the # auto-updater wipes and rebuilds them. Treat any drift here as benign. git checkout -- pc2-node/frontend/ 2>/dev/null || true git checkout -- pc2-node/dist/ 2>/dev/null || true git checkout -- src/particle-auth/assets/ 2>/dev/null || true # Now check what's left. Anything still showing as modified/deleted # is genuine source-code drift and we should stop. DIRTY="$(git status --porcelain 2>/dev/null | grep -v '^??' | head -1)" if [[ -n "$DIRTY" ]]; then echo "❌ Refusing to run: working tree at $PC2_DIR has uncommitted source changes." echo "" echo " First file flagged: $DIRTY" echo " Full status: cd $PC2_DIR && git status" echo "" echo " This script does 'git reset --hard origin/main' which would" echo " destroy those changes. If you really want to proceed (e.g. on" echo " a corrupted production node), re-run with PC2_UPDATE_FORCE=1:" echo "" # Detect invocation mode and print the right recovery command. # When run via curl|bash, $0 is just 'bash' (no path), so 'bash $0' # would render as 'bash bash' which is broken. Detect this and # print a curl|bash form instead. if [[ "$0" == "bash" || "$0" == "-bash" || ! -f "$0" ]]; then echo " PC2_UPDATE_FORCE=1 bash <(curl -fsSL https://raw.githubusercontent.com/Elacity/pc2.net/main/scripts/update.sh)" else echo " cd $PC2_DIR && PC2_UPDATE_FORCE=1 bash $0" fi echo "" exit 1 fi fi # Defence-in-depth: every npm command in this script runs with HUSKY=0 # regardless of which version of package.json is on disk. Older # package.json versions had `"prepare": "husky"` which crashed npm # install on production nodes (no husky binary). The v1.2.4 package.json # wraps it in `husky 2>/dev/null || true`, but if the tree is in a # half-updated state the OLD package.json might still be present until # git reset completes — so we belt-and-braces it here too. export HUSKY=0 export CI=true echo "╔══════════════════════════════════════════════════════════════╗" echo "║ PC2 Safe Update / Recovery ║" echo "║ Repo: $PC2_DIR" echo "╚══════════════════════════════════════════════════════════════╝" echo "" # Verify pm2 is actually reachable now that we've sourced nvm + probed # common locations. If it's still missing the user genuinely needs to # install it, and we should say so clearly. if ! command -v pm2 >/dev/null 2>&1; then echo "❌ pm2 is required but not on PATH." echo "" echo " Install it (one of):" echo " npm install -g pm2" echo " # or with nvm:" echo " source ~/.nvm/nvm.sh && nvm use default && npm install -g pm2" echo "" echo " Then re-run this script." exit 1 fi echo "✓ pm2 found at: $(command -v pm2)" echo "" # ───────────────────────────────────────────────────────────────────── # Step 1: stop PC2 cleanly so we don't fight ourselves while building # ───────────────────────────────────────────────────────────────────── echo "📛 Step 1: Stopping PC2..." pm2 stop pc2 2>/dev/null || true sleep 2 echo "🔪 Step 2: Killing orphaned processes..." pm2 delete pc2 2>/dev/null || true pkill -9 -f "node.*pc2-node.*dist/index" 2>/dev/null || true pkill -9 -f "node.*dist/index.js" 2>/dev/null || true sleep 3 echo "🔍 Step 3: Verifying ports are free..." for port in 4200 4001 4002; do if lsof -i :$port >/dev/null 2>&1; then echo " ⚠️ Port $port still in use, force killing..." fuser -k $port/tcp 2>/dev/null || true sleep 2 fi done if lsof -i :4200 >/dev/null 2>&1; then echo "❌ ERROR: Port 4200 still in use after cleanup. Manually kill the holder:" lsof -i :4200 exit 1 fi echo " ✅ All ports free" # ───────────────────────────────────────────────────────────────────── # Step 3b (v1.2.7): Migrate inline secrets out of ecosystem.config.cjs. # # Background: prior to v1.2.7, the only way to set cluster-pin / AI / # Lit / RPC credentials for pm2 was to edit ecosystem.config.cjs # directly. Step 4 below does `git reset --hard origin/main`, which # wipes any operator edits to that tracked file — so any node that # customised ecosystem.config.cjs would silently lose its credentials # on the next update. # # v1.2.7 introduces pc2-node/.env (gitignored, survives git reset) # as the supported home for these secrets, and ecosystem.config.cjs # now reads them from process.env via dotenv. # # This block scans the live ecosystem.config.cjs for known credential # vars and copies any non-empty inline values into pc2-node/.env # BEFORE git reset blows them away. We only copy vars that aren't # already present in .env (so an operator who already migrated by # hand isn't silently overwritten). # # Strictly opt-in & narrowly-scoped: we don't try to parse arbitrary # JS — we grep for `VAR_NAME: "value"` patterns on a hard-coded # allowlist of names. If parsing fails (commented-out, multiline, …) # we just skip; worst case the operator has to re-set the var by # hand, same as today. # ───────────────────────────────────────────────────────────────────── ECO_FILE="$PC2_DIR/ecosystem.config.cjs" ENV_FILE="$PC2_NODE_DIR/.env" if [[ -f "$ECO_FILE" ]]; then MIGRATED_VARS=() # Allowlist of vars we know are intended for .env. This list mirrors # pc2-node/.env.example. Adding a new var here = explicit opt-in. MIGRATABLE_VARS=( SUPERNODE_CLUSTER_PIN_URL SUPERNODE_CLUSTER_PIN_TOKEN SUPERNODE_CLUSTER_PIN_REPLICATION_MIN SUPERNODE_CLUSTER_PIN_REPLICATION_MAX NODE_TLS_REJECT_UNAUTHORIZED ELACITY_PIN_FORWARD_URL SUPERNODE_PIN_MIRRORS ANTHROPIC_API_KEY OPENAI_API_KEY GOOGLE_API_KEY XAI_API_KEY OLLAMA_BASE_URL TELEGRAM_BOT_TOKEN LIT_ACTION_CID MEDIA_ACTION_CID LIT_BACKEND SUPERNODE_RPC_URLS ) for var in "${MIGRATABLE_VARS[@]}"; do # Match `VAR: "value"` — the canonical inline form. Skip if the # value is empty string OR a `process.env.X || ""` fallback (these # are the post-v1.2.7 forms and don't need migrating). VAL="$(grep -oE "${var}:[[:space:]]*\"[^\"]+\"" "$ECO_FILE" 2>/dev/null | head -1 | sed -E "s/^${var}:[[:space:]]*\"(.*)\"$/\1/")" if [[ -z "$VAL" ]]; then continue fi # Don't overwrite an existing .env entry — operator's choice wins. if [[ -f "$ENV_FILE" ]] && grep -q "^${var}=" "$ENV_FILE" 2>/dev/null; then continue fi # First-time write: ensure pc2-node dir exists (paranoia — it # always should, but if it doesn't `>>` would create a file with # no parent dir and that's harder to debug). mkdir -p "$PC2_NODE_DIR" if [[ ! -f "$ENV_FILE" ]]; then cat > "$ENV_FILE" << 'ENV_HEADER' # Auto-created by scripts/update.sh during v1.2.7 migration. # Original values were inlined in ecosystem.config.cjs and would have # been wiped by `git reset --hard origin/main`. They are now stored # here (gitignored, survives updates) and read by dotenv at boot. # # To rotate: edit a value, then either: # pm2 reload ecosystem.config.cjs --only pc2 --update-env # picks up new env # bash scripts/update.sh # full update path # # See pc2-node/.env.example for the full catalogue. ENV_HEADER fi echo "${var}=${VAL}" >> "$ENV_FILE" MIGRATED_VARS+=("$var") done if [[ ${#MIGRATED_VARS[@]} -gt 0 ]]; then echo "🔐 Step 3b: Migrated ${#MIGRATED_VARS[@]} inline secret(s) from ecosystem.config.cjs to pc2-node/.env:" for v in "${MIGRATED_VARS[@]}"; do echo " - $v" done echo " (these would have been wiped by 'git reset --hard' in step 4)" fi fi # ───────────────────────────────────────────────────────────────────── # Step 4: Pull latest code (force-reset, no merges) # ───────────────────────────────────────────────────────────────────── echo "" echo "📥 Step 4: Pulling latest code from origin/main..." cd "$PC2_DIR" git fetch origin main git reset --hard origin/main # Drop any half-installed asset artefacts from a broken previous run git clean -fd src/particle-auth/assets/ 2>/dev/null || true # ───────────────────────────────────────────────────────────────────── # Step 5: Install at BOTH root AND pc2-node. # # This is the critical step that v1.2.0 / v1.2.1 / v1.2.2 in-app # UpdateService got wrong — it only ran `npm install` in pc2-node, so # any new dep added at the root (consumed via `await import()` from # pc2-node) was missing on the next boot. We always install both. # ───────────────────────────────────────────────────────────────────── echo "" echo "📦 Step 5: Installing root dependencies..." cd "$PC2_DIR" npm install --legacy-peer-deps --no-fund --no-audit echo "" echo "📦 Step 6: Installing pc2-node dependencies..." cd "$PC2_NODE_DIR" npm install --legacy-peer-deps --include=dev --no-fund --no-audit # ───────────────────────────────────────────────────────────────────── # Step 7: Rebuild native modules. # # v1.2.7: dropped the --build-from-source for `better-sqlite3` because # it was migrated to `@photostructure/sqlite` — a Node-API library # whose prebuilds are bundled inside the npm tarball and work across # all Node 20+ majors. No per-Node-version compile needed; no Xcode # CLT requirement on Mac. # # We still run `npm rebuild` (without --build-from-source) so that # any other native module (bcrypt, sharp, node-datachannel, etc) gets # a chance to use a fresh prebuild matching the current Node ABI. # ───────────────────────────────────────────────────────────────────── echo "" echo "🔨 Step 7: Rebuilding native modules..." # Plain `npm rebuild` lets prebuild-install resolve prebuilt binaries # when available. For node-datachannel specifically, this is the right # choice: --build-from-source forces a cmake-js build that needs cmake # installed (most users don't have it). npm rebuild 2>&1 || echo " ⚠️ Some optional native modules didn't rebuild (non-fatal — see above)" # ───────────────────────────────────────────────────────────────────── # Step 7b: Native module verification gauntlet. # # Each critical native module gets THREE attempts: # 1. Plain load — works when prebuild-install resolved cleanly. # 2. (Already done above for everything via `npm rebuild`.) # 3. Clean reinstall — wipes node_modules/MOD, runs `npm install MOD` # which forces a fresh prebuild-install query against the CURRENT # Node ABI. This is what Ahmed had to do manually after v1.2.4 # silent-shipped a broken node-datachannel — `npm rebuild` reuses # stale install metadata, only a clean reinstall queries fresh. # # If both steps fail, exit with a fix-it-yourself hint that's specific # to the module. # ───────────────────────────────────────────────────────────────────── echo "" echo "🧪 Step 7b: Verifying critical native modules load..." # @photostructure/sqlite (CJS, v1.2.7+) — verify by initialising an # in-memory db. The library bundles prebuilds for every supported # platform inside its npm tarball and uses Node-API, so the historical # "compile from source needs Xcode CLT" failure mode is gone. This # gauntlet now mostly catches genuine node_modules corruption (partial # extraction, disk full mid-install, etc). if ! node -e "const { DatabaseSync } = require('@photostructure/sqlite'); new DatabaseSync(':memory:').prepare('SELECT 1').get()" >/dev/null 2>&1; then echo " ⚠️ @photostructure/sqlite doesn't load — clean reinstalling..." rm -rf node_modules/@photostructure/sqlite npm install @photostructure/sqlite --legacy-peer-deps 2>&1 || true if ! node -e "const { DatabaseSync } = require('@photostructure/sqlite'); new DatabaseSync(':memory:').prepare('SELECT 1').get()" >/dev/null 2>&1; then echo "❌ @photostructure/sqlite cannot be made to load." echo " This is unusual — the library ships prebuilds for all platforms." echo " Try a clean reinstall: rm -rf node_modules package-lock.json && npm install" node -e "const { DatabaseSync } = require('@photostructure/sqlite'); new DatabaseSync(':memory:').prepare('SELECT 1').get()" 2>&1 exit 1 fi echo " ✅ @photostructure/sqlite recovered via clean reinstall" else echo " ✅ @photostructure/sqlite verified" fi # node-datachannel (ESM) — verify via dynamic import. if ! node -e "import('node-datachannel').then(m => { if (!m) throw new Error('null'); }).catch(e => { console.error(e.message); process.exit(1); })" >/dev/null 2>&1; then echo " ⚠️ node-datachannel doesn't load — clean reinstalling..." rm -rf node_modules/node-datachannel npm install node-datachannel --legacy-peer-deps 2>&1 || true if ! node -e "import('node-datachannel').then(m => { if (!m) throw new Error('null'); }).catch(e => { console.error(e.message); process.exit(1); })" >/dev/null 2>&1; then echo "❌ node-datachannel cannot be made to load. Try:" if [[ "$(uname -s)" == "Darwin" ]]; then echo " brew install cmake" else echo " sudo apt install cmake" fi echo " cd $PC2_NODE_DIR && rm -rf node_modules/node-datachannel && npm install node-datachannel" echo " Then re-run this script." node -e "import('node-datachannel').then(()=>console.log('would have loaded')).catch(e => console.error(e.message))" 2>&1 exit 1 fi echo " ✅ node-datachannel recovered via clean reinstall" else echo " ✅ node-datachannel verified" fi # ───────────────────────────────────────────────────────────────────── # Step 8: Build everything (gui + backend + frontend). # # Order matters: # 1. build:frontend (pc2-node) — wipes frontend/, repopulates from # src/wallet-bridge + static-assets + GUI bundle. THIS is what # copies pc2-secure-view.js into a place the browser can load it. # v1.2.0/1/2/3 update flows skipped this and so wallet-bridge # fixes never reached the browser. # 2. build:gui (root) — guarantees the desktop bundle is fresh. # 3. build:backend (pc2-node) — tsc. # ───────────────────────────────────────────────────────────────────── echo "" echo "🔨 Step 8: Syncing wallet-bridge files (build:frontend)..." cd "$PC2_NODE_DIR" # v1.2.7.8: presence check + loud failure. Previously this was # `npm run build:frontend || echo "..."` which conflated two outcomes: # (a) script not defined on this revision → legitimately skip (old v1.2.0/1/2/3 layouts) # (b) script failed (OOM, npm install failure, etc.) → silently swallowed # Then step 9 (build:gui) hit the same OOM and died with a misleading # "build failed" message. Splitting the cases lets real failures abort # under `set -e` while still skipping cleanly on older layouts. if grep -q '"build:frontend":' "$PC2_NODE_DIR/package.json" 2>/dev/null; then npm run build:frontend else echo " ⚠️ build:frontend script not defined in this revision, skipping" fi echo "" echo "🔨 Step 9: Building GUI bundle..." cd "$PC2_DIR" npm run build:gui echo "" echo "🔨 Step 10: Compiling backend..." cd "$PC2_NODE_DIR" npm run build:backend # Step 11 (sqlite ABI re-verify) was merged into Step 7b above — the # verification gauntlet now covers both @photostructure/sqlite AND # node-datachannel with the same clean-reinstall fallback pattern. # ───────────────────────────────────────────────────────────────────── # Step 11: Install transport sudoers (v1.2.7.9+). # # wg-quick and awg-quick on macOS+Linux need root to create network # interfaces. Without a sudoers.d entry, pc2-node (running headless # under pm2 with no TTY) cannot escalate, the bring-up fails, and # the connectivity cascade silently falls to ActiveProxy. # # This step asks the user for their password ONCE during update so the # bring-up at runtime works without further prompts. The grant is # scoped to ONLY the bundled wg-quick + awg-quick binaries — no broader # privilege escalation. Removing /etc/sudoers.d/pc2-wireguard revokes. # # On macOS launcher users (who don't run update.sh directly), the # WireGuardService.connect() runtime auto-prompt picks this up via # osascript GUI dialog. Here we cover terminal-update users on both # macOS and Linux. # # The script is idempotent + fail-soft: already-installed entries skip, # missing binaries skip with a hint, headless contexts skip cleanly. # ───────────────────────────────────────────────────────────────────── echo "" echo "🔐 Step 11: Configuring transport permissions..." if [ -x "$PC2_DIR/scripts/setup-transport-permissions.sh" ]; then bash "$PC2_DIR/scripts/setup-transport-permissions.sh" \ || echo " ⚠️ Sudoers install skipped or failed — pc2 will still start; runtime will retry on macOS or fall to ActiveProxy." else echo " ⚠️ setup-transport-permissions.sh not found in this revision — older pc2 layout, skipping." fi # ───────────────────────────────────────────────────────────────────── # Step 12: Start under PM2. # Prefer ecosystem.config.cjs if present (handles env, restart policy, # log paths). Falls back to bare invocation otherwise. # ───────────────────────────────────────────────────────────────────── echo "" echo "🚀 Step 12: Starting PC2..." cd "$PC2_DIR" if [ -f "ecosystem.config.cjs" ]; then pm2 start ecosystem.config.cjs else pm2 start "$PC2_NODE_DIR/dist/index.js" \ --name pc2 \ --cwd "$PC2_NODE_DIR" \ --restart-delay 10000 \ --max-restarts 5 fi pm2 save # ───────────────────────────────────────────────────────────────────── # Step 13: Verify startup. Give it 10 seconds for IPFS bootstrap # and the first Lit handshake before declaring success. # ───────────────────────────────────────────────────────────────────── echo "" echo "🔍 Step 13: Verifying startup..." sleep 10 if pm2 show pc2 | grep -q "online"; then echo "" echo "╔══════════════════════════════════════════════════════════════╗" echo "║ ✅ PC2 Updated Successfully! ║" echo "╚══════════════════════════════════════════════════════════════╝" echo "" pm2 status echo "" echo "Health check:" curl -s http://localhost:4200/health | head -1 echo "" else echo "" echo "╔══════════════════════════════════════════════════════════════╗" echo "║ ⚠️ PC2 started but may have issues. Check the logs: ║" echo "║ pm2 logs pc2 ║" echo "╚══════════════════════════════════════════════════════════════╝" pm2 status exit 1 fi