~cytrogen/gstack

ref: cf73db5f19040218ecd50d0b81acffd40b63f056 gstack/bin/gstack-update-check -rwxr-xr-x 7.7 KiB
cf73db5f — Garry Tan feat: autoplan DX integration + README docs (v0.15.4.0) (#791) 5 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#!/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
#   (nothing)                       — up to date, snoozed, disabled, or check skipped
#
# 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}"

# ─── Force flag (busts cache + snooze for standalone /gstack-upgrade) ──
if [ "${1:-}" = "--force" ]; then
  rm -f "$CACHE_FILE"
  rm -f "$SNOOZE_FILE"
fi

# ─── 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"
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"