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.
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

at c2e10731a55ee1293bfb98a325fd2e4ee1296cff 660 lines 15 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 10ENV_FILE="$SCRIPT_DIR/.env" 11RUNTIME_DIR="$SCRIPT_DIR/data/runtime" 12PID_FILE="$RUNTIME_DIR/${APP_NAME}.pid" 13LOG_FILE="$RUNTIME_DIR/${APP_NAME}.log" 14LOCK_DIR="$RUNTIME_DIR/.install.lock" 15 16ACTION="install" 17DO_INSTALL=1 18DO_BUILD=1 19DO_START=1 20DO_NATIVE_REBUILD=1 21RUNNER="auto" 22PORT_OVERRIDE="" 23HOST_OVERRIDE="" 24APP_PORT="" 25APP_HOST="" 26ACTIVE_RUNNER="" 27CREATED_JWT_SECRET=0 28BUN_BIN="" 29 30usage() { 31 cat <<'USAGE' 32Usage: ./install.sh [options] 33 34Default behavior: 35 - Installs dependencies 36 - Rebuilds native modules if needed 37 - Builds server + web app 38 - Starts in the background (PM2 if installed, otherwise nohup) 39 - Prints local web URL 40 41Options: 42 --no-start Install/build only (do not start background process) 43 --start-only Start background process only (skip install/build) 44 --stop Stop background process (PM2 and/or nohup) 45 --status Show background process status 46 --pm2 Force PM2 runner 47 --nohup Force nohup runner 48 --port <number> Set or override PORT in .env 49 --host <bind-host> Set or override HOST in .env (for example 127.0.0.1) 50 --skip-install Skip bun install 51 --skip-build Skip bun run build 52 --skip-native-rebuild Skip native-module compatibility rebuild checks 53 -h, --help Show this help 54USAGE 55} 56 57require_command() { 58 local command_name="$1" 59 if ! command -v "$command_name" >/dev/null 2>&1; then 60 echo "Required command not found: $command_name" 61 exit 1 62 fi 63} 64 65is_valid_port() { 66 local candidate="$1" 67 [[ "$candidate" =~ ^[0-9]+$ ]] || return 1 68 (( candidate >= 1 && candidate <= 65535 )) 69} 70 71ensure_bun_runtime() { 72 install_latest_bun() { 73 if command -v curl >/dev/null 2>&1; then 74 curl -fsSL https://bun.sh/install | bash >/dev/null 75 return 0 76 fi 77 if command -v wget >/dev/null 2>&1; then 78 wget -qO- https://bun.sh/install | bash >/dev/null 79 return 0 80 fi 81 82 echo "❌ Bun is required, and curl/wget is unavailable for auto-install." 83 echo " Install Bun manually: https://bun.com/docs/installation" 84 exit 1 85 } 86 87 resolve_bun_bin() { 88 if command -v bun >/dev/null 2>&1; then 89 command -v bun 90 return 0 91 fi 92 if [[ -x "${HOME}/.bun/bin/bun" ]]; then 93 printf '%s\n' "${HOME}/.bun/bin/bun" 94 return 0 95 fi 96 return 1 97 } 98 99 if ! BUN_BIN="$(resolve_bun_bin)"; then 100 echo "📦 Bun not found. Installing latest Bun..." 101 install_latest_bun 102 BUN_BIN="$(resolve_bun_bin || true)" 103 fi 104 105 if [[ -z "$BUN_BIN" || ! -x "$BUN_BIN" ]]; then 106 echo "❌ Bun could not be resolved." 107 echo " Install Bun manually: https://bun.com/docs/installation" 108 exit 1 109 fi 110 111 export PATH="$(dirname "$BUN_BIN"):$PATH" 112 113 if ! "$BUN_BIN" upgrade >/dev/null 2>&1; then 114 echo "⚠️ Bun auto-upgrade failed. Reinstalling latest Bun..." 115 install_latest_bun 116 BUN_BIN="$(resolve_bun_bin || true)" 117 fi 118 119 if [[ -z "$BUN_BIN" || ! -x "$BUN_BIN" ]]; then 120 echo "❌ Bun could not be resolved after auto-upgrade." 121 echo " Install Bun manually: https://bun.com/docs/installation" 122 exit 1 123 fi 124 125 export PATH="$(dirname "$BUN_BIN"):$PATH" 126 127 local bun_major 128 bun_major="$($BUN_BIN --version | awk -F. '{print $1}' 2>/dev/null || echo 0)" 129 if [[ "$bun_major" -lt 1 ]]; then 130 echo "❌ Bun 1.x+ is required. Current: $($BUN_BIN --version 2>/dev/null || echo 'unknown')" 131 exit 1 132 fi 133} 134 135run_bun() { 136 "$BUN_BIN" "$@" 137} 138 139acquire_lock() { 140 mkdir -p "$RUNTIME_DIR" 141 if ! mkdir "$LOCK_DIR" 2>/dev/null; then 142 echo "Another install/update operation appears to be running." 143 echo "If this is stale, remove: $LOCK_DIR" 144 exit 1 145 fi 146} 147 148release_lock() { 149 rmdir "$LOCK_DIR" >/dev/null 2>&1 || true 150} 151 152cleanup() { 153 release_lock 154} 155 156get_env_value() { 157 local key="$1" 158 if [[ ! -f "$ENV_FILE" ]]; then 159 return 0 160 fi 161 local line 162 line="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 || true)" 163 if [[ -z "$line" ]]; then 164 return 0 165 fi 166 printf '%s\n' "${line#*=}" 167} 168 169upsert_env_value() { 170 local key="$1" 171 local value="$2" 172 touch "$ENV_FILE" 173 local tmp_file 174 tmp_file="$(mktemp)" 175 awk -v key="$key" -v value="$value" ' 176 BEGIN { updated = 0 } 177 $0 ~ ("^" key "=") { 178 print key "=" value 179 updated = 1 180 next 181 } 182 { print } 183 END { 184 if (!updated) { 185 print key "=" value 186 } 187 } 188 ' "$ENV_FILE" > "$tmp_file" 189 mv "$tmp_file" "$ENV_FILE" 190} 191 192ensure_env_defaults() { 193 local existing_port 194 existing_port="$(get_env_value PORT)" 195 if [[ -n "$PORT_OVERRIDE" ]]; then 196 APP_PORT="$PORT_OVERRIDE" 197 elif [[ -n "$existing_port" ]]; then 198 APP_PORT="$existing_port" 199 else 200 APP_PORT="3000" 201 fi 202 203 if ! is_valid_port "$APP_PORT"; then 204 echo "Invalid port: $APP_PORT" 205 exit 1 206 fi 207 208 if [[ -z "$existing_port" || -n "$PORT_OVERRIDE" ]]; then 209 upsert_env_value PORT "$APP_PORT" 210 fi 211 212 local existing_host 213 existing_host="$(get_env_value HOST)" 214 if [[ -n "$HOST_OVERRIDE" ]]; then 215 APP_HOST="$HOST_OVERRIDE" 216 upsert_env_value HOST "$APP_HOST" 217 elif [[ -n "$existing_host" ]]; then 218 APP_HOST="$existing_host" 219 else 220 APP_HOST="0.0.0.0" 221 fi 222 223 local existing_secret 224 existing_secret="$(get_env_value JWT_SECRET)" 225 if [[ -z "$existing_secret" ]]; then 226 local generated_secret 227 generated_secret="$(run_bun -e "console.log(require('crypto').randomBytes(32).toString('hex'))")" 228 upsert_env_value JWT_SECRET "$generated_secret" 229 CREATED_JWT_SECRET=1 230 fi 231} 232 233ensure_node_modules_present() { 234 if [[ ! -d "$SCRIPT_DIR/node_modules" ]]; then 235 echo "Dependencies not found. Run ./install.sh (without --start-only) first." 236 exit 1 237 fi 238} 239 240native_module_compatible() { 241 run_bun -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 242} 243 244run_native_rebuild() { 245 echo "Verifying native modules for Bun $($BUN_BIN --version)..." 246 247 if run_bun run rebuild:native; then 248 return 0 249 fi 250 251 echo "rebuild:native failed. Forcing fresh Bun install..." 252 run_bun install --force 253} 254 255ensure_native_compatibility() { 256 if [[ "$DO_NATIVE_REBUILD" -eq 0 ]]; then 257 return 0 258 fi 259 260 if native_module_compatible; then 261 return 0 262 fi 263 264 echo "Detected native module mismatch (likely from runtime/dependency change)." 265 run_native_rebuild 266 267 if ! native_module_compatible; then 268 echo "Native module validation still failed after rebuild." 269 echo "Try reinstalling dependencies: rm -rf node_modules bun.lock && bun install" 270 exit 1 271 fi 272} 273 274ensure_build_artifacts() { 275 if [[ ! -f "$SCRIPT_DIR/dist/index.js" ]]; then 276 echo "Build output not found (dist/index.js). Running build now." 277 run_bun run build 278 fi 279} 280 281install_and_build() { 282 if [[ "$DO_INSTALL" -eq 1 ]]; then 283 echo "Installing dependencies" 284 run_bun install 285 fi 286 287 ensure_node_modules_present 288 ensure_native_compatibility 289 290 if [[ "$DO_BUILD" -eq 1 ]]; then 291 echo "Building server and web app" 292 run_bun run build 293 fi 294} 295 296pid_looks_like_app() { 297 local pid="$1" 298 local cmd 299 cmd="$(ps -p "$pid" -o command= 2>/dev/null || true)" 300 [[ "$cmd" == *"dist/index.js"* || "$cmd" == *"bun run start"* || "$cmd" == *"bun dist/index.js"* || "$cmd" == *"$APP_NAME"* ]] 301} 302 303stop_pid_gracefully() { 304 local pid="$1" 305 306 if ! kill -0 "$pid" >/dev/null 2>&1; then 307 return 0 308 fi 309 310 kill "$pid" >/dev/null 2>&1 || true 311 312 local attempt 313 for attempt in $(seq 1 20); do 314 if ! kill -0 "$pid" >/dev/null 2>&1; then 315 return 0 316 fi 317 sleep 0.5 318 done 319 320 kill -9 "$pid" >/dev/null 2>&1 || true 321} 322 323stop_nohup_if_running() { 324 if [[ ! -f "$PID_FILE" ]]; then 325 return 1 326 fi 327 328 local pid 329 pid="$(cat "$PID_FILE" 2>/dev/null || true)" 330 if [[ -z "$pid" ]]; then 331 rm -f "$PID_FILE" 332 return 1 333 fi 334 335 if ! kill -0 "$pid" >/dev/null 2>&1; then 336 rm -f "$PID_FILE" 337 return 1 338 fi 339 340 if ! pid_looks_like_app "$pid"; then 341 echo "PID file points to a non-app process. Removing stale PID file: $PID_FILE" 342 rm -f "$PID_FILE" 343 return 1 344 fi 345 346 stop_pid_gracefully "$pid" 347 rm -f "$PID_FILE" 348 return 0 349} 350 351stop_pm2_if_running() { 352 if ! command -v pm2 >/dev/null 2>&1; then 353 return 1 354 fi 355 356 local stopped=0 357 358 echo "[pm2] Inspecting existing PM2 processes..." 359 360 if pm2 describe "$APP_NAME" >/dev/null 2>&1; then 361 echo "[pm2] Deleting existing process: $APP_NAME" 362 pm2 delete "$APP_NAME" || true 363 stopped=1 364 fi 365 366 if pm2 describe "$LEGACY_APP_NAME" >/dev/null 2>&1; then 367 echo "[pm2] Deleting legacy process: $LEGACY_APP_NAME" 368 pm2 delete "$LEGACY_APP_NAME" || true 369 stopped=1 370 fi 371 372 if [[ "$stopped" -eq 1 ]]; then 373 echo "[pm2] Saving PM2 process list" 374 pm2 save || true 375 return 0 376 fi 377 378 return 1 379} 380 381start_with_nohup() { 382 mkdir -p "$RUNTIME_DIR" 383 stop_nohup_if_running >/dev/null 2>&1 || true 384 385 echo "Starting with nohup" 386 nohup "$BUN_BIN" run start >> "$LOG_FILE" 2>&1 & 387 echo "$!" > "$PID_FILE" 388 389 local pid 390 pid="$(cat "$PID_FILE")" 391 sleep 1 392 if ! kill -0 "$pid" >/dev/null 2>&1; then 393 echo "Failed to start background process with nohup." 394 echo "Check logs: $LOG_FILE" 395 tail -n 40 "$LOG_FILE" 2>/dev/null || true 396 exit 1 397 fi 398} 399 400start_with_pm2() { 401 echo "Starting with PM2" 402 403 if pm2 describe "$LEGACY_APP_NAME" >/dev/null 2>&1; then 404 echo "[pm2] Removing legacy process before start: $LEGACY_APP_NAME" 405 pm2 delete "$LEGACY_APP_NAME" || true 406 fi 407 408 if pm2 describe "$APP_NAME" >/dev/null 2>&1; then 409 echo "[pm2] Recreating existing process with Bun binary launcher: $APP_NAME" 410 pm2 delete "$APP_NAME" || true 411 else 412 echo "[pm2] Starting new process: $APP_NAME (cwd=$SCRIPT_DIR, script=dist/index.js)" 413 fi 414 415 pm2 start "$BUN_BIN" --name "$APP_NAME" --cwd "$SCRIPT_DIR" --update-env -- dist/index.js 416 417 echo "[pm2] Saving PM2 process list" 418 pm2 save || true 419} 420 421start_background() { 422 local resolved_runner="$RUNNER" 423 if [[ "$resolved_runner" == "auto" ]]; then 424 if command -v pm2 >/dev/null 2>&1; then 425 resolved_runner="pm2" 426 else 427 resolved_runner="nohup" 428 fi 429 fi 430 431 case "$resolved_runner" in 432 pm2) 433 require_command pm2 434 start_with_pm2 435 ACTIVE_RUNNER="pm2" 436 ;; 437 nohup) 438 start_with_nohup 439 ACTIVE_RUNNER="nohup" 440 ;; 441 *) 442 echo "Unsupported runner: $resolved_runner" 443 exit 1 444 ;; 445 esac 446} 447 448wait_for_web() { 449 local url="http://127.0.0.1:${APP_PORT}" 450 local attempt 451 452 for attempt in $(seq 1 30); do 453 if command -v curl >/dev/null 2>&1; then 454 if curl -fsS "$url" >/dev/null 2>&1; then 455 return 0 456 fi 457 else 458 if run_bun -e "const http=require('http');const req=http.get('$url',res=>{process.exit(res.statusCode && res.statusCode < 500 ? 0 : 1)});req.setTimeout(1500,()=>{req.destroy();process.exit(1)});req.on('error',()=>process.exit(1));" >/dev/null 2>&1; then 459 return 0 460 fi 461 fi 462 sleep 1 463 done 464 465 return 1 466} 467 468print_access_info() { 469 echo "" 470 echo "Setup complete." 471 echo "Bind host: ${APP_HOST}" 472 echo "Web app URL (local): http://localhost:${APP_PORT}" 473 474 if [[ "$CREATED_JWT_SECRET" -eq 1 ]]; then 475 echo "Generated JWT_SECRET in .env" 476 fi 477 478 if [[ "$APP_HOST" == "127.0.0.1" || "$APP_HOST" == "::1" || "$APP_HOST" == "localhost" ]]; then 479 echo "Access scope: local-only bind (use reverse proxy or Tailscale for remote access)" 480 else 481 echo "Access scope: network-accessible bind" 482 fi 483 484 if [[ "$ACTIVE_RUNNER" == "pm2" ]]; then 485 echo "Process manager: PM2" 486 echo "Status: pm2 status $APP_NAME" 487 echo "Logs: pm2 logs $APP_NAME" 488 echo "Stop: ./install.sh --stop" 489 elif [[ "$ACTIVE_RUNNER" == "nohup" ]]; then 490 echo "Process manager: nohup" 491 echo "PID file: $PID_FILE" 492 echo "Logs: tail -f $LOG_FILE" 493 echo "Stop: ./install.sh --stop" 494 fi 495 496 if wait_for_web; then 497 echo "Health check: OK" 498 else 499 echo "Health check: not ready yet (service may still be starting)" 500 fi 501} 502 503show_status() { 504 local found=0 505 local configured_port 506 local configured_host 507 configured_port="$(get_env_value PORT)" 508 configured_host="$(get_env_value HOST)" 509 510 if [[ -n "$configured_port" ]]; then 511 echo "Configured PORT: $configured_port" 512 fi 513 if [[ -n "$configured_host" ]]; then 514 echo "Configured HOST: $configured_host" 515 fi 516 517 if command -v pm2 >/dev/null 2>&1; then 518 if pm2 describe "$APP_NAME" >/dev/null 2>&1; then 519 found=1 520 echo "PM2 process is running: $APP_NAME" 521 pm2 status "$APP_NAME" 522 elif pm2 describe "$LEGACY_APP_NAME" >/dev/null 2>&1; then 523 found=1 524 echo "PM2 process is running: $LEGACY_APP_NAME" 525 pm2 status "$LEGACY_APP_NAME" 526 fi 527 fi 528 529 if [[ -f "$PID_FILE" ]]; then 530 local pid 531 pid="$(cat "$PID_FILE" 2>/dev/null || true)" 532 if [[ -n "$pid" ]] && kill -0 "$pid" >/dev/null 2>&1; then 533 found=1 534 echo "nohup process is running with PID $pid" 535 echo "Logs: $LOG_FILE" 536 else 537 echo "Found stale PID file at $PID_FILE" 538 fi 539 fi 540 541 if [[ "$found" -eq 0 ]]; then 542 echo "No running background process found for $APP_NAME" 543 fi 544} 545 546stop_all() { 547 local stopped=0 548 549 if stop_pm2_if_running; then 550 stopped=1 551 echo "Stopped PM2 process(es)." 552 fi 553 554 if stop_nohup_if_running; then 555 stopped=1 556 echo "Stopped nohup process from PID file" 557 fi 558 559 if [[ "$stopped" -eq 0 ]]; then 560 echo "No running process found for $APP_NAME" 561 fi 562} 563 564while [[ $# -gt 0 ]]; do 565 case "$1" in 566 --no-start) 567 DO_START=0 568 ;; 569 --start-only) 570 ACTION="start" 571 DO_INSTALL=0 572 DO_BUILD=0 573 DO_START=1 574 ;; 575 --stop) 576 ACTION="stop" 577 ;; 578 --status) 579 ACTION="status" 580 ;; 581 --pm2) 582 RUNNER="pm2" 583 ;; 584 --nohup) 585 RUNNER="nohup" 586 ;; 587 --port) 588 if [[ $# -lt 2 ]]; then 589 echo "Missing value for --port" 590 exit 1 591 fi 592 PORT_OVERRIDE="$2" 593 shift 594 ;; 595 --host) 596 if [[ $# -lt 2 ]]; then 597 echo "Missing value for --host" 598 exit 1 599 fi 600 HOST_OVERRIDE="$2" 601 shift 602 ;; 603 --skip-install) 604 DO_INSTALL=0 605 ;; 606 --skip-build) 607 DO_BUILD=0 608 ;; 609 --skip-native-rebuild) 610 DO_NATIVE_REBUILD=0 611 ;; 612 -h|--help) 613 usage 614 exit 0 615 ;; 616 *) 617 echo "Unknown option: $1" 618 usage 619 exit 1 620 ;; 621 esac 622 shift 623done 624 625case "$ACTION" in 626 stop) 627 acquire_lock 628 trap cleanup EXIT 629 stop_all 630 exit 0 631 ;; 632 status) 633 show_status 634 exit 0 635 ;; 636esac 637 638ensure_bun_runtime 639 640acquire_lock 641trap cleanup EXIT 642 643ensure_env_defaults 644 645if [[ "$ACTION" == "install" ]]; then 646 install_and_build 647else 648 ensure_node_modules_present 649 ensure_native_compatibility 650fi 651 652if [[ "$DO_START" -eq 0 ]]; then 653 echo "Install/build complete. Start later with: ./install.sh --start-only" 654 echo "Configured web URL: http://localhost:${APP_PORT}" 655 exit 0 656fi 657 658ensure_build_artifacts 659start_background 660print_access_info