#!/usr/bin/env bash # TYGR — installer and lifecycle for the AEGIS harness on a single host. # The frame holds; TYGR hands it to you. # # curl -fsSL https://tygr.sh | bash # ./tygr.sh install | status | logs | stop | start | update | doctor | uninstall # # LAW 13: nothing is ever deleted. Uninstall SHELVES the data directory. set -euo pipefail PROG="${0##*/}"; [ "$PROG" = "bash" ] || [ "$PROG" = "sh" ] && PROG="tygr.sh" VERSION="1.0.0" TYGR_HOME="${TYGR_HOME:-$HOME/.tygr}" KEYS_DIR="${TYGR_HOME}/keys" SHELF_DIR="${TYGR_HOME}/shelf" CONTAINER="${TYGR_CONTAINER:-tygr}" IMAGE="${TYGR_IMAGE:-aeon-harness:slim}" PORT="${TYGR_PORT:-3080}" HOST="${TYGR_HOST:-}" ENGINE="${TYGR_ENGINE:-}" if [ -t 1 ]; then B=$'\033[1m'; D=$'\033[2m'; G=$'\033[32m'; Y=$'\033[33m'; R=$'\033[31m'; N=$'\033[0m' else B=""; D=""; G=""; Y=""; R=""; N=""; fi say() { printf '%s\n' "$*"; } ok() { printf '%s✓%s %s\n' "$G" "$N" "$*"; } warn() { printf '%s!%s %s\n' "$Y" "$N" "$*"; } die() { printf '%s✗%s %s\n' "$R" "$N" "$*" >&2; exit 1; } hr() { printf '%s\n' "${D}────────────────────────────────────────────────────────${N}"; } banner() { say "${G}${B}" cat <<'ASCII' ▄▄▄▄▄▄▄ TYGR █ ▄▄▄ █ the frame around the embers █ ███ █ AEGIS harness · single host · nothing deleted ▀▀▀▀▀▀▀ ASCII say "${N}" } need() { command -v "$1" >/dev/null 2>&1; } alive() { # any HTTP status (401 included) means the harness is up; DSH always answers 401 until a token is presented local code code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "http://127.0.0.1:${PORT}/" 2>/dev/null || echo 000) [ "$code" != "000" ] } pick_engine() { [ -n "$ENGINE" ] && return if need docker; then ENGINE=docker elif need podman; then ENGINE=podman else die "no container engine found — install docker or podman first"; fi } preflight() { need curl || die "curl is required" pick_engine ok "engine: $ENGINE" if ! $ENGINE info >/dev/null 2>&1; then die "$ENGINE is not running or not permitted for user $(id -un). Try: sudo systemctl start $ENGINE" fi ok "engine responding" if [ "$(id -u)" = "0" ]; then warn "running as root — prefer a normal user with docker group access"; fi } ensure_dirs() { mkdir -p "$TYGR_HOME" "$KEYS_DIR" "$SHELF_DIR" chmod 700 "$TYGR_HOME" "$KEYS_DIR" ok "home: $TYGR_HOME" } ensure_keys() { local needed="mertos.kdbx mertos.key mertos.pass" local missing="" for f in $needed; do [ -f "$KEYS_DIR/$f" ] || missing="$missing $f"; done if [ -n "$missing" ]; then warn "vault not complete in $KEYS_DIR (missing:$missing)" say "${D} place these three files there, then re-run: $0 install${N}" say "${D} the harness will not start without a vault — it fails closed on purpose.${N}" return 1 fi chmod 600 "$KEYS_DIR"/* 2>/dev/null || true ok "vault present (3 files)" } image_present() { $ENGINE image inspect "$IMAGE" >/dev/null 2>&1; } pull_image() { if image_present; then ok "image present: $IMAGE"; return; fi warn "image not found locally: $IMAGE" say "${D} LAW 14: images are built in the cloud, not on this machine.${N}" say "${D} set TYGR_IMAGE=registry.example/aegis-harness@sha256:… or load it with:${N}" say "${D} $ENGINE pull $IMAGE${N}" if [[ "$IMAGE" == *"/"* ]] && [[ "$IMAGE" == *"@"* || "$IMAGE" == *"."* ]]; then say "${D} attempting pull…${N}" $ENGINE pull "$IMAGE" || die "pull failed" else die "cannot proceed without the image" fi } container_state() { $ENGINE inspect -f '{{.State.Status}}' "$CONTAINER" 2>/dev/null || echo "absent"; } install_stack() { banner; hr preflight; ensure_dirs ensure_keys || { hr; die "install halted: vault incomplete (see above)"; } pull_image; hr local st; st="$(container_state)" if [ "$st" != "absent" ]; then say "container '$CONTAINER' exists (state: $st) — recreating" $ENGINE rm -f "$CONTAINER" >/dev/null fi $ENGINE run -d \ --name "$CONTAINER" \ --restart unless-stopped \ -p "127.0.0.1:${PORT}:3080" \ -v "$KEYS_DIR:/keys:ro" \ -e "PUBLIC_URL=${HOST:+https://$HOST}" \ -e "TRUSTED_HOST=${HOST:-localhost}" \ "$IMAGE" >/dev/null ok "container started" hr for i in $(seq 1 30); do if alive; then ok "answering on 127.0.0.1:${PORT}"; break; fi sleep 1 done say "" say "${B}next steps${N}" say " 1. open the GUI: ${G}http://127.0.0.1:${PORT}/${N}" say " 2. read the login token: ${D}$ENGINE logs $CONTAINER | grep -o 'token=[A-Za-z0-9_-]*'${N}" if [ -z "$HOST" ]; then say " 3. to publish on a domain, re-run with: ${G}TYGR_HOST=tygr.download $0 install${N}" else say " 3. tunnel: ${G}cloudflared tunnel route dns $HOST${N}" say " ingress: ${D}$HOST -> http://localhost:${PORT}${N}" fi say "" say "${D}LAW 13: this installer never deletes. Data lives in $TYGR_HOME.${N}" } cmd_status() { pick_engine; hr say "${B}TYGR status${N}" say " home: $TYGR_HOME" say " image: $IMAGE" say " container: $CONTAINER ($(container_state))" say " keys: $(ls -1 "$KEYS_DIR" 2>/dev/null | wc -l | tr -d ' ') file(s)" if alive; then ok "responding on 127.0.0.1:${PORT} (401 until a token is presented — that is correct)" else warn "not answering on 127.0.0.1:${PORT}"; fi hr } cmd_doctor() { banner; hr say "${B}doctor${N}" for t in curl jq python3; do if need "$t"; then ok "$t: $($t --version 2>&1 | head -1)"; else warn "$t: missing (optional)"; fi done pick_engine ok "engine: $ENGINE $($ENGINE --version 2>&1 | head -1 | sed 's/^[^0-9]*//')" $ENGINE info >/dev/null 2>&1 && ok "engine responding" || warn "engine not responding" [ -d "$TYGR_HOME" ] && ok "home exists" || warn "home missing (run install)" ensure_keys && ok "vault complete" || true hr say "port ${PORT}: $( (ss -ltn 2>/dev/null || netstat -ltn 2>/dev/null) | grep -c ":$PORT " ) listener(s)" } cmd_uninstall() { pick_engine; hr warn "uninstall stops the container and SHELVES the data — nothing is deleted (LAW 13)" if [ -t 0 ]; then printf 'type SHELVE to continue: '; read -r ans [ "$ans" = "SHELVE" ] || { say "cancelled"; exit 0; } fi if [ "$(container_state)" != "absent" ]; then $ENGINE rm -f "$CONTAINER" >/dev/null; ok "container removed"; fi local ts; ts="$(date -u +%Y%m%dT%H%M%SZ)" if [ -d "$TYGR_HOME" ]; then mv "$TYGR_HOME" "${TYGR_HOME}.shelved-${ts}" ok "shelved to ${TYGR_HOME}.shelved-${ts}" fi say "${D}restore with: mv ${TYGR_HOME}.shelved-${ts} ${TYGR_HOME}${N}" } usage() { cat < install preflight, verify vault, start the harness container status show home, image, container state, reachability start start an existing container stop stop the container (keeps everything) logs follow container logs update pull the configured image and recreate the container doctor check tooling, engine, vault, ports uninstall stop and SHELVE the data directory (never deletes) environment: TYGR_HOME data dir (default: \$HOME/.tygr) TYGR_IMAGE container image (default: aeon-harness:slim) TYGR_PORT host port (default: 3080, bound to 127.0.0.1) TYGR_HOST public hostname (optional, e.g. tygr.sh) TYGR_ENGINE docker|podman (default: autodetect) USAGE } main() { case "${1:-}" in install|--install) install_stack ;; status) cmd_status ;; start) pick_engine; $ENGINE start "$CONTAINER" >/dev/null && ok "started" ;; stop) pick_engine; $ENGINE stop "$CONTAINER" >/dev/null && ok "stopped (nothing removed)" ;; logs) pick_engine; $ENGINE logs -f "$CONTAINER" ;; update) pick_engine; $ENGINE pull "$IMAGE" && $ENGINE rm -f "$CONTAINER" >/dev/null; install_stack ;; doctor) cmd_doctor ;; uninstall|--uninstall) cmd_uninstall ;; version|--version|-v) say "TYGR $VERSION" ;; ""|-h|--help|help) usage ;; *) die "unknown command: $1 (try: $PROG help)" ;; esac } main "$@"