#!/usr/bin/env bash # Remote commands: install, start, stop, smoke # # Host configuration (per-developer, via env or shell rc): # OSX_HOST — target macOS host for `install osx` / `start osx` / `stop osx` / `smoke osx` # Default: "plum" # LINUX_HOST — target Linux host for `install linux` / `start linux` / `stop linux` / `smoke linux` # Default: "apricot.local" (matches the RUN host in the CLAUDE.md two-host workflow) # IOS_DEVICE_ID — CoreDevice UUID for the iPhone target (launched via plum-attached xcrun) # Default: the author's device ID (override per-developer) : "${OSX_HOST:=plum}" : "${LINUX_HOST:=lilith@apricot.local}" : "${IOS_DEVICE_ID:=2FF5E256-27B9-5D56-89E5-B4DECCEFCE94}" cmd_install_osx() { local DEV_MODE=false local VERSION="" for arg in "$@"; do case "$arg" in --dev) DEV_MODE=true ;; *) VERSION="$arg" ;; esac done VERSION="${VERSION:-$(date +%Y%m%d_%H%M%S)}" local HOST="$OSX_HOST" local APP_NAME="Magic Civilization.app" local ZIP_NAME="MagicCivilization.zip" local EXPORT_FLAG="" local MODE_LABEL="release" if $DEV_MODE; then EXPORT_FLAG="--debug" MODE_LABEL="debug" fi echo -e "${BLUE}=== Install to macOS ($HOST) ===${NC}" echo -e "Version: ${GREEN}$VERSION${NC} (${MODE_LABEL})" echo "" echo -e "${YELLOW}[1/4] Exporting macOS ${MODE_LABEL} build...${NC}" if ! "$REPO_ROOT/tools/export-single.sh" macos "$VERSION" $EXPORT_FLAG 2>&1; then echo -e "${RED}Export failed.${NC}"; return 1 fi BUILD_ZIP="$REPO_ROOT/.local/build/godot/$VERSION/macos/$ZIP_NAME" [ -f "$BUILD_ZIP" ] || { echo -e "${RED}Build artifact not found: $BUILD_ZIP${NC}"; return 1; } echo -e "${GREEN} ✓ Exported $(du -h "$BUILD_ZIP" | cut -f1)${NC}" echo -e "${YELLOW}[2/4] Shipping to $HOST...${NC}" ssh -o ConnectTimeout=5 "$HOST" "echo ok" >/dev/null 2>&1 || { echo -e "${RED}Cannot reach $HOST${NC}"; return 1; } scp "$BUILD_ZIP" "$HOST:/tmp/$ZIP_NAME" echo -e "${GREEN} ✓ Uploaded${NC}" echo -e "${YELLOW}[3/4] Installing on $HOST...${NC}" INSTALL_RESULT=$(ssh "$HOST" bash <<'REMOTE_INSTALL' set -e APP_NAME="Magic Civilization.app" pkill -f "Magic Civilization" 2>/dev/null || true sleep 1 cd /tmp rm -rf "$APP_NAME" unzip -o MagicCivilization.zip > /dev/null 2>&1 xattr -cr "$APP_NAME" 2>/dev/null || true rm -rf "/Applications/$APP_NAME" mv "$APP_NAME" /Applications/ rm -f MagicCivilization.zip if [ -d "/Applications/$APP_NAME/Contents/MacOS" ]; then ARCH=$(file "/Applications/$APP_NAME/Contents/MacOS/Magic Civilization" | head -1 | sed 's/.*: //') echo "INSTALLED:$ARCH" else echo "INSTALL_FAIL" fi REMOTE_INSTALL ) if [[ "$INSTALL_RESULT" == INSTALLED:* ]]; then echo -e "${GREEN} ✓ Installed to /Applications/${NC}" else echo -e "${RED} ✗ Installation failed${NC}"; return 1 fi local RESOURCES_DIR="/Applications/$APP_NAME/Contents/Resources" if $DEV_MODE; then scp "$REPO_ROOT/.env.development" "$HOST:$RESOURCES_DIR/.env.development" scp "$REPO_ROOT/.env.production" "$HOST:$RESOURCES_DIR/.env" echo -e "${GREEN} ✓ Dev config deployed${NC}" else scp "$REPO_ROOT/.env.production" "$HOST:$RESOURCES_DIR/.env" ssh "$HOST" "rm -f '$RESOURCES_DIR/.env.development'" 2>/dev/null echo -e "${GREEN} ✓ Production config deployed${NC}" fi echo -e "${YELLOW}[4/4] Launching...${NC}" ssh "$HOST" 'open "/Applications/Magic Civilization.app"' 2>/dev/null & LAUNCH_PID=$(ssh "$HOST" bash <<'REMOTE_CHECK' for i in $(seq 1 10); do PID=$(pgrep -f "Magic Civilization" 2>/dev/null | head -1) [ -n "$PID" ] && echo "$PID" && exit 0 sleep 1 done REMOTE_CHECK ) [ -n "$LAUNCH_PID" ] && echo -e "${GREEN} ✓ Running (PID $LAUNCH_PID)${NC}" || echo -e "${YELLOW} ! Launched but could not confirm PID${NC}" echo "" echo -e "${GREEN}Installed and running on $HOST.${NC}" } cmd_install_ios() { local TARGET="$1"; shift local DEV_MODE=false; local VERSION="" for arg in "$@"; do case "$arg" in --dev) DEV_MODE=true ;; *) VERSION="$arg" ;; esac; done VERSION="${VERSION:-$(date +%Y%m%d_%H%M%S)}" local HOST="$OSX_HOST" # iOS builds still go via the macOS host (xcodebuild needed) local EXPORT_FLAG=""; local MODE_LABEL="release"; local XCODE_CONFIG="Release" if $DEV_MODE; then EXPORT_FLAG="--debug"; MODE_LABEL="debug"; XCODE_CONFIG="Debug"; fi echo -e "${BLUE}=== Install to iOS ($TARGET) via $HOST ===${NC}" echo -e "Version: ${GREEN}$VERSION${NC} (${MODE_LABEL})" echo "" if [ "$TARGET" = "sim" ]; then echo -e "${RED}iOS Simulator not supported — use ./run install iphone${NC}"; return 1 fi echo -e "${YELLOW}[1/4] Exporting iOS Xcode project...${NC}" "$REPO_ROOT/tools/export-single.sh" ios "$VERSION" $EXPORT_FLAG 2>&1 || { echo -e "${RED}Export failed.${NC}"; return 1; } local BUILD_DIR="$REPO_ROOT/.local/build/godot/$VERSION/ios" [ -d "$BUILD_DIR/MagicCivilization.xcodeproj" ] || { echo -e "${RED}Xcode project not found${NC}"; return 1; } echo -e "${GREEN} ✓ Xcode project exported${NC}" echo -e "${YELLOW}[2/4] Shipping to $HOST...${NC}" ssh -o ConnectTimeout=5 "$HOST" "echo ok" >/dev/null 2>&1 || { echo -e "${RED}Cannot reach $HOST${NC}"; return 1; } local REMOTE_BUILD="~/MagicCiv_iOS_Build" ssh "$HOST" "rm -rf $REMOTE_BUILD && mkdir -p $REMOTE_BUILD" rsync -az --progress "$BUILD_DIR/" "$HOST:$REMOTE_BUILD/" 2>&1 | tail -3 echo -e "${GREEN} ✓ Shipped to $HOST${NC}" echo -e "${YELLOW}[3/4] Building on $HOST with xcodebuild...${NC}" BUILD_RESULT=$(ssh "$HOST" bash <&1 | tail -5 echo "BUILD_OK" REMOTE_BUILD_CMD ) echo "$BUILD_RESULT" | grep -q "BUILD_OK" || { echo -e "${RED} ✗ xcodebuild failed${NC}"; echo "$BUILD_RESULT" | tail -5; return 1; } echo -e "${GREEN} ✓ Build succeeded${NC}" echo -e "${YELLOW}[4/4] Installing to device...${NC}" INSTALL_RESULT=$(ssh "$HOST" bash <<'REMOTE_DEVICE_INSTALL' set -e APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData -name "MagicCivilization.app" \ \( -path "*/Release-iphoneos/*" -o -path "*/Debug-iphoneos/*" \) 2>/dev/null | head -1) [ -n "$APP_PATH" ] || { echo "APP_NOT_FOUND"; exit 1; } codesign --force --sign "Apple Development: hinataliesterling@icloud.com (X8424J5CTB)" \ --timestamp=none --generate-entitlement-der "$APP_PATH" 2>&1 || true xcrun devicectl device install app --device "Natalie's iPhone" "$APP_PATH" 2>&1 | tail -3 echo "INSTALLED" REMOTE_DEVICE_INSTALL ) echo "$INSTALL_RESULT" | grep -q "INSTALLED" || { echo -e "${RED} ✗ Install failed${NC}"; echo "$INSTALL_RESULT"; return 1; } echo -e "${GREEN} ✓ Installed to $TARGET${NC}" echo "" echo -e "${GREEN}Deployed to $TARGET via $HOST.${NC}" } cmd_install_android() { local DEV_MODE=false; local VERSION="" for arg in "$@"; do case "$arg" in --dev) DEV_MODE=true ;; *) VERSION="$arg" ;; esac; done VERSION="${VERSION:-$(date +%Y%m%d_%H%M%S)}" local EXPORT_FLAG=""; local MODE_LABEL="release" if $DEV_MODE; then EXPORT_FLAG="--debug"; MODE_LABEL="debug"; fi local ADB="$HOME/Android/Sdk/platform-tools/adb" echo -e "${BLUE}=== Install to Android ===${NC}" echo -e "Version: ${GREEN}$VERSION${NC} (${MODE_LABEL})" echo "" "$REPO_ROOT/tools/export-single.sh" android "$VERSION" $EXPORT_FLAG 2>&1 || { echo -e "${RED}Export failed.${NC}"; return 1; } local APK="$REPO_ROOT/.local/build/godot/$VERSION/android/MagicCivilization.apk" [ -f "$APK" ] || { echo -e "${RED}APK not found: $APK${NC}"; return 1; } echo -e "${GREEN} ✓ Exported $(du -h "$APK" | cut -f1)${NC}" "$ADB" devices 2>/dev/null | grep -q "device$" || { echo -e "${YELLOW} No device connected. APK ready at: $APK${NC}"; return 0 } DEVICE=$("$ADB" devices | grep "device$" | head -1 | cut -f1) echo -e "${GREEN} ✓ Device: $DEVICE${NC}" "$ADB" install -r "$APK" 2>&1 | grep -q "Success" || { echo -e "${RED} ✗ Install failed${NC}"; return 1; } "$ADB" shell am start -n com.magicciv.game/com.godot.game.GodotApp 2>/dev/null echo -e "${GREEN} ✓ Installed and launched${NC}" echo "" echo -e "${GREEN}Deployed to Android.${NC}" } # ── Linux install/start/stop/smoke ─────────────────────────────────── # # Conventions (match the osx pattern): # - Build artifact: .local/build/godot/$VERSION/linux/MagicCivilization.x86_64 (+ .pck) # - Remote staging: ~/MagicCiv/ on $LINUX_HOST # - Process match: "MagicCivilization" (matches binary name for pgrep/pkill) # - Display env: WAYLAND_DISPLAY / XDG_RUNTIME_DIR / DISPLAY — forwarded from caller # or defaulted at launch time (wayland-0, /run/user/$(id -u), :0). _linux_binary_name() { echo "MagicCivilization.x86_64"; } _linux_remote_dir() { echo '$HOME/MagicCiv'; } # resolved remotely, not locally cmd_install_linux() { local DEV_MODE=false local VERSION="" for arg in "$@"; do case "$arg" in --dev) DEV_MODE=true ;; *) VERSION="$arg" ;; esac done VERSION="${VERSION:-$(date +%Y%m%d_%H%M%S)}" local HOST="$LINUX_HOST" local BIN_NAME BIN_NAME="$(_linux_binary_name)" local EXPORT_FLAG="" local MODE_LABEL="release" if $DEV_MODE; then EXPORT_FLAG="--debug"; MODE_LABEL="debug"; fi echo -e "${BLUE}=== Install to Linux ($HOST) ===${NC}" echo -e "Version: ${GREEN}$VERSION${NC} (${MODE_LABEL})" echo "" echo -e "${YELLOW}[1/4] Exporting Linux ${MODE_LABEL} build...${NC}" if ! "$REPO_ROOT/tools/export-single.sh" linux "$VERSION" $EXPORT_FLAG 2>&1; then echo -e "${RED}Export failed.${NC}"; return 1 fi local BUILD_DIR="$REPO_ROOT/.local/build/godot/$VERSION/linux" local BUILD_BIN="$BUILD_DIR/$BIN_NAME" [ -f "$BUILD_BIN" ] || { echo -e "${RED}Build artifact not found: $BUILD_BIN${NC}"; return 1; } echo -e "${GREEN} ✓ Exported $(du -h "$BUILD_BIN" | cut -f1)${NC}" echo -e "${YELLOW}[2/4] Shipping to $HOST...${NC}" ssh -o ConnectTimeout=5 "$HOST" "echo ok" >/dev/null 2>&1 || { echo -e "${RED}Cannot reach $HOST${NC}"; return 1; } # Stage remote dir + stop any running instance before overwriting ssh "$HOST" 'pkill -f "MagicCivilization" 2>/dev/null; mkdir -p "$HOME/MagicCiv"' # Ship the whole build directory (binary + .pck + any exported assets) rsync -az --delete "$BUILD_DIR/" "$HOST:$(_linux_remote_dir)/" 2>&1 | tail -3 ssh "$HOST" "chmod +x \"\$HOME/MagicCiv/$BIN_NAME\"" echo -e "${GREEN} ✓ Shipped${NC}" echo -e "${YELLOW}[3/4] Deploying config...${NC}" local ENV_FILE_SRC if $DEV_MODE; then ENV_FILE_SRC="$REPO_ROOT/.env.development" scp "$REPO_ROOT/.env.development" "$HOST:\$HOME/MagicCiv/.env.development" >/dev/null scp "$REPO_ROOT/.env.production" "$HOST:\$HOME/MagicCiv/.env" >/dev/null echo -e "${GREEN} ✓ Dev config deployed${NC}" else scp "$REPO_ROOT/.env.production" "$HOST:\$HOME/MagicCiv/.env" >/dev/null ssh "$HOST" 'rm -f "$HOME/MagicCiv/.env.development"' 2>/dev/null echo -e "${GREEN} ✓ Production config deployed${NC}" fi echo -e "${YELLOW}[4/4] Launching...${NC}" cmd_start_linux || echo -e "${YELLOW} ! Installed but launch did not confirm${NC}" echo "" echo -e "${GREEN}Installed on $HOST.${NC}" } cmd_start_linux() { local HOST="$LINUX_HOST" local BIN_NAME BIN_NAME="$(_linux_binary_name)" ssh -o ConnectTimeout=5 "$HOST" "echo ok" >/dev/null 2>&1 || { echo -e "${RED}Cannot reach $HOST${NC}"; return 1; } local EXISTING EXISTING=$(ssh "$HOST" 'pgrep -f "MagicCivilization" 2>/dev/null | head -1') [ -n "$EXISTING" ] && { echo -e "${YELLOW}Already running on $HOST (PID $EXISTING)${NC}"; return 0; } # Verify installed ssh "$HOST" "[ -x \"\$HOME/MagicCiv/$BIN_NAME\" ]" \ || { echo -e "${RED}Not installed on $HOST. Run: ./run install linux${NC}"; return 1; } # Launch detached with forwarded display env; rely on systemd-login's XDG_RUNTIME_DIR default. ssh "$HOST" bash </tmp/magic_civ_linux.log 2>&1 & disown REMOTE_LAUNCH local i PID for i in $(seq 1 10); do PID=$(ssh "$HOST" 'pgrep -f "MagicCivilization" 2>/dev/null | head -1') [ -n "$PID" ] && { echo -e "${GREEN}Running on $HOST (PID $PID)${NC}"; return 0; } sleep 1 done echo -e "${YELLOW}Launched on $HOST but could not confirm PID. Log: /tmp/magic_civ_linux.log${NC}" return 1 } cmd_stop_linux() { local HOST="$LINUX_HOST" ssh -o ConnectTimeout=5 "$HOST" "echo ok" >/dev/null 2>&1 || { echo -e "${RED}Cannot reach $HOST${NC}"; return 1; } local PID PID=$(ssh "$HOST" 'pgrep -f "MagicCivilization" 2>/dev/null | head -1') [ -z "$PID" ] && { echo -e "${YELLOW}Not running on $HOST.${NC}"; return 0; } ssh "$HOST" 'pkill -f "MagicCivilization"' 2>/dev/null echo -e "${GREEN}Stopped on $HOST (was PID $PID)${NC}" } cmd_smoke_linux() { # Minimal inline smoke test — no external smoke-test-linux.sh needed. # Install → wait for boot → screenshot via tools/screenshot.sh → stop. local HOST="$LINUX_HOST" local BOOT_WAIT="${1:-20}" echo -e "${BLUE}=== Linux smoke test ($HOST, ${BOOT_WAIT}s boot) ===${NC}" cmd_install_linux || { echo -e "${RED}Install failed — smoke aborted${NC}"; return 1; } echo -e "${YELLOW}Waiting ${BOOT_WAIT}s for boot...${NC}" sleep "$BOOT_WAIT" # Is it still running? local PID PID=$(ssh "$HOST" 'pgrep -f "MagicCivilization" 2>/dev/null | head -1') if [ -z "$PID" ]; then echo -e "${RED}✗ Process exited during boot — check /tmp/magic_civ_linux.log on $HOST${NC}" ssh "$HOST" 'tail -20 /tmp/magic_civ_linux.log' 2>/dev/null | head -20 return 1 fi echo -e "${GREEN} ✓ Running (PID $PID) after ${BOOT_WAIT}s${NC}" # Capture screenshot via the existing helper (host-aware via SCREENSHOT_HOST) "$REPO_ROOT/tools/screenshot.sh" "smoke_linux_$(date +%Y%m%d_%H%M%S)" || { echo -e "${YELLOW}Screenshot step reported non-zero — check tool output${NC}" } cmd_stop_linux echo -e "${GREEN}Linux smoke test complete.${NC}" } cmd_start_osx() { local HOST="$OSX_HOST" ssh -o ConnectTimeout=5 "$HOST" "echo ok" >/dev/null 2>&1 || { echo -e "${RED}Cannot reach $HOST${NC}"; return 1; } EXISTING=$(ssh "$HOST" 'pgrep -f "Magic Civilization" 2>/dev/null | head -1') [ -n "$EXISTING" ] && { echo -e "${YELLOW}Already running (PID $EXISTING)${NC}"; return 0; } ssh "$HOST" '[ -d "/Applications/Magic Civilization.app" ]' || { echo -e "${RED}Not installed. Run: ./run install osx${NC}"; return 1; } ssh "$HOST" 'open "/Applications/Magic Civilization.app"' 2>/dev/null for i in $(seq 1 10); do PID=$(ssh "$HOST" 'pgrep -f "Magic Civilization" 2>/dev/null | head -1') [ -n "$PID" ] && { echo -e "${GREEN}Running (PID $PID)${NC}"; return 0; } sleep 1 done echo -e "${YELLOW}Launched but could not confirm PID${NC}" } cmd_start_ios() { local HOST="$OSX_HOST" ssh -o ConnectTimeout=5 "$HOST" "echo ok" >/dev/null 2>&1 || { echo -e "${RED}Cannot reach $HOST${NC}"; return 1; } RESULT=$(ssh "$HOST" "xcrun devicectl device process launch --device $IOS_DEVICE_ID com.magicciv.game 2>&1") echo "$RESULT" | grep -q "launched" && echo -e "${GREEN}Launched on iPhone${NC}" || { echo -e "${RED}Launch failed${NC}"; echo "$RESULT" | grep -E "error:|NSLocalizedFailureReason" | head -3; return 1; } } cmd_stop_osx() { local HOST="$OSX_HOST" ssh -o ConnectTimeout=5 "$HOST" "echo ok" >/dev/null 2>&1 || { echo -e "${RED}Cannot reach $HOST${NC}"; return 1; } PID=$(ssh "$HOST" 'pgrep -f "Magic Civilization" 2>/dev/null | head -1') [ -z "$PID" ] && { echo -e "${YELLOW}Not running.${NC}"; return 0; } ssh "$HOST" 'pkill -f "Magic Civilization"' 2>/dev/null echo -e "${GREEN}Stopped (was PID $PID)${NC}" } cmd_smoke_osx() { "$REPO_ROOT/tools/dev/smoke-test-macos.sh" "$@" }