Skip to content

cask: upgrade auto_updates casks when bundle version is stale#21882

Merged
bevanjkay merged 1 commit into
mainfrom
cask-bundle-version
Apr 8, 2026
Merged

cask: upgrade auto_updates casks when bundle version is stale#21882
bevanjkay merged 1 commit into
mainfrom
cask-bundle-version

Conversation

@bevanjkay

@bevanjkay bevanjkay commented Apr 1, 2026

Copy link
Copy Markdown
Member

  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't other open Pull Requests for the same change?
  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests (excluding integration tests) for your changes? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) with your changes locally?

  • AI was used to generate or assist with generating this PR. Please specify below how you used AI to help you, and what steps you have taken to manually verify the changes.

I used Codex to help plan out and implement the initial version of bundle_version. I have reviewed and tested the functionality, but anticipate that there will be some edge-cases.


Outdated Description

This PR introduces an initial implementation of a bundle_version parameter for cask artifacts. In this first iteration, it only applies to app artifacts, which is the primary target for this feature.

The goal is to support metadata-only upgrades when an app has already been updated outside Homebrew, for example by an in-app updater. If a newer cask is available in Homebrew, and the installed app bundle already matches the incoming cask’s version and bundle_version, Homebrew updates its installed cask metadata without fetching or reinstalling the app.

Note that this only applies when the version matches, so brew upgrade <cask> would still run an installation when the installed version is greater than the Homebrew version. A future possible enhancement (out of scope here) could be to handle cases where the installed app is never than the incoming cask version.

I’ve kept this PR scoped to the MVP. For broader adoption, we would also want brew bump and brew bump-cask-pr support to detect and update bundle_version automatically. I intend to work on this as a follow-up, unless there is preference to include it here.


Included in this PR;

  • Initial bundle_version support for app artifacts
  • Metadata-only upgrades when the installed app matches the incoming cask’s version and bundle_version
  • Skipping fetch when a metadata-only upgrade will be performed
  • brew audit --online functionality to check the bundle_version values against the artifact

Follow-up work:

  • bundle_version detection and update support in brew bump and brew bump-cask-pr
  • Potential handling for apps already newer than the incoming cask version

This PR introduces a change to the way that we handle upgrades for simple Casks in homebrew-cask, with the qualifiers that they only have a single app artifact, and have auto_updates true. At present these are only ever upgraded with brew upgrade when the --greedy or --greedy-auto-updates flags are passed.

The PR introduces a bundle_version check that looks inside the installed applications info.plist file and compares the version numbers with the incoming latest version from the tap. If the incoming version is detected as being newer than the installed version, then we proceed with the upgrade.

There is a level of naivety involved in the version matching, as there is a lot of inconsistent between how developers set the CFBundleVersion and CFShortBundleVersionString in info.plist versus how we use version in homebrew-cask. This means that we look for a broad range of matches, which may prove to be too loose. In my testing it has seemed to be ok, especially because we still look for an outdated Caskroom file, so we shouldn't ever see repeated upgrades for the same version.

Copilot AI review requested due to automatic review settings April 1, 2026 13:06
@bevanjkay bevanjkay changed the title cask: bundle_version initial implementation cask: bundle_version initial implementation Apr 1, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds initial bundle_version support for cask app artifacts to enable “metadata-only” upgrades when an installed app bundle already matches the incoming cask’s version + bundle_version, avoiding unnecessary downloads/reinstalls. This also extends API serialization/loading and introduces an online audit to validate declared bundle_version values against extracted artifacts.

Changes:

  • Add bundle_version option to app artifacts, including API JSON serialization/loading and artifact matching logic.
  • Implement metadata-only upgrade path (and skip prefetch/fetch where applicable) when installed bundle version matches.
  • Add brew audit --online checks and unit tests covering metadata-only upgrade and bundle version auditing.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
Library/Homebrew/cask/artifact/abstract_artifact.rb Adds default “metadata-upgrade” capability hooks to artifacts.
Library/Homebrew/cask/artifact/app.rb Implements bundle_version option + matching against installed Info.plist.
Library/Homebrew/cask/upgrade.rb Adds metadata-only upgrade flow and fetch filtering for upgrades.
Library/Homebrew/cmd/upgrade.rb Skips prefetching downloads for casks eligible for metadata-only upgrades.
Library/Homebrew/cask/installer.rb Adds write_metadata helper used by metadata-only upgrades.
Library/Homebrew/cask/audit.rb Adds online audit for declared bundle_version vs extracted app bundle version.
Library/Homebrew/test/cask/upgrade_spec.rb Tests metadata-only upgrade behavior and prefetch skipping.
Library/Homebrew/test/cask/audit_spec.rb Tests online audit behavior for bundle version matching/mismatch.
Library/Homebrew/test/cask/artifact/app_spec.rb Tests bundle_version parsing/storage and matching logic on App artifacts.
Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb Tests loading bundle_version from JSON and internal JSON API artifacts.
Library/Homebrew/test/api/cask/cask_struct_generator_spec.rb Updates API artifact processing expectations for bundle_version.
Library/Homebrew/test/api/cask_spec.rb Updates expected cask source checksum due to fixture changes.
Library/Homebrew/test/support/fixtures/cask/Casks/everything.rb Adds bundle_version to fixture cask’s app stanza.
Library/Homebrew/test/support/fixtures/cask/everything.json Updates API fixture artifacts to include bundle_version and checksum.
Library/Homebrew/test/support/fixtures/cask/everything.internal.json Updates internal API fixture artifacts to include bundle_version and checksum.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Library/Homebrew/cask/audit.rb Outdated
Comment thread Library/Homebrew/cask/upgrade.rb Outdated
@bevanjkay

Copy link
Copy Markdown
Member Author

I think we may also need to add an audit to not allow the use of bundle_version when the download url is unversioned, because the download of a specific version is not guaranteed in that case.

@MikeMcQuaid MikeMcQuaid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @bevanjkay but I think this needs a different approach.

I think we may also need to add an audit to not allow the use of bundle_version when the download url is unversioned, because the download of a specific version is not guaranteed in that case.

This is revealing because I think, unfortunately, it makes the DSL pointless in that case. The "problem to be solved" also applies when the download URL is unversioned, unless I've missed something.

Basically: we want a way to have brew upgrade --greedy be the default but avoid upgrading things where the bundle version indicates it's already up-to-date. We may still want to update e.g. the caskroom version in that case so we don't keep repeatedly needing to check the same data.

We have a bundle_version attribute in our existing cask JSON API that sniffs this bundle version. That's what's already been used by other tools to do this sort of comparison.

Hope that helps, let me know what you think as I may be totally wrong about some of the above!

Comment thread Library/Homebrew/test/support/fixtures/cask/everything.json Outdated
Comment thread Library/Homebrew/test/support/fixtures/cask/Casks/everything.rb Outdated
@bevanjkay

Copy link
Copy Markdown
Member Author

This is revealing because I think, unfortunately, it makes the DSL pointless in that case. The "problem to be solved" also applies when the download URL is unversioned, unless I've missed something.

Unversioned downloads are always a worse UX because there's no guarantee the version in the cask will match brew fetch. So perhaps the issue I referred to is not actually any worse than the status quo. If the incoming cask metadata is stale with the actual download contents we just have to naively trust it anyway. Unless we handle a bundle_version comparison at upgrade time (after fetching).

@MikeMcQuaid

Copy link
Copy Markdown
Member

Unless we handle a bundle_version comparison at upgrade time (after fetching).

This seems better than nothing if we can 👍🏻

@bevanjkay
bevanjkay force-pushed the cask-bundle-version branch from 8e8cdf7 to 38e43de Compare April 2, 2026 12:10
@bevanjkay

Copy link
Copy Markdown
Member Author

Thanks for the back and forth here @MikeMcQuaid - I think it's going to take some time to land exactly what the best way forward is here, as I'm discovering some different permutations as they arise. But I think it's better to that with some logic out in the open because I think the discussion is useful, if that's ok?


This round removes the dsl updates, and automatically checks the currently installed application's info.plist and compares the values with the "latest" version in the tap. If they match it runs a "metadata_only" upgrade. This is gated to only occur within Casks that have a single app stanza, because there are no additional moving parts. The discussion below provides some cases where increasing this radius requires significant extra logic.


One thought that has arisen is that this isn't actually the primary issue. it's nice to know with brew info what the latest version is, and to keep the metadata in line with this, but I may be focussing too much on the Caskroom here.

Your comment in the earlier thread;

"do we have a good reason to believe all users need this upgrade".

made me think that maybe some detection around this idea is a better first goal, and then syncing the Caskroom metadata is secondary.

A new goal may be to find a may to detect and develop the criteria for when brew upgrade should occur by default on a Cask. With maybe three distinct pathways;

  1. A full upgrade is necessary
  2. A metadata-only upgrade will suffice
  3. A --greedy flag is required (some instances are impossible to detect without running the whole upgrade process and exiting at the end with a no-op)

In full, to decide between 1 & 2 is not as simple as comparing bundle_version because we also need to compare any other changes that may have occurred that require Homebrew to take action.

Comment thread Library/Homebrew/cask/upgrade.rb Outdated
@MikeMcQuaid

Copy link
Copy Markdown
Member

Thanks for the back and forth here @MikeMcQuaid - I think it's going to take some time to land exactly what the best way forward is here, as I'm discovering some different permutations as they arise. But I think it's better to that with some logic out in the open because I think the discussion is useful, if that's ok?

Yeh, of course: much appreciated!

If they match it runs a "metadata_only" upgrade.

What does this look like, output (and speed) wise compared to the "normal" upgrade?

In full, to decide between 1 & 2 is not as simple as comparing bundle_version because we also need to compare any other changes that may have occurred that require Homebrew to take action.

I guess my thinking is: this change is not so much about deciding whether to upgrade or not but instead having more cases of casks not currently upgraded today without --greedy that we can upgrade without --greedy without doing effective just a full reinstall of content already there.

Even if this PR ends up only enabling this for a single cask that's updated relatively often: I think that'll be a net positive and good foundation to build off.

Hope that helps!

Thanks again @bevanjkay, cracking work.

@bevanjkay
bevanjkay force-pushed the cask-bundle-version branch from 38e43de to f813bcc Compare April 4, 2026 13:37
@bevanjkay bevanjkay changed the title cask: bundle_version initial implementation cask: upgrade auto_updates casks when bundle version is stale Apr 4, 2026
Comment thread Library/Homebrew/cask/cask.rb Fixed
@bevanjkay
bevanjkay marked this pull request as draft April 4, 2026 13:39
@bevanjkay
bevanjkay requested a review from Copilot April 4, 2026 13:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

Library/Homebrew/cmd/upgrade.rb:482

  • prefetch_outdated_casks! now calls Install.show_combined_fetch_downloads_heading twice (lines 475 and 481), which will print duplicate “Fetching/Downloading…” headings during brew upgrade prefetch. It also does prefetch_names&.replace(cask_names) twice; please keep a single, correctly placed update + heading call.
        cask_names = outdated_casks.map(&:full_name)
        prefetch_names&.replace(cask_names)
        Install.show_combined_fetch_downloads_heading(cask_names:) if show_downloads_heading
        Install.enqueue_cask_installers(fetchable_cask_installers)
        prefetch_names&.replace(cask_names)
        prefetch_upgrades&.replace(
          outdated_casks.map { |cask| "#{cask.full_name} #{cask.installed_version} -> #{cask.version}" },
        )
        Install.show_combined_fetch_downloads_heading(cask_names:) if show_downloads_heading


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Library/Homebrew/cask/cask.rb Outdated
Comment thread Library/Homebrew/cask/installer.rb Outdated
@bevanjkay

Copy link
Copy Markdown
Member Author

Converting to draft, because I need to clean some things up here.

Quite a diversion from the original PR, but I think it's a productive outcome.

Currently, casks marked as auto_updates true are only upgrade when --greedy or --greedy-auto-updates is passed to brew upgrade. This PR adds some checks to allow these upgrades to run with a general brew upgrade when certain conditions are met.

For an auto_updates Cask, this PR now updates the flow to now check the applications bundle_version when there is a newer version available in the Tap, than what is currently in the Caskroom. If the application's bundle_version is older than the "latest" version in the tap, then we run the upgrade, if not we don't do anything. These casks would still be upgrade with the --greedy or --greedy-auto-updates flag, as I would suggest it implies the user doesn't want Homebrew to check the current application status.


For now this is still applied only to Cask with a single app stanza, as we ensure the bundle_version check occurs in the right place. Even so, this change will affect 1,500 casks.

@bevanjkay
bevanjkay force-pushed the cask-bundle-version branch from f813bcc to 788dccd Compare April 4, 2026 13:51
@MikeMcQuaid

Copy link
Copy Markdown
Member

This PR adds some checks to allow these upgrades to run with a general brew upgrade when certain conditions are met.
Even so, this change will affect 1,500 casks.

👏🏻

@bevanjkay
bevanjkay force-pushed the cask-bundle-version branch from 788dccd to 29c8bf3 Compare April 5, 2026 13:53
@bevanjkay
bevanjkay marked this pull request as ready for review April 5, 2026 13:57
@bevanjkay

Copy link
Copy Markdown
Member Author

I have been daily driving this branch since my last push and haven't noticed anything out of the ordinary, other than more casks being upgraded without having to pass --greedy-auto-updates.

@MikeMcQuaid MikeMcQuaid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to merge when you are, great work here!

@bevanjkay
bevanjkay added this pull request to the merge queue Apr 8, 2026
@bevanjkay

Copy link
Copy Markdown
Member Author

Let's merge now, it should give a few days before it lands in a release to hopefully pick up anything that needs adjustment.

@paschun

paschun commented Apr 9, 2026

Copy link
Copy Markdown

I don't think this info.plist inspection is working for several of my apps. Homebrew today tries to update 15 casks. Some of these I haven't opened in a while, so their internal auto-updaters haven't run, and they aren't up to date. But some of them such as signal update themselves every day, but homebrew is still trying to upgrade signal. I just checked inside the signal app, it is version 8.6.0, which is latest.

Aside from the info.plist version inspection issue, I personally prefer the old behavior of letting the app's auto updater handle updates. Because the homebrew process for cask upgrades is iirc to uninstall the current version, and install a new version. Which for some apps, can wipe out it's state/appdata .

@bevanjkay

Copy link
Copy Markdown
Member Author

This PR #21974 should resolve thesignal cask from being detected too greedily. The challenge there is that the signal application bundle has "1" set as the Bundle Version which fails in the comparison with "8.6.1".

Aside from the info.plist version inspection issue, I personally prefer the old behavior of letting the app's auto updater handle updates. Because the homebrew process for cask upgrades is iirc to uninstall the current version, and install a new version. Which for some apps, can wipe out it's state/appdata .

I see this as more of an unfortunate side-effect of the change rather than an actual causation. In saying that, if an application you use has it's state/appdata removed during a brew upgrade process then an issue should be filed in the homebrew-cask repository to resolve this, as this is not expected behaviour.

@paschun

paschun commented Apr 10, 2026

Copy link
Copy Markdown

I see this as more of an unfortunate side-effect of the change rather than an actual causation. In saying that, if an application you use has it's state/appdata removed during a brew upgrade process then an issue should be filed in the homebrew-cask repository to resolve this, as this is not expected behaviour.

Thank you for the quick response. I didn't know that was unintended behavior. Thanks for clarifying.

I guess I will continue to mention my issues here..
Another issue i'm having is with the disabled casks. I installed the mudlet cask before it was disabled. It is set to auto_updates. Now, every time I run brew upgrade, it throws an error. So I can't run brew upgrade without uninstalling the cask.

@dkav

dkav commented May 13, 2026

Copy link
Copy Markdown
Contributor

Is there a way to discern which outdated auto-update casks will be upgraded by brew upgrade as currently implemented?

For my casks, when I run brew upgrade --cask, it only updates a subset of what is listed with bw outdated --cask --greedy-auto-updates. If I run bw outdated --cask, no cask is listed. There doesn't appear to be a way to know which casks that are set to auto-update will be upgraded without running brew upgrade.

Homebrew 5.1.11-77-gd430e68

@bevanjkay

Copy link
Copy Markdown
Member Author

@dkav Thanks for reporting, #22272 should fix this.

paolomainardi added a commit to sparkfabrik/sparkdock that referenced this pull request Jun 28, 2026
…O_UPDATES_CASKS) #542

A recent brew change (Homebrew/brew#21882) made 'brew upgrade' upgrade casks with auto_updates true even without --greedy, force-upgrading self-managing apps (Chrome, Slack, Docker Desktop). Sets HOMEBREW_NO_UPGRADE_AUTO_UPDATES_CASKS=1 in the managed shell init (covers all brew usage) and in the TUI's brew upgrade run, restoring the prior behaviour. Opt back in per run with 'brew upgrade --greedy'.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]
paolomainardi added a commit to sparkfabrik/sparkdock that referenced this pull request Jun 28, 2026
* feat(tui): scaffold sparkdock terminal hub with tested core packages #542

Adds a Go/bubbletea terminal hub under src/tui following the Elm architecture with a clean split between pure core logic and injected side effects.

Core packages are unit-tested: feed (pure parser for the Ansible callback protocol), status (Checker over sparkdock-check-updates and brew, injected command runner), runner (exec ansible-playbook, stream parsed events, cancel, subprocess-scoped become password, injected command builder), and version (git commit/branch of /opt/sparkdock, no semver). The runnable layer wires a page router (app), the launch splash, and the flat grouped status dashboard.

The Runner, Password, Log, and sjust pages, the tui Ansible build tag, and the sparkdock dispatch/self-update wiring follow next.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): add runner page with streaming statusline #542

Renders a live run: scrolling content region above a pinned statusline (phase, task, ok/changed/failed, elapsed), fed by the runner backend's event stream. Supports cancel, retry, and opening the captured output. Caches theme styles via theme.Default() and cancels any still-live handle before starting a new run. Reducer is table-tested for content capture, success/failure/cancel state, and the failed-task-count case.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): add masked become-password page #542

Collects the sudo password in a masked textinput and emits Submit/Cancel; the app owns the password lifetime. Clears the input buffer immediately after submit so the secret does not linger. Also migrates the runner viewport off the deprecated LineUp/LineDown to ScrollUp/ScrollDown.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): add copyable run-log page #542

Shows the full captured output in a scrollable view, copies it to the clipboard (pbcopy), and writes it to ~/.cache/sparkdock/last-run.log. Clipboard and file writes are injected for testing. Surfaces copy and write failures in the footer instead of silently claiming success.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): add sjust recipe browser page #542

Lists sjust recipes (injected lister; DefaultLister parses 'sjust --list') and runs the selected one through the shared Runner. parseRecipes skips group-header lines so they are not mistaken for recipes.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): wire app router, run orchestration, and remaining pages #542

Integrates the runner, password, log, and sjust pages into the app router. The app builds the runner.Handle per action, gates sudo actions behind the password page, caches the become password in memory for the session, and re-prompts on a sudo authentication failure. Adds a generic command runner for sjust recipes and an Ansible become-auth-failure detector. The runner page consumes a handed-in handle, gates back/log/retry to finished runs, and emits a back signal so the user cannot leave mid-run and drop events. App orchestration flows are unit-tested with an injected fake runner.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): add sparkdock ansible stdout callback plugin #542

Emits the @@PHASE/@@TASK/@@STAT/@@done marker protocol plus glyph content lines that the terminal hub parses into a streaming run view. Active only when ANSIBLE_STDOUT_CALLBACK=sparkdock is set (the TUI sets it per run), so existing provisioning output is unchanged.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): build sparkdock-tui via ansible tag and add 'sparkdock tui' launcher #542

Adds a 'tui' Ansible tag that mirrors the menubar block: go build of src/tui installed to the Homebrew bin (become: false, no sudo). 'sparkdock tui' launches the hub and, on first use when the binary is absent, builds it via the 'tui' tag (no sudo prompt) before exec'ing it. 'sjust sparkdock-tui-install' rebuilds on demand.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* docs(tui): update README for completed pages and install path #542

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* fix(tui): create .build dir, surface run errors, add statusline spacing #542

The 'tui' build task failed because 'go build -o .build/...' does not create the output directory (unlike swift's .build); it now mkdir -p .build first. Build failures were also invisible: the build task is failed_when:false, so its stderr was captured but never shown. The follow-up fail task now includes the captured stderr/stdout, and the stdout callback surfaces stderr/stdout for any failed task instead of only the short msg. The runner view also gains a blank line between the last output line and the pinned statusline.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* fix(tui): pass dev_env_dir so runs build from the launch directory #542

The playbook hardcodes dev_env_dir: /opt/sparkdock, so a TUI run against a checkout (SPARKDOCK_ROOT=<repo>) still cd'd to /opt/sparkdock/src/tui and failed. AnsibleBuilder now passes -e dev_env_dir=<abs Dir>, aligning the playbook with the directory the run is launched from; in production (Root=/opt/sparkdock) this equals the default.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* fix(tui): make Upgrade Brew packages run brew upgrade directly #542

The upgrade action now runs 'brew upgrade' via the generic command runner instead of an ansible --tags cask provisioning run, matching what the brew-outdated status reflects.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): replace proxy dashboard with HTTP proxy management actions #542

Removes the no-op 'HTTP-proxy dashboard → opens browser' item. Adds an HTTP proxy group (Status/Start/Stop via spark-http-proxy, Upgrade via sjust http-proxy-install-update) and wires Device info to 'sjust device-info'. All run through the streaming runner view.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* test(tui): use an unplanned action in the unhandled-action test #542

Device info is now wired, so the test uses 'self-update' (still unplanned) to assert unhandled actions no-op back to the dashboard without spawning a real command.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): run under a PTY and answer password prompts interactively #542

Replaces the env/file-based become password with PTY-based prompt detection. Runs now execute under a pseudo-terminal; when the process prompts for a password (ansible --ask-become-pass, or brew/sudo), the runner detects it, the app shows a masked field, and the answer is written straight to the process's PTY. The password is never stored, cached, logged, or placed in env/argv/file. Backing out of the prompt cancels the run; ansible become-auth failures re-run so the prompt reappears. This covers brew casks (e.g. docker-desktop) that prompt mid-upgrade, which previously hung the TUI.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): size child PTY, truncate output, clearer completion, drop sjust runner #542

Sizes the child PTY to the runner view so tty-aware programs (e.g. ayse-get-sm) render to fit instead of overflowing and corrupting the layout, and truncates each output line to the view width as a safety net. The finished statusline now states the result clearly (Completed/Failed/Cancelled). Removes the automatic become-failure re-run loop (failures stay on the runner page with retry). Removes the sjust task runner from Tools (not useful) and wires Device info to ayse-get-sm.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): prime sudo for privileged runs, ansible-only summary, colored footer #542

Privileged runs (provisioning, HTTP-proxy upgrade, brew upgrade) now prime the sudo timestamp up front (sudo -v, answered via the PTY prompt) instead of feeding ansible a become password; ansible's per-task sudo -n and brew's cask sudo reuse the cached credential, so there is one prompt and no 'premature end of stream' become failures. The Summary line and ok/changed/failed counters now render only for ansible runs (plain commands like proxy status or device info no longer show a meaningless '0 ok' tally). Removes the Tools group; Device info moves to a colored 'd' shortcut in the dashboard footer, which now renders claude-code-style colored key hints.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): play a short startup chime on launch #542

Adds an internal/audio package that synthesises a soft two-note chime (E5 to A5) as an in-process WAV and plays it via macOS afplay on launch, the way OpenCode plays a startup sound. Best-effort and non-blocking: fire-and-forget, no UI delay, silent no-op when SPARKDOCK_TUI_NO_AUDIO is set, off macOS, or when afplay is absent. No external audio asset is shipped.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): play opencode sound on logo click #542

Clicking the logo (the splash, or the header on the dashboard) plays a short sound, like OpenCode's logo audio, instead of playing automatically on launch. Enables mouse input and plays asynchronously via afplay; best-effort and silent when disabled (SPARKDOCK_TUI_NO_AUDIO), off macOS, or afplay is absent. The sound is OpenCode's MIT-licensed bip-bop-01.mp3, embedded with attribution (see internal/audio/assets/NOTICE).

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): load status during splash; logo click holds the bootstrap #542

Status now loads in the background during the splash, and the splash hands off to the dashboard only once status is ready and a minimum display time has passed, so the dashboard appears already populated. A maximum timeout still forces the hand-off if the status load is slow, and any key skips. Clicking the logo plays the sound and holds the splash a little longer, so a user can keep clicking to hear it before the app boots.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* refactor(tui): drop splash click-hold timer #542

The status-load wait already gives enough time to click the logo for the sound, so a logo click now just plays the sound. Removes the click-hold state and timer; the splash hands off purely on status-ready plus the minimum display time, with the timeout fallback.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): add system-info panel to the dashboard header #542

Adds an internal/sysinfo package that gathers this machine's hardware (model, serial, chip with CPU/GPU cores, memory free/total, disk free/total, macOS) via the macOS tools sparkdock already uses (system_profiler, sysctl, vm_stat, df, sw_vers) behind an injected command runner, with pure table-tested parsers. The dashboard renders it as a right-hand column beside the status rows, gathered in the background on launch and refreshed with r; the column is width-budgeted and ANSI-truncated so it never overflows the status block, and is omitted on narrow terminals. CPU vs GPU cores are disambiguated by occurrence order (robust on base M-series); memory is shown as free/total.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* fix(tui): route SysInfoMsg to the dashboard so the panel populates #542

The system-info gather completes during the splash (system_profiler is slow), but only StatusMsg was force-routed to the dashboard; SysInfoMsg fell to the active splash page and was dropped, leaving the panel empty. It is now force-routed like StatusMsg.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): one-line model/serial/macOS in sysinfo panel; reports open at top #542

The system-info panel now shows model, serial, and macOS version on a single line (Chip, Memory, Disk remain separate). Plain-command runs (e.g. Device info) now position the runner view at the top on completion so the report reads from the beginning, while ansible runs still follow the tail to the summary.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* fix(tui): disable mouse after splash for copy; pin reports to top; consistent free format #542

Mouse reporting was capturing clicks so the terminal could not do native text selection/copy. Mouse is now enabled only on the splash (for the logo click) and disabled on hand-off to the dashboard, restoring normal selection/copy on the content pages. Plain-command output (e.g. Device info) is pinned to the top throughout the run, not just on completion, so a report reads from the start. Memory and Disk now use the same '<free> free / <total>' format.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* refactor(tui): scroll modes + interactive runner; brew without forced sudo #542

Generalises run presentation: the caller declares a runview.ScrollMode (FollowTail for streaming work, PinTop for one-shot reports like Device info and proxy status) instead of inferring it from whether the run produced stats. Stats/summary stay a runtime content concern, independent of scrolling. The runner view is now interactive while a run is live: typed keys are forwarded to the process PTY (so brew's 'Proceed? [y/n]' and any prompt can be answered), arrows scroll, ctrl+c cancels. Brew upgrade no longer primes sudo up front (formulae need none); a cask that needs sudo prompts on the PTY and is answered via the password page. Removes the now-unused ForSudoCommand.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* refactor(tui): render runs via a content strategy; embed VT terminal for raw programs #542

Splits run rendering into two strategies behind a content interface in the runner view. The runner now emits raw PTY bytes (runner.Handle.Output) and leaves decoding to the view: structuredContent decodes the sparkdock callback (feed.Decoder) into a line list with the phase/task/tally statusline (ansible), while terminalContent feeds the bytes to a charmbracelet/x/vt emulator and renders the live screen, so programs that redraw in place (brew progress bars, docker-compose) display faithfully instead of piling up duplicate lines. Adds feed.Decoder (stateful line buffering, tested). The app declares RenderMode per action (brew/proxy start-stop = Terminal, ansible/reports = Structured) alongside ScrollMode. Input forwarding to the PTY while running is retained for interactive prompts.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* fix(tui): ansible become via --ask-become-pass, answered reactively, never cached #542

Replaces the unreliable sudo-prime wrapper (its tty-bound sudo timestamp did not reach ansible's per-task sudo, causing 'Premature end of stream waiting for become success'). Sudo ansible runs now pass --ask-become-pass, so ansible prompts once and feeds the password to every become task itself, regardless of the tty's sudo timestamp. The prompt is answered reactively on the PTY via the masked password page; the password is never cached, stored, or placed in env/argv. A wrong entry fails the run and is retried with r.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): HTTP proxy upgrade uses the canonical sjust recipe #542

proxy-upgrade now runs 'sjust http-proxy-install-update', which git-updates the http-proxy repository and then re-provisions, instead of only re-running the http-proxy ansible tag (which never pulled the new version). Rendered in terminal mode since the recipe emits default ansible and git output; its nested --ask-become-pass prompt is answered via the password page.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): structured rendering for recipe-driven ansible; \r-aware decoder #542

Gives HTTP proxy upgrade (and any ansible run via a sjust recipe) the structured feed: the recipe now runs with the sparkdock stdout callback env (runner.ForCommandEnv + an absolute callback-plugin dir), so its nested ansible emits @@Markers and renders Structured with phases/glyphs/tally. The feed.Decoder now resolves carriage returns within a line (collapseCR), so single-line progress (git fetch, curl) collapses to its final state instead of piling up — making Structured robust for the whole \r-progress class while the VT emulator stays reserved for multi-line cursor TUIs (brew, docker-compose).

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): brew upgrade with --yes to skip the confirmation prompt #542

Uses brew's official -y/--yes flag so 'Upgrade Brew packages' proceeds without the interactive Proceed? prompt. Casks that need sudo still prompt on the PTY (answered via the password page).

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* fix(brew): stop upgrading auto_updates casks (HOMEBREW_NO_UPGRADE_AUTO_UPDATES_CASKS) #542

A recent brew change (Homebrew/brew#21882) made 'brew upgrade' upgrade casks with auto_updates true even without --greedy, force-upgrading self-managing apps (Chrome, Slack, Docker Desktop). Sets HOMEBREW_NO_UPGRADE_AUTO_UPDATES_CASKS=1 in the managed shell init (covers all brew usage) and in the TUI's brew upgrade run, restoring the prior behaviour. Opt back in per run with 'brew upgrade --greedy'.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* fix(ansible): set HOMEBREW_NO_UPGRADE_AUTO_UPDATES_CASKS on the provisioning play #542

Adds a play-level environment in base.yml so every brew/cask task inherits HOMEBREW_NO_UPGRADE_AUTO_UPDATES_CASKS=1 regardless of how ansible is launched (CI, launchd, menubar), keeping provisioning from upgrading self-updating casks. Covers the provisioning path alongside the shell init and the TUI.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* chore(tui): remove stray vtcheck_main build artifact #542

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* refactor(tui): apply review fixes — runner robustness, sysinfo sections, scroll, cleanup #542

From a full fresh-eyes review of the branch: guarantee the runner always sends Done (deferred, even on panic) so the UI can't hang, and cap the prompt-detection line buffer so a newline-less stream can't grow it unbounded. Parse CPU vs GPU core counts by section header (Hardware vs Graphics/Displays) instead of ordinal, robust to system_profiler changes. Fix runner-view scrollback (ScrollUp on negative delta; up-arrow/k now scroll up). Collapse a redundant double stats() call. Remove the unused PageSjust id. Set changed_when: false on the tui go build (go's cache makes it a no-op; the install copy reports real change). Also removed a stray committed build artifact earlier.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* refactor(tui): make dashboard action labels consistent #542

Unify the menu vocabulary so each action reads as verb + concrete noun and matches its status row. Rename the skills status row to "AI harness" so it matches the "Sync AI harness" action (one subsystem, one name). Rename "Run full provisioning" to "Provision system" and its run title to "Provisioning system". Rename the HTTP-proxy "Status" action to "Check status" so it is verb-first like its Start/Stop/Upgrade siblings.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* feat(tui): label actions with their CLI equivalent, rebuild on stale sources #542

Rename the full-provisioning action to "Update everything" and show each action's equivalent shell command as its detail (e.g. $ sparkdock, $ brew upgrade, $ spark-http-proxy status), so the TUI teaches the underlying CLI; live status counts stay on the status rows. Hide the not-yet-wired "Update Sparkdock" action so no dead button ships (the amber status dot still signals a stale install; run `sparkdock` to self-update). Export SPARKDOCK_ROOT from the entrypoint so the binary always resolves the right root instead of defaulting to /opt/sparkdock. The `sparkdock tui` launcher now rebuilds the binary when src/tui is newer than the installed binary, not only when it is absent, so a self-update that touched the TUI is picked up before launch.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* ci(tui): add Go build, vet, gofmt, and test workflow #542

Run gofmt check, go vet, go build, and go test for src/tui on pushes and PRs that touch the TUI, mirroring the path-filtered menubar and python workflows. Also gofmt three existing files so the new gofmt gate passes.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* docs(tui): document Go standards and the TUI in AGENTS.md #542

Add a Go Standards section (mandatory gofmt/vet/build/test gate matching the CI workflow, the injected-runner design pattern, and the cross-platform build note) and a Sparkdock TUI section describing the opt-in launch, dashboard, CLI-equivalent action labels, Runner render strategies, the never-cached password handling, how to run against a dev checkout via SPARKDOCK_ROOT, and what is not yet wired.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]

* docs(ansible): correct become/sudo description for --ask-become-pass #542

The play runs unprivileged (become: no) and root work opts in per-task with become: yes; the become password is collected natively via --ask-become-pass (interactive) or --become (CI), populating ansible_become_pass with no ANSIBLE_BECOME_PASS env var. Update the inverted guidance ("omit become: false") that predated the #541 sudo refactor now merged into this branch.

Refs: #542
Assisted-by: claude-code/claude-opus-4-8[1m]
hussainweb added a commit to hussainweb/dotfiles that referenced this pull request Jul 6, 2026
Add the HOMEBREW_NO_UPGRADE_AUTO_UPDATES_CASKS environment variable
to both zsh and fish templates. This prevents Homebrew from trying
to automatically upgrade casks that are configured to handle their
own updates (a behavior change introduced in Homebrew/brew PR 21882,
see Homebrew/brew#21882).
tksohishi added a commit to tksohishi/dotfiles that referenced this pull request Jul 9, 2026
…ate-apps

- mise 2026.7.2's aqua backend layout change broke codex PATH resolution; drop the aqua dependency entirely
- codex cask tracks in the Brewfile like everything else
- /update-apps now runs brew upgrade --cask: covers auto_updates:false casks plus genuinely stale self-updating apps (brew compares bundle versions since Homebrew/brew#21882)
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.

5 participants