From d9db3bf3e2bb226d78c412f26f833fc3b83bd309 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 06:06:25 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(cli):=20animate=20anyr=20update=20with?= =?UTF-8?q?=20from=E2=86=92to=20and=20channel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #38. Show a ticking spinner while installing, print current → target version plus the configured channel, then a checkmark success line and "Run anyr to start using the new version." Co-authored-by: Duyet Le Co-authored-by: duyetbot --- .cursor/skills/verify-anyr/features/README.md | 1 + .../verify-anyr/features/update-progress.md | 38 +++ src/help.rs | 7 + src/lib.rs | 1 + src/spinner.rs | 315 ++++++++++++++++++ src/upgrade.rs | 86 +++-- tests/cli.rs | 116 +++++++ 7 files changed, 544 insertions(+), 20 deletions(-) create mode 100644 .cursor/skills/verify-anyr/features/update-progress.md create mode 100644 src/spinner.rs diff --git a/.cursor/skills/verify-anyr/features/README.md b/.cursor/skills/verify-anyr/features/README.md index 7b65ccb..e7f13c8 100644 --- a/.cursor/skills/verify-anyr/features/README.md +++ b/.cursor/skills/verify-anyr/features/README.md @@ -53,3 +53,4 @@ Keep implementation details out of the map. Name only user paths, stable handles - [TUI launcher](./tui-launcher.md) covers `menu --dump-tui`, `config --dump-tui`, and optional PTY quit. - [Accounts and config](./accounts-and-config.md) covers whoami, account switch, config path, and logout against the isolated home. - [Per-agent bindings](./per-agent-bindings.md) covers inline model/account/key binds per coding agent and launch that honors per-agent keys. +- [Update progress](./update-progress.md) covers `anyr update` from→to + channel copy, the success hint, and a TTY spinner that actually ticks. diff --git a/.cursor/skills/verify-anyr/features/update-progress.md b/.cursor/skills/verify-anyr/features/update-progress.md new file mode 100644 index 0000000..4cf077f --- /dev/null +++ b/.cursor/skills/verify-anyr/features/update-progress.md @@ -0,0 +1,38 @@ +# Update progress + +`anyr update` shows a ticking spinner with the current → target version and configured channel, then a checkmark success line and a restart hint. The loader must actually move; a frozen `[loading icon]` is a fail. + +## Sub-features + +- `update-copy` prints `Updating vX -> vY ( channel)` using the running binary version and the selected GitHub channel. +- `update-success` prints `✔` plus the new version and `Run anyr to start using the new version.` +- `update-spinner` on a TTY rewrites the status line with changing spinner frames (`\r` plus distinct glyphs). +- `update-check` keeps the labeled current/latest/channel report and does not install. + +## How to get to it (user POV) + +- Run `anyr update` to install the latest release for the configured channel. +- Run `anyr update --beta` or `anyr update --stable` to persist a channel and update it. +- Run `anyr upgrade` (alias) the same way. +- Run `anyr update --check` to compare versions without installing. + +## Driving it with control-anyr + +Preconditions: + +- `control-anyr doctor` is clean. +- Isolated `ANYROUTER_HOME` is in use. +- Set `ANYR_RELEASES_JSON` to `control-anyr path RELEASES_FIXTURE` so the run stays offline. +- Do not hit live GitHub Releases. + +- **Non-TTY copy.** Export `ANYR_RELEASES_JSON` to the fixture and `ANYR_CHANNEL=beta`. Run `control-anyr cli --out artifacts/update-progress/update.txt -- update`. Exit code `0`. Stdout contains `Updating`, `->`, `beta channel`, a fixture target version (`0.2.0-beta.1`), `✔`, and `Run anyr to start using the new version.` Stdout does not invent model ids. +- **Check still reports.** Run `control-anyr cli --out artifacts/update-progress/check.txt -- update --check`. Exit code `0`. Stdout contains `current:`, `latest:`, `channel:`, and either `update available` or `up to date`. +- **TTY spinner ticks.** Export `ANYR_RELEASES_JSON`, `ANYR_CHANNEL=beta`, `ANYR_SPINNER_MS=20`, and `ANYR_SPINNER_MIN_TICKS=6`. Run `control-anyr pty run --timeout 6 --out artifacts/update-progress/tty.txt -- update`. The transcript contains at least two distinct spinner glyphs from `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`, a carriage return, `Updating`, `beta channel`, and `Run anyr to start using the new version.` +- **Proof.** Keep `update.txt`, `update.txt.exit`, and `tty.txt`. Together they show from→to + channel copy and that the loader frames change. + +## Gotchas + +- Fixture mode does not replace the binary (`ANYR_RELEASES_JSON` is treated as dry-run). Success copy is `Would update to` instead of `Updated to`. +- `control-anyr cli` is not a TTY, so it will not animate. Use `pty run` for frame-change proof. +- `ANYR_NO_UPDATE=1` only skips background auto-update. Explicit `anyr update` still runs. +- Existing coverage lives in `tests/cli.rs` (`update_prints_from_to_channel_and_success`, `update_spinner_animates_on_tty`) and `src/spinner.rs`. Run those as a complement, not as the only user-path proof. diff --git a/src/help.rs b/src/help.rs index b585d5e..2c3bb3c 100644 --- a/src/help.rs +++ b/src/help.rs @@ -420,6 +420,13 @@ Channels: Downloads: https://github.com/anyrouter-dev/cli/releases/download//anyr-- +While installing, a spinner ticks with the from → to versions and channel: + + ⠋ Updating v0.1.11 -> v0.1.99 (stable channel) + ✔ Updated to v0.1.99 + + Run anyr to start using the new version. + --check reports current vs latest without installing. --fixture / ANYR_RELEASES_JSON skips the network (tests / dry-run). --channel stable|beta overrides the config file for this run only. diff --git a/src/lib.rs b/src/lib.rs index c953af0..5596a6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ pub mod parse; #[cfg(feature = "native")] pub mod relay; pub mod spawn; +pub mod spinner; pub mod term; #[cfg(feature = "native")] pub mod tui; diff --git a/src/spinner.rs b/src/spinner.rs new file mode 100644 index 0000000..82dc216 --- /dev/null +++ b/src/spinner.rs @@ -0,0 +1,315 @@ +//! In-place CLI spinner. Frames actually advance on a timer so a TTY never +//! shows a frozen loading glyph. Non-TTY prints a static status line instead. + +use std::io::{self, IsTerminal, Write}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::thread::{self, JoinHandle}; +use std::time::{Duration, Instant}; + +use crate::term::{self, BLUE, SUCCESS}; + +/// Braille spinner frames. Consecutive indices are visually distinct. +pub const FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + +pub const START_USING: &str = "Run anyr to start using the new version."; + +const DEFAULT_INTERVAL_MS: u64 = 80; +const DEFAULT_MIN_TICKS: usize = 4; + +pub fn frame(index: usize) -> &'static str { + FRAMES[index % FRAMES.len()] +} + +/// One spinner line (`glyph message`). Tests use this to prove ticks change. +pub fn render(index: usize, message: &str) -> String { + format!("{} {message}", frame(index)) +} + +/// Glyphs from [`FRAMES`] that appear in a captured transcript (including `\r` history). +pub fn frames_in(output: &str) -> Vec<&'static str> { + FRAMES + .iter() + .copied() + .filter(|g| output.contains(g)) + .collect() +} + +fn interval_from_env() -> Duration { + let ms = std::env::var("ANYR_SPINNER_MS") + .ok() + .and_then(|s| s.parse::().ok()) + .filter(|n| *n > 0) + .unwrap_or(DEFAULT_INTERVAL_MS) + .min(1_000); + Duration::from_millis(ms) +} + +fn min_ticks_from_env() -> usize { + std::env::var("ANYR_SPINNER_MIN_TICKS") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(DEFAULT_MIN_TICKS) +} + +fn lock_write(out: &Mutex>, bytes: &[u8]) { + let mut guard = out.lock().unwrap_or_else(|e| e.into_inner()); + let _ = guard.write_all(bytes); + let _ = guard.flush(); +} + +fn paint_glyph(glyph: &str) -> String { + term::paint(BLUE, glyph) +} + +fn paint_ok_mark() -> String { + term::paint(SUCCESS, "✔") +} + +/// Live spinner on a TTY; a single status line otherwise. +pub struct Spinner { + stop: Arc, + ticks: Arc, + handle: Option>, + out: Arc>>, + tty: bool, + interval: Duration, + min_ticks: usize, + finished: bool, +} + +impl Spinner { + pub fn start(message: impl Into) -> Self { + Self::start_on( + io::stdout(), + io::stdout().is_terminal(), + message, + interval_from_env(), + min_ticks_from_env(), + ) + } + + pub fn start_on( + writer: W, + tty: bool, + message: impl Into, + interval: Duration, + min_ticks: usize, + ) -> Self { + let message = message.into(); + let out: Arc>> = Arc::new(Mutex::new(Box::new(writer))); + let stop = Arc::new(AtomicBool::new(false)); + let ticks = Arc::new(AtomicUsize::new(0)); + let handle = if tty { + #[cfg(not(target_arch = "wasm32"))] + { + let out_t = Arc::clone(&out); + let stop_t = Arc::clone(&stop); + let ticks_t = Arc::clone(&ticks); + let msg = message.clone(); + let interval = if interval.is_zero() { + Duration::from_millis(DEFAULT_INTERVAL_MS) + } else { + interval + }; + Some(thread::spawn(move || { + tick_loop(out_t, stop_t, ticks_t, msg, interval); + })) + } + #[cfg(target_arch = "wasm32")] + { + let _ = ( + Arc::clone(&out), + Arc::clone(&stop), + Arc::clone(&ticks), + interval, + ); + lock_write(&out, format!("{message}\n").as_bytes()); + None + } + } else { + lock_write(&out, format!("{message}\n").as_bytes()); + None + }; + Self { + stop, + ticks, + handle, + out, + tty, + interval, + min_ticks, + finished: false, + } + } + + pub fn tick_count(&self) -> usize { + self.ticks.load(Ordering::Relaxed) + } + + fn wait_min_ticks(&self) { + if !self.tty || self.min_ticks == 0 { + return; + } + let cap = self.interval.saturating_mul(self.min_ticks as u32) + Duration::from_millis(250); + let deadline = Instant::now() + cap; + while self.ticks.load(Ordering::Relaxed) < self.min_ticks { + if Instant::now() >= deadline { + break; + } + thread::sleep(Duration::from_millis(5)); + } + } + + fn stop_thread(&mut self) { + self.stop.store(true, Ordering::SeqCst); + if let Some(handle) = self.handle.take() { + let _ = handle.join(); + } + } + + fn write_line(&self, line: &str) { + lock_write(&self.out, line.as_bytes()); + } + + /// Replace the spinner with a checkmark success line and the restart hint. + pub fn succeed(mut self, message: &str) { + self.wait_min_ticks(); + self.stop_thread(); + self.finished = true; + let mark = paint_ok_mark(); + let body = if self.tty { + format!("\r{mark} {message}\x1b[K\n") + } else { + format!("{mark} {message}\n") + }; + self.write_line(&body); + self.write_line("\n"); + self.write_line(&format!("{}\n", term::dim(START_USING))); + } + + /// Clear the spinner line so a following error can print cleanly. + pub fn fail(mut self) { + self.stop_thread(); + self.finished = true; + if self.tty { + self.write_line("\r\x1b[K"); + } + } +} + +impl Drop for Spinner { + fn drop(&mut self) { + self.stop.store(true, Ordering::SeqCst); + if let Some(handle) = self.handle.take() { + let _ = handle.join(); + } + if self.tty && !self.finished { + lock_write(&self.out, b"\r\x1b[K"); + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +fn tick_loop( + out: Arc>>, + stop: Arc, + ticks: Arc, + message: String, + interval: Duration, +) { + let mut index = 0usize; + while !stop.load(Ordering::Relaxed) { + let glyph = paint_glyph(frame(index)); + let line = format!("\r{glyph} {message}\x1b[K"); + lock_write(&out, line.as_bytes()); + ticks.fetch_add(1, Ordering::Relaxed); + index = index.wrapping_add(1); + let until = Instant::now() + interval; + while Instant::now() < until && !stop.load(Ordering::Relaxed) { + thread::sleep(Duration::from_millis(5).min(interval)); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct SharedBuf(Arc>>); + + impl Write for SharedBuf { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.lock().unwrap().extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + #[test] + fn consecutive_frames_are_distinct() { + for i in 0..FRAMES.len() { + assert_ne!( + frame(i), + frame(i + 1), + "frame {i} must differ from the next tick" + ); + assert_ne!(render(i, "Updating"), render(i + 1, "Updating")); + } + assert_eq!(FRAMES.len(), 10); + } + + #[test] + fn live_spinner_writes_multiple_changing_frames() { + let buf = Arc::new(Mutex::new(Vec::new())); + let spinner = Spinner::start_on( + SharedBuf(Arc::clone(&buf)), + true, + "Updating v0.1.11 -> v0.1.99 (stable channel)", + Duration::from_millis(15), + 5, + ); + spinner.succeed("Updated to v0.1.99"); + let bytes = buf.lock().unwrap().clone(); + let text = String::from_utf8_lossy(&bytes); + let seen = frames_in(&text); + assert!( + seen.len() >= 2, + "spinner must tick distinct frames, got {seen:?} in:\n{text:?}" + ); + assert!( + text.contains('\r'), + "in-place animation uses CR, got {text:?}" + ); + assert!(text.contains("✔"), "{text}"); + assert!(text.contains("Updated to v0.1.99"), "{text}"); + assert!(text.contains(START_USING), "{text}"); + } + + #[test] + fn non_tty_prints_status_without_frozen_glyph() { + let buf = Arc::new(Mutex::new(Vec::new())); + let spinner = Spinner::start_on( + SharedBuf(Arc::clone(&buf)), + false, + "Updating v0.1.11 -> v0.1.99 (beta channel)", + Duration::from_millis(15), + 8, + ); + assert_eq!(spinner.tick_count(), 0); + spinner.succeed("Would update to v0.1.99"); + let text = String::from_utf8_lossy(&buf.lock().unwrap()).into_owned(); + assert!( + text.contains("Updating v0.1.11 -> v0.1.99 (beta channel)"), + "{text}" + ); + assert!( + frames_in(&text).is_empty(), + "non-TTY must not print a frozen spinner glyph, got {text:?}" + ); + assert!(text.contains("✔ Would update to v0.1.99"), "{text}"); + assert!(text.contains(START_USING), "{text}"); + } +} diff --git a/src/upgrade.rs b/src/upgrade.rs index f9bb2e0..23c3e5a 100644 --- a/src/upgrade.rs +++ b/src/upgrade.rs @@ -594,13 +594,8 @@ pub fn run(parsed: &ParsedArgs, env: &BTreeMap) -> Result) -> Result { + spinner.succeed(&updated_line(latest_ver)); + Ok(0) + } + Err(err) => { + spinner.fail(); + Err(err) + } + } } fn field(key: &str, value: impl std::fmt::Display) -> String { format!("{:<8} {value}", format!("{key}:")) } +/// GitHub-tag style (`v0.1.11`). Never invents a version number. +fn display_tag(ver: &str) -> String { + let v = ver.trim(); + if v.starts_with('v') || v.starts_with('V') { + v.to_string() + } else { + format!("v{v}") + } +} + +fn updating_line(from: &str, to: &str, channel: Channel) -> String { + format!( + "Updating {} -> {} ({} channel)", + display_tag(from), + display_tag(to), + channel.as_str() + ) +} + +fn updated_line(to: &str) -> String { + format!("Updated to {}", display_tag(to)) +} + +fn would_update_line(to: &str) -> String { + format!("Would update to {}", display_tag(to)) +} + fn print_version_report(channel: Channel, latest_ver: &str) { println!( "{}", @@ -882,6 +914,20 @@ mod tests { ); } + #[test] + fn updating_line_shows_from_to_and_channel() { + let line = updating_line("0.1.11", "0.1.99", Channel::Stable); + assert_eq!(line, "Updating v0.1.11 -> v0.1.99 (stable channel)"); + let beta = updating_line("v0.1.11", "0.2.0-beta.1", Channel::Beta); + assert_eq!(beta, "Updating v0.1.11 -> v0.2.0-beta.1 (beta channel)"); + assert_eq!(updated_line("0.1.99"), "Updated to v0.1.99"); + assert_eq!( + would_update_line("0.2.0-beta.1"), + "Would update to v0.2.0-beta.1" + ); + assert_eq!(display_tag("v0.1.11"), "v0.1.11"); + } + #[test] fn download_404_names_release_and_suggests_beta() { let url = diff --git a/tests/cli.rs b/tests/cli.rs index ee6b9d7..ab340b8 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -764,6 +764,10 @@ fn upgrade_help_mentions_channel_stable_beta() { stdout.contains("Auto-update") || stdout.contains("auto-update"), "upgrade help should mention auto-update, got:\n{stdout}" ); + assert!( + stdout.contains("Run anyr to start using the new version."), + "upgrade help should show the post-update hint, got:\n{stdout}" + ); } #[test] @@ -1293,6 +1297,118 @@ fn upgrade_auto_is_quiet_when_up_to_date() { ); } +#[test] +fn update_prints_from_to_channel_and_success() { + let home = std::env::temp_dir().join(format!( + "anyr-cli-update-ux-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&home).expect("home"); + let out = anyr() + .args(["update"]) + .env("ANYROUTER_HOME", &home) + .env("ANYR_RELEASES_JSON", fixture_path()) + .env("ANYR_CHANNEL", "beta") + .output() + .expect("update"); + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + assert_eq!(out.status.code().unwrap_or(1), 0, "stderr={stderr}"); + assert!( + stdout.contains("Updating") && stdout.contains("->") && stdout.contains("beta channel"), + "expected from→to + channel, got:\n{stdout}" + ); + assert!(stdout.contains("0.2.0-beta.1"), "{stdout}"); + assert!( + stdout.contains("✔") && stdout.contains("Would update to v0.2.0-beta.1"), + "{stdout}" + ); + assert!( + stdout.contains("Run anyr to start using the new version."), + "{stdout}" + ); + let _ = std::fs::remove_dir_all(&home); +} + +#[cfg(unix)] +#[test] +fn update_spinner_animates_on_tty() { + let home = std::env::temp_dir().join(format!( + "anyr-cli-update-pty-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&home).expect("home"); + let helper = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join(".cursor/skills/verify-anyr/helpers/pty-anyr.py"); + let transcript = home.join("tty.txt"); + let out = Command::new("python3") + .args([ + helper.to_str().unwrap(), + "--timeout", + "6", + "--out", + transcript.to_str().unwrap(), + "--", + env!("CARGO_BIN_EXE_anyr"), + "update", + ]) + .env("ANYR_NO_UPDATE", "1") + .env("ANYR_NO_CATALOG", "1") + .env("ANYROUTER_HOME", &home) + .env("ANYR_RELEASES_JSON", fixture_path()) + .env("ANYR_CHANNEL", "beta") + .env("ANYR_SPINNER_MS", "20") + .env("ANYR_SPINNER_MIN_TICKS", "6") + .env_remove("NO_COLOR") + .output() + .expect("pty update"); + let captured = std::fs::read_to_string(&transcript) + .unwrap_or_else(|_| String::from_utf8_lossy(&out.stdout).into_owned()); + let combined = format!( + "{}{}{}", + captured, + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + assert_eq!( + out.status.code().unwrap_or(1), + 0, + "pty update failed:\n{combined}" + ); + assert!( + combined.contains("Updating") && combined.contains("beta channel"), + "{combined}" + ); + assert!(combined.contains("->"), "{combined}"); + assert!( + combined.contains("Would update to") || combined.contains("Updated to"), + "{combined}" + ); + assert!( + combined.contains("Run anyr to start using the new version."), + "{combined}" + ); + let frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + let seen: Vec<_> = frames.iter().filter(|g| combined.contains(**g)).collect(); + assert!( + seen.len() >= 2, + "TTY spinner must tick distinct frames, got {seen:?} in:\n{combined:?}" + ); + assert!( + combined.contains('\r'), + "animation rewrites the line with CR, got {combined:?}" + ); + let _ = std::fs::remove_dir_all(&home); +} + #[test] fn menu_dump_tui_prints_plain_frame() { let dir = std::env::temp_dir().join(format!("anyr-cli-menu-dump-{}", std::process::id())); From f3535f68bd6b66a6801d2434050d8d3fb4f6d0bf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 06:08:52 +0000 Subject: [PATCH 2/2] test(cli): capture anyr update spinner TTY proof MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record isolated control-anyr artifacts for #38: from→to + channel copy, success hint, and a PTY transcript with changing spinner frames. Co-authored-by: Duyet Le Co-authored-by: duyetbot --- .../artifacts/update-progress/PROOF.md | 41 +++++++++++++++++++ .../artifacts/update-progress/check.txt | 6 +++ .../artifacts/update-progress/check.txt.err | 0 .../artifacts/update-progress/check.txt.exit | 1 + .../artifacts/update-progress/doctor.txt | 10 +++++ .../artifacts/update-progress/frames.txt | 8 ++++ .../artifacts/update-progress/tty.txt | 3 ++ .../artifacts/update-progress/update.txt | 4 ++ .../artifacts/update-progress/update.txt.err | 0 .../artifacts/update-progress/update.txt.exit | 1 + 10 files changed, 74 insertions(+) create mode 100644 .cursor/skills/verify-anyr/artifacts/update-progress/PROOF.md create mode 100644 .cursor/skills/verify-anyr/artifacts/update-progress/check.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/update-progress/check.txt.err create mode 100644 .cursor/skills/verify-anyr/artifacts/update-progress/check.txt.exit create mode 100644 .cursor/skills/verify-anyr/artifacts/update-progress/doctor.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/update-progress/frames.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/update-progress/tty.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/update-progress/update.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/update-progress/update.txt.err create mode 100644 .cursor/skills/verify-anyr/artifacts/update-progress/update.txt.exit diff --git a/.cursor/skills/verify-anyr/artifacts/update-progress/PROOF.md b/.cursor/skills/verify-anyr/artifacts/update-progress/PROOF.md new file mode 100644 index 0000000..1d3d4d0 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/update-progress/PROOF.md @@ -0,0 +1,41 @@ +# Proof: update-progress + +Feature: `update-progress` +Entry points driven: `anyr update` (fixture, non-TTY), `anyr update --check`, `anyr update` on a PTY +Harness: `.cursor/skills/verify-anyr/control-anyr` (`launch`, `doctor`, `cli`, `pty run`, `test`, `cleanup`) +Binary: `target/debug/anyr` from this checkout (`0.1.11`) +Isolated home: `/tmp/anyr-verify-20260903-060715-6103/home` (removed by cleanup) +Releases: `tests/fixtures/releases.json` via `ANYR_RELEASES_JSON` (offline) + +## Commands + +```bash +.cursor/skills/verify-anyr/control-anyr launch +.cursor/skills/verify-anyr/control-anyr doctor +ANYR_RELEASES_JSON="$(.cursor/skills/verify-anyr/control-anyr path RELEASES_FIXTURE)" +ANYR_CHANNEL=beta +.cursor/skills/verify-anyr/control-anyr cli --out artifacts/update-progress/update.txt -- update +.cursor/skills/verify-anyr/control-anyr cli --out artifacts/update-progress/check.txt -- update --check +ANYR_SPINNER_MS=20 ANYR_SPINNER_MIN_TICKS=6 \ + .cursor/skills/verify-anyr/control-anyr pty run --timeout 6 --out artifacts/update-progress/tty.txt -- update +.cursor/skills/verify-anyr/control-anyr test -- --test cli update_prints_from_to_channel_and_success +.cursor/skills/verify-anyr/control-anyr test -- --test cli update_spinner_animates_on_tty +.cursor/skills/verify-anyr/control-anyr cleanup +``` + +## Results + +| Artifact | Exit | Observable | +| --- | --- | --- | +| `update.txt` | 0 | `Updating v0.1.11 -> v0.2.0-beta.1 (beta channel)`, `✔ Would update to v0.2.0-beta.1`, `Run anyr to start using the new version.` Fixture mode does not replace the binary. | +| `check.txt` | 0 | labeled `current:` / `latest:` / `channel: beta` and `update available` | +| `tty.txt` | 0 | PTY transcript: 10 carriage returns, 6 distinct spinner glyphs (`⠋⠙⠹⠸⠼⠴`), from→to, `beta channel`, success checkmark, restart hint | +| `frames.txt` | — | binary counts for `tty.txt` (animation proof) | + +Complementary suite: `update_prints_from_to_channel_and_success` and `update_spinner_animates_on_tty` passed. Lib tests `spinner::tests::live_spinner_writes_multiple_changing_frames` and `upgrade::tests::updating_line_shows_from_to_and_channel` passed. + +No `anyr claude` without `--dry-run`. No paid tokens. No live GitHub Releases. + +## Cleanup + +`control-anyr cleanup` removed the isolated workdir and left this `artifacts/update-progress/` tree in place. diff --git a/.cursor/skills/verify-anyr/artifacts/update-progress/check.txt b/.cursor/skills/verify-anyr/artifacts/update-progress/check.txt new file mode 100644 index 0000000..ef4c895 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/update-progress/check.txt @@ -0,0 +1,6 @@ +current: 0.1.11 (built 2026-09-03 06:05:55) +latest: 0.2.0-beta.1 +channel: beta +asset: https://github.com/anyrouter-dev/cli/releases/download/v0.2.0-beta.1/anyr-linux-x86_64 +status: update available 0.1.11 -> 0.2.0-beta.1 +run: anyr update diff --git a/.cursor/skills/verify-anyr/artifacts/update-progress/check.txt.err b/.cursor/skills/verify-anyr/artifacts/update-progress/check.txt.err new file mode 100644 index 0000000..e69de29 diff --git a/.cursor/skills/verify-anyr/artifacts/update-progress/check.txt.exit b/.cursor/skills/verify-anyr/artifacts/update-progress/check.txt.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/update-progress/check.txt.exit @@ -0,0 +1 @@ +0 diff --git a/.cursor/skills/verify-anyr/artifacts/update-progress/doctor.txt b/.cursor/skills/verify-anyr/artifacts/update-progress/doctor.txt new file mode 100644 index 0000000..cfb99af --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/update-progress/doctor.txt @@ -0,0 +1,10 @@ +ok binary /workspace/target/debug/anyr +ok version 0.1.11 (built 2026-09-03 06:05:55) +ok help anyr --help names CORE COMMANDS / LAUNCH +ok isolated ANYROUTER_HOME=/tmp/anyr-verify-20260903-060715-6103/home +ok config /tmp/anyr-verify-20260903-060715-6103/home/config.yaml +ok config_path config path=/tmp/anyr-verify-20260903-060715-6103/home/config.yaml +ok whoami whoami shows masked default profile +ok update_guard ANYR_NO_UPDATE=1 ANYR_NO_CATALOG=1 +ok pty stopped +ok paid_guard do not run anyr claude/codex/… without --dry-run diff --git a/.cursor/skills/verify-anyr/artifacts/update-progress/frames.txt b/.cursor/skills/verify-anyr/artifacts/update-progress/frames.txt new file mode 100644 index 0000000..df55073 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/update-progress/frames.txt @@ -0,0 +1,8 @@ +bytes=419 +cr_count=10 +distinct_frames=6 +frames=⠋⠙⠹⠸⠼⠴ +has_updating=True +has_beta_channel=True +has_from_to=True +has_hint=True diff --git a/.cursor/skills/verify-anyr/artifacts/update-progress/tty.txt b/.cursor/skills/verify-anyr/artifacts/update-progress/tty.txt new file mode 100644 index 0000000..e501425 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/update-progress/tty.txt @@ -0,0 +1,3 @@ + ⠋ Updating v0.1.11 -> v0.2.0-beta.1 (beta channel) ⠙ Updating v0.1.11 -> v0.2.0-beta.1 (beta channel) ⠹ Updating v0.1.11 -> v0.2.0-beta.1 (beta channel) ⠸ Updating v0.1.11 -> v0.2.0-beta.1 (beta channel) ⠼ Updating v0.1.11 -> v0.2.0-beta.1 (beta channel) ⠴ Updating v0.1.11 -> v0.2.0-beta.1 (beta channel) ✔ Would update to v0.2.0-beta.1 + +Run anyr to start using the new version. diff --git a/.cursor/skills/verify-anyr/artifacts/update-progress/update.txt b/.cursor/skills/verify-anyr/artifacts/update-progress/update.txt new file mode 100644 index 0000000..ab9615b --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/update-progress/update.txt @@ -0,0 +1,4 @@ +Updating v0.1.11 -> v0.2.0-beta.1 (beta channel) +✔ Would update to v0.2.0-beta.1 + +Run anyr to start using the new version. diff --git a/.cursor/skills/verify-anyr/artifacts/update-progress/update.txt.err b/.cursor/skills/verify-anyr/artifacts/update-progress/update.txt.err new file mode 100644 index 0000000..e69de29 diff --git a/.cursor/skills/verify-anyr/artifacts/update-progress/update.txt.exit b/.cursor/skills/verify-anyr/artifacts/update-progress/update.txt.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/update-progress/update.txt.exit @@ -0,0 +1 @@ +0