#!/usr/bin/env sh set -eu base_url="${SPACES_INSTALL_BASE_URL:-https://preview.install.spaces.so}" version="${SPACES_INSTALL_VERSION:-}" install_dir="${SPACES_INSTALL_DIR:-${HOME}/.spaces/bin}" need() { if ! command -v "$1" >/dev/null 2>&1; then echo "spaces installer: missing required command: $1" >&2 exit 1 fi } fetch() { url="$1" dest="$2" if command -v curl >/dev/null 2>&1; then curl -fsSL "$url" -o "$dest" elif command -v wget >/dev/null 2>&1; then wget -q "$url" -O "$dest" else echo "spaces installer: curl or wget is required" >&2 exit 1 fi } os="$(uname -s | tr '[:upper:]' '[:lower:]')" case "$os" in darwin|linux) ;; *) echo "spaces installer: unsupported OS '$os'" >&2 exit 1 ;; esac arch="$(uname -m)" case "$arch" in x86_64|amd64) arch="amd64" ;; arm64|aarch64) arch="arm64" ;; *) echo "spaces installer: unsupported architecture '$arch'" >&2 exit 1 ;; esac need tar install_target="${install_dir}/spaces" note_existing_install() { existing="" if [ -f "$install_target" ] && [ -x "$install_target" ]; then existing="$install_target" elif command -v spaces >/dev/null 2>&1; then existing="$(command -v spaces)" fi if [ -z "$existing" ]; then return 0 fi if [ "${SPACES_INSTALL_SKIP_OVERWRITE:-}" = "1" ]; then echo "spaces installer: spaces is already installed at ${existing}; skipping (SPACES_INSTALL_SKIP_OVERWRITE=1)." exit 0 fi } path_marker="# Added by Spaces CLI installer" # append_path_line adds line to file unless the file already carries it. Its three outcomes let # the caller distinguish a successful write (0), an existing line (2), and a failed write (1). append_path_line() { file="$1" line="$2" if [ -z "$file" ] || [ -z "$line" ]; then return 1 fi # An exact whole line, not the marker and not a mention of the directory anywhere in the file. # A file that kept the marker comment after its export was deleted by hand, one whose export is # commented out, and one that merely names the directory in prose are all files the next shell # will not export from, and each of them used to read as "already done". Erring the other way is # cheap: PATH tolerates a repeated entry, while a skipped line costs the CLI its next terminal. if [ -f "$file" ] && grep -Fxq "$line" "$file" 2>/dev/null; then return 2 fi dir="$(dirname "$file")" if [ ! -d "$dir" ]; then mkdir -p "$dir" || return 1 fi { echo "" echo "$path_marker" echo "$line" } >>"$file" || return 1 return 0 } # record_path_file runs append_path_line and folds its outcome into path_state: "added" as soon # as anything is written, otherwise "present" if a file already carried the line. Returns 0 when # the line is in place either way, so the .profile fallback fires only when it is not. # The `|| status=$?` is load-bearing under `set -e`: a bare call returning 2 would end the script. record_path_file() { status=0 append_path_line "$1" "$2" || status=$? case "$status" in 0) path_state="added" return 0 ;; 2) if [ "$path_state" != "added" ]; then path_state="present" fi return 0 ;; *) return 1 ;; esac } # Whether the install directory will be on PATH in the NEXT shell is a question about startup # files, and only they can answer it. Reading $PATH here instead -- which this function used to # do, returning silently when the directory was already exported -- gets it wrong in exactly the # case that matters: a machine whose startup file lost the line while a shell that still exports # it stayed open. That shell runs the new binary happily and every new terminal reports # `command not found`. append_path_line is idempotent on its own marker, so nothing is needed here # to keep the line from being written twice. persist_install_dir_to_path() { if [ "${SPACES_INSTALL_SKIP_PATH:-}" = "1" ]; then echo "Add ${install_dir} to PATH to run spaces from any shell." return 0 fi home="${HOME:-}" if [ -z "$home" ]; then echo "Add ${install_dir} to PATH to run spaces from any shell." return 0 fi shell_name="" if [ -n "${SHELL:-}" ]; then shell_name="$(basename "$SHELL")" fi path_state="none" export_line="export PATH=\"${install_dir}:\$PATH\"" case "$shell_name" in zsh) record_path_file "${home}/.zshrc" "$export_line" || true ;; bash) if [ "$os" = "darwin" ]; then record_path_file "${home}/.bash_profile" "$export_line" || true else record_path_file "${home}/.bashrc" "$export_line" || true fi ;; fish) fish_line="fish_add_path -gm \"${install_dir}\"" if ! record_path_file "${home}/.config/fish/config.fish" "$fish_line"; then fish_line="set -gx PATH \"${install_dir}\" \$PATH" record_path_file "${home}/.config/fish/config.fish" "$fish_line" || true fi ;; esac if [ "$path_state" = "none" ]; then record_path_file "${home}/.profile" "$export_line" || true fi case "$path_state" in added|present) ;; *) echo "Add ${install_dir} to PATH to run spaces from any shell." ;; esac } note_existing_install tmp="${TMPDIR:-/tmp}/spaces-install.$$" mkdir -p "$tmp" trap 'rm -rf "$tmp"' EXIT INT TERM if [ -z "$version" ]; then fetch "${base_url}/channels/stable/version" "${tmp}/version" version="$(tr -d '[:space:]' < "${tmp}/version")" fi echo "Installing Spaces ${version} to ${install_target}" artifact="spaces_${os}_${arch}.tar.gz" artifact_url="${base_url}/releases/${version}/cli/${artifact}" checksums_url="${base_url}/releases/${version}/cli/checksums.txt" fetch "$artifact_url" "${tmp}/${artifact}" fetch "$checksums_url" "${tmp}/checksums.txt" expected="$(awk -v f="$artifact" '$2 == f { print $1 }' "${tmp}/checksums.txt")" if [ -z "$expected" ]; then echo "spaces installer: checksum for ${artifact} not found" >&2 exit 1 fi if command -v sha256sum >/dev/null 2>&1; then actual="$(sha256sum "${tmp}/${artifact}" | awk '{ print $1 }')" elif command -v shasum >/dev/null 2>&1; then actual="$(shasum -a 256 "${tmp}/${artifact}" | awk '{ print $1 }')" else echo "spaces installer: sha256sum or shasum is required" >&2 exit 1 fi if [ "$actual" != "$expected" ]; then echo "spaces installer: checksum mismatch for ${artifact}" >&2 exit 1 fi tar -xzf "${tmp}/${artifact}" -C "$tmp" binary="$(find "$tmp" -type f -name spaces -perm -u+x | head -n 1)" if [ -z "$binary" ]; then binary="$(find "$tmp" -type f -name spaces | head -n 1)" fi if [ -z "$binary" ]; then echo "spaces installer: archive did not contain a spaces binary" >&2 exit 1 fi mkdir -p "$install_dir" install -m 755 "$binary" "$install_target" persist_install_dir_to_path export PATH="${install_dir}:$PATH" if [ "${SPACES_INSTALL_SKIP_SETUP:-}" = "1" ]; then echo "Run ${install_target} setup when you are ready to configure spaces." elif [ -t 1 ] && { : < /dev/tty; } 2>/dev/null; then # The documented install runs this script as `curl -fsSL ... | sh`, which leaves # fd 0 on the pipe carrying the script rather than on the terminal. Read the # wizard's answers from the controlling terminal instead of stdin. "$install_target" setup < /dev/tty else echo "Run ${install_target} setup to configure spaces." fi