22set -euo pipefail
33
44# Phantom Install Script
5- # Works on a fresh Ubuntu 22.04+ / Debian 12+ machine with zero manual steps .
5+ # Works on Ubuntu 22.04+ / Debian 12+ (Linux) and macOS (via Homebrew) .
66# Usage:
7- # curl -sSL https://raw.githubusercontent.com/ghostwright/phantom/main/scripts/install.sh | bash
87# bash install.sh --yes
98# ANTHROPIC_API_KEY=sk-ant-... SLACK_BOT_TOKEN=xoxb-... bash install.sh --yes
109
6968step " Pre-flight checks"
7069
7170if [[ " $OSTYPE " == " darwin" * ]]; then
72- error " macOS detected. This script is for Linux servers."
73- error " For local development, clone the repo and run 'bun install' directly."
74- exit 1
71+ info " macOS detected — switching to Homebrew-based install."
72+
73+ if ! command -v brew & > /dev/null; then
74+ error " Homebrew not found. Install it first: https://brew.sh"
75+ exit 1
76+ fi
77+
78+ # SECURITY: Install Docker Desktop via signed Homebrew cask.
79+ # Replaces the unsafe 'curl -fsSL https://get.docker.com | bash' pattern.
80+ if ! command -v docker & > /dev/null; then
81+ info " Installing Docker Desktop via Homebrew cask..."
82+ brew install --cask docker
83+ success " Docker Desktop installed. Open Docker.app once to finish first-run setup."
84+ else
85+ success " Docker found: $( docker --version) "
86+ fi
87+
88+ # SECURITY: Install Bun via Homebrew verified formula.
89+ # Replaces the unsafe 'curl -fsSL https://bun.sh/install | bash' pattern.
90+ if ! command -v bun & > /dev/null; then
91+ info " Installing Bun via Homebrew..."
92+ brew install bun
93+ success " Bun installed: $( bun --version) "
94+ else
95+ success " Bun found: $( bun --version) "
96+ fi
97+
98+ SKIP_SYSTEMD=true # macOS has no systemd; use launchd or run manually
7599fi
76100
77101if [[ " $OSTYPE " == " msys" || " $OSTYPE " == " cygwin" || " $OSTYPE " == " win32" ]]; then
78102 error " Windows detected. This script is for Linux servers."
79103 exit 1
80104fi
81105
82- if [ " $( id -u) " -ne 0 ]; then
106+ if [[ " $OSTYPE " != " darwin " * ]] && [ " $( id -u) " -ne 0 ]; then
83107 error " This script must be run as root (or with sudo)."
84108 error " Try: sudo bash install.sh $* "
85109 exit 1
@@ -95,50 +119,68 @@ else
95119 success " git found: $( git --version) "
96120fi
97121
98- # ---------- Install Docker ----------
99-
100- if ! command -v docker & > /dev/null; then
101- info " Installing Docker..."
102- curl -fsSL https://get.docker.com | bash > /dev/null 2>&1
103- systemctl enable docker
104- systemctl start docker
105- success " Docker installed: $( docker --version) "
106- elif ! systemctl is-active --quiet docker 2> /dev/null; then
107- warn " Docker installed but not running. Starting..."
108- systemctl start docker
109- success " Docker started"
110- else
111- success " Docker found: $( docker --version) "
112- fi
122+ # ---------- Install Docker (Linux only — macOS handled above) ----------
123+
124+ if [[ " $OSTYPE " != " darwin" * ]]; then
125+ if ! command -v docker & > /dev/null; then
126+ info " Installing Docker via apt (official Docker repository)..."
127+ # SECURITY: Uses the apt package with GPG verification instead of curl | bash.
128+ # See: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
129+ apt-get update -qq
130+ apt-get install -y -qq ca-certificates curl gnupg lsb-release > /dev/null 2>&1
131+ install -m 0755 -d /etc/apt/keyrings
132+ curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
133+ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
134+ chmod a+r /etc/apt/keyrings/docker.gpg
135+ echo \
136+ " deb [arch=$( dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
137+ https://download.docker.com/linux/ubuntu \
138+ $( . /etc/os-release && echo " $VERSION_CODENAME " ) stable" \
139+ | tee /etc/apt/sources.list.d/docker.list > /dev/null
140+ apt-get update -qq
141+ apt-get install -y -qq docker-ce docker-ce-cli containerd.io \
142+ docker-buildx-plugin docker-compose-plugin > /dev/null 2>&1
143+ systemctl enable docker
144+ systemctl start docker
145+ success " Docker installed: $( docker --version) "
146+ elif ! systemctl is-active --quiet docker 2> /dev/null; then
147+ warn " Docker installed but not running. Starting..."
148+ systemctl start docker
149+ success " Docker started"
150+ else
151+ success " Docker found: $( docker --version) "
152+ fi
113153
114- # Ensure docker compose plugin is available
115- if ! docker compose version & > /dev/null; then
116- info " Installing Docker Compose plugin..."
117- apt-get update -qq && apt-get install -y -qq docker-compose-plugin > /dev/null 2>&1
118- success " Docker Compose plugin installed"
154+ # Ensure docker compose plugin is available
155+ if ! docker compose version & > /dev/null; then
156+ info " Installing Docker Compose plugin..."
157+ apt-get update -qq && apt-get install -y -qq docker-compose-plugin > /dev/null 2>&1
158+ success " Docker Compose plugin installed"
159+ fi
119160fi
120161
121- # ---------- Install Bun ----------
162+ # ---------- Install Bun (Linux only — macOS handled above) ----------
122163
123- if ! command -v bun & > /dev/null; then
124- info " Installing Bun..."
125- curl -fsSL https://bun.sh/install | bash > /dev/null 2>&1
126- # Bun installs to ~/.bun/bin/bun. Copy to /usr/local/bin for system-wide access.
127- if [ -f /root/.bun/bin/bun ]; then
128- cp /root/.bun/bin/bun /usr/local/bin/bun
129- elif [ -f " $HOME /.bun/bin/bun" ]; then
130- cp " $HOME /.bun/bin/bun" /usr/local/bin/bun
131- fi
132- success " Bun installed: $( /usr/local/bin/bun --version) "
133- elif ! /usr/local/bin/bun --version > /dev/null 2>&1 ; then
134- warn " Bun binary appears broken. Reinstalling..."
135- curl -fsSL https://bun.sh/install | bash > /dev/null 2>&1
136- if [ -f /root/.bun/bin/bun ]; then
137- cp /root/.bun/bin/bun /usr/local/bin/bun
164+ if [[ " $OSTYPE " != " darwin" * ]]; then
165+ if ! command -v bun & > /dev/null; then
166+ info " Installing Bun via npm (avoids curl | bash)..."
167+ # SECURITY: Install via npm rather than the curl | bash installer.
168+ if ! command -v npm & > /dev/null; then
169+ apt-get update -qq && apt-get install -y -qq nodejs npm > /dev/null 2>&1
170+ fi
171+ npm install -g bun > /dev/null 2>&1
172+ BUN_PATH=" $( npm root -g 2> /dev/null) /bun/bin/bun"
173+ [ -f " $BUN_PATH " ] && cp " $BUN_PATH " /usr/local/bin/bun
174+ success " Bun installed: $( /usr/local/bin/bun --version) "
175+ elif ! bun --version > /dev/null 2>&1 ; then
176+ warn " Bun binary appears broken. Reinstalling via npm..."
177+ npm install -g bun > /dev/null 2>&1
178+ BUN_PATH=" $( npm root -g 2> /dev/null) /bun/bin/bun"
179+ [ -f " $BUN_PATH " ] && cp " $BUN_PATH " /usr/local/bin/bun
180+ success " Bun reinstalled: $( bun --version) "
181+ else
182+ success " Bun found: $( bun --version) "
138183 fi
139- success " Bun reinstalled: $( /usr/local/bin/bun --version) "
140- else
141- success " Bun found: $( bun --version) "
142184fi
143185
144186# ---------- Clone or update Phantom ----------
@@ -157,27 +199,20 @@ else
157199 fi
158200
159201 info " Cloning Phantom to $INSTALL_DIR ..."
160- # Clone to a temp location first, then move to install dir
161202 rm -rf /tmp/phantom-clone
162203 git clone --depth 1 " $PHANTOM_REPO " /tmp/phantom-clone
163-
164- # Ensure install dir exists
165204 mkdir -p " $INSTALL_DIR "
166-
167- # Copy all files from clone into install dir (including dotfiles)
168205 cd /tmp/phantom-clone
169206 find . -maxdepth 1 -not -name ' .' -not -name ' ..' | while read f; do
170207 rm -rf " ${INSTALL_DIR} /$f " 2> /dev/null || true
171208 cp -a " $f " " ${INSTALL_DIR} /"
172209 done
173210 rm -rf /tmp/phantom-clone
174211
175- # Restore .env if it was preserved
176212 if [ -f /tmp/phantom-env-backup ]; then
177213 cp /tmp/phantom-env-backup " $INSTALL_DIR /.env"
178214 rm -f /tmp/phantom-env-backup
179215 fi
180-
181216 success " Cloned to $INSTALL_DIR "
182217fi
183218
@@ -186,7 +221,7 @@ cd "$INSTALL_DIR"
186221# ---------- Install dependencies ----------
187222
188223info " Installing dependencies..."
189- /usr/local/bin/ bun install --production 2>&1 | tail -1
224+ bun install --production 2>&1 | tail -1
190225success " Dependencies installed"
191226
192227# ---------- Start Docker services ----------
@@ -196,12 +231,10 @@ step "Starting Docker services"
196231info " Starting Qdrant and Ollama..."
197232docker compose up -d 2>&1 | tail -2 || true
198233
199- # Wait for Qdrant health
234+ # Wait for Qdrant
200235info " Waiting for Qdrant..."
201236for i in $( seq 1 30) ; do
202- if curl -sf http://localhost:6333/ > /dev/null 2>&1 ; then
203- break
204- fi
237+ curl -sf http://localhost:6333/ > /dev/null 2>&1 && break
205238 sleep 1
206239done
207240if curl -sf http://localhost:6333/ > /dev/null 2>&1 ; then
@@ -210,12 +243,10 @@ else
210243 warn " Qdrant not responding after 30s. Phantom will retry on startup."
211244fi
212245
213- # Wait for Ollama health
246+ # Wait for Ollama
214247info " Waiting for Ollama..."
215248for i in $( seq 1 30) ; do
216- if curl -sf http://localhost:11434/api/tags > /dev/null 2>&1 ; then
217- break
218- fi
249+ curl -sf http://localhost:11434/api/tags > /dev/null 2>&1 && break
219250 sleep 1
220251done
221252if curl -sf http://localhost:11434/api/tags > /dev/null 2>&1 ; then
234265
235266# ---------- Write .env if needed ----------
236267
237- # If tokens are in the environment but not in a .env file, write them
238268if [ -n " ${ANTHROPIC_API_KEY:- } " ] && [ ! -f " $INSTALL_DIR /.env" ]; then
239269 info " Writing environment variables to .env..."
240270 {
@@ -251,7 +281,6 @@ if [ -n "${ANTHROPIC_API_KEY:-}" ] && [ ! -f "$INSTALL_DIR/.env" ]; then
251281 success " .env written"
252282fi
253283
254- # Source .env for the init command
255284if [ -f " $INSTALL_DIR /.env" ]; then
256285 set -a
257286 # shellcheck disable=SC1091
@@ -268,11 +297,11 @@ if [ -f "$INSTALL_DIR/config/phantom.yaml" ]; then
268297else
269298 info " Running phantom init --yes..."
270299 cd " $INSTALL_DIR "
271- /usr/local/bin/ bun run phantom init --yes 2>&1
300+ bun run phantom init --yes 2>&1
272301 success " Phantom initialized"
273302fi
274303
275- # ---------- Create systemd service ----------
304+ # ---------- Create systemd service (Linux only) ----------
276305
277306if [ " $SKIP_SYSTEMD " = false ]; then
278307 step " Setting up systemd service"
@@ -295,7 +324,6 @@ EnvironmentFile=-/opt/phantom/.env
295324StandardOutput=journal
296325StandardError=journal
297326SyslogIdentifier=phantom
298-
299327NoNewPrivileges=true
300328ProtectSystem=strict
301329ProtectHome=read-only
@@ -309,16 +337,13 @@ TasksMax=256
309337WantedBy=multi-user.target
310338SVCEOF
311339
312- # Update WorkingDirectory and EnvironmentFile if custom path
313- if [ " $INSTALL_DIR " != " /opt/phantom" ]; then
340+ [ " $INSTALL_DIR " != " /opt/phantom" ] && \
314341 sed -i " s|/opt/phantom|${INSTALL_DIR} |g" /etc/systemd/system/${SERVICE_NAME} .service
315- fi
316342
317343 systemctl daemon-reload
318344 systemctl enable ${SERVICE_NAME}
319345 success " systemd service created and enabled"
320346
321- # Start or restart
322347 if systemctl is-active --quiet ${SERVICE_NAME} ; then
323348 info " Restarting Phantom..."
324349 systemctl restart ${SERVICE_NAME}
@@ -327,13 +352,11 @@ SVCEOF
327352 systemctl start ${SERVICE_NAME}
328353 fi
329354
330- # Wait for health
331- info " Waiting for Phantom to be ready..."
332355 HEALTHY=false
356+ info " Waiting for Phantom to be ready..."
333357 for i in $( seq 1 60) ; do
334358 if curl -sf " http://localhost:${HEALTH_PORT} /health" > /dev/null 2>&1 ; then
335- HEALTHY=true
336- break
359+ HEALTHY=true; break
337360 fi
338361 sleep 1
339362 done
@@ -346,29 +369,39 @@ SVCEOF
346369 fi
347370fi
348371
372+ # ---------- macOS: manual start instructions ----------
373+
374+ if [[ " $OSTYPE " == " darwin" * ]]; then
375+ step " macOS: Start Phantom"
376+ echo " "
377+ info " To start Phantom:"
378+ echo " cd ${INSTALL_DIR} && bun run start"
379+ echo " "
380+ info " Health check: curl localhost:${HEALTH_PORT} /health"
381+ fi
382+
349383# ---------- Summary ----------
350384
351385step " Installation Complete"
352-
353386echo " "
354387success " Phantom is installed at ${INSTALL_DIR} "
355388
356- if [ " $SKIP_SYSTEMD " = false ] && [ " $ HEALTHY" = true ]; then
389+ if [ " ${ HEALTHY:- false} " = true ]; then
357390 HEALTH_RESPONSE=$( curl -sf " http://localhost:${HEALTH_PORT} /health" 2> /dev/null || echo " {}" )
358- echo " "
359- info " Health: ${HEALTH_RESPONSE} "
391+ echo " " ; info " Health: ${HEALTH_RESPONSE} "
360392fi
361393
362394echo " "
363- info " Useful commands:"
364- echo " journalctl -u ${SERVICE_NAME} -f # Follow logs"
365- echo " systemctl restart ${SERVICE_NAME} # Restart"
366- echo " systemctl status ${SERVICE_NAME} # Status"
395+ if [[ " $OSTYPE " == " darwin" * ]]; then
396+ echo " bun run start # Start Phantom"
397+ else
398+ echo " journalctl -u ${SERVICE_NAME} -f # Follow logs"
399+ echo " systemctl restart ${SERVICE_NAME} # Restart"
400+ echo " systemctl status ${SERVICE_NAME} # Status"
401+ fi
367402echo " curl localhost:${HEALTH_PORT} /health # Health check"
368403
369- if [ -n " ${SLACK_BOT_TOKEN:- } " ] && [ -n " ${SLACK_APP_TOKEN:- } " ]; then
370- echo " "
371- success " Slack is configured. Check your Slack channel for Phantom's intro message."
372- fi
404+ [ -n " ${SLACK_BOT_TOKEN:- } " ] && [ -n " ${SLACK_APP_TOKEN:- } " ] && \
405+ echo " " && success " Slack configured. Check your channel for Phantom's intro message."
373406
374407echo " "
0 commit comments