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>
This commit is contained in:
1 parent
266655abcf
commit
916c260fd8
8 files changed
+1710
-2
No files matched your search
@@ -0,0 +1,392 @@
|
||||
#!/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"
|
||||
Reference in new issue
Block a user