* feat: add safe upstream update scripts * fix(updater): use portable PowerShell temp path --------- Co-authored-by: Wibias <37517432+Wibias@users.noreply.github.com>
393 lines
11 KiB
Bash
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"
|