Files
gstack/bin/gstack-community-backup
Garry Tan 3df8a77b00 chore: stage pre-existing community tier changes
Community tier auth, backup/restore, and test updates that were already
on this branch before the telemetry sprint. Includes updated telemetry
prompt test to match 3-option community tier flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:49:40 -07:00

137 lines
5.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# gstack-community-backup — sync local state to Supabase for cloud backup
#
# Backs up: config, analytics summary, retro history.
# Requires community tier + valid auth token.
# Rate limited to once per 30 minutes.
#
# Env overrides (for testing):
# GSTACK_STATE_DIR — override ~/.gstack state directory
# GSTACK_DIR — override auto-detected gstack root
set -uo pipefail
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
ANALYTICS_DIR="$STATE_DIR/analytics"
JSONL_FILE="$ANALYTICS_DIR/skill-usage.jsonl"
BACKUP_RATE_FILE="$ANALYTICS_DIR/.last-backup-time"
CONFIG_CMD="$GSTACK_DIR/bin/gstack-config"
AUTH_REFRESH="$GSTACK_DIR/bin/gstack-auth-refresh"
# Source Supabase config
if [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
. "$GSTACK_DIR/supabase/config.sh"
fi
ENDPOINT="${GSTACK_TELEMETRY_ENDPOINT:-}"
ANON_KEY="${GSTACK_SUPABASE_ANON_KEY:-}"
# ─── Pre-checks ─────────────────────────────────────────────
# Must be community tier
TIER="$("$CONFIG_CMD" get telemetry 2>/dev/null || true)"
[ "$TIER" != "community" ] && exit 0
# Must have auth
"$AUTH_REFRESH" --check 2>/dev/null || exit 0
# Must have endpoint
[ -z "$ENDPOINT" ] && exit 0
# Rate limit: once per 30 minutes
if [ -f "$BACKUP_RATE_FILE" ]; then
STALE=$(find "$BACKUP_RATE_FILE" -mmin +30 2>/dev/null || true)
[ -z "$STALE" ] && exit 0
fi
# ─── Get auth token ─────────────────────────────────────────
ACCESS_TOKEN="$("$AUTH_REFRESH" 2>/dev/null || true)"
[ -z "$ACCESS_TOKEN" ] && exit 0
# Read user info from auth file
AUTH_JSON="$(cat "$STATE_DIR/auth-token.json" 2>/dev/null || echo "{}")"
USER_ID="$(echo "$AUTH_JSON" | grep -o '"user_id":"[^"]*"' | head -1 | sed 's/"user_id":"//;s/"//')"
EMAIL="$(echo "$AUTH_JSON" | grep -o '"email":"[^"]*"' | head -1 | sed 's/"email":"//;s/"//')"
[ -z "$USER_ID" ] && exit 0
# ─── Build config snapshot ───────────────────────────────────
CONFIG_SNAPSHOT="{}"
if [ -f "$STATE_DIR/config.yaml" ]; then
# Convert YAML-like config to JSON safely using jq
CONFIG_SNAPSHOT="$(grep -v '^#' "$STATE_DIR/config.yaml" | grep ':' | \
jq -R 'split(": ") | {(.[0]): .[1]}' | jq -s 'add' || echo "{}")"
fi
# ─── Build analytics summary ────────────────────────────────
# Per-skill aggregates + last 100 events (not raw JSONL)
ANALYTICS_SNAPSHOT="{\"skills\":{},\"recent_events\":[]}"
if [ -f "$JSONL_FILE" ]; then
# Count per-skill totals
SKILL_COUNTS_JSON="$(grep -o '"skill":"[^"]*"' "$JSONL_FILE" 2>/dev/null | \
awk -F'"' '{print $4}' | sort | uniq -c | sort -rn | head -20 | \
jq -R 'capture("\\s+(?<count>\\d+)\\s+(?<skill>.+)") | {(.skill): {total_runs: (.count|tonumber)}}' | jq -s 'add')"
# Last 100 events (strip local-only fields)
RECENT_JSON="$(tail -100 "$JSONL_FILE" 2>/dev/null | \
jq -c 'del(._repo_slug, ._branch)' | jq -s -c '.')"
ANALYTICS_SNAPSHOT="$(jq -n \
--argjson skills "${SKILL_COUNTS_JSON:-{}}" \
--argjson recent "${RECENT_JSON:-[]}" \
'{"skills": $skills, "recent_events": $recent}')"
fi
# ─── Build retro history snapshot ────────────────────────────
RETRO_SNAPSHOT="[]"
# Look for retro files in common locations
RETRO_FILES=""
if [ -d "$STATE_DIR" ]; then
RETRO_FILES="$(find "$STATE_DIR" -name "retro-*.json" -o -name "retro_*.json" 2>/dev/null | head -20 || true)"
fi
if [ -n "$RETRO_FILES" ]; then
RETRO_SNAPSHOT="$(cat $RETRO_FILES 2>/dev/null | jq -s -c '.' || echo "[]")"
fi
# ─── Upsert to installations table ──────────────────────────
GSTACK_VERSION="$(cat "$GSTACK_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "unknown")"
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
NOW_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
PAYLOAD="$(jq -n \
--arg id "$USER_ID" \
--arg email "$EMAIL" \
--arg version "$GSTACK_VERSION" \
--arg os "$OS" \
--argjson config "${CONFIG_SNAPSHOT:-{}}" \
--argjson analytics "${ANALYTICS_SNAPSHOT:-{}}" \
--argjson retro "${RETRO_SNAPSHOT:-[]}" \
--arg last_backup "$NOW_ISO" \
'{
installation_id: $id,
user_id: $id,
email: $email,
gstack_version: $version,
os: $os,
config_snapshot: $config,
analytics_snapshot: $analytics,
retro_history: $retro,
last_backup_at: $last_backup,
last_seen: $last_backup
}')"
# Upsert (POST with Prefer: resolution=merge-duplicates)
HTTP_CODE="$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 \
-X POST "${ENDPOINT}/installations" \
-H "Content-Type: application/json" \
-H "apikey: ${ANON_KEY}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Prefer: resolution=merge-duplicates,return=minimal" \
-d "$PAYLOAD" 2>/dev/null || echo "000")"
# Update rate limit marker on success
case "$HTTP_CODE" in
2*) touch "$BACKUP_RATE_FILE" 2>/dev/null || true ;;
esac
exit 0