Two portability bugs, both found by pre-flighting a second machine (kdmirah, a Debian LXC) rather than by reading the script again. 1. `systemctl` existing does not mean a user bus is reachable. Inside an LXC, over `su`, or with no logind session, `systemctl --user` fails with "Failed to connect to user scope bus". The old check asked whether the binary was on PATH, then ran `systemctl --user daemon-reload` under `set -euo pipefail` — so the install ABORTED halfway, after the venv and before the hermes wiring. It now asks the real question, `systemctl --user show-environment`, and on failure installs no units, says why (naming XDG_RUNTIME_DIR and the usual causes), prints the three commands to run by hand, and continues to hermes. 2. The hermes interpreter was hardcoded to this box's layout, /usr/local/lib/hermes-agent/venv/bin/python. kdmirah installs hermes under ~/.local/bin with its venv at ~/.hermes/hermes-agent/venv. Both are bash wrappers that exec their python, so the path is now read out of the wrapper (with a pip console-script shebang as the second shape). Verified against both machines: each derives its own interpreter and each reports mcp present. Also: the closing summary no longer prints the dashboard URL and `journalctl --user -u socialmag-refresh` when no units were installed. Pointing someone at a journal for a unit that was never created is the same class of lie as a silent fallback — it reads as working and answers with nothing. Both branches fired in a sandbox with a fake HOME and two stub systemctls, one that answers show-environment and one that refuses it. Both exit 0; the refusing one installs nothing, warns loudly, and still registers hermes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
219 lines
9.3 KiB
Bash
Executable File
219 lines
9.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# socialmag installer — clone, run this, sign in, done.
|
|
#
|
|
# What it does not do: log you in. socialmag holds no passwords, which is the
|
|
# whole design. The last step is yours and it only happens once.
|
|
set -euo pipefail
|
|
|
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
VENV="${SOCIALMAG_VENV:-$REPO/.venv}"
|
|
CDP_ENDPOINT="${SOCIALMAG_CDP_ENDPOINT:-http://127.0.0.1:9336}"
|
|
UNIT_DIR="$HOME/.config/systemd/user"
|
|
HERMES_CONFIG="${HERMES_CONFIG:-$HOME/.hermes/config.yaml}"
|
|
|
|
say() { printf '\n\033[1;36m==>\033[0m %s\n' "$*"; }
|
|
ok() { printf ' \033[32mok\033[0m %s\n' "$*"; }
|
|
warn() { printf ' \033[33m!!\033[0m %s\n' "$*"; }
|
|
die() { printf '\n\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
# ---------------------------------------------------------------- python
|
|
|
|
say "Python"
|
|
command -v python3 >/dev/null 2>&1 || die "python3 not found on PATH"
|
|
# Every python3 call below runs from / rather than from here. The repo root is
|
|
# the package dir, so it ships selectors.py and queue.py — names the stdlib also
|
|
# uses. Python puts the current directory first on sys.path, so `python3 -m venv`
|
|
# run from inside a fresh clone dies at `import subprocess` with
|
|
# "module 'selectors' has no attribute 'SelectSelector'". Running from / is the
|
|
# whole fix. (This is the same trap cli.py exists to keep off users.)
|
|
(cd / && python3 - <<'PY') || die "socialmag needs Python 3.10 or newer"
|
|
import sys
|
|
sys.exit(0 if sys.version_info >= (3, 10) else 1)
|
|
PY
|
|
ok "$(cd / && python3 -V)"
|
|
|
|
# ---------------------------------------------------------------- venv
|
|
|
|
say "Virtualenv + package"
|
|
if [[ ! -x "$VENV/bin/python" ]]; then
|
|
(cd / && python3 -m venv "$VENV") || die "could not create a venv at $VENV
|
|
Two things cause this. Read the traceback above to tell them apart:
|
|
- 'No module named venv' -> the venv module is not installed.
|
|
Debian/Ubuntu: sudo apt install python3-venv
|
|
- anything else -> report it; the installer already runs python
|
|
from / to keep this repo off sys.path."
|
|
ok "created $VENV"
|
|
else
|
|
ok "reusing $VENV"
|
|
fi
|
|
|
|
# Editable on purpose. socialmag resolves socialmag.db, state.json and
|
|
# dashboard.json relative to its own __file__; a non-editable install would put
|
|
# all three inside site-packages, where nothing else looks for them.
|
|
(cd / && "$VENV/bin/pip" install --quiet --upgrade pip) >/dev/null
|
|
(cd / && "$VENV/bin/pip" install --quiet -e "$REPO") || die "pip install failed"
|
|
ok "socialmag installed (editable), websockets pulled in"
|
|
|
|
for script in socialmag socialmag-refresh socialmag-openbridge; do
|
|
[[ -x "$VENV/bin/$script" ]] || die "console script $script was not generated — pyproject.toml [project.scripts] did not take"
|
|
done
|
|
ok "console scripts: socialmag, socialmag-refresh, socialmag-openbridge"
|
|
|
|
# ---------------------------------------------------------------- systemd
|
|
|
|
say "systemd user units"
|
|
# systemctl being installed does NOT mean a user bus is reachable. Inside an LXC,
|
|
# over `su`, or on a box with no logind session, `systemctl --user` fails with
|
|
# "Failed to connect to user scope bus". Ask the real question — can we talk to
|
|
# the user manager — instead of asking whether the binary exists.
|
|
USER_BUS=""
|
|
if command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
|
|
USER_BUS="yes"
|
|
fi
|
|
|
|
if [[ -z "$USER_BUS" ]]; then
|
|
warn "no reachable systemd user bus — units NOT installed."
|
|
if ! command -v systemctl >/dev/null 2>&1; then
|
|
warn "reason: systemctl is not on PATH."
|
|
else
|
|
warn "reason: systemctl --user cannot reach a user manager (XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-unset})."
|
|
warn "common causes: an LXC without logind, a shell entered via su, or no lingering."
|
|
warn " sudo loginctl enable-linger $USER # then log in fresh and re-run"
|
|
fi
|
|
warn "until then, run the three by hand from $REPO:"
|
|
warn " $VENV/bin/socialmag-openbridge &"
|
|
warn " $VENV/bin/python -m http.server 8321 &"
|
|
warn " $VENV/bin/socialmag-refresh # one pass, whenever you want it"
|
|
else
|
|
mkdir -p "$UNIT_DIR"
|
|
for src in "$REPO"/deploy/user/*.in; do
|
|
dest="$UNIT_DIR/$(basename "${src%.in}")"
|
|
sed -e "s|@REPO@|$REPO|g" \
|
|
-e "s|@VENV@|$VENV|g" \
|
|
-e "s|@CDP_ENDPOINT@|$CDP_ENDPOINT|g" \
|
|
"$src" > "$dest"
|
|
ok "$(basename "$dest")"
|
|
done
|
|
cp "$REPO/deploy/user/socialmag-refresh.timer" "$UNIT_DIR/socialmag-refresh.timer"
|
|
ok "socialmag-refresh.timer"
|
|
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable --now socialmag-dash.service socialmag-openbridge.service >/dev/null
|
|
systemctl --user enable --now socialmag-refresh.timer >/dev/null
|
|
ok "enabled: dash (:8321), openbridge (:8322), refresh timer (30min)"
|
|
|
|
# These die at logout unless lingering is on. Say so rather than let someone
|
|
# discover it when the dashboard is empty tomorrow.
|
|
if ! loginctl show-user "$USER" -p Linger 2>/dev/null | grep -q 'Linger=yes'; then
|
|
warn "user services stop when you log out. To keep them running:"
|
|
warn " sudo loginctl enable-linger $USER"
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------- hermes
|
|
|
|
say "Hermes MCP registration"
|
|
|
|
# Hermes installs in more than one shape — /usr/local/bin/hermes wrapping a venv
|
|
# under /usr/local/lib, or ~/.local/bin/hermes from a user install. Read the
|
|
# interpreter out of the wrapper rather than hardcoding one box's layout.
|
|
HERMES_PY=""
|
|
if command -v hermes >/dev/null 2>&1; then
|
|
_hermes_bin="$(command -v hermes)"
|
|
if head -c 2 "$_hermes_bin" 2>/dev/null | grep -q '#!'; then
|
|
# A wrapper script: the python it exec's is the interpreter that loads MCP.
|
|
HERMES_PY="$(grep -oE '"[^"]*/bin/python[0-9.]*"|[^ "]*/bin/python[0-9.]*' "$_hermes_bin" 2>/dev/null | head -1 | tr -d '"')"
|
|
fi
|
|
# A console script generated by pip: its own shebang names the interpreter.
|
|
if [[ -z "$HERMES_PY" ]]; then
|
|
_shebang="$(head -1 "$_hermes_bin" 2>/dev/null)"
|
|
[[ "$_shebang" == '#!'*python* ]] && HERMES_PY="${_shebang#\#!}"
|
|
fi
|
|
fi
|
|
|
|
if ! command -v hermes >/dev/null 2>&1; then
|
|
warn "hermes not on PATH — skipping. Register manually later:"
|
|
printf '\n mcp_servers:\n socialmag:\n command: "%s/bin/socialmag"\n args: []\n env:\n SOCIALMAG_CDP_ENDPOINT: "%s"\n\n' "$VENV" "$CDP_ENDPOINT"
|
|
elif [[ ! -f "$HERMES_CONFIG" ]]; then
|
|
die "hermes is installed but $HERMES_CONFIG does not exist.
|
|
Start hermes once so it writes a config, then re-run this installer."
|
|
else
|
|
# Hermes treats the mcp python package as optional: without it the entire MCP
|
|
# subsystem is a no-op that logs at debug level. A server that never loaded
|
|
# looks exactly like one that loaded and did nothing, so check it here rather
|
|
# than let that be discovered later.
|
|
if [[ -x "$HERMES_PY" ]]; then
|
|
if "$HERMES_PY" -c "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('mcp') else 1)"; then
|
|
ok "hermes has the mcp package"
|
|
else
|
|
die "hermes cannot load MCP servers: the 'mcp' package is missing from
|
|
$HERMES_PY
|
|
Without it hermes silently ignores every mcp_servers entry — no error, only a
|
|
debug log line. Install it into that interpreter, then re-run:
|
|
$HERMES_PY -m pip install mcp"
|
|
fi
|
|
else
|
|
warn "could not find hermes's interpreter at $HERMES_PY — cannot check for the mcp package"
|
|
warn "if socialmag's tools never appear in hermes, that is the first thing to check"
|
|
fi
|
|
|
|
if grep -qE '^\s*mcp_servers\s*:' "$HERMES_CONFIG"; then
|
|
warn "$HERMES_CONFIG already has an mcp_servers: key — not touching it."
|
|
warn "Merging YAML blind is how someone's other servers disappear. Add this by hand:"
|
|
printf '\n socialmag:\n command: "%s/bin/socialmag"\n args: []\n env:\n SOCIALMAG_CDP_ENDPOINT: "%s"\n\n' "$VENV" "$CDP_ENDPOINT"
|
|
else
|
|
backup="$HERMES_CONFIG.bak-pre-socialmag-$(date +%Y%m%d%H%M%S)"
|
|
cp "$HERMES_CONFIG" "$backup"
|
|
cat >> "$HERMES_CONFIG" <<EOF
|
|
mcp_servers:
|
|
socialmag:
|
|
command: "$VENV/bin/socialmag"
|
|
args: []
|
|
env:
|
|
SOCIALMAG_CDP_ENDPOINT: "$CDP_ENDPOINT"
|
|
EOF
|
|
ok "registered in $HERMES_CONFIG (backup: $(basename "$backup"))"
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------- done
|
|
|
|
cat <<EOF
|
|
|
|
$(printf '\033[1;32mInstalled.\033[0m') Two things left, both yours:
|
|
|
|
1. Start the browser socialmag drives:
|
|
|
|
$REPO/launch-brave.sh
|
|
|
|
2. Sign in to your platforms in that window — Threads, Facebook, Instagram,
|
|
YouTube, TikTok, X. Once. socialmag stores no passwords; it uses the
|
|
session that now belongs to that browser profile.
|
|
|
|
Then:
|
|
|
|
EOF
|
|
|
|
# Only name the dashboard and the journal when units actually got installed.
|
|
# Pointing someone at `journalctl --user -u socialmag-refresh` for a unit that
|
|
# was never created is the same class of lie as a silent fallback: it reads as
|
|
# working and answers with nothing.
|
|
if [[ -n "$USER_BUS" ]]; then
|
|
cat <<EOF
|
|
dashboard http://127.0.0.1:8321/dashboard.html
|
|
check $VENV/bin/socialmag-refresh
|
|
logs journalctl --user -u socialmag-refresh -f
|
|
|
|
EOF
|
|
else
|
|
cat <<EOF
|
|
check $VENV/bin/socialmag-refresh
|
|
dashboard start the file server yourself (see the warnings above), then
|
|
http://127.0.0.1:8321/dashboard.html
|
|
|
|
No units were installed, so nothing refreshes on a timer yet and there is no
|
|
journal to read. Fix the user bus above and re-run to get those.
|
|
|
|
EOF
|
|
fi
|