#!/bin/sh
# The `crusible` install one-liner (RFC-0037 §5.3, DT-0014-02).
#
# Served at https://crusible.dev/install.sh. This file is the only copy: Hugo
# mounts it into the site's static tree, so the published script and the
# reviewed script cannot drift.
#
# POSIX `sh`, not bash. It is piped into whatever `sh` the machine has, macOS
# ships bash 3.2, and RFC-0093 §10 makes portability a rule with no gate behind
# it. No `local`, no arrays, no `[[`, no GNU-only flags.
#
# It verifies before it writes. RFC-0037 §5.3 admits this install path only as a
# thin front-end to the cosign-signed release, and `scripts/test/install/
# wrapper.bats` is what makes "it verifies" a tested claim.
set -eu

# The pinned default. RFC-0037 §5.3 requires a version, never "latest": a
# one-liner that resolves `latest` installs a different binary on Tuesday than it
# did on Monday, and no reader of the command can tell.
DEFAULT_VERSION="0.1.0"

REPO_BASE="https://github.com/crusiblehq/app/releases/download"
# TWO workflow paths, and both are required. The signing job moved from
# `release-agent.yml` into `nightly.yml` when the workflow set folded to three
# files on 2026-09-08. GitHub bakes `job_workflow_ref` into the certificate at
# SIGN time, so every already-published binary carries the old path and every
# future one carries the new. Dropping either breaks verification for half the
# releases, and cosign is required here — a mismatch fails closed.
IDENTITY_RE='^https://github\.com/crusiblehq/app/\.github/workflows/(release-agent|nightly)\.yml@refs/tags/crusible-v'
OIDC_ISSUER="https://token.actions.githubusercontent.com"
MANUAL_PAGE="https://crusible.dev/docs/reference/install-crusible/"

version="${CRUSIBLE_VERSION:-$DEFAULT_VERSION}"
base_url="${CRUSIBLE_BASE_URL:-$REPO_BASE}"
install_dir="${CRUSIBLE_INSTALL_DIR:-$HOME/.local/bin}"

say() { printf 'crusible: %s\n' "$1"; }
die() {
	printf 'crusible: %s\n' "$1" >&2
	exit 1
}

usage() {
	cat <<'USAGE'
curl -fsSL https://crusible.dev/install.sh | sh

  --version <semver>   Install this version instead of the pinned default.
  --dir <path>         Install here instead of ~/.local/bin. Must be writable.
  -h, --help           This text.

Environment: CRUSIBLE_VERSION, CRUSIBLE_INSTALL_DIR, CRUSIBLE_BASE_URL.

Every download is verified before anything is written: the cosign signature over
checksums.txt first, then the archive's SHA-256 against that file. Prefer to do
it by hand? The same steps, spelled out, are at
https://crusible.dev/docs/reference/install-crusible/
USAGE
}

while [ $# -gt 0 ]; do
	case "$1" in
	--version)
		[ $# -ge 2 ] || die "--version needs a value"
		version="$2"
		shift 2
		;;
	--dir)
		[ $# -ge 2 ] || die "--dir needs a value"
		install_dir="$2"
		shift 2
		;;
	-h | --help)
		usage
		exit 0
		;;
	*) die "unknown option: $1" ;;
	esac
done

# HTTPS only, with no fallback. A plain-HTTP base is refused rather than
# upgraded: an install path that silently repairs a downgrade teaches nobody that
# it happened.
case "$base_url" in
https://*) ;;
*) die "refusing a non-HTTPS download URL: $base_url" ;;
esac

# `--proto '=https'` is what makes the refusal hold through a redirect. Without
# it a 302 to `http://` is followed, and every check above passes over an
# unencrypted transfer.
if command -v curl >/dev/null 2>&1; then
	fetch() { curl -fsSL --proto '=https' --tlsv1.2 -o "$2" "$1"; }
elif command -v wget >/dev/null 2>&1; then
	fetch() { wget -q --https-only -O "$2" "$1"; }
else
	die "needs curl or wget"
fi

# One of the two, whichever the machine has. macOS ships `shasum` and no
# `sha256sum`; a minimal Linux image is often the other way round.
if command -v shasum >/dev/null 2>&1; then
	sha256_of() { shasum -a 256 "$1" | cut -d' ' -f1; }
elif command -v sha256sum >/dev/null 2>&1; then
	sha256_of() { sha256sum "$1" | cut -d' ' -f1; }
else
	die "needs shasum or sha256sum to verify the download"
fi

# cosign is REQUIRED, and this is the deliberate cost of failing closed. An
# install that proceeds without it verifies a checksum against a file served from
# the same host as the checksum, which is not verification. There is no
# skip-the-signature variable on purpose.
command -v cosign >/dev/null 2>&1 || die "$(
	printf '%s\n' \
		"needs cosign to verify the release signature, and will not install without it." \
		"  Install it:  https://docs.sigstore.dev/system_config/installation/" \
		"  Or follow the download, inspect and run path instead:" \
		"    ${MANUAL_PAGE}"
)"

os="$(uname -s)"
case "$os" in
Darwin) goos="darwin" ;;
Linux) goos="linux" ;;
*) die "unsupported operating system: ${os}. The release builds macOS and Linux." ;;
esac

machine="$(uname -m)"
case "$machine" in
x86_64 | amd64) goarch="amd64" ;;
arm64 | aarch64) goarch="arm64" ;;
*) die "unsupported architecture: ${machine}. The release builds amd64 and arm64." ;;
esac

archive="crusible_${version}_${goos}_${goarch}.tar.gz"
release_url="${base_url}/crusible-v${version}"

work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT INT TERM

say "downloading ${archive} from ${release_url}"
fetch "${release_url}/${archive}" "${work}/${archive}" ||
	die "could not download ${archive}. Does version ${version} publish a ${goos}/${goarch} archive?"
fetch "${release_url}/checksums.txt" "${work}/checksums.txt" ||
	die "could not download checksums.txt"
fetch "${release_url}/checksums.txt.bundle" "${work}/checksums.txt.bundle" ||
	die "could not download checksums.txt.bundle, the cosign signature"

# The signature first. Checking the archive against a checksum file nobody
# vouched for proves only that the two files came from the same place.
say "verifying the signature over checksums.txt"
cosign verify-blob \
	--bundle "${work}/checksums.txt.bundle" \
	--certificate-identity-regexp "$IDENTITY_RE" \
	--certificate-oidc-issuer "$OIDC_ISSUER" \
	"${work}/checksums.txt" >/dev/null 2>&1 ||
	die "$(
		printf '%s\n' \
			"the cosign signature over checksums.txt did not verify. NOTHING was installed." \
			"  Re-run to rule out a truncated download. If it fails again, do not install:" \
			"    ${MANUAL_PAGE}"
	)"

say "verifying ${archive}"
want="$(awk -v f="$archive" '$2 == f || $2 == "*" f { print $1 }' "${work}/checksums.txt")"
[ -n "$want" ] || die "checksums.txt lists no line for ${archive}. NOTHING was installed."
got="$(sha256_of "${work}/${archive}")"
[ "$want" = "$got" ] || die "$(
	printf '%s\n' \
		"${archive} does not match its checksums.txt line. NOTHING was installed." \
		"  expected ${want}" \
		"  got      ${got}"
)"

# Only now does anything get extracted. Everything above ran against bytes in a
# temporary directory that the trap removes.
tar -xzf "${work}/${archive}" -C "$work" || die "could not extract ${archive}"
[ -f "${work}/crusible" ] || die "${archive} contains no \`crusible\` binary"

# No implicit sudo, ever. A one-liner that escalates is a one-liner nobody can
# audit before running, and RFC-0037 §5.3 forbids it in as many words.
mkdir -p "$install_dir" || die "could not create ${install_dir}"
[ -w "$install_dir" ] || die "$(
	printf '%s\n' \
		"${install_dir} is not writable, and this installer will not use sudo." \
		"  Pick a writable directory:  --dir \$HOME/bin"
)"

chmod +x "${work}/crusible"
mv "${work}/crusible" "${install_dir}/crusible" ||
	die "could not write ${install_dir}/crusible"

say "installed to ${install_dir}/crusible"
"${install_dir}/crusible" --version

case ":${PATH}:" in
*":${install_dir}:"*) ;;
*) say "note: ${install_dir} is not on your PATH." ;;
esac

# Wave 1 publishes no update feed. `crusible update` refuses by name until
# DT-0015 assigns a manifest host; upgrading means re-running this script.
say "to upgrade, re-run this script with a newer --version. \`crusible update\` is not available yet."
