#!/bin/sh set -eu BASE_URL="https://cli.sfcc.xyz" BINARY_NAME="sf" # Colorize only when stderr (where info() writes) is a terminal, so escape # codes don't leak into piped logs. if [ -t 2 ]; then GREEN="$(printf '\033[32m')" BOLD="$(printf '\033[1m')" RESET="$(printf '\033[0m')" else GREEN="" BOLD="" RESET="" fi info() { printf '%s\n' "$@" >&2 } err() { printf 'error: %s\n' "$1" >&2 exit 1 } need_cmd() { if ! command -v "$1" > /dev/null 2>&1; then err "need '$1' (command not found)" fi } check_cmd() { command -v "$1" > /dev/null 2>&1 } cleanup() { if [ -n "${_TMPDIR:-}" ] && [ -d "$_TMPDIR" ]; then rm -rf "$_TMPDIR" fi } # Download a URL to stdout or to a file. # Usage: download [output_file] download() { if check_cmd curl; then if [ $# -eq 2 ]; then curl -fsSL -o "$2" "$1" else curl -fsSL "$1" fi elif check_cmd wget; then if [ $# -eq 2 ]; then wget -qO "$2" "$1" else wget -qO- "$1" fi else err "need 'curl' or 'wget' (neither found)" fi } detect_target() { local os arch os="$(uname -s)" arch="$(uname -m)" case "$os" in Linux) os="unknown-linux-musl" ;; Darwin) os="apple-darwin" ;; *) err "unsupported OS: $os" ;; esac case "$arch" in x86_64 | amd64) arch="x86_64" ;; aarch64 | arm64) arch="aarch64" ;; *) err "unsupported architecture: $arch" ;; esac TARGET="${arch}-${os}" } resolve_version() { if [ -n "${SF_VERSION:-}" ]; then # Strip leading 'v' if present VERSION="${SF_VERSION#v}" else info "Resolving latest version..." VERSION="$(download "$BASE_URL/latest.txt")" VERSION="$(printf '%s' "$VERSION" | tr -d '[:space:]')" fi if [ -z "$VERSION" ]; then err "could not resolve version" fi } resolve_install_dir() { if [ -n "${SF_INSTALL_DIR:-}" ]; then INSTALL_DIR="$SF_INSTALL_DIR" elif [ "$(id -u)" -eq 0 ]; then INSTALL_DIR="/usr/local/bin" else INSTALL_DIR="$HOME/.local/bin" fi # Man pages go to the share/man/man1 directory that's a sibling of bin/, # e.g. $HOME/.local/bin -> $HOME/.local/share/man/man1. MAN_DIR="$(dirname "$INSTALL_DIR")/share/man/man1" } verify_checksum() { local checksum_file="$1" case "$(uname -s)" in Linux) sha256sum -c "$checksum_file" > /dev/null 2>&1 ;; Darwin) shasum -a 256 -c "$checksum_file" > /dev/null 2>&1 ;; *) err "unsupported OS for checksum verification" ;; esac } check_existing_sf() { local existing existing="$(command -v sf 2>/dev/null)" || return 0 # If the existing binary is already the Rust CLI, no action needed (just upgrading). # The Rust CLI supports `sf version`; the old TypeScript CLI does not. if "$existing" version > /dev/null 2>&1; then return 0 fi local existing_dir existing_dir="$(dirname "$existing")" info "" info "Found an existing sf binary at $existing" if mv "$existing" "${existing_dir}/sf-old" 2>/dev/null; then info "Moved to ${existing_dir}/sf-old (you can still use the old CLI as sf-old)" else info "warning: could not move $existing (permission denied); it will be shadowed by $INSTALL_DIR/$BINARY_NAME" fi } main() { detect_target resolve_version resolve_install_dir local archive="sf-v${VERSION}-${TARGET}.tar.gz" local archive_url="${BASE_URL}/v${VERSION}/${archive}" local checksum_url="${archive_url}.sha256" info "Installing sf v${VERSION} (${TARGET})" trap cleanup EXIT _TMPDIR="$(mktemp -d)" info "Downloading ${archive}..." download "$archive_url" "$_TMPDIR/$archive" download "$checksum_url" "$_TMPDIR/${archive}.sha256" info "Verifying checksum..." (cd "$_TMPDIR" && verify_checksum "${archive}.sha256") || err "checksum verification failed" info "Extracting..." tar xzf "$_TMPDIR/$archive" -C "$_TMPDIR" chmod +x "$_TMPDIR/$BINARY_NAME" check_existing_sf mkdir -p "$INSTALL_DIR" mv "$_TMPDIR/$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME" # Install bundled man pages, if present in the archive. # The glob check guards against share/man/man1/ existing but being empty, # in which case a literal "*.1" would be passed to cp and fail misleadingly. if [ -d "$_TMPDIR/share/man/man1" ] && \ ls "$_TMPDIR"/share/man/man1/*.1 > /dev/null 2>&1; then # Sweep previously-installed sf man pages so renamed/removed subcommands # don't leave stale .1 files behind. clap_mangen emits sf.1 and sf-*.1. if mkdir -p "$MAN_DIR" 2>/dev/null \ && rm -f "$MAN_DIR"/sf.1 "$MAN_DIR"/sf-*.1 2>/dev/null \ && cp "$_TMPDIR"/share/man/man1/*.1 "$MAN_DIR/" 2>/dev/null; then info "Installed man pages to $MAN_DIR" else info "warning: could not install man pages to $MAN_DIR" fi fi # PATH setup: auto-add to shell config if not already in PATH. # source_file is set when re-sourcing a config would make sf usable in the # current shell; it's surfaced as the first "Next steps" action below. local source_file="" case ":$PATH:" in *":$INSTALL_DIR:"*) ;; *) local shell_config="" local export_line="export PATH=\"$INSTALL_DIR:\$PATH\"" # Detect the user's shell config file case "${SHELL:-}" in */zsh) shell_config="$HOME/.zshrc" ;; */bash) if [ -f "$HOME/.bash_profile" ]; then shell_config="$HOME/.bash_profile" else shell_config="$HOME/.bashrc" fi ;; */fish) shell_config="$HOME/.config/fish/config.fish" export_line="fish_add_path $INSTALL_DIR" ;; esac if [ -n "$shell_config" ]; then # Add the export line unless it's already there. Either way the running # shell hasn't loaded it, so re-sourcing is the next step. if [ -f "$shell_config" ] && grep -qF "$INSTALL_DIR" "$shell_config" 2>/dev/null; then info "PATH already configured in $shell_config" else mkdir -p "$(dirname "$shell_config")" printf '\n# Added by sf installer\n%s\n' "$export_line" >> "$shell_config" info "Added sf to PATH in $shell_config" fi source_file="$shell_config" # Also export for the current session so the smoke test below works. export PATH="$INSTALL_DIR:$PATH" else info "Add sf to your PATH: $export_line" fi ;; esac # Smoke-test the binary; stderr passes through so failures are diagnosable "$INSTALL_DIR/$BINARY_NAME" version > /dev/null || err "sf was installed to $INSTALL_DIR/$BINARY_NAME but failed to run" info "${GREEN}${BOLD}✓${RESET} sf v${VERSION} installed to $INSTALL_DIR/$BINARY_NAME" info "" info "${BOLD}Next steps:${RESET}" if [ -n "$source_file" ]; then info " source $source_file # pick up PATH changes" fi info " sf login Authenticate with your SF Compute account" info " sf --help See all commands" } main