mirror of
https://github.com/hanzoai/cli.git
synced 2026-10-30 15:00:27 +00:00
Choosing a release listed releases through api.github.com, one call per 100 releases per tool: 8 of the 60 anonymous calls an address gets an hour for one `curl hanzo.sh | sh`. Behind a shared NAT or on a CI runner the limit was gone before the installer ran, and it stopped at "could not list releases". Tags now come from git's ref advertisement on github.com, which has no such limit, and the highest stable tag whose archive and checksum download is the one installed. A draft's assets 404 until it is published, so a draft is never chosen. A token still reads a private repo: Basic on the refs, the API for the assets. Against live GitHub with api.github.com refused, every target of cli, mcp and dev picks what the API picked, in 1 to 8 seconds, with no API call. Co-authored-by: Hanzo Dev <[email protected]>
239 lines
10 KiB
Bash
Executable File
239 lines
10 KiB
Bash
Executable File
#!/bin/sh
|
|
# Install a Hanzo native binary.
|
|
#
|
|
# curl -fsSL https://raw.githubusercontent.com/hanzoai/cli/main/install.sh | sh
|
|
#
|
|
# Downloads the prebuilt release asset for THIS machine, verifies its sha256, and
|
|
# puts it on PATH. Nothing is built, and no package manager is involved. It
|
|
# refuses loudly on a platform we do not publish, because a script that
|
|
# half-works is worse than one that says why it cannot.
|
|
#
|
|
# This is the ONE implementation of "fetch a Hanzo binary". It defaults to the
|
|
# `hanzo` CLI, and hanzo.sh drives it once per tool by overriding three
|
|
# variables rather than carrying a second copy of platform detection, asset
|
|
# naming and checksum verification:
|
|
#
|
|
# HANZO_INSTALL_REPO owning repo (default hanzoai/cli)
|
|
# HANZO_INSTALL_BIN binary + asset prefix (default hanzo)
|
|
# HANZO_INSTALL_ALIAS second name, same build (default hanzo-node; "" = none)
|
|
# HANZO_INSTALL_PREFIX install dir (default ~/.local/bin)
|
|
# HANZO_VERSION pin a tag (default: highest stable semver)
|
|
#
|
|
# The convention every published Hanzo binary follows, and the only thing this
|
|
# needs to know: the asset is <BIN>-<os>-<arch>.tar.gz, it is accompanied by
|
|
# <asset>.sha256, and it unpacks to a single file named <BIN>.
|
|
set -eu
|
|
|
|
REPO="${HANZO_INSTALL_REPO:-hanzoai/cli}"
|
|
BIN="${HANZO_INSTALL_BIN:-hanzo}"
|
|
# The second name this same build installs under: what cloud's control binary
|
|
# delegates to. One build, two names — never two versions.
|
|
DELEGATE="${HANZO_INSTALL_ALIAS-hanzo-node}"
|
|
PREFIX="${HANZO_INSTALL_PREFIX:-$HOME/.local/bin}"
|
|
|
|
die() { printf '\n%s: %s\n' "$BIN" "$1" >&2; exit 1; }
|
|
need() { command -v "$1" >/dev/null 2>&1 || die "need $1 on PATH"; }
|
|
|
|
need curl
|
|
need tar
|
|
need awk
|
|
|
|
# A private repo answers 404 to an anonymous asset fetch, so carry a token when
|
|
# one is available. Public installs need none.
|
|
TOKEN="${HANZO_INSTALL_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}"
|
|
if [ -z "$TOKEN" ] && command -v gh >/dev/null 2>&1; then
|
|
TOKEN="$(gh auth token 2>/dev/null || true)"
|
|
fi
|
|
# Never interpolate the token into an argument list: unquoted substitution
|
|
# word-splits it into its own argv entry, and curl echoes argv on failure — which
|
|
# prints the token. Branch, and keep the header a single quoted argument.
|
|
# Whether to draw curl's own progress bar, decided once.
|
|
#
|
|
# A binary is tens of megabytes and `-s` draws nothing while it comes down, so
|
|
# a slow connection is indistinguishable from a hang — which is how somebody
|
|
# ends up pressing ctrl-c halfway through an install that was working.
|
|
#
|
|
# The bar goes to stderr. `curl hanzo.sh | sh` consumes stdout and leaves
|
|
# stderr on the terminal, so it is visible in exactly the case this is for.
|
|
# Where there is no terminal — CI, a log file, a pipe to tee — a bar is a few
|
|
# thousand carriage returns in the transcript, so it stays silent there and
|
|
# `-S` keeps errors reported either way.
|
|
if [ -t 2 ]; then DL='-#'; else DL='-sS'; fi
|
|
|
|
get() { # get <url> <dest>
|
|
if [ -n "$TOKEN" ]; then
|
|
curl -fL $DL -H "Authorization: Bearer $TOKEN" "$1" -o "$2"
|
|
else
|
|
curl -fL $DL "$1" -o "$2"
|
|
fi
|
|
}
|
|
get_stdout() { # get_stdout <url>
|
|
if [ -n "$TOKEN" ]; then
|
|
curl -fsSL -H "Authorization: Bearer $TOKEN" "$1"
|
|
else
|
|
curl -fsSL "$1"
|
|
fi
|
|
}
|
|
|
|
# Published targets: linux-{amd64,arm64}, darwin-{amd64,arm64}, windows-amd64.
|
|
os="$(uname -s)"
|
|
arch="$(uname -m)"
|
|
ext=""
|
|
case "$os" in
|
|
Linux) os=linux ;;
|
|
Darwin) os=darwin ;;
|
|
MINGW*|MSYS*|CYGWIN*|Windows_NT) os=windows; ext=".exe" ;; # git-bash / msys2
|
|
*) die "unsupported OS '$os'." ;;
|
|
esac
|
|
case "$arch" in
|
|
x86_64|amd64) arch=amd64 ;;
|
|
aarch64|arm64) arch=arm64 ;;
|
|
*) die "unsupported architecture '$arch'." ;;
|
|
esac
|
|
target="${os}-${arch}"
|
|
|
|
TAG="${HANZO_VERSION:-}"
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
|
|
asset="${BIN}-${target}.tar.gz"
|
|
|
|
# The tags come from git's ref advertisement, not the REST API. github.com serves
|
|
# it without the API's limit of 60 anonymous calls an hour per address, and
|
|
# listing releases through the API cost one call per 100 releases per tool: 8 of
|
|
# the 60 for one `curl hanzo.sh | sh`. Behind a shared NAT or on a CI runner the
|
|
# limit was spent before the installer ran, and nothing installed.
|
|
#
|
|
# /latest is not used either: it follows release chronology, which can select an
|
|
# older maintenance release over a higher version.
|
|
tags() {
|
|
url="https://github.com/$REPO.git/info/refs?service=git-upload-pack"
|
|
if [ -n "$TOKEN" ]; then
|
|
curl -fsS -H "Authorization: Basic $(printf 'x-access-token:%s' "$TOKEN" | base64 | tr -d '\n')" "$url"
|
|
else
|
|
curl -fsS "$url"
|
|
fi
|
|
}
|
|
|
|
# has <tag>: does the release at <tag> carry THIS machine's archive and its
|
|
# checksum? A release is not one object per product: MCP also publishes
|
|
# independent JavaScript versions in the same repository, and a release whose
|
|
# build for one target failed still went out with the others (hanzoai/cli
|
|
# v8.5.158 has no linux-amd64, hanzoai/mcp rust-v1.1.23 no darwin). Choosing it
|
|
# would end the install on a 404 when the release before it has this machine's
|
|
# build. A draft's assets answer 404 until it is published, so a draft is never
|
|
# chosen either.
|
|
has() {
|
|
if [ -z "$TOKEN" ]; then
|
|
curl -fs -I -o /dev/null "https://github.com/$REPO/releases/download/$1/$asset.sha256" &&
|
|
curl -fs -I -o /dev/null "https://github.com/$REPO/releases/download/$1/$asset"
|
|
return
|
|
fi
|
|
# A private release's download URL answers 404 even with a token; the API
|
|
# names its assets.
|
|
get_stdout "https://api.github.com/repos/$REPO/releases/tags/$1" > "$tmp/release.json" 2>/dev/null &&
|
|
grep -qF "\"$asset\"" "$tmp/release.json" &&
|
|
grep -qF "\"$asset.sha256\"" "$tmp/release.json"
|
|
}
|
|
|
|
if [ -z "$TAG" ]; then
|
|
tags > "$tmp/refs" || die "could not list the tags of $REPO"
|
|
# One ref per pkt-line, "<len><sha> refs/tags/<tag>", with NULs around the
|
|
# capability list; a peeled "<tag>^{}" line fails the pattern.
|
|
tr '\000' '\n' < "$tmp/refs" | awk '
|
|
$2 ~ /^refs\/tags\/(rust-)?v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/ {
|
|
tag = substr($2, 11); version = tag; sub(/^(rust-)?v/, "", version)
|
|
print version "\t" tag
|
|
}' | sort -t . -k1,1nr -k2,2nr -k3,3nr | awk '{ print $2 }' > "$tmp/tags"
|
|
while read -r candidate; do
|
|
if has "$candidate"; then TAG="$candidate"; break; fi
|
|
done < "$tmp/tags"
|
|
[ -n "$TAG" ] || die "no stable release of $REPO has a build for $target.
|
|
If $REPO is private, set GH_TOKEN (or run \`gh auth login\`); or pin HANZO_VERSION=vX.Y.Z."
|
|
fi
|
|
|
|
base="https://github.com/$REPO/releases/download/$TAG"
|
|
|
|
# A private release's browser download URL is not fetchable with a token; assets
|
|
# must come from the API by id, with an octet-stream Accept.
|
|
fetch() { # fetch <asset-name> <dest>
|
|
if [ -z "$TOKEN" ]; then
|
|
get "$base/$1" "$2"
|
|
return
|
|
fi
|
|
# A private release's browser URL is not token-fetchable; assets come from the
|
|
# API by id, with an octet-stream Accept.
|
|
# Pull the asset id out of the release JSON. GitHub pretty-prints, so collapse
|
|
# the newlines FIRST — otherwise each asset object spans many lines and the
|
|
# "id" never lands on the same record as the "name" we matched.
|
|
id="$(get_stdout "https://api.github.com/repos/$REPO/releases/tags/$TAG" \
|
|
| tr -d '\n' | tr '{' '\n' \
|
|
| grep -F "\"$1\"" \
|
|
| sed -n 's/.*"id": *\([0-9][0-9]*\).*/\1/p' | head -1)"
|
|
[ -n "$id" ] || return 1
|
|
curl -fL $DL -H "Authorization: Bearer $TOKEN" -H "Accept: application/octet-stream" \
|
|
"https://api.github.com/repos/$REPO/releases/assets/$id" -o "$2"
|
|
}
|
|
|
|
printf '%s: %s %s\n' "$BIN" "$TAG" "$target"
|
|
fetch "$asset" "$tmp/$asset" \
|
|
|| die "no published build for $target at $TAG."
|
|
fetch "$asset.sha256" "$tmp/$asset.sha256" \
|
|
|| die "release $TAG has no checksum for $target — refusing to install unverified"
|
|
|
|
# Verify BEFORE unpacking: an unverified binary is not installed, ever.
|
|
( cd "$tmp" && \
|
|
if command -v sha256sum >/dev/null 2>&1; then sha256sum -c "$asset.sha256";
|
|
elif command -v shasum >/dev/null 2>&1; then shasum -a 256 -c "$asset.sha256";
|
|
else die "need sha256sum or shasum to verify the download"; fi ) >/dev/null \
|
|
|| die "checksum MISMATCH for $asset — refusing to install"
|
|
|
|
tar -xzf "$tmp/$asset" -C "$tmp"
|
|
[ -f "$tmp/$BIN$ext" ] || die "archive did not contain '$BIN$ext'"
|
|
|
|
mkdir -p "$PREFIX"
|
|
mv "$tmp/$BIN$ext" "$PREFIX/$BIN$ext"
|
|
chmod 755 "$PREFIX/$BIN$ext"
|
|
|
|
printf '%s: installed %s\n' "$BIN" "$PREFIX/$BIN$ext"
|
|
|
|
# The SAME build under a second name. cloud's control binary hands every verb it
|
|
# does not own to `hanzo-node` (it resolves that name before `hanzo`), so the two
|
|
# names must never be two versions: install one and not the other, or upgrade one
|
|
# and not the other, and a user types `hanzo`, gets delegated, and runs an old
|
|
# build with no version anywhere on screen. That is not theoretical — a stale
|
|
# twin served ~150 commands that no longer existed, silently.
|
|
#
|
|
# A symlink makes the skew impossible rather than merely unlikely; where links
|
|
# are not available (Windows) a copy still matches by version. A caller that
|
|
# wants no second name passes HANZO_INSTALL_ALIAS=''.
|
|
if [ -n "$DELEGATE" ]; then
|
|
if ln -sf "$BIN$ext" "$PREFIX/$DELEGATE$ext" 2>/dev/null; then
|
|
:
|
|
else
|
|
cp -f "$PREFIX/$BIN$ext" "$PREFIX/$DELEGATE$ext" && chmod 755 "$PREFIX/$DELEGATE$ext"
|
|
fi
|
|
[ -e "$PREFIX/$DELEGATE$ext" ] \
|
|
|| die "could not install the second name $PREFIX/$DELEGATE$ext — a half-install is not an install"
|
|
printf '%s: installed %s (the same build, second name)\n' "$BIN" "$PREFIX/$DELEGATE$ext"
|
|
fi
|
|
|
|
# Whatever PATH says, name the copy that would ACTUALLY run. An earlier entry
|
|
# wins, and that precedence is exactly how a stale install hides: the user
|
|
# upgrades here and keeps running something else, and re-running the installer
|
|
# never fixes it because it keeps writing to a directory that never wins.
|
|
found="$(command -v "$BIN" 2>/dev/null || true)"
|
|
if [ -n "$found" ] && [ "$found" != "$PREFIX/$BIN$ext" ]; then
|
|
printf '%s: WARNING %s comes first on PATH and will run instead of the\n' "$BIN" "$found"
|
|
printf ' build just installed at %s. Remove it, or put %s first.\n' "$PREFIX/$BIN$ext" "$PREFIX"
|
|
fi
|
|
case ":$PATH:" in
|
|
*":$PREFIX:"*) ;;
|
|
*) printf '%s: %s is not on PATH — add it:\n export PATH="%s:$PATH"\n' "$BIN" "$PREFIX" "$PREFIX" ;;
|
|
esac
|
|
# `hanzo auth login`, not `hanzo login`: the top-level alias arrived in 8.5.157,
|
|
# and a build before it reads an unrecognised first word as a task for the coding
|
|
# agent, so it would start a session about the word "login" instead of signing
|
|
# anyone in. HANZO_VERSION can pin such a build.
|
|
if [ "$BIN" = hanzo ]; then printf 'hanzo: next → hanzo auth login\n'; fi
|