#!/bin/sh # install.sh — Kami CLI installer (POSIX sh). # # Served two ways from get.leistenmacher.de: # • per-CLI (recommended): curl -fsSL https://get.leistenmacher.de//install.sh | bash # • generic: curl -fsSL https://get.leistenmacher.de/install.sh | bash -s -- # # This is a TEMPLATE: is replaced at publish time — with the CLI # name for each per-CLI copy, and with nothing for the generic root copy. # # Asset layout expected: # ///-.tar.gz + .../SHA256SUMS # targets: x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu # x86_64-apple-darwin aarch64-apple-darwin # (Windows is install.ps1.) Each .tar.gz holds -/. # # Env knobs: KAMI_BASE_URL, KAMI_INSTALL_DIR, KAMI_VERSION, KAMI_NO_MODIFY_PATH=1 set -u # ---- configuration ----------------------------------------------------------- KAMI_BASE_URL="${KAMI_BASE_URL:-https://get.leistenmacher.de}" KAMI_CLIS="excalidraw listmonk opencode-ctl sish-ctl solidtime umami" # ---- tiny helpers (lifted from rustup-init.sh) ------------------------------- say() { printf 'kami-install: %s\n' "$1" >&2; } warn() { printf 'kami-install: warning: %s\n' "$1" >&2; } err() { printf 'kami-install: error: %s\n' "$1" >&2; exit 1; } check_cmd() { command -v "$1" >/dev/null 2>&1; } need_cmd() { check_cmd "$1" || err "need '$1' (command not found)"; } ensure() { "$@" || err "command failed: $*"; } # ---- which CLI? -------------------------------------------------------------- # Per-CLI installers bake the name into ; the generic one leaves it # empty and takes the CLI as the first argument. CLI="${1:-${KAMI_CLI:-}}" VERSION="${2:-${KAMI_VERSION:-latest}}" [ -n "$CLI" ] || err "no CLI selected. Use a per-CLI installer: curl -fsSL $KAMI_BASE_URL//install.sh | bash (cli is one of: $KAMI_CLIS)" _ok=0; for c in $KAMI_CLIS; do [ "$c" = "$CLI" ] && _ok=1; done [ "$_ok" = 1 ] || err "unknown CLI '$CLI'. Valid: $KAMI_CLIS" # ---- OS/arch detection -> our target triple ---------------------------------- detect_target() { _os="$(uname -s)"; _cpu="$(uname -m)" case "$_os" in Linux) # We only ship glibc (-gnu) Linux assets. Refuse on musl-only hosts. if check_cmd ldd && ldd --version 2>&1 | grep -qi musl; then err "musl libc detected (e.g. Alpine). Kami ships glibc binaries only. Run inside a glibc container or build from source." fi case "$_cpu" in x86_64|x86-64|x64|amd64) TARGET="x86_64-unknown-linux-gnu" ;; aarch64|arm64) TARGET="aarch64-unknown-linux-gnu" ;; *) err "unsupported CPU architecture: $_cpu" ;; esac ;; Darwin) # Universal binary — runs natively on both Apple Silicon and Intel, # so no arch detection (or Rosetta) is needed. TARGET="universal-apple-darwin" ;; MINGW*|MSYS*|CYGWIN*|Windows_NT) err "Windows detected — use the PowerShell installer: irm $KAMI_BASE_URL/$CLI/install.ps1 | iex" ;; *) err "unsupported OS: $_os" ;; esac } # ---- downloader (TLS 1.2 enforced, curl or wget) ----------------------------- download() { # url, dest if check_cmd curl; then curl --proto '=https' --tlsv1.2 -fsSL "$1" -o "$2" elif check_cmd wget; then wget --https-only --secure-protocol=TLSv1_2 -q "$1" -O "$2" else err "need curl or wget to download" fi } # ---- checksum verification (sha256sum, with macOS shasum fallback) ----------- sha256_of() { # file -> prints hex if check_cmd sha256sum; then sha256sum -b "$1" | awk '{print $1}' elif check_cmd shasum; then shasum -a 256 "$1" | awk '{print $1}' else return 1; fi } verify_checksum() { # file, sumsfile, asset_name _want="$(grep " \*\{0,1\}$3\$" "$2" | awk '{print $1}' | head -n1)" [ -n "$_want" ] || err "no checksum for $3 in SHA256SUMS" _got="$(sha256_of "$1")" || { warn "no sha256 tool; skipping verification"; return 0; } [ "$_got" = "$_want" ] || err "checksum mismatch for $3 want: $_want got: $_got" say "checksum OK ($3)" } # ---- install-dir selection (no sudo by default) ------------------------------ choose_install_dir() { if [ -n "${KAMI_INSTALL_DIR:-}" ]; then INSTALL_DIR="$KAMI_INSTALL_DIR" else INSTALL_DIR="$HOME/.local/bin"; fi ensure mkdir -p "$INSTALL_DIR" [ -w "$INSTALL_DIR" ] || err "install dir not writable: $INSTALL_DIR Set KAMI_INSTALL_DIR=/some/writable/path and retry." } # ---- PATH update via sourced env file (cargo-dist idiom) --------------------- maybe_modify_path() { case ":${PATH}:" in *:"$INSTALL_DIR":*) return 0 ;; esac # already on PATH [ -z "${KAMI_NO_MODIFY_PATH:-}" ] || { say "added; ensure $INSTALL_DIR is on PATH"; return 0; } _env="$INSTALL_DIR/env" if [ ! -f "$_env" ]; then cat > "$_env" </dev/null && continue printf '\n. "%s"\n' "$_env" >> "$rc" done say "added $INSTALL_DIR to PATH (restart shell or: . \"$_env\")" } # ---- main -------------------------------------------------------------------- main() { need_cmd uname; need_cmd tar; need_cmd mkdir; need_cmd awk detect_target ASSET="${CLI}-${TARGET}.tar.gz" REL="$KAMI_BASE_URL/$CLI/$VERSION" say "installing $CLI ($VERSION) for $TARGET" TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT download "$REL/$ASSET" "$TMP/$ASSET" || err "download failed: $REL/$ASSET" download "$REL/SHA256SUMS" "$TMP/SHA256SUMS" || err "download failed: $REL/SHA256SUMS" verify_checksum "$TMP/$ASSET" "$TMP/SHA256SUMS" "$ASSET" ensure tar xf "$TMP/$ASSET" --no-same-owner --strip-components 1 -C "$TMP" [ -f "$TMP/$CLI" ] || err "binary '$CLI' not found in archive" choose_install_dir ensure install -m 0755 "$TMP/$CLI" "$INSTALL_DIR/$CLI" say "installed $INSTALL_DIR/$CLI" maybe_modify_path say "done. run: $CLI --help" } main "$@"