Files
t3mp3st/scripts/update.sh
T
Joseph MaglyandWibias 916c260fd8 feat: add safe cross-platform upstream updater (#98)
* feat: add safe upstream update scripts

* fix(updater): use portable PowerShell temp path

---------

Co-authored-by: Wibias <37517432+Wibias@users.noreply.github.com>
2026-07-15 19:56:25 -04:00

393 lines
11 KiB
Bash

#!/usr/bin/env bash
# Linux / macOS / WSL updater (Windows uses update.ps1 via scripts/update.mjs).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
UPSTREAM_URL='https://github.com/elder-plinius/T3MP3ST.git'
UPSTREAM_BRANCH='main'
UPSTREAM_REMOTE='upstream'
DRY_RUN=0
HARD=0
FORCE=0
KEEP_BACKUP=0
STEP_INDEX=0
STEP_TOTAL=0
if [[ -t 1 ]]; then
C_CYAN=$'\033[36m'
C_GREEN=$'\033[32m'
C_YELLOW=$'\033[33m'
C_GRAY=$'\033[90m'
C_WHITE=$'\033[97m'
C_RESET=$'\033[0m'
else
C_CYAN='' C_GREEN='' C_YELLOW='' C_GRAY='' C_WHITE='' C_RESET=''
fi
rule() {
local title="${1:-}"
local width=68
echo
if [[ -n "$title" ]]; then
local fill=$((width - ${#title} - 4))
[[ $fill -lt 0 ]] && fill=0
printf '%s── %s %s%s\n' "$C_CYAN" "$title" "$(printf '─%.0s' $(seq 1 "$fill"))" "$C_RESET"
else
printf '%s%s%s\n' "$C_GRAY" "$(printf '─%.0s' $(seq 1 "$width"))" "$C_RESET"
fi
}
kv() {
local key="$1" val="$2" color="${3:-$C_WHITE}"
printf ' %-12s %b%s%b\n' "${key}:" "$color" "$val" "$C_RESET"
}
plan_line() {
printf ' %s %s\n' "$1" "$2"
}
start_step() {
STEP_INDEX=$((STEP_INDEX + 1))
echo
if [[ "$STEP_TOTAL" -gt 0 ]]; then
printf ' %s[%d/%d] %s%s\n' "$C_CYAN" "$STEP_INDEX" "$STEP_TOTAL" "$1" "$C_RESET"
else
printf ' [*] %s\n' "$1"
fi
}
while [[ $# -gt 0 ]]; do
case "$1" in
-DryRun|--dry-run) DRY_RUN=1 ;;
-Hard|--hard) HARD=1 ;;
-Force|--force) FORCE=1 ;;
-KeepBackup|--keep-backup) KEEP_BACKUP=1 ;;
*) echo "Unknown option: $1" >&2; exit 2 ;;
esac
shift
done
git_install_help() {
echo '[x] Git is required but was not found on PATH.' >&2
echo >&2
case "$(uname -s 2>/dev/null || echo unknown)" in
Darwin)
echo 'Install options:' >&2
echo ' brew install git' >&2
echo ' xcode-select --install' >&2
;;
Linux|GNU/*)
echo 'Install with your package manager, for example:' >&2
echo ' sudo apt install git # Debian/Ubuntu' >&2
echo ' sudo dnf install git # Fedora' >&2
echo ' sudo pacman -S git # Arch' >&2
echo 'More: https://git-scm.com/download/linux' >&2
;;
*)
echo 'Install Git: https://git-scm.com/downloads' >&2
;;
esac
echo >&2
echo 'Then run: npm run update' >&2
}
npm_install_help() {
echo '[x] npm is required but was not found on PATH.' >&2
echo 'Install Node.js (includes npm): https://nodejs.org/' >&2
echo 'Then run: npm run update' >&2
}
cd "$ROOT"
[[ -f package.json ]] || { echo '[x] package.json missing' >&2; exit 1; }
command -v git >/dev/null 2>&1 || { git_install_help; exit 1; }
command -v npm >/dev/null 2>&1 || { npm_install_help; exit 1; }
if [[ -f "$ROOT/scripts/update-banner.txt" ]]; then
cat "$ROOT/scripts/update-banner.txt"
else
echo 'T3MP3ST'
fi
echo ' T3MP3ST Updater'
echo ' Safe upstream sync - secrets, config, and bench artifacts stay local'
echo
backup="$(mktemp -d -t t3mp3st-update-backup.XXXXXX)"
manifest="$backup/manifest.txt"
: >"$manifest"
protected_count=0
protected_list="$(mktemp)"
on_err() {
echo >&2
echo '[x] Update failed partway through.' >&2
if [[ -f "$manifest" && -s "$manifest" ]]; then
echo " Local backup of protected files: $backup" >&2
fi
}
trap on_err ERR
protect_patterns() {
grep -v '^#' "$ROOT/scripts/update-protected.txt" | grep -v '^[[:space:]]*$' || true
[[ -f "$ROOT/scripts/update-protected.local.txt" ]] && grep -v '^#' "$ROOT/scripts/update-protected.local.txt" | grep -v '^[[:space:]]*$' || true
}
path_matches_pattern() {
local rel="$1" pat="$2"
pat="${pat//\\//}"
rel="${rel//\\//}"
if [[ "$pat" == */ ]]; then
local prefix="${pat%/}"
[[ "$rel" == "$prefix" || "$rel" == "$prefix"/* ]]
return
fi
if [[ "$pat" == *[*?]* ]]; then
case "$rel" in
$pat) return 0 ;;
esac
return 1
fi
[[ "$rel" == "$pat" ]]
}
collect_positive_pattern() {
local pat="$1"
if [[ "$pat" == */ ]]; then
local rel="${pat%/}"
[[ -e "$ROOT/$rel" ]] || return 0
echo "$rel" >>"$protected_list"
elif [[ "$pat" == *'*'* ]]; then
find "$ROOT" -path "$ROOT/$pat" -print 2>/dev/null | while IFS= read -r hit; do
echo "${hit#"$ROOT/"}" >>"$protected_list"
done
else
[[ -e "$ROOT/$pat" ]] || return 0
echo "$pat" >>"$protected_list"
fi
}
apply_negation_patterns() {
local -a negations=()
while IFS= read -r pat; do
[[ "$pat" == !* ]] || continue
negations+=("${pat:1}")
done < <(protect_patterns)
[[ "${#negations[@]}" -eq 0 ]] && return 0
local filtered
filtered="$(mktemp)"
while IFS= read -r rel; do
[[ -n "$rel" ]] || continue
local skip=0 neg
for neg in "${negations[@]}"; do
if path_matches_pattern "$rel" "$neg"; then
skip=1
break
fi
done
[[ "$skip" -eq 0 ]] && echo "$rel" >>"$filtered"
done <"$protected_list"
mv "$filtered" "$protected_list"
}
collect_protected() {
while IFS= read -r pat; do
[[ "$pat" == !* ]] && continue
collect_positive_pattern "$pat"
done < <(protect_patterns)
apply_negation_patterns
}
backup_protected() {
: >"$manifest"
while IFS= read -r rel; do
[[ -n "$rel" && -e "$ROOT/$rel" ]] || continue
mkdir -p "$backup/$(dirname "$rel")"
cp -a "$ROOT/$rel" "$backup/$rel"
echo "$rel" >>"$manifest"
done <"$protected_list"
}
collect_protected
protected_count="$(grep -c . "$protected_list" 2>/dev/null || echo 0)"
protected_count="$(echo "$protected_count" | tr -d '[:space:]')"
[[ "$protected_count" =~ ^[0-9]+$ ]] || protected_count=0
commit_count="$(git rev-list --count HEAD 2>/dev/null || echo 0)"
[[ -d .git ]] || commit_count=0
strategy='Merge upstream/main'
if [[ "$HARD" -eq 1 ]]; then
strategy='Hard reset to upstream/main'
elif [[ "$commit_count" -eq 0 ]]; then
strategy='First-time sync (upstream snapshot)'
fi
platform_label="$(uname -s 2>/dev/null || echo Unix)"
case "$platform_label" in
Darwin) platform_label='macOS' ;;
Linux) platform_label='Linux' ;;
esac
rule 'Overview'
kv 'Platform' "$platform_label (bash)" "$C_WHITE"
kv 'Repository' "$ROOT" "$C_WHITE"
kv 'Upstream' "$UPSTREAM_URL @ $UPSTREAM_BRANCH" "$C_WHITE"
kv 'Strategy' "$strategy" "$C_CYAN"
[[ "$DRY_RUN" -eq 1 ]] && kv 'Mode' 'Dry run (preview only)' "$C_YELLOW"
rule 'Protected paths in this repo'
if [[ "$protected_count" -eq 0 ]]; then
plan_line '-' 'No protected project files in this repo (.env, .keys.*, bench output, etc.)'
else
while IFS= read -r rel; do
[[ -n "$rel" ]] && plan_line '+' "$rel"
done <"$protected_list"
fi
rule 'Never touched (outside repo)'
plan_line '-' '$HOME/.config or AppData t3mp3st config'
plan_line '-' 'War Room browser localStorage'
plan_line '-' 'Codex / Hermes local agent auth'
local_sha='(no commits yet)'
upstream_sha='(unknown)'
if git rev-parse HEAD >/dev/null 2>&1; then
local_sha="$(git rev-parse --short HEAD 2>/dev/null || echo '(no commits yet)')"
fi
if [[ "$DRY_RUN" -eq 1 ]]; then
upstream_sha='(preview)'
elif git rev-parse "$UPSTREAM_REMOTE/$UPSTREAM_BRANCH" >/dev/null 2>&1; then
upstream_sha="$(git rev-parse --short "$UPSTREAM_REMOTE/$UPSTREAM_BRANCH")"
fi
rule 'Revisions'
kv 'Local' "$local_sha" "$C_WHITE"
kv 'Upstream' "$upstream_sha" "$C_CYAN"
plan_steps=()
if [[ "$protected_count" -gt 0 ]]; then
plan_steps+=("Back up $protected_count protected path(s) to a temp folder")
else
plan_steps+=('Skip in-repo backup (no protected project files found)')
fi
if [[ ! -d .git ]]; then
plan_steps+=('Initialize git and add upstream remote')
else
plan_steps+=('Use existing git repo and upstream remote')
fi
plan_steps+=("Fetch $UPSTREAM_REMOTE/$UPSTREAM_BRANCH")
if [[ "$HARD" -eq 1 ]]; then
plan_steps+=('Reset working tree to upstream/main')
elif [[ "$commit_count" -eq 0 ]]; then
plan_steps+=('Apply upstream snapshot to project files')
else
plan_steps+=('Merge upstream changes into your local branch')
fi
if [[ "$protected_count" -gt 0 ]]; then
plan_steps+=('Restore backed-up protected paths')
fi
plan_steps+=('Run npm install to refresh dependencies')
rule 'Sync plan'
n=1
for step in "${plan_steps[@]}"; do
printf ' %2d. %s\n' "$n" "$step"
n=$((n + 1))
done
if [[ "$FORCE" -eq 0 ]]; then
echo
printf 'Proceed with update? [y/N] '
read -r ans
[[ "$ans" =~ ^[Yy]([Ee][Ss])?$ ]] || { echo '[!] Cancelled - no changes made.'; rm -rf "$backup"; trap - ERR; exit 0; }
fi
STEP_TOTAL=2
[[ "$protected_count" -gt 0 ]] && STEP_TOTAL=$((STEP_TOTAL + 2))
STEP_INDEX=0
if [[ "$protected_count" -gt 0 ]]; then
start_step 'Backing up protected files'
if [[ "$DRY_RUN" -eq 1 ]]; then
echo ' [*] [dry-run] would back up protected paths'
else
backup_protected
printf ' [+] Backed up %d path(s)\n' "$protected_count"
fi
fi
start_step 'Syncing from upstream'
if [[ "$DRY_RUN" -eq 1 ]]; then
[[ -d .git ]] || echo ' [*] [dry-run] would run: git init'
if [[ -d .git ]] && ! git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1; then
echo " [*] [dry-run] would run: git remote add $UPSTREAM_REMOTE $UPSTREAM_URL"
fi
echo " [*] [dry-run] would run: git fetch $UPSTREAM_REMOTE $UPSTREAM_BRANCH"
if [[ "$HARD" -eq 1 ]]; then
echo " [*] [dry-run] would run: git reset --hard $UPSTREAM_REMOTE/$UPSTREAM_BRANCH"
elif [[ "$commit_count" -eq 0 ]]; then
echo ' [*] [dry-run] would apply upstream snapshot (checkout upstream/main)'
echo " [*] [dry-run] would run: git checkout -B $UPSTREAM_BRANCH $UPSTREAM_REMOTE/$UPSTREAM_BRANCH"
else
echo " [*] [dry-run] would run: git merge $UPSTREAM_REMOTE/$UPSTREAM_BRANCH"
fi
else
[[ -d .git ]] || git init
git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1 || git remote add "$UPSTREAM_REMOTE" "$UPSTREAM_URL"
git fetch "$UPSTREAM_REMOTE" "$UPSTREAM_BRANCH" 2>&1 || true
if [[ "$HARD" -eq 1 ]]; then
git reset --hard "$UPSTREAM_REMOTE/$UPSTREAM_BRANCH"
elif [[ "$commit_count" -eq 0 ]]; then
find "$ROOT" -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} +
git checkout -B "$UPSTREAM_BRANCH" "$UPSTREAM_REMOTE/$UPSTREAM_BRANCH"
else
git merge "$UPSTREAM_REMOTE/$UPSTREAM_BRANCH" --no-edit
fi
echo ' [+] Git sync finished'
fi
if [[ "$protected_count" -gt 0 ]]; then
start_step 'Restoring protected files'
if [[ "$DRY_RUN" -eq 1 ]]; then
echo ' [*] [dry-run] would restore protected paths'
else
while IFS= read -r rel; do
[[ -n "$rel" && -e "$backup/$rel" ]] || continue
mkdir -p "$(dirname "$ROOT/$rel")"
rm -rf "$ROOT/$rel"
cp -a "$backup/$rel" "$ROOT/$rel"
done <"$manifest"
echo ' [+] Protected paths restored'
fi
fi
start_step 'Refreshing npm packages'
if [[ "$DRY_RUN" -eq 1 ]]; then
echo ' [*] [dry-run] would run: npm install'
else
npm install
echo ' [+] npm install finished'
fi
new_sha="$upstream_sha"
if [[ "$DRY_RUN" -eq 0 ]] && git rev-parse HEAD >/dev/null 2>&1; then
new_sha="$(git rev-parse --short HEAD)"
fi
rule 'Done'
if [[ "$DRY_RUN" -eq 1 ]]; then
echo ' Preview only - no files were changed.'
echo ' Run without --dry-run when you are ready.'
else
echo ' Update complete'
kv 'Before' "$local_sha" "$C_WHITE"
kv 'After' "$new_sha" "$C_GREEN"
fi
echo
trap - ERR
[[ "$KEEP_BACKUP" -eq 1 ]] || rm -rf "$backup"