#!/bin/sh # p2claw installer. # # Usage: # curl -fsSL https://p2claw.com/install | sh # curl -fsSL https://p2claw.com/install | sh -s -- --version v0.1.0 # curl -fsSL https://p2claw.com/install | sh -s -- --prefix /usr/local/bin # # Env overrides: # P2CLAW_VERSION pin to a release tag (e.g. v0.1.0) # P2CLAW_INSTALL_DIR install destination (default: $HOME/.local/bin) # P2CLAW_REPO override release source (default: phact/p2claw-skill) # # Behaviour: # - macOS (Darwin) and Linux: downloads the matching binary from # GitHub releases, verifies SHA-256 if SHA256SUMS is published, # installs to ~/.local/bin/p2claw (no sudo). # - Windows (MINGW / MSYS / Cygwin): refuses with a clear pointer # to WSL; the agent's UDS local API + signal handling are # POSIX-only (`docs/local-api-auth.md §3`). # # This script is POSIX sh; no bashisms. `set -eu` makes any # unhandled failure abort instead of silently moving on. set -eu REPO="${P2CLAW_REPO:-phact/p2claw-skill}" INSTALL_DIR="${P2CLAW_INSTALL_DIR:-$HOME/.local/bin}" VERSION="${P2CLAW_VERSION:-}" BIN_NAME="p2claw" # ---------- ANSI helpers (only when stdout is a tty) ------------------ if [ -t 1 ] && command -v tput >/dev/null 2>&1 && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then RED=$(printf '\033[31m'); YEL=$(printf '\033[33m'); GRN=$(printf '\033[32m') BLD=$(printf '\033[1m'); DIM=$(printf '\033[2m'); RST=$(printf '\033[0m') else RED=; YEL=; GRN=; BLD=; DIM=; RST= fi say() { printf '%s%s%s %s\n' "$DIM" "→" "$RST" "$*"; } ok() { printf '%s✓%s %s\n' "$GRN" "$RST" "$*"; } warn() { printf '%swarn:%s %s\n' "$YEL" "$RST" "$*" >&2; } err() { printf '%serror:%s %s\n' "$RED" "$RST" "$*" >&2; exit 1; } # ---------- argv parse ------------------------------------------------ while [ $# -gt 0 ]; do case "$1" in --version) VERSION="$2"; shift 2 ;; --version=*) VERSION="${1#*=}"; shift ;; --prefix) INSTALL_DIR="$2"; shift 2 ;; --prefix=*) INSTALL_DIR="${1#*=}"; shift ;; -h|--help) sed -n '/^# Usage:/,/^# Behaviour:/p' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; *) err "unknown flag: $1 (try --help)" ;; esac done # ---------- platform detection ---------------------------------------- OS_RAW="$(uname -s)" case "$OS_RAW" in Darwin) OS="macos" ;; Linux) OS="linux" ;; MINGW*|MSYS*|CYGWIN*|Windows_NT) printf '%sWindows detected — p2claw needs WSL.%s\n' "$YEL" "$RST" >&2 cat >&2 </dev/null 2>&1; then # `getconf GNU_LIBC_VERSION` prints e.g. `glibc 2.35` on glibc; # exits non-zero (or prints nothing) on musl. GLIBC_VER="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}')" fi if [ -z "$GLIBC_VER" ] && command -v ldd >/dev/null 2>&1; then # `ldd --version` first line: `ldd (GNU libc) 2.35` (glibc) or # `musl libc (x86_64) Version 1.2.5` (musl). Pull a `M.m` shape # from the first line only. GLIBC_VER="$(ldd --version 2>&1 | head -n1 | grep -oE '[0-9]+\.[0-9]+' | head -n1)" # Disambiguate musl: if the first line contains "musl", clear # GLIBC_VER so we route to the unknown-libc branch instead of # comparing musl's version number against a glibc threshold. if ldd --version 2>&1 | head -n1 | grep -qi musl; then GLIBC_VER="" LIBC_KIND="musl" fi fi if [ -n "$GLIBC_VER" ]; then GLIBC_MAJOR="${GLIBC_VER%%.*}" GLIBC_MINOR="${GLIBC_VER#*.}" GLIBC_MINOR="${GLIBC_MINOR%%.*}" # POSIX integer comparison — major first, then minor. if [ "$GLIBC_MAJOR" -lt "$GLIBC_MIN_MAJOR" ] || \ { [ "$GLIBC_MAJOR" -eq "$GLIBC_MIN_MAJOR" ] && [ "$GLIBC_MINOR" -lt "$GLIBC_MIN_MINOR" ]; }; then err "this system's glibc is $GLIBC_VER; p2claw needs glibc ≥${GLIBC_MIN_MAJOR}.${GLIBC_MIN_MINOR}. Upgrade your distro (Debian 12+, Ubuntu 22.04+, RHEL 9+) or run p2claw inside a newer container base." fi say "glibc $GLIBC_VER (≥${GLIBC_MIN_MAJOR}.${GLIBC_MIN_MINOR}, OK)" elif [ "${LIBC_KIND-}" = "musl" ]; then err "this system uses musl libc (Alpine, void-musl, etc). p2claw's published Linux binaries are glibc-only — a musl static build is on the roadmap but not shipped yet. See https://github.com/$REPO for status." else # Neither getconf nor ldd available — rare, but surface a # warning rather than blocking. The agent will hit the # load-time symbol error if the floor is wrong; the user # gets a hint either way. warn "could not detect glibc version (no getconf or ldd on PATH); proceeding anyway. If you hit \`requires GLIBC_…\` on first run, your distro's glibc is too old." fi fi # ---------- prerequisites -------------------------------------------- require() { command -v "$1" >/dev/null 2>&1 || err "missing dependency: $1"; } require curl require uname require tar # Pick a SHA-256 verifier — Linux usually has sha256sum, macOS has shasum. if command -v sha256sum >/dev/null 2>&1; then SHA256_CMD="sha256sum" elif command -v shasum >/dev/null 2>&1; then SHA256_CMD="shasum -a 256" else SHA256_CMD="" fi # ---------- resolve version ------------------------------------------ if [ -z "$VERSION" ]; then say "resolving latest release from $REPO …" # GitHub's "latest" redirects to the tag; pull tag_name out of the # JSON without jq (sed is everywhere). VERSION="$( curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null \ | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' \ | head -n1 )" [ -n "$VERSION" ] || err "could not resolve latest release for $REPO (rate-limited or no releases yet?). Pin one with --version or P2CLAW_VERSION." fi say "target: ${BLD}$TARGET${RST}, version: ${BLD}$VERSION${RST}" # ---------- download -------------------------------------------------- ASSET="p2claw-${VERSION}-${TARGET}.tar.gz" URL="https://github.com/$REPO/releases/download/$VERSION/$ASSET" TMP="$(mktemp -d 2>/dev/null || mktemp -d -t p2claw)" trap 'rm -rf "$TMP"' EXIT INT TERM say "downloading $URL" if ! curl -fSL --progress-bar -o "$TMP/$ASSET" "$URL"; then err "download failed. Verify that ${BLD}$ASSET${RST} exists at https://github.com/$REPO/releases/$VERSION" fi # ---------- verify checksum (best-effort) ----------------------------- SUMS_URL="https://github.com/$REPO/releases/download/$VERSION/SHA256SUMS" if [ -n "$SHA256_CMD" ] && curl -fsSL -o "$TMP/SHA256SUMS" "$SUMS_URL" 2>/dev/null; then say "verifying SHA-256 …" EXPECTED="$(grep -E "[[:space:]]$ASSET\$" "$TMP/SHA256SUMS" | awk '{print $1}' | head -n1)" if [ -z "$EXPECTED" ]; then warn "SHA256SUMS published but didn't list $ASSET; skipping check" else GOT="$(cd "$TMP" && $SHA256_CMD "$ASSET" | awk '{print $1}')" if [ "$EXPECTED" = "$GOT" ]; then ok "checksum verified" else err "checksum mismatch (expected $EXPECTED, got $GOT)" fi fi else warn "SHA256SUMS not published or no sha256 tool available; skipping integrity check" fi # ---------- extract + install ---------------------------------------- say "extracting …" tar -xzf "$TMP/$ASSET" -C "$TMP" # Most release tarballs unpack the binary at the root or in a single # top-level dir. Find it either way. SRC="" if [ -f "$TMP/$BIN_NAME" ]; then SRC="$TMP/$BIN_NAME" else SRC="$(find "$TMP" -type f -name "$BIN_NAME" -perm -u+x 2>/dev/null | head -n1 || true)" fi [ -n "$SRC" ] && [ -f "$SRC" ] || err "tarball did not contain an executable named $BIN_NAME" mkdir -p "$INSTALL_DIR" DEST="$INSTALL_DIR/$BIN_NAME" mv "$SRC" "$DEST" chmod +x "$DEST" ok "installed $DEST" # ---------- PATH hint ------------------------------------------------- case ":${PATH-}:" in *":$INSTALL_DIR:"*) ;; *) warn "$INSTALL_DIR is not on \$PATH" cat >&2 <> ~/.zshrc${RST} # zsh ${BLD}echo 'export PATH="$INSTALL_DIR:\$PATH"' >> ~/.bashrc${RST} # bash Or invoke the binary by its full path: ${BLD}$DEST${RST} EOF ;; esac # ---------- next steps ------------------------------------------------ cat < ${RST} # publish a localhost upstream as a peer URL Docs and skill setup: ${DIM}https://github.com/$REPO${RST} EOF