From 648b01fe46b13783732e1571fd850ccf16577dcc Mon Sep 17 00:00:00 2001 From: jarugupj <121142710+jarugupj@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:03:32 +0000 Subject: [PATCH 1/4] Add CLI-only install script Add scripts/install-cli.sh, which installs only the hypeman CLI from kernel/hypeman-cli releases without attempting the server install. It detects OS/arch, resolves the latest CLI release, downloads the matching archive, and installs the hypeman binary to a PATH directory (/usr/local/bin, falling back to ~/.local/bin). This lets get.hypeman.sh/cli install the CLI on machines where the server install is unavailable. Co-Authored-By: Claude Opus 4.7 --- README.md | 2 +- scripts/install-cli.sh | 146 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100755 scripts/install-cli.sh diff --git a/README.md b/README.md index 6ddf2cba..f6ec8ef6 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ To use the Hypeman CLI from a **different machine** than the server: brew install kernel/tap/hypeman ``` -**Linux:** +**Install script (Linux & macOS):** ```bash curl -fsSL https://get.hypeman.sh/cli | bash ``` diff --git a/scripts/install-cli.sh b/scripts/install-cli.sh new file mode 100755 index 00000000..2e0e5494 --- /dev/null +++ b/scripts/install-cli.sh @@ -0,0 +1,146 @@ +#!/bin/bash +# +# Hypeman CLI Install Script +# +# Installs only the hypeman CLI (kernel/hypeman-cli). It does not install or +# configure the hypeman server. +# +# Usage: +# curl -fsSL https://get.hypeman.sh/cli | bash +# +# Options (via environment variables): +# CLI_VERSION - Install a specific CLI version (default: latest) +# INSTALL_DIR - Binary installation directory (default: /usr/local/bin, +# falling back to ~/.local/bin when not writable) +# + +set -e + +REPO="kernel/hypeman-cli" +BINARY_NAME="hypeman" + +# Colors for output (true color) +RED='\033[38;2;255;110;110m' +GREEN='\033[38;2;92;190;83m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } + +# Find the most recent release that has a specific artifact available +# Usage: find_release_with_artifact [ext] +# Returns: version tag (e.g., v0.5.0) or empty string if not found +find_release_with_artifact() { + local repo="$1" + local archive_prefix="$2" + local os="$3" + local arch="$4" + local ext="${5:-tar.gz}" + + local tags + tags=$(curl -fsSL "https://api.github.com/repos/${repo}/releases?per_page=10" 2>/dev/null | grep '"tag_name"' | cut -d'"' -f4) + if [ -z "$tags" ]; then + return 1 + fi + + for tag in $tags; do + local version_num="${tag#v}" + local artifact_name="${archive_prefix}_${version_num}_${os}_${arch}.${ext}" + local artifact_url="https://github.com/${repo}/releases/download/${tag}/${artifact_name}" + + if curl -fsSL --head "$artifact_url" >/dev/null 2>&1; then + echo "$tag" + return 0 + fi + done + + return 1 +} + +# ============================================================================= +# Detect OS and architecture +# ============================================================================= + +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m) +case $ARCH in + x86_64|amd64) + ARCH="amd64" + ;; + aarch64|arm64) + ARCH="arm64" + ;; + *) + error "Unsupported architecture: $ARCH (supported: amd64, arm64)" + ;; +esac + +if [ "$OS" != "linux" ] && [ "$OS" != "darwin" ]; then + error "Unsupported OS: $OS (supported: linux, darwin)" +fi + +# CLI releases use goreleaser naming: "macos" not "darwin", .zip not .tar.gz on macOS +if [ "$OS" = "darwin" ]; then + CLI_OS="macos" + CLI_EXT="zip" +else + CLI_OS="$OS" + CLI_EXT="tar.gz" +fi + +# ============================================================================= +# Resolve installation directory +# ============================================================================= + +INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" +SUDO="" +if [ -w "$INSTALL_DIR" ]; then + : +elif command -v sudo >/dev/null 2>&1; then + SUDO="sudo" +else + INSTALL_DIR="$HOME/.local/bin" +fi +$SUDO mkdir -p "$INSTALL_DIR" + +# ============================================================================= +# Resolve version and download +# ============================================================================= + +if [ -z "${CLI_VERSION:-}" ] || [ "$CLI_VERSION" = "latest" ]; then + info "Fetching latest CLI release for ${CLI_OS}/${ARCH}..." + CLI_VERSION=$(find_release_with_artifact "$REPO" "$BINARY_NAME" "$CLI_OS" "$ARCH" "$CLI_EXT" || true) + [ -n "$CLI_VERSION" ] || error "Could not find a CLI release with an artifact for ${CLI_OS}/${ARCH}" +fi + +CLI_VERSION_NUM="${CLI_VERSION#v}" +ARCHIVE_NAME="${BINARY_NAME}_${CLI_VERSION_NUM}_${CLI_OS}_${ARCH}.${CLI_EXT}" +DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${CLI_VERSION}/${ARCHIVE_NAME}" + +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT + +info "Installing hypeman CLI ${CLI_VERSION}..." +curl -fsSL "$DOWNLOAD_URL" -o "${TMP_DIR}/${ARCHIVE_NAME}" || error "Failed to download ${DOWNLOAD_URL}" + +mkdir -p "${TMP_DIR}/cli" +if [ "$CLI_EXT" = "zip" ]; then + unzip -qo "${TMP_DIR}/${ARCHIVE_NAME}" -d "${TMP_DIR}/cli" +else + tar -xzf "${TMP_DIR}/${ARCHIVE_NAME}" -C "${TMP_DIR}/cli" +fi + +[ -f "${TMP_DIR}/cli/${BINARY_NAME}" ] || error "Archive did not contain expected '${BINARY_NAME}' binary" + +$SUDO install -m 755 "${TMP_DIR}/cli/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}" + +info "Installed ${BINARY_NAME} to ${INSTALL_DIR}/${BINARY_NAME}" + +case ":$PATH:" in + *":${INSTALL_DIR}:"*) ;; + *) warn "${INSTALL_DIR} is not on your PATH. Add it with: export PATH=\"${INSTALL_DIR}:\$PATH\"" ;; +esac + +info "Run 'hypeman --help' to get started." From 971b36c2d398927d12b172e88f51116d9c69006d Mon Sep 17 00:00:00 2001 From: jarugupj <121142710+jarugupj@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:15:32 +0000 Subject: [PATCH 2/4] Note find_release_with_artifact is mirrored from install.sh Co-Authored-By: Claude Opus 4.7 --- scripts/install-cli.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/install-cli.sh b/scripts/install-cli.sh index 2e0e5494..311bdfab 100755 --- a/scripts/install-cli.sh +++ b/scripts/install-cli.sh @@ -29,6 +29,7 @@ info() { echo -e "${GREEN}[INFO]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } +# Mirrored from scripts/install.sh; keep in sync (a curl | bash script can't source a shared file). # Find the most recent release that has a specific artifact available # Usage: find_release_with_artifact [ext] # Returns: version tag (e.g., v0.5.0) or empty string if not found From 4ec6656da26749d58078487e9bd528e76cab27e3 Mon Sep 17 00:00:00 2001 From: jarugupj <121142710+jarugupj@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:24:53 +0000 Subject: [PATCH 3/4] Cover install-cli.sh in the e2e-install CI job Run the CLI-only installer into a temp directory on the macOS runner and assert the installed binary reports its version, exercising the macOS download/extract path end to end. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e333de51..f5e4aa66 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -258,6 +258,11 @@ jobs: run: brew list caddy &>/dev/null || brew install caddy - name: Run E2E install test run: bash scripts/e2e-install-test.sh + - name: Run E2E CLI-only install test + run: | + CLI_DIR="$(mktemp -d)" + INSTALL_DIR="$CLI_DIR" bash scripts/install-cli.sh + "$CLI_DIR/hypeman" --version - name: Cleanup on failure if: failure() run: bash scripts/uninstall.sh || true From 78f3e88644715a3ea6e185558b0e03f65210923a Mon Sep 17 00:00:00 2001 From: jarugupj <121142710+jarugupj@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:38:58 +0000 Subject: [PATCH 4/4] Avoid needless sudo and warn before install-dir fallback Use sudo only when neither the target dir nor its parent is writable, so a creatable path under a writable parent is made and owned by the user instead of root. Warn before falling back to ~/.local/bin rather than relocating an unwritable INSTALL_DIR silently. Co-Authored-By: Claude Opus 4.7 --- scripts/install-cli.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/install-cli.sh b/scripts/install-cli.sh index 311bdfab..a0cf18ee 100755 --- a/scripts/install-cli.sh +++ b/scripts/install-cli.sh @@ -97,11 +97,12 @@ fi INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" SUDO="" -if [ -w "$INSTALL_DIR" ]; then +if [ -w "$INSTALL_DIR" ] || [ -w "$(dirname "$INSTALL_DIR")" ]; then : elif command -v sudo >/dev/null 2>&1; then SUDO="sudo" else + warn "${INSTALL_DIR} is not writable and sudo is unavailable; installing to ~/.local/bin instead" INSTALL_DIR="$HOME/.local/bin" fi $SUDO mkdir -p "$INSTALL_DIR"