#!/bin/sh # shellcheck disable=SC3043 # 'local' is supported by dash, ash, and busybox sh set -eu # --- Logging --- info() { printf 'creft: %s\n' "$*" >&2 } warn() { printf 'creft: warning: %s\n' "$*" >&2 } error() { printf 'creft: error: %s\n' "$*" >&2 exit 1 } debug() { if [ "${CREFT_DEBUG:-}" = "1" ]; then printf 'creft: debug: %s\n' "$*" >&2 fi } # --- Platform detection --- get_os() { local os os="$(uname -s)" case "$os" in Darwin) printf 'darwin' ;; Linux) printf 'linux' ;; *) error "unsupported OS: $os" ;; esac } get_arch() { local arch arch="$(uname -m)" case "$arch" in x86_64) printf 'x86_64' ;; aarch64 | arm64) printf 'aarch64' ;; *) error "unsupported architecture: $arch" ;; esac } get_target() { local os arch os="$(get_os)" arch="$(get_arch)" case "${os}-${arch}" in darwin-aarch64) printf 'aarch64-apple-darwin' ;; darwin-x86_64) printf 'x86_64-apple-darwin' ;; linux-x86_64) printf 'x86_64-unknown-linux-gnu' ;; linux-aarch64) printf 'aarch64-unknown-linux-gnu' ;; *) error "unsupported platform: ${os}-${arch}" ;; esac } # --- Version resolution --- get_latest_version() { local api_url response version api_url="https://api.github.com/repos/chrisfentiman/creft/releases/latest" debug "querying $api_url" response="$(download_to_stdout "$api_url")" || error "failed to determine latest version" version="$(printf '%s' "$response" | grep '"tag_name"' | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" if [ -z "$version" ]; then error "failed to determine latest version" fi printf '%s' "$version" } # --- Download helpers --- check_downloader() { if command -v curl >/dev/null 2>&1; then printf 'curl' elif command -v wget >/dev/null 2>&1; then printf 'wget' else error "curl or wget is required but neither is installed" fi } github_auth_header() { local token token="${GITHUB_TOKEN:-${GH_TOKEN:-}}" if [ -n "$token" ]; then printf 'Authorization: token %s' "$token" fi } download_to_stdout() { local url downloader auth_header url="$1" downloader="$(check_downloader)" auth_header="$(github_auth_header)" case "$downloader" in curl) if [ -n "$auth_header" ]; then curl --proto '=https' --tlsv1.2 -fsSL -H "$auth_header" "$url" else curl --proto '=https' --tlsv1.2 -fsSL "$url" fi ;; wget) if [ -n "$auth_header" ]; then wget -qO- --header "$auth_header" "$url" else wget -qO- "$url" fi ;; esac } download_to_file() { local url dest downloader auth_header url="$1" dest="$2" downloader="$(check_downloader)" auth_header="$(github_auth_header)" case "$downloader" in curl) if [ -n "$auth_header" ]; then curl --proto '=https' --tlsv1.2 -fSL -H "$auth_header" -o "$dest" "$url" else curl --proto '=https' --tlsv1.2 -fSL -o "$dest" "$url" fi ;; wget) if [ -n "$auth_header" ]; then wget -q -O "$dest" --header "$auth_header" "$url" else wget -q -O "$dest" "$url" fi ;; esac } # --- Checksum --- shasum_cmd() { if command -v shasum >/dev/null 2>&1; then printf 'shasum -a 256' elif command -v sha256sum >/dev/null 2>&1; then printf 'sha256sum' else error "shasum or sha256sum is required but neither is installed" fi } verify_checksum() { local tarball checksum_url checksum_file expected actual cmd tarball="$1" checksum_url="$2" checksum_file="${tarball}.sha256" debug "downloading checksum from $checksum_url" download_to_file "$checksum_url" "$checksum_file" || error "failed to download checksum" # The .sha256 file contains " " — extract just the hash expected="$(awk '{print $1}' "$checksum_file")" if [ -z "$expected" ]; then error "checksum verification failed" fi cmd="$(shasum_cmd)" # Compute hash of the tarball actual="$($cmd "$tarball" | awk '{print $1}')" if [ "$expected" != "$actual" ]; then error "checksum verification failed" fi debug "checksum verified: $actual" } # --- Install --- install_creft() { local version tag target tarball_name tarball_url checksum_url install_dir is_upgrade # Detect an existing installation before the new binary lands. # ~/.creft/ is created by `creft up`; its presence means creft was previously set up. if [ -d "${HOME}/.creft" ] || command -v creft >/dev/null 2>&1; then is_upgrade=1 else is_upgrade=0 fi # Resolve version if [ -n "${CREFT_VERSION:-}" ]; then version="$CREFT_VERSION" info "using pinned version $version" else info "resolving latest version..." version="$(get_latest_version)" info "latest version is $version" fi # Normalize to a release tag — accept either "0.2.8" or "creft-v0.2.8" case "$version" in creft-v*) tag="$version" ;; v*) tag="creft-${version}" ;; *) tag="creft-v${version}" ;; esac target="$(get_target)" debug "target triple: $target" tarball_name="creft-${target}.tar.gz" tarball_url="https://github.com/chrisfentiman/creft/releases/download/${tag}/${tarball_name}" checksum_url="${tarball_url}.sha256" install_dir="${CREFT_INSTALL_DIR:-$HOME/.local/bin}" # Set up temp dir — declared at script scope so the EXIT trap can reference it _creft_tmp_dir="$(mktemp -d)" trap 'rm -rf "${_creft_tmp_dir:-}"' EXIT local tarball tarball="${_creft_tmp_dir}/${tarball_name}" info "downloading creft ${version} for ${target}..." download_to_file "$tarball_url" "$tarball" || error "failed to download creft ${version} for ${target}" info "verifying checksum..." verify_checksum "$tarball" "$checksum_url" info "extracting..." tar -xzf "$tarball" -C "$_creft_tmp_dir" || error "failed to extract archive" mkdir -p "$install_dir" cp "${_creft_tmp_dir}/creft" "${install_dir}/.creft.new.$$" chmod +x "${install_dir}/.creft.new.$$" mv -f "${install_dir}/.creft.new.$$" "${install_dir}/creft" info "creft ${version} installed to ${install_dir}/creft" check_path "$install_dir" if [ "$is_upgrade" = "1" ]; then # Upgrade: rebootstrap hooks and skills so they match the new binary version. info "bootstrapping hooks and skills..." "${install_dir}/creft" up || warn "creft up failed — run 'creft up' manually to finish setup" else # Fresh install: show welcome and let the user run 'creft up' when ready. "${install_dir}/creft" _creft welcome || true fi } # --- PATH check --- check_path() { local install_dir install_dir="$1" # Check if install_dir appears in PATH case ":${PATH}:" in *":${install_dir}:"*) ;; *) warn "${install_dir} is not in your PATH. Add it with:" warn " export PATH=\"${install_dir}:\$PATH\"" ;; esac } # --- Entry point --- install_creft