feat: settle screenshots, paste non-Latin text, and gate destructive … #598
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Installers | |
| on: | |
| push: | |
| # Every branch, not just main: this repo squash-merges locally and opens no | |
| # pull requests of its own, so a feature branch has no other way to reach | |
| # this lane before it lands. `branches-ignore` rather than deleting the key — | |
| # a push trigger that names `tags` and omits `branches` fires ONLY on tags, | |
| # which would disable branch CI outright. Dependabot is excluded because it | |
| # pushes its branch AND opens a PR, and the two land in different | |
| # concurrency groups, so the push run is a pure duplicate of the PR run. | |
| branches-ignore: ['dependabot/**'] | |
| # The old 'feature/**' glob could not match the branch names actually used | |
| # here (feat/..., fix/...), so non-main pushes were effectively unbuilt. | |
| tags: ['v*'] | |
| paths: | |
| - 'Package.swift' | |
| - 'Sources/**' | |
| - 'Tests/**' | |
| - 'Resources/**' | |
| - 'mirroir.sh' | |
| - 'npm/**' | |
| - 'scripts/package-fake-app.sh' | |
| - '.github/actions/setup-embacle/**' | |
| - '.github/workflows/installers.yml' | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - 'Package.swift' | |
| - 'Sources/**' | |
| - 'Tests/**' | |
| - 'Resources/**' | |
| - 'mirroir.sh' | |
| - 'npm/**' | |
| - 'scripts/package-fake-app.sh' | |
| - '.github/actions/setup-embacle/**' | |
| - '.github/workflows/installers.yml' | |
| workflow_dispatch: | |
| # Cancel superseded runs on the same branch / PR — three macos-15 jobs run here, | |
| # so a superseded run is expensive to leave in flight. | |
| concurrency: | |
| group: installers-${{ github.head_ref || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| source-install: | |
| name: Source install (mirroir.sh) | |
| runs-on: macos-15 | |
| permissions: | |
| contents: read | |
| env: | |
| MIRROIR_BUNDLE_ID: com.jfarcand.FakeMirroring | |
| MIRROIR_PROCESS_NAME: FakeMirroring | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-embacle | |
| - name: Clone skills repo | |
| run: | | |
| git clone https://github.com/jfarcand/mirroir-skills.git ../mirroir-skills | |
| mkdir -p .mirroir-mcp | |
| ln -s "$(cd ../mirroir-skills && pwd)" .mirroir-mcp/skills | |
| - name: Run mirroir.sh | |
| run: ./mirroir.sh | |
| - name: Verify binaries built | |
| run: | | |
| test -x .build/release/mirroir-mcp | |
| # Verify mirroir CLI symlink was created (Homebrew bin dir) | |
| test -L /opt/homebrew/bin/mirroir || test -L /usr/local/bin/mirroir | |
| mirroir record --help 2>&1 | grep -q "Record user interactions" | |
| - name: Verify embacle FFI linked | |
| run: nm .build/release/mirroir-mcp | grep -q embacle_init | |
| - name: Build and package FakeMirroring | |
| run: | | |
| swift build -c release --product FakeMirroring | |
| ./scripts/package-fake-app.sh | |
| - name: Quit Setup Assistant (macos-15 runner intercepts taps) | |
| run: | | |
| # Some macos-15 runners boot with Setup Assistant in the foreground | |
| # — it intercepts cghidEventTap mouse events and the target app | |
| # receives no mouseUp. Quit it before launching FakeMirroring. | |
| osascript -e 'tell application "Setup Assistant" to quit' 2>/dev/null || true | |
| pkill -f "Setup Assistant" 2>/dev/null || true | |
| sleep 1 | |
| - name: Ensure display fits the FakeMirroring window | |
| # GH macOS runners boot with a display too short for the 410x930 | |
| # window: the bottom ~60pt (the simulated tab bar) is clipped | |
| # off-screen and CGEvent taps there never land. Grow the virtual | |
| # display before launching FakeMirroring. | |
| run: | | |
| brew install --quiet jakehilborn/jakehilborn/displayplacer || brew install --quiet displayplacer || true | |
| echo "--- displays before ---" | |
| displayplacer list 2>/dev/null | grep -E "Persistent|Resolution" || true | |
| SCREEN_ID=$(displayplacer list 2>/dev/null | awk -F': ' '/Persistent screen id/{print $2; exit}') | |
| if [ -n "$SCREEN_ID" ]; then | |
| for RES in 1920x1080 1680x1050 1600x1200; do | |
| if displayplacer "id:$SCREEN_ID res:$RES" 2>/dev/null; then | |
| echo "display set to $RES" | |
| break | |
| fi | |
| done | |
| fi | |
| echo "--- displays after ---" | |
| displayplacer list 2>/dev/null | grep -E "Resolution" || true | |
| - name: Launch FakeMirroring | |
| run: | | |
| open .build/release/FakeMirroring.app | |
| sleep 3 | |
| - name: Integration tests (FakeMirroring) | |
| run: swift test --filter IntegrationTests | |
| - name: MCP initialize (installed binary) | |
| run: | | |
| echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \ | |
| | .build/release/mirroir-mcp 2>/dev/null \ | |
| | python3 -c " | |
| import sys, json | |
| obj = json.loads(sys.stdin.readline()) | |
| assert obj['result']['serverInfo']['name'] == 'mirroir-mcp' | |
| print('MCP initialize: OK') | |
| " | |
| - name: MCP tools/call screenshot (installed binary + FakeMirroring) | |
| run: | | |
| printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"screenshot","arguments":{}}}\n' \ | |
| | .build/release/mirroir-mcp --dangerously-skip-permissions 2>/dev/null \ | |
| | python3 -c " | |
| import sys, json | |
| lines = sys.stdin.readlines() | |
| resp = json.loads(lines[1]) | |
| content = resp['result']['content'] | |
| images = [c for c in content if c.get('type') == 'image'] | |
| assert len(images) > 0, f'Expected image in screenshot response, got: {content}' | |
| print(f'screenshot tool: OK ({len(images)} image block(s))') | |
| " | |
| homebrew-install: | |
| name: Homebrew install (local formula) | |
| runs-on: macos-15 | |
| permissions: | |
| contents: read | |
| env: | |
| MIRROIR_BUNDLE_ID: com.jfarcand.FakeMirroring | |
| MIRROIR_PROCESS_NAME: FakeMirroring | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-embacle | |
| - name: Clone skills repo | |
| run: | | |
| git clone https://github.com/jfarcand/mirroir-skills.git ../mirroir-skills | |
| mkdir -p .mirroir-mcp | |
| ln -s "$(cd ../mirroir-skills && pwd)" .mirroir-mcp/skills | |
| - name: Create source tarball | |
| run: | | |
| TAR_PATH="/tmp/mirroir-mcp-local.tar.gz" | |
| git archive --format=tar.gz --prefix=mirroir-mcp-local/ HEAD > "$TAR_PATH" | |
| SHA=$(shasum -a 256 "$TAR_PATH" | cut -d' ' -f1) | |
| echo "TARBALL_PATH=$TAR_PATH" >> $GITHUB_ENV | |
| echo "TARBALL_SHA=$SHA" >> $GITHUB_ENV | |
| - name: Create local tap with formula | |
| run: | | |
| brew tap-new local/test | |
| TAP_DIR="$(brew --repository)/Library/Taps/local/homebrew-test/Formula" | |
| mkdir -p "$TAP_DIR" | |
| cat > "$TAP_DIR/mirroir-mcp.rb" << RUBY | |
| class MirroirMcp < Formula | |
| desc "MCP server for controlling iPhone through macOS iPhone Mirroring" | |
| homepage "https://github.com/jfarcand/mirroir-mcp" | |
| url "file://${TARBALL_PATH}" | |
| version "0.0.0-ci" | |
| sha256 "${TARBALL_SHA}" | |
| license "Apache-2.0" | |
| depends_on :macos | |
| depends_on xcode: ["15.0", :build] | |
| depends_on "dravr-ai/tap/embacle-ffi" | |
| def install | |
| embacle_lib = Formula["embacle-ffi"].opt_lib | |
| system "swift", "build", "-c", "release", "--disable-sandbox", | |
| "-Xlinker", "-L#{embacle_lib}" | |
| bin.install ".build/release/mirroir-mcp" | |
| bin.install_symlink "mirroir-mcp" => "mirroir" | |
| end | |
| test do | |
| assert_match "mirroir-mcp", shell_output("#{bin}/mirroir-mcp --help 2>&1", 1) | |
| end | |
| end | |
| RUBY | |
| - name: Install from local tap | |
| run: brew install local/test/mirroir-mcp | |
| - name: Verify Homebrew-installed binaries | |
| run: | | |
| which mirroir-mcp | |
| which mirroir | |
| # Verify mirroir CLI works (symlink to mirroir-mcp) | |
| mirroir test --help 2>&1 | grep -q "mirroir-mcp test" | |
| - name: Verify embacle FFI linked | |
| run: nm "$(brew --prefix)/bin/mirroir-mcp" | grep -q embacle_init | |
| - name: Build and package FakeMirroring | |
| run: | | |
| swift build -c release --product FakeMirroring | |
| ./scripts/package-fake-app.sh | |
| - name: Quit Setup Assistant (macos-15 runner intercepts taps) | |
| run: | | |
| # Some macos-15 runners boot with Setup Assistant in the foreground | |
| # — it intercepts cghidEventTap mouse events and the target app | |
| # receives no mouseUp. Quit it before launching FakeMirroring. | |
| osascript -e 'tell application "Setup Assistant" to quit' 2>/dev/null || true | |
| pkill -f "Setup Assistant" 2>/dev/null || true | |
| sleep 1 | |
| - name: Ensure display fits the FakeMirroring window | |
| # GH macOS runners boot with a display too short for the 410x930 | |
| # window: the bottom ~60pt (the simulated tab bar) is clipped | |
| # off-screen and CGEvent taps there never land. Grow the virtual | |
| # display before launching FakeMirroring. | |
| run: | | |
| brew install --quiet jakehilborn/jakehilborn/displayplacer || brew install --quiet displayplacer || true | |
| echo "--- displays before ---" | |
| displayplacer list 2>/dev/null | grep -E "Persistent|Resolution" || true | |
| SCREEN_ID=$(displayplacer list 2>/dev/null | awk -F': ' '/Persistent screen id/{print $2; exit}') | |
| if [ -n "$SCREEN_ID" ]; then | |
| for RES in 1920x1080 1680x1050 1600x1200; do | |
| if displayplacer "id:$SCREEN_ID res:$RES" 2>/dev/null; then | |
| echo "display set to $RES" | |
| break | |
| fi | |
| done | |
| fi | |
| echo "--- displays after ---" | |
| displayplacer list 2>/dev/null | grep -E "Resolution" || true | |
| - name: Launch FakeMirroring | |
| run: | | |
| open .build/release/FakeMirroring.app | |
| sleep 3 | |
| - name: Integration tests (FakeMirroring) | |
| run: swift test --filter IntegrationTests | |
| - name: MCP initialize (brew-installed binary) | |
| run: | | |
| echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \ | |
| | mirroir-mcp 2>/dev/null \ | |
| | python3 -c " | |
| import sys, json | |
| obj = json.loads(sys.stdin.readline()) | |
| assert obj['result']['serverInfo']['name'] == 'mirroir-mcp' | |
| print('MCP initialize (brew path): OK') | |
| " | |
| - name: MCP tools/call screenshot (brew-installed binary + FakeMirroring) | |
| run: | | |
| printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"screenshot","arguments":{}}}\n' \ | |
| | mirroir-mcp --dangerously-skip-permissions 2>/dev/null \ | |
| | python3 -c " | |
| import sys, json | |
| lines = sys.stdin.readlines() | |
| resp = json.loads(lines[1]) | |
| content = resp['result']['content'] | |
| images = [c for c in content if c.get('type') == 'image'] | |
| assert len(images) > 0, f'Expected image in screenshot response, got: {content}' | |
| print(f'screenshot tool: OK ({len(images)} image block(s))') | |
| " | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| sudo brew uninstall mirroir-mcp || true | |
| npx-install: | |
| name: NPX install (npm package) | |
| runs-on: macos-15 | |
| permissions: | |
| contents: read | |
| env: | |
| MIRROIR_BUNDLE_ID: com.jfarcand.FakeMirroring | |
| MIRROIR_PROCESS_NAME: FakeMirroring | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-embacle | |
| - name: Clone skills repo | |
| run: | | |
| git clone https://github.com/jfarcand/mirroir-skills.git ../mirroir-skills | |
| mkdir -p .mirroir-mcp | |
| ln -s "$(cd ../mirroir-skills && pwd)" .mirroir-mcp/skills | |
| - name: Build release binaries | |
| run: swift build -c release | |
| - name: Verify embacle FFI linked | |
| run: nm .build/release/mirroir-mcp | grep -q embacle_init | |
| - name: Stage binaries into npm/bin/ (simulates install.js postinstall) | |
| run: | | |
| mkdir -p npm/bin | |
| cp .build/release/mirroir-mcp npm/bin/mirroir-mcp-native | |
| chmod +x npm/bin/mirroir-mcp-native | |
| - name: Build and package FakeMirroring | |
| run: | | |
| swift build -c release --product FakeMirroring | |
| ./scripts/package-fake-app.sh | |
| - name: Quit Setup Assistant (macos-15 runner intercepts taps) | |
| run: | | |
| # Some macos-15 runners boot with Setup Assistant in the foreground | |
| # — it intercepts cghidEventTap mouse events and the target app | |
| # receives no mouseUp. Quit it before launching FakeMirroring. | |
| osascript -e 'tell application "Setup Assistant" to quit' 2>/dev/null || true | |
| pkill -f "Setup Assistant" 2>/dev/null || true | |
| sleep 1 | |
| - name: Ensure display fits the FakeMirroring window | |
| # GH macOS runners boot with a display too short for the 410x930 | |
| # window: the bottom ~60pt (the simulated tab bar) is clipped | |
| # off-screen and CGEvent taps there never land. Grow the virtual | |
| # display before launching FakeMirroring. | |
| run: | | |
| brew install --quiet jakehilborn/jakehilborn/displayplacer || brew install --quiet displayplacer || true | |
| echo "--- displays before ---" | |
| displayplacer list 2>/dev/null | grep -E "Persistent|Resolution" || true | |
| SCREEN_ID=$(displayplacer list 2>/dev/null | awk -F': ' '/Persistent screen id/{print $2; exit}') | |
| if [ -n "$SCREEN_ID" ]; then | |
| for RES in 1920x1080 1680x1050 1600x1200; do | |
| if displayplacer "id:$SCREEN_ID res:$RES" 2>/dev/null; then | |
| echo "display set to $RES" | |
| break | |
| fi | |
| done | |
| fi | |
| echo "--- displays after ---" | |
| displayplacer list 2>/dev/null | grep -E "Resolution" || true | |
| - name: Launch FakeMirroring | |
| run: | | |
| open .build/release/FakeMirroring.app | |
| sleep 3 | |
| - name: Integration tests (FakeMirroring) | |
| run: swift test --filter IntegrationTests | |
| - name: MCP initialize (npx wrapper) | |
| run: | | |
| echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \ | |
| | node npm/bin/mirroir-mcp 2>/dev/null \ | |
| | python3 -c " | |
| import sys, json | |
| obj = json.loads(sys.stdin.readline()) | |
| assert obj['result']['serverInfo']['name'] == 'mirroir-mcp' | |
| print('MCP initialize (npx wrapper): OK') | |
| " | |
| - name: MCP tools/call screenshot (npx wrapper + FakeMirroring) | |
| run: | | |
| printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"screenshot","arguments":{}}}\n' \ | |
| | node npm/bin/mirroir-mcp --dangerously-skip-permissions 2>/dev/null \ | |
| | python3 -c " | |
| import sys, json | |
| lines = sys.stdin.readlines() | |
| resp = json.loads(lines[1]) | |
| content = resp['result']['content'] | |
| images = [c for c in content if c.get('type') == 'image'] | |
| assert len(images) > 0, f'Expected image in screenshot response, got: {content}' | |
| print(f'screenshot tool: OK ({len(images)} image block(s))') | |
| " |