From 1442740b4a776a790754c9dbde64ab7f1aa9d4e8 Mon Sep 17 00:00:00 2001 From: Darren Hinde <107584450+darrenhinde@users.noreply.github.com> Date: Fri, 20 Feb 2026 00:28:42 +0000 Subject: [PATCH] fix(update): add global installation support and path auto-detection (#230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 8 ++ update.sh | 355 ++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 316 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index ffd5fbd..2681d87 100644 --- a/README.md +++ b/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 diff --git a/update.sh b/update.sh index 5555012..82d391c 100755 --- a/update.sh +++ b/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 - print_success "Updated $path" - rm "${path}.backup" + + 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 - print_info "Failed to update $path, restoring backup" - mv "${path}.backup" "$path" + # Neither exists — return local path so main() gives a clear error + echo "$local_path" fi } -main() { - print_header - - if [ ! -d "$INSTALL_DIR" ]; then - echo "Error: $INSTALL_DIR directory not found" - echo "Run install.sh first to install components" - exit 1 +############################################################################# +# 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 - - print_step "Updating components..." - - # Update all markdown files in .opencode + + 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 -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_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 - update_component "$file" - done < <(find "$INSTALL_DIR" -name "*.md" -type f -print0) - + 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 - 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" - + 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 + + 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_components "$install_dir" + print_success "Update complete!" }