From 2afadbbbb9042bd3e8fa029157902a08a4732347 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:17:49 -0800 Subject: [PATCH] Add bump-version script and improve reload script (#7) - bump-version.sh: Automated version bumping for marketing and build versions with major/minor/patch support - reload.sh: Enhanced with better process management, GhosttyKit rebuild detection, and improved error handling --- scripts/bump-version.sh | 63 +++++++++++ scripts/reload.sh | 243 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 294 insertions(+), 12 deletions(-) create mode 100755 scripts/bump-version.sh diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 00000000..d5a7b04e --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Bump MARKETING_VERSION and CURRENT_PROJECT_VERSION in the Xcode project. +# Usage: +# ./scripts/bump-version.sh # Auto-bump minor (1.15.0 -> 1.16.0) +# ./scripts/bump-version.sh 1.16.0 # Set specific version +# ./scripts/bump-version.sh patch # Bump patch (1.15.0 -> 1.15.1) +# ./scripts/bump-version.sh major # Bump major (1.15.0 -> 2.0.0) + +PROJECT_FILE="GhosttyTabs.xcodeproj/project.pbxproj" + +if [[ ! -f "$PROJECT_FILE" ]]; then + echo "Error: $PROJECT_FILE not found. Run from repo root." >&2 + exit 1 +fi + +# Get current versions +CURRENT_MARKETING=$(grep -m1 'MARKETING_VERSION = ' "$PROJECT_FILE" | sed 's/.*= \(.*\);/\1/') +CURRENT_BUILD=$(grep -m1 'CURRENT_PROJECT_VERSION = ' "$PROJECT_FILE" | sed 's/.*= \(.*\);/\1/') + +echo "Current: MARKETING_VERSION=$CURRENT_MARKETING, CURRENT_PROJECT_VERSION=$CURRENT_BUILD" + +# Parse current marketing version +IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_MARKETING" + +# Determine new marketing version +if [[ $# -eq 0 ]] || [[ "$1" == "minor" ]]; then + NEW_MARKETING="$MAJOR.$((MINOR + 1)).0" +elif [[ "$1" == "patch" ]]; then + NEW_MARKETING="$MAJOR.$MINOR.$((PATCH + 1))" +elif [[ "$1" == "major" ]]; then + NEW_MARKETING="$((MAJOR + 1)).0.0" +elif [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + NEW_MARKETING="$1" +else + echo "Usage: $0 [version|minor|patch|major]" >&2 + echo " version: specific version like 1.16.0" >&2 + echo " minor: bump minor version (default)" >&2 + echo " patch: bump patch version" >&2 + echo " major: bump major version" >&2 + exit 1 +fi + +# Always increment build number +NEW_BUILD=$((CURRENT_BUILD + 1)) + +echo "New: MARKETING_VERSION=$NEW_MARKETING, CURRENT_PROJECT_VERSION=$NEW_BUILD" + +# Update project file +sed -i '' "s/MARKETING_VERSION = $CURRENT_MARKETING;/MARKETING_VERSION = $NEW_MARKETING;/g" "$PROJECT_FILE" +sed -i '' "s/CURRENT_PROJECT_VERSION = $CURRENT_BUILD;/CURRENT_PROJECT_VERSION = $NEW_BUILD;/g" "$PROJECT_FILE" + +# Verify +UPDATED_MARKETING=$(grep -m1 'MARKETING_VERSION = ' "$PROJECT_FILE" | sed 's/.*= \(.*\);/\1/') +UPDATED_BUILD=$(grep -m1 'CURRENT_PROJECT_VERSION = ' "$PROJECT_FILE" | sed 's/.*= \(.*\);/\1/') + +if [[ "$UPDATED_MARKETING" != "$NEW_MARKETING" ]] || [[ "$UPDATED_BUILD" != "$NEW_BUILD" ]]; then + echo "Error: Version update failed!" >&2 + exit 1 +fi + +echo "Updated $PROJECT_FILE successfully." diff --git a/scripts/reload.sh b/scripts/reload.sh index a615e75b..ef0353d5 100755 --- a/scripts/reload.sh +++ b/scripts/reload.sh @@ -1,19 +1,238 @@ #!/usr/bin/env bash set -euo pipefail -xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination 'platform=macOS' build -pkill -x "cmuxterm DEV" || true +APP_NAME="cmuxterm DEV" +BUNDLE_ID="com.cmuxterm.app.debug" +BASE_APP_NAME="cmuxterm DEV" +DERIVED_DATA="" +NAME_SET=0 +BUNDLE_SET=0 +DERIVED_SET=0 +TAG="" + +usage() { + cat <<'EOF' +Usage: ./scripts/reload.sh [options] + +Options: + --tag 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" +} + +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 [[ -n "$TAG" ]]; then + TAG_ID="$(sanitize_bundle "$TAG")" + TAG_SLUG="$(sanitize_path "$TAG")" + if [[ "$NAME_SET" -eq 0 ]]; then + APP_NAME="cmuxterm 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/cmuxterm-${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) + +xcodebuild "${XCODEBUILD_ARGS[@]}" sleep 0.2 -APP_PATH="$( - find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/cmuxterm DEV.app" -print0 \ - | xargs -0 /usr/bin/stat -f "%m %N" 2>/dev/null \ - | sort -nr \ - | head -n 1 \ - | cut -d' ' -f2- -)" -if [[ -z "${APP_PATH}" ]]; then - echo "cmuxterm DEV.app not found in DerivedData" >&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/cmuxterm" + CMUXD_SOCKET="${APP_SUPPORT_DIR}/cmuxd-dev-${TAG_SLUG}.sock" + CMUX_SOCKET="/tmp/cmuxterm-debug-${TAG_SLUG}.sock" + /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" + 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 + +pkill -f "${APP_PATH}/Contents/MacOS/" || true +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 open "$APP_PATH" -osascript -e 'tell application "cmuxterm DEV" to activate' || true +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