From d688209fc7b5c03e135119394704ce2da51ef385 Mon Sep 17 00:00:00 2001 From: Matt Wise Date: Fri, 3 Apr 2026 20:34:56 -0700 Subject: [PATCH 1/6] feat: add curl-pipe-sh installer script Adds install.sh for one-line installation from GitHub releases: curl -fsSL .../install.sh | sh Supports darwin/linux on amd64/arm64, SHA-256 checksum verification, and VERSION override for pinning. Resolves latest release via GitHub's /releases/latest redirect (no API auth needed). Co-Authored-By: Claude Opus 4.6 --- install.sh | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100755 install.sh diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..939d160 --- /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 + local tmpdir + tmpdir=$(mktemp -d) + trap 'rm -rf "$tmpdir"' EXIT + + 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 From ddbf2d42f30cd20cf871bddfe38822184d73fe5d Mon Sep 17 00:00:00 2001 From: Matt Wise Date: Fri, 3 Apr 2026 20:35:42 -0700 Subject: [PATCH 2/6] ci: add installer script test workflow Tests install.sh on Linux with: - Default latest release install - Specific version (VERSION=v0.1.0) - Custom INSTALL_DIR - Failure on nonexistent version Only triggers on PRs that touch install.sh. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/test-installer.yml | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/test-installer.yml diff --git a/.github/workflows/test-installer.yml b/.github/workflows/test-installer.yml new file mode 100644 index 0000000..d89a57d --- /dev/null +++ b/.github/workflows/test-installer.yml @@ -0,0 +1,39 @@ +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 checksum failure detection + run: | + # Corrupt the archive after download by testing with a bad VERSION + 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 ✓" From f5483bd6a4f72cb30443af1f37fb944fd4545efe Mon Sep 17 00:00:00 2001 From: Matt Wise Date: Fri, 3 Apr 2026 20:38:49 -0700 Subject: [PATCH 3/6] fix(ci): add GITHUB_TOKEN support for private repo installs The repo is private, so unauthenticated curl calls to GitHub return 404. Added gh_curl() wrapper that passes Authorization header when GITHUB_TOKEN is set, and thread it through all GitHub-facing curl calls. CI workflow now passes GITHUB_TOKEN to each step. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/test-installer.yml | 11 +++++++++-- install.sh | 16 +++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-installer.yml b/.github/workflows/test-installer.yml index d89a57d..d26a8dc 100644 --- a/.github/workflows/test-installer.yml +++ b/.github/workflows/test-installer.yml @@ -14,24 +14,31 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install latest release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | sh install.sh claude-profile --help - name: Install specific version + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION=v0.1.0 sh install.sh claude-profile --help - name: Install to custom directory + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | mkdir -p "${{ runner.temp }}/bin" INSTALL_DIR="${{ runner.temp }}/bin" sh install.sh "${{ runner.temp }}/bin/claude-profile" --help - - name: Verify checksum failure detection + - name: Verify failure on nonexistent version + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # Corrupt the archive after download by testing with a bad VERSION if VERSION=v99.99.99 sh install.sh 2>&1; then echo "Expected failure for nonexistent version" exit 1 diff --git a/install.sh b/install.sh index 939d160..337af84 100755 --- a/install.sh +++ b/install.sh @@ -8,6 +8,7 @@ # Environment variables: # VERSION — release tag to install (default: latest) # INSTALL_DIR — where to place the binary (default: /usr/local/bin) +# GITHUB_TOKEN — optional token for private repos or to avoid rate limits set -eu @@ -27,6 +28,15 @@ need_cmd() { fi } +# curl wrapper that adds auth header when GITHUB_TOKEN is set +gh_curl() { + if [ -n "${GITHUB_TOKEN:-}" ]; then + curl -H "Authorization: token ${GITHUB_TOKEN}" "$@" + else + curl "$@" + fi +} + # --- detect platform -------------------------------------------------------- detect_os() { @@ -56,7 +66,7 @@ resolve_version() { # 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}' \ + url=$(gh_curl -fsSL -o /dev/null -w '%{url_effective}' \ "https://github.com/${REPO}/releases/latest") local tag="${url##*/}" if [ -z "$tag" ] || [ "$tag" = "latest" ]; then @@ -95,14 +105,14 @@ main() { trap 'rm -rf "$tmpdir"' EXIT info "Downloading ${archive}..." - if ! curl -fsSL -o "${tmpdir}/${archive}" "$url"; then + if ! gh_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" + gh_curl -fsSL -o "${tmpdir}/checksums.txt" "$checksum_url" local expected expected=$(grep "${archive}" "${tmpdir}/checksums.txt" | awk '{print $1}') if [ -z "$expected" ]; then From 2874f9627b09fbee70aead72fc520b20ebd1dc09 Mon Sep 17 00:00:00 2001 From: Matt Wise Date: Fri, 3 Apr 2026 20:41:45 -0700 Subject: [PATCH 4/6] fix: simplify installer for public repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repo is now public — remove GITHUB_TOKEN plumbing and use the simple /releases/latest redirect approach for version resolution. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/test-installer.yml | 8 -------- install.sh | 16 +++------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test-installer.yml b/.github/workflows/test-installer.yml index d26a8dc..f85b054 100644 --- a/.github/workflows/test-installer.yml +++ b/.github/workflows/test-installer.yml @@ -14,30 +14,22 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install latest release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | sh install.sh claude-profile --help - name: Install specific version - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION=v0.1.0 sh install.sh claude-profile --help - name: Install to custom directory - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | if VERSION=v99.99.99 sh install.sh 2>&1; then echo "Expected failure for nonexistent version" diff --git a/install.sh b/install.sh index 337af84..939d160 100755 --- a/install.sh +++ b/install.sh @@ -8,7 +8,6 @@ # Environment variables: # VERSION — release tag to install (default: latest) # INSTALL_DIR — where to place the binary (default: /usr/local/bin) -# GITHUB_TOKEN — optional token for private repos or to avoid rate limits set -eu @@ -28,15 +27,6 @@ need_cmd() { fi } -# curl wrapper that adds auth header when GITHUB_TOKEN is set -gh_curl() { - if [ -n "${GITHUB_TOKEN:-}" ]; then - curl -H "Authorization: token ${GITHUB_TOKEN}" "$@" - else - curl "$@" - fi -} - # --- detect platform -------------------------------------------------------- detect_os() { @@ -66,7 +56,7 @@ resolve_version() { # Follow the /releases/latest redirect and extract the tag from the final URL. # No API auth or rate limits required. local url - url=$(gh_curl -fsSL -o /dev/null -w '%{url_effective}' \ + url=$(curl -fsSL -o /dev/null -w '%{url_effective}' \ "https://github.com/${REPO}/releases/latest") local tag="${url##*/}" if [ -z "$tag" ] || [ "$tag" = "latest" ]; then @@ -105,14 +95,14 @@ main() { trap 'rm -rf "$tmpdir"' EXIT info "Downloading ${archive}..." - if ! gh_curl -fsSL -o "${tmpdir}/${archive}" "$url"; then + 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..." - gh_curl -fsSL -o "${tmpdir}/checksums.txt" "$checksum_url" + curl -fsSL -o "${tmpdir}/checksums.txt" "$checksum_url" local expected expected=$(grep "${archive}" "${tmpdir}/checksums.txt" | awk '{print $1}') if [ -z "$expected" ]; then From 701d5b7e175bd7caff52e44e7b8bb5c67c488ce6 Mon Sep 17 00:00:00 2001 From: Matt Wise Date: Fri, 3 Apr 2026 20:42:44 -0700 Subject: [PATCH 5/6] fix: use global var for trap cleanup to avoid scope issue The trap handler can't access local variables from main() under set -u, causing 'parameter not set' error on exit. Co-Authored-By: Claude Opus 4.6 --- install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 939d160..bc9748f 100755 --- a/install.sh +++ b/install.sh @@ -90,9 +90,9 @@ main() { local checksum_url="https://github.com/${REPO}/releases/download/${version}/checksums.txt" # download to temp dir - local tmpdir - tmpdir=$(mktemp -d) - trap 'rm -rf "$tmpdir"' EXIT + 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 From 0983034c30f641f9a00961eb0142a15835d5ea5e Mon Sep 17 00:00:00 2001 From: Matt Wise Date: Fri, 3 Apr 2026 20:44:17 -0700 Subject: [PATCH 6/6] docs: add curl installer as recommended install method Co-Authored-By: Claude Opus 4.6 --- README.md | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) 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