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,384 @@
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 fakePlayerService struct {
14
+ searchResult cricinfo.NormalizedResult
15
+ profileResult cricinfo.NormalizedResult
16
+ newsResult cricinfo.NormalizedResult
17
+ statsResult cricinfo.NormalizedResult
18
+ careerResult cricinfo.NormalizedResult
19
+ matchResult cricinfo.NormalizedResult
20
+ inningsResult cricinfo.NormalizedResult
21
+ dismissals cricinfo.NormalizedResult
22
+ deliveries cricinfo.NormalizedResult
23
+ bowlingResult cricinfo.NormalizedResult
24
+ battingResult cricinfo.NormalizedResult
25
+
26
+ searchQueries []string
27
+ profileQueries []string
28
+ newsQueries []string
29
+ statsQueries []string
30
+ careerQueries []string
31
+ matchQueries []string
32
+ matchRefs []string
33
+ inningsQueries []string
34
+ inningsRefs []string
35
+ dismissQueries []string
36
+ dismissRefs []string
37
+ deliveryQueries []string
38
+ deliveryRefs []string
39
+ bowlingQueries []string
40
+ bowlingRefs []string
41
+ battingQueries []string
42
+ battingRefs []string
43
+ searchOpts []cricinfo.PlayerLookupOptions
44
+ profileOpts []cricinfo.PlayerLookupOptions
45
+ newsOpts []cricinfo.PlayerLookupOptions
46
+ statsOpts []cricinfo.PlayerLookupOptions
47
+ careerOpts []cricinfo.PlayerLookupOptions
48
+ matchOpts []cricinfo.PlayerLookupOptions
49
+ inningsOpts []cricinfo.PlayerLookupOptions
50
+ dismissOpts []cricinfo.PlayerLookupOptions
51
+ deliveryOpts []cricinfo.PlayerLookupOptions
52
+ bowlingOpts []cricinfo.PlayerLookupOptions
53
+ battingOpts []cricinfo.PlayerLookupOptions
54
+ }
55
+
56
+ func (f *fakePlayerService) Close() error { return nil }
57
+
58
+ func (f *fakePlayerService) Search(_ context.Context, query string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error) {
59
+ f.searchQueries = append(f.searchQueries, query)
60
+ f.searchOpts = append(f.searchOpts, opts)
61
+ return f.searchResult, nil
62
+ }
63
+
64
+ func (f *fakePlayerService) Profile(_ context.Context, query string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error) {
65
+ f.profileQueries = append(f.profileQueries, query)
66
+ f.profileOpts = append(f.profileOpts, opts)
67
+ return f.profileResult, nil
68
+ }
69
+
70
+ func (f *fakePlayerService) News(_ context.Context, query string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error) {
71
+ f.newsQueries = append(f.newsQueries, query)
72
+ f.newsOpts = append(f.newsOpts, opts)
73
+ return f.newsResult, nil
74
+ }
75
+
76
+ func (f *fakePlayerService) Stats(_ context.Context, query string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error) {
77
+ f.statsQueries = append(f.statsQueries, query)
78
+ f.statsOpts = append(f.statsOpts, opts)
79
+ return f.statsResult, nil
80
+ }
81
+
82
+ func (f *fakePlayerService) Career(_ context.Context, query string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error) {
83
+ f.careerQueries = append(f.careerQueries, query)
84
+ f.careerOpts = append(f.careerOpts, opts)
85
+ return f.careerResult, nil
86
+ }
87
+
88
+ func (f *fakePlayerService) MatchStats(_ context.Context, playerQuery, matchQuery string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error) {
89
+ f.matchQueries = append(f.matchQueries, playerQuery)
90
+ f.matchRefs = append(f.matchRefs, matchQuery)
91
+ f.matchOpts = append(f.matchOpts, opts)
92
+ return f.matchResult, nil
93
+ }
94
+
95
+ func (f *fakePlayerService) Innings(_ context.Context, playerQuery, matchQuery string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error) {
96
+ f.inningsQueries = append(f.inningsQueries, playerQuery)
97
+ f.inningsRefs = append(f.inningsRefs, matchQuery)
98
+ f.inningsOpts = append(f.inningsOpts, opts)
99
+ return f.inningsResult, nil
100
+ }
101
+
102
+ func (f *fakePlayerService) Dismissals(_ context.Context, playerQuery, matchQuery string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error) {
103
+ f.dismissQueries = append(f.dismissQueries, playerQuery)
104
+ f.dismissRefs = append(f.dismissRefs, matchQuery)
105
+ f.dismissOpts = append(f.dismissOpts, opts)
106
+ return f.dismissals, nil
107
+ }
108
+
109
+ func (f *fakePlayerService) Deliveries(_ context.Context, playerQuery, matchQuery string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error) {
110
+ f.deliveryQueries = append(f.deliveryQueries, playerQuery)
111
+ f.deliveryRefs = append(f.deliveryRefs, matchQuery)
112
+ f.deliveryOpts = append(f.deliveryOpts, opts)
113
+ return f.deliveries, nil
114
+ }
115
+
116
+ func (f *fakePlayerService) Bowling(_ context.Context, playerQuery, matchQuery string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error) {
117
+ f.bowlingQueries = append(f.bowlingQueries, playerQuery)
118
+ f.bowlingRefs = append(f.bowlingRefs, matchQuery)
119
+ f.bowlingOpts = append(f.bowlingOpts, opts)
120
+ return f.bowlingResult, nil
121
+ }
122
+
123
+ func (f *fakePlayerService) Batting(_ context.Context, playerQuery, matchQuery string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error) {
124
+ f.battingQueries = append(f.battingQueries, playerQuery)
125
+ f.battingRefs = append(f.battingRefs, matchQuery)
126
+ f.battingOpts = append(f.battingOpts, opts)
127
+ return f.battingResult, nil
128
+ }
129
+
130
+ func TestPlayersCommandsResolveNamesAndRenderStableJSON(t *testing.T) {
131
+ service := &fakePlayerService{
132
+ searchResult: cricinfo.NewListResult(cricinfo.EntityPlayer, []any{
133
+ cricinfo.Player{ID: "253802", DisplayName: "Virat Kohli"},
134
+ }),
135
+ profileResult: cricinfo.NewDataResult(cricinfo.EntityPlayer, cricinfo.Player{
136
+ ID: "253802",
137
+ DisplayName: "Virat Kohli",
138
+ FullName: "Virat Kohli",
139
+ Position: "Top Order Batter",
140
+ NewsRef: "http://core.espnuk.org/v2/sports/cricket/athletes/253802/news",
141
+ Team: &cricinfo.PlayerAffiliation{ID: "6", Ref: "http://core.espnuk.org/v2/sports/cricket/teams/6"},
142
+ MajorTeams: []cricinfo.PlayerAffiliation{
143
+ {ID: "6", Ref: "http://core.espnuk.org/v2/sports/cricket/teams/6"},
144
+ },
145
+ }),
146
+ newsResult: cricinfo.NewListResult(cricinfo.EntityNewsArticle, []any{
147
+ cricinfo.NewsArticle{ID: "1530499", Headline: "Players with a hat-trick of POTM awards feat. Kohli, Kallis, Sehwag and more", Published: "2026-04-04T00:00Z"},
148
+ }),
149
+ statsResult: cricinfo.NewDataResult(cricinfo.EntityPlayerStats, cricinfo.PlayerStatistics{
150
+ PlayerID: "253802",
151
+ Name: "Total",
152
+ Categories: []cricinfo.StatCategory{
153
+ {
154
+ Name: "general",
155
+ DisplayName: "General",
156
+ Stats: []cricinfo.StatValue{
157
+ {Name: "matches", DisplayName: "Matches", DisplayValue: "302"},
158
+ },
159
+ },
160
+ },
161
+ }),
162
+ careerResult: cricinfo.NewDataResult(cricinfo.EntityPlayerStats, cricinfo.PlayerStatistics{
163
+ PlayerID: "253802",
164
+ Name: "Total",
165
+ }),
166
+ matchResult: cricinfo.NewDataResult(cricinfo.EntityPlayerMatch, cricinfo.PlayerMatch{
167
+ PlayerID: "253802",
168
+ PlayerName: "Virat Kohli",
169
+ MatchID: "1529474",
170
+ Summary: cricinfo.PlayerMatchSummary{
171
+ BallsFaced: 60,
172
+ StrikeRate: 55,
173
+ DismissalName: "caught",
174
+ DismissalCard: "c",
175
+ },
176
+ }),
177
+ inningsResult: cricinfo.NewListResult(cricinfo.EntityPlayerInnings, []any{
178
+ cricinfo.PlayerInnings{PlayerID: "253802", InningsNumber: 1, Period: 1},
179
+ }),
180
+ dismissals: cricinfo.NewListResult(cricinfo.EntityPlayerDismissal, []any{
181
+ cricinfo.PlayerDismissal{PlayerID: "253802", DetailRef: "http://core.espnuk.org/v2/sports/cricket/leagues/19138/events/1529474/competitions/1529474/details/52545007", DismissalCard: "c"},
182
+ }),
183
+ deliveries: cricinfo.NewListResult(cricinfo.EntityPlayerDelivery, []any{
184
+ cricinfo.DeliveryEvent{ID: "110", BatsmanPlayerID: "253802", BowlerPlayerID: "976585", Involvement: []string{"batting"}},
185
+ }),
186
+ bowlingResult: cricinfo.NewDataResult(cricinfo.EntityPlayerMatch, cricinfo.PlayerMatch{
187
+ PlayerID: "253802",
188
+ MatchID: "1529474",
189
+ Summary: cricinfo.PlayerMatchSummary{Dots: 20, EconomyRate: 3.5},
190
+ }),
191
+ battingResult: cricinfo.NewDataResult(cricinfo.EntityPlayerMatch, cricinfo.PlayerMatch{
192
+ PlayerID: "253802",
193
+ MatchID: "1529474",
194
+ Summary: cricinfo.PlayerMatchSummary{BallsFaced: 60, StrikeRate: 55},
195
+ }),
196
+ }
197
+
198
+ originalFactory := newPlayerService
199
+ newPlayerService = func() (playerCommandService, error) { return service, nil }
200
+ defer func() {
201
+ newPlayerService = originalFactory
202
+ }()
203
+
204
+ var searchOut bytes.Buffer
205
+ var searchErr bytes.Buffer
206
+ if err := Run([]string{"players", "search", "Virat", "Kohli", "--format", "json", "--limit", "5"}, &searchOut, &searchErr); err != nil {
207
+ t.Fatalf("Run players search error: %v", err)
208
+ }
209
+ searchPayload := decodeCLIJSONMap(t, searchOut.Bytes())
210
+ if searchPayload["kind"] != string(cricinfo.EntityPlayer) {
211
+ t.Fatalf("expected kind %q, got %#v", cricinfo.EntityPlayer, searchPayload["kind"])
212
+ }
213
+ if len(service.searchQueries) != 1 || service.searchQueries[0] != "Virat Kohli" {
214
+ t.Fatalf("expected joined search query, got %+v", service.searchQueries)
215
+ }
216
+ if service.searchOpts[0].Limit != 5 {
217
+ t.Fatalf("expected search limit 5, got %+v", service.searchOpts)
218
+ }
219
+
220
+ var profileOut bytes.Buffer
221
+ var profileErr bytes.Buffer
222
+ if err := Run([]string{"players", "profile", "Virat", "Kohli", "--format", "json"}, &profileOut, &profileErr); err != nil {
223
+ t.Fatalf("Run players profile error: %v", err)
224
+ }
225
+ profilePayload := decodeCLIJSONMap(t, profileOut.Bytes())
226
+ if profilePayload["kind"] != string(cricinfo.EntityPlayer) {
227
+ t.Fatalf("expected player profile kind, got %#v", profilePayload["kind"])
228
+ }
229
+ profileData, ok := profilePayload["data"].(map[string]any)
230
+ if !ok {
231
+ t.Fatalf("expected player profile object")
232
+ }
233
+ if profileData["displayName"] != "Virat Kohli" {
234
+ t.Fatalf("expected displayName in player profile, got %#v", profileData["displayName"])
235
+ }
236
+ if _, ok := profileData["majorTeams"]; !ok {
237
+ t.Fatalf("expected majorTeams in player profile output")
238
+ }
239
+
240
+ var newsOut bytes.Buffer
241
+ var newsErr bytes.Buffer
242
+ if err := Run([]string{"players", "news", "Virat", "Kohli", "--format", "text"}, &newsOut, &newsErr); err != nil {
243
+ t.Fatalf("Run players news error: %v", err)
244
+ }
245
+ if !strings.Contains(newsOut.String(), "POTM awards") {
246
+ t.Fatalf("expected news headline in text output, got %q", newsOut.String())
247
+ }
248
+
249
+ var statsOut bytes.Buffer
250
+ var statsErr bytes.Buffer
251
+ if err := Run([]string{"players", "stats", "Virat", "Kohli", "--format", "json"}, &statsOut, &statsErr); err != nil {
252
+ t.Fatalf("Run players stats error: %v", err)
253
+ }
254
+ statsPayload := decodeCLIJSONMap(t, statsOut.Bytes())
255
+ if statsPayload["kind"] != string(cricinfo.EntityPlayerStats) {
256
+ t.Fatalf("expected kind %q, got %#v", cricinfo.EntityPlayerStats, statsPayload["kind"])
257
+ }
258
+ statsData, ok := statsPayload["data"].(map[string]any)
259
+ if !ok {
260
+ t.Fatalf("expected player statistics object")
261
+ }
262
+ categories, ok := statsData["categories"].([]any)
263
+ if !ok || len(categories) == 0 {
264
+ t.Fatalf("expected grouped categories in player statistics output")
265
+ }
266
+
267
+ var careerOut bytes.Buffer
268
+ var careerErr bytes.Buffer
269
+ if err := Run([]string{"players", "career", "Virat", "Kohli", "--format", "json"}, &careerOut, &careerErr); err != nil {
270
+ t.Fatalf("Run players career error: %v", err)
271
+ }
272
+
273
+ var matchOut bytes.Buffer
274
+ var matchErr bytes.Buffer
275
+ if err := Run([]string{"players", "match-stats", "Virat", "Kohli", "--match", "1529474", "--format", "json"}, &matchOut, &matchErr); err != nil {
276
+ t.Fatalf("Run players match-stats error: %v", err)
277
+ }
278
+ matchPayload := decodeCLIJSONMap(t, matchOut.Bytes())
279
+ if matchPayload["kind"] != string(cricinfo.EntityPlayerMatch) {
280
+ t.Fatalf("expected kind %q, got %#v", cricinfo.EntityPlayerMatch, matchPayload["kind"])
281
+ }
282
+
283
+ var inningsOut bytes.Buffer
284
+ var inningsErr bytes.Buffer
285
+ if err := Run([]string{"players", "innings", "Virat", "Kohli", "--match", "1529474", "--format", "json"}, &inningsOut, &inningsErr); err != nil {
286
+ t.Fatalf("Run players innings error: %v", err)
287
+ }
288
+
289
+ var dismissOut bytes.Buffer
290
+ var dismissErr bytes.Buffer
291
+ if err := Run([]string{"players", "dismissals", "Virat", "Kohli", "--match", "1529474", "--format", "json"}, &dismissOut, &dismissErr); err != nil {
292
+ t.Fatalf("Run players dismissals error: %v", err)
293
+ }
294
+
295
+ var deliveriesOut bytes.Buffer
296
+ var deliveriesErr bytes.Buffer
297
+ if err := Run([]string{"players", "deliveries", "Virat", "Kohli", "--match", "1529474", "--format", "json"}, &deliveriesOut, &deliveriesErr); err != nil {
298
+ t.Fatalf("Run players deliveries error: %v", err)
299
+ }
300
+
301
+ var bowlingOut bytes.Buffer
302
+ var bowlingErr bytes.Buffer
303
+ if err := Run([]string{"players", "bowling", "Virat", "Kohli", "--match", "1529474", "--format", "json"}, &bowlingOut, &bowlingErr); err != nil {
304
+ t.Fatalf("Run players bowling error: %v", err)
305
+ }
306
+
307
+ var battingOut bytes.Buffer
308
+ var battingErr bytes.Buffer
309
+ if err := Run([]string{"players", "batting", "Virat", "Kohli", "--match", "1529474", "--format", "json"}, &battingOut, &battingErr); err != nil {
310
+ t.Fatalf("Run players batting error: %v", err)
311
+ }
312
+
313
+ if service.profileQueries[0] != "Virat Kohli" || service.newsQueries[0] != "Virat Kohli" || service.statsQueries[0] != "Virat Kohli" || service.careerQueries[0] != "Virat Kohli" {
314
+ t.Fatalf("expected all player commands to preserve joined alias query")
315
+ }
316
+ if len(service.matchRefs) != 1 || service.matchRefs[0] != "1529474" {
317
+ t.Fatalf("expected match context to be passed through for player match commands")
318
+ }
319
+ }
320
+
321
+ func TestPlayersHelpListsPhase10Commands(t *testing.T) {
322
+ t.Parallel()
323
+
324
+ var out bytes.Buffer
325
+ var errBuf bytes.Buffer
326
+ if err := Run([]string{"players", "--help"}, &out, &errBuf); err != nil {
327
+ t.Fatalf("Run players --help error: %v", err)
328
+ }
329
+
330
+ helpText := out.String()
331
+ for _, snippet := range []string{
332
+ "players search", "players profile", "players news", "players stats", "players career",
333
+ "players match-stats", "players innings", "players dismissals", "players deliveries", "players bowling", "players batting",
334
+ } {
335
+ if !strings.Contains(helpText, snippet) {
336
+ t.Fatalf("expected help text to include %q, got %q", snippet, helpText)
337
+ }
338
+ }
339
+ }
340
+
341
+ func TestPlayersMatchContextCommandsRequireMatchFlag(t *testing.T) {
342
+ t.Parallel()
343
+
344
+ for _, args := range [][]string{
345
+ {"players", "match-stats", "Virat Kohli"},
346
+ {"players", "innings", "Virat Kohli"},
347
+ {"players", "dismissals", "Virat Kohli"},
348
+ {"players", "deliveries", "Virat Kohli"},
349
+ {"players", "bowling", "Virat Kohli"},
350
+ {"players", "batting", "Virat Kohli"},
351
+ } {
352
+ var out bytes.Buffer
353
+ var errBuf bytes.Buffer
354
+ err := Run(args, &out, &errBuf)
355
+ if err == nil || !strings.Contains(err.Error(), "--match is required") {
356
+ t.Fatalf("expected --match required error for %v, got %v", args, err)
357
+ }
358
+ }
359
+ }
360
+
361
+ func TestPlayersProfileJSONIsStable(t *testing.T) {
362
+ t.Parallel()
363
+
364
+ player := cricinfo.Player{
365
+ ID: "1361257",
366
+ DisplayName: "Fazal Haq Shaheen",
367
+ Styles: []cricinfo.PlayerStyle{
368
+ {Type: "batting", Description: "Left-hand bat", ShortDescription: "Lhb"},
369
+ },
370
+ }
371
+
372
+ var out bytes.Buffer
373
+ if err := cricinfo.Render(&out, cricinfo.NewDataResult(cricinfo.EntityPlayer, player), cricinfo.RenderOptions{Format: "json"}); err != nil {
374
+ t.Fatalf("Render player profile json error: %v", err)
375
+ }
376
+
377
+ var payload map[string]any
378
+ if err := json.Unmarshal(out.Bytes(), &payload); err != nil {
379
+ t.Fatalf("decode rendered player profile json: %v", err)
380
+ }
381
+ if payload["kind"] != string(cricinfo.EntityPlayer) {
382
+ t.Fatalf("expected player kind, got %#v", payload["kind"])
383
+ }
384
+ }
@@ -0,0 +1,141 @@
1
+ package cli
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "strings"
7
+
8
+ "github.com/amxv/cricinfo-cli/internal/buildinfo"
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ const commandName = "cricinfo"
13
+
14
+ type globalOptions struct {
15
+ format string
16
+ verbose bool
17
+ allFields bool
18
+ version bool
19
+ }
20
+
21
+ func Run(args []string, stdout, stderr io.Writer) error {
22
+ root := newRootCommand(stdout, stderr)
23
+ root.SetArgs(args)
24
+
25
+ if err := root.Execute(); err != nil {
26
+ return normalizeCommandError(err)
27
+ }
28
+
29
+ return nil
30
+ }
31
+
32
+ func newRootCommand(stdout, stderr io.Writer) *cobra.Command {
33
+ opts := &globalOptions{}
34
+
35
+ root := &cobra.Command{
36
+ Use: commandName,
37
+ Short: "Explore Cricinfo cricket data from the command line.",
38
+ Long: rootLongDescription(),
39
+ SilenceUsage: true,
40
+ SilenceErrors: true,
41
+ DisableAutoGenTag: true,
42
+ DisableSuggestions: true,
43
+ PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
44
+ return validateFormat(opts.format)
45
+ },
46
+ RunE: func(cmd *cobra.Command, _ []string) error {
47
+ if opts.version {
48
+ _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", commandName, buildinfo.CurrentVersion())
49
+ return nil
50
+ }
51
+
52
+ return cmd.Help()
53
+ },
54
+ }
55
+
56
+ root.SetOut(stdout)
57
+ root.SetErr(stderr)
58
+
59
+ root.Flags().BoolVar(&opts.version, "version", false, "Print version information")
60
+ root.PersistentFlags().StringVar(&opts.format, "format", "text", "Output format: text, json, or jsonl")
61
+ root.PersistentFlags().BoolVar(&opts.verbose, "verbose", false, "Show verbose output when available")
62
+ root.PersistentFlags().BoolVar(&opts.allFields, "all-fields", false, "Include long-tail fields in output")
63
+
64
+ root.AddCommand(newMatchesCommand(opts))
65
+ root.AddCommand(newPlayersCommand(opts))
66
+ root.AddCommand(newTeamsCommand(opts))
67
+ root.AddCommand(newLeaguesCommand(opts))
68
+ root.AddCommand(newSeasonsCommand(opts))
69
+ root.AddCommand(newStandingsCommand(opts))
70
+ root.AddCommand(newCompetitionsCommand(opts))
71
+ root.AddCommand(newSearchCommand(opts))
72
+ root.AddCommand(newAnalysisCommand(opts))
73
+
74
+ return root
75
+ }
76
+
77
+ func newPlaceholderGroupCommand(name, description string, nextSteps []string) *cobra.Command {
78
+ cmd := &cobra.Command{
79
+ Use: name,
80
+ Short: description,
81
+ Long: fmt.Sprintf("%s\n\n%s",
82
+ description,
83
+ formatNextSteps(nextSteps),
84
+ ),
85
+ Args: cobra.NoArgs,
86
+ RunE: func(cmd *cobra.Command, _ []string) error {
87
+ return cmd.Help()
88
+ },
89
+ }
90
+
91
+ return cmd
92
+ }
93
+
94
+ func validateFormat(value string) error {
95
+ switch strings.ToLower(strings.TrimSpace(value)) {
96
+ case "text", "json", "jsonl":
97
+ return nil
98
+ default:
99
+ return fmt.Errorf("invalid value %q for --format (expected: text, json, jsonl)", value)
100
+ }
101
+ }
102
+
103
+ func normalizeCommandError(err error) error {
104
+ message := strings.TrimSpace(err.Error())
105
+ if message == "" {
106
+ return err
107
+ }
108
+
109
+ firstLine := strings.SplitN(message, "\n", 2)[0]
110
+
111
+ if strings.HasPrefix(firstLine, "unknown command ") || strings.HasPrefix(firstLine, "unknown flag:") {
112
+ return fmt.Errorf("%s (run `%s --help`)", firstLine, commandName)
113
+ }
114
+
115
+ return fmt.Errorf("%s", firstLine)
116
+ }
117
+
118
+ func rootLongDescription() string {
119
+ return strings.Join([]string{
120
+ "Domain-driven Cricinfo CLI for real-time match, player, team, league, season, and analysis workflows.",
121
+ "",
122
+ "Quick drill-down:",
123
+ " cricinfo matches list",
124
+ " cricinfo players profile 1361257",
125
+ " cricinfo teams roster 789643 --match 1529474",
126
+ " cricinfo leagues seasons 19138",
127
+ " cricinfo analysis bowling --metric economy --scope match:1529474",
128
+ "",
129
+ "JSON output for agents:",
130
+ " cricinfo matches show 1529474 --format json",
131
+ " cricinfo analysis dismissals --league 19138 --seasons 2025 --format json",
132
+ }, "\n")
133
+ }
134
+
135
+ func formatNextSteps(commands []string) string {
136
+ lines := []string{"Next steps:"}
137
+ for _, command := range commands {
138
+ lines = append(lines, " "+command)
139
+ }
140
+ return strings.Join(lines, "\n")
141
+ }
@@ -0,0 +1,119 @@
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 searchRuntimeOptions struct {
13
+ limit int
14
+ leagueID string
15
+ matchID string
16
+ leagueRef string
17
+ seasonRef string
18
+ }
19
+
20
+ var newSearchResolver = func() (*cricinfo.Resolver, error) {
21
+ return cricinfo.NewResolver(cricinfo.ResolverConfig{})
22
+ }
23
+
24
+ func newSearchCommand(global *globalOptions) *cobra.Command {
25
+ opts := &searchRuntimeOptions{}
26
+
27
+ cmd := &cobra.Command{
28
+ Use: "search",
29
+ Short: "Cross-entity discovery for matches, players, teams, and leagues.",
30
+ Long: strings.Join([]string{
31
+ "Search domain entities using numeric IDs, refs, or cached aliases.",
32
+ "The resolver is seeded incrementally from current events, league traversal, and roster data.",
33
+ "",
34
+ "Next steps:",
35
+ " cricinfo search players <query>",
36
+ " cricinfo search teams <query>",
37
+ " cricinfo search leagues <query>",
38
+ " cricinfo search matches <query>",
39
+ }, "\n"),
40
+ Args: cobra.NoArgs,
41
+ RunE: func(cmd *cobra.Command, _ []string) error {
42
+ return cmd.Help()
43
+ },
44
+ }
45
+
46
+ cmd.PersistentFlags().IntVar(&opts.limit, "limit", 10, "Maximum number of results to return")
47
+ cmd.PersistentFlags().StringVar(&opts.leagueID, "league", "", "Preferred league ID for context-aware resolution")
48
+ cmd.PersistentFlags().StringVar(&opts.matchID, "match", "", "Preferred match/competition ID for context-aware resolution")
49
+ cmd.PersistentFlags().StringVar(&opts.leagueRef, "league-ref", "", "Known league ref to seed search context")
50
+ cmd.PersistentFlags().StringVar(&opts.seasonRef, "season-ref", "", "Known season ref to seed league traversal context")
51
+
52
+ cmd.AddCommand(newSearchEntityCommand("players", cricinfo.EntityPlayer, global, opts))
53
+ cmd.AddCommand(newSearchEntityCommand("teams", cricinfo.EntityTeam, global, opts))
54
+ cmd.AddCommand(newSearchEntityCommand("leagues", cricinfo.EntityLeague, global, opts))
55
+ cmd.AddCommand(newSearchEntityCommand("matches", cricinfo.EntityMatch, global, opts))
56
+
57
+ return cmd
58
+ }
59
+
60
+ func newSearchEntityCommand(name string, kind cricinfo.EntityKind, global *globalOptions, searchOpts *searchRuntimeOptions) *cobra.Command {
61
+ usage := fmt.Sprintf("%s [query]", name)
62
+ command := &cobra.Command{
63
+ Use: usage,
64
+ Short: fmt.Sprintf("Search %s by id, ref, or alias", name),
65
+ Long: strings.Join([]string{
66
+ fmt.Sprintf("Search %s by numeric ID, known ref, or cached alias.", name),
67
+ "",
68
+ "Examples:",
69
+ fmt.Sprintf(" cricinfo search %s 1361257", name),
70
+ fmt.Sprintf(" cricinfo search %s \"fazal haq\"", name),
71
+ }, "\n"),
72
+ Args: cobra.ArbitraryArgs,
73
+ RunE: func(cmd *cobra.Command, args []string) error {
74
+ resolver, err := newSearchResolver()
75
+ if err != nil {
76
+ return err
77
+ }
78
+ defer func() {
79
+ _ = resolver.Close()
80
+ }()
81
+
82
+ query := strings.TrimSpace(strings.Join(args, " "))
83
+ searchResult, err := resolver.Search(context.Background(), kind, query, cricinfo.ResolveOptions{
84
+ Limit: searchOpts.limit,
85
+ LeagueID: searchOpts.leagueID,
86
+ MatchID: searchOpts.matchID,
87
+ LeagueRef: searchOpts.leagueRef,
88
+ SeasonRef: searchOpts.seasonRef,
89
+ })
90
+ if err != nil {
91
+ return err
92
+ }
93
+
94
+ items := make([]any, 0, len(searchResult.Entities))
95
+ for _, entity := range searchResult.Entities {
96
+ items = append(items, entity.ToRenderable())
97
+ }
98
+
99
+ result := cricinfo.NewListResult(kind, items)
100
+ if len(searchResult.Warnings) > 0 {
101
+ if result.Status == cricinfo.ResultStatusEmpty {
102
+ result.Status = cricinfo.ResultStatusPartial
103
+ result.Message = "partial data returned"
104
+ } else {
105
+ result = cricinfo.NewPartialListResult(kind, items, searchResult.Warnings...)
106
+ }
107
+ result.Warnings = searchResult.Warnings
108
+ }
109
+
110
+ return cricinfo.Render(cmd.OutOrStdout(), result, cricinfo.RenderOptions{
111
+ Format: global.format,
112
+ Verbose: global.verbose,
113
+ AllFields: global.allFields,
114
+ })
115
+ },
116
+ }
117
+
118
+ return command
119
+ }