feat: detect rust/cargo tool versions, add brew-by-name update fallback - #41
Merged
Conversation
Plan for Variant A: automatic package-manager fallbacks symmetric to brewDirVersion — cargo-list installed-version detection + isTUITakeover guard in internal/version, brew-by-name update fallback in internal/updater, plus a present/not-installed split in the "installed:" card line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three problems, all around tools whose binary won't name its own version.
`rust` had no updater: it ships rustc/cargo, so exec.LookPath("rust") misses
and Detect returned ErrUnknownManager — the "no known updater" dead end. On a
LookPath miss Detect now looks for a Cellar/<name> or Caskroom/<name>
directory and plans `brew upgrade <name>`. Self-validating: the branch fires
only when such a keg exists, and it shares brewDirVersion's traversal guard.
Rust TUIs showed "not found" and wrote an anomaly log every startup. A ratatui
app ignores --version, boots its TUI and panics with no terminal; the capture
carried the alt-screen signature plus "ratatui-0.30.2", which versionRe would
have mis-parsed as the tool's version once the non-zero exit stopped masking
it. InstalledVersion now drops such captures (isTUITakeover — its own copy,
version sits below model in the import graph) and falls back to
`cargo install --list`, which knows the version without running the binary.
The "installed:" line collapsed "installed, version unknown" into "not found".
InstalledVersion returns (ver string, present bool) and the card splits the
two: `✓ no version` in green ui.OkStyle vs `✕ not installed` in DangerStyle.
Fallback order is --version/-V -> brew directory -> cargo list, with the cargo
probe gated on the binary existing. Tests are mutation-checked: removing the
fallback, swapping brew/cargo order, dropping the presence gate and dropping
the present flag each turn a test red.
Docs synced (CLAUDE.md, ARCHITECTURE.md, README.md). Plan moved to
docs/plans/completed/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes three problems with tools whose binary won't report its own version. Implements the plan in
docs/plans/completed/20260724-rust-version-update-detect.md(Variant A: automatic fallbacks, no manual config).What changed
1.
rusthad no updater. It shipsrustc/cargo, soexec.LookPath("rust")misses andDetectreturnedErrUnknownManager— the "no known updater" dead end. On a LookPath missDetectnow checks for aCellar/<name>orCaskroom/<name>directory and plansbrew upgrade <name>. The branch is self-validating (fires only when such a keg exists) and sharesversion.brewDirVersion's traversal guard.2. Rust TUIs read as
✕ not foundand logged an anomaly on every startup. A ratatui app ignores--version, boots its TUI and panics with no terminal. The capture carried the alt-screen signature plusratatui-0.30.2— whichversionRewould have mis-parsed as the tool's version once the non-zero exit stopped masking it.InstalledVersionnow drops such captures (isTUITakeover, its own copy sinceversionsits belowmodelin the import graph) and falls back tocargo install --list, which knows the version without running the binary at all.3.
installed:collapsed two different states into✕ not found.InstalledVersionnow returns(ver string, present bool)and the card renders them apart:✓ no version(greenui.OkStyle) for a working install that won't name a version,✕ not installed(redDangerStyle) for a genuine absence.Source order is
--version/-V→ brew directory → cargo list, with the cargo probe gated on the binary existing (an absent tool can only miss, and the probe costs a subprocess).Verification
Against a real tracker of 23 tools:
inertia✕ not found+ a log line every startup0.1.0via cargo list, no logrust[u]→ "no known updater"1.96.1via brew dir,[u]→brew upgrade rustkeeptui✕ not found✓ no versionEvery new test was mutation-checked — confirmed to fail when the path it covers is broken:
TestInstalledVersionCargoFallbackredTestInstalledVersionBrewBeatsCargoredbinaryExistsgate →TestInstalledVersionCargoGatedOnPresencered (3/3 asserts)info.InstalledPresent = msg.present→TestInstalledMsgCarriesPresenceredpresentinfetchInstalledCmd→TestFetchInstalledCmdredTwo of those tests were themselves broken on the first pass and only the mutation check caught it: a marker written with
touchnever resolved (PATH is the fixture dir alone), and an assert onpresent == falsewas vacuous sincefalseis the zero value.preflight green:
go build/go vet/go test -race ./.../golangci-lint run(0 issues).Known limitations, deliberately not addressed
keeptuiitself is tracked and answerskeeptui dev— unparseable, and a legitimate anomaly by the log's own rule. Pre-existing, unrelated to this branch.version :latestwith no PATH binary still reads✕ not installed:brewDirVersioncan't tell "no keg" from "keg with no parseable version". 0 of 15 casks on the test machine are in that state...and.pass the formula-name traversal guard. Harmless —Statonly, no writes, and brew rejects the command — and mirrored fromversion.brewDirVersion, so it should be fixed in both or neither.↑marker) depends onrust-lang/rustpublishing GitHub Releases. The dead end is gone either way; the data source is out of scope.🤖 Generated with Claude Code