diff --git a/hetzner/lib/common.sh b/hetzner/lib/common.sh
index 57e93bce..55f27d3e 100755
--- a/hetzner/lib/common.sh
+++ b/hetzner/lib/common.sh
@@ -2,242 +2,24 @@
# Common bash functions for Hetzner Cloud spawn scripts
# ============================================================
-# Provider-agnostic functions (shared with sprite/lib/common.sh)
+# Provider-agnostic functions
# ============================================================
-# Colors for output
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-NC='\033[0m' # No Color
-
-# Print colored message (to stderr so they don't pollute command substitution output)
-log_info() {
- echo -e "${GREEN}$1${NC}" >&2
+# Source shared provider-agnostic functions
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/../../shared/common.sh" || {
+ echo "ERROR: Failed to load shared/common.sh" >&2
+ exit 1
}
-log_warn() {
- echo -e "${YELLOW}$1${NC}" >&2
-}
-
-log_error() {
- echo -e "${RED}$1${NC}" >&2
-}
-
-# Safe read function that works in both interactive and non-interactive modes
-safe_read() {
- local prompt="$1"
- local result=""
-
- if [[ -t 0 ]]; then
- # stdin is a terminal - read directly
- read -p "$prompt" result
- elif echo -n "" > /dev/tty 2>/dev/null; then
- # /dev/tty is functional - use it
- read -p "$prompt" result < /dev/tty
- else
- # No interactive input available
- log_error "Cannot read input: no TTY available"
- return 1
- fi
-
- echo "$result"
-}
-
-# Listen on a port with netcat (handles busybox/Termux nc requiring -p flag)
-nc_listen() {
- local port=$1
- shift
- # Detect if nc requires -p flag (busybox nc on Termux)
- if nc --help 2>&1 | grep -q "BusyBox\|busybox" || nc --help 2>&1 | grep -q "\-p "; then
- nc -l -p "$port" "$@"
- else
- nc -l "$port" "$@"
- fi
-}
-
-# Open browser to URL (supports macOS, Linux, Termux)
-open_browser() {
- local url=$1
- if command -v termux-open-url &> /dev/null; then
- termux-open-url "$url" /dev/null; then
- open "$url" /dev/null; then
- xdg-open "$url" /dev/null; then
- log_warn "netcat (nc) not found - OAuth server unavailable"
- return 1
- fi
-
- local callback_url="http://localhost:${callback_port}/callback"
- local auth_url="https://openrouter.ai/auth?callback_url=${callback_url}"
-
- # Create a temporary directory for the OAuth flow
- local oauth_dir=$(mktemp -d)
- local code_file="$oauth_dir/code"
-
- log_warn "Starting local OAuth server on port ${callback_port}..."
-
- # Use a simpler nc approach - pipe response while capturing request
- (
- local success_response='HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n
Authentication Successful!
Redirecting back to terminal...
This tab will close automatically
'
-
- while true; do
- # Listen and capture just the first line of the request, then respond
- local response_file=$(mktemp)
- echo -e "$success_response" > "$response_file"
-
- local request=$(nc_listen "$callback_port" < "$response_file" 2>/dev/null | head -1)
- local nc_status=$?
- rm -f "$response_file"
-
- # If nc failed, exit the loop
- if [[ $nc_status -ne 0 ]]; then
- break
- fi
-
- if [[ "$request" == *"/callback?code="* ]]; then
- local code=$(echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p')
- echo "$code" > "$code_file"
- break
- fi
- done
- ) /dev/null; then
- log_warn "Failed to start OAuth server (port may be in use)"
- rm -rf "$oauth_dir"
- return 1
- fi
-
- # Open browser
- log_warn "Opening browser to authenticate with OpenRouter..."
- open_browser "$auth_url"
-
- # Wait for the code file to be created (timeout after 2 minutes)
- local timeout=120
- local elapsed=0
- while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do
- sleep 1
- ((elapsed++))
- done
-
- # Kill the background server process
- kill $server_pid 2>/dev/null || true
- wait $server_pid 2>/dev/null || true
-
- if [[ ! -f "$code_file" ]]; then
- log_warn "OAuth timeout - no response received"
- rm -rf "$oauth_dir"
- return 1
- fi
-
- local oauth_code=$(cat "$code_file")
- rm -rf "$oauth_dir"
-
- # Exchange the code for an API key
- log_warn "Exchanging OAuth code for API key..."
- local key_response=$(curl -s -X POST "https://openrouter.ai/api/v1/auth/keys" \
- -H "Content-Type: application/json" \
- -d "{\"code\": \"$oauth_code\"}")
-
- local api_key=$(echo "$key_response" | grep -o '"key":"[^"]*"' | sed 's/"key":"//;s/"$//')
-
- if [[ -z "$api_key" ]]; then
- log_error "Failed to exchange OAuth code: ${key_response}"
- return 1
- fi
-
- log_info "Successfully obtained OpenRouter API key via OAuth!"
- echo "$api_key"
-}
-
-# Main function: Try OAuth, fallback to manual entry
-get_openrouter_api_key_oauth() {
- local callback_port=${1:-5180}
-
- # Try OAuth flow first
- local api_key=$(try_oauth_flow "$callback_port")
-
- if [[ -n "$api_key" ]]; then
- echo "$api_key"
- return 0
- fi
-
- # OAuth failed, offer manual entry
- echo ""
- log_warn "OAuth authentication failed or unavailable"
- log_warn "You can enter your API key manually instead"
- echo ""
- local manual_choice=$(safe_read "Would you like to enter your API key manually? (Y/n): ") || {
- log_error "Cannot prompt for manual entry in non-interactive mode"
- log_warn "Set OPENROUTER_API_KEY environment variable for non-interactive usage"
- return 1
- }
-
- if [[ ! "$manual_choice" =~ ^[Nn]$ ]]; then
- api_key=$(get_openrouter_api_key_manual)
- echo "$api_key"
- return 0
- else
- log_error "Authentication cancelled by user"
- return 1
- fi
-}
+# Note: Provider-agnostic functions (logging, OAuth, browser, nc_listen) are now in shared/common.sh
# ============================================================
# Hetzner Cloud specific functions
# ============================================================
-HETZNER_API_BASE="https://api.hetzner.cloud/v1"
-SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -i $HOME/.ssh/id_ed25519"
+readonly HETZNER_API_BASE="https://api.hetzner.cloud/v1"
+readonly SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -i $HOME/.ssh/id_ed25519"
# Centralized curl wrapper for Hetzner API
hetzner_api() {
@@ -454,7 +236,7 @@ verify_server_connectivity() {
local attempt=1
log_warn "Waiting for SSH connectivity to $ip..."
- while [[ $attempt -le $max_attempts ]]; do
+ while [[ "$attempt" -le "$max_attempts" ]]; do
if ssh $SSH_OPTS -o ConnectTimeout=5 "root@$ip" "echo ok" >/dev/null 2>&1; then
log_info "SSH connection established"
return 0
@@ -475,7 +257,7 @@ wait_for_cloud_init() {
local attempt=1
log_warn "Waiting for cloud-init to complete..."
- while [[ $attempt -le $max_attempts ]]; do
+ while [[ "$attempt" -le "$max_attempts" ]]; do
if ssh $SSH_OPTS "root@$ip" "test -f /root/.cloud-init-complete" >/dev/null 2>&1; then
log_info "Cloud-init completed"
return 0