Skip to content

fix: refresh stale Homebrew cache and split cask/manual app sources - #43

Merged
iamr8 merged 7 commits into
developfrom
fix/brew-cache-freshness-and-cask-subapps
Aug 8, 2026
Merged

iamr8 merged 7 commits into
developfrom
fix/brew-cache-freshness-and-cask-subapps

Conversation

@iamr8

@iamr8 iamr8 commented Aug 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

Started from a report that awscli / mole / rtk were flagged outdated by brew outdated but missing from apps, 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's versions.stable lags the real latest between refreshes (default 24h). Outdated formulae and casks were therefore reported as up-to-date. Running brew outdated manually refreshed the cache and the updates appeared — with no code change — which pinned the cause.

Fix: before reading brew info, refresh the cache with brew update --quiet when it is older than a threshold. Metadata only — never brew upgrade; installed packages are never touched (verified). Version-robust staleness check globs *.jws.json (handles the brew 6.x internal/packages.<arch>.jws.json layout); honours HOMEBREW_CACHE; window configurable via APPS_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 .app and 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 / *.app name / 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 MacApplicationsScanner and reaches the orchestrator as a single record.

4. VS Code %displayName% fallback

Some extensions ship an unresolved NLS placeholder (%displayName%) as their displayName; 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 246 looked wrong. Sub-apps are now included in the discovered total so both cover the same set.

Housekeeping

  • Corrected a misleading "already updated to v{installed}" debug log.
  • Resolved CodeQL advisories in the new code (constant condition, Select/Where, Path.Join).
  • Bumps VERSION → 0.0.13-beta.

Verification

  • awscli / mole / rtk now listed as outdated; brew list confirms packages untouched.
  • OrbStack renders as parent .app + Application (Cask) sub-app, both current.
  • Claude app/ext, EditorConfig and PowerShell JB-plugin/VS-Code-extension each render as separate top-level entries.
  • Discovered and Checked totals match.
  • Build: 0 warnings. Tests: 312 passing (new coverage for cache staleness/refresh, the cask matcher, same-bundle vs different-bundle sub-app versions, parent roll-up, source/kind de-duplication, and the NLS placeholder). Verified green on the develop base.

🤖 Generated with Claude Code

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>
@iamr8 iamr8 self-assigned this Aug 8, 2026
Comment thread apps/Components/MacOs/MacApplicationsScanner.cs Fixed
Comment thread apps/Components/MacOs/MacApplicationsScanner.cs Fixed
Comment thread apps/Components/MacOs/MacApplicationsScanner.cs Fixed
Comment thread apps/Components/MacOs/MacApplicationsScanner.cs Fixed
Comment thread apps/Components/MacOs/MacApplicationsScanner.cs Fixed
iamr8 and others added 6 commits August 8, 2026 13:03
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>
@iamr8
iamr8 merged commit 1031b33 into develop Aug 8, 2026
12 checks passed
@iamr8
iamr8 deleted the fix/brew-cache-freshness-and-cask-subapps branch August 8, 2026 10:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants