From 0db567eeb412d64f12457b533bb03068f3f93c4e Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Mon, 1 Jun 2026 10:10:57 -0700 Subject: [PATCH] fix(cua-driver-rs): release installer unifies home on ~/.cua-driver + cleans up prior local install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release installer (install.sh → _install-rust.sh) defaulted its package home to the legacy ~/.cua-driver-rs, but the local installer (_install-local-rust.sh) and the runtime already use ~/.cua-driver (renamed in v0.2.16 / PR #1644). That mismatch is the root cause of a two-install collision: a user who ran install-local and then the release install.sh ended up with two homes and two conflicting installs, with the local build's artifacts left dangling. Fixes in _install-rust.sh: - Default HOME_DIR to ~/.cua-driver (still honoring CUA_DRIVER_RS_HOME for back-compat), matching install-local + runtime. - Before staging: cleanup_prior_local_install() stops the daemon and removes the prior install-local artifacts under the shared home — the `*-local-*` release dirs and the ~/.cua-driver/.tcc-signing-identity marker. Marker-gated and conservative: never touches a real release dir, the `current` symlink, or unrelated user state; best-effort + idempotent (no-op on a clean machine). - After staging: sweep a stale ~/.cua-driver-rs left by an older release, mirroring the belt-and-braces legacy-home sweep install-local already does. - TCC grants preserved: /Applications/CuaDriver.app is replaced in place via the existing release ditto (grants key on the shared com.trycua.driver bundle id); no tccutil reset, so cert-pinned grants are not churned. install.ps1 (Windows) already defaults to ~/.cua-driver and migrates the legacy home, so it is unchanged. Docs: reconcile the ~/.cua-driver-rs → ~/.cua-driver home references across the installation + linux guides, document the local/legacy cleanup behavior, and add an Unreleased changelog entry. Co-Authored-By: Claude Opus 4.8 --- .../guide/getting-started/installation.mdx | 34 +++-- .../guide/getting-started/linux.mdx | 2 +- .../docs/cua-driver/reference/changelog.mdx | 18 +++ libs/cua-driver/scripts/_install-rust.sh | 121 +++++++++++++++++- 4 files changed, 159 insertions(+), 16 deletions(-) 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 acc716cf25..28fd7203c7 100644 --- a/docs/content/docs/cua-driver/guide/getting-started/installation.mdx +++ b/docs/content/docs/cua-driver/guide/getting-started/installation.mdx @@ -27,7 +27,17 @@ irm https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/in On macOS, the installer drops `CuaDriver.app` into `/Applications` and symlinks the binary at `~/.local/bin/cua-driver`. The bundle is signed under `com.trycua.driver`, so TCC grants survive every release. -On Linux, the pre-release backend installer downloads the release into `~/.cua-driver-rs/packages/releases/`, retargets `~/.cua-driver-rs/packages/current`, and symlinks `~/.local/bin/cua-driver`. +On Linux, the pre-release backend installer downloads the release into `~/.cua-driver/packages/releases/`, retargets `~/.cua-driver/packages/current`, and symlinks `~/.local/bin/cua-driver`. + + + **Install home is `~/.cua-driver` on every platform.** Earlier `cua-driver-rs` releases (before + v0.2.16) put the Linux/macOS package home at `~/.cua-driver-rs`; the release installer now writes + to `~/.cua-driver` like the local installer and the runtime already do. Re-running the installer + over an older release **auto-sweeps** a stale `~/.cua-driver-rs`, and also cleans up any prior + `install-local` dev build (`*-local-*` release dirs + the local signing-identity marker) under the + shared home so the downloaded release is the single authoritative install. The `CUA_DRIVER_RS_HOME` + override is still honored for back-compat. + **Linux is pre-release.** Linux artifacts and install paths are published for early testing, but @@ -79,7 +89,7 @@ Linux and Windows installs land in a three-tier layout that makes upgrades and r **Linux** ``` -$CUA_DRIVER_RS_HOME/ (default: ~/.cua-driver-rs) +$CUA_DRIVER_RS_HOME/ (default: ~/.cua-driver) packages/ releases/ 0.2.0-x86_64-unknown-linux-gnu/cua-driver (real binary, immutable) @@ -114,10 +124,10 @@ Both platforms ship every release into its own per-version directory under `pack **Linux — roll back to a specific version** ```bash -# The symlink target is RELATIVE to ~/.cua-driver-rs/packages/, so just +# The symlink target is RELATIVE to ~/.cua-driver/packages/, so just # "releases/-" (no leading "../") — matches what install.sh writes. -ln -sfn releases/0.2.0-x86_64-unknown-linux-gnu ~/.cua-driver-rs/packages/.current.tmp -mv -Tf ~/.cua-driver-rs/packages/.current.tmp ~/.cua-driver-rs/packages/current +ln -sfn releases/0.2.0-x86_64-unknown-linux-gnu ~/.cua-driver/packages/.current.tmp +mv -Tf ~/.cua-driver/packages/.current.tmp ~/.cua-driver/packages/current cua-driver --version # → 0.2.0 ``` @@ -141,7 +151,7 @@ Clear `$env:CUA_DRIVER_RS_VERSION` and re-run to roll forward to the newest rele | ---------------------------------------------------- | ------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CUA_DRIVER_RS_VERSION` | unset → use baked version | unset → use baked version | Pin a specific release (e.g. `0.2.0`). | | `CUA_DRIVER_RS_INSTALL_DIR` | `~/.local/bin` | `%LOCALAPPDATA%\Programs\Cua\cua-driver\bin` | The visible PATH-entry directory. On Windows this is itself a junction. | -| `CUA_DRIVER_RS_HOME` | `~/.cua-driver-rs` | `%USERPROFILE%\.cua-driver` | Package home — holds `packages/releases//` and `packages/current`. | +| `CUA_DRIVER_RS_HOME` | `~/.cua-driver` | `%USERPROFILE%\.cua-driver` | Package home — holds `packages/releases//` and `packages/current`. (Renamed from `~/.cua-driver-rs` in v0.2.16; a stale legacy dir is auto-swept on the next install.) | | `CUA_DRIVER_RS_NO_MODIFY_PATH` _(Linux/macOS only)_ | `0` | use `-NoPathUpdate` switch on `install.ps1` | Skip the PATH append. On Windows the installer appends `%LOCALAPPDATA%\Programs\Cua\cua-driver\bin` to the User-scope `Path` by default; use the fetch+invoke form with `-NoPathUpdate` to opt out (see the Windows install section above for the exact one-liner). | | `CUA_DRIVER_RS_KEEP_VERSIONS` _(Linux/Windows only)_ | `5` | `5` | Keep the N most recent per-version release dirs after install (`0` disables GC). See the **Old-version cleanup** callout below for per-target and active-install invariants. | @@ -183,13 +193,13 @@ The macOS install path is unaffected — `/Applications/CuaDriver.app` is an in- ``` ==> another cua-driver-rs install is already in progress (lock at - ~/.cua-driver-rs/packages/.install.lock.d); waiting... + ~/.cua-driver/packages/.install.lock.d); waiting... ``` If you see this, the safe action is to let it block — it polls every 1 second and proceeds the moment the holding install finishes. The lock entry stamps the holder's pid, start time, and invocation args so you can confirm what's running: ```bash -cat ~/.cua-driver-rs/packages/.install.lock.d/info +cat ~/.cua-driver/packages/.install.lock.d/info # pid=43210 # started=2026-05-17T09:14:22Z # argv=install.sh @@ -231,7 +241,7 @@ For a structured environment + install report on any platform, run: cua-driver doctor # [ok ] binary: cua-driver 0.2.1 (aarch64-macos) # [ok ] install dir: /Users/you/.local/bin/cua-driver -# [ok ] home dir: /Users/you/.cua-driver-rs (3 release dirs cached) +# [ok ] home dir: /Users/you/.cua-driver (3 release dirs cached) # [ok ] telemetry: enabled (install-id present) # ... ``` @@ -415,7 +425,7 @@ other value (including unset) leaves the gate active. Release notes: https://github.com/trycua/cua/releases/tag/cua-driver-rs-v0.1.4 ``` -The answer is cached at `~/.cua-driver-rs/version_check.json` for ~20 +The answer is cached at `~/.cua-driver/version_check.json` for ~20 hours, so subsequent launches reuse the cached result without a network call. Network failures are silent — the next launch retries. @@ -624,8 +634,8 @@ One canonical uninstall URL per platform mirrors the install side: | Platform | Symlink / bin | App bundle / package home | Autostart entry | Skill links | Claude MCP registrations | | -------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -| macOS | `~/.local/bin/cua-driver` _(when it resolves into `CuaDriver.app` and `~/.cua-driver-rs/` exists)_ | `/Applications/CuaDriver.app` (current), `/Applications/CuaDriverRs.app` (legacy), `~/.cua-driver-rs/` | `~/Library/LaunchAgents/com.trycua.cua-driver-rs.plist` | `cua-driver` and legacy `cua-driver-rs` links under agent skill dirs | Scrubbed from `~/.claude.json` | -| Linux | `~/.local/bin/cua-driver` _(when it resolves into `~/.cua-driver-rs/`)_ | `~/.cua-driver-rs/` | `~/.config/systemd/user/cua-driver-rs.service` (stop + disable + remove) | Same as macOS | Scrubbed from `~/.claude.json` | +| macOS | `~/.local/bin/cua-driver` _(when it resolves into `CuaDriver.app` and a Rust marker exists)_ | `/Applications/CuaDriver.app` (current), `/Applications/CuaDriverRs.app` (legacy), `~/.cua-driver/` (legacy `~/.cua-driver-rs/` also swept) | `~/Library/LaunchAgents/com.trycua.cua-driver-rs.plist` | `cua-driver` and legacy `cua-driver-rs` links under agent skill dirs | Scrubbed from `~/.claude.json` | +| Linux | `~/.local/bin/cua-driver` _(when it resolves into `~/.cua-driver/`)_ | `~/.cua-driver/` (legacy `~/.cua-driver-rs/` also swept) | `~/.config/systemd/user/cua-driver-rs.service` (stop + disable + remove) | Same as macOS | Scrubbed from `~/.claude.json` | | Windows | `%LOCALAPPDATA%\Programs\Cua\cua-driver\bin` _(directory junction; legacy `Programs\trycua\cua-driver-rs\bin` also swept)_ | `%USERPROFILE%\.cua-driver\` (entire tree, including `packages\current` junction; legacy `~\.cua-driver-rs\` also swept) | Scheduled Task `cua-driver-serve` (`schtasks /Delete`; script self-elevates when needed) | Junctions under `%USERPROFILE%\.claude\skills`, `.agents\skills`, `.openclaw\skills`, `%APPDATA%\opencode\skills`, `.gemini\skills` | Not auto-edited — closing message prints the manual `claude mcp remove` command | diff --git a/docs/content/docs/cua-driver/guide/getting-started/linux.mdx b/docs/content/docs/cua-driver/guide/getting-started/linux.mdx index 47ffbf8ae6..668e3e0beb 100644 --- a/docs/content/docs/cua-driver/guide/getting-started/linux.mdx +++ b/docs/content/docs/cua-driver/guide/getting-started/linux.mdx @@ -148,7 +148,7 @@ This is a useful recipe for local CI experiments with the Linux pre-release back ## Distro-specific notes -The canonical install script (`/bin/bash -c "$(curl -fsSL …/install.sh)"`) downloads a Linux x86_64 binary tarball from GitHub Releases, drops it into `~/.cua-driver-rs/packages/releases/-x86_64-unknown-linux-gnu/`, and symlinks `~/.local/bin/cua-driver`. Because Linux support is pre-release, expect distro-specific gaps and prerequisites. +The canonical install script (`/bin/bash -c "$(curl -fsSL …/install.sh)"`) downloads a Linux x86_64 binary tarball from GitHub Releases, drops it into `~/.cua-driver/packages/releases/-x86_64-unknown-linux-gnu/`, and symlinks `~/.local/bin/cua-driver`. Because Linux support is pre-release, expect distro-specific gaps and prerequisites. The bit that varies is **what accessibility / display tooling is pre-installed**: diff --git a/docs/content/docs/cua-driver/reference/changelog.mdx b/docs/content/docs/cua-driver/reference/changelog.mdx index d0e9c38395..2bdbfba3cf 100644 --- a/docs/content/docs/cua-driver/reference/changelog.mdx +++ b/docs/content/docs/cua-driver/reference/changelog.mdx @@ -18,6 +18,24 @@ The canonical, always-current source is the release body is auto-generated per release from the commits touching `libs/cua-driver/rust`, including SHA256 checksums and install instructions. +## Unreleased + +- **Install home unified on `~/.cua-driver` + collision fix.** The release + installer (`install.sh` → `_install-rust.sh`) was still defaulting its package + home to the legacy `~/.cua-driver-rs`, while the local installer + (`install-local.sh`) and the runtime already used `~/.cua-driver` (renamed in + v0.2.16). That mismatch meant a machine could end up with two homes and two + conflicting installs. The release installer now defaults to `~/.cua-driver` + (still honoring the `CUA_DRIVER_RS_HOME` override), and on every install it (a) + cleans up a prior `install-local` dev build under the shared home — stops the + daemon, removes the `*-local-*` release dirs and the local signing-identity + marker — and (b) sweeps a stale `~/.cua-driver-rs` left by an older release. + Both steps are best-effort, idempotent, and conservative (marker-gated; they + never touch a real release dir, the `current` symlink, or unrelated user + state). TCC grants are preserved — the `/Applications/CuaDriver.app` bundle is + replaced in place, not `tccutil reset`. The Windows installer (`install.ps1`) + already used `~/.cua-driver` and migrated the legacy home, so it is unchanged. + ## 0.5.0 (2026-06-01) - **Windows: per-session agent cursors** — the per-session cursor model (macOS diff --git a/libs/cua-driver/scripts/_install-rust.sh b/libs/cua-driver/scripts/_install-rust.sh index 3d58edfc39..350c4c0952 100644 --- a/libs/cua-driver/scripts/_install-rust.sh +++ b/libs/cua-driver/scripts/_install-rust.sh @@ -28,9 +28,14 @@ # binary location # CUA_DRIVER_RS_BIN_DIR=PATH legacy alias for INSTALL_DIR # CUA_DRIVER_RS_HOME=PATH package home for versioned installs -# (default ~/.cua-driver-rs). Holds +# (default ~/.cua-driver). Holds # packages/releases/-/ and # packages/current/ on Linux/Windows. +# Renamed from ~/.cua-driver-rs in +# v0.2.16 / PR #1644 — this release +# installer was missed in that rename +# and is reconciled here; a stale +# ~/.cua-driver-rs is swept post-install. # CUA_DRIVER_RS_NO_MODIFY_PATH=1 same as --no-modify-path # CUA_DRIVER_RS_KEEP_VERSIONS=N keep the N most recent per-version # release dirs after install; older @@ -110,7 +115,17 @@ TAG_PREFIX="cua-driver-rs-v" # CUA_DRIVER_RS_INSTALL_DIR is the documented name; CUA_DRIVER_RS_BIN_DIR is # the legacy alias kept for users with the old env in their shell rc. BIN_DIR="${CUA_DRIVER_RS_INSTALL_DIR:-${CUA_DRIVER_RS_BIN_DIR:-$HOME/.local/bin}}" -HOME_DIR="${CUA_DRIVER_RS_HOME:-$HOME/.cua-driver-rs}" +# Canonical home is ~/.cua-driver (renamed from ~/.cua-driver-rs in v0.2.16 / +# PR #1644). The local installer (_install-local-rust.sh) and the runtime +# already default here; this release installer was missed in that rename and +# kept writing to the legacy ~/.cua-driver-rs, which is the root cause of the +# install collision (release wrote one home, install-local + runtime used the +# other). Reconcile the default here, keep accepting the CUA_DRIVER_RS_HOME +# override for back-compat, and sweep the stale legacy dir post-install below. +HOME_DIR="${CUA_DRIVER_RS_HOME:-$HOME/.cua-driver}" +# Pre-v0.2.16 home this installer used to write to. Swept after the new +# install is staged so a single rooted home (~/.cua-driver) is left behind. +LEGACY_HOME_DIR="$HOME/.cua-driver-rs" NO_MODIFY_PATH="${CUA_DRIVER_RS_NO_MODIFY_PATH:-0}" # Post-install GC: how many per-version release dirs to retain. Validated # below as a non-negative integer; 0 means "never GC". The dir that @@ -352,6 +367,84 @@ prune_old_releases() { printf '%s\0' "${to_prune[@]}" | xargs -0 rm -rf } +# --- Clean up a pre-existing LOCAL (install-local) install -------------- +# +# `install-local.sh` (`_install-local-rust.sh`) installs a dev build into the +# SAME canonical home this release installer now writes to (~/.cua-driver, see +# the HOME_DIR reconciliation above), under a `*-local-*` versioned release dir +# (VERSION_TAG="0.0.0-local-"). On macOS it also cert-signs the shared +# /Applications/CuaDriver.app with a self-signed identity recorded at +# `~/.cua-driver/.tcc-signing-identity`. +# +# A user who ran install-local and then runs this release installer would +# otherwise end up with the local artifacts lingering alongside the fresh +# release: the `*-local-*` release dir(s) sit in `packages/releases/` (the +# release `current` swap re-points away from them, but they're never removed +# explicitly here), and the stale `.tcc-signing-identity` marker survives even +# though the release bundle is CI-signed, not locally cert-signed. Follow the +# same logic install-local / uninstall.sh use: stop the daemon, then remove +# ONLY the unambiguously-local artifacts so the release install is the single +# authoritative one. +# +# Conservative by construction: we only ever remove `*-local-*` release dirs +# and the local signing-identity marker — never a real release dir, never the +# `current` symlink (the release branch owns that), never unrelated user state +# under the home. Every step is best-effort + idempotent; a machine with no +# prior local install is a clean no-op. +# +# TCC is preserved deliberately: we do NOT `tccutil reset` here. The bundle at +# /Applications/CuaDriver.app is shared (bundle id com.trycua.driver) and the +# subsequent release `ditto` re-points the binary in place; grants keyed on the +# bundle id survive (macOS may re-prompt once on the cdhash change, same as any +# upgrade). Churning the signing identity would gratuitously invalidate +# cert-pinned grants, so we leave it alone. +cleanup_prior_local_install() { + local releases_dir="$HOME_DIR/packages/releases" + local tcc_marker="$HOME_DIR/.tcc-signing-identity" + + # Collect the local-build release dirs (the unambiguous install-local + # signature — a release install never creates a `*-local-*` dir). + local local_dirs=() + local d + if [[ -d "$releases_dir" ]]; then + for d in "$releases_dir"/*-local-*/; do + [[ -d "$d" ]] && local_dirs+=("${d%/}") + done + fi + + # Nothing local on disk → clean no-op (no marker, no local dirs). + if [[ ${#local_dirs[@]} -eq 0 && ! -f "$tcc_marker" ]]; then + return 0 + fi + + log "detected a prior install-local build under $HOME_DIR — cleaning it up so this release install is authoritative" + + # Stop the local daemon BEFORE we yank its binary out from under it, + # mirroring the post-swap stop both installers already do. Best-effort. + stop_cua_driver_daemons + + # Remove the `*-local-*` release dirs. The release install stages into its + # own `-` dir and swaps `current` to it, so deleting the + # local dirs can't strand the active install. If `current` somehow still + # points into a local dir (e.g. a partial prior run), the release branch + # below re-creates `current` immediately after, so a transient dangling + # link is harmless. + if [[ ${#local_dirs[@]} -gt 0 ]]; then + for d in "${local_dirs[@]}"; do + rm -rf "$d" 2>/dev/null || true + log " removed local build dir ${d##*/}" + done + fi + + # Remove the local signing-identity marker — it describes the locally + # cert-signed bundle, which the release `ditto` is about to replace with + # the CI-signed one. Leaving it would misreport the bundle's identity. + if [[ -f "$tcc_marker" ]]; then + rm -f "$tcc_marker" 2>/dev/null || true + log " removed local signing-identity marker $tcc_marker" + fi +} + # --- Resolve OS/arch ---------------------------------------------------- OS=$(uname -s) @@ -506,6 +599,11 @@ fi # --- Install ------------------------------------------------------------ +# Before staging the new release, sweep any prior install-local build that +# shares this home so the release install ends up authoritative (see the +# function definition above for the conservative marker-gated logic). +cleanup_prior_local_install + mkdir -p "$BIN_DIR" # macOS: install the .app to /Applications first, then symlink the @@ -641,6 +739,23 @@ else prune_old_releases "$RELEASES_DIR" "$CURRENT_LINK" "$TARGET" "$KEEP_VERSIONS" fi +# --- Sweep the legacy ~/.cua-driver-rs home ----------------------------- +# +# This release installer used to default HOME_DIR to ~/.cua-driver-rs (the +# pre-v0.2.16 name). Now that it writes to ~/.cua-driver like install-local +# and the runtime, a prior RELEASE install can have left a stale +# ~/.cua-driver-rs behind — the source of the two-homes collision this PR +# fixes. Sweep it now that the new install is fully staged under the canonical +# home, mirroring the same belt-and-braces sweep _install-local-rust.sh does. +# Runs AFTER staging so we never delete state before the replacement exists; +# skipped when the user pinned CUA_DRIVER_RS_HOME to the legacy path on +# purpose. Best-effort + idempotent. +if [[ -d "$LEGACY_HOME_DIR" && "$HOME_DIR" != "$LEGACY_HOME_DIR" ]]; then + rm -rf "$LEGACY_HOME_DIR" 2>/dev/null \ + && log "swept legacy package home $LEGACY_HOME_DIR (reconciled onto $HOME_DIR)" \ + || log "note: could not fully remove legacy package home $LEGACY_HOME_DIR (best-effort)" +fi + # --- Stop any pre-swap cua-driver daemons ------------------------------- # # Mirror of install.ps1's `Stop-CuaDriverDaemons` call sequence. The @@ -663,7 +778,7 @@ show_cua_driver_daemon_survivors # --- Fire the one-shot install telemetry ping --------------------------- # # Anonymous adoption signal — sends `cua_driver_install` to PostHog -# exactly once per install (guarded by ~/.cua-driver-rs/.installation_recorded +# exactly once per install (guarded by ~/.cua-driver/.installation_recorded # on the binary side). The Rust port keeps its install signal independent # of the Swift `cua-driver` install (separate marker dir + separate env var) # so users can opt out of one without affecting the other.