mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-05 05:05:08 +02:00
feat: gstack-screenshot-upload CLI helper
New binary for uploading screenshots to gstack.gg with pre-upload compression (sips/ImageMagick), slug sanitization, and auth token refresh. Outputs proxy URL to stdout for PR embedding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+159
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env bash
|
||||
# gstack-screenshot-upload — upload a screenshot to gstack.gg
|
||||
#
|
||||
# Usage:
|
||||
# gstack-screenshot-upload <file> [--repo-slug X] [--branch X] [--viewport X]
|
||||
#
|
||||
# Uploads a PNG to gstack.gg and prints the proxy URL (with watermark) to stdout.
|
||||
# All diagnostics go to stderr. Exit 0 = success, 1 = error.
|
||||
#
|
||||
# Env overrides (for testing):
|
||||
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
||||
# GSTACK_DIR — override auto-detected gstack root
|
||||
# GSTACK_WEB_URL — override gstack.gg URL
|
||||
set -euo pipefail
|
||||
|
||||
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
|
||||
|
||||
# Source config
|
||||
if [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
|
||||
. "$GSTACK_DIR/supabase/config.sh"
|
||||
fi
|
||||
WEB_URL="${GSTACK_WEB_URL:-https://gstack.gg}"
|
||||
|
||||
# ─── Parse args ───────────────────────────────────────────────────
|
||||
FILE=""
|
||||
REPO_SLUG=""
|
||||
BRANCH=""
|
||||
VIEWPORT=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--repo-slug) REPO_SLUG="$2"; shift 2 ;;
|
||||
--branch) BRANCH="$2"; shift 2 ;;
|
||||
--viewport) VIEWPORT="$2"; shift 2 ;;
|
||||
-*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
*) FILE="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$FILE" ]; then
|
||||
echo "Usage: gstack-screenshot-upload <file.png> [--repo-slug X] [--branch X] [--viewport X]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$FILE" ]; then
|
||||
echo "Error: file not found: $FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── Validate PNG ─────────────────────────────────────────────────
|
||||
MIME=$(file --mime-type -b "$FILE" 2>/dev/null || echo "unknown")
|
||||
if [ "$MIME" != "image/png" ]; then
|
||||
echo "Error: only PNG files are supported (got $MIME)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── Slugify helper ───────────────────────────────────────────────
|
||||
slugify() {
|
||||
echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's|/|-|g; s|[^a-z0-9._-]||g'
|
||||
}
|
||||
|
||||
[ -n "$REPO_SLUG" ] && REPO_SLUG="$(slugify "$REPO_SLUG")"
|
||||
[ -n "$BRANCH" ] && BRANCH="$(slugify "$BRANCH")"
|
||||
|
||||
# ─── Pre-upload compression ──────────────────────────────────────
|
||||
FILE_SIZE=$(wc -c < "$FILE" | tr -d ' ')
|
||||
UPLOAD_FILE="$FILE"
|
||||
|
||||
if [ "$FILE_SIZE" -gt 2097152 ]; then # > 2MB
|
||||
echo "File is $(( FILE_SIZE / 1024 ))KB — compressing..." >&2
|
||||
TMPFILE="$(mktemp /tmp/gstack-compress-XXXXXX.png)"
|
||||
|
||||
if command -v sips >/dev/null 2>&1; then
|
||||
# macOS: sips resize to max 1920px wide
|
||||
cp "$FILE" "$TMPFILE"
|
||||
sips --resampleWidth 1920 "$TMPFILE" >/dev/null 2>&1 && UPLOAD_FILE="$TMPFILE"
|
||||
elif command -v magick >/dev/null 2>&1; then
|
||||
# ImageMagick 7+
|
||||
magick "$FILE" -resize '1920x>' "$TMPFILE" 2>/dev/null && UPLOAD_FILE="$TMPFILE"
|
||||
elif command -v convert >/dev/null 2>&1; then
|
||||
# ImageMagick 6
|
||||
convert "$FILE" -resize '1920x>' "$TMPFILE" 2>/dev/null && UPLOAD_FILE="$TMPFILE"
|
||||
else
|
||||
echo "Warning: no resize tool available (sips/magick/convert), uploading raw" >&2
|
||||
fi
|
||||
|
||||
if [ "$UPLOAD_FILE" = "$TMPFILE" ]; then
|
||||
NEW_SIZE=$(wc -c < "$TMPFILE" | tr -d ' ')
|
||||
echo "Compressed: $(( FILE_SIZE / 1024 ))KB → $(( NEW_SIZE / 1024 ))KB" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── Check file size limit ────────────────────────────────────────
|
||||
FINAL_SIZE=$(wc -c < "$UPLOAD_FILE" | tr -d ' ')
|
||||
if [ "$FINAL_SIZE" -gt 10485760 ]; then # 10MB
|
||||
echo "Error: file too large ($(( FINAL_SIZE / 1024 ))KB, max 10MB)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── Get auth token ───────────────────────────────────────────────
|
||||
if ! "$GSTACK_DIR/bin/gstack-auth-refresh" --check >/dev/null 2>&1; then
|
||||
echo "Error: not authenticated. Run: gstack-auth" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACCESS_TOKEN="$("$GSTACK_DIR/bin/gstack-auth-refresh" 2>/dev/null)"
|
||||
if [ -z "$ACCESS_TOKEN" ]; then
|
||||
echo "Error: failed to get auth token" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── Upload ───────────────────────────────────────────────────────
|
||||
HTTP_RESPONSE="$(curl -s -w "\n%{http_code}" \
|
||||
--max-time 30 \
|
||||
-X POST "${WEB_URL}/api/images/upload" \
|
||||
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
||||
-F "file=@${UPLOAD_FILE}" \
|
||||
-F "repo_slug=${REPO_SLUG}" \
|
||||
-F "branch=${BRANCH}" \
|
||||
-F "viewport=${VIEWPORT}" \
|
||||
2>/dev/null || echo -e "\n000")"
|
||||
|
||||
HTTP_CODE="$(echo "$HTTP_RESPONSE" | tail -1)"
|
||||
HTTP_BODY="$(echo "$HTTP_RESPONSE" | sed '$d')"
|
||||
|
||||
# Clean up temp file
|
||||
[ "$UPLOAD_FILE" != "$FILE" ] && rm -f "$UPLOAD_FILE" 2>/dev/null
|
||||
|
||||
case "$HTTP_CODE" in
|
||||
2*)
|
||||
# Extract proxy URL from response JSON
|
||||
URL="$(echo "$HTTP_BODY" | jq -r '.url' 2>/dev/null || echo "")"
|
||||
if [ -n "$URL" ] && [ "$URL" != "null" ]; then
|
||||
echo "$URL" # stdout: proxy URL only
|
||||
else
|
||||
echo "Error: upload succeeded but no URL in response" >&2
|
||||
echo "$HTTP_BODY" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
401)
|
||||
echo "Error: authentication failed (401). Re-run: gstack-auth" >&2
|
||||
exit 1
|
||||
;;
|
||||
413)
|
||||
echo "Error: file too large (413)" >&2
|
||||
exit 1
|
||||
;;
|
||||
415)
|
||||
echo "Error: unsupported file type (415). Only PNG supported." >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "Error: upload failed (HTTP ${HTTP_CODE})" >&2
|
||||
[ -n "$HTTP_BODY" ] && echo "$HTTP_BODY" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user