-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathsetup.sh
More file actions
executable file
·516 lines (454 loc) · 17.6 KB
/
setup.sh
File metadata and controls
executable file
·516 lines (454 loc) · 17.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
#!/bin/bash
set -e
# Colors
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${BLUE}=== ClawBridge Dashboard Installer ===${NC}"
# OS Detection
OS_TYPE=$(uname -s)
if [ "$OS_TYPE" = "Darwin" ]; then
sed_inplace() { sed -i '' "$@"; }
else
sed_inplace() { sed -i "$@"; }
fi
# Parse Args
TOKEN=""
NO_TUNNEL=false
QUICK_TUNNEL=false
FORCE_CF=false
for arg in "$@"
do
case $arg in
--token=*)
TOKEN="${arg#*=}"
shift
;;
--no-tunnel)
NO_TUNNEL=true
shift
;;
--quick)
QUICK_TUNNEL=true
shift
;;
--force-cf)
FORCE_CF=true
shift
;;
esac
done
# 1. Check Node
if ! command -v node &> /dev/null; then
echo "❌ Node.js not found. Please install Node.js v18+ first."
exit 1
fi
APP_DIR=$(pwd)
echo -e "${GREEN}📂 Installing in: $APP_DIR${NC}"
# Allow custom service name via env var (for testing/parallel installs)
SERVICE_NAME=${CLAW_SERVICE_NAME:-clawbridge}
# Stop existing service if it exists (Safe stop)
if [ "$OS_TYPE" = "Darwin" ]; then
launchctl unload "$HOME/Library/LaunchAgents/com.dreamwing.${SERVICE_NAME}.plist" >/dev/null 2>&1 || true
else
systemctl --user stop "$SERVICE_NAME" >/dev/null 2>&1 || true
if command -v systemctl &> /dev/null; then
systemctl stop "$SERVICE_NAME" >/dev/null 2>&1 || true
fi
fi
# 2. Install Dependencies
echo "📦 Installing dependencies..."
npm install --production
# 3. Setup Config/Env
ENV_FILE="$APP_DIR/.env"
CURRENT_PORT=3000
# Load existing port preference
if [ -f "$ENV_FILE" ]; then
# Read PORT manually to avoid sourcing entire file yet
EXISTING_PORT=$(grep "^PORT=" "$ENV_FILE" | cut -d'=' -f2)
if [ ! -z "$EXISTING_PORT" ]; then CURRENT_PORT=$EXISTING_PORT; fi
fi
# Find available port
DETECTED_PORT=$(node -e "
const net = require('net');
let port = parseInt('$CURRENT_PORT') || 3000;
function check() {
const s = net.createServer();
s.once('error', () => { port++; check(); });
s.once('listening', () => { s.close(); console.log(port); });
s.listen(port);
}
check();
")
if [ "$DETECTED_PORT" != "$CURRENT_PORT" ]; then
echo -e "${YELLOW}⚠️ Port $CURRENT_PORT is busy. Switching to available port: $DETECTED_PORT.${NC}"
fi
PORT=$DETECTED_PORT
if [ ! -f "$ENV_FILE" ]; then
echo "⚙️ Generating .env file..."
RAND_KEY=$(openssl rand -hex 16)
echo "ACCESS_KEY=$RAND_KEY" > "$ENV_FILE"
echo "PORT=$PORT" >> "$ENV_FILE"
echo -e "${YELLOW}🔑 Generated Access Key: $RAND_KEY${NC}"
else
echo "✅ Updating .env configuration..."
# Update or Append PORT
if grep -q "^PORT=" "$ENV_FILE"; then
sed_inplace "s/^PORT=.*/PORT=$PORT/" "$ENV_FILE"
else
echo "PORT=$PORT" >> "$ENV_FILE"
fi
source "$ENV_FILE"
if [ -z "$ACCESS_KEY" ]; then
RAND_KEY=$(openssl rand -hex 16)
echo "ACCESS_KEY=$RAND_KEY" >> "$ENV_FILE"
else
RAND_KEY=$ACCESS_KEY
fi
fi
# 3b. Auto-detect OPENCLAW_PATH
DETECTED_OPENCLAW=""
if command -v openclaw &> /dev/null; then
DETECTED_OPENCLAW=$(which openclaw)
else
# Look in the same bin directory as Node.js
NODE_BIN_DIR=$(dirname "$(which node)" 2>/dev/null)
if [ -x "$NODE_BIN_DIR/openclaw" ]; then
DETECTED_OPENCLAW="$NODE_BIN_DIR/openclaw"
fi
fi
if [ ! -z "$DETECTED_OPENCLAW" ]; then
echo "🔍 Detected openclaw at: $DETECTED_OPENCLAW"
# Update or append OPENCLAW_PATH
sed_inplace '/OPENCLAW_PATH=/d' "$ENV_FILE"
echo "OPENCLAW_PATH=$DETECTED_OPENCLAW" >> "$ENV_FILE"
fi
# 4. Setup Service
NODE_PATH=$(which node)
if [ "$OS_TYPE" = "Darwin" ]; then
# macOS launchd setup
SERVICE_DIR="$HOME/Library/LaunchAgents"
mkdir -p "$SERVICE_DIR"
SERVICE_FILE="$SERVICE_DIR/com.dreamwing.${SERVICE_NAME}.plist"
cat > "$SERVICE_FILE" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.dreamwing.${SERVICE_NAME}</string>
<key>ProgramArguments</key>
<array>
<string>$NODE_PATH</string>
<string>index.js</string>
</array>
<key>WorkingDirectory</key>
<string>$APP_DIR</string>
<key>EnvironmentVariables</key>
<dict>
<key>NODE_ENV</key>
<string>production</string>
<key>PATH</key>
<string>$PATH</string>
</dict>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/com.dreamwing.${SERVICE_NAME}.log</string>
<key>StandardErrorPath</key>
<string>/tmp/com.dreamwing.${SERVICE_NAME}.err</string>
</dict>
</plist>
EOF
echo "📝 Service file created at: $SERVICE_FILE"
echo "🚀 Loading macOS Launch Agent (com.dreamwing.${SERVICE_NAME})..."
launchctl load -w "$SERVICE_FILE" >/dev/null 2>&1 || true
# If already loaded and we just want to restart:
launchctl unload "$SERVICE_FILE" >/dev/null 2>&1 || true
launchctl load -w "$SERVICE_FILE"
echo -e "${GREEN}✅ Service started!${NC}"
else
# Linux systemd setup
SERVICE_FILE="$HOME/.config/systemd/user/${SERVICE_NAME}.service"
USE_USER_SYSTEMD=true
if [ ! -d "$HOME/.config/systemd/user" ]; then
mkdir -p "$HOME/.config/systemd/user"
fi
# Check if user dbus is active (common issue in bare VPS)
if ! systemctl --user list-units >/dev/null 2>&1; then
echo -e "${YELLOW}⚠️ User-level systemd not available. Generating standard systemd file...${NC}"
USE_USER_SYSTEMD=false
SERVICE_FILE="/tmp/${SERVICE_NAME}.service"
fi
cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=ClawBridge Dashboard (${SERVICE_NAME})
After=network.target
[Service]
Type=simple
WorkingDirectory=$APP_DIR
ExecStart=$NODE_PATH index.js
Restart=always
Environment=NODE_ENV=production
Environment=PATH=$PATH
EnvironmentFile=$APP_DIR/.env
[Install]
WantedBy=default.target
EOF
echo "📝 Service file created at: $SERVICE_FILE"
if [ "$USE_USER_SYSTEMD" = true ]; then
echo "🚀 Enabling User Service ($SERVICE_NAME)..."
systemctl --user daemon-reload
systemctl --user enable "$SERVICE_NAME"
systemctl --user restart "$SERVICE_NAME"
echo -e "${GREEN}✅ Service started!${NC}"
else
echo -e "${YELLOW}👉 Please run the following command with sudo to install the service:${NC}"
echo "sudo mv $SERVICE_FILE /etc/systemd/system/${SERVICE_NAME}.service"
echo "sudo systemctl daemon-reload"
echo "sudo systemctl enable ${SERVICE_NAME}"
echo "sudo systemctl start ${SERVICE_NAME}"
fi
fi
# 5. Remote Access (Cloudflare Tunnel)
echo -e "\n${BLUE}🌐 Remote Access Configuration${NC}"
# Logic:
# If --token is provided, use it directly (Non-Interactive).
# If --no-tunnel is provided, skip (Non-Interactive).
# Else, ask user (Interactive).
ENABLE_TUNNEL="n"
if [ ! -z "$TOKEN" ]; then
echo "✅ Token provided via argument. Configuring tunnel..."
CF_TOKEN="$TOKEN"
ENABLE_TUNNEL="y"
elif [ "$NO_TUNNEL" = true ]; then
echo "ℹ️ --no-tunnel flag detected. Skipping tunnel setup."
ENABLE_TUNNEL="n"
elif [ "$QUICK_TUNNEL" = true ]; then
echo "🌊 --quick flag detected. Enabling Quick Tunnel (Temporary URL)."
ENABLE_TUNNEL="y"
CF_TOKEN="" # Ensure empty for Quick mode
else
# Interactive fallback
read -p "Do you want to expose this dashboard to the public internet via Cloudflare Tunnel? (y/N) " ENABLE_TUNNEL
fi
# Detect VPN interfaces
USE_VPN=false
VPN_IP=""
VPN_TYPE=""
if [ "$OS_TYPE" != "Darwin" ]; then
# VPN detection (Linux only — macOS uses different interface names)
if ip addr show tailscale0 >/dev/null 2>&1; then
VPN_IP=$(ip -4 addr show tailscale0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
if [ ! -z "$VPN_IP" ]; then
USE_VPN=true
VPN_TYPE="Tailscale"
fi
elif ip addr show wg0 >/dev/null 2>&1; then
VPN_IP=$(ip -4 addr show wg0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
if [ ! -z "$VPN_IP" ]; then
USE_VPN=true
VPN_TYPE="WireGuard"
fi
fi
else
# macOS VPN detection via ifconfig
if ifconfig utun0 >/dev/null 2>&1; then
VPN_IP=$(ifconfig utun0 | grep "inet " | awk '{print $2}')
if [ ! -z "$VPN_IP" ]; then
USE_VPN=true
VPN_TYPE="Tailscale"
fi
fi
fi
if [[ "$ENABLE_TUNNEL" =~ ^[Yy]$ ]] || [ "$USE_VPN" = true ]; then
# Logic:
# 1. If VPN found AND NOT forced -> Skip cloudflared download/setup. Just use VPN IP.
# 2. If NO VPN OR Forced -> Do Cloudflare logic.
if [ "$USE_VPN" = true ] && [ -z "$TOKEN" ] && [ "$FORCE_CF" = false ]; then
echo -e "🔒 ${VPN_TYPE} detected ($VPN_IP). Skipping Cloudflare Tunnel."
echo -e "💡 To force Cloudflare anyway, run: ./install.sh --force-cf"
ENABLE_TUNNEL="n"
# Clear any existing tunnel config
sed_inplace '/TUNNEL_TOKEN=/d' "$ENV_FILE"
sed_inplace '/ENABLE_EMBEDDED_TUNNEL=/d' "$ENV_FILE"
else
# Normal Cloudflare Logic
if [ "$FORCE_CF" = true ]; then
echo "🌊 --force-cf flag detected. Enabling Cloudflare Tunnel (ignoring VPN)."
ENABLE_TUNNEL="y"
fi
if ! command -v cloudflared &> /dev/null; then
echo "⬇️ Downloading cloudflared..."
# Helper function to download securely
download_file() {
local TARGET_URL=$1
local TARGET_FILE=$2
if command -v curl &> /dev/null; then
curl -sL "$TARGET_URL" -o "$TARGET_FILE"
elif command -v wget &> /dev/null; then
wget -q "$TARGET_URL" -O "$TARGET_FILE"
else
echo "❌ Neither curl nor wget found. Please install one sequence."
exit 1
fi
}
# Detect arch
ARCH=$(uname -m)
if [ "$OS_TYPE" = "Darwin" ]; then
if [[ "$ARCH" == "x86_64" ]] || [[ "$ARCH" == "amd64" ]]; then
download_file "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz" "cloudflared.tgz"
tar -xzf cloudflared.tgz && rm cloudflared.tgz
elif [[ "$ARCH" == "arm64" ]] || [[ "$ARCH" == "aarch64" ]]; then
download_file "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz" "cloudflared.tgz"
tar -xzf cloudflared.tgz && rm cloudflared.tgz
else
echo "❌ Architecture $ARCH not supported for macOS auto-download."
exit 1
fi
else
if [[ "$ARCH" == "x86_64" ]]; then
download_file "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64" "cloudflared"
elif [[ "$ARCH" == "aarch64" ]]; then
download_file "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64" "cloudflared"
else
echo "❌ Architecture $ARCH not supported for Linux auto-download."
exit 1
fi
fi
chmod +x cloudflared
fi
# If no token and NOT quick mode, ask for it
if [ -z "$CF_TOKEN" ] && [ "$QUICK_TUNNEL" = false ] && [ "$FORCE_CF" = false ]; then
echo -e "${YELLOW}👉 Run this command to login to Cloudflare (in a separate terminal):${NC}"
echo " ./cloudflared tunnel login"
echo -e " Then create a tunnel: ./cloudflared tunnel create clawbridge"
echo -e " Then add token to .env: TUNNEL_TOKEN=..."
read -p "Paste your Cloudflare Tunnel Token (or press Enter to skip for Quick Tunnel): " CF_TOKEN
fi
# Write Config
# Clean old
sed_inplace '/TUNNEL_TOKEN=/d' "$ENV_FILE"
sed_inplace '/ENABLE_EMBEDDED_TUNNEL=/d' "$ENV_FILE"
if [ ! -z "$CF_TOKEN" ]; then
echo "TUNNEL_TOKEN=$CF_TOKEN" >> "$ENV_FILE"
echo "ENABLE_EMBEDDED_TUNNEL=true" >> "$ENV_FILE"
echo "✅ Permanent Tunnel configured."
else
# Quick Tunnel Mode (No Token)
# ONLY enable if we decided to enable tunnel (i.e. not VPN mode)
# AND if we don't already have a token in env (unless quick mode forced override)
if [ "$ENABLE_TUNNEL" == "y" ]; then
# If QUICK_TUNNEL flag was passed, we might want to force quick tunnel
# BUT if user just ran update (which runs setup.sh --quick), we shouldn't kill their perm tunnel
# Wait, install.sh calls setup.sh --quick. This is dangerous for perm tunnels.
# FIX: If TUNNEL_TOKEN exists in .env and --token wasn't passed, preserve it?
# But setup.sh sources .env.
# The issue is install.sh calls it with --quick.
# And --quick sets CF_TOKEN="".
# We need install.sh to NOT call --quick if it detects a perm tunnel?
# Or setup.sh to be smarter.
echo "ENABLE_EMBEDDED_TUNNEL=true" >> "$ENV_FILE"
echo "🌊 Quick Tunnel configured."
fi
fi
# Clean old quick tunnel url before restarting so we wait for a fresh one
rm -f "$APP_DIR/.quick_tunnel_url"
# Restart service to pick up new env
if [ "$OS_TYPE" = "Darwin" ]; then
pkill -f "node index.js" || true
launchctl unload "$HOME/Library/LaunchAgents/com.dreamwing.${SERVICE_NAME}.plist" >/dev/null 2>&1 || true
launchctl load -w "$HOME/Library/LaunchAgents/com.dreamwing.${SERVICE_NAME}.plist"
elif [ "$USE_USER_SYSTEMD" = true ]; then
# Ensure we don't have a zombie process holding the port
pkill -f "node index.js" || true
systemctl --user restart "$SERVICE_NAME"
else
pkill -f "node index.js" || true
sudo systemctl restart "$SERVICE_NAME"
fi
fi
fi
# 6. Summary
if [ "$OS_TYPE" != "Darwin" ]; then
# Robust IP detection for Linux (handles Alpine/WSL quirks where hostname -I fails silently)
IP=$( (ip route get 1 | awk '{print $(NF-2);exit}') 2>/dev/null || \
(hostname -I | awk '{print $1}') 2>/dev/null || \
(ifconfig | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}' | head -n 1) 2>/dev/null )
else
# Fallback for macOS / BSD
IP=$(ifconfig | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}' | head -n 1) 2>/dev/null
fi
PORT=${PORT:-3000}
# Helper: print QR code for a URL (interactive TTY only)
print_qr() {
local url="$1"
# Only show QR in interactive terminal sessions
if [ ! -t 1 ]; then return; fi
echo -e "\n📲 Scan to open on your phone:"
if command -v qrencode &>/dev/null; then
qrencode -t UTF8 -o - "$url"
else
"$NODE_PATH" -e "
try {
const qr = require('qrcode-terminal');
qr.generate('$url', {small: true});
} catch(e) { /* qrcode-terminal not installed, skip */ }
" 2>/dev/null || true
fi
}
echo -e "\n${GREEN}🎉 Installation Complete!${NC}"
# Check if this was an update (based on existing ENV or backups)
IS_UPDATE=false
if [ -f "$ENV_FILE.bak" ] || [ ! -z "$EXISTING_PORT" ]; then
IS_UPDATE=true
fi
if [ "$IS_UPDATE" = true ]; then
echo -e "${YELLOW}🔒 Security Notice: Since v1.1.0, direct Magic Links with keys are no longer supported for enhanced security.${NC}"
echo -e "${YELLOW}👉 Please visit the link below and enter your Access Key manually on the login page.${NC}"
fi
LOCAL_URL="http://$IP:$PORT"
echo -e "📱 Local Access: ${BLUE}${LOCAL_URL}${NC}"
if [ "$USE_VPN" = true ]; then
VPN_URL="http://$VPN_IP:$PORT"
echo -e "🔒 ${VPN_TYPE} Access: ${BLUE}${VPN_URL}${NC}"
echo -e " (Accessible via your ${VPN_TYPE} network)"
print_qr "$VPN_URL"
else
print_qr "$LOCAL_URL"
fi
# If Quick Tunnel, try to fetch the URL from the file generated by node
if [ "$QUICK_TUNNEL" = true ] || [ -z "$CF_TOKEN" ]; then
# ONLY if VPN is NOT used OR Force CF is enabled
if [ "$USE_VPN" = false ] || [ "$FORCE_CF" = true ]; then
echo "⏳ Waiting for Quick Tunnel URL (max 20s)..."
# Loop wait for 20s
for i in {1..20}; do
if [ -f "$APP_DIR/.quick_tunnel_url" ]; then
QURL=$(cat "$APP_DIR/.quick_tunnel_url")
echo -e "\n${GREEN}🚀 ClawBridge Dashboard Live:${NC}"
echo -e "👉 ${BLUE}${QURL}${NC}"
echo -e "⚠️ Note: This link expires if the dashboard restarts."
print_qr "$QURL"
break
fi
sleep 1
echo -n "."
done
if [ ! -f "$APP_DIR/.quick_tunnel_url" ]; then
if [ "$OS_TYPE" = "Darwin" ]; then
echo -e "\n${YELLOW}⚠️ URL not ready yet. Check logs later: tail -f /tmp/com.dreamwing.${SERVICE_NAME}.log${NC}"
elif [ "$USE_USER_SYSTEMD" = true ]; then
echo -e "\n${YELLOW}⚠️ URL not ready yet. Check logs later: journalctl --user -u ${SERVICE_NAME} -f${NC}"
else
echo -e "\n${YELLOW}⚠️ URL not ready yet. Check logs later: sudo journalctl -u ${SERVICE_NAME} -f${NC}"
fi
fi
fi
fi
# 7. Initialize Analytics & Pricing (Cold Start Fix)
echo -e "\n📊 Initializing data analytics & syncing prices..."
"$NODE_PATH" "$APP_DIR/scripts/sync_openrouter_prices.js" >/dev/null 2>&1 || true
"$NODE_PATH" "$APP_DIR/scripts/analyze.js" >/dev/null 2>&1 || true
echo -e "🔑 Access Key: ${YELLOW}$RAND_KEY${NC}"