#!/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)" # `import venv` succeeding proves nothing: Debian ships the venv module but # splits ensurepip into a separate package, and `python3 -m venv` then dies # with "ensurepip is not available" only after creating a half-built .venv. # Ask for ensurepip by name, before anything is written to disk. if ! (cd / && python3 -c "import ensurepip" 2>/dev/null); then _pyver="$(cd / && python3 -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")')" die "python3 has the venv module but not ensurepip, so it cannot create a venv. Debian/Ubuntu split this out. Install it and re-run: sudo apt install python3.${_pyver#*.}-venv (or: sudo apt install python3-venv)" fi ok "ensurepip present" # ---------------------------------------------------------------- venv say "Virtualenv + package" if [[ ! -x "$VENV/bin/python" ]]; then # Python's own failure text names the exact missing package. Do not paper over # it with a guess — say where to look and get out of the way. (cd / && python3 -m venv "$VENV") || die "could not create a venv at $VENV Python printed the reason above; it is more specific than anything this script could infer. A leftover half-built $VENV may need removing first." 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="" BUS_FIXED_UP="" if command -v systemctl >/dev/null 2>&1; then if systemctl --user show-environment >/dev/null 2>&1; then USER_BUS="yes" else # A shell entered via `su` keeps the ORIGINAL user's XDG_RUNTIME_DIR, so # `systemctl --user` looks for the bus in someone else's directory and finds # nothing — even when this user's manager is running perfectly well. The # runtime dir is /run/user/ by definition, so work it out instead of # asking someone to export it by hand and get the digits right. _own_runtime="/run/user/$(id -u)" if [[ -d "$_own_runtime" && "${XDG_RUNTIME_DIR:-}" != "$_own_runtime" ]]; then export XDG_RUNTIME_DIR="$_own_runtime" if systemctl --user show-environment >/dev/null 2>&1; then USER_BUS="yes" BUS_FIXED_UP="yes" fi fi fi fi if [[ -n "$BUS_FIXED_UP" ]]; then ok "user bus found at $XDG_RUNTIME_DIR (the inherited XDG_RUNTIME_DIR pointed elsewhere)" warn "this shell still has the old value. For later commands in THIS shell:" warn " export XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR" 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})." if [[ ! -d "/run/user/$(id -u)" ]]; then warn "/run/user/$(id -u) does not exist — $USER has no logind session at all." warn " sudo loginctl enable-linger $USER # then re-run; no fresh login needed" else warn "/run/user/$(id -u) exists but the manager there did not answer." warn " systemctl status user@$(id -u).service # start here" fi fi warn "until then, run the three by hand (from anywhere EXCEPT $REPO):" warn " $VENV/bin/socialmag-openbridge &" warn " $VENV/bin/python -m http.server 8321 --directory $REPO &" warn " $VENV/bin/socialmag-refresh # one pass, whenever you want it" warn " (-m from inside the repo imports its selectors.py over the stdlib's)" 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" <