Browse Source

fix(update): add global installation support and path auto-detection (#230)

Resolves #224 — update.sh now mirrors install.sh path resolution:
- Auto-detects local (.opencode/) then global (~/.config/opencode) installs
- Adds --install-dir PATH flag and OPENCODE_BRANCH/OPENCODE_INSTALL_DIR env vars
- Adds trap for .backup cleanup on SIGINT/SIGTERM
- Fixes set -e + arithmetic counter bug (uses $((n+1)) form)
- Adds path traversal guard and write-permission check
- Adds RED color variable; fixes print_error hardcoded escape code
- Renames 'skipped' counter to 'failed' for accuracy
- README: adds one-liner Keep Updated section in Quick Start
Darren Hinde 1 month ago
parent
commit
1442740b4a
2 changed files with 314 additions and 45 deletions
  1. 8 0
      README.md
  2. 306 45
      update.sh

+ 8 - 0
README.md

@@ -133,6 +133,14 @@ curl -fsSL https://raw.githubusercontent.com/darrenhinde/OpenAgentsControl/main/
 bash install.sh
 ```
 
+### Keep Updated
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/darrenhinde/OpenAgentsControl/main/update.sh | bash
+```
+
+> Use `--install-dir PATH` if you installed to a custom location (e.g. `~/.config/opencode`).
+
 ### Step 2: Start Building
 
 ```bash

+ 306 - 45
update.sh

@@ -3,80 +3,341 @@
 #############################################################################
 # OpenAgents Control Updater
 # Updates existing OpenCode components to latest versions
+#
+# Compatible with:
+# - macOS (bash 3.2+)
+# - Linux (bash 3.2+)
+# - Windows (Git Bash, WSL)
+#
+# Usage:
+#   ./update.sh                          # Auto-detect install location
+#   ./update.sh --install-dir PATH       # Update a specific install path
+#
+# Environment variables:
+#   OPENCODE_INSTALL_DIR                 # Override default install directory
+#   OPENCODE_BRANCH                      # Branch to pull from (default: main)
 #############################################################################
 
 set -e
 
-# Colors
-GREEN='\033[0;32m'
-# YELLOW='\033[1;33m' # Unused
-BLUE='\033[0;34m'
-CYAN='\033[0;36m'
-BOLD='\033[1m'
-NC='\033[0m'
+# Detect platform
+PLATFORM="$(uname -s)"
+case "$PLATFORM" in
+    Linux*)     PLATFORM="Linux";;
+    Darwin*)    PLATFORM="macOS";;
+    CYGWIN*|MINGW*|MSYS*) PLATFORM="Windows";;
+    *)          PLATFORM="Unknown";;
+esac
 
-REPO_URL="https://raw.githubusercontent.com/darrenhinde/OpenAgentsControl/main"
-INSTALL_DIR=".opencode"
+# Colors (disable on Windows terminals without color support)
+if [ "$PLATFORM" = "Windows" ] && [ -z "$WT_SESSION" ] && [ -z "$ConEmuPID" ]; then
+    RED=''
+    GREEN=''
+    YELLOW=''
+    BLUE=''
+    CYAN=''
+    BOLD=''
+    NC=''
+else
+    RED='\033[0;31m'
+    GREEN='\033[0;32m'
+    YELLOW='\033[1;33m'
+    BLUE='\033[0;34m'
+    CYAN='\033[0;36m'
+    BOLD='\033[1m'
+    NC='\033[0m'
+fi
+
+BRANCH="${OPENCODE_BRANCH:-main}"
+REPO_URL="https://raw.githubusercontent.com/darrenhinde/OpenAgentsControl/${BRANCH}"
+
+# CLI argument for custom install dir (overrides env var)
+CUSTOM_INSTALL_DIR=""
+
+# Track backup files for cleanup on exit
+BACKUP_FILES=()
+
+# Clean up any leftover backup files on exit/interrupt
+cleanup_backups() {
+    for f in "${BACKUP_FILES[@]}"; do
+        [ -f "$f" ] && rm -f "$f"
+    done
+}
+trap cleanup_backups EXIT INT TERM
+
+#############################################################################
+# Utility Functions
+#############################################################################
 
 print_success() { echo -e "${GREEN}✓${NC} $1"; }
-print_info() { echo -e "${BLUE}ℹ${NC} $1"; }
-print_step() { echo -e "\n${CYAN}${BOLD}▶${NC} $1\n"; }
+print_info()    { echo -e "${BLUE}ℹ${NC} $1"; }
+print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
+print_error()   { echo -e "${RED}✗${NC} $1" >&2; }
+print_step()    { echo -e "\n${CYAN}${BOLD}▶${NC} $1\n"; }
 
 print_header() {
     echo -e "${CYAN}${BOLD}"
     echo "╔════════════════════════════════════════════════════════════════╗"
     echo "║                                                                ║"
-    echo "║           OpenAgents Control Updater v1.0.0                   ║"
+    echo "║           OpenAgents Control Updater v1.1.0                   ║"
     echo "║                                                                ║"
     echo "╚════════════════════════════════════════════════════════════════╝"
     echo -e "${NC}"
 }
 
-update_component() {
-    local path=$1
-    local url="${REPO_URL}/${path}"
-    
-    if [ ! -f "$path" ]; then
-        print_info "Skipping $path (not installed)"
+print_usage() {
+    echo "Usage: $0 [--install-dir PATH]"
+    echo ""
+    echo "Options:"
+    echo "  --install-dir PATH   Update a specific installation directory"
+    echo "  --help               Show this help message"
+    echo ""
+    echo "Environment variables:"
+    echo "  OPENCODE_INSTALL_DIR   Override the default installation directory"
+    echo "  OPENCODE_BRANCH        Branch to pull updates from (default: main)"
+    echo ""
+    echo "Examples:"
+    echo "  # Auto-detect and update"
+    echo "  $0"
+    echo ""
+    echo "  # Update a global installation"
+    echo "  $0 --install-dir ~/.config/opencode"
+    echo ""
+    echo "  # Update via environment variable"
+    echo "  export OPENCODE_INSTALL_DIR=~/.config/opencode && $0"
+}
+
+#############################################################################
+# Path Resolution
+#############################################################################
+
+get_global_install_path() {
+    # Return platform-appropriate global installation path
+    case "$PLATFORM" in
+        macOS)
+            echo "${HOME}/.config/opencode"
+            ;;
+        Linux)
+            echo "${HOME}/.config/opencode"
+            ;;
+        Windows)
+            # Windows Git Bash/WSL: Use same as Linux
+            echo "${HOME}/.config/opencode"
+            ;;
+        *)
+            echo "${HOME}/.config/opencode"
+            ;;
+    esac
+}
+
+normalize_path() {
+    local input_path="$1"
+
+    # Handle empty path
+    if [ -z "$input_path" ]; then
+        echo ""
+        return 1
+    fi
+
+    local normalized_path
+
+    # Expand tilde to $HOME (works on Linux, macOS, Windows Git Bash)
+    if [[ $input_path == ~* ]]; then
+        normalized_path="${HOME}${input_path:1}"
+    else
+        normalized_path="$input_path"
+    fi
+
+    # Convert backslashes to forward slashes (Windows compatibility)
+    normalized_path="${normalized_path//\\//}"
+
+    # Remove trailing slashes
+    normalized_path="${normalized_path%/}"
+
+    # If path is relative, make it absolute based on current directory
+    if [[ ! "$normalized_path" = /* ]] && [[ ! "$normalized_path" =~ ^[A-Za-z]: ]]; then
+        normalized_path="$(pwd)/${normalized_path}"
+    fi
+
+    echo "$normalized_path"
+    return 0
+}
+
+resolve_install_dir() {
+    local custom_dir="$1"
+
+    # Priority: CLI arg → env var → auto-detect (local then global)
+    if [ -n "$custom_dir" ]; then
+        normalize_path "$custom_dir"
         return
     fi
-    
-    # Backup existing file
-    cp "$path" "${path}.backup"
-    
-    if curl -fsSL "$url" -o "$path"; then
+
+    if [ -n "$OPENCODE_INSTALL_DIR" ]; then
+        normalize_path "$OPENCODE_INSTALL_DIR"
+        return
+    fi
+
+    # Auto-detect: prefer local project install, fall back to global
+    local local_path
+    local_path="$(pwd)/.opencode"
+    local global_path
+    global_path=$(get_global_install_path)
+
+    if [ -d "$local_path" ]; then
+        echo "$local_path"
+    elif [ -d "$global_path" ]; then
+        echo "$global_path"
+    else
+        # Neither exists — return local path so main() gives a clear error
+        echo "$local_path"
+    fi
+}
+
+#############################################################################
+# Update Logic
+#############################################################################
+
+update_component() {
+    local path="$1"
+    local install_dir="$2"
+    local relative_path="${path#"$install_dir"/}"
+
+    # Guard: reject paths that escaped the install dir
+    if [[ "$relative_path" == /* ]] || [[ "$relative_path" == *..* ]]; then
+        print_warning "Skipping suspicious path: $path"
+        return 1
+    fi
+
+    local url="${REPO_URL}/.opencode/${relative_path}"
+    local backup="${path}.backup"
+
+    cp "$path" "$backup"
+    BACKUP_FILES+=("$backup")
+
+    if curl -fsSL "$url" -o "$path" 2>/dev/null; then
         print_success "Updated $path"
-        rm "${path}.backup"
+        rm -f "$backup"
+        # Remove from tracking array (bash 3.2 compatible)
+        local new_backups=()
+        for f in "${BACKUP_FILES[@]}"; do
+            [ "$f" != "$backup" ] && new_backups+=("$f")
+        done
+        BACKUP_FILES=("${new_backups[@]+"${new_backups[@]}"}")
     else
-        print_info "Failed to update $path, restoring backup"
-        mv "${path}.backup" "$path"
+        print_warning "Could not update $path — restoring backup"
+        mv "$backup" "$path"
+        return 1
     fi
 }
 
+update_all_components() {
+    local install_dir="$1"
+    local updated=0
+    local failed=0
+
+    # Update markdown files
+    while IFS= read -r -d '' file; do
+        if update_component "$file" "$install_dir"; then
+            updated=$((updated + 1))
+        else
+            failed=$((failed + 1))
+        fi
+    done < <(find "$install_dir" -name "*.md" -type f -print0)
+
+    # Update TypeScript files
+    while IFS= read -r -d '' file; do
+        if update_component "$file" "$install_dir"; then
+            updated=$((updated + 1))
+        else
+            failed=$((failed + 1))
+        fi
+    done < <(find "$install_dir" -name "*.ts" -type f -not -path "*/node_modules/*" -print0)
+
+    # Update shell scripts inside install dir
+    while IFS= read -r -d '' file; do
+        if update_component "$file" "$install_dir"; then
+            updated=$((updated + 1))
+        else
+            failed=$((failed + 1))
+        fi
+    done < <(find "$install_dir" -name "*.sh" -type f -print0)
+
+    print_info "Updated: $updated file(s), failed: $failed file(s)"
+}
+
+#############################################################################
+# Argument Parsing
+#############################################################################
+
+parse_args() {
+    while [[ $# -gt 0 ]]; do
+        case "$1" in
+            --install-dir=*)
+                CUSTOM_INSTALL_DIR="${1#*=}"
+                if [ -z "$CUSTOM_INSTALL_DIR" ]; then
+                    print_error "--install-dir requires a non-empty path"
+                    exit 1
+                fi
+                shift
+                ;;
+            --install-dir)
+                if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
+                    CUSTOM_INSTALL_DIR="$2"
+                    shift 2
+                else
+                    print_error "--install-dir requires a path argument"
+                    exit 1
+                fi
+                ;;
+            --help|-h)
+                print_usage
+                exit 0
+                ;;
+            *)
+                print_error "Unknown option: $1"
+                print_usage
+                exit 1
+                ;;
+        esac
+    done
+}
+
+#############################################################################
+# Main
+#############################################################################
+
 main() {
+    parse_args "$@"
+
     print_header
-    
-    if [ ! -d "$INSTALL_DIR" ]; then
-        echo "Error: $INSTALL_DIR directory not found"
-        echo "Run install.sh first to install components"
+
+    local install_dir
+    install_dir=$(resolve_install_dir "$CUSTOM_INSTALL_DIR")
+
+    if [ ! -d "$install_dir" ]; then
+        print_error "Installation directory not found: $install_dir"
+        echo ""
+        echo "Searched locations:"
+        echo "  1. --install-dir argument"
+        echo "  2. OPENCODE_INSTALL_DIR environment variable"
+        echo "  3. Local path:  $(pwd)/.opencode"
+        echo "  4. Global path: $(get_global_install_path)"
+        echo ""
+        echo "Run install.sh first to install components, or specify the correct"
+        echo "path with: $0 --install-dir PATH"
         exit 1
     fi
-    
+
+    if [ ! -w "$install_dir" ]; then
+        print_error "No write permission for: $install_dir"
+        exit 1
+    fi
+
+    print_info "Updating installation at: ${CYAN}${install_dir}${NC}"
     print_step "Updating components..."
-    
-    # Update all markdown files in .opencode
-    while IFS= read -r -d '' file; do
-        update_component "$file"
-    done < <(find "$INSTALL_DIR" -name "*.md" -type f -print0)
-    
-    # Update TypeScript files
-    while IFS= read -r -d '' file; do
-        update_component "$file"
-    done < <(find "$INSTALL_DIR" -name "*.ts" -type f -not -path "*/node_modules/*" -print0)
-    
-    # Update config files
-    [ -f "env.example" ] && update_component "env.example"
-    
+
+    update_all_components "$install_dir"
+
     print_success "Update complete!"
 }