Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 311 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
#!/usr/bin/env bash
set -euo pipefail

# --- config ---
REPO="relaystr/ndk"
BIN_NAME="ndk"
INSTALL_DIR="${NDK_INSTALL_DIR:-$HOME/.local/bin}"
VERSION="${NDK_VERSION:-latest}"
PRERELEASE="${NDK_PRERELEASE:-0}"
INSTALL_MODE="${NDK_INSTALL_MODE:-user}"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -type f -name "install.sh" | head -20

Repository: relaystr/ndk

Length of output: 226


🏁 Script executed:

fd -t f -name "install.sh"

Repository: relaystr/ndk

Length of output: 286


🏁 Script executed:

find . -name "install.sh" -type f

Repository: relaystr/ndk

Length of output: 68


🏁 Script executed:

cat -n ./install.sh | head -220

Repository: relaystr/ndk

Length of output: 6982


🏁 Script executed:

cat -n ./install.sh | sed -n '205,300p'

Repository: relaystr/ndk

Length of output: 4081


Add validation for NDK_INSTALL_MODE environment variable.

INSTALL_MODE can be set from the environment to an invalid value and will silently fall back to the default user install mode instead of rejecting it. Add validation in parse_args() to ensure only valid values (user or system) are accepted.

Suggested fix
 parse_args() {
   while [ $# -gt 0 ]; do
     case "$1" in
       --system) INSTALL_MODE="system" ;;
       --user) INSTALL_MODE="user" ;;
       -h | --help)
         cat <<EOF
 Usage: install.sh [--user|--system]

 Install mode (Linux):
   --user      Install to user paths (default):
               - binary: NDK_INSTALL_DIR (default: ~/.local/bin)
               - libs:   NDK_USER_LIB_DIR (default: ~/.local/lib)
   --system    Install to system paths:
               - binary: NDK_SYSTEM_BIN_DIR (default: /usr/bin)
               - libs:   NDK_SYSTEM_LIB_DIR (default: /usr/lib)

 Env vars:
   NDK_VERSION       Version or tag. Also supports latest, latest-pre.
   NDK_PRERELEASE    When truthy and NDK_VERSION=latest, picks latest pre-release.
   NDK_INSTALL_MODE  user|system (Linux)
 EOF
         exit 0
         ;;
       *) err "unknown argument: $1" ;;
     esac
     shift
   done
+
+  case "$INSTALL_MODE" in
+    user|system) ;;
+    *) err "invalid install mode: ${INSTALL_MODE} (expected 'user' or 'system')" ;;
+  esac
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
INSTALL_MODE="${NDK_INSTALL_MODE:-user}"
parse_args() {
while [ $# -gt 0 ]; do
case "$1" in
--system) INSTALL_MODE="system" ;;
--user) INSTALL_MODE="user" ;;
-h | --help)
cat <<EOF
Usage: install.sh [--user|--system]
Install mode (Linux):
--user Install to user paths (default):
- binary: NDK_INSTALL_DIR (default: ~/.local/bin)
- libs: NDK_USER_LIB_DIR (default: ~/.local/lib)
--system Install to system paths:
- binary: NDK_SYSTEM_BIN_DIR (default: /usr/bin)
- libs: NDK_SYSTEM_LIB_DIR (default: /usr/lib)
Env vars:
NDK_VERSION Version or tag. Also supports latest, latest-pre.
NDK_PRERELEASE When truthy and NDK_VERSION=latest, picks latest pre-release.
NDK_INSTALL_MODE user|system (Linux)
EOF
exit 0
;;
*) err "unknown argument: $1" ;;
esac
shift
done
case "$INSTALL_MODE" in
user|system) ;;
*) err "invalid install mode: ${INSTALL_MODE} (expected 'user' or 'system')" ;;
esac
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@install.sh` at line 10, Add validation in parse_args() to check the resolved
INSTALL_MODE (from NDK_INSTALL_MODE or default) and reject any value other than
"user" or "system": locate where INSTALL_MODE is set
(INSTALL_MODE="${NDK_INSTALL_MODE:-user}") and in parse_args() verify
INSTALL_MODE is exactly "user" or "system"; if not, print a clear error and exit
non‑zero. Ensure the error message references the invalid value and shows the
allowed options so callers can correct NDK_INSTALL_MODE.

USER_LIB_DIR="${NDK_USER_LIB_DIR:-$HOME/.local/lib}"
SYSTEM_BIN_DIR="${NDK_SYSTEM_BIN_DIR:-/usr/bin}"
SYSTEM_LIB_DIR="${NDK_SYSTEM_LIB_DIR:-/usr/lib}"

# --- helpers ---
info() { echo -e "\033[1;34minfo\033[0m $*"; }
ok() { echo -e "\033[1;32m ok \033[0m $*"; }
err() { echo -e "\033[1;31merror\033[0m $*" >&2; exit 1; }

need() {
command -v "$1" &>/dev/null || err "required tool not found: $1"
}

is_truthy() {
case "$1" in
1 | true | TRUE | yes | YES) return 0 ;;
*) return 1 ;;
esac
}

# --- detect platform ---
detect_target() {
local os arch
os="$(uname -s)"
arch="$(uname -m)"

case "$os" in
Linux) os="linux" ;;
Darwin) os="macos" ;;
*) err "unsupported OS: $os" ;;
esac

case "$arch" in
x86_64) arch="x64" ;;
aarch64 | arm64) arch="arm64" ;;
*) err "unsupported architecture: $arch" ;;
esac

echo "${os}-${arch}"
}

# --- resolve version/tag ---
resolve_tag() {
local use_prerelease="0"
local tag

case "$VERSION" in
latest-pre | latest-prerelease) use_prerelease="1" ;;
latest) is_truthy "$PRERELEASE" && use_prerelease="1" ;;
esac

if [ "$VERSION" = "latest" ] || [ "$VERSION" = "latest-pre" ] || [ "$VERSION" = "latest-prerelease" ]; then
need curl

if [ "$use_prerelease" = "1" ]; then
tag=$(
curl -fsSL "https://api.github.com/repos/${REPO}/releases" \
| awk '
BEGIN { tag=""; draft=0; found="" }
/"tag_name":/ {
if (tag == "") {
tag = $0
sub(/^.*"tag_name":[[:space:]]*"/, "", tag)
sub(/".*$/, "", tag)
}
}
/"draft":/ {
draft = ($0 ~ /true/) ? 1 : 0
}
/"prerelease":/ {
if ($0 ~ /true/ && draft == 0 && tag != "") {
if (found == "") {
found = tag
}
}
tag=""
draft=0
}
END {
if (found != "") {
print found
}
}
'
)
[ -n "$tag" ] || err "could not determine latest pre-release version"
else
tag=$(
curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \
| grep '"tag_name"' \
| sed -E 's/.*"([^"]+)".*/\1/'
)
[ -n "$tag" ] || err "could not determine latest version"
fi
else
if [[ "$VERSION" =~ ^v[0-9] ]]; then
tag="$VERSION"
else
tag="v${VERSION}"
fi
fi

echo "$tag"
}

get_release_json() {
local tag="$1"

if curl -fsSL "https://api.github.com/repos/${REPO}/releases/tags/${tag}"; then
return 0
fi

if [[ "$tag" =~ ^v ]]; then
curl -fsSL "https://api.github.com/repos/${REPO}/releases/tags/${tag#v}" \
|| err "could not fetch release metadata for tag: ${tag}"
else
curl -fsSL "https://api.github.com/repos/${REPO}/releases/tags/v${tag}" \
|| err "could not fetch release metadata for tag: ${tag}"
fi
}

resolve_release_asset() {
local release_json="$1"
local target="$2"

printf "%s\n" "$release_json" | awk -v target="$target" '
BEGIN { name="" }
/"name":/ {
if (name == "") {
name = $0
sub(/^.*"name":[[:space:]]*"/, "", name)
sub(/".*$/, "", name)
}
next
}
/"browser_download_url":/ {
if (name != "") {
url = $0
sub(/^.*"browser_download_url":[[:space:]]*"/, "", url)
sub(/".*$/, "", url)
if (name ~ ("-" target "\\.(tar\\.gz|tgz)$")) {
print name "\t" url
exit
}
}
name = ""
}
Comment on lines +136 to +157
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

GitHub Releases API response JSON structure assets array field order

💡 Result:

The GitHub Releases API response JSON structure includes an 'assets' array in release objects (e.g., from GET /repos/{owner}/{repo}/releases, GET /repos/{owner}/{repo}/releases/{release_id}). Official documentation does not specify any guaranteed order for the elements in this array. No sorting criteria (such as by ID, name, created_at, or upload order) is documented. In practice, it is likely unordered or sorted by creation time or ID (ascending), as is common for database-backed lists without explicit sorting, but this is not guaranteed and may vary. For reliable ordering, clients should sort the array themselves if order matters. Note that the dedicated List release assets endpoint (GET /repos/{owner}/{repo}/releases/{release_id}/assets) supports pagination but also lacks documented sort order.

Citations:


🏁 Script executed:

# Let's also create a minimal test to trace through the AWK logic
cat > /tmp/test_awk.sh << 'EOF'
#!/bin/bash

# Sample GitHub Releases API response (simplified)
release_json='
{
  "id": 123,
  "tag_name": "v1.0.0",
  "name": "Release v1.0.0",
  "assets": [
    {
      "id": 456,
      "name": "my-tool-linux-amd64.tar.gz",
      "browser_download_url": "https://example.com/my-tool-linux-amd64.tar.gz"
    },
    {
      "id": 789,
      "name": "my-tool-darwin-arm64.tar.gz",
      "browser_download_url": "https://example.com/my-tool-darwin-arm64.tar.gz"
    }
  ]
}
'

target="linux-amd64"

echo "=== Testing ORIGINAL code ==="
printf "%s\n" "$release_json" | awk -v target="$target" '
  BEGIN { name="" }
  /"name":/ {
    if (name == "") {
      name = $0
      sub(/^.*"name":[[:space:]]*"/, "", name)
      sub(/".*$/, "", name)
    }
    next
  }
  /"browser_download_url":/ {
    if (name != "") {
      url = $0
      sub(/^.*"browser_download_url":[[:space:]]*"/, "", url)
      sub(/".*$/, "", url)
      if (name ~ ("-" target "\\.(tar\\.gz|tgz)$")) {
        print name "\t" url
        exit
      }
    }
    name = ""
  }
'

echo ""
echo "=== Testing SUGGESTED FIX ==="
printf "%s\n" "$release_json" | awk -v target="$target" '
  BEGIN { in_assets=0; name="" }
  /"assets":[[:space:]]*\[/ { in_assets=1; next }
  in_assets && /^[[:space:]]*\],?$/ { exit }
  !in_assets { next }
  /"name":/ {
    if (name == "") {
      name = $0
      sub(/^.*"name":[[:space:]]*"/, "", name)
      sub(/".*$/, "", name)
    }
    next
  }
  /"browser_download_url":/ {
    if (name != "") {
      url = $0
      sub(/^.*"browser_download_url":[[:space:]]*"/, "", url)
      sub(/".*$/, "", url)
      if (name ~ ("-" target "\\.(tar\\.gz|tgz)$")) {
        print name "\t" url
        exit
      }
    }
    name = ""
  }
'
EOF

bash /tmp/test_awk.sh
EOF

Repository: relaystr/ndk

Length of output: 252


🏁 Script executed:

# Corrected test script
release_json='
{
  "id": 123,
  "tag_name": "v1.0.0",
  "name": "Release v1.0.0",
  "assets": [
    {
      "id": 456,
      "name": "my-tool-linux-amd64.tar.gz",
      "browser_download_url": "https://example.com/my-tool-linux-amd64.tar.gz"
    },
    {
      "id": 789,
      "name": "my-tool-darwin-arm64.tar.gz",
      "browser_download_url": "https://example.com/my-tool-darwin-arm64.tar.gz"
    }
  ]
}
'

target="linux-amd64"

echo "=== ORIGINAL CODE (from install.sh lines 136-157) ==="
printf "%s\n" "$release_json" | awk -v target="$target" '
  BEGIN { name="" }
  /"name":/ {
    if (name == "") {
      name = $0
      sub(/^.*"name":[[:space:]]*"/, "", name)
      sub(/".*$/, "", name)
    }
    next
  }
  /"browser_download_url":/ {
    if (name != "") {
      url = $0
      sub(/^.*"browser_download_url":[[:space:]]*"/, "", url)
      sub(/".*$/, "", url)
      if (name ~ ("-" target "\\.(tar\\.gz|tgz)$")) {
        print name "\t" url
        exit
      }
    }
    name = ""
  }
'
echo "(Exit code: $?)"

echo ""
echo "=== SUGGESTED FIX ==="
printf "%s\n" "$release_json" | awk -v target="$target" '
  BEGIN { in_assets=0; name="" }
  /"assets":[[:space:]]*\[/ { in_assets=1; next }
  in_assets && /^[[:space:]]*\],?$/ { exit }
  !in_assets { next }
  /"name":/ {
    if (name == "") {
      name = $0
      sub(/^.*"name":[[:space:]]*"/, "", name)
      sub(/".*$/, "", name)
    }
    next
  }
  /"browser_download_url":/ {
    if (name != "") {
      url = $0
      sub(/^.*"browser_download_url":[[:space:]]*"/, "", url)
      sub(/".*$/, "", url)
      if (name ~ ("-" target "\\.(tar\\.gz|tgz)$")) {
        print name "\t" url
        exit
      }
    }
    name = ""
  }
'
echo "(Exit code: $?)"

Repository: relaystr/ndk

Length of output: 236


The first release asset is never selectable.

Line 138 captures the release-level "name" before the parser reaches the assets[] array, causing the first asset's "name" to be skipped. If the target archive is listed first, resolve_release_asset() returns nothing and the install fails at lines 226–228.

Suggested fix
-  printf "%s\n" "$release_json" | awk -v target="$target" '
-    BEGIN { name="" }
+  printf "%s\n" "$release_json" | awk -v target="$target" '
+    BEGIN { in_assets=0; name="" }
+    /"assets":[[:space:]]*\[/ { in_assets=1; next }
+    in_assets && /^[[:space:]]*\],?$/ { exit }
+    !in_assets { next }
     /"name":/ {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
printf "%s\n" "$release_json" | awk -v target="$target" '
BEGIN { name="" }
/"name":/ {
if (name == "") {
name = $0
sub(/^.*"name":[[:space:]]*"/, "", name)
sub(/".*$/, "", name)
}
next
}
/"browser_download_url":/ {
if (name != "") {
url = $0
sub(/^.*"browser_download_url":[[:space:]]*"/, "", url)
sub(/".*$/, "", url)
if (name ~ ("-" target "\\.(tar\\.gz|tgz)$")) {
print name "\t" url
exit
}
}
name = ""
}
printf "%s\n" "$release_json" | awk -v target="$target" '
BEGIN { in_assets=0; name="" }
/"assets":[[:space:]]*\[/ { in_assets=1; next }
in_assets && /^[[:space:]]*\],?$/ { exit }
!in_assets { next }
/"name":/ {
if (name == "") {
name = $0
sub(/^.*"name":[[:space:]]*"/, "", name)
sub(/".*$/, "", name)
}
next
}
/"browser_download_url":/ {
if (name != "") {
url = $0
sub(/^.*"browser_download_url":[[:space:]]*"/, "", url)
sub(/".*$/, "", url)
if (name ~ ("-" target "\\.(tar\\.gz|tgz)$")) {
print name "\t" url
exit
}
}
name = ""
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@install.sh` around lines 136 - 157, The awk parser in the
resolve_release_asset() install script is capturing the release-level "name"
before entering the assets[] array so the first asset is skipped; change the awk
logic inside the shown block to only record "name" when inside the assets array
(e.g., track an assets flag set when a /"assets":/ line is seen and clear it
after the corresponding array ends), use that flag when matching /"name":/ and
/"browser_download_url":/ (variables name and url) and ensure name is reset
after emitting a match; this ensures the first asset's name/url pair is
selectable.

'
}

check_sudo_needed() {
local path parent
for path in "$@"; do
if [ -e "$path" ]; then
[ -w "$path" ] || return 0
else
parent="$(dirname "$path")"
[ -w "$parent" ] || return 0
fi
done
return 1
}

run_priv() {
if [ "${USE_SUDO:-0}" = "1" ]; then
sudo "$@"
else
"$@"
fi
}

parse_args() {
while [ $# -gt 0 ]; do
case "$1" in
--system) INSTALL_MODE="system" ;;
--user) INSTALL_MODE="user" ;;
-h | --help)
cat <<EOF
Usage: install.sh [--user|--system]
Install mode (Linux):
--user Install to user paths (default):
- binary: NDK_INSTALL_DIR (default: ~/.local/bin)
- libs: NDK_USER_LIB_DIR (default: ~/.local/lib)
--system Install to system paths:
- binary: NDK_SYSTEM_BIN_DIR (default: /usr/bin)
- libs: NDK_SYSTEM_LIB_DIR (default: /usr/lib)
Env vars:
NDK_VERSION Version or tag. Also supports latest, latest-pre.
NDK_PRERELEASE When truthy and NDK_VERSION=latest, picks latest pre-release.
NDK_INSTALL_MODE user|system (Linux)
EOF
exit 0
;;
*) err "unknown argument: $1" ;;
esac
shift
done
}

# --- main ---
main() {
parse_args "$@"

need curl
need tar

local target tag release_json asset_name download_url tmp_dir extract_dir
local bin_src lib_src lib_installed
Comment on lines +219 to +220
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /tmp && git ls-files | grep -E "install\.sh$"

Repository: relaystr/ndk

Length of output: 124


🏁 Script executed:

head -250 install.sh | tail -50

Repository: relaystr/ndk

Length of output: 1866


🏁 Script executed:

# Let's examine the trap setup and main function structure
sed -n '200,250p' install.sh

Repository: relaystr/ndk

Length of output: 1877


🏁 Script executed:

# Check how main() is called
grep -n "^main\|^.*main(" install.sh | head -20

Repository: relaystr/ndk

Length of output: 82


🏁 Script executed:

# Look for other trap definitions
grep -n "trap" install.sh

Repository: relaystr/ndk

Length of output: 95


🏁 Script executed:

# Check script structure and sourcing
head -50 install.sh

Repository: relaystr/ndk

Length of output: 1220


🏁 Script executed:

# Check if there's any cleanup at the end of main() before return
sed -n '208,320p' install.sh | tail -50

Repository: relaystr/ndk

Length of output: 1555


🏁 Script executed:

# Look for the actual end of main() function
awk '/^main\(\) {/,/^}/ {print NR": "$0}' install.sh | tail -30

Repository: relaystr/ndk

Length of output: 1093


🏁 Script executed:

# Let's verify the trap behavior with a test to confirm the bug
cat > /tmp/trap_test.sh << 'EOF'
#!/bin/bash

test_local_trap() {
  local tmp_dir
  tmp_dir="$(mktemp -d)"
  echo "Created temp dir: $tmp_dir"
  trap 'rm -rf "${tmp_dir:-}"' EXIT
  # Function returns, tmp_dir goes out of scope
}

test_global_trap() {
  TMP_DIR=""
  TMP_DIR="$(mktemp -d)"
  echo "Created global temp dir: $TMP_DIR"
  trap 'rm -rf "${TMP_DIR:-}"' EXIT
}

echo "Testing local variable trap:"
test_local_trap
echo "After function returns"
ls /tmp/tmp.* 2>/dev/null || echo "No temp dirs left (bug not present)"

# Clean up for next test
trap - EXIT

echo ""
echo "Testing global variable trap:"
test_global_trap
EOF

chmod +x /tmp/trap_test.sh
bash /tmp/trap_test.sh

Repository: relaystr/ndk

Length of output: 221


🏁 Script executed:

# Verify the script is called at the end (line 313)
tail -5 install.sh

Repository: relaystr/ndk

Length of output: 85


🏁 Script executed:

# Check if temp dirs from the test still exist
ls -la /tmp/tmp.* 2>&1 | head -20

Repository: relaystr/ndk

Length of output: 162


🏁 Script executed:

# Verify the proposed fix approach works correctly by checking the global scope
grep -n "SYSTEM_LIB_DIR\|^[A-Z_]*=" install.sh | head -15

Repository: relaystr/ndk

Length of output: 796


Temporary directory cleanup fails due to local variable going out of scope when main() returns.

When main() returns, the local tmp_dir variable is unset. The EXIT trap then runs at shell exit and attempts to expand ${tmp_dir:-}, which yields an empty string (since the variable is no longer in scope), causing rm -rf "" to silently do nothing. The temporary directory is leaked.

Declare TMP_DIR as a global variable and update all references (lines 228–231 and 233–237).

Proposed fix
 SYSTEM_LIB_DIR="${NDK_SYSTEM_LIB_DIR:-/usr/lib}"
+TMP_DIR=""

 main() {
   parse_args "$@"

   need curl
   need tar

-  local target tag release_json asset_name download_url tmp_dir extract_dir
+  local target tag release_json asset_name download_url extract_dir
   local bin_src lib_src lib_installed

   target="$(detect_target)"
   tag="$(resolve_tag)"
   release_json="$(get_release_json "$tag")"

   IFS=$'\t' read -r asset_name download_url <<< "$(resolve_release_asset "$release_json" "$target")"
   [ -n "${asset_name:-}" ] || err "could not find archive asset matching '*-${target}.tar.gz' (or .tgz) for tag ${tag}"
   [ -n "${download_url:-}" ] || err "could not determine asset download URL for tag ${tag}"

   info "installing ${BIN_NAME} ${tag} (${target})"
   info "from ${asset_name}"

-  tmp_dir="$(mktemp -d)"
-  extract_dir="${tmp_dir}/extract"
-  trap 'rm -rf "${tmp_dir:-}"' EXIT
+  TMP_DIR="$(mktemp -d)"
+  extract_dir="${TMP_DIR}/extract"
+  trap 'rm -rf "${TMP_DIR:-}"' EXIT
   mkdir -p "$extract_dir"

-  curl -fsSL --progress-bar "$download_url" -o "${tmp_dir}/${asset_name}" \
+  curl -fsSL --progress-bar "$download_url" -o "${TMP_DIR}/${asset_name}" \
     || err "download failed — check that release asset exists: ${download_url}"

-  tar -xzf "${tmp_dir}/${asset_name}" -C "$extract_dir" \
+  tar -xzf "${TMP_DIR}/${asset_name}" -C "$extract_dir" \
     || err "failed to extract archive: ${asset_name}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local target tag release_json asset_name download_url tmp_dir extract_dir
local bin_src lib_src lib_installed
SYSTEM_LIB_DIR="${NDK_SYSTEM_LIB_DIR:-/usr/lib}"
TMP_DIR=""
main() {
parse_args "$@"
need curl
need tar
local target tag release_json asset_name download_url extract_dir
local bin_src lib_src lib_installed
target="$(detect_target)"
tag="$(resolve_tag)"
release_json="$(get_release_json "$tag")"
IFS=$'\t' read -r asset_name download_url <<< "$(resolve_release_asset "$release_json" "$target")"
[ -n "${asset_name:-}" ] || err "could not find archive asset matching '*-${target}.tar.gz' (or .tgz) for tag ${tag}"
[ -n "${download_url:-}" ] || err "could not determine asset download URL for tag ${tag}"
info "installing ${BIN_NAME} ${tag} (${target})"
info "from ${asset_name}"
TMP_DIR="$(mktemp -d)"
extract_dir="${TMP_DIR}/extract"
trap 'rm -rf "${TMP_DIR:-}"' EXIT
mkdir -p "$extract_dir"
curl -fsSL --progress-bar "$download_url" -o "${TMP_DIR}/${asset_name}" \
|| err "download failed — check that release asset exists: ${download_url}"
tar -xzf "${TMP_DIR}/${asset_name}" -C "$extract_dir" \
|| err "failed to extract archive: ${asset_name}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@install.sh` around lines 214 - 215, The temporary directory variable tmp_dir
is declared local inside main(), so the EXIT trap cannot see it when the shell
exits and the tmp dir is leaked; make TMP_DIR a global variable (e.g.,
initialize TMP_DIR='' near the top), remove tmp_dir from the local declaration,
set TMP_DIR in main() instead of tmp_dir, and update all uses (the EXIT trap
cleanup, and any references to tmp_dir in functions or code) to reference
TMP_DIR (e.g., rm -rf "${TMP_DIR:-}"). Ensure the trap uses the global TMP_DIR
and that TMP_DIR is assigned before any traps that rely on it run.


target="$(detect_target)"
tag="$(resolve_tag)"
release_json="$(get_release_json "$tag")"

IFS=$'\t' read -r asset_name download_url <<< "$(resolve_release_asset "$release_json" "$target")"
[ -n "${asset_name:-}" ] || err "could not find archive asset matching '*-${target}.tar.gz' (or .tgz) for tag ${tag}"
[ -n "${download_url:-}" ] || err "could not determine asset download URL for tag ${tag}"

info "installing ${BIN_NAME} ${tag} (${target})"
info "from ${asset_name}"

tmp_dir="$(mktemp -d)"
extract_dir="${tmp_dir}/extract"
trap 'rm -rf "${tmp_dir:-}"' EXIT
mkdir -p "$extract_dir"

curl -fsSL --progress-bar "$download_url" -o "${tmp_dir}/${asset_name}" \
|| err "download failed — check that release asset exists: ${download_url}"

tar -xzf "${tmp_dir}/${asset_name}" -C "$extract_dir" \
|| err "failed to extract archive: ${asset_name}"
Comment on lines +238 to +242
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Downloaded artifact is extracted and executed without integrity verification.

The installer currently trusts network-delivered assets before extraction and execution. Add checksum/signature verification first (and fail closed) to reduce supply-chain risk.

💡 Proposed hardening direction
+  # Verify archive integrity before extraction/execution.
+  # Example: require a sidecar checksum asset and validate here.
+  # (Use sha256sum/shasum depending on platform.)
+  # err if checksum is unavailable or mismatched.

+  # Reject unsafe tar paths before extraction.
+  tar -tzf "${TMP_DIR}/${asset_name}" \
+    | awk '($0 ~ /^\//) || ($0 ~ /(^|\/)\.\.(\/|$)/) { bad=1 } END { exit bad }' \
+    || err "archive contains unsafe paths"

Also applies to: 246-250

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@install.sh` around lines 233 - 237, Downloaded artifacts are extracted and
executed without integrity checks; add strict checksum/signature verification
before extraction. After downloading "${tmp_dir}/${asset_name}" from
"$download_url", download or obtain the corresponding checksum/signature file
and verify the artifact (e.g., validate SHA256 with sha256sum --check or verify
a detached GPG signature) and make the script exit non-zero on any mismatch or
verification failure; only proceed to tar -xzf into "$extract_dir" when
verification passes. Apply the same verification guard to the other
download/extract block that also uses
download_url/tmp_dir/asset_name/extract_dir.


bin_src="${extract_dir}/bin/${BIN_NAME}"
[ -f "$bin_src" ] || err "archive does not contain expected binary: bin/${BIN_NAME}"

chmod +x "$bin_src"

# quick sanity check (make bundled libs discoverable before install)
# not all builds implement --version, so accept --help or default invocation too
if ! LD_LIBRARY_PATH="${extract_dir}/lib:${LD_LIBRARY_PATH:-}" "$bin_src" --version &>/dev/null; then
if ! LD_LIBRARY_PATH="${extract_dir}/lib:${LD_LIBRARY_PATH:-}" "$bin_src" --help &>/dev/null; then
LD_LIBRARY_PATH="${extract_dir}/lib:${LD_LIBRARY_PATH:-}" "$bin_src" &>/dev/null \
|| err "downloaded binary failed to run — wrong platform?"
fi
fi

if [ "${target#linux-}" != "$target" ] && [ "$INSTALL_MODE" = "system" ]; then
USE_SUDO="0"
if [ "$(id -u)" -ne 0 ] && check_sudo_needed "$SYSTEM_BIN_DIR" "$SYSTEM_LIB_DIR"; then
need sudo
USE_SUDO="1"
fi

run_priv mkdir -p "$SYSTEM_BIN_DIR" "$SYSTEM_LIB_DIR"
run_priv install -m 0755 "$bin_src" "${SYSTEM_BIN_DIR}/${BIN_NAME}"

lib_installed="0"
for lib_src in "${extract_dir}/lib"/*; do
[ -f "$lib_src" ] || continue
run_priv install -m 0644 "$lib_src" "${SYSTEM_LIB_DIR}/$(basename "$lib_src")"
lib_installed="1"
done

[ "$lib_installed" = "1" ] || err "archive does not contain shared libraries under lib/"
run_priv ldconfig || true

ok "${BIN_NAME} installed to ${SYSTEM_BIN_DIR}/${BIN_NAME}"
ok "shared libraries installed to ${SYSTEM_LIB_DIR}"
return
fi
Comment on lines +258 to +281
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

--system is silently ignored on non-Linux targets.

If users pass --system on macOS, the script falls back to user install without warning. It should error explicitly.

💡 Proposed fix
   target="$(detect_target)"
+  if [ "$INSTALL_MODE" = "system" ] && [ "${target#linux-}" = "$target" ]; then
+    err "--system install mode is only supported on Linux"
+  fi
   tag="$(resolve_tag)"
   release_json="$(get_release_json "$tag")"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@install.sh` around lines 253 - 276, The script silently downgrades a
requested system install on non-Linux targets because the linux-only check ([
"${target#linux-}" != "$target" ]) prevents honoring INSTALL_MODE="system";
update the logic that handles INSTALL_MODE and target to explicitly fail when
INSTALL_MODE is "system" but the detected target is not Linux: when
INSTALL_MODE="system" and target does not start with "linux-" call err with a
clear message (mentioning INSTALL_MODE and BIN_NAME) and exit, instead of
falling back to a user install; adjust the conditional around the existing linux
branch (the block using run_priv, SYSTEM_BIN_DIR, SYSTEM_LIB_DIR, lib_installed
and ldconfig) so it only runs on Linux and add the explicit err path for
non-Linux system installs.


# user install (default)
mkdir -p "$INSTALL_DIR"
mkdir -p "$USER_LIB_DIR"
install -m 0755 "$bin_src" "${INSTALL_DIR}/${BIN_NAME}"

lib_installed="0"
for lib_src in "${extract_dir}/lib"/*; do
[ -f "$lib_src" ] || continue
install -m 0644 "$lib_src" "${USER_LIB_DIR}/$(basename "$lib_src")"
lib_installed="1"
done

[ "$lib_installed" = "1" ] || err "archive does not contain shared libraries under lib/"

ok "${BIN_NAME} installed to ${INSTALL_DIR}/${BIN_NAME}"
ok "shared libraries installed to ${USER_LIB_DIR}"

# PATH hint
if ! echo "$PATH" | grep -q "$INSTALL_DIR"; then
echo ""
echo " Add this to your shell profile (~/.bashrc / ~/.zshrc):"
echo ""
echo " export PATH=\"\$PATH:${INSTALL_DIR}\""
echo ""
fi

}

main "$@"
Loading