diff --git a/.github/workflows/test-installer.yml b/.github/workflows/test-installer.yml new file mode 100644 index 0000000..f85b054 --- /dev/null +++ b/.github/workflows/test-installer.yml @@ -0,0 +1,38 @@ +name: Test Installer + +on: + pull_request: + paths: + - install.sh + +jobs: + test-installer: + name: Test install.sh + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Install latest release + run: | + sh install.sh + claude-profile --help + + - name: Install specific version + run: | + VERSION=v0.1.0 sh install.sh + claude-profile --help + + - name: Install to custom directory + run: | + mkdir -p "${{ runner.temp }}/bin" + INSTALL_DIR="${{ runner.temp }}/bin" sh install.sh + "${{ runner.temp }}/bin/claude-profile" --help + + - name: Verify failure on nonexistent version + run: | + if VERSION=v99.99.99 sh install.sh 2>&1; then + echo "Expected failure for nonexistent version" + exit 1 + fi + echo "Correctly failed on nonexistent version ✓" diff --git a/README.md b/README.md index f32ba66..3986103 100644 --- a/README.md +++ b/README.md @@ -23,19 +23,37 @@ Each profile is a directory under `~/.claude-profiles//` containing: ## Installation -### From Source +### Quick Install (Recommended) -Requires Go 1.25+: +```bash +curl -fsSL https://raw.githubusercontent.com/diranged/claude-profile/main/install.sh | sh +``` + +This detects your OS and architecture, downloads the latest release, verifies the SHA-256 checksum, and installs to `/usr/local/bin`. + +To install a specific version or to a custom directory: ```bash -git clone https://github.com/diranged/claude-profile-go.git -cd claude-profile-go -make install # Builds and copies to $GOPATH/bin +# Pin a version +curl -fsSL https://raw.githubusercontent.com/diranged/claude-profile/main/install.sh | VERSION=v0.1.0 sh + +# Custom install directory +curl -fsSL https://raw.githubusercontent.com/diranged/claude-profile/main/install.sh | INSTALL_DIR=~/.local/bin sh ``` ### From GitHub Releases -Download a prebuilt binary from the [Releases](https://github.com/diranged/claude-profile-go/releases) page. Binaries are available for Linux, macOS, and Windows on both amd64 and arm64. +Download a prebuilt binary from the [Releases](https://github.com/diranged/claude-profile/releases) page. Binaries are available for Linux, macOS, and Windows on both amd64 and arm64. + +### From Source + +Requires Go 1.25+: + +```bash +git clone https://github.com/diranged/claude-profile.git +cd claude-profile +make install # Builds and copies to $GOPATH/bin +``` ### From Go diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..bc9748f --- /dev/null +++ b/install.sh @@ -0,0 +1,152 @@ +#!/bin/sh +# install.sh — install claude-profile from GitHub releases +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/diranged/claude-profile/main/install.sh | sh +# curl -fsSL ... | VERSION=v0.2.0 sh +# +# Environment variables: +# VERSION — release tag to install (default: latest) +# INSTALL_DIR — where to place the binary (default: /usr/local/bin) + +set -eu + +REPO="diranged/claude-profile" +BINARY="claude-profile" +INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" + +# --- helpers ---------------------------------------------------------------- + +info() { printf ' \033[38;5;108m▸\033[0m %s\n' "$1"; } +warn() { printf ' \033[33m▸\033[0m %s\n' "$1"; } +error() { printf ' \033[31m✗\033[0m %s\n' "$1" >&2; exit 1; } + +need_cmd() { + if ! command -v "$1" > /dev/null 2>&1; then + error "required command not found: $1" + fi +} + +# --- detect platform -------------------------------------------------------- + +detect_os() { + case "$(uname -s)" in + Darwin) echo "darwin" ;; + Linux) echo "linux" ;; + *) error "unsupported OS: $(uname -s)" ;; + esac +} + +detect_arch() { + case "$(uname -m)" in + x86_64|amd64) echo "amd64" ;; + aarch64|arm64) echo "arm64" ;; + *) error "unsupported architecture: $(uname -m)" ;; + esac +} + +# --- resolve version -------------------------------------------------------- + +resolve_version() { + if [ -n "${VERSION:-}" ]; then + echo "$VERSION" + return + fi + need_cmd curl + # Follow the /releases/latest redirect and extract the tag from the final URL. + # No API auth or rate limits required. + local url + url=$(curl -fsSL -o /dev/null -w '%{url_effective}' \ + "https://github.com/${REPO}/releases/latest") + local tag="${url##*/}" + if [ -z "$tag" ] || [ "$tag" = "latest" ]; then + error "could not determine latest release" + fi + echo "$tag" +} + +# --- main ------------------------------------------------------------------- + +main() { + need_cmd curl + need_cmd tar + need_cmd uname + + printf '\n \033[1mClaude Profile Installer\033[0m\n\n' + + local os arch version + os=$(detect_os) + arch=$(detect_arch) + version=$(resolve_version) + local version_num="${version#v}" + + info "Platform: ${os}/${arch}" + info "Version: ${version}" + info "Target: ${INSTALL_DIR}/${BINARY}" + printf '\n' + + local archive="${BINARY}_${version_num}_${os}_${arch}.tar.gz" + local url="https://github.com/${REPO}/releases/download/${version}/${archive}" + local checksum_url="https://github.com/${REPO}/releases/download/${version}/checksums.txt" + + # download to temp dir + TMPDIR_CLEANUP=$(mktemp -d) + trap 'rm -rf "$TMPDIR_CLEANUP"' EXIT + local tmpdir="$TMPDIR_CLEANUP" + + info "Downloading ${archive}..." + if ! curl -fsSL -o "${tmpdir}/${archive}" "$url"; then + error "download failed — check that ${version} exists at https://github.com/${REPO}/releases" + fi + + # verify checksum if sha256sum or shasum is available + if command -v sha256sum > /dev/null 2>&1 || command -v shasum > /dev/null 2>&1; then + info "Verifying checksum..." + curl -fsSL -o "${tmpdir}/checksums.txt" "$checksum_url" + local expected + expected=$(grep "${archive}" "${tmpdir}/checksums.txt" | awk '{print $1}') + if [ -z "$expected" ]; then + warn "checksum entry not found for ${archive}, skipping verification" + else + local actual + if command -v sha256sum > /dev/null 2>&1; then + actual=$(sha256sum "${tmpdir}/${archive}" | awk '{print $1}') + else + actual=$(shasum -a 256 "${tmpdir}/${archive}" | awk '{print $1}') + fi + if [ "$expected" != "$actual" ]; then + error "checksum mismatch!\n expected: ${expected}\n got: ${actual}" + fi + info "Checksum verified ✓" + fi + else + warn "sha256sum/shasum not found, skipping checksum verification" + fi + + # extract + info "Extracting..." + tar -xzf "${tmpdir}/${archive}" -C "${tmpdir}" + + # install + if [ -w "$INSTALL_DIR" ]; then + mv "${tmpdir}/${BINARY}" "${INSTALL_DIR}/${BINARY}" + else + info "Elevated permissions required to write to ${INSTALL_DIR}" + sudo mv "${tmpdir}/${BINARY}" "${INSTALL_DIR}/${BINARY}" + fi + chmod +x "${INSTALL_DIR}/${BINARY}" + + printf '\n \033[38;5;108m✓\033[0m \033[1m%s %s\033[0m installed to %s\n\n' \ + "$BINARY" "$version" "$INSTALL_DIR" + + # verify it runs + if command -v "$BINARY" > /dev/null 2>&1; then + info "Run 'claude-profile --help' to get started" + else + warn "${INSTALL_DIR} may not be in your PATH" + warn "Add it with: export PATH=\"${INSTALL_DIR}:\$PATH\"" + fi + printf '\n' +} + +main