-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdev.sh
More file actions
executable file
·486 lines (432 loc) · 16.8 KB
/
Copy pathdev.sh
File metadata and controls
executable file
·486 lines (432 loc) · 16.8 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
#!/bin/bash
# SwarmAI Development Helper
# Usage:
# ./dev.sh — Start everything (backend + frontend) in dev mode
# ./dev.sh backend — Restart backend only (after Python changes)
# ./dev.sh frontend — Start frontend only (backend already running)
# ./dev.sh deploy — Build backend + deploy to daemon (the dev deploy path)
# ./dev.sh quick — Quick build: skip PyInstaller, rebuild Tauri only
# ./dev.sh kill — Kill all dev processes
# ./dev.sh status — Show what's running
# ./dev.sh build — (deprecated → runs deploy)
#
# Daemon & Channel management:
# ./dev.sh daemon restart — Restart the backend daemon (launchd)
# ./dev.sh daemon stop — Stop the daemon
# ./dev.sh daemon start — Start the daemon
# ./dev.sh daemon status — Show daemon status
# ./dev.sh daemon logs — Tail daemon logs (Ctrl-C to stop)
# ./dev.sh channel restart [id] — Restart channel(s) via API
# ./dev.sh channel stop [id] — Stop channel(s)
# ./dev.sh channel list — List all channels + status
set -e
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="$PROJECT_ROOT/backend"
DESKTOP_DIR="$PROJECT_ROOT/desktop"
BACKEND_PORT=8000
BACKEND_PID_FILE="/tmp/swarmai-backend.pid"
LOG_DIR="$HOME/.swarm-ai/logs"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
NC='\033[0m'
mkdir -p "$LOG_DIR"
# ── Helpers ─────────────────────────────────────────────────
_log() { echo -e "${CYAN}[swarm]${NC} $*"; }
_ok() { echo -e "${GREEN}✅${NC} $*"; }
_warn() { echo -e "${YELLOW}⚠️${NC} $*"; }
_err() { echo -e "${RED}❌${NC} $*"; }
# ── Port PID helpers (lsof with 2s timeout — dev script only) ──
# lsof is the ONLY way to get port→PID on macOS (no `ss`, netstat lacks PIDs).
# Wrapped with `timeout 2` to prevent macOS hang (sandbox/network extensions).
# Steering rule "no lsof" applies to daemon/production scripts (use nc -z for
# port checks). Dev scripts may use lsof with timeout for PID discovery.
_get_port_pid() {
# Get first PID listening on a port. Returns empty string on failure/timeout.
local port="$1"
timeout 2 lsof -ti :"$port" 2>/dev/null | head -1
}
_kill_port_holders() {
# Kill all processes listening on a port (with 2s timeout on PID lookup).
local port="$1"
local pids
pids=$(timeout 2 lsof -ti :"$port" 2>/dev/null)
if [ -n "$pids" ]; then
_log "Killing processes on port $port: $pids"
echo "$pids" | xargs kill -9 2>/dev/null || true
sleep 0.5
fi
}
# ── Daemon (shared library) ────────────────────────────────
_DAEMON_CMD="dev.sh"
_DAEMON_VERBOSE=1
source "$PROJECT_ROOT/scripts/daemon-lib.sh"
_is_backend_running() {
if [ -f "$BACKEND_PID_FILE" ]; then
local pid=$(cat "$BACKEND_PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
return 0
fi
rm -f "$BACKEND_PID_FILE"
fi
# Fallback: check by port (nc -z is instant; lsof hangs on some macOS configs)
nc -z 127.0.0.1 $BACKEND_PORT 2>/dev/null
}
_kill_backend() {
if [ -f "$BACKEND_PID_FILE" ]; then
local pid=$(cat "$BACKEND_PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
_log "Stopping backend (PID $pid)..."
kill "$pid" 2>/dev/null
sleep 1
kill -9 "$pid" 2>/dev/null || true
fi
rm -f "$BACKEND_PID_FILE"
fi
# Kill anything else on the port — use kill-by-port helper
if nc -z 127.0.0.1 $BACKEND_PORT 2>/dev/null; then
_kill_port_holders "$BACKEND_PORT"
fi
}
_start_backend() {
_kill_backend
_log "Starting backend on port $BACKEND_PORT..."
cd "$BACKEND_DIR"
# Activate venv and sync deps (always sync to catch new dependencies)
if [ ! -d ".venv" ]; then
_log "Creating venv..."
uv venv .venv
fi
source .venv/bin/activate
uv sync --group dev
# Start in background, log to separate dev file (don't clobber daemon's backend.log)
# SWARMAI_MODE=dev prevents channel gateway startup (avoids Slack Socket Mode conflict
# with the production daemon which may be running simultaneously).
SWARMAI_MODE=dev DATABASE_TYPE=sqlite python main.py --port $BACKEND_PORT \
> "$LOG_DIR/backend-dev.log" 2>&1 &
local pid=$!
echo "$pid" > "$BACKEND_PID_FILE"
# Wait for health check
_log "Waiting for backend..."
for i in $(seq 1 30); do
if curl -s --max-time 1 "http://localhost:$BACKEND_PORT/health" >/dev/null 2>&1; then
_ok "Backend running (PID $pid, port $BACKEND_PORT)"
return 0
fi
# Check if process died
if ! kill -0 "$pid" 2>/dev/null; then
_err "Backend process died. Check $LOG_DIR/backend-dev.log"
tail -20 "$LOG_DIR/backend-dev.log"
return 1
fi
sleep 1
done
_err "Backend didn't respond in 30s. Check $LOG_DIR/backend-dev.log"
return 1
}
_start_frontend() {
_log "Starting frontend dev server..."
cd "$DESKTOP_DIR"
npm install --silent 2>/dev/null
npm run tauri:dev 2>&1 | tee "$LOG_DIR/frontend.log"
}
_build_time() {
local start=$1
local end=$(date +%s)
local elapsed=$((end - start))
local min=$((elapsed / 60))
local sec=$((elapsed % 60))
echo "${min}m ${sec}s"
}
# ── Commands ────────────────────────────────────────────────
cmd_start() {
_log "Starting SwarmAI dev environment..."
# Daemon conflict: kill + bootout daemon before dev mode to avoid two backends
# (two ChannelGateways = duplicate Slack connections, DB write conflicts)
# SIGKILL first (SSE streams block SIGTERM/bootout's internal SIGTERM for 20s)
if _daemon_is_running; then
_warn "Backend daemon running — stopping for dev mode..."
launchctl kill SIGKILL "$GUI_TARGET" 2>/dev/null || true
sleep 1
launchctl bootout "$GUI_TARGET" 2>/dev/null || true
_wait_port_free "$DAEMON_PORT" 5 || true
_ok "Daemon stopped (will re-bootstrap on ./dev.sh kill or next app launch)"
fi
_start_backend
_start_frontend
}
cmd_backend() {
local start=$(date +%s)
_start_backend
_ok "Backend restarted in $(_build_time $start)"
_log "Tail logs: tail -f $LOG_DIR/backend-dev.log"
# NOTE: Daemon is NOT restarted here. The daemon runs the built binary,
# not dev source code. Use './dev.sh deploy' to update the daemon.
if _daemon_is_running; then
_warn "Daemon is running (built binary). Dev backend on port $BACKEND_PORT."
_warn "To update daemon: ./dev.sh deploy"
fi
}
cmd_frontend() {
if ! _is_backend_running; then
_warn "Backend not running — starting it first..."
_start_backend
fi
_start_frontend
}
cmd_build() {
# DEPRECATED: Use ./prod.sh release for full DMG builds,
# or ./dev.sh deploy for backend-only deploy to daemon.
_warn "DEPRECATED: './dev.sh build' is deprecated."
_warn " Backend deploy: ./dev.sh deploy (PyInstaller + deploy + restart)"
_warn " Full release: ./prod.sh release (backend + DMG + tag + publish)"
echo ""
_log "Running './dev.sh deploy' for you..."
cmd_deploy
}
cmd_deploy() {
local start=$(date +%s)
_log "Backend-only deploy to daemon (no frontend/Tauri)..."
echo ""
# Step 0: Sync versions
_log "Step 0: Syncing version from VERSION file..."
bash "$PROJECT_ROOT/scripts/sync-version.sh"
echo ""
# Step 1: PyInstaller
_log "Step 1/3: PyInstaller backend build..."
cd "$DESKTOP_DIR"
npm run build:backend
# Step 2: Verify
_log "Step 2/3: Post-build verification..."
cd "$BACKEND_DIR"
if python scripts/verify_build.py "$BACKEND_BINARY"; then
_ok "Verification passed"
else
_err "Verification FAILED — binary has missing capabilities"
return 1
fi
# Step 3: Deploy then restart (deploy first — script may die on kill)
_log "Step 3/3: Deploy to daemon..."
local daemon_was_running=false
if _daemon_is_running; then
daemon_was_running=true
fi
_deploy_daemon_binary
echo ""
_ok "Deploy complete in $(_build_time $start)"
# Kill old daemon so it restarts with new binary (deploy already done)
# Use SIGKILL (not SIGTERM): SSE streams block SIGTERM indefinitely.
# KeepAlive is safe here because deploy already finished (no rsync race).
if [ "$daemon_was_running" = true ]; then
_log "Restarting daemon to pick up new binary..."
launchctl kill SIGKILL "$GUI_TARGET" 2>/dev/null || true
_daemon_wait_healthy 90
else
_log "Daemon was not running. Start with: ./dev.sh daemon start"
fi
}
cmd_quick() {
local start=$(date +%s)
_log "Quick build (skip PyInstaller, Tauri only)..."
cd "$DESKTOP_DIR"
# Check backend bundle exists (onedir, bundled into .app for daemon deployment)
local binary="$DESKTOP_DIR/src-tauri/binaries/python-backend-aarch64-apple-darwin/python-backend"
if [ ! -f "$binary" ]; then
_warn "No backend bundle found — need full build first"
_log "Running: ./dev.sh deploy"
cmd_deploy
return
fi
_log "Step 1/2: Frontend build..."
npm run build
_log "Step 2/2: Tauri build..."
npm run tauri build
_ok "Quick build complete in $(_build_time $start)"
local dmg=$(ls "$DESKTOP_DIR/src-tauri/target/release/bundle/dmg/"*.dmg 2>/dev/null | head -1)
if [ -n "$dmg" ]; then
_ok "DMG: $dmg ($(du -h "$dmg" | cut -f1))"
fi
}
cmd_kill() {
_kill_backend
# Kill Vite dev server
if nc -z 127.0.0.1 1420 2>/dev/null; then
_log "Killing Vite dev server..."
_kill_port_holders 1420
fi
_ok "All dev processes stopped"
# Re-bootstrap daemon if plist exists (restore 24/7 Slack after dev session)
local plist="$HOME/Library/LaunchAgents/${DAEMON_LABEL}.plist"
if [ -f "$plist" ] && ! _daemon_is_running; then
_log "Re-starting backend daemon (Slack/channels back online)..."
cmd_daemon start
fi
}
cmd_status() {
echo ""
echo "SwarmAI Dev Status"
echo "──────────────────"
# Backend
if _is_backend_running; then
local pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || _get_port_pid $BACKEND_PORT)
local health=$(curl -s --max-time 2 "http://localhost:$BACKEND_PORT/health" 2>/dev/null)
if echo "$health" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('status')=='healthy' else 1)" 2>/dev/null; then
_ok "Backend: running (PID $pid, port $BACKEND_PORT)"
else
_warn "Backend: process alive but not healthy (PID $pid)"
fi
else
_err "Backend: not running"
fi
# Frontend
if nc -z 127.0.0.1 1420 2>/dev/null; then
_ok "Frontend: Vite dev server running (port 1420)"
else
_err "Frontend: not running"
fi
# Daemon
echo ""
if _daemon_is_running; then
local daemon_port=""
local daemon_health=""
# Read port from backend.json (source of truth written by running daemon)
local bjson="$HOME/.swarm-ai/backend.json"
if [ -f "$bjson" ]; then
daemon_port=$(python3 -c "import json; print(json.load(open('$bjson')).get('port',''))" 2>/dev/null)
fi
: "${daemon_port:=$DAEMON_PORT}" # fallback to hardcoded constant
daemon_health=$(curl -sf --max-time 2 "http://127.0.0.1:${daemon_port}/health" 2>/dev/null)
if [ -n "$daemon_health" ]; then
local dver=$(echo "$daemon_health" | python3 -c "import sys,json; print(json.load(sys.stdin).get('version','?'))" 2>/dev/null)
_ok "Daemon: healthy on port ${daemon_port} (v${dver})"
else
_warn "Daemon: launchd running but API not responding on port ${daemon_port}"
fi
_check_daemon_version 2>/dev/null || true
else
_err "Daemon: not running"
fi
# Backend bundle (PyInstaller onedir)
local binary="$DESKTOP_DIR/src-tauri/binaries/python-backend-aarch64-apple-darwin/python-backend"
if [ -f "$binary" ]; then
local bundle_dir="$DESKTOP_DIR/src-tauri/binaries/python-backend-aarch64-apple-darwin"
local age=$(( ($(date +%s) - $(stat -f %m "$binary")) / 3600 ))
_ok "Backend bundle: exists ($(du -sh "$bundle_dir" | cut -f1), ${age}h old)"
else
_warn "Backend bundle: not built"
fi
# Recent changes since last build
echo ""
_log "Recent uncommitted changes:"
cd "$PROJECT_ROOT"
git diff --stat HEAD 2>/dev/null | tail -5
echo ""
}
# ── Channel management ──────────────────────────────────────
_api() {
# Helper: call daemon API, pretty-print JSON response
local method="$1" path="$2"
local resp
resp=$(curl -sfL -X "$method" "${DAEMON_API}${path}" 2>&1) || {
_err "API call failed: $method $path"
_warn "Is daemon running? Try: ./dev.sh daemon status"
return 1
}
echo "$resp" | python3 -m json.tool 2>/dev/null || echo "$resp"
}
cmd_channel() {
local sub="${1:-list}"
local channel_id="${2:-}"
case "$sub" in
list)
_log "Channels:"
_api GET /api/channels
;;
restart)
if [ -z "$channel_id" ]; then
# Restart ALL channels
_log "Restarting all channels..."
local ids
ids=$(curl -sfL "${DAEMON_API}/api/channels/" | python3 -c "
import sys, json
data = json.load(sys.stdin)
channels = data if isinstance(data, list) else data.get('channels', [])
for ch in channels:
print(ch['id'])
" 2>/dev/null)
if [ -z "$ids" ]; then
_warn "No channels found"
return
fi
for id in $ids; do
_log " Restarting $id..."
_api POST "/api/channels/${id}/restart"
done
_ok "All channels restarted"
else
_log "Restarting channel $channel_id..."
_api POST "/api/channels/${channel_id}/restart"
fi
;;
stop)
if [ -z "$channel_id" ]; then
_warn "Usage: ./dev.sh channel stop <channel_id>"
_warn "Use './dev.sh channel list' to see channel IDs"
return 1
fi
_log "Stopping channel $channel_id..."
_api POST "/api/channels/${channel_id}/stop"
;;
status)
if [ -z "$channel_id" ]; then
cmd_channel list
return
fi
_log "Channel $channel_id:"
_api GET "/api/channels/${channel_id}/status"
;;
*)
echo "Usage: ./dev.sh channel [list|restart|stop|status] [channel_id]"
;;
esac
}
# ── Main ────────────────────────────────────────────────────
case "${1:-start}" in
start) cmd_start ;;
backend) cmd_backend ;;
frontend) cmd_frontend ;;
build) cmd_build ;;
deploy) cmd_deploy ;;
quick) cmd_quick ;;
kill) cmd_kill ;;
status) cmd_status ;;
daemon) shift; cmd_daemon "$@" ;;
channel) shift; cmd_channel "$@" ;;
*)
echo "Usage: ./dev.sh [command]"
echo ""
echo "Commands:"
echo " start Start backend + frontend (default)"
echo " backend Restart backend only (after Python changes)"
echo " frontend Start frontend only"
echo " deploy Build backend + deploy to daemon (the dev deploy path)"
echo " build (deprecated → runs deploy)"
echo " quick Quick build: skip PyInstaller (frontend/Rust only)"
echo " kill Stop all dev processes"
echo " status Show what's running"
echo ""
echo "Daemon:"
echo " daemon restart Restart the backend daemon (launchd)"
echo " daemon stop Stop the daemon"
echo " daemon start Start the daemon"
echo " daemon status Show daemon health (default)"
echo " daemon logs Tail daemon logs"
echo ""
echo "Channels:"
echo " channel list List all channels + status"
echo " channel restart Restart all channels (or: channel restart <id>)"
echo " channel stop <id> Stop a specific channel"
echo " channel status [id] Show channel status"
;;
esac