A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.
at master 470 lines 11 kB view raw
1#!/usr/bin/env bash 2 3set -euo pipefail 4 5APP_NAME="tweets-2-bsky" 6LEGACY_APP_NAME="twitter-mirror" 7SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 8cd "$SCRIPT_DIR" 9 10RUNTIME_DIR="$SCRIPT_DIR/data/runtime" 11PID_FILE="$RUNTIME_DIR/${APP_NAME}.pid" 12LOCK_DIR="$RUNTIME_DIR/.update.lock" 13 14CONFIG_FILE="$SCRIPT_DIR/config.json" 15ENV_FILE="$SCRIPT_DIR/.env" 16 17DO_INSTALL=1 18DO_BUILD=1 19DO_NATIVE_REBUILD=1 20DO_RESTART=1 21REMOTE_OVERRIDE="" 22BRANCH_OVERRIDE="" 23 24STASH_REF="" 25STASH_CREATED=0 26STASH_RESTORED=0 27 28BACKUP_SOURCES=() 29BACKUP_PATHS=() 30 31usage() { 32 cat <<'USAGE' 33Usage: ./update.sh [options] 34 35Default behavior: 36 - Pull latest git changes safely 37 - Install dependencies 38 - Rebuild native modules if needed 39 - Build server + web dashboard 40 - Restart existing runtime (PM2 or nohup) when possible 41 42Options: 43 --remote <name> Git remote to pull from (default: origin or first remote) 44 --branch <name> Git branch to pull (default: current branch or remote HEAD) 45 --skip-install Skip npm install 46 --skip-build Skip npm run build 47 --skip-native-rebuild Skip native-module rebuild checks 48 --no-restart Do not restart process after update 49 -h, --help Show this help 50USAGE 51} 52 53require_command() { 54 local command_name="$1" 55 if ! command -v "$command_name" >/dev/null 2>&1; then 56 echo "❌ Required command not found: $command_name" 57 exit 1 58 fi 59} 60 61check_node_version() { 62 local node_major 63 node_major="$(node -p "Number(process.versions.node.split('.')[0])" 2>/dev/null || echo 0)" 64 if [[ "$node_major" -lt 22 ]]; then 65 echo "❌ Node.js 22+ is required. Current: $(node -v 2>/dev/null || echo 'unknown')" 66 exit 1 67 fi 68} 69 70acquire_lock() { 71 mkdir -p "$RUNTIME_DIR" 72 if ! mkdir "$LOCK_DIR" 2>/dev/null; then 73 echo "❌ Another update appears to be running." 74 echo " If this is stale, remove: $LOCK_DIR" 75 exit 1 76 fi 77} 78 79release_lock() { 80 rmdir "$LOCK_DIR" >/dev/null 2>&1 || true 81} 82 83backup_file() { 84 local file="$1" 85 if [[ ! -f "$file" ]]; then 86 return 0 87 fi 88 89 local base 90 base="$(basename "$file")" 91 local backup_path 92 backup_path="$(mktemp "${TMPDIR:-/tmp}/tweets2bsky-${base}.XXXXXX")" 93 cp "$file" "$backup_path" 94 BACKUP_SOURCES+=("$file") 95 BACKUP_PATHS+=("$backup_path") 96} 97 98restore_backups() { 99 local idx 100 for idx in "${!BACKUP_SOURCES[@]}"; do 101 local src="${BACKUP_SOURCES[$idx]}" 102 local bak="${BACKUP_PATHS[$idx]}" 103 if [[ -f "$bak" ]]; then 104 cp "$bak" "$src" 105 rm -f "$bak" 106 fi 107 done 108} 109 110cleanup() { 111 restore_backups 112 release_lock 113} 114 115ensure_git_repo() { 116 if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then 117 echo "❌ This directory is not a git repository: $SCRIPT_DIR" 118 exit 1 119 fi 120} 121 122resolve_remote() { 123 if [[ -n "$REMOTE_OVERRIDE" ]]; then 124 if ! git remote | grep -qx "$REMOTE_OVERRIDE"; then 125 echo "❌ Remote '$REMOTE_OVERRIDE' does not exist." 126 exit 1 127 fi 128 printf '%s\n' "$REMOTE_OVERRIDE" 129 return 0 130 fi 131 132 if git remote | grep -qx "origin"; then 133 printf '%s\n' "origin" 134 return 0 135 fi 136 137 local first_remote 138 first_remote="$(git remote | head -n 1)" 139 if [[ -z "$first_remote" ]]; then 140 echo "❌ No git remote configured." 141 exit 1 142 fi 143 144 printf '%s\n' "$first_remote" 145} 146 147resolve_branch() { 148 local remote="$1" 149 150 if [[ -n "$BRANCH_OVERRIDE" ]]; then 151 printf '%s\n' "$BRANCH_OVERRIDE" 152 return 0 153 fi 154 155 local current_branch 156 current_branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" 157 if [[ -n "$current_branch" ]] && git show-ref --verify --quiet "refs/remotes/${remote}/${current_branch}"; then 158 printf '%s\n' "$current_branch" 159 return 0 160 fi 161 162 local remote_head 163 remote_head="$(git symbolic-ref --quiet --short "refs/remotes/${remote}/HEAD" 2>/dev/null || true)" 164 if [[ -n "$remote_head" ]]; then 165 printf '%s\n' "${remote_head#${remote}/}" 166 return 0 167 fi 168 169 if git show-ref --verify --quiet "refs/remotes/${remote}/main"; then 170 printf '%s\n' "main" 171 return 0 172 fi 173 174 if git show-ref --verify --quiet "refs/remotes/${remote}/master"; then 175 printf '%s\n' "master" 176 return 0 177 fi 178 179 if [[ -n "$current_branch" ]]; then 180 printf '%s\n' "$current_branch" 181 return 0 182 fi 183 184 echo "❌ Could not determine target branch for remote '$remote'." 185 exit 1 186} 187 188working_tree_dirty() { 189 [[ -n "$(git status --porcelain --untracked-files=normal)" ]] 190} 191 192stash_local_changes() { 193 if ! working_tree_dirty; then 194 return 0 195 fi 196 197 echo "🧳 Stashing local changes before update..." 198 199 local before after message 200 before="$(git stash list -n 1 --format=%gd || true)" 201 message="tweets-2-bsky-update-autostash-$(date -u +%Y%m%d-%H%M%S)" 202 git stash push -u -m "$message" >/dev/null 203 after="$(git stash list -n 1 --format=%gd || true)" 204 205 if [[ -n "$after" && "$after" != "$before" ]]; then 206 STASH_REF="$after" 207 STASH_CREATED=1 208 echo "✅ Saved local changes to $STASH_REF" 209 fi 210} 211 212restore_stash_if_needed() { 213 if [[ "$STASH_CREATED" -ne 1 || -z "$STASH_REF" ]]; then 214 return 0 215 fi 216 217 echo "🔁 Restoring stashed local changes ($STASH_REF)..." 218 if git stash apply --index "$STASH_REF" >/dev/null 2>&1; then 219 git stash drop "$STASH_REF" >/dev/null 2>&1 || true 220 STASH_RESTORED=1 221 echo "✅ Restored local changes from stash." 222 else 223 echo "⚠️ Could not auto-apply $STASH_REF cleanly." 224 echo " Your changes are still preserved in stash." 225 echo " Review manually with: git stash show -p $STASH_REF" 226 fi 227} 228 229checkout_branch() { 230 local remote="$1" 231 local target_branch="$2" 232 233 if ! git show-ref --verify --quiet "refs/remotes/${remote}/${target_branch}"; then 234 echo "❌ Remote branch not found: ${remote}/${target_branch}" 235 exit 1 236 fi 237 238 local current_branch 239 current_branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" 240 241 if [[ "$current_branch" == "$target_branch" ]]; then 242 return 0 243 fi 244 245 if git show-ref --verify --quiet "refs/heads/${target_branch}"; then 246 git switch "$target_branch" >/dev/null 2>&1 || git checkout "$target_branch" >/dev/null 2>&1 247 else 248 git switch -c "$target_branch" --track "${remote}/${target_branch}" >/dev/null 2>&1 || \ 249 git checkout -b "$target_branch" --track "${remote}/${target_branch}" >/dev/null 2>&1 250 fi 251} 252 253pull_latest() { 254 local remote="$1" 255 local branch="$2" 256 257 echo "⬇️ Fetching latest changes from $remote..." 258 git fetch "$remote" --prune 259 260 checkout_branch "$remote" "$branch" 261 git branch --set-upstream-to="${remote}/${branch}" "$branch" >/dev/null 2>&1 || true 262 263 echo "🔄 Pulling latest changes from ${remote}/${branch}..." 264 if ! git pull --ff-only "$remote" "$branch"; then 265 echo "ℹ️ Fast-forward pull failed, retrying with rebase..." 266 git pull --rebase "$remote" "$branch" 267 fi 268} 269 270native_module_compatible() { 271 node -e "try{require('better-sqlite3');process.exit(0)}catch(e){console.error(e && e.message ? e.message : e);process.exit(1)}" >/dev/null 2>&1 272} 273 274rebuild_native_modules() { 275 if [[ "$DO_NATIVE_REBUILD" -eq 0 ]]; then 276 return 0 277 fi 278 279 if native_module_compatible; then 280 return 0 281 fi 282 283 echo "🔧 Native module mismatch detected, rebuilding..." 284 if npm run rebuild:native; then 285 return 0 286 fi 287 288 echo "⚠️ rebuild:native failed, trying npm rebuild better-sqlite3..." 289 if npm rebuild better-sqlite3; then 290 return 0 291 fi 292 293 npm rebuild better-sqlite3 --build-from-source 294} 295 296install_dependencies() { 297 if [[ "$DO_INSTALL" -ne 1 ]]; then 298 return 0 299 fi 300 301 echo "📦 Installing dependencies..." 302 npm install --no-audit --no-fund 303} 304 305build_project() { 306 if [[ "$DO_BUILD" -ne 1 ]]; then 307 return 0 308 fi 309 310 echo "🏗️ Building server + web dashboard..." 311 npm run build 312} 313 314pm2_has_process() { 315 local name="$1" 316 command -v pm2 >/dev/null 2>&1 && pm2 describe "$name" >/dev/null 2>&1 317} 318 319nohup_process_running() { 320 if [[ ! -f "$PID_FILE" ]]; then 321 return 1 322 fi 323 324 local pid 325 pid="$(cat "$PID_FILE" 2>/dev/null || true)" 326 if [[ -z "$pid" ]]; then 327 return 1 328 fi 329 330 if ! kill -0 "$pid" >/dev/null 2>&1; then 331 return 1 332 fi 333 334 local cmd 335 cmd="$(ps -p "$pid" -o command= 2>/dev/null || true)" 336 [[ "$cmd" == *"dist/index.js"* || "$cmd" == *"npm start"* || "$cmd" == *"$APP_NAME"* ]] 337} 338 339restart_runtime() { 340 if [[ "$DO_RESTART" -ne 1 ]]; then 341 echo "⏭️ Skipping restart (--no-restart)." 342 return 0 343 fi 344 345 echo "🔄 Restarting runtime..." 346 347 if pm2_has_process "$APP_NAME"; then 348 pm2 restart "$APP_NAME" --update-env >/dev/null 2>&1 || { 349 echo "⚠️ PM2 restart failed for $APP_NAME. Recreating process..." 350 pm2 delete "$APP_NAME" >/dev/null 2>&1 || true 351 pm2 start dist/index.js --name "$APP_NAME" --cwd "$SCRIPT_DIR" --update-env >/dev/null 2>&1 352 } 353 pm2 save >/dev/null 2>&1 || true 354 echo "✅ Restarted PM2 process: $APP_NAME" 355 return 0 356 fi 357 358 if pm2_has_process "$LEGACY_APP_NAME"; then 359 pm2 restart "$LEGACY_APP_NAME" --update-env >/dev/null 2>&1 || { 360 echo "⚠️ PM2 restart failed for $LEGACY_APP_NAME. Recreating under $APP_NAME..." 361 pm2 delete "$LEGACY_APP_NAME" >/dev/null 2>&1 || true 362 pm2 start dist/index.js --name "$APP_NAME" --cwd "$SCRIPT_DIR" --update-env >/dev/null 2>&1 363 } 364 pm2 save >/dev/null 2>&1 || true 365 echo "✅ Restarted PM2 process." 366 return 0 367 fi 368 369 if nohup_process_running; then 370 bash "$SCRIPT_DIR/install.sh" --start-only --nohup --skip-native-rebuild >/dev/null 371 echo "✅ Restarted nohup runtime." 372 return 0 373 fi 374 375 if command -v pm2 >/dev/null 2>&1; then 376 bash "$SCRIPT_DIR/install.sh" --start-only --pm2 --skip-native-rebuild >/dev/null 377 echo "✅ Started PM2 runtime (was not running)." 378 return 0 379 fi 380 381 bash "$SCRIPT_DIR/install.sh" --start-only --nohup --skip-native-rebuild >/dev/null 382 echo "✅ Started nohup runtime (was not running)." 383} 384 385print_summary() { 386 echo "" 387 echo "✅ Update complete!" 388 echo "" 389 echo "Current commit: $(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')" 390 echo "Current branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'unknown')" 391 392 if [[ "$STASH_CREATED" -eq 1 ]]; then 393 if [[ "$STASH_RESTORED" -eq 1 ]]; then 394 echo "Stash restore: restored" 395 else 396 echo "Stash restore: pending manual apply ($STASH_REF)" 397 fi 398 fi 399} 400 401while [[ $# -gt 0 ]]; do 402 case "$1" in 403 --remote) 404 if [[ $# -lt 2 ]]; then 405 echo "Missing value for --remote" 406 exit 1 407 fi 408 REMOTE_OVERRIDE="$2" 409 shift 410 ;; 411 --branch) 412 if [[ $# -lt 2 ]]; then 413 echo "Missing value for --branch" 414 exit 1 415 fi 416 BRANCH_OVERRIDE="$2" 417 shift 418 ;; 419 --skip-install) 420 DO_INSTALL=0 421 ;; 422 --skip-build) 423 DO_BUILD=0 424 ;; 425 --skip-native-rebuild) 426 DO_NATIVE_REBUILD=0 427 ;; 428 --no-restart) 429 DO_RESTART=0 430 ;; 431 -h|--help) 432 usage 433 exit 0 434 ;; 435 *) 436 echo "Unknown option: $1" 437 usage 438 exit 1 439 ;; 440 esac 441 shift 442done 443 444echo "🔄 Tweets-2-Bsky Updater" 445echo "=========================" 446 447require_command git 448require_command node 449require_command npm 450check_node_version 451ensure_git_repo 452 453acquire_lock 454trap cleanup EXIT 455 456backup_file "$CONFIG_FILE" 457backup_file "$ENV_FILE" 458 459stash_local_changes 460 461remote="$(resolve_remote)" 462branch="$(resolve_branch "$remote")" 463 464pull_latest "$remote" "$branch" 465install_dependencies 466rebuild_native_modules 467build_project 468restart_runtime 469restore_stash_if_needed 470print_summary