diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100755
index 0000000..2019b7e
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,24 @@
+#!/bin/bash
+# .githooks/pre-commit
+# Pre-commit hook for version validation
+
+set -e
+
+echo "🔍 Pre-commit: Validating version synchronization..."
+
+# Check if validation script exists
+if [ ! -f "scripts/validate-github-version-sync.sh" ]; then
+ echo "⚠️ Version validation script not found, skipping check"
+ exit 0
+fi
+
+# Run version validation (but don't fail the commit if GitHub CLI is unavailable)
+if ./scripts/validate-github-version-sync.sh 2>/dev/null; then
+ echo "✅ Pre-commit: Version validation passed"
+else
+ echo "⚠️ Pre-commit: Version validation failed or GitHub CLI unavailable"
+ echo "💡 Consider running: ./scripts/sync-version-from-github.sh"
+ echo "🔄 Continuing with commit (validation not enforced)"
+fi
+
+echo "✅ Pre-commit checks complete"
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index b6e2c89..287db07 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -439,6 +439,109 @@ echo 'export CODE_SIGN_IDENTITY="Apple Development: Your Name (TEAM_ID)"' >> ~/.
2. **Avoid concurrency conflicts**: Use proper `@MainActor` isolation without redundant dispatch
3. **Test permission changes**: Always test toggling permissions ON/OFF during development
+## Version Management System
+
+ClickIt uses an automated version management system that synchronizes version numbers between the UI, GitHub releases, and build processes.
+
+### Version Architecture
+
+**Single Source of Truth**: GitHub Release tags (e.g., `v1.4.15`)
+- **GitHub Release**: Latest published version
+- **Info.plist**: `CFBundleShortVersionString` (synced automatically)
+- **UI Display**: Reads from `Bundle.main.infoDictionary` at runtime
+- **Build Scripts**: Extract version from Info.plist (no hardcoding)
+
+### Version Management Scripts
+
+**Sync version with GitHub releases**:
+```bash
+./scripts/sync-version-from-github.sh
+```
+Automatically updates Info.plist to match the latest GitHub release version.
+
+**Validate version synchronization**:
+```bash
+./scripts/validate-github-version-sync.sh
+```
+Checks if local version matches GitHub release. Used in build validation.
+
+**Update to new version**:
+```bash
+./scripts/update-version.sh 1.5.0 # Creates GitHub release automatically
+./scripts/update-version.sh 1.5.0 false # Update without GitHub release
+```
+Complete version update workflow including Info.plist update, git commit, tag creation, and optional GitHub release trigger.
+
+### Fastlane Integration
+
+**Sync with GitHub**:
+```bash
+fastlane sync_version_with_github
+```
+
+**Release new version**:
+```bash
+fastlane release_with_github version:1.5.0
+```
+
+**Validate synchronization**:
+```bash
+fastlane validate_github_sync
+```
+
+### Git Hooks (Optional)
+
+**Install version validation hooks**:
+```bash
+./scripts/install-git-hooks.sh
+```
+Adds pre-commit hook that validates version synchronization before commits.
+
+### Build Integration
+
+Build scripts automatically:
+- Extract version from Info.plist
+- Validate sync with GitHub releases
+- Display version warnings if mismatched
+- Build with correct version in app bundle
+
+### Troubleshooting Version Issues
+
+**UI shows wrong version**:
+```bash
+# Sync Info.plist with GitHub release
+./scripts/sync-version-from-github.sh
+
+# Rebuild app bundle
+./build_app_unified.sh release
+```
+
+**Version mismatch detected**:
+```bash
+# Check current status
+./scripts/validate-github-version-sync.sh
+
+# Fix automatically
+./scripts/sync-version-from-github.sh
+```
+
+**Release new version**:
+```bash
+# Complete workflow (recommended)
+./scripts/update-version.sh 1.5.0
+
+# Or use Fastlane
+fastlane release_with_github version:1.5.0
+```
+
+### CI/CD Integration
+
+The GitHub Actions release workflow (`.github/workflows/release.yml`) automatically:
+- Validates version synchronization
+- Auto-fixes version mismatches
+- Verifies built app version matches tag
+- Creates releases with proper version metadata
+
## Documentation References
- Full product requirements: `docs/clickit_autoclicker_prd.md`
diff --git a/ClickIt/Info.plist b/ClickIt/Info.plist
index 563915c..102d62d 100644
--- a/ClickIt/Info.plist
+++ b/ClickIt/Info.plist
@@ -2,55 +2,51 @@
- CFBundleDisplayName
- ClickIt
- CFBundleExecutable
- ClickIt
- CFBundleIdentifier
- com.jsonify.ClickIt
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- ClickIt
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- 1.2.0
- CFBundleVersion
- $(CURRENT_PROJECT_VERSION)
- LSMinimumSystemVersion
- 14.0
- LSUIElement
-
- NSHighResolutionCapable
-
- NSSupportsAutomaticGraphicsSwitching
-
-
- NSAppleEventsUsageDescription
- ClickIt needs to send Apple Events to simulate mouse clicks in target applications.
- NSSystemAdministrationUsageDescription
- ClickIt requires accessibility access to simulate mouse clicks and detect window information.
-
- NSAccessibilityUsageDescription
- ClickIt needs accessibility access to control mouse clicks and interact with other applications.
- NSScreenCaptureUsageDescription
- ClickIt needs screen recording access to detect windows and provide visual feedback overlays.
-
- CFBundleIconFile
- AppIcon
- CFBundleIconName
- AppIcon
-
- SUFeedURL
- https://jsonify.github.io/ClickIt/appcast.xml
- SUPublicEDKey
- auto-generated-when-needed
- SUAutomaticallyUpdate
-
- SUEnableAutomaticChecks
-
- SUCheckAtStartup
-
+ CFBundleDisplayName
+ ClickIt
+ CFBundleExecutable
+ ClickIt
+ CFBundleIconFile
+ AppIcon
+ CFBundleIconName
+ AppIcon
+ CFBundleIdentifier
+ com.jsonify.ClickIt
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ ClickIt
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.4.15
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ LSMinimumSystemVersion
+ 14.0
+ LSUIElement
+
+ NSAccessibilityUsageDescription
+ ClickIt needs accessibility access to control mouse clicks and interact with other applications.
+ NSAppleEventsUsageDescription
+ ClickIt needs to send Apple Events to simulate mouse clicks in target applications.
+ NSHighResolutionCapable
+
+ NSScreenCaptureUsageDescription
+ ClickIt needs screen recording access to detect windows and provide visual feedback overlays.
+ NSSupportsAutomaticGraphicsSwitching
+
+ NSSystemAdministrationUsageDescription
+ ClickIt requires accessibility access to simulate mouse clicks and detect window information.
+ SUAutomaticallyUpdate
+
+ SUCheckAtStartup
+
+ SUEnableAutomaticChecks
+
+ SUFeedURL
+ https://jsonify.github.io/ClickIt/appcast.xml
+ SUPublicEDKey
+ auto-generated-when-needed
-
\ No newline at end of file
+
diff --git a/build_app_unified.sh b/build_app_unified.sh
index cd7d8fb..3c67345 100755
--- a/build_app_unified.sh
+++ b/build_app_unified.sh
@@ -9,10 +9,32 @@ BUILD_SYSTEM="${2:-auto}" # auto, spm, xcode
DIST_DIR="dist"
APP_NAME="ClickIt"
BUNDLE_ID="com.jsonify.clickit"
-VERSION="1.0.0"
+# Get version from Info.plist (synced with GitHub releases)
+get_version_from_plist() {
+ /usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ClickIt/Info.plist 2>/dev/null || echo "1.0.0"
+}
+
+VERSION=$(get_version_from_plist)
BUILD_NUMBER=$(date +%Y%m%d%H%M)
echo "🔨 Building $APP_NAME app bundle ($BUILD_MODE mode)..."
+echo "📦 Version: $VERSION (from Info.plist, synced with GitHub releases)"
+
+# Validate version is synchronized with GitHub (optional validation)
+if command -v gh > /dev/null 2>&1; then
+ GITHUB_TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || git describe --tags --abbrev=0 2>/dev/null || echo "")
+ if [ -n "$GITHUB_TAG" ]; then
+ GITHUB_VERSION=${GITHUB_TAG#v}
+ if [ "$VERSION" != "$GITHUB_VERSION" ]; then
+ echo "⚠️ Warning: Version mismatch detected!"
+ echo " Building: v$VERSION"
+ echo " GitHub: $GITHUB_TAG"
+ echo " Run './scripts/sync-version-from-github.sh' to sync versions"
+ else
+ echo "✅ Version synchronized with GitHub release $GITHUB_TAG"
+ fi
+ fi
+fi
# Detect build system
detect_build_system() {
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index a9bd5d4..571f571 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -175,6 +175,25 @@ platform :mac do
lane :dev do
UI.header("Starting ClickIt Development Workflow")
+ # Validate and auto-sync version before development
+ begin
+ UI.message("🔍 Checking version synchronization...")
+ validate_github_sync
+ UI.success("✅ Version already synchronized")
+ rescue => exception
+ UI.message("⚠️ Version mismatch detected: #{exception.message}")
+ UI.message("🔄 Auto-syncing version with GitHub release...")
+
+ begin
+ sync_version_with_github(auto_sync: true)
+ UI.success("✅ Version automatically synchronized for development")
+ rescue => sync_exception
+ UI.error("❌ Auto-sync failed: #{sync_exception.message}")
+ UI.message("💡 Manual fix required: ./scripts/sync-version-from-github.sh")
+ UI.message("🔄 Continuing with development workflow anyway...")
+ end
+ end
+
# Build debug and run
launch
@@ -386,6 +405,104 @@ platform :mac do
auto_prod(version: new_version)
end
+ desc "🔍 Validate GitHub version synchronization"
+ lane :validate_github_sync do
+ UI.header "🔍 GitHub Version Validation"
+
+ project_root = File.expand_path("..", Dir.pwd) if File.basename(Dir.pwd) == "fastlane"
+ project_root ||= Dir.pwd
+
+ Dir.chdir(project_root) do
+ begin
+ # Get versions
+ plist_version = sh("/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' ClickIt/Info.plist", log: false).strip
+ github_tag = sh("gh release list --limit 1 --json tagName --jq '.[0].tagName'", log: false).strip
+ github_version = github_tag.gsub(/^v/, '')
+
+ UI.message("📋 Version Status:")
+ UI.message(" Info.plist (UI): #{plist_version}")
+ UI.message(" GitHub Release: #{github_version}")
+
+ if plist_version != github_version
+ UI.error("")
+ UI.error("❌ VERSION MISMATCH DETECTED!")
+ UI.error(" The UI will show v#{plist_version}")
+ UI.error(" But the latest release is #{github_tag}")
+ UI.error("")
+ UI.error("🔧 To fix, run: fastlane sync_version_with_github")
+ UI.user_error!("Version synchronization required")
+ else
+ UI.success("✅ Versions are synchronized")
+ UI.success(" UI will display: v#{plist_version}")
+ UI.success(" GitHub release: #{github_tag}")
+ end
+
+ rescue => exception
+ UI.error("❌ GitHub CLI not available or authentication failed")
+ raise exception
+ end
+ end
+ end
+
+ desc "🔄 Sync version with latest GitHub release"
+ lane :sync_version_with_github do |options|
+ UI.header "🔄 Syncing Version with GitHub"
+
+ # Ensure we're in project root
+ project_root = File.expand_path("..", Dir.pwd) if File.basename(Dir.pwd) == "fastlane"
+ project_root ||= Dir.pwd
+
+ Dir.chdir(project_root) do
+ begin
+ # Try GitHub CLI first
+ latest_release = sh("gh release list --limit 1 --json tagName --jq '.[0].tagName'", log: false).strip
+ version = latest_release.gsub(/^v/, '')
+
+ UI.message("📦 Latest GitHub release: #{latest_release}")
+ UI.message("📝 Extracted version: #{version}")
+
+ rescue => gh_exception
+ # Fallback to git tags if GitHub CLI fails
+ UI.message("⚠️ GitHub CLI unavailable, falling back to git tags...")
+ begin
+ latest_tag = sh("git describe --tags --abbrev=0", log: false).strip
+ version = latest_tag.gsub(/^v/, '')
+ UI.message("📦 Latest git tag: #{latest_tag}")
+ UI.message("📝 Extracted version: #{version}")
+ rescue => git_exception
+ if options[:auto_sync]
+ UI.error("❌ Cannot determine latest version (GitHub CLI and git tags both failed)")
+ raise "Auto-sync failed: No version source available"
+ else
+ UI.error("❌ GitHub CLI not available or not authenticated")
+ UI.message("Install GitHub CLI: brew install gh")
+ UI.message("Authenticate: gh auth login")
+ raise gh_exception
+ end
+ end
+ end
+
+ # Get current Info.plist version
+ current_version = sh("/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' ClickIt/Info.plist", log: false).strip
+
+ if version != current_version
+ UI.message("⚠️ Version mismatch detected!")
+ UI.message(" Info.plist: #{current_version}")
+ UI.message(" Latest release/tag: #{version}")
+ UI.message("")
+ UI.message("🔧 Updating Info.plist to match latest version...")
+
+ # Update Info.plist
+ sh("/usr/libexec/PlistBuddy -c 'Set CFBundleShortVersionString #{version}' ClickIt/Info.plist")
+
+ UI.success("✅ Info.plist updated to v#{version}")
+ UI.success("🔄 UI will now display v#{version}")
+ else
+ UI.success("✅ Versions are synchronized (v#{version})")
+ end
+ end
+ end
+
error do |lane, exception|
UI.error("❌ Lane '#{lane}' failed with error: #{exception.message}")
end
diff --git a/scripts/install-git-hooks.sh b/scripts/install-git-hooks.sh
new file mode 100755
index 0000000..566cbef
--- /dev/null
+++ b/scripts/install-git-hooks.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+# scripts/install-git-hooks.sh
+# Install git hooks for version validation
+
+set -e
+
+echo "🔧 Installing git hooks for version validation..."
+
+# Check if we're in a git repository
+if [ ! -d ".git" ]; then
+ echo "❌ Not in a git repository"
+ exit 1
+fi
+
+# Create .git/hooks directory if it doesn't exist
+mkdir -p .git/hooks
+
+# Install pre-commit hook
+if [ -f ".githooks/pre-commit" ]; then
+ cp .githooks/pre-commit .git/hooks/pre-commit
+ chmod +x .git/hooks/pre-commit
+ echo "✅ Pre-commit hook installed"
+else
+ echo "❌ .githooks/pre-commit not found"
+ exit 1
+fi
+
+# Set git hooks path (optional - uses .githooks directly)
+git config core.hooksPath .githooks
+
+echo "🎉 Git hooks installation complete!"
+echo ""
+echo "📋 Installed hooks:"
+echo " • pre-commit: Version synchronization validation"
+echo ""
+echo "💡 The pre-commit hook will:"
+echo " • Validate version sync before each commit"
+echo " • Provide warnings if versions are mismatched"
+echo " • Not block commits (warnings only)"
+echo ""
+echo "🔧 To uninstall: git config --unset core.hooksPath"
\ No newline at end of file
diff --git a/scripts/sync-version-from-github.sh b/scripts/sync-version-from-github.sh
new file mode 100755
index 0000000..c09da6b
--- /dev/null
+++ b/scripts/sync-version-from-github.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+# scripts/sync-version-from-github.sh
+# Sync Info.plist version with latest GitHub release
+set -e
+
+echo "🔄 Syncing version from GitHub releases..."
+
+# Get latest GitHub release tag
+LATEST_TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || git describe --tags --abbrev=0)
+VERSION=${LATEST_TAG#v} # Remove 'v' prefix
+
+echo "📦 Latest GitHub release: $LATEST_TAG"
+echo "📝 Extracted version: $VERSION"
+
+# Current Info.plist version
+CURRENT_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ClickIt/Info.plist)
+
+if [ "$VERSION" != "$CURRENT_VERSION" ]; then
+ echo "⚠️ Version mismatch detected!"
+ echo " Info.plist: $CURRENT_VERSION"
+ echo " GitHub: $VERSION"
+ echo ""
+ echo "🔧 Updating Info.plist to match GitHub release..."
+
+ # Update Info.plist
+ /usr/libexec/PlistBuddy -c "Set CFBundleShortVersionString $VERSION" ClickIt/Info.plist
+
+ echo "✅ Info.plist updated to v$VERSION"
+ echo "🔄 UI will now display v$VERSION"
+else
+ echo "✅ Versions are synchronized (v$VERSION)"
+fi
+
+echo ""
+echo "📋 Final Status:"
+echo " GitHub Release: $LATEST_TAG"
+echo " Info.plist: v$VERSION"
+echo " UI will show: v$VERSION"
\ No newline at end of file
diff --git a/scripts/update-version.sh b/scripts/update-version.sh
new file mode 100755
index 0000000..853e8d9
--- /dev/null
+++ b/scripts/update-version.sh
@@ -0,0 +1,102 @@
+#!/bin/bash
+# scripts/update-version.sh
+# Enhanced version update with GitHub Release integration
+set -e
+
+NEW_VERSION="$1"
+CREATE_RELEASE="${2:-true}"
+
+if [ -z "$NEW_VERSION" ]; then
+ echo "Usage: $0 [create_release]"
+ echo ""
+ echo "Examples:"
+ echo " $0 1.5.0 # Update to 1.5.0 and trigger GitHub release"
+ echo " $0 1.5.0 false # Update to 1.5.0 without GitHub release"
+ echo ""
+ echo "This script will:"
+ echo " 1. Update Info.plist CFBundleShortVersionString"
+ echo " 2. Commit the change to git"
+ echo " 3. Create and push git tag"
+ echo " 4. Optionally trigger GitHub release via CI/CD"
+ exit 1
+fi
+
+echo "🔄 Updating ClickIt to version $NEW_VERSION"
+
+# Validate version format (basic semantic versioning)
+if [[ ! "$NEW_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "❌ Invalid version format. Use semantic versioning (e.g., 1.5.0)"
+ exit 1
+fi
+
+# Get current version
+CURRENT_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ClickIt/Info.plist)
+echo "📦 Current version: $CURRENT_VERSION"
+echo "📦 New version: $NEW_VERSION"
+
+# Check if version already exists
+if [ "$CURRENT_VERSION" = "$NEW_VERSION" ]; then
+ echo "⚠️ Version $NEW_VERSION is already current"
+ exit 0
+fi
+
+# Check if git tag already exists
+if git rev-parse "v$NEW_VERSION" >/dev/null 2>&1; then
+ echo "❌ Git tag v$NEW_VERSION already exists"
+ exit 1
+fi
+
+# Update Info.plist
+echo "🔧 Updating Info.plist..."
+/usr/libexec/PlistBuddy -c "Set CFBundleShortVersionString $NEW_VERSION" ClickIt/Info.plist
+
+# Verify the change
+UPDATED_VERSION=$(/usr/libexec/PListBuddy -c "Print CFBundleShortVersionString" ClickIt/Info.plist)
+if [ "$UPDATED_VERSION" != "$NEW_VERSION" ]; then
+ echo "❌ Failed to update Info.plist"
+ exit 1
+fi
+
+echo "✅ Info.plist updated to v$NEW_VERSION"
+
+# Git operations
+echo "📝 Committing changes..."
+git add ClickIt/Info.plist
+git commit -m "chore: bump version to v$NEW_VERSION
+
+- Update CFBundleShortVersionString to $NEW_VERSION
+- UI will now display v$NEW_VERSION
+- Synchronized with GitHub release workflow"
+
+# Create and push tag
+echo "🏷️ Creating git tag v$NEW_VERSION..."
+git tag "v$NEW_VERSION"
+
+echo "🚀 Pushing to remote..."
+git push origin main
+git push origin "v$NEW_VERSION"
+
+if [ "$CREATE_RELEASE" = "true" ]; then
+ echo ""
+ echo "🎉 Version v$NEW_VERSION pushed successfully!"
+ echo ""
+ echo "🚀 GitHub Release will be created automatically:"
+ echo " - Monitor CI/CD: https://github.com/$(git remote get-url origin | sed 's/.*github.com[:/]\(.*\)\.git/\1/')/actions"
+ echo " - Release will be at: https://github.com/$(git remote get-url origin | sed 's/.*github.com[:/]\(.*\)\.git/\1/')/releases/tag/v$NEW_VERSION"
+ echo ""
+ echo "📦 The release will include:"
+ echo " - Universal macOS app bundle (ClickIt.app.zip)"
+ echo " - Automatic release notes with changelog"
+ echo " - Build metadata and verification"
+else
+ echo ""
+ echo "📝 Version v$NEW_VERSION updated locally without GitHub release"
+ echo " - Git tag created and pushed"
+ echo " - To create release later, push the tag: git push origin v$NEW_VERSION"
+fi
+
+echo ""
+echo "✅ Version update complete!"
+echo " Previous: v$CURRENT_VERSION"
+echo " Current: v$NEW_VERSION"
+echo " UI will display: v$NEW_VERSION"
\ No newline at end of file
diff --git a/scripts/validate-github-version-sync.sh b/scripts/validate-github-version-sync.sh
new file mode 100755
index 0000000..5257ddf
--- /dev/null
+++ b/scripts/validate-github-version-sync.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+# scripts/validate-github-version-sync.sh
+# Validate version synchronization between Info.plist and GitHub releases
+set -e
+
+echo "🔍 Validating version synchronization with GitHub..."
+
+# Get versions
+PLIST_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ClickIt/Info.plist)
+GITHUB_TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || git describe --tags --abbrev=0)
+GITHUB_VERSION=${GITHUB_TAG#v}
+
+echo "📋 Version Status:"
+echo " Info.plist (UI): $PLIST_VERSION"
+echo " GitHub Release: $GITHUB_VERSION"
+
+if [ "$PLIST_VERSION" != "$GITHUB_VERSION" ]; then
+ echo ""
+ echo "❌ VERSION MISMATCH DETECTED!"
+ echo " The UI will show v$PLIST_VERSION"
+ echo " But the latest release is $GITHUB_TAG"
+ echo ""
+ echo "🔧 To fix, run: ./scripts/sync-version-from-github.sh"
+ exit 1
+else
+ echo "✅ Versions are synchronized"
+ echo " UI will display: v$PLIST_VERSION"
+ echo " GitHub release: $GITHUB_TAG"
+fi
\ No newline at end of file