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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dirgo"
version = "0.8.0"
version = "0.8.1"
default-run = "dgo"
edition = "2024"
rust-version = "1.89"
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <command> --help` for the complete interface.
Expand Down Expand Up @@ -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`.
Expand All @@ -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. |

Expand Down
13 changes: 7 additions & 6 deletions docs/architecture/update-state.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion powershell/DirgoPredictor/DirgoPredictor.psd1
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
12 changes: 11 additions & 1 deletion scripts/pty-palette-smoke.exp
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,23 @@ 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)" {}
timeout { puts stderr "interactive version omitted the current version"; file delete -force $sandbox; exit 1 }
}
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} {
Expand All @@ -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
Expand Down
19 changes: 12 additions & 7 deletions scripts/pty-update-status.exp
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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
}
Expand All @@ -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
29 changes: 29 additions & 0 deletions scripts/pty-version-fresh.exp
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion scripts/release-preflight.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
116 changes: 112 additions & 4 deletions src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,86 @@ pub fn print_version() -> Result<i32> {
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>,
) -> 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())
}
Expand Down Expand Up @@ -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"),
Expand Down