From 01ab6300854f10d4ac1ee137b0acf5bc227c8ca6 Mon Sep 17 00:00:00 2001 From: Mannu Date: Sun, 19 Apr 2026 01:24:23 +0530 Subject: [PATCH] feat(dev): deploy.sh + local-dev.sh + bridge remote mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deploy.sh: Validated explicit-deploy workflow. Pre-flight checks (local build, uncommitted changes, server reachability) run on Mac before touching server. Code pushed to server via 'git push ssh://...' over the existing SSH connection — no Mac SSH server required. Server does git reset --hard to the pushed commit, reinstalls deps if package.json changed, rebuilds dashboard, restarts services, verifies health. Full troubleshooting guide in file header. local-dev.sh: Runs bridge (:3457) and dashboard (:3101) locally on Mac while reaching Tiger via SSH. Separate ports + separate SQLite DB keep it isolated from prod (still live on :3100/:3456). Hot-reload in both layers. Clean Ctrl-C shutdown. bridge remote mode: Added TIGER_REMOTE=true support in bridge/src/tiger.ts and chat.ts. When set, 'docker exec tiger-openclaw' calls are prefixed with 'ssh $TIGER_REMOTE_SSH'. Backward-compatible: VPS leaves TIGER_REMOTE unset and runs docker locally as before. Workflow moving forward: • Edit locally on Mac • ./local-dev.sh to test against real Tiger • git commit small + often • ./deploy.sh to push to production --- bridge/src/routes/chat.ts | 6 +- bridge/src/tiger.ts | 25 +++- deploy.sh | 297 ++++++++++++++++++++++++++++++++++++++ local-dev.sh | 180 +++++++++++++++++++++++ 4 files changed, 504 insertions(+), 4 deletions(-) create mode 100755 deploy.sh create mode 100755 local-dev.sh diff --git a/bridge/src/routes/chat.ts b/bridge/src/routes/chat.ts index e81a7ef..10d07f6 100644 --- a/bridge/src/routes/chat.ts +++ b/bridge/src/routes/chat.ts @@ -98,7 +98,11 @@ router.post("/", async (req, res) => { // Use openclaw agent to send a message to the main session // Session ID: c1e6a067-7ca5-423b-9506-105db0702997 (agent:main:main) - const cmd = `docker exec tiger-openclaw openclaw agent --session-id c1e6a067-7ca5-423b-9506-105db0702997 -m '${escapedMessage}' --json --timeout 120`; + // In TIGER_REMOTE mode, prefix with ssh so docker runs on the VPS. + const sshPrefix = process.env.TIGER_REMOTE === "true" + ? `ssh ${process.env.TIGER_REMOTE_SSH || "root@100.75.128.45"} ` + : ""; + const cmd = `${sshPrefix}docker exec tiger-openclaw openclaw agent --session-id c1e6a067-7ca5-423b-9506-105db0702997 -m '${escapedMessage}' --json --timeout 120`; const tBeforeSpawn = Date.now(); tSpawn = tBeforeSpawn - tStart; diff --git a/bridge/src/tiger.ts b/bridge/src/tiger.ts index e0d575d..7efcd23 100644 --- a/bridge/src/tiger.ts +++ b/bridge/src/tiger.ts @@ -25,6 +25,19 @@ const GATEWAY_WATCHDOG = "/root/gateway-watchdog.sh"; // Timeout for commands (30s default, some ops need longer) const DEFAULT_TIMEOUT = 30_000; +// ─── Remote mode for local development ────────────────────────── +// When running this bridge on a dev machine (not the VPS), we need to +// reach the tiger-openclaw container over SSH. Setting TIGER_REMOTE=true +// in the env prefixes all docker/host commands with `ssh `. +// On the real VPS: TIGER_REMOTE is unset → commands run locally as before. +const IS_REMOTE = process.env.TIGER_REMOTE === "true"; +const REMOTE_SSH = process.env.TIGER_REMOTE_SSH || "root@100.75.128.45"; +const SSH_PREFIX = IS_REMOTE ? `ssh ${REMOTE_SSH} ` : ""; + +if (IS_REMOTE) { + console.log(`[bridge] REMOTE MODE: docker commands will run via ssh ${REMOTE_SSH}`); +} + /** * Execute a command inside the Tiger container. * Commands run directly via docker exec (no kubectl needed). @@ -33,8 +46,9 @@ export async function execInSandbox( command: string, timeoutMs = DEFAULT_TIMEOUT ): Promise<{ stdout: string; stderr: string; exitCode: number }> { - // Run command directly inside tiger-openclaw container - const fullCmd = `docker exec ${DOCKER_CONTAINER} sh -c ${JSON.stringify(command)}`; + // Run command directly inside tiger-openclaw container. + // SSH_PREFIX is empty on the VPS, 'ssh root@host ' for local dev mode. + const fullCmd = `${SSH_PREFIX}docker exec ${DOCKER_CONTAINER} sh -c ${JSON.stringify(command)}`; try { const { stdout, stderr } = await execAsync(fullCmd, { @@ -61,7 +75,12 @@ export async function execOnHost( timeoutMs = DEFAULT_TIMEOUT ): Promise<{ stdout: string; stderr: string; exitCode: number }> { try { - const { stdout, stderr } = await execAsync(command, { + // In remote mode, wrap the command so it runs on the VPS host, not on Mac. + // Use single-quoted form to avoid local shell interpreting it. + const fullCmd = IS_REMOTE + ? `ssh ${REMOTE_SSH} ${JSON.stringify(command)}` + : command; + const { stdout, stderr } = await execAsync(fullCmd, { timeout: timeoutMs, maxBuffer: 5 * 1024 * 1024, }); diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..665a03d --- /dev/null +++ b/deploy.sh @@ -0,0 +1,297 @@ +#!/bin/bash +# ═══════════════════════════════════════════════════════════════════ +# deploy.sh — Push local changes to the Tiger VPS +# +# WHAT IT DOES: +# 1. Validates local state (no uncommitted changes, build works) +# 2. SSHes to server and pulls YOUR local git repo as the source +# 3. Installs any new dependencies +# 4. Rebuilds the Next.js dashboard +# 5. Restarts tiger-bridge and tiger-dashboard services +# 6. Verifies everything came back healthy +# +# USAGE: +# ./deploy.sh # deploy whatever is at current HEAD +# ./deploy.sh --skip-build-check # skip local build (NOT recommended) +# ./deploy.sh --dry-run # show what would happen, don't do it +# +# FAILURE MODES (what to do when it breaks): +# - "uncommitted changes" → git commit first, or git stash +# - "local build failed" → fix TypeScript errors locally, retry +# - "server unreachable" → check Tailscale; ssh root@100.75.128.45 manually +# - "deploy.sh fails mid-way" → server might be in broken state. See +# troubleshooting section at bottom of this file. +# ═══════════════════════════════════════════════════════════════════ + +set -euo pipefail + +# ─── Configuration ─────────────────────────────────────────────────── +SERVER="root@100.75.128.45" +SERVER_PATH="/root/NemoClawDashboard" +LOCAL_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Colors — makes scanning the output easier when things go wrong +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # reset + +# Parse flags +SKIP_BUILD_CHECK=false +DRY_RUN=false +for arg in "$@"; do + case $arg in + --skip-build-check) SKIP_BUILD_CHECK=true ;; + --dry-run) DRY_RUN=true ;; + --help|-h) + echo "Usage: $0 [--skip-build-check] [--dry-run]" + exit 0 + ;; + esac +done + +# ─── Helper functions ──────────────────────────────────────────────── +log() { echo -e "${BLUE}[deploy]${NC} $*"; } +ok() { echo -e "${GREEN}✓${NC} $*"; } +warn() { echo -e "${YELLOW}⚠${NC} $*"; } +die() { echo -e "${RED}✗${NC} $*"; exit 1; } +section() { echo; echo -e "${BLUE}═══ $* ═══${NC}"; } + +run_remote() { + if $DRY_RUN; then + echo " [dry-run] ssh $SERVER '$*'" + else + ssh "$SERVER" "$@" + fi +} + +START_TIME=$(date +%s) + +# ═══════════════════════════════════════════════════════════════════ +# PRE-FLIGHT: Local checks (fail fast, don't touch server if local is bad) +# ═══════════════════════════════════════════════════════════════════ + +section "Pre-flight checks" + +# [1] Are we in the right directory? +cd "$LOCAL_PATH" +if [ ! -d .git ] || [ ! -d dashboard ] || [ ! -d bridge ]; then + die "Not in NemoClawDashboard repo root. cd to the repo first." +fi +ok "In repo: $LOCAL_PATH" + +# [2] Uncommitted changes? +# WHY: if we deploy code that isn't committed, git log can't tell us what's +# running on the server. Commit first, always. +if ! git diff-index --quiet HEAD --; then + warn "You have uncommitted changes:" + git status --short | head -10 + echo + read -r -p "Deploy anyway? Uncommitted changes will NOT be deployed. (y/N) " ans + if [ "$ans" != "y" ]; then + die "Aborted. Commit or stash, then retry." + fi +fi + +# [3] Untracked files of note (warn only) +UNTRACKED=$(git status --short | grep '^??' | wc -l | xargs) +if [ "$UNTRACKED" -gt 0 ]; then + warn "$UNTRACKED untracked file(s) exist — they won't be deployed." +fi + +# [4] What commit are we about to deploy? +LOCAL_SHA=$(git rev-parse HEAD) +LOCAL_SHA_SHORT=$(git rev-parse --short HEAD) +LOCAL_MSG=$(git log -1 --pretty=format:"%s") +ok "Deploying commit: ${LOCAL_SHA_SHORT} — ${LOCAL_MSG}" + +# [5] Local build sanity check +# WHY: catches TypeScript errors in 30s on Mac instead of 5min on server. +# Building the dashboard also pre-checks bridge ts imports if types are +# shared, though bridge has its own tsc separately. +if ! $SKIP_BUILD_CHECK; then + log "Running local build check (dashboard)…" + if $DRY_RUN; then + echo " [dry-run] would: cd dashboard && npm run build" + else + # Use a subshell so we don't accidentally cd out of the repo + (cd dashboard && npm run build > /tmp/deploy-build.log 2>&1) || { + warn "Local build FAILED. Last 30 lines:" + tail -30 /tmp/deploy-build.log + die "Fix local build errors before deploying. (Or use --skip-build-check to bypass, NOT recommended.)" + } + fi + ok "Local dashboard build passed" +fi + +# [6] Is the server reachable? +log "Checking server reachability…" +if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "$SERVER" 'echo ok' >/dev/null 2>&1; then + die "Can't SSH to $SERVER. Check Tailscale + SSH auth." +fi +ok "Server reachable" + +# ═══════════════════════════════════════════════════════════════════ +# DEPLOY: Push code and restart services +# ═══════════════════════════════════════════════════════════════════ + +section "Deploying to server" + +# [7] On server: make sure its working tree is clean before we overwrite +# WHY: if someone SSHed in and edited files directly, this reset would +# silently discard their work. Check first so we can bail if so. +log "Checking server working tree…" +SERVER_DIRTY=$(ssh "$SERVER" "cd $SERVER_PATH && git status --porcelain | wc -l" 2>/dev/null | xargs) +if [ "$SERVER_DIRTY" -gt 0 ]; then + warn "Server has uncommitted changes:" + ssh "$SERVER" "cd $SERVER_PATH && git status --short | head -10" + read -r -p "Discard them and deploy? (y/N) " ans + if [ "$ans" != "y" ]; then + die "Aborted. SSH in and review those changes first." + fi +fi + +# [8] Ensure 'mac' remote exists on the server (points back to Mac via ssh) +# WHY: server needs to fetch from Mac. First-time setup = add remote. +log "Ensuring server can fetch from Mac…" +run_remote "cd $SERVER_PATH && git remote get-url mac 2>/dev/null || git remote add mac $(whoami)@$(hostname).local:$LOCAL_PATH" >/dev/null 2>&1 || true + +# ☝ Note: for this to work, Mac needs SSH server enabled. +# Alternative that doesn't require Mac to accept SSH: push over the +# existing SSH connection using git's stdio protocol. We'll use that +# instead — more reliable, no Mac SSH config required. + +# [9] Push local commits to server via SSH stdio +# WHY: uses the same SSH connection we already have to server, pushing +# our .git/ contents to a bare-ish path. This works even if Mac doesn't +# accept incoming SSH. +log "Pushing commits to server…" +if $DRY_RUN; then + echo " [dry-run] would: git push -f ssh://$SERVER$SERVER_PATH HEAD:refs/heads/incoming" +else + # We push to a throwaway branch 'incoming' on server. Then server-side + # we reset main to match. This lets us push even when server's main + # is checked out (which would otherwise block a direct push). + git push -f "ssh://$SERVER$SERVER_PATH" "HEAD:refs/heads/incoming" 2>&1 | tail -5 +fi +ok "Commits pushed" + +# [10] On server: reset main to the newly-pushed incoming, clean state +log "Updating server working tree…" +run_remote "cd $SERVER_PATH && \ + git checkout main 2>/dev/null && \ + git reset --hard refs/heads/incoming && \ + git branch -D incoming 2>/dev/null; true" +ok "Server at commit $LOCAL_SHA_SHORT" + +# [11] Install new deps if package.json changed +# WHY: if someone added a new library to dashboard or bridge, npm install +# must run or the build/runtime will error with 'Cannot find module'. +# We detect whether package.json or lockfile changed in the last commit. +log "Checking for dependency changes…" +if ! $DRY_RUN; then + DEPS_CHANGED=$(ssh "$SERVER" "cd $SERVER_PATH && git diff HEAD~1 HEAD --name-only 2>/dev/null | grep -E '(package\.json|package-lock\.json)$' | head -5") + if [ -n "$DEPS_CHANGED" ]; then + warn "Dependencies changed in this commit:" + echo "$DEPS_CHANGED" | sed 's/^/ /' + # Install in whichever subdirs have changes + if echo "$DEPS_CHANGED" | grep -q "^dashboard/"; then + log " Running npm install in dashboard/…" + run_remote "cd $SERVER_PATH/dashboard && npm install --no-audit --no-fund 2>&1 | tail -5" + fi + if echo "$DEPS_CHANGED" | grep -q "^bridge/"; then + log " Running npm install in bridge/…" + run_remote "cd $SERVER_PATH/bridge && npm install --no-audit --no-fund 2>&1 | tail -5" + fi + else + ok "No dependency changes" + fi +fi + +# [12] Rebuild dashboard +# WHY: see ChunkLoadError story — Next.js prod server won't hot-reload +# when .next/ changes on disk. Must rebuild before restart. +log "Building dashboard on server…" +run_remote "cd $SERVER_PATH/dashboard && npm run build > /tmp/deploy-build.log 2>&1" || { + warn "Server build failed. Last 30 lines:" + ssh "$SERVER" "tail -30 /tmp/deploy-build.log" + die "Server build failed. Server is now in inconsistent state — run deploy.sh again after fixing." +} +ok "Dashboard built" + +# [13] Restart services — bridge first (tsx auto-picks up src/ changes +# on restart), then dashboard (must be restarted to pick up new .next/) +log "Restarting services…" +run_remote "systemctl restart tiger-bridge" +sleep 3 +run_remote "systemctl restart tiger-dashboard" +sleep 5 + +# [14] Health verification — are services actually up and responding? +log "Verifying services…" +BRIDGE_STATE=$(ssh "$SERVER" "systemctl is-active tiger-bridge" 2>&1) +DASH_STATE=$(ssh "$SERVER" "systemctl is-active tiger-dashboard" 2>&1) +if [ "$BRIDGE_STATE" != "active" ]; then + warn "tiger-bridge is '$BRIDGE_STATE' — checking logs:" + ssh "$SERVER" 'journalctl -u tiger-bridge -n 15 --no-pager' + die "tiger-bridge failed to start" +fi +if [ "$DASH_STATE" != "active" ]; then + warn "tiger-dashboard is '$DASH_STATE' — checking logs:" + ssh "$SERVER" 'journalctl -u tiger-dashboard -n 15 --no-pager' + die "tiger-dashboard failed to start" +fi +ok "tiger-bridge: $BRIDGE_STATE" +ok "tiger-dashboard: $DASH_STATE" + +# [15] Final sanity check: does /api/tiger/status respond? +log "Probing /api/tiger/status…" +STATUS_CODE=$(ssh "$SERVER" "curl -sS -o /dev/null -w '%{http_code}' --max-time 10 http://127.0.0.1:3100/api/tiger/status" 2>&1) +if [ "$STATUS_CODE" = "200" ]; then + ok "Dashboard API responding (HTTP $STATUS_CODE)" +else + warn "Dashboard API returned HTTP $STATUS_CODE — investigate with: ssh $SERVER 'journalctl -u tiger-dashboard -f'" +fi + +# ═══════════════════════════════════════════════════════════════════ +# SUMMARY +# ═══════════════════════════════════════════════════════════════════ + +END_TIME=$(date +%s) +DURATION=$((END_TIME - START_TIME)) + +section "Deploy complete" +echo " Commit: $LOCAL_SHA_SHORT — $LOCAL_MSG" +echo " Duration: ${DURATION}s" +echo " Production: https://agent.manohargupta.com/" +echo +echo " Rollback: git checkout && ./deploy.sh" +echo " Live logs: ssh $SERVER 'journalctl -u tiger-bridge -f'" +echo " ssh $SERVER 'journalctl -u tiger-dashboard -f'" + +# ═══════════════════════════════════════════════════════════════════ +# TROUBLESHOOTING (read this before panicking) +# ═══════════════════════════════════════════════════════════════════ +# +# "Site is down after deploy" +# ssh $SERVER 'journalctl -u tiger-bridge -n 50 --no-pager' +# ssh $SERVER 'journalctl -u tiger-dashboard -n 50 --no-pager' +# Common cause: TypeScript error in bridge src/ (tsx reads live so +# syntax errors crash it). Fix src/ on Mac, redeploy. +# +# "I deployed broken code — how to rollback?" +# git log --oneline # find the last-known-good commit +# git checkout # get back to it locally +# ./deploy.sh # push the good state to server +# git checkout main # return to tip +# (Or: git revert on main, ./deploy.sh — preserves history.) +# +# "deploy.sh hangs at 'Checking server reachability'" +# Tailscale is down on Mac or server. Check: tailscale status +# Or: ssh -v $SERVER — look for 'Connection timed out' +# +# "npm install takes forever" +# First deploy after new deps IS slow. Subsequent deploys skip it +# because only changed deps get reinstalled. +# ═══════════════════════════════════════════════════════════════════ diff --git a/local-dev.sh b/local-dev.sh new file mode 100755 index 0000000..c62b4aa --- /dev/null +++ b/local-dev.sh @@ -0,0 +1,180 @@ +#!/bin/bash +# ═══════════════════════════════════════════════════════════════════ +# local-dev.sh — Run bridge + dashboard locally on Mac +# +# WHAT IT DOES: +# Starts two Node.js processes: +# - Bridge on :3457 (runs in TIGER_REMOTE=true mode, talks to +# the real tiger-openclaw container on VPS via SSH) +# - Dashboard on :3101 (Next.js dev mode with hot-reload) +# +# ARCHITECTURE: +# +# [Mac] Dashboard :3101 ──HTTP──▶ [Mac] Bridge :3457 +# │ +# ├── SQLite (local ./data/tiger-local.db) +# │ +# └──SSH──▶ [VPS] docker exec tiger-openclaw +# │ +# └── OpenClaw + Tiger +# +# SAFETY: +# - Uses separate ports (3101, 3457) so production on :3100, :3456 +# keeps running normally. +# - Uses a SEPARATE SQLite file (data/tiger-local.db) so local chat +# history doesn't mix with production. +# - Read-only for Tiger state: your local bridge can invoke Tiger and +# READ its workspace, but there's no "commit my local chat history +# to server" step. Production Tiger is untouched. +# +# USAGE: +# ./local-dev.sh # start both services +# ./local-dev.sh --bridge # only bridge +# ./local-dev.sh --dashboard # only dashboard +# Ctrl-C # stop everything cleanly +# +# REQUIREMENTS: +# - Node 20+ (already have this) +# - bridge/node_modules installed (cd bridge && npm install, once) +# - dashboard/node_modules installed (cd dashboard && npm install, once) +# - SSH access to root@100.75.128.45 (already set up via Tailscale) +# ═══════════════════════════════════════════════════════════════════ + +set -uo pipefail + +LOCAL_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$LOCAL_PATH" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +RED='\033[0;31m' +NC='\033[0m' + +# Parse flags +MODE="both" +for arg in "$@"; do + case $arg in + --bridge) MODE="bridge" ;; + --dashboard) MODE="dashboard" ;; + --help|-h) + echo "Usage: $0 [--bridge | --dashboard]" + exit 0 + ;; + esac +done + +# ─── Pre-flight ────────────────────────────────────────────────────── + +# Check ssh to server (we need it if bridge is starting) +if [ "$MODE" != "dashboard" ]; then + if ! ssh -o ConnectTimeout=5 -o BatchMode=yes root@100.75.128.45 'echo ok' >/dev/null 2>&1; then + echo -e "${RED}✗${NC} Can't SSH to server. Bridge needs it for remote docker exec." + echo " Try: ssh root@100.75.128.45 manually; check tailscale status" + exit 1 + fi + echo -e "${GREEN}✓${NC} SSH to server works" +fi + +# Check node_modules are installed +if [ "$MODE" != "dashboard" ] && [ ! -d bridge/node_modules ]; then + echo -e "${YELLOW}⚠${NC} bridge/node_modules missing. Installing…" + (cd bridge && npm install --no-audit --no-fund) || { echo "npm install failed"; exit 1; } +fi +if [ "$MODE" != "bridge" ] && [ ! -d dashboard/node_modules ]; then + echo -e "${YELLOW}⚠${NC} dashboard/node_modules missing. Installing…" + (cd dashboard && npm install --no-audit --no-fund) || { echo "npm install failed"; exit 1; } +fi + +# Check port conflicts (we use 3101 and 3457 to stay clear of prod's 3100/3456) +for port in 3101 3457; do + if lsof -ti:$port >/dev/null 2>&1; then + echo -e "${RED}✗${NC} Port $port is already in use. Kill the process first:" + echo " lsof -ti:$port | xargs kill" + exit 1 + fi +done + +# ─── PID tracking so Ctrl-C cleans up children ────────────────────── + +PIDS=() +cleanup() { + echo + echo -e "${YELLOW}Shutting down…${NC}" + for pid in "${PIDS[@]}"; do + kill "$pid" 2>/dev/null || true + done + wait 2>/dev/null + echo -e "${GREEN}✓${NC} All services stopped" + exit 0 +} +trap cleanup INT TERM + +# ─── Start bridge ──────────────────────────────────────────────────── + +start_bridge() { + echo -e "${BLUE}[bridge]${NC} Starting on :3457 in REMOTE mode…" + cd "$LOCAL_PATH/bridge" + + # Environment for local bridge: + # TIGER_REMOTE=true → prefix docker commands with SSH + # TIGER_REMOTE_SSH=... → which host to SSH to + # TIGER_BRIDGE_PORT=3457 → don't collide with prod :3456 + # TIGER_DB_DIR=./data → separate SQLite file from prod + # TIGER_BRIDGE_TOKEN=dev-local-token → auth for dev (server uses different token) + export TIGER_REMOTE=true + export TIGER_REMOTE_SSH=root@100.75.128.45 + export TIGER_BRIDGE_PORT=3457 + export TIGER_BRIDGE_HOST=127.0.0.1 + export TIGER_BRIDGE_TOKEN=dev-local-token + export TIGER_DB_DIR="$LOCAL_PATH/data" + + # Use tsx directly (same as systemd does on server) so changes to src/ + # reload automatically without rebuilding. + node --import tsx src/index.ts 2>&1 | sed -u "s/^/$(printf "${BLUE}[bridge]${NC} ")/" & + PIDS+=($!) + cd "$LOCAL_PATH" +} + +# ─── Start dashboard ───────────────────────────────────────────────── + +start_dashboard() { + echo -e "${GREEN}[dashboard]${NC} Starting on :3101…" + cd "$LOCAL_PATH/dashboard" + + # Environment for local dashboard: + # PORT=3101 → don't collide with prod :3100 + # TIGER_BRIDGE_URL=localhost:3457 → talk to OUR local bridge, not prod + # TIGER_BRIDGE_TOKEN=dev-local-token → match local bridge's auth + export PORT=3101 + export TIGER_BRIDGE_URL=http://localhost:3457 + export TIGER_BRIDGE_TOKEN=dev-local-token + + # next dev → hot-reload, fast compile + npm run dev 2>&1 | sed -u "s/^/$(printf "${GREEN}[dashboard]${NC} ")/" & + PIDS+=($!) + cd "$LOCAL_PATH" +} + +# ─── Start requested services ────────────────────────────────────── + +echo +echo "═══════════════════════════════════════════════════════════════" +echo "Local dev environment starting" +echo "═══════════════════════════════════════════════════════════════" +[ "$MODE" = "both" ] || [ "$MODE" = "bridge" ] && start_bridge +[ "$MODE" = "both" ] || [ "$MODE" = "dashboard" ] && start_dashboard + +sleep 2 +echo +echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}" +[ "$MODE" != "bridge" ] && echo -e " Dashboard: ${GREEN}http://localhost:3101${NC}" +[ "$MODE" != "dashboard" ] && echo -e " Bridge: ${BLUE}http://localhost:3457${NC} (TIGER_REMOTE mode)" +echo -e " Production: https://agent.manohargupta.com (${YELLOW}untouched${NC})" +echo -e " Stop with: ${YELLOW}Ctrl-C${NC}" +echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}" +echo + +# Wait for any child to exit (usually Ctrl-C triggers cleanup first) +wait "${PIDS[@]}"