Repository navigation
fix: refresh stale Homebrew cache and split cask/manual app sources - #43
Merged
Merged
Conversation
Formulae and casks were silently reported as up-to-date when Homebrew's
local API cache was stale. `brew info` (the only Homebrew command this
scanner runs) never triggers Homebrew's auto-update, so `versions.stable`
lagged the real latest between refreshes and outdated packages went
unlisted. Refresh the cache with `brew update --quiet` (metadata only —
never `upgrade`, installed packages untouched) when it is older than a
threshold before reading it.
Also reconcile an app discovered from two sources — a scanned .app bundle
and its Homebrew cask. They are separate update channels (e.g. a Sparkle
self-updater vs the cask) whose versions can legitimately differ, so both
surface: the scanned app stays the parent and the cask is attached as a
sub-app (one level deep). When the cask installs the same bundle the
on-disk version is authoritative — brew's receipt lags for casks that
auto-update themselves — so the sub-app reports the bundle's version and
is not falsely flagged outdated. Parents roll up sub-app updates so a
cask-channel update still shows in the default outdated-only view.
Details:
- Version-robust staleness check globs *.jws.json (handles the brew 6.x
internal/packages layout) and honours HOMEBREW_CACHE; window is
configurable via APPS_BREW_CACHE_MAX_AGE_HOURS (default 6h).
- Reusable cask/app matcher (artifact target path, *.app name, or bundle
id; case-insensitive) drives same-bundle detection and cross-source
de-duplication when display names differ.
- Correct a misleading "already updated to v{installed}" debug log.
- Bump VERSION to 0.0.13-beta.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A same-named app and extension — e.g. the "Claude" desktop app and the "Claude" Chrome extension — were collapsed into one parent + sub-app entry because ScanOrchestrator de-duplicated on name alone, nesting an Application under an Extension and hiding the desktop app's own update status. Key the cross-scanner de-duplication on name AND kind so they stay distinct top-level entries and are each checked on their own; genuine same-kind duplicates still merge into sub-apps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Some VS Code extensions ship an NLS placeholder such as `%displayName%` as their package.json displayName (a key into package.nls.json the manifest never inlines). The scanner used it verbatim, so several extensions all showed up named "%displayName%" and collided. Reject the placeholder so the extension id (e.g. github.vscode-pull-request-github) is used as the name instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keying on name (and then name+kind) alone still merged same-named items from different scanners — the "EditorConfig"/"PowerShell" JetBrains plugins vs their VS Code extensions — nesting unrelated software. Include the source scanner in the de-duplication key so only genuine duplicates from the same scanner collapse. The manual-app + Homebrew-cask pairing is unaffected: it is reconciled inside MacApplicationsScanner and reaches the orchestrator as a single record. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apps whose update status could not be determined were shown dimmed with a leading "?" on the name. Flag the row red instead — mirroring how outdated rows are flagged green — by colouring the name and version red and dropping the secondary dimming on the kind and source columns. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The discovered total counted only top-level records while the checked total also counts sub-apps (a scanned bundle's Homebrew cask channel is checked on its own), so "Discovered 244 · Checked 246" looked inconsistent. Include sub-apps in the discovered total so both cover the same set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Log the nullable cache write time directly instead of a redundant null-conditional the analyzer flags as a constant condition. - Map file write times with Select/Max instead of a manual foreach. - Match cask artifact app entries with Any instead of a foreach filter. - Use Path.Join (never drops earlier segments) instead of Path.Combine. Co-Authored-By: Claude Opus 4.8 <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.
Summary
Started from a report that
awscli/mole/rtkwere flagged outdated bybrew outdatedbut missing fromapps, and grew to fix several related discovery/rendering issues surfaced along the way.1. Stale Homebrew cache → missing updates (root cause)
brew info --json(the only Homebrew command the scanner runs) never triggers Homebrew's auto-update, so the local API cache'sversions.stablelags the real latest between refreshes (default 24h). Outdated formulae and casks were therefore reported as up-to-date. Runningbrew outdatedmanually refreshed the cache and the updates appeared — with no code change — which pinned the cause.Fix: before reading
brew info, refresh the cache withbrew update --quietwhen it is older than a threshold. Metadata only — neverbrew upgrade; installed packages are never touched (verified). Version-robust staleness check globs*.jws.json(handles the brew 6.xinternal/packages.<arch>.jws.jsonlayout); honoursHOMEBREW_CACHE; window configurable viaAPPS_BREW_CACHE_MAX_AGE_HOURS(default 6h); fresh cache adds zero latency; failures are non-fatal.2. An app and its Homebrew cask are two channels
A scanned
.appand its cask are separate update channels (e.g. a Sparkle self-updater vs the cask) whose versions can differ, so both surface: the scanned app is the parent and the cask is a sub-app (one level deep). When the cask installs the same bundle, the on-disk version is authoritative — brew's receipt lags for casks that auto-update themselves (OrbStack self-updated to 2.2.3 while brew still recorded 2.2.2) — so the sub-app reports the bundle's version and isn't falsely flagged outdated. Parents roll up sub-app updates (AppRecord.HasUpdate) so a cask-channel update still shows in the default outdated-only view. A reusable, case-insensitive matcher (artifact target path /*.appname / bundle id) drives same-bundle detection and cross-source de-duplication.3. Correct cross-scanner de-duplication (source + kind + name)
De-duplicating on name alone merged unrelated items that share a name: the "Claude" desktop app under the "Claude" Chrome extension, and the "EditorConfig" / "PowerShell" JetBrains plugins under their VS Code extensions. The key now includes the source scanner and kind, so only genuine duplicates from the same scanner collapse; everything else stays a distinct top-level entry. The manual-app + Homebrew-cask pairing is unaffected — it is reconciled inside
MacApplicationsScannerand reaches the orchestrator as a single record.4. VS Code
%displayName%fallbackSome extensions ship an unresolved NLS placeholder (
%displayName%) as theirdisplayName; several then collided under that literal. The placeholder is now rejected so the extension id (e.g.github.vscode-pull-request-github) is used as the name.5. Uncheckable apps flagged red
Apps whose update status can't be determined were shown dimmed with a leading
?. They're now flagged red — mirroring how outdated rows are flagged green — with no?prefix.6. Consistent discovered / checked totals
"Discovered" counted only top-level records while "Checked" also counts sub-apps, so
Discovered 244 · Checked 246looked wrong. Sub-apps are now included in the discovered total so both cover the same set.Housekeeping
"already updated to v{installed}"debug log.Path.Join).VERSION→0.0.13-beta.Verification
awscli/mole/rtknow listed as outdated;brew listconfirms packages untouched..app+Application (Cask)sub-app, both current.developbase.🤖 Generated with Claude Code