cricinfo-cli-go 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/AGENTS.md +63 -0
  2. package/CONTRIBUTORS.md +75 -0
  3. package/LICENSE +21 -0
  4. package/Makefile +131 -0
  5. package/README.md +130 -0
  6. package/bin/cricinfo.js +44 -0
  7. package/cmd/cricinfo/main.go +15 -0
  8. package/go.mod +10 -0
  9. package/go.sum +10 -0
  10. package/internal/app/app.go +11 -0
  11. package/internal/app/app_test.go +122 -0
  12. package/internal/buildinfo/buildinfo.go +16 -0
  13. package/internal/cli/analysis.go +262 -0
  14. package/internal/cli/analysis_test.go +175 -0
  15. package/internal/cli/competitions.go +154 -0
  16. package/internal/cli/competitions_test.go +165 -0
  17. package/internal/cli/leagues.go +297 -0
  18. package/internal/cli/leagues_test.go +194 -0
  19. package/internal/cli/matches.go +403 -0
  20. package/internal/cli/matches_test.go +413 -0
  21. package/internal/cli/players.go +263 -0
  22. package/internal/cli/players_test.go +384 -0
  23. package/internal/cli/root.go +141 -0
  24. package/internal/cli/search.go +119 -0
  25. package/internal/cli/teams.go +214 -0
  26. package/internal/cli/teams_test.go +192 -0
  27. package/internal/cricinfo/analysis.go +1401 -0
  28. package/internal/cricinfo/analysis_phase15_test.go +267 -0
  29. package/internal/cricinfo/client.go +471 -0
  30. package/internal/cricinfo/client_test.go +280 -0
  31. package/internal/cricinfo/cmd/fixture-refresh/main.go +145 -0
  32. package/internal/cricinfo/competitions.go +405 -0
  33. package/internal/cricinfo/competitions_phase13_test.go +234 -0
  34. package/internal/cricinfo/coverage_ledger.go +122 -0
  35. package/internal/cricinfo/coverage_ledger_test.go +253 -0
  36. package/internal/cricinfo/decode.go +115 -0
  37. package/internal/cricinfo/decode_test.go +100 -0
  38. package/internal/cricinfo/entity_index.go +618 -0
  39. package/internal/cricinfo/entity_index_test.go +175 -0
  40. package/internal/cricinfo/fixture_matrix.go +243 -0
  41. package/internal/cricinfo/fixture_matrix_test.go +49 -0
  42. package/internal/cricinfo/fixtures_test.go +264 -0
  43. package/internal/cricinfo/historical_hydration.go +1641 -0
  44. package/internal/cricinfo/historical_phase14_test.go +542 -0
  45. package/internal/cricinfo/leagues.go +1210 -0
  46. package/internal/cricinfo/leagues_phase12_test.go +324 -0
  47. package/internal/cricinfo/live_leagues_test.go +169 -0
  48. package/internal/cricinfo/live_matches_test.go +203 -0
  49. package/internal/cricinfo/live_matrix_test.go +118 -0
  50. package/internal/cricinfo/live_players_test.go +122 -0
  51. package/internal/cricinfo/live_search_test.go +86 -0
  52. package/internal/cricinfo/live_smoke_test.go +213 -0
  53. package/internal/cricinfo/live_teams_test.go +104 -0
  54. package/internal/cricinfo/matches.go +1508 -0
  55. package/internal/cricinfo/matches_phase7_test.go +207 -0
  56. package/internal/cricinfo/matches_phase9_test.go +253 -0
  57. package/internal/cricinfo/normalize_entities.go +1727 -0
  58. package/internal/cricinfo/normalize_leagues.go +346 -0
  59. package/internal/cricinfo/players.go +1332 -0
  60. package/internal/cricinfo/players_phase10_test.go +174 -0
  61. package/internal/cricinfo/players_phase11_test.go +373 -0
  62. package/internal/cricinfo/render_contract.go +1088 -0
  63. package/internal/cricinfo/render_phase4_test.go +633 -0
  64. package/internal/cricinfo/renderer.go +1689 -0
  65. package/internal/cricinfo/resolver.go +813 -0
  66. package/internal/cricinfo/resolver_test.go +244 -0
  67. package/internal/cricinfo/teams.go +603 -0
  68. package/internal/cricinfo/teams_phase8_test.go +231 -0
  69. package/internal/cricinfo/testdata/fixtures/README.md +43 -0
  70. package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/broadcasts.json +11 -0
  71. package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/officials.json +150 -0
  72. package/internal/cricinfo/testdata/fixtures/details-plays/detail-110.json +157 -0
  73. package/internal/cricinfo/testdata/fixtures/details-plays/detail-52545007.json +145 -0
  74. package/internal/cricinfo/testdata/fixtures/details-plays/detail-52559021.json +143 -0
  75. package/internal/cricinfo/testdata/fixtures/details-plays/plays.json +15 -0
  76. package/internal/cricinfo/testdata/fixtures/endpoint-matrix.tsv +19 -0
  77. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow-1.json +12 -0
  78. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow.json +42 -0
  79. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/innings-1-2.json +38 -0
  80. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnership-1.json +31 -0
  81. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnerships.json +42 -0
  82. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-offdays.json +20 -0
  83. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-ondays.json +21 -0
  84. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar.json +14 -0
  85. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-2025.json +13 -0
  86. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-group-1.json +13 -0
  87. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-groups.json +11 -0
  88. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-type-1.json +13 -0
  89. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-types.json +11 -0
  90. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/seasons.json +30 -0
  91. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-item-1.json +72 -0
  92. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-root.json +3 -0
  93. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings.json +15 -0
  94. package/internal/cricinfo/testdata/fixtures/matches-competitions/competition.json +460 -0
  95. package/internal/cricinfo/testdata/fixtures/matches-competitions/event-1529474.json +86 -0
  96. package/internal/cricinfo/testdata/fixtures/matches-competitions/matchcards-1527966.json +368 -0
  97. package/internal/cricinfo/testdata/fixtures/matches-competitions/situation-1529474.json +10 -0
  98. package/internal/cricinfo/testdata/fixtures/players/athlete-1361257-statistics.json +126 -0
  99. package/internal/cricinfo/testdata/fixtures/players/athlete-1361257.json +113 -0
  100. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-1-statistics-0.json +208 -0
  101. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-2-statistics-0.json +252 -0
  102. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores.json +74 -0
  103. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-statistics-0.json +1008 -0
  104. package/internal/cricinfo/testdata/fixtures/root-discovery/events.json +72 -0
  105. package/internal/cricinfo/testdata/fixtures/root-discovery/root.json +28 -0
  106. package/internal/cricinfo/testdata/fixtures/team-competitor/competitor-789643.json +40 -0
  107. package/internal/cricinfo/testdata/fixtures/team-competitor/leaders-789643.json +353 -0
  108. package/internal/cricinfo/testdata/fixtures/team-competitor/records-789643.json +91 -0
  109. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772-object.json +231 -0
  110. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772.json +235 -0
  111. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-789643.json +322 -0
  112. package/internal/cricinfo/testdata/fixtures/team-competitor/scores-789643.json +19 -0
  113. package/internal/cricinfo/testdata/fixtures/team-competitor/statistics-789643.json +629 -0
  114. package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643-athletes.json +7 -0
  115. package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643.json +67 -0
  116. package/internal/cricinfo/testdata/golden/match-empty.golden +1 -0
  117. package/internal/cricinfo/testdata/golden/match-list.golden +2 -0
  118. package/internal/cricinfo/testdata/golden/match-partial.golden +3 -0
  119. package/internal/cricinfo/types.go +54 -0
  120. package/package.json +51 -0
  121. package/scripts/postinstall.js +153 -0
@@ -0,0 +1,165 @@
1
+ package cli
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "strings"
8
+ "testing"
9
+
10
+ "github.com/amxv/cricinfo-cli/internal/cricinfo"
11
+ )
12
+
13
+ type fakeCompetitionService struct {
14
+ showResult cricinfo.NormalizedResult
15
+ officialsResult cricinfo.NormalizedResult
16
+ broadcastsResult cricinfo.NormalizedResult
17
+ ticketsResult cricinfo.NormalizedResult
18
+ oddsResult cricinfo.NormalizedResult
19
+ metadataResult cricinfo.NormalizedResult
20
+
21
+ showQueries []string
22
+ officialsQueries []string
23
+ broadcastsQueries []string
24
+ metadataQueries []string
25
+ showOpts []cricinfo.CompetitionLookupOptions
26
+ officialsOpts []cricinfo.CompetitionLookupOptions
27
+ broadcastsOpts []cricinfo.CompetitionLookupOptions
28
+ metadataOpts []cricinfo.CompetitionLookupOptions
29
+ }
30
+
31
+ func (f *fakeCompetitionService) Close() error { return nil }
32
+
33
+ func (f *fakeCompetitionService) Show(_ context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error) {
34
+ f.showQueries = append(f.showQueries, query)
35
+ f.showOpts = append(f.showOpts, opts)
36
+ return f.showResult, nil
37
+ }
38
+
39
+ func (f *fakeCompetitionService) Officials(_ context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error) {
40
+ f.officialsQueries = append(f.officialsQueries, query)
41
+ f.officialsOpts = append(f.officialsOpts, opts)
42
+ return f.officialsResult, nil
43
+ }
44
+
45
+ func (f *fakeCompetitionService) Broadcasts(_ context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error) {
46
+ f.broadcastsQueries = append(f.broadcastsQueries, query)
47
+ f.broadcastsOpts = append(f.broadcastsOpts, opts)
48
+ return f.broadcastsResult, nil
49
+ }
50
+
51
+ func (f *fakeCompetitionService) Tickets(context.Context, string, cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error) {
52
+ return f.ticketsResult, nil
53
+ }
54
+
55
+ func (f *fakeCompetitionService) Odds(context.Context, string, cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error) {
56
+ return f.oddsResult, nil
57
+ }
58
+
59
+ func (f *fakeCompetitionService) Metadata(_ context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error) {
60
+ f.metadataQueries = append(f.metadataQueries, query)
61
+ f.metadataOpts = append(f.metadataOpts, opts)
62
+ return f.metadataResult, nil
63
+ }
64
+
65
+ func TestCompetitionsCommandsRenderAndPreserveLookupInputs(t *testing.T) {
66
+ service := &fakeCompetitionService{
67
+ showResult: cricinfo.NewDataResult(cricinfo.EntityCompetition, cricinfo.Competition{
68
+ ID: "1529474",
69
+ CompetitionID: "1529474",
70
+ LeagueID: "19138",
71
+ EventID: "1529474",
72
+ Description: "3rd Match",
73
+ ShortDescription: "3rd Match",
74
+ OfficialsRef: "http://core.espnuk.org/v2/sports/cricket/leagues/19138/events/1529474/competitions/1529474/officials",
75
+ }),
76
+ officialsResult: cricinfo.NewListResult(cricinfo.EntityCompOfficial, []any{
77
+ cricinfo.CompetitionMetadataEntry{DisplayName: "Anil Chaugai", Role: "umpire", Order: 1},
78
+ }),
79
+ broadcastsResult: cricinfo.NewListResult(cricinfo.EntityCompBroadcast, []any{}),
80
+ ticketsResult: cricinfo.NewListResult(cricinfo.EntityCompTicket, []any{}),
81
+ oddsResult: cricinfo.NewListResult(cricinfo.EntityCompOdds, []any{
82
+ cricinfo.CompetitionMetadataEntry{Name: "Win Probability", Value: "0.61"},
83
+ }),
84
+ metadataResult: cricinfo.NewDataResult(cricinfo.EntityCompMetadata, cricinfo.CompetitionMetadataSummary{
85
+ Competition: cricinfo.Competition{ID: "1529474"},
86
+ Officials: []cricinfo.CompetitionMetadataEntry{{DisplayName: "Anil Chaugai"}},
87
+ Broadcasts: []cricinfo.CompetitionMetadataEntry{},
88
+ Tickets: []cricinfo.CompetitionMetadataEntry{},
89
+ Odds: []cricinfo.CompetitionMetadataEntry{{Name: "Win Probability", Value: "0.61"}},
90
+ }),
91
+ }
92
+
93
+ originalFactory := newCompetitionService
94
+ newCompetitionService = func() (competitionCommandService, error) { return service, nil }
95
+ defer func() {
96
+ newCompetitionService = originalFactory
97
+ }()
98
+
99
+ var showOut bytes.Buffer
100
+ var showErr bytes.Buffer
101
+ if err := Run([]string{"competitions", "show", "3rd", "Match", "--league", "19138", "--format", "json"}, &showOut, &showErr); err != nil {
102
+ t.Fatalf("Run competitions show error: %v", err)
103
+ }
104
+
105
+ var showPayload map[string]any
106
+ if err := json.Unmarshal(showOut.Bytes(), &showPayload); err != nil {
107
+ t.Fatalf("decode competitions show payload: %v", err)
108
+ }
109
+ if showPayload["kind"] != string(cricinfo.EntityCompetition) {
110
+ t.Fatalf("expected kind %q, got %#v", cricinfo.EntityCompetition, showPayload["kind"])
111
+ }
112
+
113
+ var officialsOut bytes.Buffer
114
+ var officialsErr bytes.Buffer
115
+ if err := Run([]string{"competitions", "officials", "1529474", "--league", "19138", "--format", "text"}, &officialsOut, &officialsErr); err != nil {
116
+ t.Fatalf("Run competitions officials error: %v", err)
117
+ }
118
+ if !strings.Contains(officialsOut.String(), "Anil Chaugai") {
119
+ t.Fatalf("expected officials text output to include official name, got %q", officialsOut.String())
120
+ }
121
+
122
+ var broadcastsOut bytes.Buffer
123
+ var broadcastsErr bytes.Buffer
124
+ if err := Run([]string{"competitions", "broadcasts", "1529474", "--league", "19138", "--format", "text"}, &broadcastsOut, &broadcastsErr); err != nil {
125
+ t.Fatalf("Run competitions broadcasts error: %v", err)
126
+ }
127
+ if !strings.Contains(strings.ToLower(broadcastsOut.String()), "no competition broadcasts found") {
128
+ t.Fatalf("expected clean zero-result message for broadcasts, got %q", broadcastsOut.String())
129
+ }
130
+
131
+ var metadataOut bytes.Buffer
132
+ var metadataErr bytes.Buffer
133
+ if err := Run([]string{"competitions", "metadata", "3rd", "Match", "--league", "19138", "--format", "json"}, &metadataOut, &metadataErr); err != nil {
134
+ t.Fatalf("Run competitions metadata error: %v", err)
135
+ }
136
+ var metadataPayload map[string]any
137
+ if err := json.Unmarshal(metadataOut.Bytes(), &metadataPayload); err != nil {
138
+ t.Fatalf("decode competitions metadata payload: %v", err)
139
+ }
140
+ if metadataPayload["kind"] != string(cricinfo.EntityCompMetadata) {
141
+ t.Fatalf("expected kind %q, got %#v", cricinfo.EntityCompMetadata, metadataPayload["kind"])
142
+ }
143
+
144
+ if len(service.showQueries) != 1 || service.showQueries[0] != "3rd Match" {
145
+ t.Fatalf("expected joined show query, got %+v", service.showQueries)
146
+ }
147
+ if len(service.officialsQueries) != 1 || service.officialsQueries[0] != "1529474" {
148
+ t.Fatalf("expected officials query 1529474, got %+v", service.officialsQueries)
149
+ }
150
+ if len(service.metadataQueries) != 1 || service.metadataQueries[0] != "3rd Match" {
151
+ t.Fatalf("expected joined metadata query, got %+v", service.metadataQueries)
152
+ }
153
+ if len(service.showOpts) != 1 || service.showOpts[0].LeagueID != "19138" {
154
+ t.Fatalf("expected show league option to propagate, got %+v", service.showOpts)
155
+ }
156
+ if len(service.officialsOpts) != 1 || service.officialsOpts[0].LeagueID != "19138" {
157
+ t.Fatalf("expected officials league option to propagate, got %+v", service.officialsOpts)
158
+ }
159
+ if len(service.broadcastsOpts) != 1 || service.broadcastsOpts[0].LeagueID != "19138" {
160
+ t.Fatalf("expected broadcasts league option to propagate, got %+v", service.broadcastsOpts)
161
+ }
162
+ if len(service.metadataOpts) != 1 || service.metadataOpts[0].LeagueID != "19138" {
163
+ t.Fatalf("expected metadata league option to propagate, got %+v", service.metadataOpts)
164
+ }
165
+ }
@@ -0,0 +1,297 @@
1
+ package cli
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "strings"
7
+
8
+ "github.com/amxv/cricinfo-cli/internal/cricinfo"
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ type leagueCommandService interface {
13
+ Close() error
14
+ List(ctx context.Context, opts cricinfo.LeagueListOptions) (cricinfo.NormalizedResult, error)
15
+ Show(ctx context.Context, leagueQuery string) (cricinfo.NormalizedResult, error)
16
+ Events(ctx context.Context, leagueQuery string, opts cricinfo.LeagueListOptions) (cricinfo.NormalizedResult, error)
17
+ Calendar(ctx context.Context, leagueQuery string) (cricinfo.NormalizedResult, error)
18
+ Athletes(ctx context.Context, leagueQuery string, opts cricinfo.LeagueListOptions) (cricinfo.NormalizedResult, error)
19
+ Standings(ctx context.Context, leagueQuery string) (cricinfo.NormalizedResult, error)
20
+ Seasons(ctx context.Context, leagueQuery string) (cricinfo.NormalizedResult, error)
21
+ SeasonShow(ctx context.Context, leagueQuery string, opts cricinfo.SeasonLookupOptions) (cricinfo.NormalizedResult, error)
22
+ SeasonTypes(ctx context.Context, leagueQuery string, opts cricinfo.SeasonLookupOptions) (cricinfo.NormalizedResult, error)
23
+ SeasonGroups(ctx context.Context, leagueQuery string, opts cricinfo.SeasonLookupOptions) (cricinfo.NormalizedResult, error)
24
+ }
25
+
26
+ type leagueRuntimeOptions struct {
27
+ limit int
28
+ season string
29
+ typ string
30
+ }
31
+
32
+ var newLeagueService = func() (leagueCommandService, error) {
33
+ return cricinfo.NewLeagueService(cricinfo.LeagueServiceConfig{})
34
+ }
35
+
36
+ func newLeaguesCommand(global *globalOptions) *cobra.Command {
37
+ opts := &leagueRuntimeOptions{}
38
+
39
+ cmd := &cobra.Command{
40
+ Use: "leagues",
41
+ Short: "League discovery and navigation across events, calendar, athletes, standings, and seasons.",
42
+ Long: strings.Join([]string{
43
+ "Resolve leagues by ID/ref/alias and drill into events, calendar views, athletes, standings, and season navigation.",
44
+ "",
45
+ "Next steps:",
46
+ " cricinfo leagues list",
47
+ " cricinfo leagues show <league>",
48
+ " cricinfo leagues events <league>",
49
+ " cricinfo leagues calendar <league>",
50
+ " cricinfo leagues athletes <league>",
51
+ " cricinfo leagues standings <league>",
52
+ " cricinfo leagues seasons <league>",
53
+ }, "\n"),
54
+ Args: cobra.NoArgs,
55
+ RunE: func(cmd *cobra.Command, _ []string) error {
56
+ return cmd.Help()
57
+ },
58
+ }
59
+
60
+ listCmd := &cobra.Command{
61
+ Use: "list",
62
+ Short: "List leagues from /leagues",
63
+ Args: cobra.NoArgs,
64
+ RunE: func(cmd *cobra.Command, _ []string) error {
65
+ return runLeagueCommand(cmd, global, func(ctx context.Context, service leagueCommandService) (cricinfo.NormalizedResult, error) {
66
+ return service.List(ctx, cricinfo.LeagueListOptions{Limit: opts.limit})
67
+ })
68
+ },
69
+ }
70
+ listCmd.Flags().IntVar(&opts.limit, "limit", 20, "Maximum number of leagues to return")
71
+
72
+ showCmd := &cobra.Command{
73
+ Use: "show <league>",
74
+ Short: "Show one league summary",
75
+ Args: cobra.MinimumNArgs(1),
76
+ RunE: func(cmd *cobra.Command, args []string) error {
77
+ leagueQuery := strings.TrimSpace(strings.Join(args, " "))
78
+ return runLeagueCommand(cmd, global, func(ctx context.Context, service leagueCommandService) (cricinfo.NormalizedResult, error) {
79
+ return service.Show(ctx, leagueQuery)
80
+ })
81
+ },
82
+ }
83
+
84
+ eventsCmd := &cobra.Command{
85
+ Use: "events <league>",
86
+ Short: "List matches/events for one league",
87
+ Args: cobra.MinimumNArgs(1),
88
+ RunE: func(cmd *cobra.Command, args []string) error {
89
+ leagueQuery := strings.TrimSpace(strings.Join(args, " "))
90
+ return runLeagueCommand(cmd, global, func(ctx context.Context, service leagueCommandService) (cricinfo.NormalizedResult, error) {
91
+ return service.Events(ctx, leagueQuery, cricinfo.LeagueListOptions{Limit: opts.limit})
92
+ })
93
+ },
94
+ }
95
+ eventsCmd.Flags().IntVar(&opts.limit, "limit", 20, "Maximum number of league events/matches to return")
96
+
97
+ calendarCmd := &cobra.Command{
98
+ Use: "calendar <league>",
99
+ Short: "Show normalized league calendar day entries",
100
+ Args: cobra.MinimumNArgs(1),
101
+ RunE: func(cmd *cobra.Command, args []string) error {
102
+ leagueQuery := strings.TrimSpace(strings.Join(args, " "))
103
+ return runLeagueCommand(cmd, global, func(ctx context.Context, service leagueCommandService) (cricinfo.NormalizedResult, error) {
104
+ return service.Calendar(ctx, leagueQuery)
105
+ })
106
+ },
107
+ }
108
+
109
+ athletesCmd := &cobra.Command{
110
+ Use: "athletes <league>",
111
+ Short: "Show league athlete views, with roster-driven fallback when direct pages are sparse",
112
+ Args: cobra.MinimumNArgs(1),
113
+ RunE: func(cmd *cobra.Command, args []string) error {
114
+ leagueQuery := strings.TrimSpace(strings.Join(args, " "))
115
+ return runLeagueCommand(cmd, global, func(ctx context.Context, service leagueCommandService) (cricinfo.NormalizedResult, error) {
116
+ return service.Athletes(ctx, leagueQuery, cricinfo.LeagueListOptions{Limit: opts.limit})
117
+ })
118
+ },
119
+ }
120
+ athletesCmd.Flags().IntVar(&opts.limit, "limit", 20, "Maximum number of athletes to return")
121
+
122
+ standingsCmd := &cobra.Command{
123
+ Use: "standings <league>",
124
+ Short: "Show normalized standings groups for one league",
125
+ Args: cobra.MinimumNArgs(1),
126
+ RunE: func(cmd *cobra.Command, args []string) error {
127
+ leagueQuery := strings.TrimSpace(strings.Join(args, " "))
128
+ return runLeagueCommand(cmd, global, func(ctx context.Context, service leagueCommandService) (cricinfo.NormalizedResult, error) {
129
+ return service.Standings(ctx, leagueQuery)
130
+ })
131
+ },
132
+ }
133
+
134
+ seasonsCmd := &cobra.Command{
135
+ Use: "seasons <league>",
136
+ Short: "Show season refs for one league",
137
+ Args: cobra.MinimumNArgs(1),
138
+ RunE: func(cmd *cobra.Command, args []string) error {
139
+ leagueQuery := strings.TrimSpace(strings.Join(args, " "))
140
+ return runLeagueCommand(cmd, global, func(ctx context.Context, service leagueCommandService) (cricinfo.NormalizedResult, error) {
141
+ return service.Seasons(ctx, leagueQuery)
142
+ })
143
+ },
144
+ }
145
+
146
+ cmd.AddCommand(
147
+ listCmd,
148
+ showCmd,
149
+ eventsCmd,
150
+ calendarCmd,
151
+ athletesCmd,
152
+ standingsCmd,
153
+ seasonsCmd,
154
+ )
155
+ return cmd
156
+ }
157
+
158
+ func newSeasonsCommand(global *globalOptions) *cobra.Command {
159
+ opts := &leagueRuntimeOptions{}
160
+
161
+ cmd := &cobra.Command{
162
+ Use: "seasons",
163
+ Short: "Season navigation across league season, type, and group hierarchies.",
164
+ Long: strings.Join([]string{
165
+ "Resolve seasons, types, and groups by league while hiding pointer-heavy upstream traversal.",
166
+ "",
167
+ "Next steps:",
168
+ " cricinfo seasons show <league> --season <season>",
169
+ " cricinfo seasons types <league> --season <season>",
170
+ " cricinfo seasons groups <league> --season <season> --type <type>",
171
+ }, "\n"),
172
+ Args: cobra.NoArgs,
173
+ RunE: func(cmd *cobra.Command, _ []string) error {
174
+ return cmd.Help()
175
+ },
176
+ }
177
+
178
+ showCmd := &cobra.Command{
179
+ Use: "show <league>",
180
+ Short: "Show one selected league season",
181
+ Args: cobra.MinimumNArgs(1),
182
+ RunE: func(cmd *cobra.Command, args []string) error {
183
+ if strings.TrimSpace(opts.season) == "" {
184
+ return fmt.Errorf("--season is required")
185
+ }
186
+ leagueQuery := strings.TrimSpace(strings.Join(args, " "))
187
+ return runLeagueCommand(cmd, global, func(ctx context.Context, service leagueCommandService) (cricinfo.NormalizedResult, error) {
188
+ return service.SeasonShow(ctx, leagueQuery, cricinfo.SeasonLookupOptions{
189
+ SeasonQuery: opts.season,
190
+ })
191
+ })
192
+ },
193
+ }
194
+ showCmd.Flags().StringVar(&opts.season, "season", "", "Required: season ID/ref (for example 2025)")
195
+
196
+ typesCmd := &cobra.Command{
197
+ Use: "types <league>",
198
+ Short: "List normalized season types for a selected league season",
199
+ Args: cobra.MinimumNArgs(1),
200
+ RunE: func(cmd *cobra.Command, args []string) error {
201
+ if strings.TrimSpace(opts.season) == "" {
202
+ return fmt.Errorf("--season is required")
203
+ }
204
+ leagueQuery := strings.TrimSpace(strings.Join(args, " "))
205
+ return runLeagueCommand(cmd, global, func(ctx context.Context, service leagueCommandService) (cricinfo.NormalizedResult, error) {
206
+ return service.SeasonTypes(ctx, leagueQuery, cricinfo.SeasonLookupOptions{
207
+ SeasonQuery: opts.season,
208
+ })
209
+ })
210
+ },
211
+ }
212
+ typesCmd.Flags().StringVar(&opts.season, "season", "", "Required: season ID/ref (for example 2025)")
213
+
214
+ groupsCmd := &cobra.Command{
215
+ Use: "groups <league>",
216
+ Short: "List normalized season groups for a selected season type",
217
+ Args: cobra.MinimumNArgs(1),
218
+ RunE: func(cmd *cobra.Command, args []string) error {
219
+ if strings.TrimSpace(opts.season) == "" {
220
+ return fmt.Errorf("--season is required")
221
+ }
222
+ if strings.TrimSpace(opts.typ) == "" {
223
+ return fmt.Errorf("--type is required")
224
+ }
225
+ leagueQuery := strings.TrimSpace(strings.Join(args, " "))
226
+ return runLeagueCommand(cmd, global, func(ctx context.Context, service leagueCommandService) (cricinfo.NormalizedResult, error) {
227
+ return service.SeasonGroups(ctx, leagueQuery, cricinfo.SeasonLookupOptions{
228
+ SeasonQuery: opts.season,
229
+ TypeQuery: opts.typ,
230
+ })
231
+ })
232
+ },
233
+ }
234
+ groupsCmd.Flags().StringVar(&opts.season, "season", "", "Required: season ID/ref (for example 2025)")
235
+ groupsCmd.Flags().StringVar(&opts.typ, "type", "", "Required: type ID/ref/name (for example 1)")
236
+
237
+ cmd.AddCommand(showCmd, typesCmd, groupsCmd)
238
+ return cmd
239
+ }
240
+
241
+ func newStandingsCommand(global *globalOptions) *cobra.Command {
242
+ cmd := &cobra.Command{
243
+ Use: "standings",
244
+ Short: "Standings traversal by league.",
245
+ Long: strings.Join([]string{
246
+ "Traverse league standings routes and normalize ref-heavy standings resources into group entries.",
247
+ "",
248
+ "Next steps:",
249
+ " cricinfo standings show <league>",
250
+ " cricinfo leagues standings <league>",
251
+ }, "\n"),
252
+ Args: cobra.NoArgs,
253
+ RunE: func(cmd *cobra.Command, _ []string) error {
254
+ return cmd.Help()
255
+ },
256
+ }
257
+
258
+ showCmd := &cobra.Command{
259
+ Use: "show <league>",
260
+ Short: "Show normalized standings groups for one league",
261
+ Args: cobra.MinimumNArgs(1),
262
+ RunE: func(cmd *cobra.Command, args []string) error {
263
+ leagueQuery := strings.TrimSpace(strings.Join(args, " "))
264
+ return runLeagueCommand(cmd, global, func(ctx context.Context, service leagueCommandService) (cricinfo.NormalizedResult, error) {
265
+ return service.Standings(ctx, leagueQuery)
266
+ })
267
+ },
268
+ }
269
+
270
+ cmd.AddCommand(showCmd)
271
+ return cmd
272
+ }
273
+
274
+ func runLeagueCommand(
275
+ cmd *cobra.Command,
276
+ global *globalOptions,
277
+ fn func(ctx context.Context, service leagueCommandService) (cricinfo.NormalizedResult, error),
278
+ ) error {
279
+ service, err := newLeagueService()
280
+ if err != nil {
281
+ return err
282
+ }
283
+ defer func() {
284
+ _ = service.Close()
285
+ }()
286
+
287
+ result, err := fn(cmd.Context(), service)
288
+ if err != nil {
289
+ return err
290
+ }
291
+
292
+ return cricinfo.Render(cmd.OutOrStdout(), result, cricinfo.RenderOptions{
293
+ Format: global.format,
294
+ Verbose: global.verbose,
295
+ AllFields: global.allFields,
296
+ })
297
+ }
@@ -0,0 +1,194 @@
1
+ package cli
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "strings"
8
+ "testing"
9
+
10
+ "github.com/amxv/cricinfo-cli/internal/cricinfo"
11
+ )
12
+
13
+ type fakeLeagueService struct {
14
+ listResult cricinfo.NormalizedResult
15
+ showResult cricinfo.NormalizedResult
16
+ eventsResult cricinfo.NormalizedResult
17
+ calendarResult cricinfo.NormalizedResult
18
+ athletesResult cricinfo.NormalizedResult
19
+ standingsResult cricinfo.NormalizedResult
20
+ seasonsResult cricinfo.NormalizedResult
21
+ seasonShow cricinfo.NormalizedResult
22
+ seasonTypes cricinfo.NormalizedResult
23
+ seasonGroups cricinfo.NormalizedResult
24
+
25
+ showQueries []string
26
+ standingsQueries []string
27
+ seasonShowQueries []string
28
+ seasonTypeQueries []string
29
+ seasonGroupQueries []string
30
+ seasonShowOpts []cricinfo.SeasonLookupOptions
31
+ seasonTypeOpts []cricinfo.SeasonLookupOptions
32
+ seasonGroupOpts []cricinfo.SeasonLookupOptions
33
+ }
34
+
35
+ func (f *fakeLeagueService) Close() error { return nil }
36
+
37
+ func (f *fakeLeagueService) List(context.Context, cricinfo.LeagueListOptions) (cricinfo.NormalizedResult, error) {
38
+ return f.listResult, nil
39
+ }
40
+
41
+ func (f *fakeLeagueService) Show(_ context.Context, leagueQuery string) (cricinfo.NormalizedResult, error) {
42
+ f.showQueries = append(f.showQueries, leagueQuery)
43
+ return f.showResult, nil
44
+ }
45
+
46
+ func (f *fakeLeagueService) Events(context.Context, string, cricinfo.LeagueListOptions) (cricinfo.NormalizedResult, error) {
47
+ return f.eventsResult, nil
48
+ }
49
+
50
+ func (f *fakeLeagueService) Calendar(context.Context, string) (cricinfo.NormalizedResult, error) {
51
+ return f.calendarResult, nil
52
+ }
53
+
54
+ func (f *fakeLeagueService) Athletes(context.Context, string, cricinfo.LeagueListOptions) (cricinfo.NormalizedResult, error) {
55
+ return f.athletesResult, nil
56
+ }
57
+
58
+ func (f *fakeLeagueService) Standings(_ context.Context, leagueQuery string) (cricinfo.NormalizedResult, error) {
59
+ f.standingsQueries = append(f.standingsQueries, leagueQuery)
60
+ return f.standingsResult, nil
61
+ }
62
+
63
+ func (f *fakeLeagueService) Seasons(context.Context, string) (cricinfo.NormalizedResult, error) {
64
+ return f.seasonsResult, nil
65
+ }
66
+
67
+ func (f *fakeLeagueService) SeasonShow(_ context.Context, leagueQuery string, opts cricinfo.SeasonLookupOptions) (cricinfo.NormalizedResult, error) {
68
+ f.seasonShowQueries = append(f.seasonShowQueries, leagueQuery)
69
+ f.seasonShowOpts = append(f.seasonShowOpts, opts)
70
+ return f.seasonShow, nil
71
+ }
72
+
73
+ func (f *fakeLeagueService) SeasonTypes(_ context.Context, leagueQuery string, opts cricinfo.SeasonLookupOptions) (cricinfo.NormalizedResult, error) {
74
+ f.seasonTypeQueries = append(f.seasonTypeQueries, leagueQuery)
75
+ f.seasonTypeOpts = append(f.seasonTypeOpts, opts)
76
+ return f.seasonTypes, nil
77
+ }
78
+
79
+ func (f *fakeLeagueService) SeasonGroups(_ context.Context, leagueQuery string, opts cricinfo.SeasonLookupOptions) (cricinfo.NormalizedResult, error) {
80
+ f.seasonGroupQueries = append(f.seasonGroupQueries, leagueQuery)
81
+ f.seasonGroupOpts = append(f.seasonGroupOpts, opts)
82
+ return f.seasonGroups, nil
83
+ }
84
+
85
+ func TestLeaguesAndStandingsCommandsResolveLeagueName(t *testing.T) {
86
+ service := &fakeLeagueService{
87
+ showResult: cricinfo.NewDataResult(cricinfo.EntityLeague, cricinfo.League{
88
+ ID: "19138",
89
+ Name: "Mirwais Nika Provincial 3-Day",
90
+ Slug: "19138",
91
+ }),
92
+ standingsResult: cricinfo.NewListResult(cricinfo.EntityStandingsGroup, []any{
93
+ cricinfo.StandingsGroup{ID: "1", SeasonID: "2026", GroupID: "1"},
94
+ }),
95
+ }
96
+
97
+ originalFactory := newLeagueService
98
+ newLeagueService = func() (leagueCommandService, error) { return service, nil }
99
+ defer func() {
100
+ newLeagueService = originalFactory
101
+ }()
102
+
103
+ var showOut bytes.Buffer
104
+ var showErr bytes.Buffer
105
+ if err := Run([]string{"leagues", "show", "Mirwais", "Nika", "--format", "json"}, &showOut, &showErr); err != nil {
106
+ t.Fatalf("Run leagues show error: %v", err)
107
+ }
108
+ if len(service.showQueries) != 1 || service.showQueries[0] != "Mirwais Nika" {
109
+ t.Fatalf("expected joined league alias query, got %+v", service.showQueries)
110
+ }
111
+
112
+ var showPayload map[string]any
113
+ if err := json.Unmarshal(showOut.Bytes(), &showPayload); err != nil {
114
+ t.Fatalf("decode leagues show payload: %v", err)
115
+ }
116
+ if showPayload["kind"] != string(cricinfo.EntityLeague) {
117
+ t.Fatalf("expected kind %q, got %#v", cricinfo.EntityLeague, showPayload["kind"])
118
+ }
119
+
120
+ var standingsOut bytes.Buffer
121
+ var standingsErr bytes.Buffer
122
+ if err := Run([]string{"standings", "show", "Mirwais", "Nika", "--format", "json"}, &standingsOut, &standingsErr); err != nil {
123
+ t.Fatalf("Run standings show error: %v", err)
124
+ }
125
+ if len(service.standingsQueries) != 1 || service.standingsQueries[0] != "Mirwais Nika" {
126
+ t.Fatalf("expected joined standings league query, got %+v", service.standingsQueries)
127
+ }
128
+ }
129
+
130
+ func TestSeasonsCommandsRequireAndPropagateSelectors(t *testing.T) {
131
+ service := &fakeLeagueService{
132
+ seasonShow: cricinfo.NewDataResult(cricinfo.EntitySeason, cricinfo.Season{
133
+ ID: "2025",
134
+ Year: 2025,
135
+ LeagueID: "19138",
136
+ }),
137
+ seasonTypes: cricinfo.NewListResult(cricinfo.EntitySeasonType, []any{
138
+ cricinfo.SeasonType{ID: "1", SeasonID: "2025", LeagueID: "19138", Name: "Regular Season"},
139
+ }),
140
+ seasonGroups: cricinfo.NewListResult(cricinfo.EntitySeasonGroup, []any{
141
+ cricinfo.SeasonGroup{ID: "1", SeasonID: "2025", TypeID: "1", LeagueID: "1479935"},
142
+ }),
143
+ }
144
+
145
+ originalFactory := newLeagueService
146
+ newLeagueService = func() (leagueCommandService, error) { return service, nil }
147
+ defer func() {
148
+ newLeagueService = originalFactory
149
+ }()
150
+
151
+ var showOut bytes.Buffer
152
+ var showErr bytes.Buffer
153
+ if err := Run([]string{"seasons", "show", "Mirwais", "Nika", "--season", "2025", "--format", "json"}, &showOut, &showErr); err != nil {
154
+ t.Fatalf("Run seasons show error: %v", err)
155
+ }
156
+ if len(service.seasonShowOpts) != 1 || service.seasonShowOpts[0].SeasonQuery != "2025" {
157
+ t.Fatalf("expected season selector to be propagated, got %+v", service.seasonShowOpts)
158
+ }
159
+
160
+ var typesOut bytes.Buffer
161
+ var typesErr bytes.Buffer
162
+ if err := Run([]string{"seasons", "types", "Mirwais", "Nika", "--season", "2025", "--format", "json"}, &typesOut, &typesErr); err != nil {
163
+ t.Fatalf("Run seasons types error: %v", err)
164
+ }
165
+ if len(service.seasonTypeOpts) != 1 || service.seasonTypeOpts[0].SeasonQuery != "2025" {
166
+ t.Fatalf("expected season selector for types, got %+v", service.seasonTypeOpts)
167
+ }
168
+
169
+ var groupsOut bytes.Buffer
170
+ var groupsErr bytes.Buffer
171
+ if err := Run([]string{"seasons", "groups", "Mirwais", "Nika", "--season", "2025", "--type", "1", "--format", "json"}, &groupsOut, &groupsErr); err != nil {
172
+ t.Fatalf("Run seasons groups error: %v", err)
173
+ }
174
+ if len(service.seasonGroupOpts) != 1 {
175
+ t.Fatalf("expected one season groups invocation, got %+v", service.seasonGroupOpts)
176
+ }
177
+ if service.seasonGroupOpts[0].SeasonQuery != "2025" || service.seasonGroupOpts[0].TypeQuery != "1" {
178
+ t.Fatalf("expected season/type selectors to be propagated, got %+v", service.seasonGroupOpts[0])
179
+ }
180
+
181
+ var missingSeasonOut bytes.Buffer
182
+ var missingSeasonErr bytes.Buffer
183
+ err := Run([]string{"seasons", "show", "Mirwais", "Nika"}, &missingSeasonOut, &missingSeasonErr)
184
+ if err == nil || !strings.Contains(err.Error(), "--season is required") {
185
+ t.Fatalf("expected --season required error, got %v", err)
186
+ }
187
+
188
+ var missingTypeOut bytes.Buffer
189
+ var missingTypeErr bytes.Buffer
190
+ err = Run([]string{"seasons", "groups", "Mirwais", "Nika", "--season", "2025"}, &missingTypeOut, &missingTypeErr)
191
+ if err == nil || !strings.Contains(err.Error(), "--type is required") {
192
+ t.Fatalf("expected --type required error, got %v", err)
193
+ }
194
+ }