-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·254 lines (209 loc) · 7.34 KB
/
install.sh
File metadata and controls
executable file
·254 lines (209 loc) · 7.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
#!/usr/bin/env bash
set -e # Exit immediately if a command exits with a non-zero status
set -u # Treat unset variables as an error
set -o pipefail # Catch errors in piped commands
#=============================================================================
# HeroDevs CLI Installer
#
# This script installs the HeroDevs CLI by downloading the appropriate tarball
# from GitHub releases and setting it up for use on macOS and Linux systems.
#
# Design Decisions:
# - Bootstrap pattern: Initial install via GitHub. Plan to add S3 support in the future for auto-updates.
# - Symlink architecture: Separates executable path from installation files
# - Non-root approach: User-level installation without admin privileges
# - Shell compatibility: Works with Bash 3+ (macOS default and Linux)
#
# Security Considerations:
# - HTTPS downloads for all components
# - Timeout controls for network operations
# - Proper cleanup of temporary files
# - Error handling for failed operations
#
# Usage: curl -sSfL https://raw.githubusercontent.com/herodevs/cli/refs/heads/main/scripts/install.sh | bash
#=============================================================================
# Configuration
REPO_OWNER="herodevs"
REPO_NAME="cli"
BIN_NAME="hd"
INSTALL_DIR="$HOME/.herodevs"
BIN_DIR="$INSTALL_DIR/bin"
GITHUB_API_URL="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases"
TMP_DIR=""
LATEST_VERSION="v2.0.0"
DEBUG=${DEBUG:-}
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
PURPLE='\033[38;5;140m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Initialize logging system
# Save original stdout to FD 3 so we can use it for program output
exec 3>&1
# Central logging function
log() {
local level="$1"
local message="$2"
local timestamp
timestamp=$(date +"%Y-%m-%d %H:%M:%S")
# All logs go to stderr (FD 2)
case "$level" in
INFO) echo -e "${timestamp} ${GREEN}[INFO]${NC} $message" >&2 ;;
WARNING) echo -e "${timestamp} ${YELLOW}[WARNING]${NC} $message" >&2 ;;
ERROR) echo -e "${timestamp} ${RED}[ERROR]${NC} $message" >&2 ;;
DEBUG)
if [ -n "$DEBUG" ]; then
echo -e "${timestamp} ${BLUE}[DEBUG]${NC} $message" >&2
fi
;;
esac
}
# Function to output data (not logs) to the original stdout
emit() {
echo "$@" >&3
}
# Function to exit with error
error_exit() {
log "ERROR" "$1"
exit 1
}
# Cleanup on exit/interrupt
cleanup() {
if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
log "DEBUG" "Cleaning up temporary directory: $TMP_DIR"
rm -rf "$TMP_DIR"
fi
}
trap cleanup EXIT INT TERM
log "INFO" "Installing HeroDevs CLI"
# Download and install
install() {
# Fetch releases from GitHub
releases=$(curl --silent --connect-timeout 10 --max-time 30 "$GITHUB_API_URL" 2>&1)
curl_exit=$?
if [ $curl_exit -ne 0 ]; then
error_exit "Failed to fetch releases from GitHub API: $releases"
fi
# Validate the response is not empty
if [ -z "$releases" ]; then
error_exit "Empty response from GitHub API. Please try again later."
fi
local version_tag="$LATEST_VERSION"
# Remove 'v' prefix if present
local version
version=${version_tag#v}
log "INFO" "Installing version: $version"
# Detect system
local os
local arch
os=$(uname -s | tr '[:upper:]' '[:lower:]')
arch=$(uname -m)
if [ "$arch" = "x86_64" ]; then
arch="x64"
fi
if [ "$arch" = "aarch64" ]; then
arch="arm64"
fi
log "INFO" "Detected system: $os-$arch"
# Construct OS-ARCH pattern
pattern="${os}-${arch}"
# Extract all browser_download_url values for the matching release
release_block=$(echo "$releases" | awk -v tag="\"$LATEST_VERSION\"" '
BEGIN {found=0; block=""}
$0 ~ tag {found=1}
found {
block = block $0 "\n"
if ($0 ~ /^\s*\],?$/) { found=0; print block; exit }
}
')
# Parse browser_download_url from release_block and check against pattern
download_url=""
while read -r url; do
if echo "$url" | grep -qi "$pattern"; then
download_url="$url"
break
fi
done < <(echo "$release_block" | grep '"browser_download_url":' | sed -E 's/.*"browser_download_url": "([^"]+)".*/\1/')
# Ensure we found a matching asset
if [ -z "$download_url" ]; then
error_exit "No release found system $pattern for release $LATEST_VERSION"
fi
log "DEBUG" "Download URL: $download_url"
log "INFO" "Downloading and installing tarball"
local tarball_name="${REPO_NAME}-${version}-${os}-${arch}.tar.gz"
# Check for existing installation
if [ -d "$INSTALL_DIR" ]; then
log "INFO" "Updating existing installation in $INSTALL_DIR"
else
log "INFO" "Installing to $INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
fi
mkdir -p "$BIN_DIR"
# Create temp dir and download
TMP_DIR=$(mktemp -d)
log "INFO" "Downloading ${os}-${arch} tarball..."
log "DEBUG" "Using temporary directory: $TMP_DIR"
# Split command and capture output separately for Bash 3 compatibility
local curl_output
curl_output=$(curl -L --connect-timeout 10 --max-time 120 "$download_url" -o "$TMP_DIR/$tarball_name" 2>&1)
local curl_status=$?
if [ $curl_status -ne 0 ]; then
error_exit "Failed to download from $download_url: $curl_output"
fi
# Extract and set up
log "INFO" "Extracting..."
tar -xzf "$TMP_DIR/$tarball_name" -C "$INSTALL_DIR"
local tar_status=$?
if [ $tar_status -ne 0 ]; then
error_exit "Failed to extract tarball"
fi
# Create symlink in bin directory
log "DEBUG" "Creating symlink from $INSTALL_DIR/$BIN_NAME to $BIN_DIR/$BIN_NAME"
ln -sf "$INSTALL_DIR/$BIN_NAME/bin/hd" "$BIN_DIR/$BIN_NAME"
# Add to PATH if needed
if ! echo "$PATH" | tr ':' '\n' | grep -q "^$BIN_DIR$"; then
log "DEBUG" "BIN_DIR not found in PATH, adding it"
local profile_file=""
if [ -f "$HOME/.zshrc" ]; then
profile_file="$HOME/.zshrc"
elif [ -f "$HOME/.bashrc" ]; then
profile_file="$HOME/.bashrc"
elif [ -f "$HOME/.bash_profile" ]; then
profile_file="$HOME/.bash_profile"
fi
if [ -n "$profile_file" ]; then
echo "export PATH=\"\$PATH:$BIN_DIR\"" >> "$profile_file"
log "INFO" "Added $BIN_DIR to PATH in $profile_file"
log "WARNING" "Please restart your terminal or run 'source $profile_file' to update your PATH"
else
log "WARNING" "Could not find shell profile. Please add $BIN_DIR to your PATH manually:"
emit "export PATH=\"$BIN_DIR:\$PATH:\""
fi
fi
log "INFO" "Installation complete! You can now run: $BIN_NAME --help"
emit "The CLI will automatically check for updates when run."
}
check_dependencies() {
log "INFO" "Checking dependencies"
if ! command -v curl >/dev/null 2>&1; then
error_exit "curl is required but not installed"
fi
if ! command -v tar >/dev/null 2>&1; then
error_exit "tar is required but not installed"
fi
}
check_dependencies
install
if [ -n "$DEBUG" ]; then
emit -e "${PURPLE}"
emit " ――――――――――――――――――――――――――――――――――――――――――――――――――――"
emit " @herodevs/cli installed 🎉🎉🎉"
emit " 👻"
emit " Finding EOL deps before they come back to haunt you"
emit " ――――――――――――――――――――――――――――――――――――――――――――――――――――"
emit -e "${NC}"
fi
# Restore stdout
exec 1>&3 3>&-