#!/bin/sh # PlanBefore installer for macOS and Linux. # # curl -fsSL https://planbefore.kvnlabs.xyz/install.sh | sh # # Optional environment variables: # # REPOFLOW_VERSION version to install, e.g. v0.1.0 # (default: the latest stable release) # REPOFLOW_INSTALL_DIR destination directory (default: $HOME/.local/bin) # # What it does, in order: detects your OS and CPU architecture, resolves the # version, downloads the archive and checksums.txt from the official PlanBefore # GitHub Releases over HTTPS, verifies the SHA-256 checksum (and stops if it # does not match), extracts `pbnode`, installs it and runs # `pbnode version`. # # It never asks for credentials, never runs `pbnode login`, never touches # your repositories and never edits your shell profile: if the install # directory is not on your PATH it tells you what to add. # # Releases published before the CLI was renamed only contain `repoflow_*` # archives. This installer installs pbnode and nothing else: for such a # release (pinned with REPOFLOW_VERSION, or because it is still the latest # stable one) it stops with a clear message and installs nothing. It never # downloads a `repoflow` archive to install it under the pbnode name. # # Upgrading from the previous `repoflow` command: `pbnode` reuses the same # configuration, Node identity and projects. The old executable is left # untouched (it is reported as legacy); it is never deleted and no `repoflow` # alias is created. # # The whole script is wrapped in main(), called on the last line, so a # truncated download cannot run a partial script. set -eu REPO="kevincoder91/planbefore" RELEASES_URL="https://github.com/${REPO}/releases" DOCS_URL="https://planbefore.kvnlabs.xyz/install" say() { printf '%s\n' "$*" } warn() { printf 'warning: %s\n' "$*" >&2 } fail() { printf 'error: %s\n' "$*" >&2 exit 1 } need() { command -v "$1" >/dev/null 2>&1 || fail "'$1' is required but was not found in PATH" } detect_os() { case "$(uname -s)" in Darwin) echo darwin ;; Linux) echo linux ;; MINGW* | MSYS* | CYGWIN*) fail "on Windows, install with PowerShell: irm https://planbefore.kvnlabs.xyz/install.ps1 | iex" ;; *) fail "unsupported operating system: $(uname -s). See ${DOCS_URL}" ;; esac } detect_arch() { arch=$(uname -m) # An x86_64 shell running under Rosetta 2 on Apple Silicon reports # x86_64; the native arm64 build is the right one. if [ "$1" = darwin ] && [ "$arch" = x86_64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = 1 ]; then arch=arm64 fi case "$arch" in x86_64 | amd64) echo amd64 ;; arm64 | aarch64) echo arm64 ;; *) fail "unsupported CPU architecture: ${arch}. PlanBefore is built for amd64 and arm64." ;; esac } # download URL FILE: HTTPS only, including every redirect. download() { curl --proto '=https' --tlsv1.2 -fsSL --retry 3 -o "$2" "$1" || fail "download failed: $1" } is_version() { printf '%s\n' "$1" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' } resolve_version() { if [ -n "${REPOFLOW_VERSION:-}" ]; then v=$REPOFLOW_VERSION case "$v" in v*) ;; *) v="v$v" ;; esac is_version "$v" || fail "REPOFLOW_VERSION must look like v1.2.3 (got '${REPOFLOW_VERSION}')" echo "$v" return fi # /releases/latest redirects to /releases/tag/ of the latest stable # (non pre-release) release. No API call, so no API rate limit. url=$(curl --proto '=https' --tlsv1.2 -fsSLI -o /dev/null -w '%{url_effective}' "${RELEASES_URL}/latest") || fail "could not reach ${RELEASES_URL}" v=${url##*/} is_version "$v" || fail "no stable PlanBefore release is published yet. See ${RELEASES_URL}" echo "$v" } sha256_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 -r "$1" | awk '{print $1}' else fail "no SHA-256 tool found (sha256sum, shasum or openssl); cannot verify the download" fi } path_hint() { dir=$1 case ":${PATH}:" in *":${dir}:"*) return ;; esac say "" say "${dir} is not on your PATH. This installer does not edit shell files;" say "add it yourself, then open a new terminal:" case "$(basename "${SHELL:-sh}")" in zsh) say " echo 'export PATH=\"${dir}:\$PATH\"' >> ~/.zshrc" ;; bash) if [ "$(uname -s)" = Darwin ]; then say " echo 'export PATH=\"${dir}:\$PATH\"' >> ~/.bash_profile" else say " echo 'export PATH=\"${dir}:\$PATH\"' >> ~/.bashrc" fi ;; fish) say " fish_add_path ${dir}" ;; *) say " export PATH=\"${dir}:\$PATH\" # in your shell's startup file" ;; esac } dependency_hint() { missing="" command -v git >/dev/null 2>&1 || missing="${missing} git" command -v rg >/dev/null 2>&1 || missing="${missing} ripgrep" [ -z "$missing" ] && return say "" warn "PlanBefore Node needs git and ripgrep (rg). Missing:${missing}" if [ "$1" = darwin ]; then say " brew install${missing}" else say " Debian/Ubuntu: sudo apt install${missing}" say " Fedora: sudo dnf install${missing}" say " Arch: sudo pacman -S${missing}" fi } # legacy_hint PATH: the previous CLI was called `repoflow`. pbnode reuses its # configuration and identity, so the old executable is only reported, never # removed or replaced by an alias. legacy_hint() { [ -n "$1" ] || return 0 say "" say "Found the previous PlanBefore Node command (legacy): $1" say "pbnode uses the same configuration, Node identity and projects, so no" say "'pbnode login' is needed. The legacy 'repoflow' executable was left in" say "place; once 'pbnode version' and 'pbnode status' look right, you can" say "remove it." } # historical_release CHECKSUMS: true when the release was published for the # previous `repoflow` CLI (its checksums.txt lists repoflow_* archives). historical_release() { awk '{ f = $2; sub(/^\*/, "", f); if (f ~ /^repoflow_/) found = 1 } END { exit(found ? 0 : 1) }' "$1" } # fail_historical VERSION PINNED: nothing was downloaded besides # checksums.txt and nothing is installed. fail_historical() { printf 'error: PlanBefore %s was published for the previous "repoflow" CLI and has no pbnode build.\n' "$1" >&2 if [ "$2" = true ]; then printf 'This installer installs pbnode only; it cannot install %s as pbnode.\n' "$1" >&2 printf 'Set REPOFLOW_VERSION to a PlanBefore Node release published after the rename, or unset it.\n' >&2 else printf 'It is still the latest stable release: no PlanBefore Node (pbnode) release is published yet.\n' >&2 printf 'Run this installer again once a pbnode release is out, or pin one with REPOFLOW_VERSION.\n' >&2 fi printf 'Releases: %s\nNothing was installed.\n' "$RELEASES_URL" >&2 exit 1 } main() { need curl need tar need uname need mktemp need awk need grep os=$(detect_os) arch=$(detect_arch "$os") version=$(resolve_version) pinned=false [ -n "${REPOFLOW_VERSION:-}" ] && pinned=true archive="pbnode_${version#v}_${os}_${arch}.tar.gz" install_dir=${REPOFLOW_INSTALL_DIR:-${HOME}/.local/bin} target="${install_dir}/pbnode" tmp=$(mktemp -d 2>/dev/null || mktemp -d -t pbnode) trap 'rm -rf "$tmp"' EXIT trap 'exit 130' INT TERM # checksums.txt first: it says which archives the release has, so a # release without a pbnode build is reported before any archive download. download "${RELEASES_URL}/download/${version}/checksums.txt" "${tmp}/checksums.txt" expected=$(awk -v f="$archive" '$2 == f || $2 == "*" f { print $1; exit }' "${tmp}/checksums.txt") if [ -z "$expected" ]; then historical_release "${tmp}/checksums.txt" && fail_historical "$version" "$pinned" fail "no build of PlanBefore ${version} for ${os}/${arch} in checksums.txt. See ${RELEASES_URL}" fi say "Installing PlanBefore ${version} for ${os}/${arch}" say "Downloading ${archive}" download "${RELEASES_URL}/download/${version}/${archive}" "${tmp}/${archive}" actual=$(sha256_of "${tmp}/${archive}") expected=$(printf '%s' "$expected" | tr 'A-F' 'a-f') actual=$(printf '%s' "$actual" | tr 'A-F' 'a-f') if [ "$expected" != "$actual" ]; then fail "SHA-256 mismatch for ${archive} (expected ${expected}, got ${actual}). Nothing was installed." fi say "SHA-256 verified: ${actual}" mkdir -p "${tmp}/x" # Extract only the one expected member: nothing else in the archive can # be written anywhere. tar -xzf "${tmp}/${archive}" -C "${tmp}/x" pbnode || fail "could not extract pbnode from ${archive}" [ -f "${tmp}/x/pbnode" ] || fail "${archive} does not contain the pbnode binary" mkdir -p "$install_dir" || fail "could not create ${install_dir} (set REPOFLOW_INSTALL_DIR to another directory)" replaced=false [ -e "$target" ] && replaced=true # Copy next to the target and rename: the replacement is atomic, and a # running Node keeps its old binary until it restarts. cp "${tmp}/x/pbnode" "${target}.tmp.$$" || fail "cannot write to ${install_dir} (set REPOFLOW_INSTALL_DIR to a directory you own)" chmod 755 "${target}.tmp.$$" mv -f "${target}.tmp.$$" "$target" say "Installed ${target}" say "" "$target" version || fail "the installed binary did not run: ${target}" path_hint "$install_dir" found=$(command -v pbnode 2>/dev/null || true) if [ -n "$found" ] && [ "$found" != "$target" ]; then warn "another pbnode comes first in your PATH: ${found}" fi dependency_hint "$os" legacy=$(command -v repoflow 2>/dev/null || true) if [ "$replaced" = true ] || [ -n "$legacy" ]; then say "" say "If a PlanBefore Node was already running, restart it to use this version:" say " pbnode stop && pbnode serve --background" fi legacy_hint "$legacy" say "" say "Next steps:" say " pbnode init" say " pbnode login" say " pbnode expose " say " pbnode serve --background" say " pbnode status" say "" say "Guide: ${DOCS_URL}" } main "$@"