#!/bin/sh # specsnip installer # # curl -fsSL https://get.specsnip.ru | sh # (the same script is also served at https://specsnip.ru/install) # # Puts a single file, `specsnip`, into ~/.local/bin after checking its SHA-256 against # https://specsnip.ru/SHA256SUMS. Needs Node.js 20+ and curl (or wget). No sudo, nothing else is touched. # # Options (environment variables): # SPECSNIP_VERSION install a specific version, e.g. 1.0.0 (default: the latest) # SPECSNIP_INSTALL_DIR where to put the file (default: $HOME/.local/bin) # SPECSNIP_BASE_URL where to download from (default: https://specsnip.ru) # # Uninstall: rm "$HOME/.local/bin/specsnip" # # Everything below runs inside main(), which is called on the very last line: if the download is # cut short, the shell sees an unfinished function and executes nothing. main() { set -eu base="${SPECSNIP_BASE_URL:-https://specsnip.ru}" version="${SPECSNIP_VERSION:-latest}" dir="${SPECSNIP_INSTALL_DIR:-${HOME:?HOME is not set}/.local/bin}" fail() { printf 'specsnip install: %s\n' "$*" >&2 exit 1 } # --- requirements ----------------------------------------------------------------------------- command -v node >/dev/null 2>&1 || fail 'Node.js 20 or newer is required: https://nodejs.org' node_major="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)" [ "$node_major" -ge 20 ] 2>/dev/null || fail "Node.js 20 or newer is required, found $(node -v 2>/dev/null || echo 'none')" fetch() { if command -v curl >/dev/null 2>&1; then curl -fsSL "$1" elif command -v wget >/dev/null 2>&1; then wget -qO- "$1" else fail 'curl or wget is required' fi } sha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum | cut -d' ' -f1 elif command -v shasum >/dev/null 2>&1; then shasum -a 256 | cut -d' ' -f1 else fail 'sha256sum or shasum is required to verify the download' fi } # --- which file ------------------------------------------------------------------------------- if [ "$version" = latest ]; then file='specsnip.mjs' else case "$version" in *[!0-9A-Za-z.+-]* | '') fail "invalid SPECSNIP_VERSION: $version" ;; esac file="specsnip-$version.mjs" fi tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT # --- download and verify ---------------------------------------------------------------------- printf 'Downloading %s/%s\n' "$base" "$file" fetch "$base/$file" >"$tmp/specsnip" || fail "cannot download $base/$file" fetch "$base/SHA256SUMS" >"$tmp/sums" || fail "cannot download $base/SHA256SUMS" want="$(awk -v f="$file" '$2 == f { print $1 }' "$tmp/sums")" [ -n "$want" ] || fail "$file is not listed in $base/SHA256SUMS (unknown version?)" got="$(sha256 <"$tmp/specsnip")" [ "$got" = "$want" ] || fail "checksum mismatch for $file: expected $want, got $got. Nothing was installed" printf 'Checksum OK (%s)\n' "$(printf '%s' "$want" | cut -c1-12)" # --- install ---------------------------------------------------------------------------------- mkdir -p "$dir" 2>/dev/null || fail "cannot create $dir (set SPECSNIP_INSTALL_DIR to a writable folder)" [ -w "$dir" ] || fail "$dir is not writable (set SPECSNIP_INSTALL_DIR to a writable folder)" # Copy next to the target and rename: an upgrade never leaves a half-written file behind. cp "$tmp/specsnip" "$dir/.specsnip.$$" chmod 755 "$dir/.specsnip.$$" mv -f "$dir/.specsnip.$$" "$dir/specsnip" installed="$("$dir/specsnip" --version 2>/dev/null)" || fail "the installed file does not run: $dir/specsnip --version" printf 'Installed specsnip %s to %s/specsnip\n' "$installed" "$dir" case ":$PATH:" in *":$dir:"*) ;; *) # в подсказке пишем $HOME, а не длинный абсолютный путь: её копируют в ~/.zshrc как есть shown="$dir" case "$dir" in "$HOME"/*) shown="\$HOME${dir#"$HOME"}" ;; esac printf '\n%s is not in your PATH. Add it, for example:\n\n export PATH="%s:$PATH"\n\n' "$dir" "$shown" printf 'put that line into ~/.zshrc or ~/.bashrc to make it permanent.\n' ;; esac printf '\nTry: specsnip --help\n' } main "$@"