diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b02a79..28980d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes follow Keep a Changelog and Semantic Versioning. ## [Unreleased] +## [0.8.1] - 2026-09-06 + +### Fixed + +- Check GitHub during each interactive `dgo --version` call instead of treating a cached result as a fresh confirmation. Failed checks explicitly label cached information; redirected version output remains one line without network access. + ## [0.8.0] - 2026-09-04 ### Added diff --git a/Cargo.lock b/Cargo.lock index db43b64..8ac9114 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -412,7 +412,7 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "dirgo" -version = "0.8.0" +version = "0.8.1" dependencies = [ "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index 20d2963..fa236df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dirgo" -version = "0.8.0" +version = "0.8.1" default-run = "dgo" edition = "2024" rust-version = "1.89" diff --git a/README.md b/README.md index f552b59..c36e30c 100644 --- a/README.md +++ b/README.md @@ -226,9 +226,9 @@ dgo library/adobe/cep/extensions ``` `dgo --version` keeps its stable one-line output in pipes. In an interactive -terminal it shows the cached update state immediately and, when due, starts or -observes one detached refresh without waiting for the network. A stale cached -newer release remains visible while Dirgo checks again. +terminal it prints the installed version immediately, then checks GitHub for +the latest stable release. If the request fails, the result is explicitly +unavailable and any last known release is labelled as cached. Disable or restore update notices with `dgo update-notifications off|on`. #### New in 0.8 · Workflow Intelligence @@ -435,7 +435,7 @@ dgo workflows export export private path-redacted JSONL dgo --update install the latest stable release dgo update-notifications off disable new-version notices dgo update-notifications on enable new-version notices -dgo --version show version and interactive cached update state +dgo --version show version and check for updates in a terminal ``` Run `dgo --help` or `dgo --help` for the complete interface. @@ -545,8 +545,8 @@ against your own index. ## Privacy and security - No telemetry, analytics, account, or cloud sync. -- Search, ranking, Palette filtering, and `dgo --version` never wait for the network. -- A successful release response stays fresh for 24 hours; failed checks use short bounded retry delays, and update notifications can be disabled completely. +- Search, ranking, Palette filtering, and redirected `dgo --version` never wait for the network. +- Interactive `dgo --version` checks GitHub on each call. Background notices cache successful responses for 24 hours and use bounded retry delays; update checks can be disabled completely. - Suggestions and command-history collection are independently disabled by default. - Context history stays local, project-scoped, bounded, and inspectable; likely secrets are never stored. - History exports omit filesystem paths by default and never overwrite without `--force`. @@ -565,7 +565,7 @@ Report vulnerabilities privately through [SECURITY.md](SECURITY.md). | Version | Status | User-visible scope | | --- | --- | --- | -| **0.8.0** | Current stable release | Local bounded Workflow Intelligence, `NEXT` suggestions, saved 2–8 step workflows, Palette preview, management CLI, private redacted export, and reliable update scheduling. | +| **0.8.1** | Current stable release | Fresh interactive update checks, plus local bounded Workflow Intelligence, `NEXT` suggestions, saved workflows, and Workspace Palette. | | **0.7.1** | Previous stable release | Workspace Palette, focused roots, ordered path search, bounded lazy previews, safe source switching, cached update awareness, and easier Windows installation. | | **0.6.0** | Previous stable release | Opt-in completed-command context, schema v2 migration, project/success-aware ranking, scoped inspection, clearing, and privacy-preserving export. | diff --git a/docs/architecture/update-state.md b/docs/architecture/update-state.md index 29bac2b..7f87410 100644 --- a/docs/architecture/update-state.md +++ b/docs/architecture/update-state.md @@ -1,10 +1,11 @@ # Update state architecture -Dirgo answers update questions from local state first. An interactive -`dgo --version` never waits for GitHub: it prints the installed version, reads -the last valid release response, tries to start one detached checker when due, -and renders what actually happened. Redirected version output returns before -path discovery and remains exactly one line. +Interactive `dgo --version` prints and flushes the installed version, then +checks GitHub on every call. It bypasses background freshness, leases, and +backoff without waiting for their locks. A successful response is rendered +even if caching fails. Failed requests explicitly label any retained knowledge +as cached and never claim the installed version is current. Redirected output +returns before path discovery and remains exactly one line. ## Independent facts @@ -44,7 +45,7 @@ settings. ## Scheduling and failure behavior -A successful response remains fresh for 24 hours. When it is stale or missing, +For background navigation checks, a response remains fresh for 24 hours. When it is stale or missing, the foreground process claims a five-minute attempt lease and starts the hidden checker with null standard streams. Other simultaneous processes observe the lease and do not create another child. diff --git a/powershell/DirgoPredictor/DirgoPredictor.psd1 b/powershell/DirgoPredictor/DirgoPredictor.psd1 index 20a3eda..015d6fc 100644 --- a/powershell/DirgoPredictor/DirgoPredictor.psd1 +++ b/powershell/DirgoPredictor/DirgoPredictor.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'DirgoPredictor.dll' - ModuleVersion = '0.8.0' + ModuleVersion = '0.8.1' GUID = '531a90d6-192f-46c7-94cb-2c81413eed9b' Author = 'Dirgo contributors' CompanyName = 'Dirgo' diff --git a/scripts/pty-palette-smoke.exp b/scripts/pty-palette-smoke.exp index 005141a..e21a920 100755 --- a/scripts/pty-palette-smoke.exp +++ b/scripts/pty-palette-smoke.exp @@ -64,6 +64,14 @@ set update [open $update_cache w] puts $update "{\"checked_at\":[clock seconds],\"latest_version\":\"9.9.9\"}" close $update unset env(DGO_DISABLE_UPDATE_CHECK) +file mkdir "$sandbox/fake-bin" +set transport [open "$sandbox/fake-bin/curl" w] +puts $transport {#!/bin/sh} +puts $transport {printf '%s\n' '{"tag_name":"v9.9.9"}'} +close $transport +file attributes "$sandbox/fake-bin/curl" -permissions 0755 +set original_path $env(PATH) +set env(PATH) "$sandbox/fake-bin:$env(PATH)" spawn -noecho "$env(DGO_BIN)" --version expect { -exact "dgo $env(DGO_VERSION)" {} @@ -71,7 +79,8 @@ expect { } expect { -exact "Update 9.9.9 available" {} - timeout { puts stderr "interactive version omitted cached update status"; file delete -force $sandbox; exit 1 } + timeout { puts stderr "interactive version omitted fresh update status"; file delete -force $sandbox; exit 1 } + eof { puts stderr "interactive version exited without fresh update status"; file delete -force $sandbox; exit 1 } } expect eof if {[lindex [wait] 3] != 0} { @@ -80,6 +89,7 @@ if {[lindex [wait] 3] != 0} { exit 1 } set env(DGO_DISABLE_UPDATE_CHECK) 1 +set env(PATH) $original_path proc probe_values {} { global env diff --git a/scripts/pty-update-status.exp b/scripts/pty-update-status.exp index fcd7e15..9fda3d8 100755 --- a/scripts/pty-update-status.exp +++ b/scripts/pty-update-status.exp @@ -48,23 +48,23 @@ send -- "\r" expect -exact "UPDATE:ready" expect -exact "UPDATE> " -set timeout 2 +set timeout 6 send -- "\"$env(DGO_BIN)\" --version\r" expect { -exact "dgo $env(DGO_VERSION)" {} timeout { puts stderr "interactive version blocked before rendering"; file delete -force $sandbox; exit 1 } } expect -exact "Update 9.9.9 available" -expect -exact "Cached result · checking again" +expect -exact "Run `dgo --update`" expect { -exact "UPDATE> " {} - timeout { puts stderr "interactive version waited for the background fetch"; file delete -force $sandbox; exit 1 } + timeout { puts stderr "interactive version did not complete its fetch"; file delete -force $sandbox; exit 1 } } -# A second shell command observes the same lease and must not start another child. +# A second explicit request must also check the server, even with a fresh cache. send -- "\"$env(DGO_BIN)\" --version\r" expect -exact "dgo $env(DGO_VERSION)" -expect -exact "Cached result · checking again" +expect -exact "Run `dgo --update`" expect { -exact "UPDATE> " {} timeout { puts stderr "second interactive version blocked"; file delete -force $sandbox; exit 1 } @@ -109,8 +109,8 @@ if {![file exists $env(DGO_UPDATE_TEST_COUNT)]} { set count_file [open $env(DGO_UPDATE_TEST_COUNT) r] set count_lines [split [string trim [read $count_file]] "\n"] close $count_file -if {[llength $count_lines] != 1} { - puts stderr "concurrent version calls started [llength $count_lines] fetches" +if {[llength $count_lines] != 2} { + puts stderr "two explicit version calls started [llength $count_lines] fetches" file delete -force $sandbox exit 1 } @@ -130,4 +130,9 @@ if {[lindex [wait] 3] != 0} { puts "UPDATE-PTY:status-and-scheduling:ok" file delete -force $sandbox +if {[catch {exec expect [file join [file dirname [info script]] pty-version-fresh.exp]} result]} { + puts stderr $result + exit 1 +} +puts $result exit 0 diff --git a/scripts/pty-version-fresh.exp b/scripts/pty-version-fresh.exp new file mode 100644 index 0000000..88ad638 --- /dev/null +++ b/scripts/pty-version-fresh.exp @@ -0,0 +1,29 @@ +#!/usr/bin/expect -f +set timeout 8 +log_user 1 +set sandbox [exec mktemp -d] +file mkdir "$sandbox/bin" "$sandbox/cache/dirgo" +set env(XDG_CACHE_HOME) "$sandbox/cache" +set env(XDG_STATE_HOME) "$sandbox/state" +set env(XDG_CONFIG_HOME) "$sandbox/config" +set env(PATH) "$sandbox/bin:$env(PATH)" +set env(NO_COLOR) 1 +set env(TERM) dumb +unset -nocomplain env(DGO_DISABLE_UPDATE_CHECK) +set f [open "$sandbox/bin/curl" w] +puts $f {#!/bin/sh} +puts $f {printf '%s\n' '{"tag_name":"v9.9.9"}'} +close $f +file attributes "$sandbox/bin/curl" -permissions 0755 +set f [open "$sandbox/cache/dirgo/update.json" w] +puts $f "{\"checked_at\":[clock seconds],\"latest_version\":\"$env(DGO_VERSION)\"}" +close $f +spawn -noecho $env(DGO_BIN) --version +expect { + -exact "Update 9.9.9 available" {} + eof {puts stderr "FAIL: fresh cached current version suppressed live update"; exit 1} + timeout {puts stderr "FAIL: update check did not finish"; exit 1} +} +expect eof +if {[lindex [wait] 3] != 0} {exit 1} +puts "VERSION-FRESH:ok" diff --git a/scripts/release-preflight.sh b/scripts/release-preflight.sh index 84d96f9..9acd91f 100755 --- a/scripts/release-preflight.sh +++ b/scripts/release-preflight.sh @@ -89,7 +89,7 @@ for required_file in \ scripts/render-scoop-manifest.sh scripts/render-context-engine-demo.sh scripts/render-workspace-palette-demo.sh \ scripts/render-workflows-demo.sh scripts/render-workflow-frame.swift scripts/repository-hygiene.sh \ scripts/pty-suggestions-smoke.exp scripts/pty-zsh-live-completion.exp scripts/pty-palette-smoke.exp \ - scripts/pty-update-status.exp \ + scripts/pty-update-status.exp scripts/pty-version-fresh.exp \ scripts/windows-suggestions-smoke.ps1 \ powershell/DirgoPredictor/DirgoPredictor.cs \ powershell/DirgoPredictor/DirgoPredictor.csproj \ diff --git a/src/update.rs b/src/update.rs index 3305d44..761c808 100644 --- a/src/update.rs +++ b/src/update.rs @@ -142,13 +142,86 @@ pub fn print_version() -> Result { let dumb_terminal = env::var("TERM").is_ok_and(|term| term.eq_ignore_ascii_case("dumb")); let color = env::var_os("NO_COLOR").is_none() && !dumb_terminal; let unicode = env::var_os("DGO_NO_UNICODE").is_none() && !dumb_terminal; - let timestamp = now(); - let mut view = local_view_at(&paths, timestamp); - view.refresh = schedule_refresh_at(&paths, timestamp, spawn_background_check); - print!("{}", render_version_status(&view, color, unicode)); + io::stdout() + .flush() + .map_err(|error| DirgoError::io("stdout", error))?; + print!( + "{}", + checked_version_status(&paths, color, unicode, fetch_latest_version) + ); Ok(0) } +fn checked_version_status( + paths: &AppPaths, + color: bool, + unicode: bool, + fetcher: impl FnOnce() -> Result, +) -> String { + let cached = local_view(paths); + if matches!( + cached.refresh, + RefreshDisposition::Disabled | RefreshDisposition::StartFailed + ) { + return render_version_status( + &UpdateView { + relation: VersionRelation::Unknown, + ..cached + }, + color, + unicode, + ); + } + // An explicit version request checks independently of background leases and backoff. + // It never waits for a background process or its state-file lock. + let latest = fetcher().and_then(|text| { + parse_version(&text) + .map(|version| (text, version)) + .ok_or_else(|| DirgoError::User("invalid stable release version".into())) + }); + match latest { + Ok((text, latest)) => { + let timestamp = now(); + if let Err(error) = publish_cache(paths, text, timestamp) { + tracing::debug!(%error, "could not cache explicit update check"); + } + let current = parse_version(env!("CARGO_PKG_VERSION")).expect("package version"); + let relation = match current.cmp(&latest) { + std::cmp::Ordering::Less => VersionRelation::UpdateAvailable { latest }, + std::cmp::Ordering::Equal => VersionRelation::Current { latest }, + std::cmp::Ordering::Greater => VersionRelation::AheadOfLatest { latest }, + }; + render_version_status( + &UpdateView { + relation, + freshness: CacheFreshness::Fresh, + last_success_at: Some(timestamp), + refresh: RefreshDisposition::NotDue, + }, + color, + unicode, + ) + } + Err(error) => { + tracing::debug!(%error, "explicit update check failed"); + let marker = if unicode { "●" } else { "*" }; + let mut status = format!( + "\n{marker} Could not check for updates\n Check your connection and try `dgo --version` again.\n" + ); + match cached.relation { + VersionRelation::UpdateAvailable { latest } => status.push_str(&format!( + " Last known stable: {latest} (cached) · run `dgo --update`\n" + )), + VersionRelation::Current { latest } | VersionRelation::AheadOfLatest { latest } => { + status.push_str(&format!(" Last known stable: {latest} (cached)\n")) + } + VersionRelation::Unknown => {} + } + status + } + } +} + pub fn local_view(paths: &AppPaths) -> UpdateView { local_view_at(paths, now()) } @@ -713,6 +786,41 @@ fn now() -> u64 { mod tests { use super::*; + #[test] + fn explicit_failure_never_claims_cached_current_is_up_to_date() { + let temp = tempfile::tempdir().unwrap(); + let paths = test_paths(&temp); + publish_cache(&paths, env!("CARGO_PKG_VERSION").into(), now()).unwrap(); + let status = checked_version_status(&paths, false, false, || { + Err(DirgoError::User("offline".into())) + }); + assert!(status.contains("Could not check for updates")); + assert!(status.contains("(cached)")); + assert!(!status.contains("up to date")); + } + + #[test] + fn explicit_check_respects_disabled_setting() { + let temp = tempfile::tempdir().unwrap(); + let paths = test_paths(&temp); + paths.ensure_dirs().unwrap(); + fs::write(&paths.update_notice_disabled_file, b"disabled\n").unwrap(); + let status = + checked_version_status(&paths, false, false, || panic!("disabled check fetched")); + assert!(status.contains("Update checks are off")); + } + + #[test] + fn explicit_check_reports_server_result_even_when_cache_cannot_be_written() { + let temp = tempfile::tempdir().unwrap(); + let paths = test_paths(&temp); + paths.ensure_dirs().unwrap(); + fs::create_dir(&paths.update_cache_file).unwrap(); + let status = checked_version_status(&paths, false, false, || Ok("9.9.9".into())); + assert!(status.contains("Update 9.9.9 available")); + assert!(paths.update_cache_file.is_dir()); + } + fn test_paths(temp: &tempfile::TempDir) -> AppPaths { AppPaths { config_file: temp.path().join("config.toml"),