11package main
22
33import (
4+ "context"
45 "encoding/json"
56 "fmt"
67 "net/http"
@@ -33,6 +34,32 @@ type Source struct {
3334 DisplayName string
3435 Owner string
3536 Repo string
37+ BinaryNames []string
38+ VersionArgs []string
39+ }
40+
41+ type InstalledInfo struct {
42+ Installed bool `json:"installed"`
43+ Version string `json:"version,omitempty"`
44+ Binary string `json:"binary,omitempty"`
45+ }
46+
47+ var semverRegex = regexp .MustCompile (`(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)` )
48+
49+ func (s Source ) DetectInstalled (ctx context.Context ) InstalledInfo {
50+ for _ , bin := range s .BinaryNames {
51+ path , err := exec .LookPath (bin )
52+ if err != nil {
53+ continue
54+ }
55+ ctx , cancel := context .WithTimeout (ctx , 3 * time .Second )
56+ defer cancel ()
57+ cmd := exec .CommandContext (ctx , path , s .VersionArgs ... )
58+ out , _ := cmd .CombinedOutput ()
59+ ver := semverRegex .FindString (string (out ))
60+ return InstalledInfo {Installed : true , Version : ver , Binary : bin }
61+ }
62+ return InstalledInfo {}
3663}
3764
3865func (s Source ) URL () string {
@@ -44,11 +71,11 @@ func (s Source) Fetch() ([]ChangelogEntry, error) {
4471}
4572
4673var sources = map [string ]Source {
47- "claude" : {DisplayName : "Claude Code" , Owner : "anthropics" , Repo : "claude-code" },
48- "codex" : {DisplayName : "OpenAI Codex" , Owner : "openai" , Repo : "codex" },
49- "opencode" : {DisplayName : "OpenCode" , Owner : "sst" , Repo : "opencode" },
50- "gemini" : {DisplayName : "Gemini CLI" , Owner : "google-gemini" , Repo : "gemini-cli" },
51- "copilot" : {DisplayName : "GitHub Copilot CLI" , Owner : "github" , Repo : "copilot-cli" },
74+ "claude" : {DisplayName : "Claude Code" , Owner : "anthropics" , Repo : "claude-code" , BinaryNames : [] string { "claude" }, VersionArgs : [] string { "--version" } },
75+ "codex" : {DisplayName : "OpenAI Codex" , Owner : "openai" , Repo : "codex" , BinaryNames : [] string { "codex" }, VersionArgs : [] string { "--version" } },
76+ "opencode" : {DisplayName : "OpenCode" , Owner : "sst" , Repo : "opencode" , BinaryNames : [] string { "opencode" }, VersionArgs : [] string { "--version" } },
77+ "gemini" : {DisplayName : "Gemini CLI" , Owner : "google-gemini" , Repo : "gemini-cli" , BinaryNames : [] string { "gemini" }, VersionArgs : [] string { "--version" } },
78+ "copilot" : {DisplayName : "GitHub Copilot CLI" , Owner : "github" , Repo : "copilot-cli" , BinaryNames : [] string { "github-copilot-cli" , "copilot" }, VersionArgs : [] string { "--version" } },
5279}
5380
5481func main () {
@@ -303,11 +330,15 @@ func runStatusCommand(jsonOutput bool) {
303330 }
304331
305332 results := make (chan statusResult , len (sources ))
333+ installedResults := make (map [string ]InstalledInfo )
334+ var mu sync.Mutex
306335 var wg sync.WaitGroup
307336
308- // Fetch up to 10 entries from each source concurrently
337+ ctx := context .Background ()
338+
339+ // Fetch releases and detect installed tools concurrently
309340 for name , src := range sources {
310- wg .Add (1 )
341+ wg .Add (2 )
311342 go func (name string , src Source ) {
312343 defer wg .Done ()
313344 entries , err := src .Fetch ()
@@ -318,6 +349,13 @@ func runStatusCommand(jsonOutput bool) {
318349 err : err ,
319350 }
320351 }(name , src )
352+ go func (name string , src Source ) {
353+ defer wg .Done ()
354+ info := src .DetectInstalled (ctx )
355+ mu .Lock ()
356+ installedResults [name ] = info
357+ mu .Unlock ()
358+ }(name , src )
321359 }
322360
323361 go func () {
@@ -326,13 +364,14 @@ func runStatusCommand(jsonOutput bool) {
326364 }()
327365
328366 type statusEntry struct {
329- Name string `json:"name"`
330- Version string `json:"version"`
331- PreviousVersion string `json:"previous_version"`
332- UpdatedAgo string `json:"updated_ago"`
333- UpdatedRecently bool `json:"updated_recently"`
334- AvgReleaseFreq string `json:"avg_release_freq"`
335- releasedAt time.Time
367+ Name string `json:"name"`
368+ InstalledVersion string `json:"installed_version"`
369+ Version string `json:"version"`
370+ UpdatedAgo string `json:"updated_ago"`
371+ UpdatedRecently bool `json:"updated_recently"`
372+ AvgReleaseFreq string `json:"avg_release_freq"`
373+ sourceName string
374+ releasedAt time.Time
336375 }
337376
338377 var statusEntries []statusEntry
@@ -349,17 +388,14 @@ func runStatusCommand(jsonOutput bool) {
349388 }
350389
351390 entry := statusEntry {
352- Name : r .displayName ,
353- Version : r .entries [0 ].Version ,
354- PreviousVersion : "-" ,
355- UpdatedAgo : "-" ,
356- UpdatedRecently : false ,
357- AvgReleaseFreq : "-" ,
358- releasedAt : r .entries [0 ].ReleasedAt ,
359- }
360-
361- if len (r .entries ) > 1 {
362- entry .PreviousVersion = r .entries [1 ].Version
391+ Name : r .displayName ,
392+ InstalledVersion : "-" ,
393+ Version : r .entries [0 ].Version ,
394+ UpdatedAgo : "-" ,
395+ UpdatedRecently : false ,
396+ AvgReleaseFreq : "-" ,
397+ sourceName : r .source ,
398+ releasedAt : r .entries [0 ].ReleasedAt ,
363399 }
364400
365401 if ! r .entries [0 ].ReleasedAt .IsZero () {
@@ -373,6 +409,20 @@ func runStatusCommand(jsonOutput bool) {
373409 statusEntries = append (statusEntries , entry )
374410 }
375411
412+ // Populate installed versions
413+ for i := range statusEntries {
414+ mu .Lock ()
415+ info , ok := installedResults [statusEntries [i ].sourceName ]
416+ mu .Unlock ()
417+ if ok && info .Installed {
418+ if info .Version != "" {
419+ statusEntries [i ].InstalledVersion = info .Version
420+ } else {
421+ statusEntries [i ].InstalledVersion = "yes"
422+ }
423+ }
424+ }
425+
376426 // Sort by most recently updated
377427 sort .Slice (statusEntries , func (i , j int ) bool {
378428 if statusEntries [i ].releasedAt .IsZero () && statusEntries [j ].releasedAt .IsZero () {
@@ -397,40 +447,39 @@ func runStatusCommand(jsonOutput bool) {
397447 // Print table with borders
398448 // Column widths
399449 const (
400- colTool = 20
401- col24h = 3
402- colVersion = 12
403- colPrevious = 12
404- colUpdated = 10
405- colFreq = 19
450+ colTool = 20
451+ col24h = 3
452+ colInstalled = 12
453+ colVersion = 12
454+ colUpdated = 10
455+ colFreq = 19
406456 )
407457
458+ border := func (left , mid , right string ) string {
459+ return fmt .Sprintf ("%s%s%s%s%s%s%s%s%s%s%s%s%s\n " ,
460+ left , strings .Repeat ("─" , colTool + 2 ),
461+ mid , strings .Repeat ("─" , col24h + 2 ),
462+ mid , strings .Repeat ("─" , colInstalled + 2 ),
463+ mid , strings .Repeat ("─" , colVersion + 2 ),
464+ mid , strings .Repeat ("─" , colUpdated + 2 ),
465+ mid , strings .Repeat ("─" , colFreq + 2 ),
466+ right )
467+ }
468+
408469 // Top border
409- fmt .Printf ("┌%s┬%s┬%s┬%s┬%s┬%s┐\n " ,
410- strings .Repeat ("─" , colTool + 2 ),
411- strings .Repeat ("─" , col24h + 2 ),
412- strings .Repeat ("─" , colVersion + 2 ),
413- strings .Repeat ("─" , colPrevious + 2 ),
414- strings .Repeat ("─" , colUpdated + 2 ),
415- strings .Repeat ("─" , colFreq + 2 ))
470+ fmt .Print (border ("┌" , "┬" , "┐" ))
416471
417472 // Header row
418473 fmt .Printf ("│ %-*s │ %-*s │ %-*s │ %-*s │ %-*s │ %-*s │\n " ,
419474 colTool , "Tool" ,
420475 col24h , "24h" ,
421- colVersion , "Version " ,
422- colPrevious , "Previous " ,
476+ colInstalled , "Installed " ,
477+ colVersion , "Latest " ,
423478 colUpdated , "Updated" ,
424479 colFreq , "Vers. Release Freq." )
425480
426481 // Header separator
427- fmt .Printf ("├%s┼%s┼%s┼%s┼%s┼%s┤\n " ,
428- strings .Repeat ("─" , colTool + 2 ),
429- strings .Repeat ("─" , col24h + 2 ),
430- strings .Repeat ("─" , colVersion + 2 ),
431- strings .Repeat ("─" , colPrevious + 2 ),
432- strings .Repeat ("─" , colUpdated + 2 ),
433- strings .Repeat ("─" , colFreq + 2 ))
482+ fmt .Print (border ("├" , "┼" , "┤" ))
434483
435484 // Data rows
436485 for _ , e := range statusEntries {
@@ -441,20 +490,14 @@ func runStatusCommand(jsonOutput bool) {
441490 fmt .Printf ("│ %-*s │ %s │ %-*s │ %-*s │ %-*s │ %-*s │\n " ,
442491 colTool , truncateString (e .Name , colTool ),
443492 recentMarker ,
493+ colInstalled , truncateString (e .InstalledVersion , colInstalled ),
444494 colVersion , truncateString (e .Version , colVersion ),
445- colPrevious , truncateString (e .PreviousVersion , colPrevious ),
446495 colUpdated , e .UpdatedAgo ,
447496 colFreq , e .AvgReleaseFreq )
448497 }
449498
450499 // Bottom border
451- fmt .Printf ("└%s┴%s┴%s┴%s┴%s┴%s┘\n " ,
452- strings .Repeat ("─" , colTool + 2 ),
453- strings .Repeat ("─" , col24h + 2 ),
454- strings .Repeat ("─" , colVersion + 2 ),
455- strings .Repeat ("─" , colPrevious + 2 ),
456- strings .Repeat ("─" , colUpdated + 2 ),
457- strings .Repeat ("─" , colFreq + 2 ))
500+ fmt .Print (border ("└" , "┴" , "┘" ))
458501}
459502
460503func truncateString (s string , maxLen int ) string {
0 commit comments