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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,49 @@ If a grant still reads `NOT granted` after granting in the dialog, open **System
(including unset) leaves the gate active.
</Callout>

<Callout type="info">
**Update-available banner (`cua-driver mcp` / `serve` / `doctor`).** The
long-running interactive entry points kick off a non-blocking background
check against the GitHub releases API at startup and print a small
two-line banner to stderr when a newer `cua-driver-rs-v*` release exists:

```
✨ cua-driver v0.1.4 is available (you have v0.1.3).
Update with: cua-driver update
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
hours, so subsequent launches reuse the cached result without a network
call. Network failures are silent — the next launch retries.

**Scripted / machine-readable entry points are NOT instrumented.**
The banner only fires from the long-running entry points (`mcp`,
`serve`, `doctor`). One-shot and machine-readable subcommands —
`--version`, `list-tools`, `describe`, `call`, `dump-docs`,
`mcp-config`, `update`, `stop`, `status`, `recording`, `config`,
`diagnose`, and `telemetry install-event` — all skip it so piped
output (`cua-driver list-tools | jq …`) stays clean.

**Disable per-invocation:**

```bash
CUA_DRIVER_RS_UPDATE_CHECK=false cua-driver serve
```

Accepted "off" values (case-insensitive): `0`, `false`, `no`, `off`.

**Disable permanently** via the persisted config:

```bash
cua-driver config set update_check_enabled false
```

Source / dev builds (any `CARGO_PKG_VERSION` with a pre-release suffix
like `-dev`) auto-skip the check — there is no matching published
release for them to recommend.
</Callout>

## Requirements

- macOS 14 (Sonoma) or later
Expand Down
12 changes: 2 additions & 10 deletions docs/content/docs/cua-driver/reference/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Command Line Interface reference for Cua Driver
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/cua-driver.ts
Source: recursive Swift sources under libs/cua-driver/Sources
Version: 0.1.7
Version: 0.1.9
*/}

import { Callout } from 'fumadocs-ui/components/callout';
Expand All @@ -16,7 +16,7 @@ import { VersionHeader } from '@/components/version-selector';
<VersionHeader
versions={[{"version":"0.1","href":"/cua-driver/reference/cli-reference","isCurrent":true}]}
currentVersion="0.1"
fullVersion="0.1.7"
fullVersion="0.1.9"
packageName="cua-driver"
installCommand="curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh | bash"
/>
Expand Down Expand Up @@ -142,13 +142,6 @@ Subsequent `cua-driver call/list-tools/describe` invocations auto-detect
the socket and forward their requests, so the AppStateEngine's per-pid
element_index cache survives across CLI calls.

On macOS, `serve` runs a **first-launch permissions gate** before binding
the socket. When TCC grants for Accessibility or Screen Recording are
missing, it prints a banner listing exactly what is missing, auto-opens
the matching `System Settings → Privacy & Security` pane(s), and polls
every second until the user grants both. When grants are already active
the gate is a transparent no-op.

**Options:**

| Name | Type | Default | Description |
Expand All @@ -160,7 +153,6 @@ the gate is a transparent no-op.
| Name | Description |
| ---- | ----------- |
| `--no-relaunch` | Stay in the current process instead of re-execing via `open -n -g -a CuaDriver`. |
| `--no-permissions-gate` | Skip the macOS TCC permissions gate at startup. Use for CI / headless runners where blocking on user input would deadlock the process. Also toggleable by setting `CUA_DRIVER_RS_PERMISSIONS_GATE` to any of `0`, `false`, `no`, or `off` (case-insensitive — e.g. `CUA_DRIVER_RS_PERMISSIONS_GATE=FALSE` works too). |

### cua-driver stop

Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/cua-driver/reference/mcp-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Reference for every MCP tool cua-driver exposes
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/cua-driver.ts
Source: recursive Swift sources under libs/cua-driver/Sources
Version: 0.1.7
Version: 0.1.9
*/}

import { Callout } from 'fumadocs-ui/components/callout';
Expand Down
2 changes: 2 additions & 0 deletions libs/cua-driver-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

106 changes: 106 additions & 0 deletions libs/cua-driver-rs/PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -1775,3 +1775,109 @@ surfaced to stdout/stderr unless `CUA_DRIVER_RS_TELEMETRY_DEBUG=true`.
GUI surface yet, so the constant is reserved but unused.
- **`is_ci` uses env-var probing only.** Same probe list as Swift; no
extra Rust-specific signals.

## Startup flow: update-available banner (`mcp` / `serve` / `doctor`)

- Swift: not present (the Swift port has no analogous banner today)
- Rust: `crates/cua-driver/src/version_check.rs`
- Status: INTENTIONAL_ADDITION (Rust-only)
- Test: `crates/cua-driver/src/version_check.rs` `#[cfg(test)] mod tests`
(22 unit tests: semver edge cases, cache round-trip in a
tempdir, dismissal persistence, 20-hour refresh threshold,
env-var + config opt-out, JSON release-list filtering,
banner format, ISO-8601 timestamp)

### Behavior

On the long-running interactive entry points (`mcp`, `serve`,
`doctor`) the binary kicks off a background HTTP check against
`https://api.github.com/repos/trycua/cua/releases?per_page=40`,
filters to the `cua-driver-rs-v*` tag prefix, and prints a two-line
banner to **stderr** if the highest non-draft non-prerelease release
is strictly newer than `CARGO_PKG_VERSION` and the user hasn't
previously dismissed it:

```text
✨ cua-driver v0.1.4 is available (you have v0.1.3).
Update with: cua-driver update
Release notes: https://github.com/trycua/cua/releases/tag/cua-driver-rs-v0.1.4
```

The check runs on `tokio::task::spawn_blocking` when a runtime is
live, else a short-lived OS thread, so the daemon's start-up path
is never delayed by network latency.

### Cache

The latest-version answer is cached on disk at
`~/.cua-driver-rs/version_check.json`:

```json
{
"last_checked_unix": 1700000000,
"last_checked_at": "2023-11-14T22:13:20Z",
"latest_version": "0.1.4",
"dismissed_versions": []
}
```

Refreshed only when the cached `last_checked_unix` is more than 20
hours old, bounding outbound requests to roughly one per machine per
day even on a hot reload loop. Failed HTTP fetches fall back to the
cached value (better an old banner than none on a brief network blip).

### Skipped contexts

Only `mcp`, `serve`, and `doctor` call `maybe_announce_update()`.
The following entry points are **NOT** instrumented because they're
routinely piped from scripts and a banner would corrupt their
parseable output:

- `--version` / `-V`
- `list-tools`
- `describe <tool>`
- `call <tool>`
- `dump-docs`
- `mcp-config`
- `update`, `stop`, `status`, `recording`, `config`, `diagnose`,
`telemetry install-event`

### Opt-out (three layers, any one disables the check)

1. **Env var** `CUA_DRIVER_RS_UPDATE_CHECK=false` (also `0`, `no`,
`off`; case-insensitive) — single-invocation off.
2. **Config flag** `update_check_enabled = false` in
`~/.cua-driver/config.json` — persistent off. Set via
`cua-driver config set update_check_enabled false`.
3. **Pre-release build auto-skip** — any `CARGO_PKG_VERSION` that
carries a semver pre-release suffix (`-dev`, `-rc.1`, `-beta`)
short-circuits the check entirely. There is no matching published
release for a source / development build to recommend.

### HTTP client

`ureq` v3 with `Accept: application/vnd.github+json` and a
`cua-driver-rs/<version>` user-agent. 4-second timeout. Single GET,
fire-and-forget — the response body is read into `serde_json::Value`
on the background task, never touching the foreground startup path.

Network errors, timeouts, 4xx/5xx responses, and JSON-parse failures
are logged via `tracing::debug!(target: "cua_driver::version_check",
…)` only — never surfaced to stderr.

### Shared with `cua-driver update`

`run_update_cmd` calls into the same
`version_check::fetch_latest_version()` and `version_check::is_newer()`
helpers so the proactive banner and the manual subcommand agree on
tag-filtering rules and semver compare semantics. This also removes
the prior shell-out to `curl` from `cli::run_update_cmd`.

### Dismissal API

`version_check::dismiss_version(&str)` appends a version string to
the `dismissed_versions` list on disk. No call site in the current
binary (banner is informational only today); kept public so a future
interactive prompt path (TUI helper, GUI extra) can persist the
"skip until next version" choice without re-implementing the cache
layer.
7 changes: 7 additions & 0 deletions libs/cua-driver-rs/crates/cua-driver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ uuid = { workspace = true }
# PostHog ingest is a single fire-and-forget POST with a 3s timeout. Uses
# rustls (default) so Linux/Windows builds don't require system OpenSSL.
ureq = { version = "3", features = ["json"] }
# Strict semver comparison for the startup update-available banner so that
# pre-release tags (`-dev`, `-rc.1`) sort below their corresponding release
# and we never recommend a pre-release as an "update".
semver = "1"

# Used by crate::bundle::parent_is_not_launchd() for the TCC
# auto-relaunch detection path on Unix (only the macOS heuristic
Expand All @@ -45,6 +49,9 @@ platform-linux = { path = "../platform-linux" }
tokio = { workspace = true, features = ["full"] }
image = { workspace = true }
base64 = { workspace = true }
# Isolated HOME / cache directories for `version_check` unit tests so they
# never touch the developer's real `~/.cua-driver-rs/version_check.json`.
tempfile = "3"

[target.'cfg(target_os = "windows")'.dev-dependencies]
platform-windows = { path = "../platform-windows" }
68 changes: 12 additions & 56 deletions libs/cua-driver-rs/crates/cua-driver/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,23 +796,29 @@ pub fn run_recording_cmd(subcommand: &str, args: &[String], socket: Option<&str>

/// `cua-driver update [--apply]` — check for a newer release and optionally apply it.
///
/// Uses `curl` to query the GitHub releases API (same as Swift reference).
/// Pass `--apply` to download and install via the canonical install.sh.
/// Shares the GitHub releases fetch with the startup banner via
/// [`crate::version_check::fetch_latest_version`] so both code paths
/// agree on tag filtering and HTTP semantics. Pass `--apply` to download
/// and install via the canonical install.sh.
pub fn run_update_cmd(apply: bool) {
let current = env!("CARGO_PKG_VERSION");
println!("Current version: {current}");
println!("Checking for updates…");

let latest = fetch_latest_version();
let latest = crate::version_check::fetch_latest_version();
match latest {
None => {
Err(e) => {
// The shared helper returns a human-readable error string for
// the CLI surface — pass it through so the user can see why
// (timeout, parse error, etc.) instead of just "unreachable".
tracing::debug!(target: "cua_driver::update", "fetch failed: {e}");
println!("Could not reach GitHub — check your connection and try again.");
process::exit(1);
}
Some(v) if !is_version_newer(&v, current) => {
Ok(v) if !crate::version_check::is_newer(&v, current) => {
println!("Already up to date.");
}
Some(v) => {
Ok(v) => {
println!("New version available: {v}");

if !apply {
Expand Down Expand Up @@ -845,56 +851,6 @@ pub fn run_update_cmd(apply: bool) {
}
}

/// Fetch the latest `cua-driver-v*` release tag from GitHub using curl.
/// Returns the version string (e.g. "0.1.2") or `None` on any error.
fn fetch_latest_version() -> Option<String> {
let out = std::process::Command::new("curl")
.args(["-s", "--max-time", "4",
"-H", "Accept: application/vnd.github+json",
"https://api.github.com/repos/trycua/cua/releases?per_page=40"])
.output()
.ok()?;
if !out.status.success() { return None; }
let text = String::from_utf8_lossy(&out.stdout);
let releases: serde_json::Value = serde_json::from_str(&text).ok()?;
// `cua-driver-rs-v*` releases are distinct from the Swift `cua-driver-v*`
// releases on the same repo — must filter by the Rust-port prefix or
// we'd recommend the Swift binary instead.
let prefix = "cua-driver-rs-v";
let mut versions: Vec<String> = releases.as_array()?.iter()
.filter_map(|r| {
let tag = r["tag_name"].as_str()?;
if !tag.starts_with(prefix) { return None; }
if r["draft"].as_bool().unwrap_or(false) { return None; }
if r["prerelease"].as_bool().unwrap_or(false) { return None; }
Some(tag[prefix.len()..].to_owned())
})
.collect();
// Sort descending (newest first).
versions.sort_by(|a, b| compare_versions(b, a));
versions.into_iter().next()
}

/// True when `candidate` is strictly newer than `current` (semver compare).
fn is_version_newer(candidate: &str, current: &str) -> bool {
compare_versions(candidate, current) == std::cmp::Ordering::Greater
}

fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering {
let parts = |s: &str| -> Vec<u64> {
s.split('.').filter_map(|p| p.parse().ok()).collect()
};
let pa = parts(a);
let pb = parts(b);
for (x, y) in pa.iter().zip(pb.iter()) {
match x.cmp(y) {
std::cmp::Ordering::Equal => continue,
other => return other,
}
}
pa.len().cmp(&pb.len())
}

/// `cua-driver dump-docs [--pretty]` — output all MCP tool schemas as JSON.
pub fn run_dump_docs(registry: &ToolRegistry, pretty: bool) {
run_dump_docs_with_type(registry, pretty, "all")
Expand Down
Loading
Loading