#!/bin/sh # Kurs CLI installer. Re-run to update; settings are never removed. set -eu fail() { printf 'Kurs: %s\n' "$*" >&2; exit 1; } base=${KURS_DOWNLOAD_BASE:-https://kurside-releases.storage.yandexcloud.net/cli} root=${KURS_INSTALL_ROOT:-$HOME/.kurside/cli/runtime} bin=${KURS_BIN_DIR:-$HOME/.local/bin} case "$base" in https://*) ;; http://127.0.0.1:*|http://localhost:*) ;; *) fail 'Download URL must use HTTPS';; esac case "$root:$bin" in *' '*) fail 'Invalid installation path';; esac command -v curl >/dev/null || fail 'curl is required' command -v tar >/dev/null || fail 'tar is required' case "$(uname -s)" in Darwin) os=darwin;; Linux) os=linux;; *) fail 'Supported systems: macOS and Linux. Use install.ps1 on Windows.';; esac case "$(uname -m)" in arm64|aarch64) arch=arm64;; x86_64|amd64) arch=x64-baseline;; *) fail 'Unsupported CPU architecture';; esac if [ "$os" = linux ] && ldd --version 2>&1 | grep -qi musl; then fail 'This release requires glibc (for example Ubuntu).'; fi fetch() { curl --fail --location --silent --show-error --connect-timeout 15 --max-time 600 --retry 2 "$1" -o "$2"; } tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT HUP INT TERM version=${KURS_VERSION:-} if [ -z "$version" ]; then fetch "$base/latest.txt" "$tmp/version"; version=$(cat "$tmp/version"); fi printf '%s\n' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$' || fail 'Invalid release version' name=kurs-$version-$os-$arch.tar.gz printf 'Installing Kurs CLI %s (%s %s)...\n' "$version" "$os" "$arch" printf '[1/4] Downloading archive (progress below)...\n' curl --fail --location --progress-bar --show-error --connect-timeout 15 --max-time 600 --retry 2 "$base/releases/$version/$name" -o "$tmp/$name" printf '[2/4] Verifying SHA-256...\n' fetch "$base/releases/$version/$name.sha256" "$tmp/checksum" expected=$(awk 'NR==1 {print $1}' "$tmp/checksum") [ "${#expected}" -eq 64 ] || fail 'Invalid checksum' case "$expected" in *[!0-9a-f]*) fail 'Invalid checksum';; esac if command -v sha256sum >/dev/null; then actual=$(sha256sum "$tmp/$name" | awk '{print $1}'); elif command -v shasum >/dev/null; then actual=$(shasum -a 256 "$tmp/$name" | awk '{print $1}'); else fail 'sha256sum or shasum is required'; fi [ "$actual" = "$expected" ] || fail 'Checksum mismatch; installation cancelled' printf '[3/4] Extracting archive...\n' # Reject absolute and traversing members before extracting. tar -tzf "$tmp/$name" > "$tmp/members" if grep -Eq '(^/|(^|/)\.\.(/|$))' "$tmp/members"; then fail 'Unsafe archive paths'; fi mkdir "$tmp/unpacked" tar -xzf "$tmp/$name" -C "$tmp/unpacked" [ -f "$tmp/unpacked/kurs" ] || fail 'Archive has no kurs executable' printf '[4/4] Checking executable and installing...\n' chmod +x "$tmp/unpacked/kurs" "$tmp/unpacked/kurs" --version mkdir -p "$root" "$bin" # Refuse to overwrite an unrelated command. Older Kurs development launchers are recognized. if [ -e "$bin/kurs" ] && ! { [ -L "$bin/kurs" ] && [ -f "$bin/.kurs-managed" ] && [ "$(cat "$bin/.kurs-managed")" = "$root" ]; } && ! grep -Eq 'Kurs CLI launcher|/kurs-cli/dist/kurs-' "$bin/kurs"; then fail "An unmanaged command already exists at $bin/kurs; set KURS_BIN_DIR to another directory." fi # Unique directories keep a running version intact during an update. target=$(mktemp -d "$root/$version-$os-$arch.XXXXXX") cp -R "$tmp/unpacked/." "$target/" json_string() { printf '%s' "$1" | awk 'BEGIN {printf "\""} {gsub(/\\/, "\\\\"); gsub(/"/, "\\\""); printf "%s", $0} END {printf "\""}'; } printf '{"root":%s,"bin":%s}\n' "$(json_string "$root")" "$(json_string "$bin")" > "$target/.kurs-install.json" # A symlink keeps spaces and quotes in installation paths safe. ln -s "$target/kurs" "$bin/.kurs-link-$$" # Managed symlinks are accepted on subsequent runs using the adjacent ownership marker. mv -f "$bin/.kurs-link-$$" "$bin/kurs" printf '%s\n' "$root" > "$bin/.kurs-managed" printf '\nInstalled: %s/kurs\n' "$bin" case ":$PATH:" in *":$bin:"*) ;; *) printf 'Add this directory to PATH, then open a new terminal: %s\n' "$bin" # Persist only the standard directory, without embedding arbitrary shell text. if [ "$bin" = "$HOME/.local/bin" ] && [ "${KURS_NO_MODIFY_PATH:-0}" != 1 ]; then case "${SHELL:-}" in */zsh) profile=$HOME/.zshrc;; */bash) profile=$HOME/.bashrc;; *) profile=$HOME/.profile;; esac line='export PATH="$HOME/.local/bin:$PATH"' if ! grep -Fqx "$line" "$profile" 2>/dev/null; then printf '\n# Kurs CLI\n%s\n' "$line" >> "$profile"; fi fi;; esac printf 'Next: kurs login, then run kurs in your project. Re-run this installer to update.\n'