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
This commit is contained in:
Darren Hinde 2026-02-20 00:28:42 +00:00 committed by GitHub
parent 8e8084e4ae
commit 1442740b4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 316 additions and 47 deletions

View file

@ -133,6 +133,14 @@ curl -fsSL https://raw.githubusercontent.com/darrenhinde/OpenAgentsControl/main/
bash install.sh 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 ### Step 2: Start Building
```bash ```bash

333
update.sh
View file

@ -3,79 +3,340 @@
############################################################################# #############################################################################
# OpenAgents Control Updater # OpenAgents Control Updater
# Updates existing OpenCode components to latest versions # 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 set -e
# Colors # Detect platform
GREEN='\033[0;32m' PLATFORM="$(uname -s)"
# YELLOW='\033[1;33m' # Unused case "$PLATFORM" in
BLUE='\033[0;34m' Linux*) PLATFORM="Linux";;
CYAN='\033[0;36m' Darwin*) PLATFORM="macOS";;
BOLD='\033[1m' CYGWIN*|MINGW*|MSYS*) PLATFORM="Windows";;
NC='\033[0m' *) PLATFORM="Unknown";;
esac
REPO_URL="https://raw.githubusercontent.com/darrenhinde/OpenAgentsControl/main" # Colors (disable on Windows terminals without color support)
INSTALL_DIR=".opencode" 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_success() { echo -e "${GREEN}✓${NC} $1"; }
print_info() { echo -e "${BLUE}ℹ${NC} $1"; } 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_step() { echo -e "\n${CYAN}${BOLD}▶${NC} $1\n"; }
print_header() { print_header() {
echo -e "${CYAN}${BOLD}" echo -e "${CYAN}${BOLD}"
echo "╔════════════════════════════════════════════════════════════════╗" echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ ║" echo "║ ║"
echo "║ OpenAgents Control Updater v1.0.0 ║" echo "║ OpenAgents Control Updater v1.1.0 ║"
echo "║ ║" echo "║ ║"
echo "╚════════════════════════════════════════════════════════════════╝" echo "╚════════════════════════════════════════════════════════════════╝"
echo -e "${NC}" echo -e "${NC}"
} }
update_component() { print_usage() {
local path=$1 echo "Usage: $0 [--install-dir PATH]"
local url="${REPO_URL}/${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"
}
if [ ! -f "$path" ]; then #############################################################################
print_info "Skipping $path (not installed)" # 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 return
fi fi
# Backup existing file if [ -n "$OPENCODE_INSTALL_DIR" ]; then
cp "$path" "${path}.backup" normalize_path "$OPENCODE_INSTALL_DIR"
return
fi
if curl -fsSL "$url" -o "$path"; then # Auto-detect: prefer local project install, fall back to global
print_success "Updated $path" local local_path
rm "${path}.backup" 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 else
print_info "Failed to update $path, restoring backup" # Neither exists — return local path so main() gives a clear error
mv "${path}.backup" "$path" echo "$local_path"
fi fi
} }
main() { #############################################################################
print_header # Update Logic
#############################################################################
if [ ! -d "$INSTALL_DIR" ]; then update_component() {
echo "Error: $INSTALL_DIR directory not found" local path="$1"
echo "Run install.sh first to install components" local install_dir="$2"
exit 1 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 fi
print_step "Updating components..." local url="${REPO_URL}/.opencode/${relative_path}"
local backup="${path}.backup"
# Update all markdown files in .opencode 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 while IFS= read -r -d '' file; do
update_component "$file" if update_component "$file" "$install_dir"; then
done < <(find "$INSTALL_DIR" -name "*.md" -type f -print0) updated=$((updated + 1))
else
failed=$((failed + 1))
fi
done < <(find "$install_dir" -name "*.md" -type f -print0)
# Update TypeScript files # Update TypeScript files
while IFS= read -r -d '' file; do while IFS= read -r -d '' file; do
update_component "$file" if update_component "$file" "$install_dir"; then
done < <(find "$INSTALL_DIR" -name "*.ts" -type f -not -path "*/node_modules/*" -print0) updated=$((updated + 1))
else
failed=$((failed + 1))
fi
done < <(find "$install_dir" -name "*.ts" -type f -not -path "*/node_modules/*" -print0)
# Update config files # Update shell scripts inside install dir
[ -f "env.example" ] && update_component "env.example" 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!" print_success "Update complete!"
} }