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