#!/bin/sh # SpecStory CLI installer. Usage: curl -fsSL https://specstory.com/install.sh | sh # Downloads official GitHub release artifacts and verifies their SHA-256 checksum. # Optional: SPECSTORY_INSTALL_DIR=/absolute/path, SPECSTORY_VERSION=2.11.0 main() ( set -eu umask 022 fail() { printf 'SpecStory: %s\n' "$*" >&2; exit 1; } fetch() { curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \ --tlsv1.2 --connect-timeout 15 --max-time 300 --retry 3 "$@" } for dependency in curl tar mktemp awk sed chmod mv mkdir cat rm uname; do command -v "$dependency" >/dev/null 2>&1 || fail "Install $dependency, then try again." done if command -v sha256sum >/dev/null 2>&1; then checksum() { sha256sum "$1"; } elif command -v shasum >/dev/null 2>&1; then checksum() { shasum -a 256 "$1"; } else fail 'SHA-256 verification requires sha256sum or shasum. Install one, then try again.' fi case "$(uname -s)" in Darwin) platform=Darwin ;; Linux) platform=Linux ;; *) fail 'Use macOS or Linux (including WSL), or download a Windows binary from https://github.com/specstoryai/getspecstory/releases/latest.' ;; esac case "$(uname -m)" in x86_64|amd64) architecture=x86_64 ;; arm64|aarch64) architecture=arm64 ;; *) fail 'Supported processors are x86_64 and arm64. See https://github.com/specstoryai/getspecstory/releases/latest.' ;; esac install_dir=${SPECSTORY_INSTALL_DIR:-${HOME:?HOME must be set}/.local/bin} case "$install_dir" in /*) ;; *) fail 'SPECSTORY_INSTALL_DIR must be an absolute path.' ;; esac # Do not replace links owned by package managers or follow a directory target. if [ -L "$install_dir/specstory" ] || [ -d "$install_dir/specstory" ]; then fail "Refusing to replace $install_dir/specstory: it is a symlink or directory. Use your package manager to update, or choose a different SPECSTORY_INSTALL_DIR." fi releases=https://github.com/specstoryai/getspecstory/releases if [ -n "${SPECSTORY_VERSION:-}" ]; then version=${SPECSTORY_VERSION#v} else printf 'Finding the latest SpecStory CLI release…\n' release_url=$(fetch --output /dev/null --write-out '%{url_effective}' "$releases/latest") || fail 'Could not find the latest release. Check your connection and try again.' case "$release_url" in "$releases/tag/v"*) version=${release_url#"$releases/tag/v"} ;; *) fail 'GitHub did not return a release tag. Try again shortly.' ;; esac fi # Only stable numeric versions become part of a download URL. [ "$(printf '%s' "$version" | sed -n '/^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$/p')" = "$version" ] && [ -n "$version" ] || fail 'Expected a stable version such as 2.11.0.' archive="SpecStoryCLI_${platform}_${architecture}.tar.gz" checksums="SpecStoryCLI_${version}_checksums.txt" download_dir=$(mktemp -d "${TMPDIR:-/tmp}/specstory-install.XXXXXXXX") || fail 'Could not create a temporary download directory.' staged_binary='' cleanup() { rm -rf "$download_dir" if [ -n "$staged_binary" ]; then rm -f "$staged_binary"; fi } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM trap 'exit 129' HUP printf '\nSpecStory CLI · %s\n\n 1/3 Downloading for %s %s…\n' "$version" "$platform" "$architecture" # Pin both downloads to the same release, even if a newer release appears now. fetch --output "$download_dir/$archive" "$releases/download/v$version/$archive" || fail 'Download failed. Your existing installation has not changed.' fetch --output "$download_dir/checksums.txt" "$releases/download/v$version/$checksums" || fail 'Could not download release checksums. Your existing installation has not changed.' printf ' 2/3 Verifying SHA-256 checksum…\n' expected=$(awk -v file="$archive" '$2 == file { print $1 }' "$download_dir/checksums.txt") [ "${#expected}" -eq 64 ] || fail 'The release must contain exactly one SHA-256 checksum for this archive.' case "$expected" in *[!0-9a-fA-F]*) fail 'The release checksum is invalid.' ;; esac actual=$(checksum "$download_dir/$archive") || fail 'Could not calculate the download checksum.' actual=${actual%% *} [ "$expected" = "$actual" ] || fail 'Checksum mismatch. Nothing was installed. Retry the download or report the release to SpecStory.' # Extract only the binary. Reject links rather than installing their targets. tar -xzf "$download_dir/$archive" -C "$download_dir" specstory || fail 'Could not extract the SpecStory binary.' [ -f "$download_dir/specstory" ] && [ ! -L "$download_dir/specstory" ] || fail 'The archive does not contain a regular SpecStory binary.' chmod 755 "$download_dir/specstory" || fail 'Could not make the downloaded binary executable.' binary_version=$("$download_dir/specstory" --no-usage-analytics --no-version-check --version) || fail 'The downloaded binary could not run. Your existing installation has not changed.' [ "$binary_version" = "$version (SpecStory)" ] || fail "The binary reports an unexpected version: $binary_version. Your existing installation has not changed." printf ' 3/3 Installing…\n' mkdir -p "$install_dir" || fail "Could not create $install_dir. Choose a writable SPECSTORY_INSTALL_DIR." staged_binary=$(mktemp "$install_dir/.specstory.XXXXXXXX") || fail "Cannot write to $install_dir. Choose a writable SPECSTORY_INSTALL_DIR." cat "$download_dir/specstory" > "$staged_binary" || fail 'Could not stage the binary. Your existing installation has not changed.' chmod 755 "$staged_binary" || fail 'Could not make the binary executable.' mv -f "$staged_binary" "$install_dir/specstory" || fail 'Could not install the binary.' staged_binary='' printf '\nInstalled and verified: %s\nLocation: %s/specstory\n' "$binary_version" "$install_dir" # Inspect PATH entries without invoking another install or modifying Homebrew. other_installs=$(printf '%s' "$PATH" | awk -v RS=: 'NF { print }' | while IFS= read -r directory; do candidate="$directory/specstory" if [ -x "$candidate" ] && [ ! -d "$candidate" ] && [ "$candidate" != "$install_dir/specstory" ]; then printf ' %s\n' "$candidate"; fi done | awk '!seen[$0]++') if [ -n "$other_installs" ]; then printf '\nOther installations found (left in place):\n%s\n' "$other_installs" fi active_binary=$(command -v specstory 2>/dev/null || true) # A child sh sees newly installed commands immediately; the user's parent # Zsh/Bash may still cache an older Homebrew path. Always show a refresh step. printf '\nActivate it in your current terminal:\n' quoted_dir=$(printf '%s' "$install_dir" | sed "s/'/'\\\\''/g") case "${SHELL:-}" in */fish) printf ' fish_add_path --move '\''%s'\''\n' "$quoted_dir" ;; *) if [ "$active_binary" != "$install_dir/specstory" ]; then printf ' export PATH='\''%s'\'':"$PATH"\n' "$quoted_dir"; fi case "${SHELL:-}" in */zsh) printf ' rehash\n' ;; *) printf ' hash -r\n' ;; esac ;; esac printf ' specstory version\n\nYou should see %s (SpecStory). Refreshing the command cache prevents an older Homebrew or manual install from being used.\n' "$version" if [ "$active_binary" != "$install_dir/specstory" ]; then case "${SHELL:-}" in */fish) ;; *) printf '\nKeep the printed export PATH line in your shell profile for new terminals.\n' ;; esac fi printf '\nThen check your coding agents:\n specstory check\n\nFrom your project folder, start a session:\n specstory run claude\n # Or: specstory run codex\n\nSetup help: https://docs.specstory.com/integrations/terminal-coding-agents\n' ) # Keep this call last so a truncated download cannot run a partial installer. main "$@"