#!/usr/bin/env bash # installer/install.sh — one-shot logmind binary installer. # # Distributed at https://logmind.dev/install.sh (hosted by the # logmind-site Vercel project — separate repo). # # Usage: # curl -fsSL logmind.dev/install.sh | bash # curl -fsSL logmind.dev/install.sh | bash -s -- --prefix=$HOME/.local # curl -fsSL logmind.dev/install.sh | bash -s -- --version=v2.0.0 # # # Pin via env var (preserved for back-compat — see v1.1.0 notes): # LOGMIND_VERSION=v2.0.0 curl -fsSL logmind.dev/install.sh | bash # # What it does: # 1. Detect OS (darwin | linux) + arch (x86_64 | arm64). # 2. Resolve target version: default = latest GitHub Release (auto # fetch — no hardcoded version anywhere in this script). # Override priority: --version= > $LOGMIND_VERSION > latest # 3. Skip when an existing logmind at the same version already lives # at $PREFIX/bin/logmind (idempotent — print "already installed", # exit 0). # 4. Download the matching archive + SHA256SUMS file from the GitHub # Release assets. # 5. SHA256-verify the archive against the published SHA256SUMS line. # 6. Extract, chmod +x, move to $PREFIX/bin/logmind. # 7. Run `logmind --version` as a self-check. # 8. When $GITHUB_ACTIONS=true, print a one-liner pointing CI users at # the `thrillmade/setup-logmind` action. # # Default install prefix: # - $HOME/.local — no sudo required. Users with `~/.local/bin` already # in $PATH get a working binary immediately. We deliberately don't # default to /usr/local because every paste-curl-install script that # requires sudo trips a security-conscious user's flag — and # ~/.local/bin is the XDG-compatible spot. # - Override with --prefix=/usr/local for system-wide install. If # /usr/local/bin isn't user-writable the script will print a # `sudo mv` hint instead of silently failing. # # Windows: not supported by this installer. Use scoop / winget / direct # download from the GitHub Release page. (Bash on Git Bash WOULD work, # but the archive extraction logic targets POSIX tar.) # # v1.1.0 (2026-06-05) — `LOGMIND_VERSION` env var support + idempotency # check + GitHub Actions advisory. Per the 2026-06-05 distribution # lock, this installer is the LAPTOP path; GitHub Actions consumers # should use `uses: thrillmade/setup-logmind@v1.0.0` and let # Dependabot bump the action pin. The advisory at the end of a CI run # nudges them off curl-install when this script slips into a workflow. # # This script is intentionally one file with no external deps beyond # coreutils + curl/wget + tar + (sha256sum | shasum). Maintainability # wins out over modularity at this size; longer is fine. # Bash-only — the script uses [[ ]], $'...' ANSI-C quoting, and # `set -o pipefail`. When piped through `sh` (Debian/Ubuntu point `sh` # at `dash`), those constructs are no-ops or hard errors — most # importantly the [[ ]] in the checksum verification block below would # silently evaluate as false, letting a tampered archive install. So we # detect dash/ash/posh up front and refuse with a corrective message # BEFORE the first set/pipefail line that would error on those shells. # # BASH_VERSION is unset under any non-bash shell; the POSIX `[ -z ... ]` # probe works across dash, ash, posh, and the POSIX sh in Solaris/BSDs. if [ -z "${BASH_VERSION:-}" ]; then printf 'logmind installer needs bash, not sh.\n' >&2 printf 'Re-run with: curl -fsSL logmind.dev/install.sh | bash\n' >&2 exit 1 fi set -euo pipefail # ---------------------------------------------------------------------- # Config + flag parsing # ---------------------------------------------------------------------- REPO="thrillmade/logmind" DEFAULT_PREFIX="${HOME}/.local" PREFIX="" # TARGET_VERSION is seeded from $LOGMIND_VERSION (back-compat with the # pre-1.1.0 env override pattern). The CLI flag --version=… overrides # the env var per the precedence documented in the header block above. # An explicit "latest" is normalised to empty so the resolve step # below fetches the GitHub /latest tag. TARGET_VERSION="${LOGMIND_VERSION:-}" if [ "${TARGET_VERSION}" = "latest" ]; then TARGET_VERSION="" fi # Color helpers — disabled when stdout isn't a TTY (e.g. piped to less). if [[ -t 1 ]] && [[ -t 2 ]]; then C_BOLD=$'\033[1m' C_DIM=$'\033[2m' C_RED=$'\033[31m' C_GREEN=$'\033[32m' C_RESET=$'\033[0m' else C_BOLD=""; C_DIM=""; C_RED=""; C_GREEN=""; C_RESET="" fi info() { printf "%s==>%s %s\n" "${C_GREEN}${C_BOLD}" "${C_RESET}" "$*"; } warn() { printf "%s==>%s %s\n" "${C_RED}${C_BOLD}" "${C_RESET}" "$*" >&2; } fatal() { warn "$*"; exit 1; } dim() { printf "%s%s%s\n" "${C_DIM}" "$*" "${C_RESET}"; } usage() { cat </dev/null 2>&1; } if have curl; then fetch() { curl -fsSL "$1" -o "$2"; } elif have wget; then fetch() { wget -q -O "$2" "$1"; } else fatal "need curl or wget to download. Install one and retry." fi # ---------------------------------------------------------------------- # Resolve target tag # ---------------------------------------------------------------------- # Two resolution paths: # # 1. GitHub API (/repos/.../releases/latest) — gives us the JSON # tag_name directly. Used when curl is available. Cheapest + # most stable: doesn't depend on the unauthenticated /releases/latest # redirect quirks. POSIX-compatible sed extraction; no jq needed. # # 2. Redirect-follow (wget fallback) — wget doesn't expose the final # URL cleanly, so we tail the Location header from `wget -S`. Still # jq-free and works when only wget is on the box. if [[ -z "${TARGET_VERSION}" ]]; then info "resolving latest logmind release..." if have curl; then # /releases/latest JSON is unauthenticated, no rate-limit concerns # for one-shot installers (GH API is 60 req/hour/IP — well above # any reasonable curl-install burst). sed picks out tag_name without # pulling in jq as a hard dep. TARGET_VERSION="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ | sed -n 's/.*"tag_name": "\(.*\)".*/\1/p' \ | head -n1)" # If the API call failed or sed returned nothing (e.g. GitHub # returned an error JSON without tag_name), fall back to the # redirect-follow path that the v1.0.x installer used. if [[ -z "${TARGET_VERSION}" ]]; then LATEST_URL="$(curl -fsSLI -o /dev/null -w '%{url_effective}' "https://github.com/${REPO}/releases/latest")" TARGET_VERSION="${LATEST_URL##*/}" fi else LATEST_URL="$(wget -q --max-redirect 0 -S "https://github.com/${REPO}/releases/latest" 2>&1 | sed -n 's/^[[:space:]]*[Ll]ocation: //p' | head -n1)" TARGET_VERSION="${LATEST_URL##*/}" fi if [[ -z "${TARGET_VERSION}" || "${TARGET_VERSION}" == "latest" ]]; then fatal "could not resolve latest release. Pass --version=vX.Y.Z to override." fi fi # Strip leading "v" for the archive name (matches goreleaser's # .Version template which omits the "v" prefix). VERSION_BARE="${TARGET_VERSION#v}" info "logmind ${C_BOLD}${TARGET_VERSION}${C_RESET} for ${OS}/${ARCH}" # ---------------------------------------------------------------------- # Idempotency — bail out early when the same version is already installed # ---------------------------------------------------------------------- # Two checks make this robust against the "user runs curl-install in # a CI step that already provisioned the same version" pattern that's # common when shell scripts run unconditionally: # # - existing binary at $BIN_DIR/logmind # - its `--version` output mentions $VERSION_BARE (the bare semver # without the leading "v", since `logmind --version` prints # "logmind X.Y.Z (...)" not "logmind vX.Y.Z (...)" — see # internal/version/version.go ldflags substitution). # # Match either "vX.Y.Z" or bare "X.Y.Z" in the version string to cover # both injection styles. When matched, print and exit 0. DEST_PREVIEW="${BIN_DIR}/logmind" if [ -x "${DEST_PREVIEW}" ]; then EXISTING_VER_OUTPUT="$("${DEST_PREVIEW}" --version 2>/dev/null || true)" if [[ "${EXISTING_VER_OUTPUT}" == *"${VERSION_BARE}"* ]] \ || [[ "${EXISTING_VER_OUTPUT}" == *"${TARGET_VERSION}"* ]]; then info "logmind ${TARGET_VERSION} already installed at ${DEST_PREVIEW}" # GitHub Actions advisory — print even on the idempotent path so # CI users discover setup-logmind without re-running the install. if [ "${GITHUB_ACTIONS:-}" = "true" ]; then echo "" info "Tip: GitHub Actions users — use \`uses: thrillmade/setup-logmind@v1.0.0\` instead." dim " See https://github.com/thrillmade/setup-logmind for details." fi exit 0 fi fi # ---------------------------------------------------------------------- # Compose download URLs + paths # ---------------------------------------------------------------------- ARCHIVE_NAME="logmind_${VERSION_BARE}_${OS}_${ARCH}.tar.gz" ARCHIVE_URL="https://github.com/${REPO}/releases/download/${TARGET_VERSION}/${ARCHIVE_NAME}" CHECKSUMS_URL="https://github.com/${REPO}/releases/download/${TARGET_VERSION}/SHA256SUMS" TMP_DIR="$(mktemp -d -t logmind-install.XXXXXX)" trap 'rm -rf "${TMP_DIR}"' EXIT ARCHIVE_PATH="${TMP_DIR}/${ARCHIVE_NAME}" CHECKSUMS_PATH="${TMP_DIR}/SHA256SUMS" # ---------------------------------------------------------------------- # Download # ---------------------------------------------------------------------- info "downloading ${ARCHIVE_NAME}" fetch "${ARCHIVE_URL}" "${ARCHIVE_PATH}" info "downloading SHA256SUMS" fetch "${CHECKSUMS_URL}" "${CHECKSUMS_PATH}" # ---------------------------------------------------------------------- # Verify checksum # ---------------------------------------------------------------------- # Prefer sha256sum (Linux coreutils), fall back to shasum (BSD/Mac). if have sha256sum; then SHA_CMD="sha256sum" elif have shasum; then SHA_CMD="shasum -a 256" else fatal "need sha256sum or shasum to verify download. Install coreutils and retry." fi EXPECTED_LINE="$(grep " ${ARCHIVE_NAME}\$" "${CHECKSUMS_PATH}" || true)" if [[ -z "${EXPECTED_LINE}" ]]; then fatal "no checksum line for ${ARCHIVE_NAME} in SHA256SUMS. Release malformed?" fi EXPECTED_SHA="${EXPECTED_LINE%% *}" ACTUAL_SHA="$(${SHA_CMD} "${ARCHIVE_PATH}" | awk '{print $1}')" if [[ "${EXPECTED_SHA}" != "${ACTUAL_SHA}" ]]; then warn "checksum mismatch!" dim " expected: ${EXPECTED_SHA}" dim " actual: ${ACTUAL_SHA}" fatal "refusing to install. File the bug at https://github.com/${REPO}/issues" fi info "checksum verified" # ---------------------------------------------------------------------- # Extract + install # ---------------------------------------------------------------------- info "extracting" tar -xzf "${ARCHIVE_PATH}" -C "${TMP_DIR}" EXTRACTED_BIN="${TMP_DIR}/logmind" if [[ ! -x "${EXTRACTED_BIN}" ]]; then # Some tarballs put the binary inside a subdir; check there too. EXTRACTED_BIN="$(find "${TMP_DIR}" -name logmind -type f -perm -u+x | head -n1)" if [[ -z "${EXTRACTED_BIN}" ]]; then fatal "no executable named 'logmind' in archive" fi fi mkdir -p "${BIN_DIR}" || true DEST="${BIN_DIR}/logmind" if mv "${EXTRACTED_BIN}" "${DEST}" 2>/dev/null; then info "installed: ${C_BOLD}${DEST}${C_RESET}" else warn "could not write to ${DEST} (permission denied)" dim " try: sudo install -m 0755 \"${EXTRACTED_BIN}\" \"${DEST}\"" dim " or: install.sh --prefix=\$HOME/.local" exit 1 fi chmod +x "${DEST}" # ---------------------------------------------------------------------- # Self-check + success line # ---------------------------------------------------------------------- if ! "${DEST}" --version >/dev/null 2>&1; then fatal "${DEST} --version failed. Binary may be corrupted; please re-run installer." fi VERSION_OUTPUT="$(${DEST} --version)" info "${VERSION_OUTPUT}" # v1.1.0 success line — explicit "$TAG installed to $PREFIX/bin" # format so consumer dashboards (and the user) get a deterministic # string to grep on. Distinct from the `info` "installed:" line above # (that one names the full file path; this one names the directory). printf "%slogmind %s installed to %s%s\n" "${C_GREEN}${C_BOLD}" "${TARGET_VERSION}" "${BIN_DIR}" "${C_RESET}" # ---------------------------------------------------------------------- # PATH advisory # ---------------------------------------------------------------------- # Check if BIN_DIR is in PATH; nag the user once if not. We don't # auto-edit shell rcs (too many shells, too much surface area for the # user's expectations). Just point at the line they need to add. if ! echo ":${PATH}:" | grep -q ":${BIN_DIR}:"; then echo "" warn "${BIN_DIR} is not in your \$PATH" dim " add this to your shell rc file (~/.zshrc, ~/.bashrc, ~/.config/fish/config.fish):" dim " export PATH=\"${BIN_DIR}:\$PATH\"" fi # ---------------------------------------------------------------------- # GitHub Actions advisory # ---------------------------------------------------------------------- # When this installer runs inside a GitHub Actions job (the runner # always sets GITHUB_ACTIONS=true), point CI users at the # `thrillmade/setup-logmind` action — it's the v1.1.0 lock-step # partner for the "install once, stays current forever" UX. The # action handles platform detection, version pinning, Dependabot # updates, and step caching that a one-shot curl-install can't. # Static one-liner; doesn't fail the install if anything's off. if [ "${GITHUB_ACTIONS:-}" = "true" ]; then echo "" info "Tip: GitHub Actions users — use \`uses: thrillmade/setup-logmind@v1.0.0\` instead." dim " See https://github.com/thrillmade/setup-logmind for details." fi echo "" info "${C_BOLD}logmind${C_RESET} installed. Run ${C_BOLD}logmind --help${C_RESET} to get started."