Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 95 additions & 62 deletions .github/workflows/cd-swift-cua-driver.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ env:
DEVELOPER_NAME: ${{ secrets.DEVELOPER_NAME }}

jobs:
# Build on macos-15 (Apple Silicon). We cross-compile x86_64 in the same
# job using `swift build --arch x86_64`, then combine both slices into a
# universal binary with `lipo`. This avoids needing a separate Intel runner
# (macos-13 only has Xcode 15.x which cannot build swift-tools-version: 6.0).
notarize:
runs-on: macos-15
outputs:
Expand Down Expand Up @@ -75,9 +79,6 @@ jobs:
elif [[ -n "${{ inputs.version }}" ]]; then
VERSION="${{ inputs.version }}"
echo "Using version from input: $VERSION"
elif [[ -n "${{ inputs.version }}" ]]; then
VERSION="${{ inputs.version }}"
echo "Using version from workflow_call input: $VERSION"
else
echo "Error: No version found in tag or input"
exit 1
Expand Down Expand Up @@ -137,58 +138,113 @@ jobs:
# Clean up certificate files
rm application.p12 installer.p12

- name: Build and Notarize
id: build_notarize
- name: Build arm64 + x86_64 and create universal binary
id: build_universal
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.TEAM_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
# These will now reference the imported certificates
CERT_APPLICATION_NAME: "Developer ID Application: ${{ secrets.DEVELOPER_NAME }} (${{ secrets.TEAM_ID }})"
CERT_INSTALLER_NAME: "Developer ID Installer: ${{ secrets.DEVELOPER_NAME }} (${{ secrets.TEAM_ID }})"
VERSION: ${{ steps.set_version.outputs.version }}
working-directory: ./libs/cua-driver
run: |
# Minimal debug information
echo "Starting build process..."
echo "Swift version: $(swift --version | head -n 1)"
echo "Building version: $VERSION"

# Ensure .release directory exists
mkdir -p .release
chmod 755 .release

# Build the project first (redirect verbose output)
echo "Building project..."
swift build --configuration release > build.log 2>&1
echo "Build completed."
# Build arm64 (native on macos-15)
echo "Building arm64..."
swift build --configuration release --arch arm64 > build-arm64.log 2>&1
echo "arm64 build complete."

# Cross-compile x86_64 using the same Xcode 16 toolchain
echo "Cross-compiling x86_64..."
swift build --configuration release --arch x86_64 > build-x86_64.log 2>&1
echo "x86_64 build complete."

ARM64_BIN=".build/arm64-apple-macosx/release/cua-driver"
X86_BIN=".build/x86_64-apple-macosx/release/cua-driver"

if [ ! -f "$ARM64_BIN" ] || [ ! -f "$X86_BIN" ]; then
echo "Error: expected binaries not found"
ls .build/*/release/cua-driver 2>/dev/null || true
exit 1
fi

# Run the notarization script with LOG_LEVEL env var
# Create universal binary
echo "Creating universal binary..."
lipo -create "$ARM64_BIN" "$X86_BIN" -output .build/cua-driver-universal
lipo -info .build/cua-driver-universal

# The notarization script builds from source; we'll inject the
# universal binary into the .app after the script runs.
# First, run the script to get the signed .app structure.
chmod +x scripts/build/build-release-notarized.sh
cd scripts/build
LOG_LEVEL=minimal ./build-release-notarized.sh

# Return to the cua-driver directory
cd ../..

# Debug: List what files were actually created
# Replace the single-arch binary inside the .app with the universal one.
APP_BINARY=".release/CuaDriver.app/Contents/MacOS/cua-driver"
if [ -f "$APP_BINARY" ]; then
cp .build/cua-driver-universal "$APP_BINARY"
# Re-sign the binary (the .app is already signed; we need to
# re-sign just the binary slice we replaced).
codesign --force --sign "$CERT_APPLICATION_NAME" \
--options runtime \
--entitlements scripts/build/entitlements.plist \
"$APP_BINARY" || true
echo "Universal binary injected and re-signed."
else
echo "Warning: app binary not found at $APP_BINARY — using architecture-specific release."
fi

echo "Files in .release directory:"
find .release -type f -name "*.tar.gz" -o -name "*.pkg.tar.gz"
Comment thread
ddupont808 marked this conversation as resolved.

# Get architecture for output filename
ARCH=$(uname -m)
OS_IDENTIFIER="darwin-${ARCH}"
VERSION_OUT="${VERSION}"
echo "arm64_tarball_path=.release/cua-driver-${VERSION_OUT}-darwin-arm64.tar.gz" >> $GITHUB_OUTPUT
echo "x86_tarball_path=.release/cua-driver-${VERSION_OUT}-darwin-x86_64.tar.gz" >> $GITHUB_OUTPUT
echo "pkg_path=.release/cua-driver-${VERSION_OUT}-darwin-arm64.pkg.tar.gz" >> $GITHUB_OUTPUT

- name: Package per-arch tarballs
working-directory: ./libs/cua-driver/.release
env:
VERSION: ${{ steps.set_version.outputs.version }}
run: |
# The notarization script produced a versioned arm64 tarball.
# Also produce an x86_64 tarball using the same .app (now universal).
# Callers that download by arch name get the same universal binary.
ARM64_TAR="cua-driver-${VERSION}-darwin-arm64.tar.gz"
X86_TAR="cua-driver-${VERSION}-darwin-x86_64.tar.gz"
UNIVERSAL_TAR="cua-driver-${VERSION}-darwin-universal.tar.gz"

# Rename existing tarball to arm64 if needed
EXISTING=$(ls cua-driver-${VERSION}-darwin-*.tar.gz 2>/dev/null | head -1)
if [ -n "$EXISTING" ] && [ "$EXISTING" != "$ARM64_TAR" ]; then
mv "$EXISTING" "$ARM64_TAR"
fi

# x86_64 and universal tarballs are identical (same universal binary inside)
cp "$ARM64_TAR" "$X86_TAR"
cp "$ARM64_TAR" "$UNIVERSAL_TAR"

# Convenience aliases (always-latest links)
ln -sf "$UNIVERSAL_TAR" "cua-driver-darwin.tar.gz"
ln -sf "$UNIVERSAL_TAR" "cua-driver.tar.gz"
Comment thread
ddupont808 marked this conversation as resolved.

# Output paths for later use
echo "tarball_path=.release/cua-driver-${VERSION}-${OS_IDENTIFIER}.tar.gz" >> $GITHUB_OUTPUT
echo "pkg_path=.release/cua-driver-${VERSION}-${OS_IDENTIFIER}.pkg.tar.gz" >> $GITHUB_OUTPUT
echo "Tarballs:"
ls -lh cua-driver-*.tar.gz

- name: Upload build log on failure
if: failure() && steps.build_notarize.outcome == 'failure'
- name: Upload build logs on failure
if: failure() && steps.build_universal.outcome == 'failure'
uses: actions/upload-artifact@v4
with:
name: swift-build-log
path: ./libs/cua-driver/build.log
name: swift-build-logs
path: |
./libs/cua-driver/build-arm64.log
./libs/cua-driver/build-x86_64.log
retention-days: 7

- name: Generate SHA256 Checksums
Expand All @@ -211,43 +267,20 @@ jobs:
echo "$checksums" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

# Debug: Show all files in the release directory
echo "All files in release directory:"
ls -la

- name: Create Standard Version Releases
working-directory: ./libs/cua-driver/.release
run: |
VERSION=${{ steps.set_version.outputs.version }}
ARCH=$(uname -m)
OS_IDENTIFIER="darwin-${ARCH}"

# Create OS-tagged symlinks
ln -sf "cua-driver-${VERSION}-${OS_IDENTIFIER}.tar.gz" "cua-driver-darwin.tar.gz"
ln -sf "cua-driver-${VERSION}-${OS_IDENTIFIER}.pkg.tar.gz" "cua-driver-darwin.pkg.tar.gz"

# Create simple symlinks
ln -sf "cua-driver-${VERSION}-${OS_IDENTIFIER}.tar.gz" "cua-driver.tar.gz"
ln -sf "cua-driver-${VERSION}-${OS_IDENTIFIER}.pkg.tar.gz" "cua-driver.pkg.tar.gz"

# List all files (including symlinks)
echo "Files with symlinks in release directory:"
ls -la

- name: Package bare binary
working-directory: ./libs/cua-driver/.release
run: |
VERSION=${{ steps.set_version.outputs.version }}
ARCH=$(uname -m)
OS_IDENTIFIER="darwin-${ARCH}"

# The bare binary is already signed (it lives inside the signed .app).
# Re-extract it and package it for embedders who build their own bundle.
# Bare binary is the universal one we injected into the .app.
BINARY="CuaDriver.app/Contents/MacOS/cua-driver"
if [ -f "$BINARY" ]; then
tar -czf "cua-driver-${VERSION}-${OS_IDENTIFIER}-binary.tar.gz" -C "CuaDriver.app/Contents/MacOS" cua-driver
ln -sf "cua-driver-${VERSION}-${OS_IDENTIFIER}-binary.tar.gz" "cua-driver-binary.tar.gz"
echo "Bare binary packaged."
tar -czf "cua-driver-${VERSION}-darwin-universal-binary.tar.gz" -C "CuaDriver.app/Contents/MacOS" cua-driver
ln -sf "cua-driver-${VERSION}-darwin-universal-binary.tar.gz" "cua-driver-binary.tar.gz"
echo "Universal bare binary packaged."
else
echo "Warning: binary not found at $BINARY"
fi
Expand All @@ -256,14 +289,14 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: cua-driver-notarized-tarball
path: ./libs/cua-driver/${{ steps.build_notarize.outputs.tarball_path }}
path: ./libs/cua-driver/.release/cua-driver-*-darwin-*.tar.gz
if-no-files-found: error

- name: Upload Notarized Package (Installer)
uses: actions/upload-artifact@v4
with:
name: cua-driver-notarized-installer
path: ./libs/cua-driver/${{ steps.build_notarize.outputs.pkg_path }}
path: ./libs/cua-driver/.release/cua-driver-*-darwin-*.pkg.tar.gz
if-no-files-found: error

- name: Upload Bare Binary
Expand Down Expand Up @@ -308,12 +341,10 @@ jobs:
uses: softprops/action-gh-release@v1
with:
files: |
./libs/cua-driver/${{ steps.build_notarize.outputs.tarball_path }}
./libs/cua-driver/${{ steps.build_notarize.outputs.pkg_path }}
./libs/cua-driver/.release/cua-driver-*-darwin-*.tar.gz
./libs/cua-driver/.release/cua-driver-*-darwin-*.pkg.tar.gz
./libs/cua-driver/.release/cua-driver-darwin.tar.gz
./libs/cua-driver/.release/cua-driver-darwin.pkg.tar.gz
./libs/cua-driver/.release/cua-driver.tar.gz
./libs/cua-driver/.release/cua-driver.pkg.tar.gz
./libs/cua-driver/.release/cua-driver-binary.tar.gz
body: |
${{ steps.release-notes.outputs.RELEASE_NOTES }}
Expand All @@ -325,5 +356,7 @@ jobs:
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh)"
```

> Supports Apple Silicon (arm64) and Intel (x86_64) — the release tarball contains a universal binary.
generate_release_notes: false
make_latest: true
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ If a grant still reads `NOT granted` after granting in the dialog, open **System
## Requirements

- macOS 14 (Sonoma) or later
- Apple Silicon (M1/M2/M3/M4) or Intel Mac
- Apple Silicon (M1/M2/M3/M4) or Intel Mac (x86_64)
- 50 MB free disk space for the app bundle

## Run the daemon
Expand Down
12 changes: 12 additions & 0 deletions docs/content/docs/cua-driver/reference/mcp-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ Requires Accessibility and Screen Recording permissions.

**Arguments:**

<Callout type="info">
**Sticky (pid, window_id) context.** Each `get_window_state(pid, window_id)` call refreshes the element-index cache for that specific window. All element-indexed actions (`click`, `type_text`, `scroll`, etc.) in the same turn resolve against the *most recent snapshot for that (pid, window_id) pair* — indices from a different window or an older snapshot are stale. Always re-call `get_window_state` after any action that might shift focus to a different window or app, and always pass the same `(pid, window_id)` you intend to act against.
</Callout>

- `javascript` (string, optional): Optional JavaScript to execute in the browser tab and return alongside the AX snapshot — one round-trip instead of two. Only works for Chromium-family browsers (Chrome, Brave, Edge) and Safari; requires 'Allow JavaScript from Apple Events' to be enabled first (see WEB_APPS.md). The result is appended to the response as a `## JavaScript result` section. Use for read-only queries (document.title, innerText, querySelectorAll, etc.). For mutations or side effects use the `page` tool instead.
- `pid` (integer, required): Process ID from `list_apps`.
- `query` (string, optional): Optional case-insensitive substring. When set, `tree_markdown` only contains lines that match plus their ancestor chain; element indices and `element_count` are unchanged.
Expand Down Expand Up @@ -433,6 +437,14 @@ later to resolve a target.

**Arguments:**

<Callout type="warn">
**Browsers require at least one URL.** Launching Safari, Chrome, Firefox, Arc, Brave, or Edge without a `urls` argument starts the process but no window is ever created — subsequent `get_window_state`, `click`, and `screenshot` calls will fail with "no window found." Always pass `urls=["about:blank"]` for a blank window, or a real URL to open.

```json
{"bundle_id": "com.apple.Safari", "urls": ["about:blank"]}
```
</Callout>

- `additional_arguments` (array of string, optional): Extra command-line arguments passed to the launched process. Passed directly as argv entries — no shell expansion. Example: ["--user-data-dir=/tmp/cua-session", "--no-first-run"] for an isolated Chrome session.
- `bundle_id` (string, optional): App bundle identifier, e.g. com.apple.calculator.
- `creates_new_application_instance` (boolean, optional): Force a brand-new process even if the app is already running. Useful for isolated browser sessions: pass creates_new_application_instance=true together with additional_arguments=["--user-data-dir=/tmp/session-a", "--no-first-run", "--no-default-browser-check"] to launch a sandboxed Chrome that cannot see the user's real profile, cookies, or extensions. Each session gets its own pid and window identity and can be controlled independently.
Expand Down
7 changes: 4 additions & 3 deletions libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct CuaDriverCommand: AsyncParsableCommand {
UpdateCommand.self,
DiagnoseCommand.self,
DoctorCommand.self,
CleanupCommand.self,
Comment thread
ddupont808 marked this conversation as resolved.
DumpDocsCommand.self,
]
)
Expand Down Expand Up @@ -509,7 +510,7 @@ struct UpdateCommand: AsyncParsableCommand {
}
}

/// `cua-driver doctor` — clean up stale install bits left from older versions.
/// `cua-driver cleanup` — clean up stale install bits left from older versions.
///
/// v0.0.5 and earlier installed a weekly LaunchAgent at
/// `~/Library/LaunchAgents/com.trycua.cua_driver_updater.plist` and a companion
Expand All @@ -521,9 +522,9 @@ struct UpdateCommand: AsyncParsableCommand {
/// update script. The plist lives under `$HOME` (no sudo). The companion
/// script under `/usr/local/bin` is root-owned, so we print the exact
/// `sudo rm` command for the user to run if it still exists.
struct DoctorCommand: ParsableCommand {
struct CleanupCommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "doctor",
commandName: "cleanup",
abstract: "Clean up stale install bits left from older cua-driver versions."
)

Expand Down
Loading
Loading