#!/bin/sh # Pipe-safe POSIX installer for dtfm, lmdtfy, and dtfm-relay. # Usage: curl -fsSL https://raw.githubusercontent.com/iinozemtsev-lab/dtfm/main/scripts/install.sh | sh set -eu main() { export LC_ALL=C REPO="${DTFM_REPO:-iinozemtsev-lab/dtfm}" GITHUB_URL="${DTFM_GITHUB_URL:-https://github.com}" DEST="${DTFM_INSTALL_DIR:-}" TAG="${DTFM_VERSION:-}" FORCE=0 COMPONENTS="" TMP_DIR="" STAGE_FILE="" usage() { cat <<'EOF' Usage: install.sh [OPTIONS] [COMPONENT...] Install dtfm, lmdtfy, and/or dtfm-relay static binaries from GitHub Releases with SHA-256 checksum verification. Components (default: dtfm lmdtfy): dtfm Operator terminal approval listener lmdtfy Agent-side command dispatcher relay, dtfm-relay Stateless E2EE WebSocket relay (installed as dtfm-relay) all Install dtfm, lmdtfy, and dtfm-relay Options: --dir Target directory (default: /usr/local/bin if root, else ~/.local/bin) --version Install a specific release tag (default: latest release) --repo GitHub repository (default: iinozemtsev-lab/dtfm) --force Reinstall even if already at target version; replace symlinks -h, --help Show this help message and exit Environment overrides: DTFM_INSTALL_DIR, DTFM_VERSION, DTFM_REPO, DTFM_GITHUB_URL EOF } die() { printf 'Error: %s\n' "$*" >&2 exit 1 } has_cmd() { command -v "$1" >/dev/null 2>&1 } cleanup() { if [ -n "${STAGE_FILE:-}" ]; then rm -f -- "$STAGE_FILE" /dev/null 2>&1 || true fi if [ -n "${TMP_DIR:-}" ] && [ -d "$TMP_DIR" ]; then rm -rf -- "$TMP_DIR" /dev/null 2>&1 || true fi } add_component() { case " $COMPONENTS " in *" $1 "*) ;; *) COMPONENTS="${COMPONENTS:+$COMPONENTS }$1" ;; esac } validate_tag() { case "$1" in ""|*..*|*[!0-9A-Za-z.-]*) return 1 ;; esac awk -v t="$1" 'BEGIN { exit (t ~ /^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$/) ? 0 : 1 }' ." DEST="${HOME}/.local/bin" fi fi while :; do case "$DEST" in /) break ;; */) DEST="${DEST%/}" ;; *) break ;; esac done [ -n "$DEST" ] || DEST="/" case "$DEST" in -*) DEST="./$DEST" ;; esac } resolve_latest_tag() { latest_url="${GITHUB_URL}/${REPO}/releases/latest" if has_cmd curl; then effective_url="$(curl -fsSLI --connect-timeout 10 --max-time 30 -o /dev/null -w '%{url_effective}' -- "$latest_url" &1)" || \ die "Failed to resolve latest release from $latest_url" effective_url="$(printf '%s\n' "$wget_out" | awk ' /^[[:space:]]*[Ll]ocation:[[:space:]]*/ { line = $0 sub(/^[[:space:]]*[Ll]ocation:[[:space:]]*/, "", line) sub(/\r$/, "", line) sub(/[[:space:]].*$/, "", line) loc = line } END { print loc } ')" else die "Neither curl nor wget is installed." fi effective_url="${effective_url%%\?*}" effective_url="${effective_url%%\#*}" effective_url="${effective_url%/}" case "$effective_url" in */releases/tag/*) TAG="${effective_url##*/releases/tag/}" ;; *) die "No published release found at $latest_url (redirected to: ${effective_url:-none})." ;; esac } download_file() { dl_url="$1" dl_out="$2" if has_cmd curl; then curl -fsSL --connect-timeout 10 --max-time 120 -o "$dl_out" -- "$dl_url" /dev/null || printf 'unknown')" die "Refusing to overwrite symlink $target -> $link_dest (pass --force to replace)." fi if [ "$FORCE" -ne 1 ] && [ -f "$target" ] && [ -x "$target" ]; then cur_ver="$("$target" --version /dev/null || true)" if [ "$cur_ver" = "$bin $TAG" ]; then continue fi fi TO_INSTALL="${TO_INSTALL:+$TO_INSTALL }$bin" done if [ -z "$TO_INSTALL" ]; then printf '%s (%s) already up to date in %s\n' "$COMPONENTS" "$TAG" "$DEST" exit 0 fi TMP_DIR="$(mktemp -d %s/%s\n' "$bin" "$DEST" "$bin" done first_bin="" for bin in $TO_INSTALL; do first_bin="$bin" break done printf 'Verify build provenance: gh attestation verify "%s/%s" --repo %s\n' "$DEST" "$first_bin" "$REPO" case ":${PATH:-}:" in *":${DEST}:"*) ;; *) printf 'Add to PATH: export PATH="%s:$PATH"\n' "$DEST" ;; esac } main "$@"