#!/usr/bin/env bash # Hermes front door (Mac / Linux). curl -fsSL https://get.smbdm.com | bash # Windows twin: irm https://get.smbdm.com/win | iex # # One question, then the right path: # 1 Revive Hermes bring the main Hermes back from its encrypted backup (needs the Bitwarden token) # 2 New machine add one of the owner's own computers (no code: joins MANAGED; or --code for a full member) # 3 Fleet worker add a worker computer (no code: joins MANAGED; or --code) # 4 Family Hermes + starter kit; optionally linked for help # 5 Friend same as family # 6 Stranger plain Hermes + clean starter kit; never connects to us # 7 Team member company-managed staff Mac (no code: joins MANAGED; or --code from the tech team) # # MANAGED (code-free 2/3/7): the machine is ours to control but can't reach any of our machines, and its # model key starts at $0 until the owner sets a budget. A --code join is unchanged. # Flags for tests / scripted use: # --mode revive|machine|fleet|family|friend|stranger|team (or 1-7) --watch yes|no # --code JOINCODE --name SHORTNAME (2/3/7 without a code) --profile office|dev (7) # --link-code CODE --dry-run --yes (no questions; take defaults) # --skip-install (do not run the Hermes installer) --no-start (never start/restart services) # --snapshot ID (revive; default latest) --from NAME (revive; default controller) # This file holds no secrets. Revive asks for the Bitwarden token at run time (hidden typing). set -uo pipefail umask 022 FD_VERSION="2026-10-05.3" BASE="${HERMES_FD_BASE:-https://get.smbdm.com}" # Public join/link address of the main Hermes (Tailscale Funnel). Only used by options 2-5. JOIN_BASE="${HERMES_JOIN_BASE:-https://controller.tailb7d574.ts.net}" OFFICIAL_SH="https://hermes-agent.nousresearch.com/install.sh" MODE="" WATCH="" CODE="${HERMES_JOIN_CODE:-}" LINK_CODE="${HERMES_LINK_CODE:-}" NAME="${HERMES_JOIN_NAME:-}" PROFILE="" DRY=0 YES=0 SKIP_INSTALL=0 NO_START=0 SNAP="latest" FROM="controller" say() { printf '\n==> %s\n' "$*"; } info() { printf ' %s\n' "$*"; } warn() { printf '!! %s\n' "$*" >&2; } die() { printf '\nSTOPPED: %s\n' "$*" >&2; exit 1; } has_tty() { (: /dev/null; } dry() { [ "$DRY" = 1 ]; } usage() { cat <<'EOF' Hermes front door. curl -fsSL https://get.smbdm.com | bash (Windows: irm https://get.smbdm.com/win | iex) --mode revive|machine|fleet|family|friend|stranger|team (or 1-7) --watch yes|no --code JOINCODE --name SHORTNAME (machine/fleet/team, no code) --profile office|dev (team) --link-code CODE --dry-run --yes (no questions; take defaults) --skip-install --no-start --snapshot ID (revive) --from NAME (revive; default controller) Pass flags through curl like this: curl -fsSL https://get.smbdm.com | bash -s -- --mode fleet --name office-mini EOF } sha256() { if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}'; else sha256sum "$1" | awk '{print $1}'; fi; } while [ $# -gt 0 ]; do case "$1" in --mode) MODE="${2:-}"; shift 2 ;; --watch) WATCH="${2:-}"; shift 2 ;; --code) CODE="${2:-}"; shift 2 ;; --name) NAME="${2:-}"; shift 2 ;; --profile) PROFILE="${2:-}"; shift 2 ;; --link-code) LINK_CODE="${2:-}"; shift 2 ;; --snapshot) SNAP="${2:-latest}"; shift 2 ;; --from) FROM="${2:-controller}"; shift 2 ;; --dry-run) DRY=1; shift ;; --yes|-y) YES=1; shift ;; --skip-install) SKIP_INSTALL=1; shift ;; --no-start) NO_START=1; shift ;; -h|--help) usage; exit 0 ;; *) die "unknown option: $1 (try --help)" ;; esac done # ask VAR "question" default -> reads from the terminal even under curl | bash ask() { local __v="$1" __q="$2" __d="${3:-}" __a="" if [ "$YES" = 1 ] || ! has_tty; then [ -n "$__d" ] || die "need an answer for: $__q (no terminal; pass it as a flag, see --help)" __a="$__d" else printf '%s ' "$__q" >/dev/tty; IFS= read -r __a /dev/tty; IFS= read -rs __a /dev/tty printf -v "$__v" '%s' "$__a" } yesno() { # yesno "question" default(y|n) -> 0 for yes local a; ask a "$1 [$( [ "$2" = y ] && echo Y/n || echo y/N )]" "$2" case "$a" in y|Y|yes|YES|Yes) return 0 ;; *) return 1 ;; esac } OS="$(uname -s)"; ARCH="$(uname -m)" case "$ARCH" in arm64|aarch64) GOARCH=arm64 ;; x86_64|amd64) GOARCH=amd64 ;; *) GOARCH="$ARCH" ;; esac sedi() { local e="$1" f="$2"; sed "$e" "$f" > "$f.fdtmp" && cat "$f.fdtmp" > "$f" && rm -f "$f.fdtmp"; } HH_DEFAULT="$HOME/.hermes" HH="${HERMES_HOME:-$HH_DEFAULT}" find_hermes() { command -v hermes 2>/dev/null || { [ -x "$HOME/.local/bin/hermes" ] && echo "$HOME/.local/bin/hermes"; } || true; } # --------------------------------------------------------------------------------------------------- menu() { cat <<'EOF' Hermes setup — what are we doing? 1 Revive Hermes bring the main Hermes back from its backup 2 New machine add another of the owner's own computers (no code needed) 3 Fleet worker add a worker computer (no code needed) 4 Family Hermes for a family member 5 Friend Hermes for a friend 6 Stranger plain Hermes, no connection to us 7 Team member company-managed work Mac for a staff member (no code needed) EOF } norm_mode() { case "$1" in 1|revive|Revive) echo revive ;; 2|machine|own|mine) echo machine ;; 3|fleet|worker) echo fleet ;; 4|family) echo family ;; 5|friend) echo friend ;; 6|stranger|plain|public) echo stranger ;; 7|team|staff|member) echo team ;; *) echo "" ;; esac } # --------------------------------------------------------------------------------------------------- install_hermes() { local hb; hb="$(find_hermes)" if [ -n "$hb" ]; then info "Hermes is already installed ($hb) — keeping it."; return 0; fi if [ "$SKIP_INSTALL" = 1 ]; then info "skipping the Hermes installer (--skip-install)"; return 0; fi if dry; then info "[dry-run] would run the official installer: curl -fsSL $OFFICIAL_SH | bash"; return 0; fi say "Installing Hermes with the official installer (a few minutes)" local args=() [ "$YES" = 1 ] && args+=(--non-interactive) # shellcheck disable=SC2086 [ -n "${FD_INSTALL_ARGS:-}" ] && args+=($FD_INSTALL_ARGS) curl -fsSL "$OFFICIAL_SH" | bash -s -- ${args[@]+"${args[@]}"} || die "the Hermes installer failed. Run this line again; it picks up where it stopped." export PATH="$HOME/.local/bin:$PATH" } restart_gateway_if_ours() { [ "$NO_START" = 1 ] && { info "not restarting services (--no-start). Later: hermes gateway restart"; return 0; } dry && { info "[dry-run] would restart the Hermes gateway if it is running"; return 0; } local hb; hb="$(find_hermes)"; [ -n "$hb" ] || return 0 if [ "$HH" = "$HH_DEFAULT" ] && "$hb" gateway status >/dev/null 2>&1; then "$hb" gateway restart >/dev/null 2>&1 && info "gateway restarted (loads the memory kit)" || warn "gateway restart failed; run: hermes gateway restart" else info "gateway not running yet. Start it any time with: hermes gateway start" fi } # Starter kit: SOUL template, first prompts, memory kit (pre-recall plugin + daily drift check). No secrets. install_kit() { local who="$1" kd="$HH/starter-kit" t say "Adding the starter kit ($who)" if dry; then curl -fsSI "$BASE/kit/starter.tar.gz" >/dev/null 2>&1 && info "[dry-run] kit reachable: $BASE/kit/starter.tar.gz" || warn "[dry-run] kit NOT reachable" info "[dry-run] would unpack to $kd, set SOUL.md if none, run scripts/install_memory_kit.sh" return 0 fi t="$(mktemp -d "${TMPDIR:-/tmp}/hermes-kit.XXXXXX")" curl -fsSL -o "$t/kit.tgz" "$BASE/kit/starter.tar.gz" || die "could not download the starter kit" curl -fsSL -o "$t/SHA256SUMS" "$BASE/kit/SHA256SUMS" 2>/dev/null || true if [ -s "$t/SHA256SUMS" ]; then local want got; want="$(awk '/starter.tar.gz/{print $1}' "$t/SHA256SUMS")" got="$(sha256 "$t/kit.tgz")" [ -z "$want" ] || [ "$want" = "$got" ] || die "starter kit checksum mismatch — try again later" fi mkdir -p "$kd" && tar -xzf "$t/kit.tgz" -C "$kd" && rm -rf "$t" mkdir -p "$HH" if [ -s "$HH/SOUL.md" ] && head -c 60 "$HH/SOUL.md" | grep -q '^You are Hermes Agent, built by Nous Research'; then mv "$HH/SOUL.md" "$HH/SOUL.md.stock" # the installer's stock SOUL: swap for the starter one, keep a copy fi # Replace a missing SOUL.md or the installer's stock one (small, starts with the stock line); keep anything personal. if [ ! -s "$HH/SOUL.md" ] || { [ "$(wc -c <"$HH/SOUL.md")" -lt 2000 ] && head -c 60 "$HH/SOUL.md" | grep -q '^You are Hermes Agent, built by Nous Research'; }; then [ -s "$HH/SOUL.md" ] && cp "$HH/SOUL.md" "$HH/SOUL.md.stock" cp "$kd/SOUL-starter.md" "$HH/SOUL.md"; info "SOUL.md set from the starter template (edit it: $HH/SOUL.md)" else info "kept your own SOUL.md (starter template at $kd/SOUL-starter.md)"; fi if [ -n "$(find_hermes)" ] || [ -d "$HH/hermes-agent" ]; then PATH="$HOME/.local/bin:$PATH" HERMES_HOME="$HH" bash "$kd/scripts/install_memory_kit.sh" || warn "memory kit reported a problem (Hermes still works)" else warn "Hermes is not installed, so the memory kit waits. After installing, run: bash $kd/scripts/install_memory_kit.sh" fi info "first things to try: $kd/FIRST-PROMPTS.md" } # --------------------------------------------------------------------------------------------------- probe_join() { # probe_join URL OUTFILE -> prints HTTP code (000 = no answer) curl -sS -m 20 -o "$2" -w '%{http_code}' "$1" 2>/dev/null || true } do_join() { # options 2 and 3 local role="$1" code="$CODE" t http [ -n "$code" ] || { do_open "$role"; return $?; } # no code: managed join code="$(printf '%s' "$code" | tr -cd 'A-Za-z0-9_-')" [ -n "$code" ] || die "no join code. Ask the main Hermes on Telegram: 'join code for a new $role machine'." t="$(mktemp "${TMPDIR:-/tmp}/hermes-join.XXXXXX")" say "Checking the main Hermes is reachable" http="$(probe_join "$JOIN_BASE/join/$code" "$t")" case "$http" in 200) head -c 64 "$t" | grep -q '^#!' || { rm -f "$t"; die "the join answer did not look like a script"; } bash -n "$t" || { rm -f "$t"; die "the join script does not parse"; } info "reachable; join code accepted ($(wc -c <"$t" | tr -d ' ') bytes, sha256 $(sha256 "$t" | cut -c1-12))" if dry; then info "[dry-run] would now run the join script for role=$role watch=$WATCH"; rm -f "$t"; return 0; fi say "Joining (the main Hermes finishes the rest and reports on Telegram)" FD_ROLE="$role" FD_WATCH="$WATCH" bash "$t" /dev/null 2>&1 && return 0 info "installing $name" case "$OS" in Darwin) os_r=darwin; os_c=osx ;; Linux) os_r=linux; os_c=linux ;; *) die "unsupported OS $OS" ;; esac case "$name" in restic) tag="$(curl -fsSL https://api.github.com/repos/restic/restic/releases/latest | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -1)" v="${tag#v}"; [ -n "$v" ] || die "cannot look up restic" curl -fsSL "https://github.com/restic/restic/releases/download/$tag/restic_${v}_${os_r}_${GOARCH}.bz2" | bunzip2 > "$BIN/restic" && chmod 755 "$BIN/restic" ;; rclone) curl -fsSL -o "$WORK/rclone.zip" "https://downloads.rclone.org/rclone-current-${os_c}-${GOARCH}.zip" || die "cannot download rclone" (cd "$WORK" && rm -rf rclone-*-"$os_c"-* && unzip -oq rclone.zip && install -m 755 rclone-*/rclone "$BIN/rclone") ;; bws) case "$OS" in Darwin) pat="macos-universal" ;; Linux) [ "$GOARCH" = arm64 ] && pat="aarch64-unknown-linux-gnu" || pat="x86_64-unknown-linux-gnu" ;; esac url="$(curl -fsSL 'https://api.github.com/repos/bitwarden/sdk-sm/releases?per_page=40' | python3 -c ' import json,sys pat=sys.argv[1] for r in json.load(sys.stdin): if r["tag_name"].startswith("bws-v"): for a in r["assets"]: if pat in a["name"] and a["name"].endswith(".zip"): print(a["browser_download_url"]); sys.exit()' "$pat")" [ -n "$url" ] || die "cannot look up the Bitwarden CLI" curl -fsSL -o "$WORK/bws.zip" "$url" && (cd "$WORK" && rm -rf bwsx && unzip -oq bws.zip -d bwsx && install -m 755 bwsx/bws "$BIN/bws") ;; esac command -v "$name" >/dev/null 2>&1 || die "could not install $name" } do_revive() { [ "$OS" = Darwin ] || warn "The main Hermes was a Mac. Restoring onto $OS works for files, but services and paths are Mac-style." say "Revive: restore the main Hermes from its encrypted backup (backup name: $FROM)" info "Needs: this computer online, and the Bitwarden machine-account access token (project 'Hermes')." info "The old computer does NOT need to be alive." WORK="$HOME/hermes-dr"; BIN="$WORK/bin"; SEC="$WORK/secrets" mkdir -p "$BIN" "$SEC"; chmod 700 "$WORK" "$SEC"; export PATH="$BIN:$HOME/.local/bin:$PATH" trap 'rm -rf "$SEC"' EXIT command -v python3 >/dev/null || die "python3 is missing. On a Mac run: xcode-select --install then run this line again." say "1/7 Backup tools (no admin needed)" fetch_tool restic; fetch_tool rclone; fetch_tool bws info "$(restic version | head -1); $(rclone version | head -1); bws $(bws --version 2>/dev/null | awk '{print $2}')" say "2/7 Bitwarden token" if [ -z "${BWS_ACCESS_TOKEN:-}" ]; then ask_secret BWS_ACCESS_TOKEN "Paste the Bitwarden access token (typing is hidden), then Enter:"; fi [ -n "${BWS_ACCESS_TOKEN:-}" ] || die "no token given" export BWS_ACCESS_TOKEN say "3/7 Fetching the restore keys and the recovery guide from Bitwarden" ( umask 077; bws secret list --output json > "$SEC/all.json" 2>"$SEC/err" ) || die "Bitwarden rejected the token. Make a new access token with READ access to project 'Hermes'." ( umask 077; python3 - "$SEC" "$WORK" <<'PY' import json, sys, os d, w = sys.argv[1], sys.argv[2] s = {x["key"]: x["value"] for x in json.load(open(f"{d}/all.json"))} miss = [k for k in ("BACKUP_RESTIC_PASSWORD", "BACKUP_RCLONE_CONF") if k not in s] if miss: sys.exit("missing in Bitwarden: " + ", ".join(miss)) open(f"{d}/restic.pass", "w").write(s["BACKUP_RESTIC_PASSWORD"].strip() + "\n") open(f"{d}/rclone.conf", "w").write(s["BACKUP_RCLONE_CONF"]) env = "".join(f"export {k}={s[k]!r}\n" for k in ("DR_S3_BUCKET", "DR_S3_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY") if s.get(k)) open(f"{d}/s3.env", "w").write(env) if "DR_RESTORE_README" in s: open(f"{w}/DR_RESTORE_README.md", "w").write(s["DR_RESTORE_README"]) print("keys ok" + ("; S3 copy known" if "DR_S3_BUCKET=" in env else "") + ("; recovery guide saved" if "DR_RESTORE_README" in s else "; (no DR_RESTORE_README in Bitwarden)")) PY ) || die "could not read the restore keys from Bitwarden" rm -f "$SEC/all.json" "$SEC/err" [ -f "$WORK/DR_RESTORE_README.md" ] && chmod 600 "$WORK/DR_RESTORE_README.md" && info "full guide for a human or an AI: $WORK/DR_RESTORE_README.md" export RESTIC_PASSWORD_FILE="$SEC/restic.pass" RCLONE_CONFIG="$SEC/rclone.conf" RESTIC_CACHE_DIR="$WORK/cache" export RESTIC_REPOSITORY="rclone:hermesdr:$FROM" say "4/7 Opening the backup" if ! restic snapshots --compact --latest 3 2>"$WORK/snap.err"; then warn "Google Drive copy failed: $(tail -1 "$WORK/snap.err")" # shellcheck disable=SC1091 . "$SEC/s3.env" [ -n "${DR_S3_BUCKET:-}" ] || die "cannot open the backup and there is no S3 copy. If Google says the sign-in expired: refresh BACKUP_RCLONE_CONF (one Google sign-in), then run again." export RESTIC_REPOSITORY="s3:s3.$DR_S3_REGION.amazonaws.com/$DR_S3_BUCKET/$FROM" info "trying the Amazon S3 copy" restic snapshots --compact --latest 3 || die "cannot open either backup copy" fi if dry; then say "[dry-run] backup opens fine. Stopping before restoring anything."; return 0; fi say "5/7 Restoring snapshot '$SNAP' into a staging folder (several GB; 10-30 minutes)" rm -rf "$WORK/r" restic restore "$SNAP" --target "$WORK/r" || die "restore failed (re-run continues from scratch; the backup is untouched)" local oldhome src stamp d f n oldhome="$(cd "$WORK/r/Users" 2>/dev/null && ls | head -1)" [ -n "$oldhome" ] || oldhome="$(cd "$WORK/r/home" 2>/dev/null && ls | head -1)" src="$WORK/r/Users/$oldhome"; [ -d "$src" ] || src="$WORK/r/home/$oldhome" [ -d "$src/.hermes" ] || die "the backup did not contain a Hermes folder" local oldpath="${src#"$WORK/r"}" say "6/7 Putting everything in place" local hb; hb="$(find_hermes)" if [ -n "$hb" ] && [ "$NO_START" = 0 ] && [ -d "$HOME/.hermes" ]; then "$hb" gateway stop >/dev/null 2>&1 || true; fi stamp="$(date +%Y%m%d%H%M%S)" for d in .hermes .ssh .config; do [ -d "$src/$d" ] || continue if [ -e "$HOME/$d" ]; then mv "$HOME/$d" "$HOME/$d.pre-revive-$stamp"; info "kept the old $d as $d.pre-revive-$stamp"; fi mv "$src/$d" "$HOME/$d" done H="$HOME/.hermes" if [ -d "$src/Library/LaunchAgents" ]; then mkdir -p "$HOME/Library/LaunchAgents" for f in "$src/Library/LaunchAgents"/*; do [ -f "$f" ] && sed "s#$oldpath/#$HOME/#g" "$f" > "$HOME/Library/LaunchAgents/$(basename "$f")"; done fi chmod 700 "$HOME/.ssh" "$H/secrets" 2>/dev/null || true chmod 600 "$HOME"/.ssh/id_* "$H/.env" 2>/dev/null || true n=0 if [ -d "$H/dr-stage/sqlite" ]; then while IFS= read -r f; do f="${f#./}"; mkdir -p "$H/$(dirname "$f")"; cp -p "$H/dr-stage/sqlite/$f" "$H/$f"; rm -f "$H/$f-wal" "$H/$f-shm"; n=$((n+1)) done < <(cd "$H/dr-stage/sqlite" && find . -name '*.db') fi info "databases restored from consistent copies: $n" mkdir -p "$H/secrets" cp "$SEC/restic.pass" "$H/secrets/restic-dr.pass"; cp "$SEC/rclone.conf" "$H/secrets/rclone-dr.conf" chmod 600 "$H/secrets/restic-dr.pass" "$H/secrets/rclone-dr.conf" if [ "$oldpath" != "$HOME" ]; then info "home folder changed ($oldpath -> $HOME): fixing paths in config files" for f in "$H"/config.yaml "$H"/profiles/*/config.yaml "$HOME/.ssh/config" "$H/cron/jobs.json"; do [ -f "$f" ] && grep -qF "$oldpath/" "$f" && sedi "s#$oldpath/#$HOME/#g" "$f" done fi if [ "$SKIP_INSTALL" = 1 ]; then info "skipping the Hermes installer (--skip-install)" else say "Reinstalling Hermes code (it is never in the backup)" curl -fsSL "$OFFICIAL_SH" | bash -s -- --non-interactive || warn "installer reported a problem — run: curl -fsSL $OFFICIAL_SH | bash" export PATH="$HOME/.local/bin:$H/hermes-agent/venv/bin:$PATH" fi local R="$H/fleet/hermes-skills" if [ -f "$R/fleet/scripts/install_memory_kit.sh" ]; then HERMES_HOME="$H" bash "$R/fleet/scripts/install_memory_kit.sh" --no-provider 2>/dev/null || warn "memory kit step reported a problem" fi say "7/7 Checking the restore" local bad=0 ic jobs [ -s "$H/config.yaml" ] && info "OK config.yaml ($(wc -l <"$H/config.yaml" | tr -d ' ') lines)" || { warn "config.yaml missing"; bad=1; } [ -s "$H/memories/MEMORY.md" ] && info "OK memories ($(ls "$H/memories" | wc -l | tr -d ' ') files)" || { warn "memories/MEMORY.md missing"; bad=1; } [ -s "$H/SOUL.md" ] && info "OK SOUL.md" || warn "SOUL.md missing" [ -d "$H/notes" ] && info "OK notes ($(find "$H/notes" -type f | wc -l | tr -d ' ') files)" for f in "$H/state.db" $(cd "$H/dr-stage/sqlite" 2>/dev/null && find . -name '*.db' ! -path './state.db' | sed "s#^\./#$H/#"); do [ -f "$f" ] || continue ic="$(sqlite3 "$f" 'PRAGMA integrity_check;' 2>&1 | head -1)" if [ "$ic" = ok ]; then info "OK ${f#"$H/"} integrity ok"; else warn "${f#"$H/"} integrity: $ic"; bad=1; fi done [ -f "$H/state.db" ] || { warn "state.db missing"; bad=1; } jobs="$(python3 -c 'import json,sys;d=json.load(open(sys.argv[1]));j=d.get("jobs",d) if isinstance(d,dict) else d;print(len(j))' "$H/cron/jobs.json" 2>/dev/null || echo '?')" info "OK scheduled jobs: $jobs" hb="$(find_hermes)"; [ -n "$hb" ] && info "OK $("$hb" --version 2>/dev/null | head -1)" if [ "$NO_START" = 1 ]; then info "services NOT started (--no-start)" else say "Starting the Telegram connection" [ -f "$HOME/Library/LaunchAgents/ai.hermes.gateway.plist" ] && launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/ai.hermes.gateway.plist" 2>/dev/null || true [ -n "$hb" ] && { "$hb" gateway status >/dev/null 2>&1 || "$hb" gateway start >/dev/null 2>&1 || true; } fi rm -rf "$WORK/r" "$WORK/cache" [ "$bad" = 0 ] && say "REVIVED. Restored '$FROM' snapshot '$SNAP'." || say "Restored with WARNINGS above — read them before going on." cat < in Terminal: hermes gateway start 2. macOS permissions when asked (System Settings > Privacy & Security): Full Disk Access + Accessibility. 3. Tailscale: install https://tailscale.com/download and sign in with the same account. 4. Tell Hermes on Telegram: "rebuild the fleet" (it follows RECOVERY.md part D). 5. Other background jobs in ~/Library/LaunchAgents are restored but not started; Hermes checks and starts them. 6. In Bitwarden, delete the temporary machine-account token you used for this restore. Full guide: $WORK/DR_RESTORE_README.md What used to be installed: ~/.hermes/dr-stage/MANIFEST.txt EOF return $bad } # --------------------------------------------------------------------------------------------------- main() { printf 'Hermes front door %s%s\n' "$FD_VERSION" "$(dry && echo ' [dry-run: nothing will be changed]')" local m; m="$(norm_mode "$MODE")" if [ -z "$m" ]; then [ -n "$MODE" ] && die "unknown --mode '$MODE'" menu; local a; ask a "Type 1-7 and press Enter:" ""; m="$(norm_mode "$a")" [ -n "$m" ] || die "please pick a number from 1 to 7" fi if [ "$m" = team ]; then [ "$WATCH" = no ] && die "a team-member Mac is company-managed: it is always watched (drop --watch no)" WATCH=yes elif { [ "$m" = machine ] || [ "$m" = fleet ]; } && [ -z "$CODE" ]; then [ "$WATCH" = no ] && die "a managed (code-free) $m join is always watched (drop --watch no, or use --code)" WATCH=yes elif [ "$m" != revive ] && [ "$m" != stranger ]; then case "$WATCH" in yes|no) ;; y|Y) WATCH=yes ;; n|N) WATCH=no ;; "") local def=y; [ "$m" = friend ] && def=n if yesno "Watch it (health checks and help if something breaks)?" "$def"; then WATCH=yes; else WATCH=no; fi ;; *) die "--watch must be yes or no" ;; esac else WATCH=no; fi say "Mode: $m$( [ "$m" != revive ] && [ "$m" != stranger ] && echo ", watch: $WATCH")" case "$m" in revive) do_revive ;; machine) do_join machine ;; fleet) do_join fleet ;; family|friend|stranger) do_person "$m" ;; team) do_team ;; esac } main