@@ -2,6 +2,7 @@ package internal
22
33import (
44 "context"
5+ "encoding/json"
56 "fmt"
67 "os"
78 "os/exec"
@@ -160,6 +161,68 @@ func repoSlug() selfupdate.RepositorySlug {
160161 return selfupdate .NewRepositorySlug (repoOwner , repoName )
161162}
162163
164+ func parseBrewFormulaVersion (data []byte ) (string , error ) {
165+ var result struct {
166+ Formulae []struct {
167+ Versions struct {
168+ Stable string `json:"stable"`
169+ } `json:"versions"`
170+ } `json:"formulae"`
171+ }
172+ if err := json .Unmarshal (data , & result ); err != nil {
173+ return "" , err
174+ }
175+ if len (result .Formulae ) == 0 || result .Formulae [0 ].Versions .Stable == "" {
176+ return "" , fmt .Errorf ("no version found in brew info output" )
177+ }
178+ return result .Formulae [0 ].Versions .Stable , nil
179+ }
180+
181+ // brewFormulaVersion returns the version from the local Homebrew tap formula.
182+ //
183+ // Homebrew caches third-party tap formulae as local git clones and only
184+ // refreshes them when auto-update fires. Auto-update is triggered by
185+ // commands like `brew install` or `brew upgrade`, but has a 24h debounce
186+ // (HOMEBREW_AUTO_UPDATE_SECS, default 86400s) — if any brew command ran
187+ // within that window, the refresh is skipped. There is no background
188+ // process; taps stay stale until the next brew command after the debounce
189+ // expires, or until the user runs `brew update` manually.
190+ //
191+ // Unlike `go install @vX.Y.Z`, there is no way to tell brew to install a
192+ // specific version of a tap formula. The `@version` syntax (e.g.
193+ // `python@3.11`) only works when a separate versioned formula file exists
194+ // in the tap, which we don't publish.
195+ //
196+ // This function lets us detect when the local formula is behind the latest
197+ // GitHub release, so we can suppress misleading update prompts rather than
198+ // pointing the user at an upgrade that would silently no-op.
199+ func brewFormulaVersion (ctx context.Context ) (string , error ) {
200+ cmd := exec .CommandContext (ctx , "brew" , "info" , "--json=v2" , "threedotslabs/tap/tdl" )
201+ out , err := cmd .Output ()
202+ if err != nil {
203+ return "" , fmt .Errorf ("brew info failed: %w" , err )
204+ }
205+ return parseBrewFormulaVersion (out )
206+ }
207+
208+ // isVersionAvailableForInstallMethod checks whether the given version
209+ // is available through the user's install method. Currently only
210+ // Homebrew is checked — see brewFormulaVersion for why.
211+ // Returns true for all other methods or on any error (graceful fallback).
212+ func isVersionAvailableForInstallMethod (version string ) bool {
213+ method := DetectInstallMethod ()
214+ if method != InstallMethodHomebrew {
215+ return true
216+ }
217+ formulaVer , err := brewFormulaVersion (context .Background ())
218+ if err != nil {
219+ logrus .WithError (err ).Debug ("Could not check brew formula version" )
220+ return true
221+ }
222+ target := strings .TrimPrefix (version , "v" )
223+ return ! isNewerVersion (target , formulaVer )
224+ }
225+
163226// RunUpdate checks for updates and applies them based on the installation method.
164227func RunUpdate (ctx context.Context , currentVersion string , opts UpdateOptions ) error {
165228 if os .Getenv ("TDL_NO_UPDATE_CHECK" ) != "" {
@@ -243,6 +306,22 @@ func RunUpdate(ctx context.Context, currentVersion string, opts UpdateOptions) e
243306 // Branch on install method
244307 switch method {
245308 case InstallMethodHomebrew :
309+ // Homebrew's local tap may still point to an older formula version
310+ // (24h auto-update debounce). Bail out early instead of running a
311+ // doomed `brew upgrade` that would print "already installed".
312+ formulaVer , err := brewFormulaVersion (ctx )
313+ if err == nil {
314+ target := strings .TrimPrefix (targetVersion , "v" )
315+ if isNewerVersion (target , formulaVer ) {
316+ fmt .Println (color .YellowString (
317+ "Version %s is not yet available via Homebrew (formula has %s)." ,
318+ targetVersion , formulaVer ,
319+ ))
320+ fmt .Println ("Homebrew updates its formula cache periodically. To refresh manually, run:" )
321+ fmt .Printf (" %s\n \n " , SprintCommand ("brew update && brew upgrade tdl" ))
322+ return nil
323+ }
324+ }
246325 return updateViaCommand (ctx , "brew" , opts , currentVersion , targetVersion ,
247326 []string {"upgrade" , "tdl" })
248327
0 commit comments