Skip to content

Commit 17100ba

Browse files
authored
feat: add install.sh script (#193)
* feat: add install.sh script * chore: add missing end of file
1 parent 0c35010 commit 17100ba

1 file changed

Lines changed: 308 additions & 0 deletions

File tree

scripts/install.sh

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
#!/usr/bin/env bash
2+
set -e # Exit immediately if a command exits with a non-zero status
3+
set -u # Treat unset variables as an error
4+
set -o pipefail # Catch errors in piped commands
5+
6+
#=============================================================================
7+
# HeroDevs CLI Installer
8+
#
9+
# This script installs the HeroDevs CLI by downloading the appropriate tarball
10+
# from GitHub releases and setting it up for use on macOS and Linux systems.
11+
#
12+
# Design Decisions:
13+
# - Bootstrap pattern: Initial install via GitHub. Plan to add S3 support in the future for auto-updates.
14+
# - Symlink architecture: Separates executable path from installation files
15+
# - Non-root approach: User-level installation without admin privileges
16+
# - Shell compatibility: Works with Bash 3+ (macOS default and Linux)
17+
#
18+
# Security Considerations:
19+
# - HTTPS downloads for all components
20+
# - Timeout controls for network operations
21+
# - Proper cleanup of temporary files
22+
# - Error handling for failed operations
23+
#
24+
# Usage:
25+
# Beta release: curl -sSfL https://raw.githubusercontent.com/herodevs/cli/main/install.sh | bash
26+
# Latest release: curl -sSfL https://raw.githubusercontent.com/herodevs/cli/main/install.sh | bash -s -- --latest
27+
#=============================================================================
28+
29+
# Configuration
30+
REPO_OWNER="herodevs"
31+
REPO_NAME="cli"
32+
BIN_NAME="hd"
33+
INSTALL_DIR="$HOME/.herodevs"
34+
BIN_DIR="$INSTALL_DIR/bin"
35+
GITHUB_API_URL="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases"
36+
TMP_DIR=""
37+
DEBUG=${DEBUG:-}
38+
39+
# Colors for output
40+
GREEN='\033[0;32m'
41+
YELLOW='\033[0;33m'
42+
RED='\033[0;31m'
43+
PURPLE='\033[38;5;140m'
44+
BLUE='\033[0;34m'
45+
NC='\033[0m' # No Color
46+
47+
# Initialize logging system
48+
# Save original stdout to FD 3 so we can use it for program output
49+
exec 3>&1
50+
51+
# Central logging function
52+
log() {
53+
local level="$1"
54+
local message="$2"
55+
local timestamp
56+
timestamp=$(date +"%Y-%m-%d %H:%M:%S")
57+
58+
# All logs go to stderr (FD 2)
59+
case "$level" in
60+
INFO) echo -e "${timestamp} ${GREEN}[INFO]${NC} $message" >&2 ;;
61+
WARNING) echo -e "${timestamp} ${YELLOW}[WARNING]${NC} $message" >&2 ;;
62+
ERROR) echo -e "${timestamp} ${RED}[ERROR]${NC} $message" >&2 ;;
63+
DEBUG)
64+
if [ -n "$DEBUG" ]; then
65+
echo -e "${timestamp} ${BLUE}[DEBUG]${NC} $message" >&2
66+
fi
67+
;;
68+
esac
69+
}
70+
71+
# Function to output data (not logs) to the original stdout
72+
emit() {
73+
echo "$@" >&3
74+
}
75+
76+
# Function to exit with error
77+
error_exit() {
78+
log "ERROR" "$1"
79+
exit 1
80+
}
81+
82+
# Parse arguments
83+
USE_BETA=true
84+
while [ $# -gt 0 ]; do
85+
case $1 in
86+
-l | --latest)
87+
USE_BETA=false
88+
shift
89+
;;
90+
-h | --help)
91+
# Help text goes to the original stdout
92+
emit "Usage: $0 [-l|--latest]"
93+
emit " -l, --latest Install latest release (default: install beta)"
94+
emit " -h, --help Show this help message"
95+
exit 0
96+
;;
97+
*)
98+
error_exit "Unknown option: $1"
99+
;;
100+
esac
101+
done
102+
103+
# Cleanup on exit/interrupt
104+
cleanup() {
105+
if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
106+
log "DEBUG" "Cleaning up temporary directory: $TMP_DIR"
107+
rm -rf "$TMP_DIR"
108+
fi
109+
}
110+
111+
trap cleanup EXIT INT TERM
112+
113+
log "INFO" "Installing HeroDevs CLI"
114+
115+
# Get release version (beta or latest)
116+
get_version() {
117+
local use_beta="$1"
118+
local releases_data="$2"
119+
120+
log "INFO" "Extracting release version"
121+
local all_tags
122+
local latest_release
123+
local beta_release
124+
local version_tag
125+
126+
# Split complex command chains for Bash 3 compatibility
127+
all_tags=$(echo "$releases_data" | grep -o '"tag_name": "[^"]*"')
128+
all_tags=$(echo "$all_tags" | cut -d'"' -f4)
129+
130+
# Get latest non-beta release
131+
latest_release=$(echo "$all_tags" | grep -v "beta" | head -n 1)
132+
133+
# Get latest beta release
134+
beta_release=$(echo "$all_tags" | grep "beta" | head -n 1)
135+
136+
log "DEBUG" "All tags: $all_tags"
137+
log "DEBUG" "Latest release: $latest_release"
138+
log "DEBUG" "Beta release: $beta_release"
139+
140+
if [ "$use_beta" = "true" ]; then
141+
version_tag="$beta_release"
142+
if [ -z "$version_tag" ]; then
143+
log "ERROR" "No beta release found. Please try again later or use --latest to install the latest stable release."
144+
exit 1
145+
fi
146+
else
147+
version_tag="$latest_release"
148+
if [ -z "$version_tag" ]; then
149+
error_exit "No latest release found. Please try again later."
150+
fi
151+
fi
152+
153+
log "INFO" "Using version: $version_tag"
154+
# Output actual return value to the original stdout (FD 3)
155+
emit "$version_tag"
156+
}
157+
158+
# Download and install
159+
install() {
160+
local version_tag="$1"
161+
log "INFO" "Downloading and installing tarball"
162+
163+
# Remove 'v' prefix if present
164+
local version
165+
version=${version_tag#v}
166+
167+
log "DEBUG" "Version string: $version"
168+
169+
# Detect system
170+
local os
171+
local arch
172+
173+
os=$(uname -s | tr '[:upper:]' '[:lower:]')
174+
arch=$(uname -m)
175+
176+
if [ "$arch" = "x86_64" ]; then
177+
arch="x64"
178+
fi
179+
if [ "$arch" = "aarch64" ]; then
180+
arch="arm64"
181+
fi
182+
183+
log "INFO" "Detected system: $os-$arch"
184+
185+
local tarball_name="${REPO_NAME}-${version}-${os}-${arch}.tar.gz"
186+
local download_url="https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${version_tag}/${tarball_name}"
187+
188+
log "DEBUG" "Download URL: $download_url"
189+
190+
# Check for existing installation
191+
if [ -d "$INSTALL_DIR" ]; then
192+
log "INFO" "Updating existing installation in $INSTALL_DIR"
193+
else
194+
log "INFO" "Installing to $INSTALL_DIR"
195+
mkdir -p "$INSTALL_DIR"
196+
fi
197+
198+
mkdir -p "$BIN_DIR"
199+
200+
# Create temp dir and download
201+
TMP_DIR=$(mktemp -d)
202+
log "INFO" "Downloading ${os}-${arch} tarball..."
203+
log "DEBUG" "Using temporary directory: $TMP_DIR"
204+
205+
# Split command and capture output separately for Bash 3 compatibility
206+
local curl_output
207+
curl_output=$(curl -L --connect-timeout 10 --max-time 120 "$download_url" -o "$TMP_DIR/$tarball_name" 2>&1)
208+
local curl_status=$?
209+
210+
if [ $curl_status -ne 0 ]; then
211+
error_exit "Failed to download from $download_url: $curl_output"
212+
fi
213+
214+
# Extract and set up
215+
log "INFO" "Extracting..."
216+
tar -xzf "$TMP_DIR/$tarball_name" -C "$INSTALL_DIR"
217+
local tar_status=$?
218+
219+
if [ $tar_status -ne 0 ]; then
220+
error_exit "Failed to extract tarball"
221+
fi
222+
223+
# Create symlink in bin directory
224+
log "DEBUG" "Creating symlink from $INSTALL_DIR/$BIN_NAME to $BIN_DIR/$BIN_NAME"
225+
ln -sf "$INSTALL_DIR/$BIN_NAME" "$BIN_DIR/$BIN_NAME"
226+
227+
# Add to PATH if needed
228+
if ! echo "$PATH" | tr ':' '\n' | grep -q "^$BIN_DIR$"; then
229+
log "DEBUG" "BIN_DIR not found in PATH, adding it"
230+
231+
local profile_file=""
232+
if [ -f "$HOME/.zshrc" ]; then
233+
profile_file="$HOME/.zshrc"
234+
elif [ -f "$HOME/.bashrc" ]; then
235+
profile_file="$HOME/.bashrc"
236+
elif [ -f "$HOME/.bash_profile" ]; then
237+
profile_file="$HOME/.bash_profile"
238+
fi
239+
240+
if [ -n "$profile_file" ]; then
241+
echo "export PATH=\"\$PATH:$BIN_DIR\"" >> "$profile_file"
242+
log "INFO" "Added $BIN_DIR to PATH in $profile_file"
243+
log "WARNING" "Please restart your terminal or run 'source $profile_file' to update your PATH"
244+
else
245+
log "WARNING" "Could not find shell profile. Please add $BIN_DIR to your PATH manually:"
246+
emit " export PATH=\"\$PATH:$BIN_DIR\""
247+
fi
248+
fi
249+
250+
log "INFO" "Installation complete! You can now run: $BIN_NAME --help"
251+
emit "The CLI will automatically check for updates from S3 when run."
252+
}
253+
254+
check_dependencies() {
255+
log "INFO" "Checking dependencies"
256+
if ! command -v curl >/dev/null 2>&1; then
257+
error_exit "curl is required but not installed"
258+
fi
259+
if ! command -v tar >/dev/null 2>&1; then
260+
error_exit "tar is required but not installed"
261+
fi
262+
}
263+
264+
fetch_release_and_set_version() {
265+
log "INFO" "Fetching releases from GitHub API"
266+
log "DEBUG" "Attempting to fetch from: $GITHUB_API_URL"
267+
268+
local releases
269+
local curl_exit
270+
271+
releases=$(curl --silent --connect-timeout 10 --max-time 30 "$GITHUB_API_URL" 2>&1)
272+
curl_exit=$?
273+
274+
if [ $curl_exit -ne 0 ]; then
275+
error_exit "Failed to fetch releases from GitHub API: $releases"
276+
fi
277+
278+
# Validate the response is not empty and contains release data
279+
if [ -z "$releases" ]; then
280+
error_exit "Empty response from GitHub API. Please try again later."
281+
fi
282+
283+
if ! echo "$releases" | grep -q '"releases"\|"tag_name"'; then
284+
error_exit "Invalid response from GitHub API. Please try again later."
285+
fi
286+
287+
# Store the output in a variable (coming from FD 3 via the emit in get_version)
288+
VERSION_TAG=$(get_version "$USE_BETA" "$releases")
289+
}
290+
291+
check_dependencies
292+
293+
fetch_release_and_set_version
294+
295+
install "$VERSION_TAG"
296+
297+
if [ -n "$DEBUG" ]; then
298+
emit -e "${PURPLE}"
299+
emit " ――――――――――――――――――――――――――――――――――――――――――――――――――――"
300+
emit " @herodevs/cli installed 🎉🎉🎉"
301+
emit " 👻"
302+
emit " Finding EOL deps before they come back to haunt you"
303+
emit " ――――――――――――――――――――――――――――――――――――――――――――――――――――"
304+
emit -e "${NC}"
305+
fi
306+
307+
# Restore stdout
308+
exec 1>&3 3>&-

0 commit comments

Comments
 (0)