Skip to content

feat(cua-driver-rs): Windows installer + ARM64 build + versioned-dirs install layout - #1540

Merged
f-trycua merged 9 commits into
mainfrom
feat/cua-driver-rs-install-ux
May 17, 2026
Merged

feat(cua-driver-rs): Windows installer + ARM64 build + versioned-dirs install layout#1540
f-trycua merged 9 commits into
mainfrom
feat/cua-driver-rs-install-ux

Conversation

@f-trycua

@f-trycua f-trycua commented May 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Three related install-UX improvements for the cua-driver-rs (Rust) port — landing together because they share the same on-disk layout / asset-naming conventions.

1. Versioned-dirs + current-symlink install layout (Linux)

Linux installs now drop the binary into a per-version directory and swap the active version via an atomic rename of a current symlink:

$CUA_DRIVER_RS_HOME/                            (default ~/.cua-driver-rs)
  packages/
    releases/
      0.2.0-x86_64-unknown-linux-gnu/cua-driver   (immutable per version)
      0.2.1-x86_64-unknown-linux-gnu/cua-driver
    current/cua-driver -> ../releases/<active>/cua-driver
$CUA_DRIVER_RS_INSTALL_DIR/cua-driver -> $CUA_DRIVER_RS_HOME/packages/current/cua-driver

PATH consumers (and MCP client configs that hard-code ~/.local/bin/cua-driver) never need to change when the active version moves. Rollback is a single ln -sfn against current.

Also: the legacy CUA_DRIVER_RS_BIN_DIR env is renamed CUA_DRIVER_RS_INSTALL_DIR (old name still accepted, undocumented), and CUA_DRIVER_RS_HOME is added to override the package home.

2. PowerShell installer + ARM64 Windows builds

New libs/cua-driver-rs/scripts/install.ps1 (~600 LOC including doc-comments + inline C# P/Invoke for NTFS reparse points). Same three-tier layout as Linux, but wired with directory junctions instead of symlinks:

<visibleBinDir>   [junction → currentDir]   = %LOCALAPPDATA%\Programs\trycua\cua-driver-rs\bin
<currentDir>      [junction → release dir]  = %USERPROFILE%\.cua-driver-rs\packages\current
<release dir>     [real dir, immutable]     = %USERPROFILE%\.cua-driver-rs\packages\releases\<v>-<target>
                                                cua-driver.exe

Directory junctions (IO_REPARSE_TAG_MOUNT_POINT) are creatable by any unprivileged user — no admin, no Developer Mode. Implementation is inline C# inside the PS1 (a CuaDriverInstaller.Junction static class exposing SetTarget / GetTarget via P/Invoke to DeviceIoControl); PowerShell wrappers Ensure-Junction / Test-IsJunction / Set-JunctionTarget sit on top. Ensure-Junction refuses to clobber an existing non-junction directory at the link path so the installer never silently nukes user files.

Architecture detection uses [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture — so an x64 PowerShell on an arm64 host still picks the native arm64 binary. Unsupported arches fail fast with a hint to file an issue (no silent fallback).

CD workflow changes:

  • build-windows is now matrixed across x86_64 + arm64 (both cross-compile from windows-latest; no separate arm64 runner needed).
  • The release job stages install.sh + install.ps1 as first-class release assets, so the canonical one-liners hit github.com/trycua/cua/releases/latest/download/install.{sh,ps1} — stable across releases.

3. Rosetta detection on Apple Silicon (already-landed first commit)

uname -m reports x86_64 when called from a Rosetta-translated shell on an Apple Silicon Mac (e.g. arch -x86_64 bash, Homebrew pinned to /usr/local/). We now check sysctl.proc_translated and switch to the native arm64 download so users don't end up running an x86_64 binary under Rosetta.

macOS asymmetry (intentional)

macOS still places CuaDriverRs.app in /Applications and symlinks ~/.local/bin/cua-driver into the bundle. The .app placement is the anchor for both TCC attribution (cdhash + bundle id) and LaunchServices (open -a CuaDriverRs) — symlinking the .app from /Applications to a versioned dir under $CUA_DRIVER_RS_HOME would break both. Rollback on macOS therefore = reinstall with CUA_DRIVER_RS_VERSION=<x.y.z>. A code comment in the install.sh branch explains this; the docs page surfaces it in a callout.

Test plan

  • bash -n libs/cua-driver-rs/scripts/install.sh — passes
  • bash -n libs/cua-driver/scripts/install.sh — passes
  • macOS smoke-test of --experimental-rust delegation:
    CUA_DRIVER_RS_HOME=/tmp/smoke-test-home CUA_DRIVER_RS_VERSION=0.2.0 \
      libs/cua-driver/scripts/install.sh --experimental-rust \
      --bin-dir /tmp/smoke-test-bindir --no-modify-path
    Verified: hits the macOS .app branch (unchanged), drops CuaDriverRs.app into /Applications, symlinks /tmp/smoke-test-bindir/cua-driver. The new HOME_DIR / versioned-dirs code path is skipped on macOS by design.
  • Linux smoke-test of the new versioned-dirs path — needs a Linux runner with a published cua-driver-rs-v* release. Manual recipe (any user):
    CUA_DRIVER_RS_HOME=/tmp/cdrs-home CUA_DRIVER_RS_VERSION=0.2.0 \
      libs/cua-driver-rs/scripts/install.sh --bin-dir /tmp/cdrs-bin --no-modify-path
    ls -la /tmp/cdrs-home/packages/{current,releases}
    /tmp/cdrs-bin/cua-driver --version
    # Then exercise rollback once a second release exists:
    ln -sfn ../releases/0.1.9-x86_64-unknown-linux-gnu /tmp/cdrs-home/packages/.current.tmp
    mv -Tf /tmp/cdrs-home/packages/.current.tmp /tmp/cdrs-home/packages/current
    /tmp/cdrs-bin/cua-driver --version    # → 0.1.9
  • Windows smoke-test of install.ps1 — blocked on this PR landing + a new cua-driver-rs-v* tag (install.ps1 needs to exist at the release asset URL). Once the next release ships, manual recipe in a clean Windows 11 user shell:
    irm https://github.com/trycua/cua/releases/latest/download/install.ps1 | iex
    # Verify the junction chain:
    fsutil reparsepoint query "$env:LOCALAPPDATA\Programs\trycua\cua-driver-rs\bin"
    fsutil reparsepoint query "$env:USERPROFILE\.cua-driver-rs\packages\current"
    # Roll back:
    $env:CUA_DRIVER_RS_VERSION = "0.2.0"
    irm https://github.com/trycua/cua/releases/latest/download/install.ps1 | iex
  • CI: validate that the windows-arm64 cross-compile from windows-latest succeeds on the first matrixed run. If it doesn't, fallback is a windows-11-arm runner — but Rust + MSVC ARM64 cross has been first-class for a while, so we expect it to just work.
  • PSScriptAnalyzer lint: not run locally (pwsh is not available on the dev machine without a sudo install). Will land in the first windows-latest CI run.

Notes for reviewers

  • The PS1 file is large because the inline C# Junction class needs the full DeviceIoControl P/Invoke + REPARSE_DATA_BUFFER layout — pulling it out into a separate .cs file would mean either a build step at install time or shipping a precompiled DLL, both worse trade-offs than ~50 LOC of inline C# the user can read.
  • The --experimental-rust delegation path in libs/cua-driver/scripts/install.sh is unchanged; the only relevant rename (CUA_DRIVER_RS_BIN_DIRCUA_DRIVER_RS_INSTALL_DIR) is accepted under both names in install.sh, so forwarded args from the Swift installer keep working.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Windows ARM64 support for cua-driver-rs
    • Introduced PowerShell installer script for Windows with versioned releases and rollback capability
    • Enhanced shell installer with atomic upgrade/rollback for Linux
  • Documentation

    • Added comprehensive installation guide for cua-driver-rs across Linux, Windows, and macOS with platform-specific one-liner commands and environment variable overrides
    • Documented versioned release layout and rollback procedures

Review Change Stack

f-trycua and others added 4 commits May 17, 2026 13:01
`uname -m` reflects the running process's architecture, not the physical
CPU. On Apple Silicon driving a Rosetta-translated shell (e.g.
`arch -x86_64 bash`, or a Homebrew install pinned to /usr/local/), uname
reports x86_64 and the installer pulled the x86_64 binary -- which then
ran under Rosetta even though a native arm64 binary was on the release
page.

Probe `sysctl.proc_translated` after the Darwin/x86_64 case: if it
returns 1, the current shell is translated and the native arch is arm64.
Switch ARCH_RAW to arm64 so the rest of the script picks the right
tarball. Silent no-op on Linux (sysctl key missing -> stderr swallowed).
Linux installs now drop the binary into
$CUA_DRIVER_RS_HOME/packages/releases/<version>-<target>/cua-driver and
swap the active version via an atomic rename of a `current` symlink
under packages/. The visible $CUA_DRIVER_RS_INSTALL_DIR/cua-driver
symlinks into `current`, so PATH consumers (and MCP client configs)
never need to change when the active version moves. Older releases stay
on disk; rollback is a one-line `ln -sfn` against `current`.

Also rename the legacy CUA_DRIVER_RS_BIN_DIR env to
CUA_DRIVER_RS_INSTALL_DIR (the BIN_DIR name is kept as a quiet alias
so existing shell rc lines keep working), document a CUA_DRIVER_RS_HOME
override for the package home, and update the unsupported-platform
hint to point Windows users at install.ps1.

macOS path stays unchanged. The .app placement at
/Applications/CuaDriverRs.app is the anchor for both TCC attribution
(cdhash + bundle id) and LaunchServices' `open -a` discovery —
symlinking the .app from /Applications to a versioned dir under
$HOME_DIR would break both. The asymmetry is deliberate; rollback on
macOS = reinstall an older release tag. A code comment explains this
in the install branch.

Smoke-tested locally via
  libs/cua-driver/scripts/install.sh --experimental-rust \\
    --bin-dir /tmp/smoke-test-bindir --no-modify-path
which exercises the macOS delegation path end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three-tier install layout on Windows, wired with directory junctions
(NTFS reparse points, IO_REPARSE_TAG_MOUNT_POINT) so the install runs
without admin and without Developer Mode:

  <visibleBinDir>   [junction → currentDir]
    = %LOCALAPPDATA%\Programs\trycua\cua-driver-rs\bin
  <currentDir>      [junction → release dir]
    = %USERPROFILE%\.cua-driver-rs\packages\current
  <release dir>     [real dir, immutable per version]
    = %USERPROFILE%\.cua-driver-rs\packages\releases\<v>-<target>
        cua-driver.exe

Atomic upgrade = retarget <currentDir> at a new release dir. Rollback =
retarget <currentDir> at an older release dir already on disk. PATH
entry stays stable across both.

Implementation: ~50 LOC of inline C# inside install.ps1 exposes
CuaDriverInstaller.Junction with SetTarget / GetTarget via P/Invoke
to DeviceIoControl(FSCTL_SET_REPARSE_POINT / FSCTL_GET_REPARSE_POINT).
PowerShell helpers Ensure-Junction / Test-IsJunction sit on top. The
Ensure-Junction helper refuses to clobber an existing non-junction
directory at the link path — print a clear message and exit non-zero
so the installer never silently nukes user files.

Architecture detection uses
[System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture so
an x64 PowerShell on an arm64 host still picks the native arm64
binary (no WOW64 fallback). Unsupported arches fail fast with a hint
to file an issue — no silent fallback to a slower target.

CD workflow changes:
- build-windows is now matrixed across x86_64 and arm64. Both targets
  cross-compile from the windows-latest runner; no separate arm64
  runner needed.
- release job stages install.sh + install.ps1 as first-class release
  assets so the canonical one-liners hit
  github.com/trycua/cua/releases/latest/download/install.{sh,ps1}
  — that URL is stable across releases.
- Release notes get a Windows one-liner block plus arm64 artifact
  entries.

The Windows VM smoke test is blocked on this PR landing + a new
release tag (install.ps1 needs to be downloadable from the release
URL). Local syntax review: install.{sh} bash -n passes; pwsh is not
on this dev machine (would need sudo to install via brew), so the
PSScriptAnalyzer lint will land in CI on the first windows-latest
job run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds three things to the cua-driver-rs section of the installation page:

- Windows one-liner using install.ps1 from the GitHub Releases asset URL,
  with a note that the installer auto-detects x64 / arm64 and needs
  neither admin nor Developer Mode.
- ASCII trees for the versioned-dirs install layout on Linux and Windows,
  showing the chain bin → current → releases/<v> and which links are
  symlinks (Linux) vs directory junctions (Windows).
- Rollback recipes per platform — `mv -Tf` for Linux, re-run-installer-
  with-version-pin for Windows (the installer's idempotent path skips
  the download and just retargets the junction when the release is
  already on disk).
- A four-row env-var table covering CUA_DRIVER_RS_{VERSION,INSTALL_DIR,
  HOME,NO_MODIFY_PATH} with defaults per platform.
- A callout explaining why macOS keeps its .app-in-/Applications layout
  (TCC attribution + LaunchServices anchoring) instead of using the
  versioned-dirs layout — and that rollback on macOS is therefore
  reinstall-with-version-pin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 17, 2026 0:56am

Request Review

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6b273f08-4e34-4da4-9667-919b39fa5240

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements cross-platform direct installation for the Rust-based CUA driver using versioned immutable release directories with atomic upgrade support. New PowerShell and updated bash installers manage symlink/junction-based "current" pointers per platform, while the CI workflow matrices Windows builds for x86_64/ARM64 and publishes the installer scripts as canonical release artifacts.

Changes

Direct Installation Scripts and Multi-Platform Distribution

Layer / File(s) Summary
Installation Guide Documentation
docs/content/docs/cua-driver/guide/getting-started/installation.mdx
Documents cross-platform one-liners for Linux/Windows/macOS, versioned directory layout using packages/releases/<version>-<target>/ with atomic current symlink/junction pointers, upgrade/rollback behavior (symlink retarget + safe rename for Linux, junction retarget for Windows), and environment variable overrides (CUA_DRIVER_RS_VERSION, CUA_DRIVER_RS_INSTALL_DIR, CUA_DRIVER_RS_HOME, CUA_DRIVER_RS_NO_MODIFY_PATH). Includes macOS-specific callout explaining layout differences and reinstall-based rollback.
Bash Installer Updates (Linux/macOS)
libs/cua-driver-rs/scripts/install.sh
Adds support for CUA_DRIVER_RS_INSTALL_DIR and CUA_DRIVER_RS_HOME environment variables with backward compatibility. Detects Rosetta translation on macOS via sysctl.proc_translated to select native arm64 instead of emulated x86_64 downloads. Replaces direct binary placement with versioned release directory layout for Linux/WSL ($HOME_DIR/packages/releases/<version>-<target>/) and implements atomic current symlink updates with Linux mv -Tf and fallback behavior. Repoints the visible $BIN_LINK to the versioned path instead of direct binary installation.
PowerShell Installer (Windows)
libs/cua-driver-rs/scripts/install.ps1
New Windows installer that auto-detects runtime architecture via RuntimeInformation.OSArchitecture and maps to Rust target triples. Resolves version from CUA_DRIVER_RS_VERSION environment variable or queries GitHub releases API (sorting semver-descending) to find the latest matching release. Downloads pre-built binary, manages versioned release directories ($HOME_DIR/packages/releases/<version>-<target>/), and implements NTFS directory junction indirection via inline P/Invoke C# code for reparse point management. Supports atomic upgrade by retargeting junctions. Includes best-effort telemetry, PATH validation, and comprehensive user instructions without auto-modifying PATH.
Workflow Configuration and Distribution
.github/workflows/cd-rust-cua-driver.yml
Converts Windows build job to a matrix strategy for x86_64 and arm64 targets, parameterizing Rust compilation targets, cache keys, and per-architecture artifact naming. Copies installer scripts (install.sh and install.ps1) into staged release assets as canonical first-class artifacts. Replaces Windows installation instructions in release notes with a PowerShell one-liner using install.ps1 and documents auto-detection/install-layout behavior. Extends artifacts documentation to include Windows ARM64 binaries and explicitly lists installer scripts.

Possibly Related PRs

  • trycua/cua#1518: Modifies libs/cua-driver-rs/scripts/install.sh macOS target architecture selection for downloaded assets, overlapping directly with this PR's Rosetta detection logic.
  • trycua/cua#1530: Changes install.sh macOS installer logic for CuaDriverRs.app bundle and symlink handling, directly related to this PR's macOS install layout modifications.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~75 minutes


🐰 A tale of install scripts so spry,
From Windows to Linux, reaching the sky,
With junctions and symlinks, atomic and clean,
Versioned releases—the best we've seen!
One hop, two hops, the binary takes flight,
Cross-platform installs done just right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and accurately summarizes the three main changes: Windows installer script, ARM64 build support, and versioned-directory install layout for cua-driver-rs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cua-driver-rs-install-ux

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/cd-rust-cua-driver.yml (2)

427-459: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

The advertised releases/latest installer URL won't point at this release.

This workflow publishes cua-driver-rs as a prerelease with make_latest: false (lines 427, 487), but the release body instructs Windows users to fetch /releases/latest/download/install.ps1. GitHub's /releases/latest endpoint excludes both prereleases and releases marked with make_latest: false, so that URL will resolve to an older non-prerelease release instead of this one. The Windows one-liner will fail to download the current release assets until a non-prerelease version includes install.ps1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/cd-rust-cua-driver.yml around lines 427 - 459, The release
body currently instructs Windows users to fetch
/releases/latest/download/install.ps1 but the workflow creates the release with
prerelease: true and make_latest: false, so that URL will not point to this
prerelease; update the Windows install link in the release body (the PowerShell
one-liner) to use the tag-specific download URL generated by the release step
instead of /releases/latest (for example build the URL from the release/tag
produced by the create_release step outputs such as
steps.create_release.outputs.tag_name or
steps.create_release.outputs.upload_url), or alternatively change the
create_release settings (prerelease/make_latest) if you intend /releases/latest
to resolve to this release; ensure the replacement references the same workflow
symbols used in the file (prerelease, make_latest,
steps.create_release.outputs.tag_name) so the link reliably points to this
release's install.ps1.

134-142: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Archive the stage directory, not just its contents.

The installer script expects the ZIP to extract to extracted/cua-driver-rs-<version>-<arch>/cua-driver.exe, but Compress-Archive -Path "release/$stage/*" strips the top-level $stage folder. Every Windows install fails the validation check that expects cua-driver.exe inside the top-level directory.

Suggested fix
-          Compress-Archive -Path "release/$stage/*" -DestinationPath "release/$stage.zip" -Force
+          Compress-Archive -LiteralPath "release/$stage" -DestinationPath "release/$stage.zip" -Force
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/cd-rust-cua-driver.yml around lines 134 - 142, The archive
command currently strips the top-level folder because Compress-Archive is called
with "release/$stage/*"; update the call that creates the full stage archive so
it passes the folder itself (use "release/$stage") instead of its contents,
keeping the same -DestinationPath "release/$stage.zip" and leaving the separate
bare-binary Compress-Archive for "release/$stage/cua-driver.exe" unchanged; look
for the $stage variable and the Compress-Archive invocation to modify.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/content/docs/cua-driver/guide/getting-started/installation.mdx`:
- Around line 82-90: The docs show the wrong symlink layout: the installer makes
$CUA_DRIVER_RS_HOME/packages/current a symlink to releases/<version>-<target>,
not $CUA_DRIVER_RS_HOME/packages/current/cua-driver; update the example and
rollback snippet to show packages/current ->
../releases/0.2.1-x86_64-unknown-linux-gnu and the install link as
$CUA_DRIVER_RS_INSTALL_DIR/cua-driver ->
$CUA_DRIVER_RS_HOME/packages/current/cua-driver (with releases/.../cua-driver
remaining the real binary); adjust the examples referenced around the rollback
snippet and lines 114-117 accordingly so packages/.current.tmp resolution
matches the actual installer behavior.

In `@libs/cua-driver-rs/scripts/install.ps1`:
- Around line 385-416: Ensure-Junction currently removes the existing junction
with Remove-Item then recreates it which causes a brief window where the link is
missing; instead, create the new reparse-point under a temporary name next to
$linkPath (e.g., $linkPath + '.tmp'), point that temp junction at $targetPath
using Set-JunctionTarget/New-Item, and then atomically replace the original link
by renaming/moving the temp into place (Move-Item or an atomic Replace API) so
the swap is instantaneous; reference Ensure-Junction, Remove-Item and
Set-JunctionTarget in your change, make sure to handle failure by removing the
temp and preserve behavior when the existing target already matches, and only
fall back to the current delete-and-create if atomic replace is unavailable on
the host.
- Around line 432-450: The current resolution only requests a single page of
releases (per_page=40) and can miss matching cua-driver-rs tags; modify the
logic to paginate GitHub releases by looping requests with a page parameter
(incrementing page until the API returns no items) and aggregate all responses
into $releases (or a new collection) before filtering by $TagPrefix, then
compute $matches, sort by SemVer and pick $latest/$version as before; ensure you
use a reasonable per_page (e.g., 100), stop when an empty page is returned or
the Link header indicates no next page, and preserve existing handling of no
matches and error exit.

---

Outside diff comments:
In @.github/workflows/cd-rust-cua-driver.yml:
- Around line 427-459: The release body currently instructs Windows users to
fetch /releases/latest/download/install.ps1 but the workflow creates the release
with prerelease: true and make_latest: false, so that URL will not point to this
prerelease; update the Windows install link in the release body (the PowerShell
one-liner) to use the tag-specific download URL generated by the release step
instead of /releases/latest (for example build the URL from the release/tag
produced by the create_release step outputs such as
steps.create_release.outputs.tag_name or
steps.create_release.outputs.upload_url), or alternatively change the
create_release settings (prerelease/make_latest) if you intend /releases/latest
to resolve to this release; ensure the replacement references the same workflow
symbols used in the file (prerelease, make_latest,
steps.create_release.outputs.tag_name) so the link reliably points to this
release's install.ps1.
- Around line 134-142: The archive command currently strips the top-level folder
because Compress-Archive is called with "release/$stage/*"; update the call that
creates the full stage archive so it passes the folder itself (use
"release/$stage") instead of its contents, keeping the same -DestinationPath
"release/$stage.zip" and leaving the separate bare-binary Compress-Archive for
"release/$stage/cua-driver.exe" unchanged; look for the $stage variable and the
Compress-Archive invocation to modify.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6f722e0b-b0d5-44eb-9809-c5467ae20c4f

📥 Commits

Reviewing files that changed from the base of the PR and between 544725e and be292d4.

📒 Files selected for processing (4)
  • .github/workflows/cd-rust-cua-driver.yml
  • docs/content/docs/cua-driver/guide/getting-started/installation.mdx
  • libs/cua-driver-rs/scripts/install.ps1
  • libs/cua-driver-rs/scripts/install.sh

Comment on lines +82 to +90
```
$CUA_DRIVER_RS_HOME/ (default: ~/.cua-driver-rs)
packages/
releases/
0.2.0-x86_64-unknown-linux-gnu/cua-driver (real binary, immutable)
0.2.1-x86_64-unknown-linux-gnu/cua-driver (real binary, immutable)
current/cua-driver -> ../releases/0.2.1-x86_64-unknown-linux-gnu/cua-driver (symlink — active version)
$CUA_DRIVER_RS_INSTALL_DIR/cua-driver -> $CUA_DRIVER_RS_HOME/packages/current/cua-driver
(default: ~/.local/bin/cua-driver)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the Linux current symlink examples.

libs/cua-driver-rs/scripts/install.sh makes ~/.cua-driver-rs/packages/current itself the symlink to releases/<version>-<target>. The tree here and the rollback snippet both model current/cua-driver as the link, and ../releases/... from packages/.current.tmp resolves to the wrong directory, so the documented rollback command would create a broken link.

Suggested doc fix
-    current/cua-driver -> ../releases/0.2.1-x86_64-unknown-linux-gnu/cua-driver  (symlink — active version)
+    current -> releases/0.2.1-x86_64-unknown-linux-gnu  (symlink — active version)
 $CUA_DRIVER_RS_INSTALL_DIR/cua-driver -> $CUA_DRIVER_RS_HOME/packages/current/cua-driver
@@
-ln -sfn ../releases/0.2.0-x86_64-unknown-linux-gnu ~/.cua-driver-rs/packages/.current.tmp
+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
 cua-driver --version    # → 0.2.0

Also applies to: 114-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/content/docs/cua-driver/guide/getting-started/installation.mdx` around
lines 82 - 90, The docs show the wrong symlink layout: the installer makes
$CUA_DRIVER_RS_HOME/packages/current a symlink to releases/<version>-<target>,
not $CUA_DRIVER_RS_HOME/packages/current/cua-driver; update the example and
rollback snippet to show packages/current ->
../releases/0.2.1-x86_64-unknown-linux-gnu and the install link as
$CUA_DRIVER_RS_INSTALL_DIR/cua-driver ->
$CUA_DRIVER_RS_HOME/packages/current/cua-driver (with releases/.../cua-driver
remaining the real binary); adjust the examples referenced around the rollback
snippet and lines 114-117 accordingly so packages/.current.tmp resolution
matches the actual installer behavior.

Comment on lines +385 to +416
function Ensure-Junction([string]$linkPath, [string]$targetPath) {
if (Test-Path -LiteralPath $linkPath) {
if (Test-IsJunction $linkPath) {
# Existing junction — retarget it. Removing the empty
# reparse-point dir and recreating it is the simplest way to
# change the target atomically from PowerShell's POV (the
# underlying DeviceIoControl will fail with "directory not
# empty" otherwise).
$existingTarget = Get-JunctionTarget $linkPath
if ($existingTarget -and ($existingTarget.TrimEnd('\') -ieq $targetPath.TrimEnd('\'))) {
Write-Step "junction $linkPath already points at $targetPath (no change)"
return
}
# Remove-Item on a junction removes the link, not the target.
# -Force handles read-only / hidden attributes.
Remove-Item -LiteralPath $linkPath -Force -Recurse
}
else {
Write-ErrorStep "found existing non-junction directory at $linkPath; refusing to replace"
Write-ErrorStep " Move or remove $linkPath manually, then re-run the installer."
Write-ErrorStep " (The installer needs to put a directory junction there so future"
Write-ErrorStep " upgrades retarget the junction instead of overwriting your files.)"
exit 1
}
}
# Make sure the parent dir exists — CreateFile won't auto-mkdir.
$parent = Split-Path -Parent $linkPath
if ($parent -and -not (Test-Path -LiteralPath $parent)) {
New-Item -ItemType Directory -Force -Path $parent | Out-Null
}
Set-JunctionTarget $linkPath $targetPath
Write-Step "junction $linkPath -> $targetPath"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

This junction retarget is not atomic.

Ensure-Junction() deletes the live reparse point with Remove-Item before recreating it, so both packages\current and the visible bin junction disappear briefly on every upgrade/rollback. That breaks the atomic-swap contract described in the docs and can make concurrent cua-driver.exe lookups fail during installs.

🧰 Tools
🪛 PSScriptAnalyzer (1.25.0)

[warning] 385-385: The cmdlet 'Ensure-Junction' uses an unapproved verb.

(PSUseApprovedVerbs)


[warning] Missing BOM encoding for non-ASCII encoded file 'install.ps1'

(PSUseBOMForUnicodeEncodedFile)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver-rs/scripts/install.ps1` around lines 385 - 416,
Ensure-Junction currently removes the existing junction with Remove-Item then
recreates it which causes a brief window where the link is missing; instead,
create the new reparse-point under a temporary name next to $linkPath (e.g.,
$linkPath + '.tmp'), point that temp junction at $targetPath using
Set-JunctionTarget/New-Item, and then atomically replace the original link by
renaming/moving the temp into place (Move-Item or an atomic Replace API) so the
swap is instantaneous; reference Ensure-Junction, Remove-Item and
Set-JunctionTarget in your change, make sure to handle failure by removing the
temp and preserve behavior when the existing target already matches, and only
fall back to the current delete-and-create if atomic replace is unavailable on
the host.

Comment on lines +432 to +450
Write-Step "resolving latest $TagPrefix* release via GitHub API"
# Pull the first 40 releases so we still find the latest one even when
# several unrelated tag prefixes have shipped recently. The cua-driver-rs
# tag prefix is distinct from the Swift cua-driver tag prefix (one extra
# "-rs-"), so the simple StartsWith filter is unambiguous.
$releases = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases?per_page=40" `
-UseBasicParsing
$matches = $releases | Where-Object { $_.tag_name -like "$TagPrefix*" }
if (-not $matches) {
Write-ErrorStep "no release matching $TagPrefix* found on $Repo"
exit 1
}
# Sort by SemVer descending. [version] correctly orders dotted triples.
$latest = $matches | Sort-Object {
$v = $_.tag_name.Substring($TagPrefix.Length)
try { [version]$v } catch { [version]"0.0.0" }
} -Descending | Select-Object -First 1
$version = $latest.tag_name.Substring($TagPrefix.Length)
Write-Step "latest release: $($latest.tag_name)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't stop "latest" resolution at the first 40 repo releases.

This repo publishes multiple release families. Once 40 newer non-cua-driver-rs releases exist, page 1 can contain zero matching tags and this resolver will either fail or pin to a stale Rust release. Please paginate until you exhaust the release list or find the highest cua-driver-rs-v* tag.

🧰 Tools
🪛 PSScriptAnalyzer (1.25.0)

[warning] 439-439: The Variable 'matches' is an automatic variable that is built into PowerShell, assigning to it might have undesired side effects. If assignment is not by design, please use a different name.

(PSAvoidAssignmentToAutomaticVariable)


[warning] Missing BOM encoding for non-ASCII encoded file 'install.ps1'

(PSUseBOMForUnicodeEncodedFile)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver-rs/scripts/install.ps1` around lines 432 - 450, The current
resolution only requests a single page of releases (per_page=40) and can miss
matching cua-driver-rs tags; modify the logic to paginate GitHub releases by
looping requests with a page parameter (incrementing page until the API returns
no items) and aggregate all responses into $releases (or a new collection)
before filtering by $TagPrefix, then compute $matches, sort by SemVer and pick
$latest/$version as before; ensure you use a reasonable per_page (e.g., 100),
stop when an empty page is returned or the Link header indicates no next page,
and preserve existing handling of no matches and error exit.

f-trycua and others added 3 commits May 17, 2026 13:37
The cua-driver-rs releases ship as prereleases (prerelease: true +
make_latest: false), so GitHub's /releases/latest/download/ endpoint
excludes them — that URL was resolving to a non-prerelease that does
not contain install.ps1. Switch the Windows one-liner to fetch
install.ps1 from raw.githubusercontent.com/trycua/cua/main/... — the
same pattern the Linux/macOS install.sh one-liner already uses, and
stable across releases regardless of prerelease flags.

Also fix the Linux symlink layout shown in installation.mdx: the
"current" entry under packages/ IS the symlink (pointing at a
releases/<v>-<target>/ directory), and "current/cua-driver" is the
binary inside the target dir reached through that symlink. Older text
incorrectly drew "current/cua-driver" as the symlink itself. The
rollback recipe now uses the correct relative target shape
(releases/<v>-<target>, no leading "../") matching install.sh's
ln -s output.

Updates: workflow release body, docs page (two one-liner blocks +
layout tree + rollback recipe), install.ps1 self-doc, and install.sh's
Windows-user error hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Compress-Archive with a trailing /* on the source path strips the
wrapper directory and dumps files at the archive root. install.ps1
expects the zip to expand to extracted/cua-driver-rs-<v>-<arch>/
cua-driver.exe — without the wrapper the install lookup hits
expected $BinaryName inside $zipName but didn't find it and aborts.

Drop the /* so the archive includes the top-level $stage dir.
Verified locally with the zip(1) equivalent (zip -r out.zip <dir>
without a trailing /*): the resulting archive lists
cua-driver-rs-<v>-<arch>/cua-driver.exe at the expected path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ensure-Junction: drop the Remove-Item-then-recreate dance. NTFS exposes
DeviceIoControl(FSCTL_SET_REPARSE_POINT) as an in-place update of the
reparse-data buffer on an existing reparse point — a single kernel
call — so calling Set-JunctionTarget against the existing junction
swaps the target atomically without a window where the path is absent.
Removing first opened a brief gap where any concurrent path lookup
through the junction (PATH consumer, MCP client spawning cua-driver,
etc.) saw a missing directory.

Resolve-Version: paginate the GitHub releases endpoint instead of
relying on the first 40 entries. As the repo grows, cua-driver-rs-v*
tags can fall off the first page (Swift cua-driver-v* and other
release prefixes ship to the same repo), causing the resolver to
either pick a stale version or return no match. Now walks pages of
100 until the API returns fewer than per_page results or we've seen
10 pages (1000 releases — generous insurance against an unbounded
loop).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@f-trycua

Copy link
Copy Markdown
Collaborator Author

Addressed all 5 CodeRabbit findings — pushed 3 fixup commits to feat/cua-driver-rs-install-ux:

8a07d2f3 — Windows one-liner URL + docs symlink layout

  • Windows one-liner now fetches install.ps1 from raw.githubusercontent.com/trycua/cua/main/libs/cua-driver-rs/scripts/install.ps1. The previous /releases/latest/download/install.ps1 resolves to a non-prerelease (we ship as prerelease + make_latest: false, so we're excluded from /releases/latest), which doesn't contain install.ps1. Same pattern the Linux/macOS one-liner already uses.
  • Updated: workflow release body, docs page (two one-liner blocks), install.ps1 self-doc, install.sh's Windows-user error hint.
  • Also fixed the Linux symlink layout in installation.mdx: packages/current IS the symlink (pointing at releases/<v>-<target>/), and packages/current/cua-driver is the binary inside the target dir reached through that symlink. Rollback recipe corrected to use the right relative target shape (releases/<v>-<target>, no leading ../) matching what install.sh writes.

18039c7d — preserve top-level dir in Windows zip

  • Compress-Archive -Path "release/$stage/*" strips the wrapper dir; install.ps1 expects extracted/cua-driver-rs-<v>-<arch>/cua-driver.exe. Drop the trailing /* so the zip contains the top-level $stage directory.
  • Verified the layout locally with the zip(1) equivalent (zip -r out.zip <dir> without trailing /*); resulting archive lists cua-driver-rs-<v>-<arch>/cua-driver.exe at the expected path.

3425af0b — atomic junction retarget + paginated release lookup

  • Ensure-Junction: drop the Remove-Item-then-recreate dance. DeviceIoControl(FSCTL_SET_REPARSE_POINT) on an existing reparse point is an in-place update of the reparse-data buffer (single kernel call) — atomic, no window where the path is absent. The previous flow opened a brief gap where any concurrent path lookup through the junction (PATH consumer, MCP client spawn) could see a missing directory.
  • Resolve-Version: paginate the GitHub releases endpoint instead of relying on the first 40 entries. As the repo accumulates releases (Swift cua-driver-v* + other prefixes ship to the same repo), cua-driver-rs-v* tags can fall off the first page. Now walks pages of 100 until the API returns fewer than per_page results or 10 pages have been fetched (1000 releases, generous insurance against an unbounded loop).

Verified after each commit: bash -n libs/cua-driver-rs/scripts/install.sh passes, yq round-trips the workflow YAML cleanly. PowerShell parse not verified locally (no pwsh on dev machine — will surface in the first windows-latest CI run).

@coderabbitai please review the three new commits.

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I'll pull the three commit diffs now to give a thorough review.


🏁 Script executed:

#!/bin/bash
# Fetch diffs for the three fixup commits
for sha in 8a07d2f3 18039c7d 3425af0b; do
  echo "======== COMMIT $sha ========"
  gh api repos/trycua/cua/commits/$sha --jq '.commit.message' 2>/dev/null || echo "(message unavailable)"
  echo "--- DIFF ---"
  gh api repos/trycua/cua/commits/$sha --jq '.files[] | "FILE: \(.filename)\n+\(.additions) -\(.deletions)\nPATCH:\n\(.patch // "(binary or empty)")'
  echo ""
done

Length of output: 4040


Let me fix the script to properly retrieve the diffs.


🏁 Script executed:

#!/bin/bash
for sha in 8a07d2f3 18039c7d 3425af0b; do
  echo "======== COMMIT $sha ========"
  gh api repos/trycua/cua/commits/$sha > /tmp/commit_${sha}.json 2>&1
  jq -r '.files[] | "FILE: \(.filename)\n+\(.additions) -\(.deletions)\nPATCH:\n\(.patch // "(binary or empty)")\n---"' /tmp/commit_${sha}.json
  echo ""
done

Length of output: 10694


@f-trycua: All three commits look good. Here's a breakdown:


8a07d2f3 — Windows one-liner URL + docs symlink layout

All changes are correct and consistent:

  • Switching to raw.githubusercontent.com/trycua/cua/main/... is the right call — the prerelease + make_latest: false config correctly excludes the repo from /releases/latest/, and the raw URL is already the pattern Linux/macOS use.
  • The docs symlink tree fix is accurate: packages/current is the directory symlink pointing at releases/<v>-<target>/, and current/cua-driver is the binary reached through it — not a symlink itself.
  • The rollback recipe (releases/0.2.0-x86_64-unknown-linux-gnu, no leading ../) now matches the relative target install.sh actually writes with ln -s.

18039c7d — preserve top-level dir in Windows zip

Correct fix. Compress-Archive -Path "release/$stage/*" dumps files at the archive root (no wrapper); dropping the /* includes the $stage directory itself, so the extracted layout is cua-driver-rs-<v>-<arch>/cua-driver.exe — exactly what install.ps1 walks into.


3425af0b — atomic junction retarget + paginated release lookup

Both changes are sound:

  • Ensure-Junction: The in-place FSCTL_SET_REPARSE_POINT approach is the correct atomic primitive for NTFS junction retargeting. The old remove-then-recreate pattern had a real availability gap; the new code eliminates it.
  • Resolve-Version: Pagination (pages of 100, max 10 pages, early exit when $batch.Count -lt 100) is a robust improvement over a single 40-item fetch.

One minor naming note: $matches is a PowerShell automatic variable that holds regex match results — overwriting it can break regex operations. Within Resolve-Version the filter uses -like (not -match), so each -match call on scalar input that returns True would overwrite the automatic $Matches variable — meaning if any future edit introduces a -match call in the same scope, it would silently clobber the accumulator. The idiomatic fix is a descriptive name like $releaseMatches. This is low-severity given the current code only uses -like, but PSScriptAnalyzer would flag it.

# Suggested rename in Resolve-Version
$releaseMatches = @()
for ($page = 1; $page -le 10; $page++) {
    $uri = "https://api.github.com/repos/$Repo/releases?per_page=100&page=$page"
    $batch = Invoke-RestMethod -Uri $uri -UseBasicParsing
    if (-not $batch -or $batch.Count -eq 0) { break }
    $releaseMatches += @($batch | Where-Object { $_.tag_name -like "$TagPrefix*" })
    if ($batch.Count -lt 100) { break }
}
if (-not $releaseMatches -or $releaseMatches.Count -eq 0) { ... }

Otherwise the PR is in good shape. The PowerShell parse verification on windows-latest CI will be the final confirmation for the .ps1 changes.

f-trycua and others added 2 commits May 17, 2026 14:54
…ing CD bake step

Adds a sentinel-block-wrapped baked default to both scripts/install.sh
and scripts/install.ps1, with resolution order env > baked > API. The
default `curl … | bash` / `irm … | iex` install path from main now
resolves the release tag locally with zero GitHub API calls — an API
outage, rate limit, or network blip no longer breaks a default install.
The API path becomes the fallback used only by dev installs from
un-baked branches.

cd-rust-cua-driver.yml grows a Bake version into install scripts step
that mirrors the equivalent Swift workflow step: it generates a GitHub
App token (RELEASE_APP_ID + RELEASE_APP_PRIVATE_KEY) so the push to
main bypasses the pull-request ruleset, sed-rewrites the sentinel-block
line in both install scripts, and commits as trycua-release[bot] with
[skip ci] to suppress the recursive CD trigger. Uses GNU sed syntax
since the release job runs on ubuntu-latest (the Swift workflow uses
BSD sed because it runs on macos-15).

PARITY.md gets a new Installer: version-resolution chain section that
documents the precedence, the rationale (API outages, tag-prefix
pagination drift), the sentinel-block format, and the GNU-vs-BSD sed
asymmetry between the two CD workflows.

Initialised to 0.2.0 (the current latest tag) so the baked default is
live as soon as this lands — the next release will rebake it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… precedence

Adds a callout next to the env-var override table explaining how the
installer picks a version: explicit env pin first, then the baked-in
default that the release pipeline auto-updates after every published
release, then the GitHub Releases API as a fallback. Also tweaks the
CUA_DRIVER_RS_VERSION table entry — the default behaviour is now use
baked version, not resolve latest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@f-trycua

Copy link
Copy Markdown
Collaborator Author

Pushed two follow-up commits adding a baked-version fallback to both Rust install scripts, matching the Swift installer's env > baked > API resolution chain. Summary:

  • feat(install): baked-version fallback in Rust install scripts + matching CD bake step (b8af910a) — adds a sentinel-block-wrapped CUA_DRIVER_RS_BAKED_VERSION to scripts/install.sh and a $Script:CuaDriverRsBakedVersion to scripts/install.ps1, both initialised to 0.2.0. Reorders Resolve-Version (sh + ps1) so the precedence is env > baked > API. Adds a Bake version into install scripts step to cd-rust-cua-driver.yml that uses the GitHub App token (RELEASE_APP_ID + RELEASE_APP_PRIVATE_KEY) — same auth pattern as cd-swift-cua-driver.yml after 544725e4 — so the post-release push to main bypasses the pull-request ruleset. PARITY.md gets a dedicated section documenting the chain, the rationale, the sentinel-block format, and the GNU-vs-BSD sed asymmetry between the Rust and Swift workflows.
  • docs(installation): document the env > baked > API version-resolution precedence (f9a91ec6) — updates the fumadocs installer page with a callout next to the env-var override table.

Why: the default curl … | bash / irm … | iex install path against main now resolves the release tag locally with zero GitHub API calls. An API outage, an unauthenticated rate limit (60 req/hr per IP), or a transient network blip no longer breaks a default install. The API path becomes the fallback hit only by dev installs from un-baked branches.

Notes:

  • The install.sh single-page pagination (?per_page=40) is intentionally not touched in this change. It is now fallback-only — the bug surface is dev installs from branches that haven't been baked yet — so the urgency drops. Easy follow-up if anyone wants belt-and-braces (the pagination fix already landed in install.ps1 in 3425af0b).
  • Validations run locally: bash -n libs/cua-driver-rs/scripts/install.sh, bash -n libs/cua-driver/scripts/install.sh (Swift installer untouched, no regression), yq parse on the rewritten workflow. Sed bake patterns dry-run against the in-tree files to confirm the assignment lines are matched and rewritten end-to-end.

@coderabbitai please re-review the two new commits — b8af910a and f9a91ec6.

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
for sha in b8af910a f9a91ec6; do
  echo "======== COMMIT $sha ========"
  gh api repos/trycua/cua/commits/$sha > /tmp/commit_${sha}.json 2>&1
  jq -r '.commit.message' /tmp/commit_${sha}.json
  echo "--- FILES ---"
  jq -r '.files[] | "FILE: \(.filename)\n+\(.additions) -\(.deletions)\nPATCH:\n\(.patch // "(binary or empty)")\n---"' /tmp/commit_${sha}.json
  echo ""
done

Length of output: 14845


@f-trycua Both commits look solid. Here's the breakdown:


b8af910a — baked-version fallback in Rust install scripts + matching CD bake step

install.sh
Sentinel block and elif branch are correct. The ${CUA_DRIVER_RS_BAKED_VERSION#v} strip ensures either 0.2.0 or v0.2.0 written by the CD bake step works identically. ✓

install.ps1
$Script: scope is the right choice — script-scoped so Resolve-Version can read it without parameter threading. The falsy guard (if ($Script:CuaDriverRsBakedVersion)) gracefully falls through to the API on an empty string. ✓

cd-rust-cua-driver.yml — sed escape chain
The double-escape \\\$ in the bash double-quoted string collapses correctly:

  • bash "..." processing: \\\$\ + $ = \$
  • sed receives: ^\$Script:CuaDriverRsBakedVersion = .*
  • GNU sed BRE: \$ = literal $, so the pattern hits the exact line ✓

The replacement side works the same way, and ${VERSION} expands cleanly before sed sees it. The bake-from-origin/main + bake-version:main push mirrors the Swift workflow exactly. ✓

PARITY.md
The GNU vs. BSD sed -i asymmetry table is a good catch to document for future maintainers — that kind of cross-platform CI footgun is easy to forget.

Minor note (non-blocking): The actual Resolve-Version precedence order is $env:CUA_DRIVER_RS_VERSION → -Release arg → baked → API, whereas the PR description shortens it to env > baked > API. The inline code comments say it correctly (> -Release arg >), so nothing is wrong — just something to be aware of if the docs callout ever needs to call out the -Release escape hatch explicitly.


f9a91ec6 — docs update

The env-var table default correction (unset → use baked version) and the new <Callout> are accurate and match the implementation. The rate-limit context (60 req/hr per IP) is a useful user-facing explanation for why the baked default exists. ✓


Both commits are in good shape. The first real-world validation will be the CI run that fires after the next cua-driver-rs-v* tag push (the bake step + Windows pwsh parse).

@f-trycua

Copy link
Copy Markdown
Collaborator Author

@coderabbitai please re-review the latest commits — b8af910a (feat(install): baked-version fallback in Rust install scripts + matching CD bake step) and f9a91ec6 (docs(installation): document the env > baked > API version-resolution precedence) landed after your previous LGTM. They add a third tier to the version-resolution chain (env > baked > API) so the common curl | bash path doesn't depend on the GitHub releases API, mirroring the Swift install.sh shape. CD bake step uses the same App-token pattern that just landed in cd-swift-cua-driver.yml.

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the latest commits on the PR branch
gh api repos/trycua/cua/pulls/1540 --jq '.head.sha, .head.ref, .commits'
echo "--- Latest commits on branch ---"
gh api repos/trycua/cua/pulls/1540/commits --jq '.[] | "\(.sha[0:8]) \(.commit.message | split("\n")[0])"'

Length of output: 777


@f-trycua: The branch HEAD is still f9a91ec6 — no new commits since the previous review. Both b8af910a and f9a91ec6 were already covered in my response directly above this one. Quick recap for convenience:


b8af910a — baked-version fallback

  • install.sh: Sentinel block + elif branch correct; ${CUA_DRIVER_RS_BAKED_VERSION#v} strips an optional leading v from whatever the bake step writes. ✓
  • install.ps1: $Script: scope is appropriate — readable by Resolve-Version without parameter threading; falsy guard falls through cleanly to the API on an empty string. ✓
  • cd-rust-cua-driver.yml sed escape chain: \\\$ in bash double-quotes → \$ sent to sed → BRE literal $, matching the exact assignment line. The ${VERSION} substitution on the replacement side is correct. Bake-step App-token auth mirrors cd-swift-cua-driver.yml. ✓
  • PARITY.md: GNU vs. BSD sed -i asymmetry documented — useful for future maintainers. ✓

f9a91ec6 — docs update

  • Env-var table default correction (unset → use baked version) and the <Callout> block are accurate and consistent with the implementation. Rate-limit context is a helpful user-facing note. ✓

The only outstanding validation is the first CI run after a cua-driver-rs-v* tag push (the bake step rewrite + Windows pwsh parse of install.ps1). Everything else is good to go.

@f-trycua
f-trycua merged commit 497509e into main May 17, 2026
6 of 7 checks passed
@f-trycua
f-trycua deleted the feat/cua-driver-rs-install-ux branch May 17, 2026 14:01
f-trycua added a commit that referenced this pull request May 17, 2026
…Windows)

The versioned-dirs layout (PR #1540) preserves every released binary
under packages/releases/<v>-<target>/ so rollback is a one-step
symlink/junction retarget. Disk usage grows ~15 MB per upgrade and
accumulates indefinitely, which is fine for the first few upgrades
and not fine forever.

Post-install GC trims oldest per-target dirs back to a configurable
cap, defaulting to the N=5 most recent:

  CUA_DRIVER_RS_KEEP_VERSIONS=<N>  (default 5; set 0 to disable)

Invariants:
- Per-target filtering — multi-arch dirs are GC'd independently of
  each other (only entries matching the current $TARGET suffix are
  prune candidates).
- Active install is always preserved — even if `current` resolves
  to a dir older than the keep window (e.g. user rolled back). The
  worst-case post-GC dir count is keep + 1.
- Runs after the atomic `current` swap, so the about-to-be-active
  version is never a deletion candidate.
- macOS path untouched — /Applications/CuaDriverRs.app installs are
  in-place replacements (no per-version accumulation), so the GC
  pass is a no-op there by construction.

install.sh: prune_old_releases uses `ls -dt` for mtime-sorted
candidates, readlink-derives the current dir to exempt it, and
`xargs -0 rm -rf` removes the excess.

install.ps1: Invoke-OldReleasesGc uses
`Get-ChildItem | Where-Object -like "*-$target" | Sort-Object
LastWriteTime -Descending`, resolves the current junction target
to exempt it, and `Remove-Item -Recurse -Force` removes the excess.

Both validators reject non-integer / negative env values with a
warning + fallback to the default so a typo can't silently disable
GC for everyone.

PARITY.md: the existing "Installer: version-resolution chain"
section is promoted to a broader "Installer: layout + lifecycle"
parent with the GC behavior + verification recipes as a peer
sub-section to the version-resolution chain doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f-trycua added a commit that referenced this pull request May 17, 2026
…t-install lockfile (#1541)

* feat(install): garbage-collect old per-version release dirs (Linux + Windows)

The versioned-dirs layout (PR #1540) preserves every released binary
under packages/releases/<v>-<target>/ so rollback is a one-step
symlink/junction retarget. Disk usage grows ~15 MB per upgrade and
accumulates indefinitely, which is fine for the first few upgrades
and not fine forever.

Post-install GC trims oldest per-target dirs back to a configurable
cap, defaulting to the N=5 most recent:

  CUA_DRIVER_RS_KEEP_VERSIONS=<N>  (default 5; set 0 to disable)

Invariants:
- Per-target filtering — multi-arch dirs are GC'd independently of
  each other (only entries matching the current $TARGET suffix are
  prune candidates).
- Active install is always preserved — even if `current` resolves
  to a dir older than the keep window (e.g. user rolled back). The
  worst-case post-GC dir count is keep + 1.
- Runs after the atomic `current` swap, so the about-to-be-active
  version is never a deletion candidate.
- macOS path untouched — /Applications/CuaDriverRs.app installs are
  in-place replacements (no per-version accumulation), so the GC
  pass is a no-op there by construction.

install.sh: prune_old_releases uses `ls -dt` for mtime-sorted
candidates, readlink-derives the current dir to exempt it, and
`xargs -0 rm -rf` removes the excess.

install.ps1: Invoke-OldReleasesGc uses
`Get-ChildItem | Where-Object -like "*-$target" | Sort-Object
LastWriteTime -Descending`, resolves the current junction target
to exempt it, and `Remove-Item -Recurse -Force` removes the excess.

Both validators reject non-integer / negative env values with a
warning + fallback to the default so a typo can't silently disable
GC for everyone.

PARITY.md: the existing "Installer: version-resolution chain"
section is promoted to a broader "Installer: layout + lifecycle"
parent with the GC behavior + verification recipes as a peer
sub-section to the version-resolution chain doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(install): per-host install lockfile to serialize concurrent installs

Two installs running at the same time — e.g. a user clicks the
one-liner while a CI script is also installing, two terminals racing,
or a cron-driven reinstall colliding with a manual one — can race on
the atomic `current` symlink swap and leave the visible binary
pointing at a partially-populated release dir.

Serialize installs per $HOME_DIR with a process-level mutex:

  install.sh : mkdir-based mutex on $HOME_DIR/packages/.install.lock.d
               (atomic on POSIX, no flock dependency).
  install.ps1: System.IO.FileStream with FileShare::None on
               $HomeDir\install.lock (Windows-native, no admin needed,
               no named-mutex registration).

Both primitives are unprivileged — no sudo, no Developer Mode, no
admin elevation. The open call itself is the mutex acquisition; a
second concurrent attempt blocks until the first holder releases.

UX:
- Poll every 1s, print "another cua-driver-rs install is already in
  progress (lock at <path>); waiting..." exactly once on first stall.
- 600s stale-detection threshold (named constant
  LOCK_STALE_AFTER_SECONDS / $Script:LockStaleAfterSeconds — not a
  magic number). After 600s of waiting, log "lock appears stale
  (>600s), forcing release" and reclaim. Hanging forever with no
  recovery path would wedge users with no obvious fix.
- Stamp pid + ISO timestamp + invocation args into the lock entry so
  a user investigating a stuck install can `cat` / `Get-Content` the
  info file and see exactly who's holding it.

Cleanup guarantees:
- install.sh: trap cleanup_on_exit EXIT + per-signal traps for INT/TERM
  that release then re-raise (so $? still reflects the signal exit
  code). A partially-installed run frees the lock for the next user.
- install.ps1: try { Main } finally { Release-InstallLock } wrapping
  the entire Main block. PowerShell's finally fires on normal exit,
  exceptions, `exit`, and Ctrl-C (pipeline-stop).

PARITY.md: new "Per-host concurrent-install lockfile" sub-section
under "Installer: layout + lifecycle" covering the primitive choice
per platform, the wait/stale UX, the lock-info stamp, and the
release-on-every-exit-path guarantee. Verification recipes included.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(installation): document GC + concurrent-install lockfile behavior

Extends the cua-driver-rs installation guide with two new callouts
under "Env-var overrides", matching the install-script features that
just landed.

1. Old-version cleanup — explains the default keep=5 GC, the
   CUA_DRIVER_RS_KEEP_VERSIONS env override (including the `0`
   sentinel for disabling), and the two invariants users care about
   (active install always preserved, per-target filtering for
   multi-arch dirs). Calls out that the macOS path is unaffected.

2. Concurrent-install lockfile — explains the "waiting for lock..."
   message users may see when two installs race, where to find the
   lock entry per platform, and what the 600s stale-recovery
   threshold means in practice (worst-case 10-minute wait, not a
   manual rm-rf on internal paths).

The new CUA_DRIVER_RS_KEEP_VERSIONS row is also added to the existing
env-var table so it surfaces alongside the other overrides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): verify lock holder is dead before force-releasing stale lock

The 600s stale-lock branch on both install.sh and install.ps1 used to
delete the lock unconditionally once the timeout elapsed. A long-running
but live install (big download, slow network) would get its lock yanked
out from under it and a second install would then race the active one
on the atomic current swap — the exact bug the lockfile exists to
prevent.

Both platforms now verify the previous holder is actually dead before
reclaiming:

- install.sh: parse pid= from $LOCK_INFO (stamped right after mkdir),
  then `kill -0 <pid>` to test liveness. Live → keep waiting. Dead or
  unparseable info file → force-release as before. Missing info file
  means the holder didn't get far enough to stamp pid, so we treat it
  as dead and reclaim (preferring progress over hanging forever).

- install.ps1: probe the lockfile with the same FileShare::None
  primitive used by the main acquire path. Open succeeds → previous
  holder's FileStream handle is really gone, the leftover file is safe
  to delete. IOException → holder is alive but slow, keep waiting.

The 600s threshold itself doesn't change — the fix is about verifying
liveness BEFORE acting on the timeout, not about extending the wait.

PowerShell syntax check skipped (pwsh unavailable in the dev env);
install.sh bash -n clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cd): bake version in-tree before staging release assets

The release job was copying libs/cua-driver-rs/scripts/install.{sh,ps1}
into release-upload/ at the Stage step, then later running the
bake-version sed inside a separate `git checkout -B bake-version
origin/main` worktree. The on-main commit got the freshly-baked
scripts, but the per-tag GitHub Release assets shipped the N-1 baked
default — anyone fetching install.sh / install.ps1 via the tag-pinned
asset URL got the previous version's CUA_DRIVER_RS_BAKED_VERSION,
which is exactly what the baked-version fallback exists to avoid.

Split the bake step in two:

1. New `Bake version into install scripts (in-tree)` step runs BEFORE
   Stage release files, rewriting the working-tree copies via the same
   GNU sed patterns. The cp into release-upload/ then picks up the
   freshly-baked content.

2. Existing bake step renamed to `Bake version into install scripts
   (commit + push)` and runs at the same place as before (after the
   GitHub Release is created). It does `git checkout -B bake-version
   origin/main` and re-applies the sed against that clean tree, so the
   commit landing on main has the right baked version regardless of
   the in-tree step (idempotent — sed on an already-baked file is a
   no-op for that version, and the working-tree edits are discarded
   by the checkout anyway).

Also fix PARITY.md MD040 — two bare code fences (the BAKED_VERSION
sentinel block and the install-lock info-file example) now declare
`bash` as their language so markdownlint stops flagging them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(installation): split long sentences in Windows-install + KEEP_VERSIONS rows

Two prose chunks in the cua-driver-rs install page packed multiple
ideas into a single sentence and were flagged as hard to scan:

- The Windows-installer paragraph mashed arch detection, no-admin /
  no-Developer-Mode, and the NTFS-junction layout into one sentence.
  Split into three short paragraphs — one each for arch detection, the
  privilege story, and the junction-based layout (plus the underlying
  IO_REPARSE_TAG_MOUNT_POINT detail since that's the actual reason no
  elevation is needed).

- The CUA_DRIVER_RS_KEEP_VERSIONS table cell had four ideas crammed
  in: keep semantics, the 0=disabled override, per-target filtering,
  and the active-install-always-preserved invariant. Reduced to just
  name + default + the keep / 0=off semantics, with a pointer to the
  Old-version cleanup callout further down the page (where both
  invariants are already documented in full).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant