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,413 @@
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 fakeMatchService struct {
14
+ listResult cricinfo.NormalizedResult
15
+ liveResult cricinfo.NormalizedResult
16
+ showResult cricinfo.NormalizedResult
17
+ statusResult cricinfo.NormalizedResult
18
+ scorecardResult cricinfo.NormalizedResult
19
+ detailsResult cricinfo.NormalizedResult
20
+ playsResult cricinfo.NormalizedResult
21
+ situationResult cricinfo.NormalizedResult
22
+ inningsResult cricinfo.NormalizedResult
23
+ partnerships cricinfo.NormalizedResult
24
+ fowResult cricinfo.NormalizedResult
25
+ deliveries cricinfo.NormalizedResult
26
+
27
+ inningsQueries []string
28
+ inningsOpts []cricinfo.MatchInningsOptions
29
+ partnershipsQueries []string
30
+ partnershipsOpts []cricinfo.MatchInningsOptions
31
+ fowQueries []string
32
+ fowOpts []cricinfo.MatchInningsOptions
33
+ deliveriesQueries []string
34
+ deliveriesOpts []cricinfo.MatchInningsOptions
35
+ }
36
+
37
+ func (f *fakeMatchService) Close() error { return nil }
38
+
39
+ func (f *fakeMatchService) List(context.Context, cricinfo.MatchListOptions) (cricinfo.NormalizedResult, error) {
40
+ return f.listResult, nil
41
+ }
42
+
43
+ func (f *fakeMatchService) Live(context.Context, cricinfo.MatchListOptions) (cricinfo.NormalizedResult, error) {
44
+ return f.liveResult, nil
45
+ }
46
+
47
+ func (f *fakeMatchService) Show(context.Context, string, cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error) {
48
+ return f.showResult, nil
49
+ }
50
+
51
+ func (f *fakeMatchService) Status(context.Context, string, cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error) {
52
+ return f.statusResult, nil
53
+ }
54
+
55
+ func (f *fakeMatchService) Scorecard(context.Context, string, cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error) {
56
+ return f.scorecardResult, nil
57
+ }
58
+
59
+ func (f *fakeMatchService) Details(context.Context, string, cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error) {
60
+ return f.detailsResult, nil
61
+ }
62
+
63
+ func (f *fakeMatchService) Plays(context.Context, string, cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error) {
64
+ return f.playsResult, nil
65
+ }
66
+
67
+ func (f *fakeMatchService) Situation(context.Context, string, cricinfo.MatchLookupOptions) (cricinfo.NormalizedResult, error) {
68
+ return f.situationResult, nil
69
+ }
70
+
71
+ func (f *fakeMatchService) Innings(_ context.Context, query string, opts cricinfo.MatchInningsOptions) (cricinfo.NormalizedResult, error) {
72
+ f.inningsQueries = append(f.inningsQueries, query)
73
+ f.inningsOpts = append(f.inningsOpts, opts)
74
+ return f.inningsResult, nil
75
+ }
76
+
77
+ func (f *fakeMatchService) Partnerships(_ context.Context, query string, opts cricinfo.MatchInningsOptions) (cricinfo.NormalizedResult, error) {
78
+ f.partnershipsQueries = append(f.partnershipsQueries, query)
79
+ f.partnershipsOpts = append(f.partnershipsOpts, opts)
80
+ return f.partnerships, nil
81
+ }
82
+
83
+ func (f *fakeMatchService) FallOfWicket(_ context.Context, query string, opts cricinfo.MatchInningsOptions) (cricinfo.NormalizedResult, error) {
84
+ f.fowQueries = append(f.fowQueries, query)
85
+ f.fowOpts = append(f.fowOpts, opts)
86
+ return f.fowResult, nil
87
+ }
88
+
89
+ func (f *fakeMatchService) Deliveries(_ context.Context, query string, opts cricinfo.MatchInningsOptions) (cricinfo.NormalizedResult, error) {
90
+ f.deliveriesQueries = append(f.deliveriesQueries, query)
91
+ f.deliveriesOpts = append(f.deliveriesOpts, opts)
92
+ return f.deliveries, nil
93
+ }
94
+
95
+ func TestMatchesCommandsRenderTextAndJSON(t *testing.T) {
96
+ match := cricinfo.Match{
97
+ ID: "1529474",
98
+ LeagueID: "19138",
99
+ EventID: "1529474",
100
+ CompetitionID: "1529474",
101
+ Description: "3rd Match",
102
+ MatchState: "Boost lead by 20 runs",
103
+ Date: "2026-04-09T05:30Z",
104
+ VenueSummary: "Aino Maina, Kandahar, Afghanistan",
105
+ ScoreSummary: "BOOST 319 & 69/2 (19 ov) | SGH 368",
106
+ Teams: []cricinfo.Team{
107
+ {ID: "789643", Name: "Boost Region", ShortName: "BOOST", ScoreSummary: "319 & 69/2 (19 ov)"},
108
+ {ID: "789647", Name: "Speen Ghar Region", ShortName: "SGH", ScoreSummary: "368"},
109
+ },
110
+ StatusRef: "http://core.espnuk.org/v2/sports/cricket/leagues/19138/events/1529474/competitions/1529474/status",
111
+ DetailsRef: "http://core.espnuk.org/v2/sports/cricket/leagues/19138/events/1529474/competitions/1529474/details",
112
+ }
113
+
114
+ scorecard := cricinfo.MatchScorecard{
115
+ MatchID: "1529474",
116
+ BattingCards: []cricinfo.BattingCard{{InningsNumber: 3, TeamName: "Boost", Players: []cricinfo.BattingCardEntry{{PlayerName: "Numan Shah", Runs: "52"}}}},
117
+ BowlingCards: []cricinfo.BowlingCard{{InningsNumber: 3, TeamName: "Speen-Ghar", Players: []cricinfo.BowlingCardEntry{{PlayerName: "Hayatullah Noori", Wickets: "1"}}}},
118
+ PartnershipCards: []cricinfo.PartnershipCard{
119
+ {InningsNumber: 3, TeamName: "Boost", Players: []cricinfo.PartnershipCardEntry{{PartnershipWicketName: "1st", PartnershipRuns: "50"}}},
120
+ },
121
+ }
122
+
123
+ x := 12.5
124
+ y := 27.25
125
+ delivery := cricinfo.DeliveryEvent{
126
+ ID: "110",
127
+ ShortText: "Amanullah to Fazal Haq Shaheen, 1 run",
128
+ BatsmanRef: "http://core.espnuk.org/v2/sports/cricket/leagues/19138/athletes/1361257",
129
+ BowlerRef: "http://core.espnuk.org/v2/sports/cricket/leagues/19138/athletes/976585",
130
+ ScoreValue: 1,
131
+ Dismissal: map[string]any{"dismissal": false, "type": ""},
132
+ PlayType: map[string]any{"id": "1", "description": "run"},
133
+ BBBTimestamp: 0,
134
+ XCoordinate: &x,
135
+ YCoordinate: &y,
136
+ }
137
+
138
+ situation := cricinfo.MatchSituation{
139
+ Ref: "http://core.espnuk.org/v2/sports/cricket/leagues/19138/events/1529474/competitions/1529474/situation",
140
+ OddsRef: "http://core.espnuk.org/v2/sports/cricket/leagues/19138/events/1529474/competitions/1529474/situation/odds",
141
+ Data: map[string]any{"session": "Day 2"},
142
+ }
143
+
144
+ service := &fakeMatchService{
145
+ listResult: cricinfo.NewListResult(cricinfo.EntityMatch, []any{match}),
146
+ liveResult: cricinfo.NewListResult(cricinfo.EntityMatch, []any{match}),
147
+ showResult: cricinfo.NewDataResult(cricinfo.EntityMatch, match),
148
+ statusResult: cricinfo.NewDataResult(cricinfo.EntityMatch, match),
149
+ scorecardResult: cricinfo.NewDataResult(cricinfo.EntityMatchScorecard, scorecard),
150
+ detailsResult: cricinfo.NewListResult(cricinfo.EntityDeliveryEvent, []any{delivery}),
151
+ playsResult: cricinfo.NewListResult(cricinfo.EntityDeliveryEvent, []any{delivery}),
152
+ situationResult: cricinfo.NewDataResult(cricinfo.EntityMatchSituation, situation),
153
+ inningsResult: cricinfo.NewListResult(cricinfo.EntityInnings, []any{
154
+ cricinfo.Innings{
155
+ TeamName: "BOOST",
156
+ InningsNumber: 1,
157
+ Period: 3,
158
+ Score: "69/2 (19 ov)",
159
+ OverTimeline: []cricinfo.InningsOver{
160
+ {Number: 12, Runs: 10, WicketCount: 1},
161
+ },
162
+ WicketTimeline: []cricinfo.InningsWicket{
163
+ {Number: 1, FOW: "60/1", Over: "11.2", DetailRef: "http://core.espnuk.org/v2/sports/cricket/leagues/19138/events/1529474/competitions/1529474/details/52545007"},
164
+ },
165
+ },
166
+ }),
167
+ partnerships: cricinfo.NewListResult(cricinfo.EntityPartnership, []any{
168
+ cricinfo.Partnership{WicketName: "1st", Runs: 60, Overs: 11.2, InningsID: "1", Period: "1"},
169
+ }),
170
+ fowResult: cricinfo.NewListResult(cricinfo.EntityFallOfWicket, []any{
171
+ cricinfo.FallOfWicket{WicketNumber: 1, Runs: 60, WicketOver: 11.2, InningsID: "1", Period: "1"},
172
+ }),
173
+ deliveries: cricinfo.NewDataResult(cricinfo.EntityInnings, cricinfo.Innings{
174
+ TeamName: "BOOST",
175
+ InningsNumber: 1,
176
+ Period: 1,
177
+ OverTimeline: []cricinfo.InningsOver{
178
+ {Number: 12, Runs: 10, WicketCount: 1},
179
+ },
180
+ WicketTimeline: []cricinfo.InningsWicket{
181
+ {Number: 1, FOW: "60/1", Over: "11.2", DetailRef: "http://core.espnuk.org/v2/sports/cricket/leagues/19138/events/1529474/competitions/1529474/details/52545007"},
182
+ },
183
+ }),
184
+ }
185
+
186
+ originalFactory := newMatchService
187
+ newMatchService = func() (matchCommandService, error) { return service, nil }
188
+ defer func() {
189
+ newMatchService = originalFactory
190
+ }()
191
+
192
+ var textOut bytes.Buffer
193
+ var textErr bytes.Buffer
194
+ if err := Run([]string{"matches", "list", "--format", "text"}, &textOut, &textErr); err != nil {
195
+ t.Fatalf("Run matches list --format text error: %v", err)
196
+ }
197
+ text := textOut.String()
198
+ if !strings.Contains(text, "Matches (1)") {
199
+ t.Fatalf("expected text list header, got %q", text)
200
+ }
201
+ if !strings.Contains(text, "BOOST") || !strings.Contains(text, "SGH") {
202
+ t.Fatalf("expected team names in text output, got %q", text)
203
+ }
204
+ if !strings.Contains(text, "Boost lead by 20 runs") {
205
+ t.Fatalf("expected match state in text output, got %q", text)
206
+ }
207
+
208
+ var jsonOut bytes.Buffer
209
+ var jsonErr bytes.Buffer
210
+ if err := Run([]string{"matches", "show", "1529474", "--format", "json"}, &jsonOut, &jsonErr); err != nil {
211
+ t.Fatalf("Run matches show --format json error: %v", err)
212
+ }
213
+
214
+ var payload map[string]any
215
+ if err := json.Unmarshal(jsonOut.Bytes(), &payload); err != nil {
216
+ t.Fatalf("decode JSON output: %v", err)
217
+ }
218
+ if payload["kind"] != string(cricinfo.EntityMatch) {
219
+ t.Fatalf("expected kind %q, got %#v", cricinfo.EntityMatch, payload["kind"])
220
+ }
221
+ data, ok := payload["data"].(map[string]any)
222
+ if !ok {
223
+ t.Fatalf("expected object data in json output")
224
+ }
225
+ if data["id"] != "1529474" {
226
+ t.Fatalf("expected id 1529474 in json output, got %#v", data["id"])
227
+ }
228
+ if data["matchState"] != "Boost lead by 20 runs" {
229
+ t.Fatalf("expected matchState in json output, got %#v", data["matchState"])
230
+ }
231
+ if data["scoreSummary"] == nil {
232
+ t.Fatalf("expected scoreSummary in json output")
233
+ }
234
+
235
+ var scorecardOut bytes.Buffer
236
+ var scorecardErr bytes.Buffer
237
+ if err := Run([]string{"matches", "scorecard", "1529474", "--format", "text"}, &scorecardOut, &scorecardErr); err != nil {
238
+ t.Fatalf("Run matches scorecard --format text error: %v", err)
239
+ }
240
+ scorecardText := scorecardOut.String()
241
+ if !strings.Contains(scorecardText, "Batting") || !strings.Contains(scorecardText, "Bowling") || !strings.Contains(scorecardText, "Partnerships") {
242
+ t.Fatalf("expected scorecard sections in text output, got %q", scorecardText)
243
+ }
244
+
245
+ var detailsOut bytes.Buffer
246
+ var detailsErr bytes.Buffer
247
+ if err := Run([]string{"matches", "details", "1529474", "--format", "json"}, &detailsOut, &detailsErr); err != nil {
248
+ t.Fatalf("Run matches details --format json error: %v", err)
249
+ }
250
+
251
+ var detailsPayload map[string]any
252
+ if err := json.Unmarshal(detailsOut.Bytes(), &detailsPayload); err != nil {
253
+ t.Fatalf("decode details JSON output: %v", err)
254
+ }
255
+ items, ok := detailsPayload["items"].([]any)
256
+ if !ok || len(items) == 0 {
257
+ t.Fatalf("expected items in details json output")
258
+ }
259
+ first, ok := items[0].(map[string]any)
260
+ if !ok {
261
+ t.Fatalf("expected first details item object")
262
+ }
263
+ if _, ok := first["dismissal"]; !ok {
264
+ t.Fatalf("expected dismissal field in details json output")
265
+ }
266
+ if _, ok := first["playType"]; !ok {
267
+ t.Fatalf("expected playType field in details json output")
268
+ }
269
+ if _, ok := first["bbbTimestamp"]; !ok {
270
+ t.Fatalf("expected bbbTimestamp field in details json output")
271
+ }
272
+ if _, ok := first["xCoordinate"]; !ok {
273
+ t.Fatalf("expected xCoordinate field in details json output")
274
+ }
275
+ if _, ok := first["yCoordinate"]; !ok {
276
+ t.Fatalf("expected yCoordinate field in details json output")
277
+ }
278
+
279
+ var playsOut bytes.Buffer
280
+ var playsErr bytes.Buffer
281
+ if err := Run([]string{"matches", "plays", "1529474", "--format", "text"}, &playsOut, &playsErr); err != nil {
282
+ t.Fatalf("Run matches plays --format text error: %v", err)
283
+ }
284
+ if !strings.Contains(playsOut.String(), "Amanullah to Fazal Haq Shaheen, 1 run") {
285
+ t.Fatalf("expected plays text output to include normalized short text, got %q", playsOut.String())
286
+ }
287
+
288
+ var situationOut bytes.Buffer
289
+ var situationErr bytes.Buffer
290
+ if err := Run([]string{"matches", "situation", "1529474", "--format", "json"}, &situationOut, &situationErr); err != nil {
291
+ t.Fatalf("Run matches situation --format json error: %v", err)
292
+ }
293
+ var situationPayload map[string]any
294
+ if err := json.Unmarshal(situationOut.Bytes(), &situationPayload); err != nil {
295
+ t.Fatalf("decode situation JSON output: %v", err)
296
+ }
297
+ if situationPayload["kind"] != string(cricinfo.EntityMatchSituation) {
298
+ t.Fatalf("expected kind %q in situation output, got %#v", cricinfo.EntityMatchSituation, situationPayload["kind"])
299
+ }
300
+
301
+ var inningsOut bytes.Buffer
302
+ var inningsErr bytes.Buffer
303
+ if err := Run([]string{"matches", "innings", "1529474", "--team", "BOOST", "--format", "text"}, &inningsOut, &inningsErr); err != nil {
304
+ t.Fatalf("Run matches innings --format text error: %v", err)
305
+ }
306
+ inningsText := inningsOut.String()
307
+ if !strings.Contains(strings.ToLower(inningsText), "innings 1/3") {
308
+ t.Fatalf("expected innings heading in text output, got %q", inningsText)
309
+ }
310
+
311
+ var partnershipsOut bytes.Buffer
312
+ var partnershipsErr bytes.Buffer
313
+ if err := Run([]string{"matches", "partnerships", "1529474", "--team", "BOOST", "--innings", "1", "--period", "1", "--format", "json"}, &partnershipsOut, &partnershipsErr); err != nil {
314
+ t.Fatalf("Run matches partnerships --format json error: %v", err)
315
+ }
316
+ var partnershipsPayload map[string]any
317
+ if err := json.Unmarshal(partnershipsOut.Bytes(), &partnershipsPayload); err != nil {
318
+ t.Fatalf("decode partnerships JSON output: %v", err)
319
+ }
320
+ if partnershipsPayload["kind"] != string(cricinfo.EntityPartnership) {
321
+ t.Fatalf("expected kind %q in partnerships output, got %#v", cricinfo.EntityPartnership, partnershipsPayload["kind"])
322
+ }
323
+
324
+ var fowOut bytes.Buffer
325
+ var fowErr bytes.Buffer
326
+ if err := Run([]string{"matches", "fow", "1529474", "--team", "BOOST", "--innings", "1", "--period", "1", "--format", "text"}, &fowOut, &fowErr); err != nil {
327
+ t.Fatalf("Run matches fow --format text error: %v", err)
328
+ }
329
+ if !strings.Contains(fowOut.String(), "Wicket 1") && !strings.Contains(fowOut.String(), "wicket 1") {
330
+ t.Fatalf("expected wicket summary in fow output, got %q", fowOut.String())
331
+ }
332
+
333
+ var deliveriesOut bytes.Buffer
334
+ var deliveriesErr bytes.Buffer
335
+ if err := Run([]string{"matches", "deliveries", "1529474", "--team", "BOOST", "--innings", "1", "--period", "1", "--format", "text"}, &deliveriesOut, &deliveriesErr); err != nil {
336
+ t.Fatalf("Run matches deliveries --format text error: %v", err)
337
+ }
338
+ deliveriesText := deliveriesOut.String()
339
+ if !strings.Contains(deliveriesText, "Over Timeline") || !strings.Contains(deliveriesText, "Wicket Timeline") {
340
+ t.Fatalf("expected deliveries timeline sections in text output, got %q", deliveriesText)
341
+ }
342
+ }
343
+
344
+ func TestMatchesHelpIncludesDrillDownHints(t *testing.T) {
345
+ var out bytes.Buffer
346
+ var errBuf bytes.Buffer
347
+
348
+ if err := Run([]string{"matches", "status", "--help"}, &out, &errBuf); err != nil {
349
+ t.Fatalf("Run matches status --help error: %v", err)
350
+ }
351
+
352
+ help := out.String()
353
+ if !strings.Contains(help, "matches scorecard") {
354
+ t.Fatalf("expected scorecard drill-down hint in help output, got %q", help)
355
+ }
356
+ if !strings.Contains(help, "matches innings") {
357
+ t.Fatalf("expected innings drill-down hint in help output, got %q", help)
358
+ }
359
+ }
360
+
361
+ func TestMatchesInningsDepthSelectorsAndRequiredFlags(t *testing.T) {
362
+ service := &fakeMatchService{
363
+ inningsResult: cricinfo.NewListResult(cricinfo.EntityInnings, []any{
364
+ cricinfo.Innings{InningsNumber: 1, Period: 2, TeamName: "BOOST"},
365
+ }),
366
+ partnerships: cricinfo.NewListResult(cricinfo.EntityPartnership, []any{
367
+ cricinfo.Partnership{InningsID: "1", Period: "2", Runs: 32, Overs: 5.5},
368
+ }),
369
+ fowResult: cricinfo.NewListResult(cricinfo.EntityFallOfWicket, []any{
370
+ cricinfo.FallOfWicket{InningsID: "1", Period: "2", WicketNumber: 1},
371
+ }),
372
+ deliveries: cricinfo.NewDataResult(cricinfo.EntityInnings, cricinfo.Innings{InningsNumber: 1, Period: 2}),
373
+ }
374
+
375
+ originalFactory := newMatchService
376
+ newMatchService = func() (matchCommandService, error) { return service, nil }
377
+ defer func() {
378
+ newMatchService = originalFactory
379
+ }()
380
+
381
+ var out bytes.Buffer
382
+ var errBuf bytes.Buffer
383
+ if err := Run([]string{"matches", "partnerships", "1529474", "--team", "Boost Region", "--innings", "1", "--period", "2", "--format", "json"}, &out, &errBuf); err != nil {
384
+ t.Fatalf("Run matches partnerships with selectors error: %v", err)
385
+ }
386
+ if len(service.partnershipsOpts) != 1 {
387
+ t.Fatalf("expected partnerships opts capture, got %d", len(service.partnershipsOpts))
388
+ }
389
+ if service.partnershipsOpts[0].TeamQuery != "Boost Region" || service.partnershipsOpts[0].Innings != 1 || service.partnershipsOpts[0].Period != 2 {
390
+ t.Fatalf("unexpected partnerships options: %+v", service.partnershipsOpts[0])
391
+ }
392
+
393
+ var missingPeriodOut bytes.Buffer
394
+ var missingPeriodErr bytes.Buffer
395
+ err := Run([]string{"matches", "partnerships", "1529474", "--team", "Boost Region", "--innings", "1"}, &missingPeriodOut, &missingPeriodErr)
396
+ if err == nil || !strings.Contains(err.Error(), "--period is required") {
397
+ t.Fatalf("expected --period required error, got %v", err)
398
+ }
399
+
400
+ var missingTeamOut bytes.Buffer
401
+ var missingTeamErr bytes.Buffer
402
+ err = Run([]string{"matches", "fow", "1529474", "--innings", "1", "--period", "2"}, &missingTeamOut, &missingTeamErr)
403
+ if err == nil || !strings.Contains(err.Error(), "--team is required") {
404
+ t.Fatalf("expected --team required error, got %v", err)
405
+ }
406
+
407
+ var missingInningsOut bytes.Buffer
408
+ var missingInningsErr bytes.Buffer
409
+ err = Run([]string{"matches", "deliveries", "1529474", "--team", "Boost Region", "--period", "2"}, &missingInningsOut, &missingInningsErr)
410
+ if err == nil || !strings.Contains(err.Error(), "--innings is required") {
411
+ t.Fatalf("expected --innings required error, got %v", err)
412
+ }
413
+ }
@@ -0,0 +1,263 @@
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 playerCommandService interface {
13
+ Close() error
14
+ Search(ctx context.Context, query string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error)
15
+ Profile(ctx context.Context, query string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error)
16
+ News(ctx context.Context, query string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error)
17
+ Stats(ctx context.Context, query string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error)
18
+ Career(ctx context.Context, query string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error)
19
+ MatchStats(ctx context.Context, playerQuery, matchQuery string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error)
20
+ Innings(ctx context.Context, playerQuery, matchQuery string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error)
21
+ Dismissals(ctx context.Context, playerQuery, matchQuery string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error)
22
+ Deliveries(ctx context.Context, playerQuery, matchQuery string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error)
23
+ Bowling(ctx context.Context, playerQuery, matchQuery string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error)
24
+ Batting(ctx context.Context, playerQuery, matchQuery string, opts cricinfo.PlayerLookupOptions) (cricinfo.NormalizedResult, error)
25
+ }
26
+
27
+ type playerRuntimeOptions struct {
28
+ leagueID string
29
+ limit int
30
+ match string
31
+ }
32
+
33
+ var newPlayerService = func() (playerCommandService, error) {
34
+ return cricinfo.NewPlayerService(cricinfo.PlayerServiceConfig{})
35
+ }
36
+
37
+ func newPlayersCommand(global *globalOptions) *cobra.Command {
38
+ opts := &playerRuntimeOptions{}
39
+
40
+ cmd := &cobra.Command{
41
+ Use: "players",
42
+ Short: "Player discovery with profile, news, and grouped career statistics.",
43
+ Long: strings.Join([]string{
44
+ "Resolve players by ID/ref/alias and inspect normalized profile, related news, and grouped statistics.",
45
+ "",
46
+ "Next steps:",
47
+ " cricinfo players search <query>",
48
+ " cricinfo players profile <player>",
49
+ " cricinfo players news <player>",
50
+ " cricinfo players stats <player>",
51
+ " cricinfo players career <player>",
52
+ " cricinfo players match-stats <player> --match <match>",
53
+ " cricinfo players innings <player> --match <match>",
54
+ " cricinfo players dismissals <player> --match <match>",
55
+ " cricinfo players deliveries <player> --match <match>",
56
+ " cricinfo players bowling <player> --match <match>",
57
+ " cricinfo players batting <player> --match <match>",
58
+ }, "\n"),
59
+ Args: cobra.NoArgs,
60
+ RunE: func(cmd *cobra.Command, _ []string) error {
61
+ return cmd.Help()
62
+ },
63
+ }
64
+
65
+ cmd.PersistentFlags().StringVar(&opts.leagueID, "league", "", "Preferred league ID for resolver context")
66
+ cmd.PersistentFlags().IntVar(&opts.limit, "limit", 10, "Maximum number of results to return for search and news")
67
+
68
+ searchCmd := &cobra.Command{
69
+ Use: "search <query>",
70
+ Short: "Search players by ID, ref, or alias",
71
+ Args: cobra.MinimumNArgs(1),
72
+ RunE: func(cmd *cobra.Command, args []string) error {
73
+ query := strings.TrimSpace(strings.Join(args, " "))
74
+ return runPlayerCommand(cmd, global, func(ctx context.Context, service playerCommandService) (cricinfo.NormalizedResult, error) {
75
+ return service.Search(ctx, query, cricinfo.PlayerLookupOptions{LeagueID: opts.leagueID, Limit: opts.limit})
76
+ })
77
+ },
78
+ }
79
+
80
+ profileCmd := &cobra.Command{
81
+ Use: "profile <player>",
82
+ Short: "Show one normalized player profile",
83
+ Args: cobra.MinimumNArgs(1),
84
+ RunE: func(cmd *cobra.Command, args []string) error {
85
+ query := strings.TrimSpace(strings.Join(args, " "))
86
+ return runPlayerCommand(cmd, global, func(ctx context.Context, service playerCommandService) (cricinfo.NormalizedResult, error) {
87
+ return service.Profile(ctx, query, cricinfo.PlayerLookupOptions{LeagueID: opts.leagueID})
88
+ })
89
+ },
90
+ }
91
+
92
+ newsCmd := &cobra.Command{
93
+ Use: "news <player>",
94
+ Short: "Show normalized related news articles for one player",
95
+ Args: cobra.MinimumNArgs(1),
96
+ RunE: func(cmd *cobra.Command, args []string) error {
97
+ query := strings.TrimSpace(strings.Join(args, " "))
98
+ return runPlayerCommand(cmd, global, func(ctx context.Context, service playerCommandService) (cricinfo.NormalizedResult, error) {
99
+ return service.News(ctx, query, cricinfo.PlayerLookupOptions{LeagueID: opts.leagueID, Limit: opts.limit})
100
+ })
101
+ },
102
+ }
103
+
104
+ statsCmd := &cobra.Command{
105
+ Use: "stats <player>",
106
+ Short: "Show grouped global player statistics",
107
+ Args: cobra.MinimumNArgs(1),
108
+ RunE: func(cmd *cobra.Command, args []string) error {
109
+ query := strings.TrimSpace(strings.Join(args, " "))
110
+ return runPlayerCommand(cmd, global, func(ctx context.Context, service playerCommandService) (cricinfo.NormalizedResult, error) {
111
+ return service.Stats(ctx, query, cricinfo.PlayerLookupOptions{LeagueID: opts.leagueID})
112
+ })
113
+ },
114
+ }
115
+
116
+ careerCmd := &cobra.Command{
117
+ Use: "career <player>",
118
+ Short: "Show grouped career statistics for one player",
119
+ Args: cobra.MinimumNArgs(1),
120
+ RunE: func(cmd *cobra.Command, args []string) error {
121
+ query := strings.TrimSpace(strings.Join(args, " "))
122
+ return runPlayerCommand(cmd, global, func(ctx context.Context, service playerCommandService) (cricinfo.NormalizedResult, error) {
123
+ return service.Career(ctx, query, cricinfo.PlayerLookupOptions{LeagueID: opts.leagueID})
124
+ })
125
+ },
126
+ }
127
+
128
+ matchStatsCmd := &cobra.Command{
129
+ Use: "match-stats <player>",
130
+ Short: "Show player-in-match batting/bowling/fielding statistics",
131
+ Args: cobra.MinimumNArgs(1),
132
+ RunE: func(cmd *cobra.Command, args []string) error {
133
+ if strings.TrimSpace(opts.match) == "" {
134
+ return fmt.Errorf("--match is required")
135
+ }
136
+ playerQuery := strings.TrimSpace(strings.Join(args, " "))
137
+ return runPlayerCommand(cmd, global, func(ctx context.Context, service playerCommandService) (cricinfo.NormalizedResult, error) {
138
+ return service.MatchStats(ctx, playerQuery, opts.match, cricinfo.PlayerLookupOptions{LeagueID: opts.leagueID})
139
+ })
140
+ },
141
+ }
142
+ matchStatsCmd.Flags().StringVar(&opts.match, "match", "", "Required: match ID/ref/alias")
143
+
144
+ inningsCmd := &cobra.Command{
145
+ Use: "innings <player>",
146
+ Short: "Show player innings splits from roster-player linescores",
147
+ Args: cobra.MinimumNArgs(1),
148
+ RunE: func(cmd *cobra.Command, args []string) error {
149
+ if strings.TrimSpace(opts.match) == "" {
150
+ return fmt.Errorf("--match is required")
151
+ }
152
+ playerQuery := strings.TrimSpace(strings.Join(args, " "))
153
+ return runPlayerCommand(cmd, global, func(ctx context.Context, service playerCommandService) (cricinfo.NormalizedResult, error) {
154
+ return service.Innings(ctx, playerQuery, opts.match, cricinfo.PlayerLookupOptions{LeagueID: opts.leagueID})
155
+ })
156
+ },
157
+ }
158
+ inningsCmd.Flags().StringVar(&opts.match, "match", "", "Required: match ID/ref/alias")
159
+
160
+ dismissalsCmd := &cobra.Command{
161
+ Use: "dismissals <player>",
162
+ Short: "Show dismissal and wicket views for a player in one match",
163
+ Args: cobra.MinimumNArgs(1),
164
+ RunE: func(cmd *cobra.Command, args []string) error {
165
+ if strings.TrimSpace(opts.match) == "" {
166
+ return fmt.Errorf("--match is required")
167
+ }
168
+ playerQuery := strings.TrimSpace(strings.Join(args, " "))
169
+ return runPlayerCommand(cmd, global, func(ctx context.Context, service playerCommandService) (cricinfo.NormalizedResult, error) {
170
+ return service.Dismissals(ctx, playerQuery, opts.match, cricinfo.PlayerLookupOptions{LeagueID: opts.leagueID})
171
+ })
172
+ },
173
+ }
174
+ dismissalsCmd.Flags().StringVar(&opts.match, "match", "", "Required: match ID/ref/alias")
175
+
176
+ deliveriesCmd := &cobra.Command{
177
+ Use: "deliveries <player>",
178
+ Short: "Show player delivery events (including coordinate-aware shots/balls) for one match",
179
+ Args: cobra.MinimumNArgs(1),
180
+ RunE: func(cmd *cobra.Command, args []string) error {
181
+ if strings.TrimSpace(opts.match) == "" {
182
+ return fmt.Errorf("--match is required")
183
+ }
184
+ playerQuery := strings.TrimSpace(strings.Join(args, " "))
185
+ return runPlayerCommand(cmd, global, func(ctx context.Context, service playerCommandService) (cricinfo.NormalizedResult, error) {
186
+ return service.Deliveries(ctx, playerQuery, opts.match, cricinfo.PlayerLookupOptions{LeagueID: opts.leagueID})
187
+ })
188
+ },
189
+ }
190
+ deliveriesCmd.Flags().StringVar(&opts.match, "match", "", "Required: match ID/ref/alias")
191
+
192
+ bowlingCmd := &cobra.Command{
193
+ Use: "bowling <player>",
194
+ Short: "Show player-in-match bowling split categories",
195
+ Args: cobra.MinimumNArgs(1),
196
+ RunE: func(cmd *cobra.Command, args []string) error {
197
+ if strings.TrimSpace(opts.match) == "" {
198
+ return fmt.Errorf("--match is required")
199
+ }
200
+ playerQuery := strings.TrimSpace(strings.Join(args, " "))
201
+ return runPlayerCommand(cmd, global, func(ctx context.Context, service playerCommandService) (cricinfo.NormalizedResult, error) {
202
+ return service.Bowling(ctx, playerQuery, opts.match, cricinfo.PlayerLookupOptions{LeagueID: opts.leagueID})
203
+ })
204
+ },
205
+ }
206
+ bowlingCmd.Flags().StringVar(&opts.match, "match", "", "Required: match ID/ref/alias")
207
+
208
+ battingCmd := &cobra.Command{
209
+ Use: "batting <player>",
210
+ Short: "Show player-in-match batting split categories",
211
+ Args: cobra.MinimumNArgs(1),
212
+ RunE: func(cmd *cobra.Command, args []string) error {
213
+ if strings.TrimSpace(opts.match) == "" {
214
+ return fmt.Errorf("--match is required")
215
+ }
216
+ playerQuery := strings.TrimSpace(strings.Join(args, " "))
217
+ return runPlayerCommand(cmd, global, func(ctx context.Context, service playerCommandService) (cricinfo.NormalizedResult, error) {
218
+ return service.Batting(ctx, playerQuery, opts.match, cricinfo.PlayerLookupOptions{LeagueID: opts.leagueID})
219
+ })
220
+ },
221
+ }
222
+ battingCmd.Flags().StringVar(&opts.match, "match", "", "Required: match ID/ref/alias")
223
+
224
+ cmd.AddCommand(
225
+ searchCmd,
226
+ profileCmd,
227
+ newsCmd,
228
+ statsCmd,
229
+ careerCmd,
230
+ matchStatsCmd,
231
+ inningsCmd,
232
+ dismissalsCmd,
233
+ deliveriesCmd,
234
+ bowlingCmd,
235
+ battingCmd,
236
+ )
237
+ return cmd
238
+ }
239
+
240
+ func runPlayerCommand(
241
+ cmd *cobra.Command,
242
+ global *globalOptions,
243
+ fn func(ctx context.Context, service playerCommandService) (cricinfo.NormalizedResult, error),
244
+ ) error {
245
+ service, err := newPlayerService()
246
+ if err != nil {
247
+ return err
248
+ }
249
+ defer func() {
250
+ _ = service.Close()
251
+ }()
252
+
253
+ result, err := fn(cmd.Context(), service)
254
+ if err != nil {
255
+ return err
256
+ }
257
+
258
+ return cricinfo.Render(cmd.OutOrStdout(), result, cricinfo.RenderOptions{
259
+ Format: global.format,
260
+ Verbose: global.verbose,
261
+ AllFields: global.allFields,
262
+ })
263
+ }