#!/usr/bin/env bash set -euo pipefail APP_NAME="cmux DEV" BUNDLE_ID="com.cmuxterm.app.debug" BASE_APP_NAME="cmux DEV" DERIVED_DATA="" NAME_SET=0 BUNDLE_SET=0 DERIVED_SET=0 TAG="" CMUX_DEBUG_LOG="" usage() { cat <<'EOF' Usage: ./scripts/reload.sh --tag [options] Options: --tag Required. Short tag for parallel builds (e.g., feature-xyz-lol). Sets app name, bundle id, and derived data path unless overridden. --name Override app display/bundle name. --bundle-id Override bundle identifier. --derived-data Override derived data path. -h, --help Show this help. EOF } sanitize_bundle() { local raw="$1" local cleaned cleaned="$(echo "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/./g; s/^\\.+//; s/\\.+$//; s/\\.+/./g')" if [[ -z "$cleaned" ]]; then cleaned="agent" fi echo "$cleaned" } sanitize_path() { local raw="$1" local cleaned cleaned="$(echo "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')" if [[ -z "$cleaned" ]]; then cleaned="agent" fi echo "$cleaned" } print_tag_cleanup_reminder() { local current_slug="$1" local path="" local tag="" local seen=" " local -a stale_tags=() while IFS= read -r -d '' path; do tag="${path#/tmp/cmux-}" if [[ "$tag" == "$current_slug" ]]; then continue fi # Only surface stale debug tag builds. if [[ ! -d "$path/Build/Products/Debug" ]]; then continue fi if [[ "$seen" == *" $tag "* ]]; then continue fi seen="${seen}${tag} " stale_tags+=("$tag") done < <(find /tmp -maxdepth 1 -type d -name 'cmux-*' -print0 2>/dev/null) echo echo "Tag cleanup status:" echo " current tag: ${current_slug} (keep this running until you verify)" if [[ "${#stale_tags[@]}" -eq 0 ]]; then echo " stale tags: none" echo " stale cleanup: not needed" else echo " stale tags:" for tag in "${stale_tags[@]}"; do echo " - ${tag}" done echo "Cleanup stale tags only:" for tag in "${stale_tags[@]}"; do echo " pkill -f \"cmux DEV ${tag}.app/Contents/MacOS/cmux DEV\"" echo " rm -rf \"/tmp/cmux-${tag}\" \"/tmp/cmux-debug-${tag}.sock\"" echo " rm -f \"/tmp/cmux-debug-${tag}.log\"" echo " rm -f \"$HOME/Library/Application Support/cmux/cmuxd-dev-${tag}.sock\"" done fi echo "After you verify current tag, cleanup command:" echo " pkill -f \"cmux DEV ${current_slug}.app/Contents/MacOS/cmux DEV\"" echo " rm -rf \"/tmp/cmux-${current_slug}\" \"/tmp/cmux-debug-${current_slug}.sock\"" echo " rm -f \"/tmp/cmux-debug-${current_slug}.log\"" echo " rm -f \"$HOME/Library/Application Support/cmux/cmuxd-dev-${current_slug}.sock\"" } while [[ $# -gt 0 ]]; do case "$1" in --tag) TAG="${2:-}" if [[ -z "$TAG" ]]; then echo "error: --tag requires a value" >&2 exit 1 fi shift 2 ;; --name) APP_NAME="${2:-}" if [[ -z "$APP_NAME" ]]; then echo "error: --name requires a value" >&2 exit 1 fi NAME_SET=1 shift 2 ;; --bundle-id) BUNDLE_ID="${2:-}" if [[ -z "$BUNDLE_ID" ]]; then echo "error: --bundle-id requires a value" >&2 exit 1 fi BUNDLE_SET=1 shift 2 ;; --derived-data) DERIVED_DATA="${2:-}" if [[ -z "$DERIVED_DATA" ]]; then echo "error: --derived-data requires a value" >&2 exit 1 fi DERIVED_SET=1 shift 2 ;; -h|--help) usage exit 0 ;; *) echo "error: unknown option $1" >&2 usage exit 1 ;; esac done if [[ -z "$TAG" ]]; then echo "error: --tag is required (example: ./scripts/reload.sh --tag fix-sidebar-theme)" >&2 usage exit 1 fi if [[ -n "$TAG" ]]; then TAG_ID="$(sanitize_bundle "$TAG")" TAG_SLUG="$(sanitize_path "$TAG")" if [[ "$NAME_SET" -eq 0 ]]; then APP_NAME="cmux DEV ${TAG}" fi if [[ "$BUNDLE_SET" -eq 0 ]]; then BUNDLE_ID="com.cmuxterm.app.debug.${TAG_ID}" fi if [[ "$DERIVED_SET" -eq 0 ]]; then DERIVED_DATA="/tmp/cmux-${TAG_SLUG}" fi fi XCODEBUILD_ARGS=( -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination 'platform=macOS' ) if [[ -n "$DERIVED_DATA" ]]; then XCODEBUILD_ARGS+=(-derivedDataPath "$DERIVED_DATA") fi if [[ -z "$TAG" ]]; then XCODEBUILD_ARGS+=( INFOPLIST_KEY_CFBundleName="$APP_NAME" INFOPLIST_KEY_CFBundleDisplayName="$APP_NAME" PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID" ) fi XCODEBUILD_ARGS+=(build) XCODE_LOG="/tmp/cmux-xcodebuild-${TAG_SLUG}.log" xcodebuild "${XCODEBUILD_ARGS[@]}" 2>&1 | tee "$XCODE_LOG" | grep -E '(warning:|error:|fatal:|BUILD FAILED|BUILD SUCCEEDED|\*\* BUILD)' || true XCODE_EXIT="${PIPESTATUS[0]}" echo "Full build log: $XCODE_LOG" if [[ "$XCODE_EXIT" -ne 0 ]]; then echo "error: xcodebuild failed with exit code $XCODE_EXIT" >&2 exit "$XCODE_EXIT" fi sleep 0.2 FALLBACK_APP_NAME="$BASE_APP_NAME" SEARCH_APP_NAME="$APP_NAME" if [[ -n "$TAG" ]]; then SEARCH_APP_NAME="$BASE_APP_NAME" fi if [[ -n "$DERIVED_DATA" ]]; then APP_PATH="${DERIVED_DATA}/Build/Products/Debug/${SEARCH_APP_NAME}.app" if [[ ! -d "${APP_PATH}" && "$SEARCH_APP_NAME" != "$FALLBACK_APP_NAME" ]]; then APP_PATH="${DERIVED_DATA}/Build/Products/Debug/${FALLBACK_APP_NAME}.app" fi else APP_BINARY="$( find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/${SEARCH_APP_NAME}.app/Contents/MacOS/${SEARCH_APP_NAME}" -print0 \ | xargs -0 /usr/bin/stat -f "%m %N" 2>/dev/null \ | sort -nr \ | head -n 1 \ | cut -d' ' -f2- )" if [[ -n "${APP_BINARY}" ]]; then APP_PATH="$(dirname "$(dirname "$(dirname "$APP_BINARY")")")" fi if [[ -z "${APP_PATH}" && "$SEARCH_APP_NAME" != "$FALLBACK_APP_NAME" ]]; then APP_BINARY="$( find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/${FALLBACK_APP_NAME}.app/Contents/MacOS/${FALLBACK_APP_NAME}" -print0 \ | xargs -0 /usr/bin/stat -f "%m %N" 2>/dev/null \ | sort -nr \ | head -n 1 \ | cut -d' ' -f2- )" if [[ -n "${APP_BINARY}" ]]; then APP_PATH="$(dirname "$(dirname "$(dirname "$APP_BINARY")")")" fi fi fi if [[ -z "${APP_PATH}" || ! -d "${APP_PATH}" ]]; then echo "${APP_NAME}.app not found in DerivedData" >&2 exit 1 fi if [[ -n "$TAG" && "$APP_NAME" != "$SEARCH_APP_NAME" ]]; then TAG_APP_PATH="$(dirname "$APP_PATH")/${APP_NAME}.app" rm -rf "$TAG_APP_PATH" cp -R "$APP_PATH" "$TAG_APP_PATH" INFO_PLIST="$TAG_APP_PATH/Contents/Info.plist" if [[ -f "$INFO_PLIST" ]]; then /usr/libexec/PlistBuddy -c "Set :CFBundleName $APP_NAME" "$INFO_PLIST" 2>/dev/null \ || /usr/libexec/PlistBuddy -c "Add :CFBundleName string $APP_NAME" "$INFO_PLIST" /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $APP_NAME" "$INFO_PLIST" 2>/dev/null \ || /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string $APP_NAME" "$INFO_PLIST" /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_ID" "$INFO_PLIST" 2>/dev/null \ || /usr/libexec/PlistBuddy -c "Add :CFBundleIdentifier string $BUNDLE_ID" "$INFO_PLIST" if [[ -n "${TAG_SLUG:-}" ]]; then APP_SUPPORT_DIR="$HOME/Library/Application Support/cmux" CMUXD_SOCKET="${APP_SUPPORT_DIR}/cmuxd-dev-${TAG_SLUG}.sock" CMUX_SOCKET="/tmp/cmux-debug-${TAG_SLUG}.sock" CMUX_DEBUG_LOG="/tmp/cmux-debug-${TAG_SLUG}.log" echo "$CMUX_SOCKET" > /tmp/cmux-last-socket-path || true echo "$CMUX_DEBUG_LOG" > /tmp/cmux-last-debug-log-path || true /usr/libexec/PlistBuddy -c "Add :LSEnvironment dict" "$INFO_PLIST" 2>/dev/null || true /usr/libexec/PlistBuddy -c "Set :LSEnvironment:CMUXD_UNIX_PATH \"${CMUXD_SOCKET}\"" "$INFO_PLIST" 2>/dev/null \ || /usr/libexec/PlistBuddy -c "Add :LSEnvironment:CMUXD_UNIX_PATH string \"${CMUXD_SOCKET}\"" "$INFO_PLIST" /usr/libexec/PlistBuddy -c "Set :LSEnvironment:CMUX_SOCKET_PATH \"${CMUX_SOCKET}\"" "$INFO_PLIST" 2>/dev/null \ || /usr/libexec/PlistBuddy -c "Add :LSEnvironment:CMUX_SOCKET_PATH string \"${CMUX_SOCKET}\"" "$INFO_PLIST" /usr/libexec/PlistBuddy -c "Set :LSEnvironment:CMUX_DEBUG_LOG \"${CMUX_DEBUG_LOG}\"" "$INFO_PLIST" 2>/dev/null \ || /usr/libexec/PlistBuddy -c "Add :LSEnvironment:CMUX_DEBUG_LOG string \"${CMUX_DEBUG_LOG}\"" "$INFO_PLIST" if [[ -S "$CMUXD_SOCKET" ]]; then for PID in $(lsof -t "$CMUXD_SOCKET" 2>/dev/null); do kill "$PID" 2>/dev/null || true done rm -f "$CMUXD_SOCKET" fi if [[ -S "$CMUX_SOCKET" ]]; then rm -f "$CMUX_SOCKET" fi fi /usr/bin/codesign --force --sign - --timestamp=none --generate-entitlement-der "$TAG_APP_PATH" >/dev/null 2>&1 || true fi APP_PATH="$TAG_APP_PATH" fi # Ensure any running instance is fully terminated, regardless of DerivedData path. /usr/bin/osascript -e "tell application id \"${BUNDLE_ID}\" to quit" >/dev/null 2>&1 || true sleep 0.3 if [[ -z "$TAG" ]]; then # Non-tag mode: kill any running instance (across any DerivedData path) to avoid socket conflicts. pkill -f "/${BASE_APP_NAME}.app/Contents/MacOS/${BASE_APP_NAME}" || true else # Tag mode: only kill the tagged instance; allow side-by-side with the main app. pkill -f "${APP_NAME}.app/Contents/MacOS/${BASE_APP_NAME}" || true fi sleep 0.3 CMUXD_SRC="$PWD/cmuxd/zig-out/bin/cmuxd" if [[ -d "$PWD/cmuxd" ]]; then (cd "$PWD/cmuxd" && zig build -Doptimize=ReleaseFast) fi if [[ -x "$CMUXD_SRC" ]]; then BIN_DIR="$APP_PATH/Contents/Resources/bin" mkdir -p "$BIN_DIR" cp "$CMUXD_SRC" "$BIN_DIR/cmuxd" chmod +x "$BIN_DIR/cmuxd" fi # Avoid inheriting cmux/ghostty environment variables from the terminal that # runs this script (often inside another cmux instance), which can cause # socket and resource-path conflicts. OPEN_CLEAN_ENV=( env -u CMUX_SOCKET_PATH -u CMUX_TAB_ID -u CMUX_PANEL_ID -u CMUXD_UNIX_PATH -u CMUX_TAG -u CMUX_DEBUG_LOG -u CMUX_BUNDLE_ID -u CMUX_SHELL_INTEGRATION -u GHOSTTY_BIN_DIR -u GHOSTTY_RESOURCES_DIR -u GHOSTTY_SHELL_FEATURES # Dev shells (including CI/Codex) often force-disable paging by exporting these. # Don't leak that into cmux, otherwise `git diff` won't page even with PAGER=less. -u GIT_PAGER -u GH_PAGER -u TERMINFO -u XDG_DATA_DIRS ) if [[ -n "${TAG_SLUG:-}" && -n "${CMUX_SOCKET:-}" ]]; then # Ensure tag-specific socket paths win even if the caller has CMUX_* overrides. "${OPEN_CLEAN_ENV[@]}" CMUX_TAG="$TAG_SLUG" CMUX_SOCKET_PATH="$CMUX_SOCKET" CMUXD_UNIX_PATH="$CMUXD_SOCKET" CMUX_DEBUG_LOG="$CMUX_DEBUG_LOG" open "$APP_PATH" elif [[ -n "${TAG_SLUG:-}" ]]; then "${OPEN_CLEAN_ENV[@]}" CMUX_TAG="$TAG_SLUG" CMUX_DEBUG_LOG="$CMUX_DEBUG_LOG" open "$APP_PATH" else echo "/tmp/cmux-debug.log" > /tmp/cmux-last-debug-log-path || true "${OPEN_CLEAN_ENV[@]}" open "$APP_PATH" fi osascript -e "tell application id \"${BUNDLE_ID}\" to activate" || true # Safety: ensure only one instance is running. sleep 0.2 PIDS=($(pgrep -f "${APP_PATH}/Contents/MacOS/" || true)) if [[ "${#PIDS[@]}" -gt 1 ]]; then NEWEST_PID="" NEWEST_AGE=999999 for PID in "${PIDS[@]}"; do AGE="$(ps -o etimes= -p "$PID" | tr -d ' ')" if [[ -n "$AGE" && "$AGE" -lt "$NEWEST_AGE" ]]; then NEWEST_AGE="$AGE" NEWEST_PID="$PID" fi done for PID in "${PIDS[@]}"; do if [[ "$PID" != "$NEWEST_PID" ]]; then kill "$PID" 2>/dev/null || true fi done fi if [[ -n "${TAG_SLUG:-}" ]]; then print_tag_cleanup_reminder "$TAG_SLUG" fi