mirror of https://github.com/garrytan/gstack.git
274 lines
10 KiB
Bash
Executable File
274 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-update-check — periodic version check for all skills.
|
|
#
|
|
# Output (one line, or nothing):
|
|
# JUST_UPGRADED <old> <new> — marker found from recent upgrade
|
|
# UPGRADE_AVAILABLE <old> <new> — remote VERSION differs from local
|
|
# UPGRADED <old> <new> — `--apply` ran a successful upgrade
|
|
# (nothing) — up to date, snoozed, disabled, or check skipped
|
|
#
|
|
# Flags:
|
|
# --force Bust cache and snooze (used by standalone /gstack-upgrade).
|
|
# --apply When an upgrade is available, actually run it: `git fetch` +
|
|
# `git reset --hard origin/main` + `./setup` inside `$GSTACK_DIR`.
|
|
# For the full ceremony (stash, vendored-install handling,
|
|
# local-vendored copy sync, migrations), use /gstack-upgrade.
|
|
# `--apply` is the minimal headless variant for CI / scripted
|
|
# update gates.
|
|
#
|
|
# Env overrides (for testing):
|
|
# GSTACK_DIR — override auto-detected gstack root
|
|
# GSTACK_REMOTE_URL — override remote VERSION URL
|
|
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
|
set -euo pipefail
|
|
|
|
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
|
|
CACHE_FILE="$STATE_DIR/last-update-check"
|
|
MARKER_FILE="$STATE_DIR/just-upgraded-from"
|
|
SNOOZE_FILE="$STATE_DIR/update-snoozed"
|
|
VERSION_FILE="$GSTACK_DIR/VERSION"
|
|
REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}"
|
|
|
|
# ─── Parse flags ──────────────────────────────────────────────
|
|
APPLY=0
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--force)
|
|
rm -f "$CACHE_FILE"
|
|
rm -f "$SNOOZE_FILE"
|
|
;;
|
|
--apply)
|
|
APPLY=1
|
|
# Force a fresh check so a stale cache doesn't suppress the upgrade.
|
|
rm -f "$CACHE_FILE"
|
|
;;
|
|
*)
|
|
# Unknown flag — fall through silently to preserve the historical
|
|
# zero-noise contract of the periodic-check path.
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# ─── Step 0: Check if updates are disabled ────────────────────
|
|
_UC=$("$GSTACK_DIR/bin/gstack-config" get update_check 2>/dev/null || true)
|
|
if [ "$_UC" = "false" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# ─── Migration: fix stale Codex descriptions (one-time) ───────
|
|
# Existing installs may have .agents/skills/gstack/SKILL.md with oversized
|
|
# descriptions (>1024 chars) that Codex rejects. We can't regenerate from
|
|
# the runtime root (no bun/scripts), so delete oversized files — the next
|
|
# ./setup or /gstack-upgrade will regenerate them properly.
|
|
# Marker file ensures this runs at most once per install.
|
|
if [ ! -f "$STATE_DIR/.codex-desc-healed" ]; then
|
|
for _AGENTS_SKILL in "$GSTACK_DIR"/.agents/skills/*/SKILL.md; do
|
|
[ -f "$_AGENTS_SKILL" ] || continue
|
|
_DESC=$(awk '/^---$/{n++;next}n==1&&/^description:/{d=1;sub(/^description:\s*/,"");if(length>0)print;next}d&&/^ /{sub(/^ /,"");print;next}d{d=0}' "$_AGENTS_SKILL" | wc -c | tr -d ' ')
|
|
if [ "${_DESC:-0}" -gt 1024 ]; then
|
|
rm -f "$_AGENTS_SKILL"
|
|
fi
|
|
done
|
|
mkdir -p "$STATE_DIR"
|
|
touch "$STATE_DIR/.codex-desc-healed"
|
|
fi
|
|
|
|
# ─── Snooze helper ──────────────────────────────────────────
|
|
# check_snooze <remote_version>
|
|
# Returns 0 if snoozed (should stay quiet), 1 if not snoozed (should output).
|
|
#
|
|
# Snooze file format: <version> <level> <epoch>
|
|
# Level durations: 1=24h, 2=48h, 3+=7d
|
|
# New version (version mismatch) resets snooze.
|
|
check_snooze() {
|
|
local remote_ver="$1"
|
|
if [ ! -f "$SNOOZE_FILE" ]; then
|
|
return 1 # no snooze file → not snoozed
|
|
fi
|
|
local snoozed_ver snoozed_level snoozed_epoch
|
|
snoozed_ver="$(awk '{print $1}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
|
snoozed_level="$(awk '{print $2}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
|
snoozed_epoch="$(awk '{print $3}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
|
|
|
# Validate: all three fields must be non-empty
|
|
if [ -z "$snoozed_ver" ] || [ -z "$snoozed_level" ] || [ -z "$snoozed_epoch" ]; then
|
|
return 1 # corrupt file → not snoozed
|
|
fi
|
|
|
|
# Validate: level and epoch must be integers
|
|
case "$snoozed_level" in *[!0-9]*) return 1 ;; esac
|
|
case "$snoozed_epoch" in *[!0-9]*) return 1 ;; esac
|
|
|
|
# New version dropped? Ignore snooze.
|
|
if [ "$snoozed_ver" != "$remote_ver" ]; then
|
|
return 1
|
|
fi
|
|
|
|
# Compute snooze duration based on level
|
|
local duration
|
|
case "$snoozed_level" in
|
|
1) duration=86400 ;; # 24 hours
|
|
2) duration=172800 ;; # 48 hours
|
|
*) duration=604800 ;; # 7 days (level 3+)
|
|
esac
|
|
|
|
local now
|
|
now="$(date +%s)"
|
|
local expires=$(( snoozed_epoch + duration ))
|
|
if [ "$now" -lt "$expires" ]; then
|
|
return 0 # still snoozed
|
|
fi
|
|
|
|
return 1 # snooze expired
|
|
}
|
|
|
|
# ─── Step 1: Read local version ──────────────────────────────
|
|
LOCAL=""
|
|
if [ -f "$VERSION_FILE" ]; then
|
|
LOCAL="$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
fi
|
|
if [ -z "$LOCAL" ]; then
|
|
exit 0 # No VERSION file → skip check
|
|
fi
|
|
|
|
# ─── Step 2: Check "just upgraded" marker ─────────────────────
|
|
if [ -f "$MARKER_FILE" ]; then
|
|
OLD="$(cat "$MARKER_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
rm -f "$MARKER_FILE"
|
|
rm -f "$SNOOZE_FILE"
|
|
if [ -n "$OLD" ]; then
|
|
echo "JUST_UPGRADED $OLD $LOCAL"
|
|
fi
|
|
# Don't exit — fall through to remote check in case
|
|
# more updates landed since the upgrade
|
|
fi
|
|
|
|
# ─── Step 3: Check cache freshness ──────────────────────────
|
|
# UP_TO_DATE: 60 min TTL (detect new releases quickly)
|
|
# UPGRADE_AVAILABLE: 720 min TTL (keep nagging)
|
|
if [ -f "$CACHE_FILE" ]; then
|
|
CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)"
|
|
case "$CACHED" in
|
|
UP_TO_DATE*) CACHE_TTL=60 ;;
|
|
UPGRADE_AVAILABLE*) CACHE_TTL=720 ;;
|
|
*) CACHE_TTL=0 ;; # corrupt → force re-fetch
|
|
esac
|
|
|
|
STALE=$(find "$CACHE_FILE" -mmin +$CACHE_TTL 2>/dev/null || true)
|
|
if [ -z "$STALE" ] && [ "$CACHE_TTL" -gt 0 ]; then
|
|
case "$CACHED" in
|
|
UP_TO_DATE*)
|
|
CACHED_VER="$(echo "$CACHED" | awk '{print $2}')"
|
|
if [ "$CACHED_VER" = "$LOCAL" ]; then
|
|
exit 0
|
|
fi
|
|
;;
|
|
UPGRADE_AVAILABLE*)
|
|
CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')"
|
|
if [ "$CACHED_OLD" = "$LOCAL" ]; then
|
|
CACHED_NEW="$(echo "$CACHED" | awk '{print $3}')"
|
|
if check_snooze "$CACHED_NEW"; then
|
|
exit 0 # snoozed — stay quiet
|
|
fi
|
|
echo "$CACHED"
|
|
exit 0
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
fi
|
|
|
|
# ─── Step 4: Slow path — fetch remote version ────────────────
|
|
mkdir -p "$STATE_DIR"
|
|
|
|
# Fire Supabase install ping in background (parallel, non-blocking)
|
|
# This logs an update check event for community health metrics via edge function.
|
|
# If Supabase is not configured or telemetry is off, this is a no-op.
|
|
if [ -z "${GSTACK_SUPABASE_URL:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
|
|
. "$GSTACK_DIR/supabase/config.sh"
|
|
fi
|
|
_SUPA_URL="${GSTACK_SUPABASE_URL:-}"
|
|
_SUPA_KEY="${GSTACK_SUPABASE_ANON_KEY:-}"
|
|
# Respect telemetry opt-out — don't ping Supabase if user set telemetry: off
|
|
_TEL_TIER="$("$GSTACK_DIR/bin/gstack-config" get telemetry 2>/dev/null || true)"
|
|
if [ -n "$_SUPA_URL" ] && [ -n "$_SUPA_KEY" ] && [ "${_TEL_TIER:-off}" != "off" ]; then
|
|
_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
curl -sf --max-time 5 \
|
|
-X POST "${_SUPA_URL}/functions/v1/update-check" \
|
|
-H "Content-Type: application/json" \
|
|
-H "apikey: ${_SUPA_KEY}" \
|
|
-d "{\"version\":\"$LOCAL\",\"os\":\"$_OS\"}" \
|
|
>/dev/null 2>&1 &
|
|
fi
|
|
|
|
# GitHub raw fetch (primary, always reliable)
|
|
REMOTE=""
|
|
REMOTE="$(curl -sf --max-time 5 "$REMOTE_URL" 2>/dev/null || true)"
|
|
REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')"
|
|
|
|
# Validate: must look like a version number (reject HTML error pages)
|
|
if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9.]+$'; then
|
|
# Invalid or empty response — assume up to date
|
|
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$LOCAL" = "$REMOTE" ]; then
|
|
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
|
|
exit 0
|
|
fi
|
|
|
|
# Versions differ — upgrade available
|
|
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE"
|
|
|
|
# ─── Apply branch ────────────────────────────────────────────
|
|
# `--apply` is the headless / scripted variant of /gstack-upgrade Step 4
|
|
# for git installs. It does NOT cover vendored installs, local-vendored
|
|
# copy sync, or the migrations pipeline — point users at /gstack-upgrade
|
|
# for any of those. Snooze is intentionally ignored on --apply: the
|
|
# caller asked for the upgrade explicitly.
|
|
if [ "$APPLY" = "1" ]; then
|
|
if [ ! -d "$GSTACK_DIR/.git" ]; then
|
|
echo "ERROR: --apply requires a git install at $GSTACK_DIR" >&2
|
|
echo " Run /gstack-upgrade for vendored installs." >&2
|
|
exit 1
|
|
fi
|
|
if ! git -C "$GSTACK_DIR" fetch origin main >/dev/null 2>&1; then
|
|
echo "ERROR: --apply failed to fetch origin/main from $GSTACK_DIR" >&2
|
|
exit 1
|
|
fi
|
|
if ! git -C "$GSTACK_DIR" reset --hard origin/main >/dev/null 2>&1; then
|
|
echo "ERROR: --apply failed to reset $GSTACK_DIR to origin/main" >&2
|
|
exit 1
|
|
fi
|
|
if [ -x "$GSTACK_DIR/setup" ]; then
|
|
if ! ( cd "$GSTACK_DIR" && ./setup >/dev/null 2>&1 ); then
|
|
echo "ERROR: --apply ran git reset but ./setup failed in $GSTACK_DIR" >&2
|
|
echo " Working tree is at the new version; re-run ./setup manually." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
# Confirm the version moved.
|
|
NEW_LOCAL=""
|
|
if [ -f "$VERSION_FILE" ]; then
|
|
NEW_LOCAL="$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
fi
|
|
echo "UPGRADED $LOCAL ${NEW_LOCAL:-$REMOTE}" > "$CACHE_FILE"
|
|
echo "UPGRADED $LOCAL ${NEW_LOCAL:-$REMOTE}"
|
|
exit 0
|
|
fi
|
|
|
|
if check_snooze "$REMOTE"; then
|
|
exit 0 # snoozed — stay quiet
|
|
fi
|
|
|
|
# Log upgrade_prompted event (only on slow-path fetch, not cached replays)
|
|
TEL_CMD="$GSTACK_DIR/bin/gstack-telemetry-log"
|
|
if [ -x "$TEL_CMD" ]; then
|
|
"$TEL_CMD" --event-type upgrade_prompted --skill "" --duration 0 \
|
|
--outcome success --session-id "update-$$-$(date +%s)" 2>/dev/null &
|
|
fi
|
|
|
|
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE"
|