Repository navigation
Expand file tree
/
Copy pathinstall-visualizer.sh
More file actions
executable file
·235 lines (213 loc) · 7.57 KB
/
Copy pathinstall-visualizer.sh
File metadata and controls
executable file
·235 lines (213 loc) · 7.57 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
#!/bin/bash
# Engram Visualizer — launchd install/uninstall script
#
# Usage:
# ./scripts/install-visualizer.sh install Install and load the service
# ./scripts/install-visualizer.sh uninstall Unload and remove the service
# ./scripts/install-visualizer.sh status Check service status
# ./scripts/install-visualizer.sh restart Restart the service
#
# Port liveness is probed portably: a node TCP connect (node is mandatory for
# engram anyway), falling back to curl against /api/health. lsof is only an
# optional fast path when it happens to be installed, so `status` no longer
# reports "not listening" on hosts without it.
#
# Directories follow the CLI: ENGRAM_DATA_DIR (default ~/.local/share/engram) and
# ENGRAM_LOGS_DIR (default $ENGRAM_DATA_DIR/logs).
set -euo pipefail
LABEL="com.engram.visualizer"
PLIST_SRC="$(cd "$(dirname "$0")/../launchd" && pwd)/${LABEL}.plist"
PLIST_DST="${HOME}/Library/LaunchAgents/${LABEL}.plist"
DATA_DIR="${ENGRAM_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/engram}"
LOG_DIR="${ENGRAM_LOGS_DIR:-$DATA_DIR/logs}"
ENGRAM_DIR="$(cd "$(dirname "$0")/.." && pwd)"
PORT="${PORT:-3001}"
HOST="127.0.0.1"
# Up-front tool checks: one line naming what to install, instead of a bare
# "command not found" halfway through.
need() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Error: '$1' not found — $2" >&2
exit 1
fi
}
need launchctl "this installer targets macOS launchd; on Linux run the visualizer under your own supervisor (see README)"
need npm "install Node.js 22+ (https://nodejs.org); it ships npm"
ensure_dirs() {
mkdir -p "$LOG_DIR"
}
# A source checkout is rebuilt so the service runs the code on disk. An
# installed package (`npm i -g`: dist/ but no src/, and no tsc) ships built,
# so `engram setup` can install the services from it (#61).
build_engram() {
if [ -d "$ENGRAM_DIR/src" ]; then
echo "Building engram..."
(cd "$ENGRAM_DIR" && npm run build)
elif [ -f "$ENGRAM_DIR/dist/interfaces/cli/index.js" ]; then
echo "Installed package at $ENGRAM_DIR: already built, skipping npm run build"
else
echo "Error: neither src/ nor dist/interfaces/cli/index.js found in $ENGRAM_DIR — run 'npm run build' in a checkout or reinstall the package" >&2
exit 1
fi
}
resolve_node() {
local fnm_default="${HOME}/.local/share/fnm/aliases/default/bin/node"
local homebrew="/opt/homebrew/bin/node"
local system="/usr/local/bin/node"
if [ -x "$fnm_default" ]; then
echo "$fnm_default"
elif [ -x "$homebrew" ]; then
echo "$homebrew"
elif [ -x "$system" ]; then
echo "$system"
else
local fallback
fallback="$(command -v node 2>/dev/null || true)"
if [ -n "$fallback" ]; then
echo "$fallback"
else
echo "ERROR: No node binary found — install Node.js 22+ (https://nodejs.org)." >&2
exit 1
fi
fi
}
# port_listening PORT — succeeds when something accepts TCP connections on
# 127.0.0.1:PORT. Order: lsof (optional fast path), node TCP connect, curl /api/health.
port_listening() {
local port="$1" node_bin
if command -v lsof >/dev/null 2>&1; then
if lsof -nP -iTCP:"$port" -sTCP:LISTEN -t >/dev/null 2>&1; then
return 0
fi
fi
node_bin="$(command -v node 2>/dev/null || true)"
if [ -n "$node_bin" ]; then
if "$node_bin" -e '
const s = require("node:net").connect({ host: process.argv[1], port: Number(process.argv[2]) });
s.setTimeout(1500);
s.once("connect", () => { s.destroy(); process.exit(0); });
s.once("timeout", () => { s.destroy(); process.exit(1); });
s.once("error", () => process.exit(1));
' "$HOST" "$port"; then
return 0
fi
return 1
fi
if command -v curl >/dev/null 2>&1; then
if curl -fsS --max-time 2 "http://${HOST}:${port}/api/health" >/dev/null 2>&1; then
return 0
fi
return 1
fi
echo "Warning: cannot probe port $port — install node (https://nodejs.org) or curl." >&2
return 1
}
# Free the port before (re)installing. Only lsof can map a port to a PID
# portably enough here; without it we just warn so the failure is not silent.
stop_port_holder() {
if command -v lsof >/dev/null 2>&1; then
lsof -nP -iTCP:"$PORT" -sTCP:LISTEN -t 2>/dev/null | xargs kill 2>/dev/null || true
elif port_listening "$PORT"; then
echo "Warning: something is already listening on ${HOST}:${PORT}; the service cannot bind until it is stopped." >&2
fi
}
install_service() {
ensure_dirs
if [ ! -f "$PLIST_SRC" ]; then
echo "Error: plist template not found at $PLIST_SRC"
exit 1
fi
# Stop any existing graph-server process (dev server, previous install)
stop_port_holder
NODE_BIN="$(resolve_node)"
echo "Using node: $NODE_BIN ($($NODE_BIN --version))"
# Build the project first (a checkout); an installed package is already built
build_engram
# Render the template: placeholders are substituted with the *resolved*
# checkout, node binary, and log directory so the service works from any
# clone location and any node installer (fnm, brew, nvm, system).
sed -e "s|__ENGRAM_DIR__|${ENGRAM_DIR}|g" \
-e "s|__NODE_BIN__|${NODE_BIN}|g" \
-e "s|__LOG_DIR__|${LOG_DIR}|g" \
"$PLIST_SRC" > "$PLIST_DST"
if grep -q "__[A-Z_]*__" "$PLIST_DST"; then
echo "Error: unresolved placeholder in rendered plist:" >&2
grep -n "__[A-Z_]*__" "$PLIST_DST" >&2
rm -f "$PLIST_DST"
exit 1
fi
# Load the agent
launchctl unload "$PLIST_DST" 2>/dev/null || true
launchctl load "$PLIST_DST"
echo ""
echo "Engram visualizer installed and loaded."
echo " Plist: $PLIST_DST"
echo " URL: http://localhost:${PORT}/graph"
echo " Logs: $LOG_DIR/visualizer.log"
echo ""
echo "To restart: $0 restart"
echo "To uninstall: $0 uninstall"
}
uninstall_service() {
if [ -f "$PLIST_DST" ]; then
launchctl unload "$PLIST_DST" 2>/dev/null || true
rm -f "$PLIST_DST"
echo "Engram visualizer unloaded and removed."
else
echo "Service not installed (plist not found at $PLIST_DST)."
fi
}
status_service() {
if launchctl list "$LABEL" 2>/dev/null; then
echo ""
echo "Service is loaded."
if port_listening "$PORT"; then
echo "Port ${PORT}: listening"
else
echo "Port ${PORT}: not listening (may be starting)"
fi
else
echo "Service is not loaded."
fi
if [ -f "${LOG_DIR}/visualizer.log" ]; then
echo ""
echo "Last 5 log lines:"
tail -5 "${LOG_DIR}/visualizer.log"
fi
if [ -f "${LOG_DIR}/visualizer-error.log" ]; then
local errors
errors="$(tail -5 "${LOG_DIR}/visualizer-error.log" 2>/dev/null)"
if [ -n "$errors" ]; then
echo ""
echo "Last 5 error lines:"
echo "$errors"
fi
fi
}
restart_service() {
if [ ! -f "$PLIST_DST" ]; then
echo "Service not installed. Run '$0 install' first."
exit 1
fi
launchctl unload "$PLIST_DST" 2>/dev/null || true
launchctl load "$PLIST_DST"
echo "Engram visualizer restarted."
}
case "${1:-}" in
install)
install_service
;;
uninstall)
uninstall_service
;;
status)
status_service
;;
restart)
restart_service
;;
*)
echo "Usage: $0 {install|uninstall|status|restart}"
exit 1
;;
esac