#!/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+(?\\d+)\\s+(?.+)") | {(.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