From 661f433576ff336bf7e5b652a870a59a9756a21f Mon Sep 17 00:00:00 2001 From: yuval <3dyuval@gmail.com> Date: Sat, 1 Nov 2025 09:58:24 +0200 Subject: [PATCH 1/7] [#5] added --auto feature for non-tty --- gh-install | 261 ++++++++++++++++++++++++++++++++++++++-------- gh-install-lib.sh | 158 ++++++++++++++++++++++++++++ 2 files changed, 377 insertions(+), 42 deletions(-) create mode 100644 gh-install-lib.sh diff --git a/gh-install b/gh-install index 5e0ffe1..88228e9 100755 --- a/gh-install +++ b/gh-install @@ -3,38 +3,134 @@ set -eo pipefail TMP="/tmp/.gh-install" BINPATH="${GH_BINPATH:-$HOME/.local/bin}" -REPO=$1 -if [ -z $REPO ]; then - echo "usage: gh install user/repo" +# Flags +AUTO_MODE=false +FLAG_VERSION="" +FLAG_PATTERN="" +FLAG_NAME="" +REPO="" + +show_help() { + cat << EOF +usage: gh install [OPTIONS] + +Install GitHub CLI tools from releases with smart platform detection. + +OPTIONS: + -a, --auto Enable non-interactive mode with auto-detection + -v, --version Version to install (default: latest in auto mode) + -p, --pattern Asset filename pattern (e.g., '*linux*', '*musl*') + -n, --name Binary name (default: auto-detect from filename) + -h, --help Show this help message + +ENVIRONMENT: + GH_BINPATH Installation directory (default: ~/.local/bin) + +EXAMPLES: + # Interactive mode (current behavior) + gh install cli/cli + + # Auto-detect everything + gh install cli/cli --auto + + # Auto-detect with specific version + gh install cli/cli --auto --version v2.40.0 + + # Auto-detect with pattern override (for musl vs gnu, etc) + gh install BurntSushi/ripgrep --auto --pattern '*musl*' + + # Auto-detect with custom binary name + gh install sharkdp/fd --auto --name fdfind + +PLATFORM DETECTION: + Automatically detects OS (linux/darwin) and architecture (x86_64/arm64/etc) + Matches against common release patterns: linux_amd64, darwin_arm64, etc. + +EOF + exit 0 +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -a|--auto) + AUTO_MODE=true + shift + ;; + -v|--version) + FLAG_VERSION="$2" + shift 2 + ;; + -p|--pattern) + FLAG_PATTERN="$2" + shift 2 + ;; + -n|--name) + FLAG_NAME="$2" + shift 2 + ;; + -h|--help) + show_help + ;; + -*) + echo "Unknown option: $1" + echo "Run 'gh install --help' for usage" + exit 1 + ;; + *) + if [ -z "$REPO" ]; then + REPO="$1" + else + echo "Error: Too many arguments" + echo "Run 'gh install --help' for usage" + exit 1 + fi + shift + ;; + esac +done + +if [ -z "$REPO" ]; then + echo "usage: gh install [OPTIONS]" + echo "Run 'gh install --help' for more information" exit 1 fi -choose () { - if command -v fzf 2>&1> /dev/null; then - echo $@ | xargs -n 1 | fzf --height 10 --prompt "$PS3" -1 +# Auto-enable auto mode if not a tty (piped/scripted) +if [ ! -t 0 ] && [ "$AUTO_MODE" = false ]; then + AUTO_MODE=true + echo "[auto] Non-interactive mode enabled (stdin is not a tty)" +fi + +choose() { + if [ "$AUTO_MODE" = true ]; then + # Auto mode: return first item (we expect exactly one from filtering) + echo "$@" | xargs -n 1 | head -n 1 + elif command -v fzf 2>&1> /dev/null; then + echo "$@" | xargs -n 1 | fzf --height 10 --prompt "$PS3" -1 else - select opt in $@; do break; done - echo $opt + select opt in "$@"; do break; done + echo "$opt" fi } -extract () { - for arg in $@ ; do - if [ -f $arg ] ; then - case $arg in - *.tar.bz2) tar xjf $arg ;; - *.tar.gz) tar xzf $arg ;; - *.tar.xz) tar xf $arg ;; - *.tar.zst) tar xf $arg ;; - *.bz2) bunzip2 $arg ;; - *.gz) gunzip $arg ;; - *.tar) tar xf $arg ;; - *.tbz2) tar xjf $arg ;; - *.tgz) tar xzf $arg ;; - *.zip) unzip $arg ;; - *.Z) uncompress $arg ;; - *.rar) rar x $arg ;; # 'rar' must to be installed +extract() { + for arg in "$@"; do + if [ -f "$arg" ]; then + case "$arg" in + *.tar.bz2) tar xjf "$arg" ;; + *.tar.gz) tar xzf "$arg" ;; + *.tar.xz) tar xf "$arg" ;; + *.tar.zst) tar xf "$arg" ;; + *.bz2) bunzip2 "$arg" ;; + *.gz) gunzip "$arg" ;; + *.tar) tar xf "$arg" ;; + *.tbz2) tar xjf "$arg" ;; + *.tgz) tar xzf "$arg" ;; + *.zip) unzip -q "$arg" ;; + *.Z) uncompress "$arg" ;; + *.rar) rar x "$arg" ;; *) echo "'$arg' cannot be extracted, assuming it's binary" && return 1;; esac else @@ -44,44 +140,125 @@ extract () { return 0 } -cleanup () { - rm -rf $TMP +cleanup() { + rm -rf "$TMP" } trap cleanup EXIT -PS3="> Select version: " -tag=$(choose `gh api "repos/$REPO/releases" -q ".[].tag_name"`) -echo "[version] $tag" +# Load auto-mode helpers if needed +if [ "$AUTO_MODE" = true ]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PS3="> Select file: " -filename=$(choose `gh api "repos/$REPO/releases" -q '.[] | select(.tag_name == "'$tag'") | .assets[].name'`) -echo "[filename] $filename" + if [ -f "$SCRIPT_DIR/gh-install-lib.sh" ]; then + source "$SCRIPT_DIR/gh-install-lib.sh" + else + echo "Error: Could not find gh-install-lib.sh" >&2 + echo "Auto mode requires gh-install-lib.sh in the same directory" >&2 + exit 1 + fi + + detect_platform + echo "[platform] $OS_NAME $ARCH_NAME" +fi +# Step 1: Select version +if [ "$AUTO_MODE" = true ]; then + tag=$(auto_select_version "$REPO" "$FLAG_VERSION") + if [ -n "$FLAG_VERSION" ]; then + echo "[version] $tag (from --version flag)" + else + echo "[version] $tag (latest)" + fi +else + PS3="> Select version: " + tag=$(choose $(gh api "repos/$REPO/releases" -q ".[].tag_name")) + echo "[version] $tag" +fi + +# Step 2: Select file/asset +all_assets=$(gh api "repos/$REPO/releases" -q ".[] | select(.tag_name == \"$tag\") | .assets[].name") + +if [ "$AUTO_MODE" = true ]; then + # Auto-detect based on platform (with optional pattern to narrow down) + filename=$(auto_select_asset "$all_assets" "$FLAG_PATTERN") + + if [ $? -ne 0 ]; then + exit 1 + fi + + if [ -n "$FLAG_PATTERN" ]; then + echo "[filename] $filename (auto + pattern: $FLAG_PATTERN)" + else + echo "[filename] $filename (auto-detected)" + fi +elif [ -n "$FLAG_PATTERN" ]; then + # Non-auto mode with explicit pattern + # Convert glob pattern to grep pattern (* -> .*, ? -> .) + grep_pattern=$(echo "$FLAG_PATTERN" | sed 's/\*/.*/g' | sed 's/?/./g') + filename=$(echo "$all_assets" | grep -E "$grep_pattern" | head -n 1) + + if [ -z "$filename" ]; then + echo "Error: No assets match pattern: $FLAG_PATTERN" >&2 + echo "Available assets:" >&2 + echo "$all_assets" | sed 's/^/ /' >&2 + exit 1 + fi + + echo "[filename] $filename (matched pattern: $FLAG_PATTERN)" +else + # Interactive + PS3="> Select file: " + filename=$(choose $(echo "$all_assets")) + echo "[filename] $filename" +fi + +# Step 3: Download echo "[*] Downloading... $filename" -gh release download $tag --repo "$REPO" --pattern "$filename" --dir "$TMP" +gh release download "$tag" --repo "$REPO" --pattern "$filename" --dir "$TMP" ( - cd $TMP + cd "$TMP" - if [[ $filename == *.deb ]]; then + # Handle debian packages + if [[ "$filename" == *.deb ]]; then echo "[*] Installing debian package..." - sudo apt install ./$filename + sudo apt install "./$filename" exit 0 fi echo "[*] Extracting..." - if extract $filename; then - PS3="> Select binary: " - bin=$(choose `find * -type f -not -path "*$filename"`) + # Step 4: Extract and select binary + if extract "$filename"; then + if [ "$AUTO_MODE" = true ]; then + bin=$(auto_select_binary "$filename") + if [ $? -ne 0 ]; then + exit 1 + fi + echo "[binary] $bin (auto-detected)" + else + PS3="> Select binary: " + bin=$(choose $(find . -type f -not -path "*$filename")) + fi else - bin=$filename + bin="$filename" fi - # install + # Step 5: Install basename=$(basename "$bin") - read -p "> Choose a name (empty to leave: $basename): " name + + if [ "$AUTO_MODE" = true ]; then + name=$(auto_select_name "$bin" "$FLAG_NAME") + if [ -n "$FLAG_NAME" ]; then + echo "[name] $name (from --name flag)" + else + echo "[name] $name (auto-detected)" + fi + else + read -p "> Choose a name (empty to leave: $basename): " name + fi + mkdir -p "$BINPATH" target="$BINPATH/${name:-$basename}" mv "$bin" "$target" diff --git a/gh-install-lib.sh b/gh-install-lib.sh new file mode 100644 index 0000000..e043e1b --- /dev/null +++ b/gh-install-lib.sh @@ -0,0 +1,158 @@ +#!/bin/bash +# gh-install auto-mode helper library +# Sourced by gh-install when --auto flag is used + +# Platform detection variables +OS_NAME="" +OS_ALIASES="" +ARCH_NAME="" +ARCH_ALIASES="" + +detect_platform() { + local os=$(uname -s | tr '[:upper:]' '[:lower:]') + local arch=$(uname -m) + + # Normalize OS names + case "$os" in + linux) + OS_NAME="linux" + OS_ALIASES="linux" + ;; + darwin) + OS_NAME="darwin" + OS_ALIASES="darwin|macos|osx|mac|apple" + ;; + mingw*|msys*|cygwin*) + OS_NAME="windows" + OS_ALIASES="windows|win64|win32|win" + ;; + *) + OS_NAME="$os" + OS_ALIASES="$os" + ;; + esac + + # Normalize architecture names + case "$arch" in + x86_64|amd64) + ARCH_NAME="x86_64" + ARCH_ALIASES="amd64|x86_64|x64" + ;; + aarch64|arm64) + ARCH_NAME="arm64" + ARCH_ALIASES="arm64|aarch64" + ;; + armv7l|armv6l) + ARCH_NAME="arm" + ARCH_ALIASES="arm|armv7|armv6" + ;; + i386|i686) + ARCH_NAME="i386" + ARCH_ALIASES="i386|i686|x86" + ;; + *) + ARCH_NAME="$arch" + ARCH_ALIASES="$arch" + ;; + esac +} + +auto_select_version() { + local repo="$1" + local version_override="$2" + + if [ -n "$version_override" ]; then + echo "$version_override" + else + gh api "repos/$repo/releases/latest" -q ".tag_name" + fi +} + +auto_select_asset() { + local assets="$1" # Newline-separated list + local pattern="$2" # Optional pattern to narrow down + + # Filter by OS first + local os_matches=$(echo "$assets" | grep -iE "$OS_ALIASES" || true) + + if [ -z "$os_matches" ]; then + echo "Error: No assets found for OS: $OS_NAME" >&2 + echo "Available assets:" >&2 + echo "$assets" | sed 's/^/ /' >&2 + return 1 + fi + + # Filter by architecture + local matched=$(echo "$os_matches" | grep -iE "$ARCH_ALIASES" || true) + + if [ -z "$matched" ]; then + echo "Error: No assets found for $OS_NAME $ARCH_NAME" >&2 + echo "Assets matching OS ($OS_NAME):" >&2 + echo "$os_matches" | sed 's/^/ /' >&2 + return 1 + fi + + # If pattern provided, narrow down further + if [ -n "$pattern" ]; then + # Convert glob pattern to grep pattern (* -> .*, ? -> .) + local grep_pattern=$(echo "$pattern" | sed 's/\*/.*/g' | sed 's/?/./g') + local narrowed=$(echo "$matched" | grep -E "$grep_pattern" || true) + + if [ -z "$narrowed" ]; then + echo "Error: No platform matches satisfy pattern: $pattern" >&2 + echo "Platform matches:" >&2 + echo "$matched" | sed 's/^/ /' >&2 + return 1 + fi + + matched="$narrowed" + fi + + # Count matches + local count=$(echo "$matched" | wc -l | tr -d ' ') + + if [ "$count" -eq 1 ]; then + echo "$matched" + return 0 + else + echo "Error: Multiple assets match platform ($count found):" >&2 + echo "$matched" | sed 's/^/ /' >&2 + echo "" >&2 + if [ -z "$pattern" ]; then + echo "Use --pattern to narrow down (e.g., --pattern '*musl*' or --pattern '*.tar.gz')" >&2 + else + echo "Pattern '$pattern' still matches multiple files. Be more specific." >&2 + fi + return 1 + fi +} + +auto_select_binary() { + local filename="$1" + + # Find executable file automatically + local bin=$(find . -type f -not -path "*$filename" -executable | head -n 1) + + if [ -z "$bin" ]; then + # No executable found, try to find any binary file + bin=$(find . -type f -not -path "*$filename" | head -n 1) + fi + + if [ -z "$bin" ]; then + echo "Error: No binary found in archive" >&2 + return 1 + fi + + echo "$bin" +} + +auto_select_name() { + local binary_path="$1" + local name_override="$2" + + if [ -n "$name_override" ]; then + echo "$name_override" + else + basename "$binary_path" + fi +} From 77b6dacf5c7104472cc80235afbf01486e3199b3 Mon Sep 17 00:00:00 2001 From: yuval <3dyuval@gmail.com> Date: Sat, 1 Nov 2025 10:03:16 +0200 Subject: [PATCH 2/7] docs: Update README with --auto feature documentation --- README.md | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ccb29b..fbac75b 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,50 @@ gh extension install redraw/gh-install ## Usage +### Interactive mode (default) + ```bash gh install / ``` +The interactive mode will prompt you to select: +- Version to install +- Asset file to download +- Binary to extract (if archive) +- Name for the installed binary + Optional: For fuzzy search support, install [fzf](https://github.com/junegunn/fzf) +### Auto mode (for scripts and CI/CD) + +```bash +gh install / --auto +``` + +Auto mode automatically detects your platform (OS and architecture) and installs the latest version without prompts. + +**Examples:** + +```bash +# Auto-detect everything (version, OS, architecture, binary name) +gh install cli/cli --auto + +# Auto-detect with specific version +gh install cli/cli --auto --version v2.40.0 + +# Auto-detect with pattern to choose variant (musl vs gnu, etc) +gh install BurntSushi/ripgrep --auto --pattern '*.tar.gz$' + +# Auto-detect with custom binary name +gh install sharkdp/fd --auto --name fdfind +``` + +**Options:** +- `-a, --auto` - Enable non-interactive mode with auto-detection +- `-v, --version ` - Version to install (default: latest in auto mode) +- `-p, --pattern ` - Asset filename pattern to narrow down matches +- `-n, --name ` - Binary name (default: auto-detect from filename) +- `-h, --help` - Show help message + ## Environment variables -- `$GH_BINPATH` path to install binaries, defaults to `$HOME/.local/bin` +- `$GH_BINPATH` - Path to install binaries, defaults to `$HOME/.local/bin` From 7f322b4f2c653a4f18c00db9dd41e452a069c77d Mon Sep 17 00:00:00 2001 From: redraw Date: Fri, 28 Nov 2025 01:29:39 -0300 Subject: [PATCH 3/7] add minimal github workflow to test non-interactive installations --- .github/workflows/test.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 44a850a..5b84229 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,3 +20,23 @@ jobs: - name: Install extension locally run: gh extension install . + + - name: Test basic auto mode + run: | + gh install orf/gping --auto + gping --version + + - name: Test with pattern filter + run: | + gh install BurntSushi/ripgrep --auto --pattern '*.tar.gz$' + rg --version + + - name: Test with custom binary name + run: | + gh install sharkdp/fd --auto --name fdfind + fdfind --version + + - name: Test with specific version + run: | + gh install cli/cli --auto --version v2.40.0 + gh version | grep "2.40.0" From 01f986e4c14a428fa00dd45ba94ebfc07001beee Mon Sep 17 00:00:00 2001 From: redraw Date: Fri, 28 Nov 2025 02:36:30 -0300 Subject: [PATCH 4/7] add missing env in github action --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5b84229..ce810f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,9 @@ jobs: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} + env: + GH_TOKEN: ${{ github.token }} + steps: - name: Checkout repository uses: actions/checkout@v4 From 64c9d1434e3c2034b1e1e21dfa045541f122a629 Mon Sep 17 00:00:00 2001 From: "Yuval.D" Date: Tue, 2 Dec 2025 18:04:03 +0200 Subject: [PATCH 5/7] created test matrix for multiple code sources `test.yml` fixed local testing using `gh act push` --- .actrc | 2 + .github/workflows/test.yml | 84 ++++++++++++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 .actrc diff --git a/.actrc b/.actrc new file mode 100644 index 0000000..f10cea7 --- /dev/null +++ b/.actrc @@ -0,0 +1,2 @@ +-P ubuntu-latest=catthehacker/ubuntu:act-latest +-P macos-latest=-self-hosted diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce810f9..1d88431 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,9 +10,60 @@ on: jobs: test: strategy: + fail-fast: false matrix: os: [ubuntu-latest, macos-latest] + tool: + # Basic tools with simple release naming + - repo: orf/gping + verify: gping --version + desc: "simple tarball" + + # Tools requiring pattern filter (multiple variants like musl/gnu) + - repo: BurntSushi/ripgrep + pattern: "*.tar.gz$" + verify: rg --version + desc: "pattern filter" + + # Custom binary name (fd installs as 'fd' but we rename) + - repo: sharkdp/fd + name: fdfind + verify: fdfind --version + desc: "custom name" + + # Specific version pinning + - repo: sharkdp/bat + version: v0.24.0 + verify: bat --version | grep "0.24.0" + desc: "pinned version" + + # Tools with different archive structures + - repo: junegunn/fzf + verify: fzf --version + desc: "simple binary" + + # Tools with .zip releases (common on some projects) + - repo: astral-sh/uv + verify: uv --version + desc: "uv package manager" + + # Go-based tools (often have unique naming) + - repo: jesseduffield/lazygit + verify: lazygit --version + desc: "go binary" + + # Rust-based tools + - repo: sharkdp/hyperfine + verify: hyperfine --version + desc: "rust binary" + + # Tools with nested directory structure in archive + - repo: dandavison/delta + verify: delta --version + desc: "nested archive" + runs-on: ${{ matrix.os }} + name: "${{ matrix.tool.desc }} (${{ matrix.os }})" env: GH_TOKEN: ${{ github.token }} @@ -21,25 +72,26 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Install GitHub CLI (for act) + if: ${{ env.ACT }} + run: | + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null + sudo apt-get update + sudo apt-get install -y gh + - name: Install extension locally run: gh extension install . - - name: Test basic auto mode + - name: "Test: ${{ matrix.tool.desc }}" run: | - gh install orf/gping --auto - gping --version + args="--auto" + [ -n "${{ matrix.tool.pattern }}" ] && args="$args --pattern '${{ matrix.tool.pattern }}'" + [ -n "${{ matrix.tool.name }}" ] && args="$args --name ${{ matrix.tool.name }}" + [ -n "${{ matrix.tool.version }}" ] && args="$args --version ${{ matrix.tool.version }}" - - name: Test with pattern filter - run: | - gh install BurntSushi/ripgrep --auto --pattern '*.tar.gz$' - rg --version + echo "Running: gh install ${{ matrix.tool.repo }} $args" + eval "gh install ${{ matrix.tool.repo }} $args" - - name: Test with custom binary name - run: | - gh install sharkdp/fd --auto --name fdfind - fdfind --version - - - name: Test with specific version - run: | - gh install cli/cli --auto --version v2.40.0 - gh version | grep "2.40.0" + - name: Verify installation + run: ${{ matrix.tool.verify }} From 368ae81e4ed7b528c0dfbe846af0c4d91c37e823 Mon Sep 17 00:00:00 2001 From: "Yuval.D" Date: Thu, 4 Dec 2025 11:59:54 +0200 Subject: [PATCH 6/7] added windows-latest with gitbash to test-matrix --- .github/workflows/test.yml | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1d88431..e390198 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,8 @@ name: Test gh-install on: - push: - branches: [ main ] pull_request: - branches: [ main ] + branches: [main] workflow_dispatch: jobs: @@ -12,52 +10,52 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] tool: - # Basic tools with simple release naming + # Basic tarball - repo: orf/gping verify: gping --version desc: "simple tarball" - # Tools requiring pattern filter (multiple variants like musl/gnu) + # Test --pattern flag (force musl variant) - repo: BurntSushi/ripgrep - pattern: "*.tar.gz$" + pattern: "*musl*" verify: rg --version desc: "pattern filter" - # Custom binary name (fd installs as 'fd' but we rename) + # Test --name flag - repo: sharkdp/fd name: fdfind verify: fdfind --version desc: "custom name" - # Specific version pinning + # Test --version flag - repo: sharkdp/bat version: v0.24.0 verify: bat --version | grep "0.24.0" desc: "pinned version" - # Tools with different archive structures + # Simple binary (no gnu/musl variants) - repo: junegunn/fzf verify: fzf --version desc: "simple binary" - # Tools with .zip releases (common on some projects) + # Multiple file types (.tar.gz + .sha256) - repo: astral-sh/uv verify: uv --version desc: "uv package manager" - # Go-based tools (often have unique naming) + # Go binary - repo: jesseduffield/lazygit verify: lazygit --version desc: "go binary" - # Rust-based tools + # Rust binary - repo: sharkdp/hyperfine verify: hyperfine --version desc: "rust binary" - # Tools with nested directory structure in archive + # Nested archive structure - repo: dandavison/delta verify: delta --version desc: "nested archive" @@ -65,6 +63,10 @@ jobs: runs-on: ${{ matrix.os }} name: "${{ matrix.tool.desc }} (${{ matrix.os }})" + defaults: + run: + shell: bash + env: GH_TOKEN: ${{ github.token }} @@ -94,4 +96,6 @@ jobs: eval "gh install ${{ matrix.tool.repo }} $args" - name: Verify installation - run: ${{ matrix.tool.verify }} + run: | + export PATH="$HOME/.local/bin:$PATH" + ${{ matrix.tool.verify }} From 7b84abb99dfeede1b95fa282af00b80fc9826dcf Mon Sep 17 00:00:00 2001 From: "Yuval.D" Date: Thu, 4 Dec 2025 12:01:22 +0200 Subject: [PATCH 7/7] Add colorized logs, improve auto mode: prefer gnu, exclude checksums --- README.md | 21 ++++++++++++++-- gh-install | 59 +++++++++++++++++++++++++++++++------------- gh-install-lib.sh | 62 +++++++++++++++++++++++++++++------------------ 3 files changed, 99 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index fbac75b..98790c6 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ gh install / ``` The interactive mode will prompt you to select: + - Version to install - Asset file to download - Binary to extract (if archive) @@ -26,13 +27,27 @@ The interactive mode will prompt you to select: Optional: For fuzzy search support, install [fzf](https://github.com/junegunn/fzf) -### Auto mode (for scripts and CI/CD) +### Non-interactive mode: --auto (or -a) + +For scripts, CI/CD pipelines, and non-interactive use. ```bash gh install / --auto ``` -Auto mode automatically detects your platform (OS and architecture) and installs the latest version without prompts. +Auto mode automatically: + +- Detects your platform (OS and architecture) +- Selects the latest version +- Prefers `gnu` over `musl` when both are available +- Excludes checksum files (.sha256, .sig, etc.) + +> [!NOTE] +> To prefer `musl` instead, use the pattern flag: +> +> ```bash +> gh install / --auto --pattern '*musl*' +> ``` **Examples:** @@ -51,6 +66,7 @@ gh install sharkdp/fd --auto --name fdfind ``` **Options:** + - `-a, --auto` - Enable non-interactive mode with auto-detection - `-v, --version ` - Version to install (default: latest in auto mode) - `-p, --pattern ` - Asset filename pattern to narrow down matches @@ -58,4 +74,5 @@ gh install sharkdp/fd --auto --name fdfind - `-h, --help` - Show help message ## Environment variables + - `$GH_BINPATH` - Path to install binaries, defaults to `$HOME/.local/bin` diff --git a/gh-install b/gh-install index 88228e9..3f718a0 100755 --- a/gh-install +++ b/gh-install @@ -4,6 +4,26 @@ set -eo pipefail TMP="/tmp/.gh-install" BINPATH="${GH_BINPATH:-$HOME/.local/bin}" +# Colors (only if terminal supports it) +if [ -t 1 ] && [ -z "$NO_COLOR" ]; then + O='\033[38;5;208m' # orange (actions) + D='\033[90m' # dim/grey (auto-detected) + B='\033[1m' # bold + G='\033[32m' # green + M='\033[35m' # magenta + R='\033[0m' # reset +else + O='' D='' B='' G='' M='' R='' +fi + +log() { + echo -e "${D}$1${R}" +} + +action() { + echo -e "${O}$1${R}" +} + # Flags AUTO_MODE=false FLAG_VERSION="" @@ -97,10 +117,16 @@ if [ -z "$REPO" ]; then exit 1 fi +# Normalize repo: strip GitHub URL prefix if present +REPO="${REPO#https://github.com/}" +REPO="${REPO#http://github.com/}" +REPO="${REPO#github.com/}" +REPO="${REPO%.git}" + # Auto-enable auto mode if not a tty (piped/scripted) if [ ! -t 0 ] && [ "$AUTO_MODE" = false ]; then AUTO_MODE=true - echo "[auto] Non-interactive mode enabled (stdin is not a tty)" + log "[auto-detected mode] non-interactive (stdin is not a tty)" fi choose() { @@ -159,16 +185,16 @@ if [ "$AUTO_MODE" = true ]; then fi detect_platform - echo "[platform] $OS_NAME $ARCH_NAME" + log "[auto-detected platform] ${B}$OS_NAME $ARCH_NAME${R}" fi # Step 1: Select version if [ "$AUTO_MODE" = true ]; then - tag=$(auto_select_version "$REPO" "$FLAG_VERSION") + tag=$(auto_select_version) if [ -n "$FLAG_VERSION" ]; then - echo "[version] $tag (from --version flag)" + log "[using version] ${B}$tag${R} ${M}(from --version)${R}" else - echo "[version] $tag (latest)" + log "[auto-detected version] ${B}$tag${R} ${G}(latest)${R}" fi else PS3="> Select version: " @@ -181,16 +207,16 @@ all_assets=$(gh api "repos/$REPO/releases" -q ".[] | select(.tag_name == \"$tag\ if [ "$AUTO_MODE" = true ]; then # Auto-detect based on platform (with optional pattern to narrow down) - filename=$(auto_select_asset "$all_assets" "$FLAG_PATTERN") + filename=$(auto_select_asset "$all_assets") if [ $? -ne 0 ]; then exit 1 fi if [ -n "$FLAG_PATTERN" ]; then - echo "[filename] $filename (auto + pattern: $FLAG_PATTERN)" + log "[auto-detected asset] ${B}$filename${R} ${M}(pattern: $FLAG_PATTERN)${R}" else - echo "[filename] $filename (auto-detected)" + log "[auto-detected asset] ${B}$filename${R}" fi elif [ -n "$FLAG_PATTERN" ]; then # Non-auto mode with explicit pattern @@ -214,7 +240,7 @@ else fi # Step 3: Download -echo "[*] Downloading... $filename" +action "downloading ${B}$filename${R}" gh release download "$tag" --repo "$REPO" --pattern "$filename" --dir "$TMP" ( @@ -222,12 +248,12 @@ gh release download "$tag" --repo "$REPO" --pattern "$filename" --dir "$TMP" # Handle debian packages if [[ "$filename" == *.deb ]]; then - echo "[*] Installing debian package..." + action "installing debian package ${B}$filename${R}" sudo apt install "./$filename" exit 0 fi - echo "[*] Extracting..." + action "extracting..." # Step 4: Extract and select binary if extract "$filename"; then @@ -236,7 +262,7 @@ gh release download "$tag" --repo "$REPO" --pattern "$filename" --dir "$TMP" if [ $? -ne 0 ]; then exit 1 fi - echo "[binary] $bin (auto-detected)" + log "[auto-detected binary] ${B}$bin${R}" else PS3="> Select binary: " bin=$(choose $(find . -type f -not -path "*$filename")) @@ -249,11 +275,11 @@ gh release download "$tag" --repo "$REPO" --pattern "$filename" --dir "$TMP" basename=$(basename "$bin") if [ "$AUTO_MODE" = true ]; then - name=$(auto_select_name "$bin" "$FLAG_NAME") + name=$(auto_select_name "$bin") if [ -n "$FLAG_NAME" ]; then - echo "[name] $name (from --name flag)" + log "[using name] ${B}$name${R} ${M}(from --name)${R}" else - echo "[name] $name (auto-detected)" + log "[auto-detected name] ${B}$name${R}" fi else read -p "> Choose a name (empty to leave: $basename): " name @@ -264,6 +290,5 @@ gh release download "$tag" --repo "$REPO" --pattern "$filename" --dir "$TMP" mv "$bin" "$target" chmod +x "$target" - echo "Success!" - echo "Saved in: $target" + echo -e "${G}installed ${B}$target${R}" ) diff --git a/gh-install-lib.sh b/gh-install-lib.sh index e043e1b..cb7328c 100644 --- a/gh-install-lib.sh +++ b/gh-install-lib.sh @@ -1,6 +1,9 @@ #!/bin/bash # gh-install auto-mode helper library # Sourced by gh-install when --auto flag is used +# +# Expects these globals from main script: +# REPO, FLAG_VERSION, FLAG_PATTERN, FLAG_NAME # Platform detection variables OS_NAME="" @@ -58,19 +61,17 @@ detect_platform() { } auto_select_version() { - local repo="$1" - local version_override="$2" - - if [ -n "$version_override" ]; then - echo "$version_override" + # Uses globals: REPO, FLAG_VERSION + if [ -n "$FLAG_VERSION" ]; then + echo "$FLAG_VERSION" else - gh api "repos/$repo/releases/latest" -q ".tag_name" + gh api "repos/$REPO/releases/latest" -q ".tag_name" fi } auto_select_asset() { - local assets="$1" # Newline-separated list - local pattern="$2" # Optional pattern to narrow down + # Uses globals: FLAG_PATTERN + local assets="$1" # Newline-separated list (must be passed) # Filter by OS first local os_matches=$(echo "$assets" | grep -iE "$OS_ALIASES" || true) @@ -93,13 +94,13 @@ auto_select_asset() { fi # If pattern provided, narrow down further - if [ -n "$pattern" ]; then + if [ -n "$FLAG_PATTERN" ]; then # Convert glob pattern to grep pattern (* -> .*, ? -> .) - local grep_pattern=$(echo "$pattern" | sed 's/\*/.*/g' | sed 's/?/./g') + local grep_pattern=$(echo "$FLAG_PATTERN" | sed 's/\*/.*/g' | sed 's/?/./g') local narrowed=$(echo "$matched" | grep -E "$grep_pattern" || true) if [ -z "$narrowed" ]; then - echo "Error: No platform matches satisfy pattern: $pattern" >&2 + echo "Error: No platform matches satisfy pattern: $FLAG_PATTERN" >&2 echo "Platform matches:" >&2 echo "$matched" | sed 's/^/ /' >&2 return 1 @@ -108,23 +109,36 @@ auto_select_asset() { matched="$narrowed" fi + # Exclude checksum/signature files + matched=$(echo "$matched" | grep -vE '\.(sha256|sha512|sig|asc)$' || echo "$matched") + # Count matches local count=$(echo "$matched" | wc -l | tr -d ' ') if [ "$count" -eq 1 ]; then echo "$matched" return 0 - else - echo "Error: Multiple assets match platform ($count found):" >&2 - echo "$matched" | sed 's/^/ /' >&2 - echo "" >&2 - if [ -z "$pattern" ]; then - echo "Use --pattern to narrow down (e.g., --pattern '*musl*' or --pattern '*.tar.gz')" >&2 - else - echo "Pattern '$pattern' still matches multiple files. Be more specific." >&2 + fi + + # Multiple matches: prefer gnu over musl (more common on standard Linux) + if [ "$count" -gt 1 ]; then + local gnu_match=$(echo "$matched" | grep -i 'gnu' | head -n 1) + if [ -n "$gnu_match" ]; then + echo "$gnu_match" + return 0 fi - return 1 fi + + # Still multiple matches, error out + echo "Error: Multiple assets match platform ($count found):" >&2 + echo "$matched" | sed 's/^/ /' >&2 + echo "" >&2 + if [ -z "$FLAG_PATTERN" ]; then + echo "Use --pattern to narrow down (e.g., --pattern '*musl*' or --pattern '*.tar.gz')" >&2 + else + echo "Pattern '$FLAG_PATTERN' still matches multiple files. Be more specific." >&2 + fi + return 1 } auto_select_binary() { @@ -147,11 +161,11 @@ auto_select_binary() { } auto_select_name() { - local binary_path="$1" - local name_override="$2" + # Uses globals: FLAG_NAME + local binary_path="$1" # Must be passed (computed at runtime) - if [ -n "$name_override" ]; then - echo "$name_override" + if [ -n "$FLAG_NAME" ]; then + echo "$FLAG_NAME" else basename "$binary_path" fi