-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwatcher.sh
More file actions
executable file
·279 lines (242 loc) · 9.74 KB
/
watcher.sh
File metadata and controls
executable file
·279 lines (242 loc) · 9.74 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
#!/usr/bin/env bash
# Apollo Watcher (Cloud) - Install, Update, and Run
#
# Connects to Apollo's cloud backend — no Docker required.
#
# First run: Installs prerequisites, sets up hooks, starts the Watcher UI
# Later runs: Checks for updates, starts the Watcher UI
#
# Usage:
# ./watcher.sh # Normal start (with auto-update)
# ./watcher.sh --no-update # Skip update check
# ./watcher.sh --install-only # Install/update prerequisites without starting
# ./watcher.sh --uninstall # Stop services and remove Claude Code hooks
#
# Port configuration (via .env or environment variables):
# WATCHER_BASE_PORT Base port; backend = base + 1 (default: 31410)
# WATCHER_BACKEND_PORT Watcher UI backend port (default: 31411)
# The cloud release backend serves the built frontend, so hook links use
# WATCHER_BACKEND_PORT — there is no separate frontend port to configure.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WATCHER_VERSION="$(cat "$SCRIPT_DIR/.version" 2>/dev/null || true)"
if [ -z "$WATCHER_VERSION" ]; then
WATCHER_VERSION="unknown"
fi
# Ensure WATCHER_API_URL has a scheme: keep existing http(s)://, add http://
# for localhost, default to https:// for everything else (bare hostnames).
normalize_watcher_api_url() {
local raw_url="$1"
if [[ "$raw_url" =~ ^https?:// ]]; then
printf '%s\n' "$raw_url"
return 0
fi
if [[ "$raw_url" =~ ^(localhost|127\.0\.0\.1)([:/]|$) ]]; then
printf 'http://%s\n' "$raw_url"
return 0
fi
printf 'https://%s\n' "$raw_url"
}
configure_release_ports() {
WATCHER_BASE_PORT="${WATCHER_BASE_PORT:-31410}"
export WATCHER_BACKEND_PORT="${WATCHER_BACKEND_PORT:-$((WATCHER_BASE_PORT + 1))}"
# In release artifacts, the backend serves the built frontend. Hook links
# must therefore point at the backend port, not the dev Vite port.
export WATCHER_FRONTEND_PORT="$WATCHER_BACKEND_PORT"
}
if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then
return 0
fi
# ─── Parse flags ───
NO_UPDATE=false
INSTALL_ONLY=false
UNINSTALL=false
for arg in "$@"; do
case "$arg" in
--no-update) NO_UPDATE=true ;;
--install-only) INSTALL_ONLY=true ;;
--uninstall) UNINSTALL=true ;;
-*) echo "Unknown flag: $arg"; echo "Usage: ./watcher.sh [--no-update] [--install-only] [--uninstall]"; exit 1 ;;
esac
done
# ─── Uninstall ───
# Stop services and remove Claude Code hooks. Does not delete the .venv, the
# .env, or ~/.apollo_monitor — those are preserved by default; the script
# prints removal commands at the end.
do_uninstall() {
echo "Uninstalling Apollo Watcher..."
echo ""
hooks_removed=false
if [ -d "$SCRIPT_DIR/.venv" ]; then
echo "Stopping Watcher processes..."
(cd "$SCRIPT_DIR" && uv run --no-sync python -m apollo_monitor.watcher.watcher all stop) 2>/dev/null || true
echo "Removing Claude Code hooks..."
if (cd "$SCRIPT_DIR" && uv run --no-sync python -m apollo_monitor.watcher.watcher live uninstall); then
hooks_removed=true
else
echo ""
echo "Hook removal failed. Edit ~/.claude/settings.json manually and remove"
echo "entries containing 'apollo_monitor' from the 'hooks' and 'statusLine'"
echo "sections."
fi
else
echo "No .venv found at $SCRIPT_DIR/.venv — skipping process stop and hook removal."
echo "To remove hooks manually:"
echo " uv run --project \"$SCRIPT_DIR\" python -m apollo_monitor.watcher.watcher live uninstall"
fi
echo ""
if [ "$hooks_removed" = true ]; then
echo "Apollo Watcher hooks and services have been removed."
else
echo "Apollo Watcher services have been stopped, but Claude Code hooks were NOT removed."
fi
echo ""
echo "The following are preserved. Remove manually if you want a full wipe:"
echo " - Python venv: rm -rf \"$SCRIPT_DIR/.venv\""
echo " - Frontend modules: rm -rf \"$SCRIPT_DIR/src/apollo_monitor/watcher/frontend/node_modules\""
echo " - Configuration: rm \"$SCRIPT_DIR/.env\""
echo " - Watcher data: rm -rf \"$HOME/.apollo_monitor\""
echo " - This directory: rm -rf \"$SCRIPT_DIR\""
echo ""
echo "After hooks are removed, restart Claude Code so any running sessions pick up the change."
[ "$hooks_removed" = true ]
}
if [ "$UNINSTALL" = true ]; then
if do_uninstall; then
exit 0
else
exit 1
fi
fi
# Capture caller-provided URL overrides before sourcing .env. .env is rewritten
# on every run (see below), so without this, a value baked into .env from a prior
# run would clobber the caller's env var.
CALLER_WATCHER_API_URL="${WATCHER_API_URL:-}"
CALLER_XYLON_URL="${XYLON_URL:-}"
# Load .env if it exists (for URL overrides and port config)
if [ -f "$SCRIPT_DIR/.env" ]; then
set -a
source "$SCRIPT_DIR/.env"
set +a
fi
# Precedence: caller's env vars > values sourced from .env > default. Both
# WATCHER_API_URL and XYLON_URL are accepted at each layer, in that order.
XYLON_URL="${CALLER_WATCHER_API_URL:-${CALLER_XYLON_URL:-${WATCHER_API_URL:-${XYLON_URL:-https://app.apolloresearch.ai/api}}}}"
export XYLON_URL="$(normalize_watcher_api_url "$XYLON_URL")"
# Port defaults — set WATCHER_BASE_PORT to shift all ports together,
# or override WATCHER_BACKEND_PORT for fine-grained control.
configure_release_ports
# ─── Helper functions ───
confirm() {
local prompt="$1"
read -rp "$prompt [Y/n] " response
[[ -z "$response" || "$response" =~ ^[Yy] ]]
}
ensure_command() {
local cmd="$1" name="$2" install_cmd="$3" post_install="${4:-}"
if command -v "$cmd" >/dev/null 2>&1; then
return 0
fi
echo "$name is not installed."
local display_cmd="$install_cmd"
if [ -n "$post_install" ]; then
display_cmd="$install_cmd && $post_install"
fi
if confirm "Install $name? ($display_cmd)"; then
$install_cmd
if [ -n "$post_install" ]; then
$post_install
fi
else
echo "Error: $name is required. Please install it manually."
exit 1
fi
}
# ─── Prerequisites ───
echo "Watcher v${WATCHER_VERSION}"
echo "=================="
echo ""
# Homebrew
if ! command -v brew >/dev/null 2>&1; then
echo "Homebrew is not installed."
if confirm "Install Homebrew? (/bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\")"; then
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
else
echo "Error: Homebrew is required to install other dependencies."
exit 1
fi
fi
ensure_command uv "uv (Python package manager)" "brew install uv"
ensure_command node "Node.js" "brew install node"
ensure_command gh "GitHub CLI" "brew install gh"
# Ensure gh is authenticated (needed for auto-update from private repo)
if ! gh auth status >/dev/null 2>&1; then
echo "GitHub CLI is not authenticated. Running 'gh auth login'..."
gh auth login
fi
echo ""
echo "All prerequisites installed."
echo ""
# ─── Auto-update (via git fetch + reset) ───
# Uses reset --hard to always match remote main, even after force-push.
if [ "$NO_UPDATE" = false ] && [ -d "$SCRIPT_DIR/.git" ]; then
echo "Checking for updates..."
OLD_HEAD=$(git -C "$SCRIPT_DIR" rev-parse HEAD)
git -C "$SCRIPT_DIR" fetch origin
PROCEED=true
if [ -n "$(git -C "$SCRIPT_DIR" status --porcelain)" ]; then
echo "Local changes:"
git -C "$SCRIPT_DIR" status
if ! confirm "You have local changes. Discard them and update to match remote?"; then
echo "Skipping update."
PROCEED=false
fi
fi
if [ "$PROCEED" = true ]; then
git -C "$SCRIPT_DIR" reset --hard origin/HEAD 2>/dev/null || git -C "$SCRIPT_DIR" reset --hard origin/main
if [ "$(git -C "$SCRIPT_DIR" rev-parse HEAD)" != "$OLD_HEAD" ]; then
echo "Updated. Re-launching with new version..."
exec "$0" --no-update "$@"
fi
fi
fi
[ "$INSTALL_ONLY" = true ] && { echo "Install complete."; exit 0; }
# ─── Install dependencies ───
if [ ! -d "$SCRIPT_DIR/.venv" ]; then
echo "Installing Python dependencies..."
(cd "$SCRIPT_DIR" && uv sync)
fi
FRONTEND_DIR="$SCRIPT_DIR/src/apollo_monitor/watcher/frontend"
if [ -d "$FRONTEND_DIR" ] && [ ! -d "$FRONTEND_DIR/node_modules" ]; then
echo "Installing frontend dependencies..."
(cd "$FRONTEND_DIR" && npm install)
fi
# ─── Write .env for hooks ───
# Hooks run as separate processes (invoked by Claude Code) and don't inherit
# our shell environment. Write the current config to .env so hooks can source it.
ENV_FILE="$SCRIPT_DIR/.env"
{
echo "# Auto-generated by watcher.sh — hooks source this file on each invocation."
echo "# Edit manually or re-run watcher.sh to regenerate."
echo ""
echo "# Watcher API (cloud)"
echo "WATCHER_API_URL=${XYLON_URL}"
echo ""
echo "# Ports (set WATCHER_BASE_PORT to shift all ports together)"
echo "WATCHER_BASE_PORT=${WATCHER_BASE_PORT}"
echo "WATCHER_FRONTEND_PORT=${WATCHER_FRONTEND_PORT}"
echo "WATCHER_BACKEND_PORT=${WATCHER_BACKEND_PORT}"
} > "$ENV_FILE"
echo "Wrote $ENV_FILE"
# ─── Setup Claude Code hooks + start Watcher ───
echo "Configuring Claude Code hooks..."
(cd "$SCRIPT_DIR" && uv run python -m apollo_monitor.watcher.watcher live setup)
echo ""
echo "Apollo Watcher is running!"
echo " Watcher UI: http://localhost:${WATCHER_BACKEND_PORT}"
echo " Local API: http://localhost:${WATCHER_BACKEND_PORT}"
echo " Cloud API: ${XYLON_URL}"
echo ""
echo "Press Ctrl-C to stop."
echo ""
(cd "$SCRIPT_DIR" && uv run python -m apollo_monitor.watcher.watcher all start)