diff --git a/README.md b/README.md index d5a3d4c32b..f9b5813390 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ Drive any native macOS app **in the background** — agents click, type, and ver /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh)" ``` +> Want to try the cross-platform Rust port early? Add `-- --experimental-rust` to the line above — it delegates to the [`cua-driver-rs`](libs/cua-driver-rs/) installer (separate bundle, coexists with the Swift binary). + Full tool reference, architecture notes, and the Claude Code skill ship with the package: [`libs/cua-driver/README.md`](libs/cua-driver/README.md). --- diff --git a/docs/content/docs/cua-driver/guide/getting-started/installation.mdx b/docs/content/docs/cua-driver/guide/getting-started/installation.mdx index 23075dab9a..b02a13f5e4 100644 --- a/docs/content/docs/cua-driver/guide/getting-started/installation.mdx +++ b/docs/content/docs/cua-driver/guide/getting-started/installation.mdx @@ -28,6 +28,33 @@ The install runs **without sudo**: `/Applications` is user-writable on personal **Upgrading from v0.0.x?** The new installer puts the CLI at `~/.local/bin/cua-driver`. Any existing `/usr/local/bin/cua-driver` symlink is left in place so existing MCP client configs (Claude Code, Codex, etc.) keep working. Re-register them at your leisure to use the new path. +### Try the Rust backend (experimental) + + +**`cua-driver-rs` is a cross-platform Rust port of Cua Driver** — same MCP tool surface, with native Windows and Linux support and macOS parity in progress. Opt in with a single flag to help us soak-test it ahead of GA: + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh)" -- --experimental-rust +``` + +(Equivalent long form: `--backend=rust`.) + +The flag delegates to [`libs/cua-driver-rs/scripts/install.sh`](https://github.com/trycua/cua/blob/main/libs/cua-driver-rs/scripts/install.sh), which: + +- Installs `CuaDriverRs.app` to `/Applications/` — separate bundle id (`com.trycua.cuadriverrs`) from the Swift driver's `com.trycua.driver`. Both can coexist on the same machine with independent TCC grants and independent telemetry IDs. +- Points `~/.local/bin/cua-driver` at the Rust binary. The Swift `CuaDriver.app` (if previously installed) is left untouched and still launchable via its bundle path (`/Applications/CuaDriver.app/Contents/MacOS/cua-driver`). +- Same `--bin-dir` / `--no-modify-path` flags as the Swift installer — anything you put after `--experimental-rust` is forwarded to the Rust installer verbatim. + +**Switch back to Swift:** + +```bash +rm -rf /Applications/CuaDriverRs.app +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh)" +``` + +The second command rewires the `~/.local/bin/cua-driver` symlink back to the Swift bundle. + + ### Verify it worked ```bash diff --git a/libs/cua-driver/scripts/install.sh b/libs/cua-driver/scripts/install.sh index 10aa1df2d2..3411d0c5c5 100755 --- a/libs/cua-driver/scripts/install.sh +++ b/libs/cua-driver/scripts/install.sh @@ -12,6 +12,13 @@ # ~/.local/bin (e.g. /usr/local/bin — that target needs sudo) # --no-modify-path skip auto-appending an `export PATH=...` line to your # shell rc when ~/.local/bin is missing from PATH +# --experimental-rust opt into the experimental cua-driver-rs (Rust port) +# backend instead of the Swift binary. Delegates to +# libs/cua-driver-rs/scripts/install.sh — see that +# script for backend-specific env vars. Installs to a +# separate bundle (CuaDriverRs.app) so the Swift +# binary is left untouched. Also accepted as +# --backend=rust. # # Env overrides: # CUA_DRIVER_VERSION=0.1.0 pin a specific release tag @@ -30,16 +37,97 @@ APP_DEST="/Applications/$APP_NAME" BIN_DIR="${CUA_DRIVER_BIN_DIR:-$HOME/.local/bin}" NO_MODIFY_PATH="${CUA_DRIVER_NO_MODIFY_PATH:-0}" +# Rust-backend delegation target. Kept in sync with the canonical path on +# main; --experimental-rust below either execs the on-disk copy (when this +# script runs from a checked-out tree) or curls this URL and pipes it to +# bash (the `curl ... | bash` install path). +RUST_INSTALLER_URL="https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver-rs/scripts/install.sh" + # Lightweight flag parsing (avoid getopt; macOS getopt is GNU-incompatible). +# +# Two-pass shape: +# 1. Walk all argv and collect every unrecognised arg into FORWARDED_ARGS. +# That bucket is what we'd hand off to the Rust installer if the user +# opted in. Recognised Swift-only flags (--bin-dir, --no-modify-path) +# are consumed in this pass and applied to local state. +# 2. If --experimental-rust (or --backend=rust) was seen anywhere in argv, +# exec into the Rust installer with FORWARDED_ARGS and never reach the +# Swift install path below. +# +# This lets the experimental flag appear at any position, lets `--` end +# Swift-flag parsing without breaking forwarding, and keeps both installers' +# argv shapes (--bin-dir, --no-modify-path) bit-compatible so the same +# command works regardless of backend. +USE_RUST_BACKEND=0 +FORWARDED_ARGS=() +PASSTHROUGH=0 while [[ $# -gt 0 ]]; do + if [[ "$PASSTHROUGH" == "1" ]]; then + FORWARDED_ARGS+=("$1"); shift; continue + fi case "$1" in - --bin-dir) BIN_DIR="$2"; shift 2 ;; - --bin-dir=*) BIN_DIR="${1#*=}"; shift ;; - --no-modify-path) NO_MODIFY_PATH=1; shift ;; - *) shift ;; + --experimental-rust) USE_RUST_BACKEND=1; shift ;; + --backend=rust) USE_RUST_BACKEND=1; shift ;; + --backend=swift) shift ;; # explicit default — no-op + --backend=*) + printf 'error: unknown backend %q; supported: swift, rust\n' "${1#*=}" >&2 + exit 2 + ;; + --bin-dir) + if [[ -z "${2:-}" || "${2:0:1}" == "-" ]]; then + printf 'error: --bin-dir requires a value\n' >&2 + exit 2 + fi + BIN_DIR="$2"; FORWARDED_ARGS+=("$1" "$2"); shift 2 ;; + --bin-dir=*) BIN_DIR="${1#*=}"; FORWARDED_ARGS+=("$1"); shift ;; + --no-modify-path) NO_MODIFY_PATH=1; FORWARDED_ARGS+=("$1"); shift ;; + --) PASSTHROUGH=1; shift ;; # forward the rest verbatim + *) FORWARDED_ARGS+=("$1"); shift ;; esac done +# --- Optional delegation to the experimental Rust backend --------------- +# +# If the user opted in with --experimental-rust / --backend=rust, hand the +# rest of argv to cua-driver-rs/scripts/install.sh and exit. The Swift +# install path below is never touched in this case, so the Swift binary +# (if present) is left exactly as-is — users can roll back by deleting +# /Applications/CuaDriverRs.app and re-running this script without the flag. +if [[ "$USE_RUST_BACKEND" == "1" ]]; then + printf 'note: installing experimental Rust backend (cua-driver-rs). The Swift binary won'"'"'t be touched.\n' >&2 + + # Prefer the on-disk copy when this script is running from a checked-out + # tree (dev / CI). Falls back to curling the canonical URL for the + # `curl ... | bash` install path, where $BASH_SOURCE is unset / -. + LOCAL_RUST_INSTALLER="" + if [[ -n "${BASH_SOURCE[0]:-}" && "${BASH_SOURCE[0]}" != "-" && -f "${BASH_SOURCE[0]}" ]]; then + SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + CANDIDATE="$SCRIPT_DIR/../../cua-driver-rs/scripts/install.sh" + if [[ -f "$CANDIDATE" ]]; then + LOCAL_RUST_INSTALLER="$CANDIDATE" + fi + fi + + # macOS ships bash 3.2, which trips `set -u` when expanding an empty + # array via "${arr[@]}" — guard with the +alt-value pattern so the + # zero-arg case becomes a literal no-expansion. + if [[ -n "$LOCAL_RUST_INSTALLER" ]]; then + exec /bin/bash "$LOCAL_RUST_INSTALLER" ${FORWARDED_ARGS[@]+"${FORWARDED_ARGS[@]}"} + else + if ! command -v curl >/dev/null 2>&1; then + printf 'error: curl not found on PATH; cannot fetch %s\n' "$RUST_INSTALLER_URL" >&2 + exit 1 + fi + # `exec` so the Rust installer replaces this process — we don't want + # to fall through to the Swift install path on any error here. + RUST_INSTALLER_SCRIPT="$(curl -fsSL "$RUST_INSTALLER_URL")" || { + printf 'error: failed to download Rust installer from %s\n' "$RUST_INSTALLER_URL" >&2 + exit 1 + } + exec /bin/bash -c "$RUST_INSTALLER_SCRIPT" cua-driver-rs-install ${FORWARDED_ARGS[@]+"${FORWARDED_ARGS[@]}"} + fi +fi + BIN_LINK="$BIN_DIR/$BINARY_NAME" TMP_DIR=$(mktemp -d) trap 'rm -rf "$TMP_DIR"' EXIT