Skip to content

Commit 06c9df6

Browse files
arimxyerclaude
andcommitted
Add status command with table view
New `aic status` command displays a table of all tools showing: - Latest and previous version numbers (truncated if too long) - 24h update indicator with [✓] marker - Relative time since last update - Average release frequency (calculated from last 10 releases) Table is sorted by most recently updated and supports -json flag. Also adds project mise.toml with build/run/test tasks. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 244c213 commit 06c9df6

File tree

2 files changed

+274
-2
lines changed

2 files changed

+274
-2
lines changed

main.go

Lines changed: 260 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ func main() {
9393
os.Exit(0)
9494
}
9595

96+
if args[0] == "status" {
97+
var jsonOutput bool
98+
for i := 1; i < len(args); i++ {
99+
if args[i] == "-json" || args[i] == "--json" {
100+
jsonOutput = true
101+
}
102+
}
103+
runStatusCommand(jsonOutput)
104+
os.Exit(0)
105+
}
106+
96107
sourceName := args[0]
97108
source, ok := sources[sourceName]
98109
if !ok {
@@ -169,15 +180,17 @@ func main() {
169180
func printUsage() {
170181
fmt.Fprintf(os.Stderr, "aic - AI Coding Agent Changelog Viewer\n\n")
171182
fmt.Fprintf(os.Stderr, "Usage: aic <source> [flags]\n")
172-
fmt.Fprintf(os.Stderr, " aic latest [flags]\n\n")
183+
fmt.Fprintf(os.Stderr, " aic latest [flags]\n")
184+
fmt.Fprintf(os.Stderr, " aic status [flags]\n\n")
173185
fmt.Fprintf(os.Stderr, "Sources:\n")
174186
fmt.Fprintf(os.Stderr, " claude Claude Code (Anthropic)\n")
175187
fmt.Fprintf(os.Stderr, " codex Codex CLI (OpenAI)\n")
176188
fmt.Fprintf(os.Stderr, " opencode OpenCode (SST)\n")
177189
fmt.Fprintf(os.Stderr, " gemini Gemini CLI (Google)\n")
178190
fmt.Fprintf(os.Stderr, " copilot Copilot CLI (GitHub)\n\n")
179191
fmt.Fprintf(os.Stderr, "Commands:\n")
180-
fmt.Fprintf(os.Stderr, " latest Show releases from all sources in last 24h\n\n")
192+
fmt.Fprintf(os.Stderr, " latest Show releases from all sources in last 24h\n")
193+
fmt.Fprintf(os.Stderr, " status Show status table of all sources\n\n")
181194
fmt.Fprintf(os.Stderr, "Flags:\n")
182195
fmt.Fprintf(os.Stderr, " -json Output as JSON\n")
183196
fmt.Fprintf(os.Stderr, " -md Output as markdown\n")
@@ -191,6 +204,7 @@ func printUsage() {
191204
fmt.Fprintf(os.Stderr, " aic opencode -list # List OpenCode versions\n")
192205
fmt.Fprintf(os.Stderr, " aic gemini -version 0.21.0 # Specific Gemini version\n")
193206
fmt.Fprintf(os.Stderr, " aic latest # All releases in last 24h\n")
207+
fmt.Fprintf(os.Stderr, " aic status # Status table of all tools\n")
194208
}
195209

196210
func runLatestCommand(jsonOutput bool) {
@@ -263,6 +277,250 @@ func runLatestCommand(jsonOutput bool) {
263277
}
264278
}
265279

280+
func runStatusCommand(jsonOutput bool) {
281+
type statusResult struct {
282+
source string
283+
displayName string
284+
entries []ChangelogEntry
285+
err error
286+
}
287+
288+
results := make(chan statusResult, len(sources))
289+
var wg sync.WaitGroup
290+
291+
// Fetch up to 10 entries from each source concurrently
292+
for name, src := range sources {
293+
wg.Add(1)
294+
go func(name string, src Source) {
295+
defer wg.Done()
296+
entries, err := src.FetchFunc()
297+
results <- statusResult{
298+
source: name,
299+
displayName: src.DisplayName,
300+
entries: entries,
301+
err: err,
302+
}
303+
}(name, src)
304+
}
305+
306+
go func() {
307+
wg.Wait()
308+
close(results)
309+
}()
310+
311+
type statusEntry struct {
312+
Name string `json:"name"`
313+
Version string `json:"version"`
314+
PreviousVersion string `json:"previous_version"`
315+
UpdatedAgo string `json:"updated_ago"`
316+
UpdatedRecently bool `json:"updated_recently"`
317+
AvgReleaseFreq string `json:"avg_release_freq"`
318+
releasedAt time.Time
319+
}
320+
321+
var statusEntries []statusEntry
322+
cutoff := time.Now().Add(-24 * time.Hour)
323+
324+
for r := range results {
325+
if r.err != nil {
326+
fmt.Fprintf(os.Stderr, "Warning: Failed to fetch %s: %v\n", r.displayName, r.err)
327+
continue
328+
}
329+
330+
if len(r.entries) == 0 {
331+
continue
332+
}
333+
334+
entry := statusEntry{
335+
Name: r.displayName,
336+
Version: r.entries[0].Version,
337+
PreviousVersion: "-",
338+
UpdatedAgo: "-",
339+
UpdatedRecently: false,
340+
AvgReleaseFreq: "-",
341+
releasedAt: r.entries[0].ReleasedAt,
342+
}
343+
344+
if len(r.entries) > 1 {
345+
entry.PreviousVersion = r.entries[1].Version
346+
}
347+
348+
if !r.entries[0].ReleasedAt.IsZero() {
349+
entry.UpdatedAgo = formatRelativeTime(r.entries[0].ReleasedAt)
350+
entry.UpdatedRecently = r.entries[0].ReleasedAt.After(cutoff)
351+
}
352+
353+
// Calculate average release frequency from up to 10 entries
354+
entry.AvgReleaseFreq = calculateAvgReleaseFreq(r.entries)
355+
356+
statusEntries = append(statusEntries, entry)
357+
}
358+
359+
// Sort by most recently updated
360+
sort.Slice(statusEntries, func(i, j int) bool {
361+
if statusEntries[i].releasedAt.IsZero() && statusEntries[j].releasedAt.IsZero() {
362+
return statusEntries[i].Name < statusEntries[j].Name
363+
}
364+
if statusEntries[i].releasedAt.IsZero() {
365+
return false
366+
}
367+
if statusEntries[j].releasedAt.IsZero() {
368+
return true
369+
}
370+
return statusEntries[i].releasedAt.After(statusEntries[j].releasedAt)
371+
})
372+
373+
if jsonOutput {
374+
encoder := json.NewEncoder(os.Stdout)
375+
encoder.SetIndent("", " ")
376+
encoder.Encode(statusEntries)
377+
return
378+
}
379+
380+
// Print table with borders
381+
// Column widths
382+
const (
383+
colTool = 20
384+
col24h = 3
385+
colVersion = 12
386+
colPrevious = 12
387+
colUpdated = 10
388+
colFreq = 19
389+
)
390+
391+
// Top border
392+
fmt.Printf("┌%s┬%s┬%s┬%s┬%s┬%s┐\n",
393+
strings.Repeat("─", colTool+2),
394+
strings.Repeat("─", col24h+2),
395+
strings.Repeat("─", colVersion+2),
396+
strings.Repeat("─", colPrevious+2),
397+
strings.Repeat("─", colUpdated+2),
398+
strings.Repeat("─", colFreq+2))
399+
400+
// Header row
401+
fmt.Printf("│ %-*s │ %-*s │ %-*s │ %-*s │ %-*s │ %-*s │\n",
402+
colTool, "Tool",
403+
col24h, "24h",
404+
colVersion, "Version",
405+
colPrevious, "Previous",
406+
colUpdated, "Updated",
407+
colFreq, "Vers. Release Freq.")
408+
409+
// Header separator
410+
fmt.Printf("├%s┼%s┼%s┼%s┼%s┼%s┤\n",
411+
strings.Repeat("─", colTool+2),
412+
strings.Repeat("─", col24h+2),
413+
strings.Repeat("─", colVersion+2),
414+
strings.Repeat("─", colPrevious+2),
415+
strings.Repeat("─", colUpdated+2),
416+
strings.Repeat("─", colFreq+2))
417+
418+
// Data rows
419+
for _, e := range statusEntries {
420+
recentMarker := " "
421+
if e.UpdatedRecently {
422+
recentMarker = "[✓]"
423+
}
424+
fmt.Printf("│ %-*s │ %s │ %-*s │ %-*s │ %-*s │ %-*s │\n",
425+
colTool, truncateString(e.Name, colTool),
426+
recentMarker,
427+
colVersion, truncateString(e.Version, colVersion),
428+
colPrevious, truncateString(e.PreviousVersion, colPrevious),
429+
colUpdated, e.UpdatedAgo,
430+
colFreq, e.AvgReleaseFreq)
431+
}
432+
433+
// Bottom border
434+
fmt.Printf("└%s┴%s┴%s┴%s┴%s┴%s┘\n",
435+
strings.Repeat("─", colTool+2),
436+
strings.Repeat("─", col24h+2),
437+
strings.Repeat("─", colVersion+2),
438+
strings.Repeat("─", colPrevious+2),
439+
strings.Repeat("─", colUpdated+2),
440+
strings.Repeat("─", colFreq+2))
441+
}
442+
443+
func truncateString(s string, maxLen int) string {
444+
if len(s) <= maxLen {
445+
return s
446+
}
447+
if maxLen <= 3 {
448+
return s[:maxLen]
449+
}
450+
return s[:maxLen-3] + "..."
451+
}
452+
453+
func formatRelativeTime(t time.Time) string {
454+
if t.IsZero() {
455+
return "-"
456+
}
457+
458+
duration := time.Since(t)
459+
460+
minutes := int(duration.Minutes())
461+
hours := int(duration.Hours())
462+
days := hours / 24
463+
weeks := days / 7
464+
months := days / 30
465+
466+
if minutes < 60 {
467+
return fmt.Sprintf("%dm ago", minutes)
468+
}
469+
if hours < 24 {
470+
return fmt.Sprintf("%dh ago", hours)
471+
}
472+
if days < 7 {
473+
return fmt.Sprintf("%dd ago", days)
474+
}
475+
if weeks < 4 {
476+
return fmt.Sprintf("%dw ago", weeks)
477+
}
478+
return fmt.Sprintf("%dmo ago", months)
479+
}
480+
481+
func calculateAvgReleaseFreq(entries []ChangelogEntry) string {
482+
// Need at least 2 entries with valid dates to calculate average
483+
var validEntries []ChangelogEntry
484+
for _, e := range entries {
485+
if !e.ReleasedAt.IsZero() {
486+
validEntries = append(validEntries, e)
487+
}
488+
if len(validEntries) >= 10 {
489+
break
490+
}
491+
}
492+
493+
if len(validEntries) < 2 {
494+
return "-"
495+
}
496+
497+
// Calculate intervals between consecutive releases
498+
var totalDuration time.Duration
499+
for i := 0; i < len(validEntries)-1; i++ {
500+
interval := validEntries[i].ReleasedAt.Sub(validEntries[i+1].ReleasedAt)
501+
totalDuration += interval
502+
}
503+
504+
avgDuration := totalDuration / time.Duration(len(validEntries)-1)
505+
506+
// Format as relative time
507+
hours := int(avgDuration.Hours())
508+
days := hours / 24
509+
weeks := days / 7
510+
months := days / 30
511+
512+
if days < 1 {
513+
return fmt.Sprintf("~%dh", hours)
514+
}
515+
if days < 7 {
516+
return fmt.Sprintf("~%dd", days)
517+
}
518+
if weeks < 4 {
519+
return fmt.Sprintf("~%dw", weeks)
520+
}
521+
return fmt.Sprintf("~%dmo", months)
522+
}
523+
266524
func fetchClaudeChangelog() ([]ChangelogEntry, error) {
267525
url := "https://raw.githubusercontent.com/anthropics/claude-code/main/CHANGELOG.md"
268526
content, err := httpGet(url)

mise.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[tools]
2+
go = "latest"
3+
4+
[tasks.build]
5+
description = "Build the aic binary"
6+
run = "go build -o aic ."
7+
8+
[tasks.run]
9+
description = "Run aic with arguments"
10+
run = "go run . {{arg(name='args', var=true)}}"
11+
12+
[tasks.test]
13+
description = "Run tests"
14+
run = "go test ./..."

0 commit comments

Comments
 (0)