mirror of https://github.com/garrytan/gstack.git
feat: GStack Browser .app bundle — launcher script + build system
- scripts/app/gstack-browser: dual-mode launcher (dev + .app bundle) - scripts/build-app.sh: compiles binary, bundles Chromium + extension, creates DMG - Rebrands Chromium plist during build for "GStack Browser" in menu bar - 389MB .app, 189MB compressed DMG, launches in ~5s
This commit is contained in:
parent
126cebf4c4
commit
157dc74255
|
|
@ -0,0 +1,75 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# GStack Browser launcher — starts browse server + headed Chromium with extension
|
||||||
|
#
|
||||||
|
# Works in two modes:
|
||||||
|
# 1. Inside .app bundle: Contents/MacOS/gstack-browser → Resources are at ../Resources/
|
||||||
|
# 2. Dev mode (run directly): uses global gstack install at ~/.claude/skills/gstack/
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# open "GStack Browser.app" # .app bundle mode
|
||||||
|
# scripts/app/gstack-browser # dev mode (uses global gstack install)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# Detect mode: .app bundle or dev
|
||||||
|
if [ -d "$SCRIPT_DIR/../Resources" ]; then
|
||||||
|
# .app bundle mode — resources are alongside in the bundle
|
||||||
|
DIR="$(cd "$SCRIPT_DIR/../Resources" && pwd)"
|
||||||
|
else
|
||||||
|
# Dev mode — use global gstack install
|
||||||
|
DIR="$HOME/.claude/skills/gstack"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Point Playwright at bundled Chromium (only in .app mode)
|
||||||
|
if [ -d "$DIR/chromium" ]; then
|
||||||
|
CHROMIUM_APP=$(ls -d "$DIR/chromium/"*.app 2>/dev/null | head -1)
|
||||||
|
if [ -n "$CHROMIUM_APP" ]; then
|
||||||
|
export GSTACK_CHROMIUM_PATH="$CHROMIUM_APP/Contents/MacOS/$(ls "$CHROMIUM_APP/Contents/MacOS/" | head -1)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Browse server config
|
||||||
|
export BROWSE_PORT=34567
|
||||||
|
export BROWSE_HEADED=1
|
||||||
|
|
||||||
|
# Extension: bundled first, then global install
|
||||||
|
if [ -d "$DIR/extension" ]; then
|
||||||
|
export BROWSE_EXTENSIONS_DIR="$DIR/extension"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Server script: bundled source first, then global install
|
||||||
|
if [ -f "$DIR/src/server.ts" ]; then
|
||||||
|
export BROWSE_SERVER_SCRIPT="$DIR/src/server.ts"
|
||||||
|
elif [ -f "$HOME/.claude/skills/gstack/browse/src/server.ts" ]; then
|
||||||
|
export BROWSE_SERVER_SCRIPT="$HOME/.claude/skills/gstack/browse/src/server.ts"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Browse binary: bundled .app first, then global install
|
||||||
|
# Note: -x on a directory is true, so check -f (regular file) too
|
||||||
|
BROWSE_BIN=""
|
||||||
|
for candidate in "$DIR/browse" "$DIR/browse/dist/browse" "$HOME/.claude/skills/gstack/browse/dist/browse"; do
|
||||||
|
if [ -f "$candidate" ] && [ -x "$candidate" ]; then
|
||||||
|
BROWSE_BIN="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$BROWSE_BIN" ]; then
|
||||||
|
echo "ERROR: browse binary not found. Run 'bun run build' in the gstack repo or reinstall GStack Browser."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure profile directory
|
||||||
|
mkdir -p ~/.gstack/chromium-profile
|
||||||
|
|
||||||
|
# Project binding: use last-used project dir, default to home
|
||||||
|
PROJECT_DIR=$(cat ~/.gstack/last-project 2>/dev/null || echo "$HOME")
|
||||||
|
if [ ! -d "$PROJECT_DIR" ]; then
|
||||||
|
PROJECT_DIR="$HOME"
|
||||||
|
fi
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
|
# Launch browse in connect mode
|
||||||
|
exec "$BROWSE_BIN" connect "$@"
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Build GStack Browser.app — macOS application bundle
|
||||||
|
#
|
||||||
|
# Creates a self-contained .app with:
|
||||||
|
# - Compiled browse binary
|
||||||
|
# - Playwright's bundled Chromium
|
||||||
|
# - Chrome extension (sidebar)
|
||||||
|
# - Info.plist with bundle ID
|
||||||
|
#
|
||||||
|
# Output: dist/GStack Browser.app and dist/GStack-Browser.dmg
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/build-app.sh # Build .app + DMG
|
||||||
|
# ./scripts/build-app.sh --no-dmg # Build .app only
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
APP_NAME="GStack Browser"
|
||||||
|
BUNDLE_ID="com.gstack.browser"
|
||||||
|
VERSION=$(cat "$ROOT/VERSION" 2>/dev/null || echo "0.0.1")
|
||||||
|
BUILD_DIR="$ROOT/dist"
|
||||||
|
APP_DIR="$BUILD_DIR/$APP_NAME.app"
|
||||||
|
|
||||||
|
echo "Building $APP_NAME v$VERSION..."
|
||||||
|
|
||||||
|
# ─── Step 1: Compile browse binary ─────────────────────────────
|
||||||
|
echo " Compiling browse binary..."
|
||||||
|
cd "$ROOT/browse"
|
||||||
|
bun build --compile src/cli.ts --outfile "$BUILD_DIR/browse-app" --target=bun 2>/dev/null
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
# ─── Step 2: Find Playwright's Chromium ─────────────────────────
|
||||||
|
echo " Locating Playwright Chromium..."
|
||||||
|
PW_CACHE="$HOME/Library/Caches/ms-playwright"
|
||||||
|
CHROMIUM_DIR=$(ls -d "$PW_CACHE"/chromium-*/chrome-mac-arm64 2>/dev/null | sort -V | tail -1)
|
||||||
|
|
||||||
|
if [ -z "$CHROMIUM_DIR" ]; then
|
||||||
|
echo "ERROR: Playwright Chromium not found in $PW_CACHE"
|
||||||
|
echo "Run: bunx playwright install chromium"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CHROME_APP=$(ls -d "$CHROMIUM_DIR"/*.app 2>/dev/null | head -1)
|
||||||
|
if [ -z "$CHROME_APP" ]; then
|
||||||
|
echo "ERROR: Chrome .app not found in $CHROMIUM_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " Found: $(basename "$CHROME_APP")"
|
||||||
|
|
||||||
|
# ─── Step 3: Create .app structure ──────────────────────────────
|
||||||
|
echo " Building .app bundle..."
|
||||||
|
rm -rf "$APP_DIR"
|
||||||
|
mkdir -p "$APP_DIR/Contents/MacOS"
|
||||||
|
mkdir -p "$APP_DIR/Contents/Resources"
|
||||||
|
|
||||||
|
# Launcher script
|
||||||
|
cp "$ROOT/scripts/app/gstack-browser" "$APP_DIR/Contents/MacOS/gstack-browser"
|
||||||
|
chmod +x "$APP_DIR/Contents/MacOS/gstack-browser"
|
||||||
|
|
||||||
|
# Browse binary
|
||||||
|
cp "$BUILD_DIR/browse-app" "$APP_DIR/Contents/Resources/browse"
|
||||||
|
chmod +x "$APP_DIR/Contents/Resources/browse"
|
||||||
|
|
||||||
|
# Extension
|
||||||
|
cp -r "$ROOT/extension" "$APP_DIR/Contents/Resources/extension"
|
||||||
|
# Remove .auth.json if present (auth now via /health endpoint)
|
||||||
|
rm -f "$APP_DIR/Contents/Resources/extension/.auth.json"
|
||||||
|
|
||||||
|
# Server source (needed for `bun run server.ts` subprocess)
|
||||||
|
# The launcher sets BROWSE_SERVER_SCRIPT to point at this.
|
||||||
|
# Copy the full src/ directory since server.ts imports other modules.
|
||||||
|
echo " Copying browse source..."
|
||||||
|
cp -r "$ROOT/browse/src" "$APP_DIR/Contents/Resources/src"
|
||||||
|
# Also need package.json for module resolution
|
||||||
|
cp "$ROOT/browse/package.json" "$APP_DIR/Contents/Resources/" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Chromium
|
||||||
|
mkdir -p "$APP_DIR/Contents/Resources/chromium"
|
||||||
|
echo " Copying Chromium (~330MB)..."
|
||||||
|
cp -a "$CHROME_APP" "$APP_DIR/Contents/Resources/chromium/"
|
||||||
|
|
||||||
|
# ─── Step 3b: Rebrand Chromium ────────────────────────────────────
|
||||||
|
# Patch the bundled Chromium's Info.plist so macOS shows "GStack Browser"
|
||||||
|
# in the menu bar, Dock, and Cmd+Tab instead of "Google Chrome for Testing"
|
||||||
|
CHROMIUM_PLIST="$APP_DIR/Contents/Resources/chromium/$(basename "$CHROME_APP")/Contents/Info.plist"
|
||||||
|
if [ -f "$CHROMIUM_PLIST" ]; then
|
||||||
|
echo " Rebranding Chromium → $APP_NAME..."
|
||||||
|
/usr/libexec/PlistBuddy -c "Set :CFBundleName '$APP_NAME'" "$CHROMIUM_PLIST"
|
||||||
|
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName '$APP_NAME'" "$CHROMIUM_PLIST"
|
||||||
|
# Also update the localized strings if present
|
||||||
|
CHROMIUM_STRINGS="$APP_DIR/Contents/Resources/chromium/$(basename "$CHROME_APP")/Contents/Resources/en.lproj/InfoPlist.strings"
|
||||||
|
if [ -f "$CHROMIUM_STRINGS" ]; then
|
||||||
|
# InfoPlist.strings may be binary plist, convert to xml first
|
||||||
|
plutil -convert xml1 "$CHROMIUM_STRINGS" 2>/dev/null || true
|
||||||
|
sed -i '' "s/Google Chrome for Testing/$APP_NAME/g" "$CHROMIUM_STRINGS" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Step 4: Info.plist ──────────────────────────────────────────
|
||||||
|
cat > "$APP_DIR/Contents/Info.plist" << PLIST
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$APP_NAME</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>$APP_NAME</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$BUNDLE_ID</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$VERSION</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$VERSION</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>gstack-browser</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>12.0</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>icon</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.developer-tools</string>
|
||||||
|
<key>NSSupportsAutomaticTermination</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
PLIST
|
||||||
|
|
||||||
|
# ─── Step 5: App size report ────────────────────────────────────
|
||||||
|
APP_SIZE=$(du -sh "$APP_DIR" | cut -f1)
|
||||||
|
echo ""
|
||||||
|
echo " $APP_NAME.app: $APP_SIZE"
|
||||||
|
echo " Contents/MacOS/gstack-browser (launcher)"
|
||||||
|
echo " Contents/Resources/browse ($(du -sh "$APP_DIR/Contents/Resources/browse" | cut -f1))"
|
||||||
|
echo " Contents/Resources/extension/ ($(du -sh "$APP_DIR/Contents/Resources/extension" | cut -f1))"
|
||||||
|
echo " Contents/Resources/chromium/ ($(du -sh "$APP_DIR/Contents/Resources/chromium" | cut -f1))"
|
||||||
|
|
||||||
|
# ─── Step 6: DMG (optional) ─────────────────────────────────────
|
||||||
|
if [ "${1:-}" = "--no-dmg" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Done. App at: $APP_DIR"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
DMG_PATH="$BUILD_DIR/GStack-Browser.dmg"
|
||||||
|
echo ""
|
||||||
|
echo " Creating DMG..."
|
||||||
|
rm -f "$DMG_PATH"
|
||||||
|
|
||||||
|
# Create a temporary directory for DMG contents
|
||||||
|
DMG_TMP=$(mktemp -d)
|
||||||
|
cp -a "$APP_DIR" "$DMG_TMP/"
|
||||||
|
ln -s /Applications "$DMG_TMP/Applications"
|
||||||
|
|
||||||
|
hdiutil create -volname "$APP_NAME" \
|
||||||
|
-srcfolder "$DMG_TMP" \
|
||||||
|
-ov -format UDZO \
|
||||||
|
"$DMG_PATH" \
|
||||||
|
> /dev/null 2>&1
|
||||||
|
|
||||||
|
rm -rf "$DMG_TMP"
|
||||||
|
|
||||||
|
DMG_SIZE=$(du -sh "$DMG_PATH" | cut -f1)
|
||||||
|
echo " DMG: $DMG_SIZE → $DMG_PATH"
|
||||||
|
echo ""
|
||||||
|
echo "Done. Install: open $DMG_PATH"
|
||||||
Loading…
Reference in New Issue