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,262 @@
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 analysisCommandService interface {
13
+ Close() error
14
+ Dismissals(ctx context.Context, opts cricinfo.AnalysisDismissalOptions) (cricinfo.NormalizedResult, error)
15
+ Bowling(ctx context.Context, opts cricinfo.AnalysisMetricOptions) (cricinfo.NormalizedResult, error)
16
+ Batting(ctx context.Context, opts cricinfo.AnalysisMetricOptions) (cricinfo.NormalizedResult, error)
17
+ Partnerships(ctx context.Context, opts cricinfo.AnalysisMetricOptions) (cricinfo.NormalizedResult, error)
18
+ }
19
+
20
+ type analysisRuntimeOptions struct {
21
+ leagueID string
22
+ typeQuery string
23
+ groupQuery string
24
+ dateFrom string
25
+ dateTo string
26
+ matchLimit int
27
+ }
28
+
29
+ var newAnalysisService = func() (analysisCommandService, error) {
30
+ return cricinfo.NewAnalysisService(cricinfo.AnalysisServiceConfig{})
31
+ }
32
+
33
+ func newAnalysisCommand(global *globalOptions) *cobra.Command {
34
+ base := &analysisRuntimeOptions{}
35
+
36
+ cmd := &cobra.Command{
37
+ Use: "analysis",
38
+ Short: "Derived cricket analysis over normalized real-time hydrated data.",
39
+ Long: strings.Join([]string{
40
+ "Run ranking and grouping analysis over live scoped Cricinfo data.",
41
+ "Analysis reuses in-process hydration for the active command execution only.",
42
+ "No persistent analysis cache is written.",
43
+ "",
44
+ "Next steps:",
45
+ " cricinfo analysis dismissals --league 19138 --seasons 2024-2025",
46
+ " cricinfo analysis bowling --metric economy --scope season:2025 --league 19138",
47
+ " cricinfo analysis batting --metric strike-rate --scope match:1529474",
48
+ " cricinfo analysis partnerships --scope season:2025 --league 19138",
49
+ }, "\n"),
50
+ Args: cobra.NoArgs,
51
+ RunE: func(cmd *cobra.Command, _ []string) error {
52
+ return cmd.Help()
53
+ },
54
+ }
55
+
56
+ cmd.PersistentFlags().StringVar(&base.leagueID, "league", "", "League ID/ref/alias (required for season scope and dismissals)")
57
+ cmd.PersistentFlags().StringVar(&base.typeQuery, "type", "", "Optional season type scope for season traversal")
58
+ cmd.PersistentFlags().StringVar(&base.groupQuery, "group", "", "Optional season group scope for season traversal")
59
+ cmd.PersistentFlags().StringVar(&base.dateFrom, "date-from", "", "Optional lower date bound (RFC3339 or YYYY-MM-DD)")
60
+ cmd.PersistentFlags().StringVar(&base.dateTo, "date-to", "", "Optional upper date bound (RFC3339 or YYYY-MM-DD)")
61
+ cmd.PersistentFlags().IntVar(&base.matchLimit, "match-limit", 0, "Optional cap on scoped matches before analysis")
62
+
63
+ type dismissRuntime struct {
64
+ seasons string
65
+ groupBy string
66
+ team string
67
+ player string
68
+ dismissalType string
69
+ innings int
70
+ period int
71
+ top int
72
+ }
73
+ dismiss := &dismissRuntime{}
74
+ dismissalsCmd := &cobra.Command{
75
+ Use: "dismissals",
76
+ Short: "Rank dismissal patterns across league seasons",
77
+ Args: cobra.NoArgs,
78
+ RunE: func(cmd *cobra.Command, _ []string) error {
79
+ if strings.TrimSpace(base.leagueID) == "" {
80
+ return fmt.Errorf("--league is required")
81
+ }
82
+ if strings.TrimSpace(dismiss.seasons) == "" {
83
+ return fmt.Errorf("--seasons is required")
84
+ }
85
+ return runAnalysisCommand(cmd, global, func(ctx context.Context, service analysisCommandService) (cricinfo.NormalizedResult, error) {
86
+ return service.Dismissals(ctx, cricinfo.AnalysisDismissalOptions{
87
+ LeagueQuery: base.leagueID,
88
+ Seasons: dismiss.seasons,
89
+ TypeQuery: base.typeQuery,
90
+ GroupQuery: base.groupQuery,
91
+ DateFrom: base.dateFrom,
92
+ DateTo: base.dateTo,
93
+ MatchLimit: base.matchLimit,
94
+ GroupBy: dismiss.groupBy,
95
+ TeamQuery: dismiss.team,
96
+ PlayerQuery: dismiss.player,
97
+ DismissalType: dismiss.dismissalType,
98
+ Innings: dismiss.innings,
99
+ Period: dismiss.period,
100
+ Top: dismiss.top,
101
+ })
102
+ })
103
+ },
104
+ }
105
+ dismissalsCmd.Flags().StringVar(&dismiss.seasons, "seasons", "", "Required: season range (for example 2023-2025 or 2024,2025)")
106
+ dismissalsCmd.Flags().StringVar(&dismiss.groupBy, "group-by", "dismissal-type", "Grouping fields (comma-separated): player,team,league,season,dismissal-type,innings")
107
+ dismissalsCmd.Flags().StringVar(&dismiss.team, "team", "", "Optional team filter (id or alias)")
108
+ dismissalsCmd.Flags().StringVar(&dismiss.player, "player", "", "Optional player filter (id or alias)")
109
+ dismissalsCmd.Flags().StringVar(&dismiss.dismissalType, "dismissal-type", "", "Optional dismissal type filter (for example caught, bowled)")
110
+ dismissalsCmd.Flags().IntVar(&dismiss.innings, "innings", 0, "Optional innings filter")
111
+ dismissalsCmd.Flags().IntVar(&dismiss.period, "period", 0, "Optional period filter")
112
+ dismissalsCmd.Flags().IntVar(&dismiss.top, "top", 20, "Maximum ranked rows to return")
113
+
114
+ type metricRuntime struct {
115
+ scope string
116
+ metric string
117
+ groupBy string
118
+ team string
119
+ player string
120
+ top int
121
+ }
122
+ bowling := &metricRuntime{}
123
+ bowlingCmd := &cobra.Command{
124
+ Use: "bowling",
125
+ Short: "Rank bowling metrics by match or season scope",
126
+ Args: cobra.NoArgs,
127
+ RunE: func(cmd *cobra.Command, _ []string) error {
128
+ if strings.TrimSpace(bowling.scope) == "" {
129
+ return fmt.Errorf("--scope is required")
130
+ }
131
+ if strings.TrimSpace(bowling.metric) == "" {
132
+ return fmt.Errorf("--metric is required")
133
+ }
134
+ return runAnalysisCommand(cmd, global, func(ctx context.Context, service analysisCommandService) (cricinfo.NormalizedResult, error) {
135
+ return service.Bowling(ctx, cricinfo.AnalysisMetricOptions{
136
+ Metric: bowling.metric,
137
+ Scope: bowling.scope,
138
+ LeagueQuery: base.leagueID,
139
+ TypeQuery: base.typeQuery,
140
+ GroupQuery: base.groupQuery,
141
+ DateFrom: base.dateFrom,
142
+ DateTo: base.dateTo,
143
+ MatchLimit: base.matchLimit,
144
+ GroupBy: bowling.groupBy,
145
+ TeamQuery: bowling.team,
146
+ PlayerQuery: bowling.player,
147
+ Top: bowling.top,
148
+ })
149
+ })
150
+ },
151
+ }
152
+ bowlingCmd.Flags().StringVar(&bowling.scope, "scope", "", "Required: match:<match> or season:<season>")
153
+ bowlingCmd.Flags().StringVar(&bowling.metric, "metric", "", "Required: economy, dots, or sixes-conceded")
154
+ bowlingCmd.Flags().StringVar(&bowling.groupBy, "group-by", "player", "Grouping fields (comma-separated): player,team,league,season")
155
+ bowlingCmd.Flags().StringVar(&bowling.team, "team", "", "Optional team filter")
156
+ bowlingCmd.Flags().StringVar(&bowling.player, "player", "", "Optional player filter")
157
+ bowlingCmd.Flags().IntVar(&bowling.top, "top", 20, "Maximum ranked rows to return")
158
+
159
+ batting := &metricRuntime{}
160
+ battingCmd := &cobra.Command{
161
+ Use: "batting",
162
+ Short: "Rank batting metrics by match or season scope",
163
+ Args: cobra.NoArgs,
164
+ RunE: func(cmd *cobra.Command, _ []string) error {
165
+ if strings.TrimSpace(batting.scope) == "" {
166
+ return fmt.Errorf("--scope is required")
167
+ }
168
+ if strings.TrimSpace(batting.metric) == "" {
169
+ return fmt.Errorf("--metric is required")
170
+ }
171
+ return runAnalysisCommand(cmd, global, func(ctx context.Context, service analysisCommandService) (cricinfo.NormalizedResult, error) {
172
+ return service.Batting(ctx, cricinfo.AnalysisMetricOptions{
173
+ Metric: batting.metric,
174
+ Scope: batting.scope,
175
+ LeagueQuery: base.leagueID,
176
+ TypeQuery: base.typeQuery,
177
+ GroupQuery: base.groupQuery,
178
+ DateFrom: base.dateFrom,
179
+ DateTo: base.dateTo,
180
+ MatchLimit: base.matchLimit,
181
+ GroupBy: batting.groupBy,
182
+ TeamQuery: batting.team,
183
+ PlayerQuery: batting.player,
184
+ Top: batting.top,
185
+ })
186
+ })
187
+ },
188
+ }
189
+ battingCmd.Flags().StringVar(&batting.scope, "scope", "", "Required: match:<match> or season:<season>")
190
+ battingCmd.Flags().StringVar(&batting.metric, "metric", "", "Required: fours, sixes, or strike-rate")
191
+ battingCmd.Flags().StringVar(&batting.groupBy, "group-by", "player", "Grouping fields (comma-separated): player,team,league,season")
192
+ battingCmd.Flags().StringVar(&batting.team, "team", "", "Optional team filter")
193
+ battingCmd.Flags().StringVar(&batting.player, "player", "", "Optional player filter")
194
+ battingCmd.Flags().IntVar(&batting.top, "top", 20, "Maximum ranked rows to return")
195
+
196
+ type partnershipRuntime struct {
197
+ scope string
198
+ groupBy string
199
+ team string
200
+ innings int
201
+ top int
202
+ }
203
+ partners := &partnershipRuntime{}
204
+ partnershipsCmd := &cobra.Command{
205
+ Use: "partnerships",
206
+ Short: "Rank partnerships by match or season scope",
207
+ Args: cobra.NoArgs,
208
+ RunE: func(cmd *cobra.Command, _ []string) error {
209
+ if strings.TrimSpace(partners.scope) == "" {
210
+ return fmt.Errorf("--scope is required")
211
+ }
212
+ return runAnalysisCommand(cmd, global, func(ctx context.Context, service analysisCommandService) (cricinfo.NormalizedResult, error) {
213
+ return service.Partnerships(ctx, cricinfo.AnalysisMetricOptions{
214
+ Scope: partners.scope,
215
+ LeagueQuery: base.leagueID,
216
+ TypeQuery: base.typeQuery,
217
+ GroupQuery: base.groupQuery,
218
+ DateFrom: base.dateFrom,
219
+ DateTo: base.dateTo,
220
+ MatchLimit: base.matchLimit,
221
+ GroupBy: partners.groupBy,
222
+ TeamQuery: partners.team,
223
+ Innings: partners.innings,
224
+ Top: partners.top,
225
+ })
226
+ })
227
+ },
228
+ }
229
+ partnershipsCmd.Flags().StringVar(&partners.scope, "scope", "", "Required: match:<match> or season:<season>")
230
+ partnershipsCmd.Flags().StringVar(&partners.groupBy, "group-by", "innings", "Grouping fields (comma-separated): innings,team,league,season")
231
+ partnershipsCmd.Flags().StringVar(&partners.team, "team", "", "Optional team filter")
232
+ partnershipsCmd.Flags().IntVar(&partners.innings, "innings", 0, "Optional innings filter")
233
+ partnershipsCmd.Flags().IntVar(&partners.top, "top", 20, "Maximum ranked rows to return")
234
+
235
+ cmd.AddCommand(dismissalsCmd, bowlingCmd, battingCmd, partnershipsCmd)
236
+ return cmd
237
+ }
238
+
239
+ func runAnalysisCommand(
240
+ cmd *cobra.Command,
241
+ global *globalOptions,
242
+ fn func(ctx context.Context, service analysisCommandService) (cricinfo.NormalizedResult, error),
243
+ ) error {
244
+ service, err := newAnalysisService()
245
+ if err != nil {
246
+ return err
247
+ }
248
+ defer func() {
249
+ _ = service.Close()
250
+ }()
251
+
252
+ result, err := fn(cmd.Context(), service)
253
+ if err != nil {
254
+ return err
255
+ }
256
+
257
+ return cricinfo.Render(cmd.OutOrStdout(), result, cricinfo.RenderOptions{
258
+ Format: global.format,
259
+ Verbose: global.verbose,
260
+ AllFields: global.allFields,
261
+ })
262
+ }
@@ -0,0 +1,175 @@
1
+ package cli
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "strings"
7
+ "testing"
8
+
9
+ "github.com/amxv/cricinfo-cli/internal/cricinfo"
10
+ )
11
+
12
+ type fakeAnalysisService struct {
13
+ dismissalsResult cricinfo.NormalizedResult
14
+ bowlingResult cricinfo.NormalizedResult
15
+ battingResult cricinfo.NormalizedResult
16
+ partnershipResult cricinfo.NormalizedResult
17
+
18
+ dismissOpts []cricinfo.AnalysisDismissalOptions
19
+ bowlingOpts []cricinfo.AnalysisMetricOptions
20
+ battingOpts []cricinfo.AnalysisMetricOptions
21
+ partnerOpts []cricinfo.AnalysisMetricOptions
22
+ }
23
+
24
+ func (f *fakeAnalysisService) Close() error { return nil }
25
+
26
+ func (f *fakeAnalysisService) Dismissals(_ context.Context, opts cricinfo.AnalysisDismissalOptions) (cricinfo.NormalizedResult, error) {
27
+ f.dismissOpts = append(f.dismissOpts, opts)
28
+ return f.dismissalsResult, nil
29
+ }
30
+
31
+ func (f *fakeAnalysisService) Bowling(_ context.Context, opts cricinfo.AnalysisMetricOptions) (cricinfo.NormalizedResult, error) {
32
+ f.bowlingOpts = append(f.bowlingOpts, opts)
33
+ return f.bowlingResult, nil
34
+ }
35
+
36
+ func (f *fakeAnalysisService) Batting(_ context.Context, opts cricinfo.AnalysisMetricOptions) (cricinfo.NormalizedResult, error) {
37
+ f.battingOpts = append(f.battingOpts, opts)
38
+ return f.battingResult, nil
39
+ }
40
+
41
+ func (f *fakeAnalysisService) Partnerships(_ context.Context, opts cricinfo.AnalysisMetricOptions) (cricinfo.NormalizedResult, error) {
42
+ f.partnerOpts = append(f.partnerOpts, opts)
43
+ return f.partnershipResult, nil
44
+ }
45
+
46
+ func TestAnalysisCommandsCallServiceWithScopedOptions(t *testing.T) {
47
+ service := &fakeAnalysisService{
48
+ dismissalsResult: cricinfo.NewDataResult(cricinfo.EntityAnalysisDismiss, cricinfo.AnalysisView{Command: "dismissals"}),
49
+ bowlingResult: cricinfo.NewDataResult(cricinfo.EntityAnalysisBowl, cricinfo.AnalysisView{Command: "bowling", Metric: "economy"}),
50
+ battingResult: cricinfo.NewDataResult(cricinfo.EntityAnalysisBat, cricinfo.AnalysisView{Command: "batting", Metric: "strike-rate"}),
51
+ partnershipResult: cricinfo.NewDataResult(cricinfo.EntityAnalysisPart, cricinfo.AnalysisView{Command: "partnerships"}),
52
+ }
53
+
54
+ originalFactory := newAnalysisService
55
+ newAnalysisService = func() (analysisCommandService, error) { return service, nil }
56
+ defer func() {
57
+ newAnalysisService = originalFactory
58
+ }()
59
+
60
+ var dismissOut bytes.Buffer
61
+ var dismissErr bytes.Buffer
62
+ if err := Run([]string{
63
+ "analysis", "dismissals",
64
+ "--league", "19138",
65
+ "--seasons", "2024-2025",
66
+ "--group-by", "dismissal-type,team",
67
+ "--team", "BOOST",
68
+ "--dismissal-type", "caught",
69
+ "--innings", "1",
70
+ "--top", "12",
71
+ "--format", "json",
72
+ }, &dismissOut, &dismissErr); err != nil {
73
+ t.Fatalf("Run analysis dismissals error: %v", err)
74
+ }
75
+ dismissPayload := decodeCLIJSONMap(t, dismissOut.Bytes())
76
+ if dismissPayload["kind"] != string(cricinfo.EntityAnalysisDismiss) {
77
+ t.Fatalf("expected kind %q, got %#v", cricinfo.EntityAnalysisDismiss, dismissPayload["kind"])
78
+ }
79
+ if len(service.dismissOpts) != 1 {
80
+ t.Fatalf("expected one dismissals call, got %d", len(service.dismissOpts))
81
+ }
82
+ if service.dismissOpts[0].LeagueQuery != "19138" || service.dismissOpts[0].Seasons != "2024-2025" {
83
+ t.Fatalf("unexpected dismissals opts: %+v", service.dismissOpts[0])
84
+ }
85
+
86
+ var bowlingOut bytes.Buffer
87
+ var bowlingErr bytes.Buffer
88
+ if err := Run([]string{
89
+ "analysis", "bowling",
90
+ "--scope", "season:2025",
91
+ "--league", "19138",
92
+ "--metric", "economy",
93
+ "--group-by", "player,team",
94
+ "--player", "1361257",
95
+ "--top", "5",
96
+ "--format", "json",
97
+ }, &bowlingOut, &bowlingErr); err != nil {
98
+ t.Fatalf("Run analysis bowling error: %v", err)
99
+ }
100
+ if len(service.bowlingOpts) != 1 {
101
+ t.Fatalf("expected one bowling call, got %d", len(service.bowlingOpts))
102
+ }
103
+ if service.bowlingOpts[0].Metric != "economy" || service.bowlingOpts[0].Scope != "season:2025" {
104
+ t.Fatalf("unexpected bowling opts: %+v", service.bowlingOpts[0])
105
+ }
106
+
107
+ var battingOut bytes.Buffer
108
+ var battingErr bytes.Buffer
109
+ if err := Run([]string{
110
+ "analysis", "batting",
111
+ "--scope", "match:1529474",
112
+ "--metric", "strike-rate",
113
+ "--group-by", "player",
114
+ "--top", "7",
115
+ "--format", "json",
116
+ }, &battingOut, &battingErr); err != nil {
117
+ t.Fatalf("Run analysis batting error: %v", err)
118
+ }
119
+ if len(service.battingOpts) != 1 {
120
+ t.Fatalf("expected one batting call, got %d", len(service.battingOpts))
121
+ }
122
+ if service.battingOpts[0].Metric != "strike-rate" || service.battingOpts[0].Scope != "match:1529474" {
123
+ t.Fatalf("unexpected batting opts: %+v", service.battingOpts[0])
124
+ }
125
+
126
+ var partnershipsOut bytes.Buffer
127
+ var partnershipsErr bytes.Buffer
128
+ if err := Run([]string{
129
+ "analysis", "partnerships",
130
+ "--scope", "season:2025",
131
+ "--league", "19138",
132
+ "--group-by", "innings,team",
133
+ "--innings", "1",
134
+ "--top", "9",
135
+ "--format", "json",
136
+ }, &partnershipsOut, &partnershipsErr); err != nil {
137
+ t.Fatalf("Run analysis partnerships error: %v", err)
138
+ }
139
+ if len(service.partnerOpts) != 1 {
140
+ t.Fatalf("expected one partnerships call, got %d", len(service.partnerOpts))
141
+ }
142
+ if service.partnerOpts[0].Scope != "season:2025" || service.partnerOpts[0].Innings != 1 {
143
+ t.Fatalf("unexpected partnerships opts: %+v", service.partnerOpts[0])
144
+ }
145
+ }
146
+
147
+ func TestAnalysisCommandsValidateRequiredFlags(t *testing.T) {
148
+ service := &fakeAnalysisService{}
149
+
150
+ originalFactory := newAnalysisService
151
+ newAnalysisService = func() (analysisCommandService, error) { return service, nil }
152
+ defer func() {
153
+ newAnalysisService = originalFactory
154
+ }()
155
+
156
+ assertErrContains := func(args []string, want string) {
157
+ t.Helper()
158
+ var out bytes.Buffer
159
+ var errBuf bytes.Buffer
160
+ err := Run(args, &out, &errBuf)
161
+ if err == nil {
162
+ t.Fatalf("expected error for %v", args)
163
+ }
164
+ if !strings.Contains(err.Error(), want) {
165
+ t.Fatalf("expected %q in error for %v, got %v", want, args, err)
166
+ }
167
+ }
168
+
169
+ assertErrContains([]string{"analysis", "dismissals", "--seasons", "2025"}, "--league is required")
170
+ assertErrContains([]string{"analysis", "dismissals", "--league", "19138"}, "--seasons is required")
171
+ assertErrContains([]string{"analysis", "bowling", "--metric", "economy"}, "--scope is required")
172
+ assertErrContains([]string{"analysis", "bowling", "--scope", "match:1529474"}, "--metric is required")
173
+ assertErrContains([]string{"analysis", "batting", "--scope", "match:1529474"}, "--metric is required")
174
+ assertErrContains([]string{"analysis", "partnerships"}, "--scope is required")
175
+ }
@@ -0,0 +1,154 @@
1
+ package cli
2
+
3
+ import (
4
+ "context"
5
+ "strings"
6
+
7
+ "github.com/amxv/cricinfo-cli/internal/cricinfo"
8
+ "github.com/spf13/cobra"
9
+ )
10
+
11
+ type competitionCommandService interface {
12
+ Close() error
13
+ Show(ctx context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error)
14
+ Officials(ctx context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error)
15
+ Broadcasts(ctx context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error)
16
+ Tickets(ctx context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error)
17
+ Odds(ctx context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error)
18
+ Metadata(ctx context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error)
19
+ }
20
+
21
+ type competitionRuntimeOptions struct {
22
+ leagueID string
23
+ }
24
+
25
+ var newCompetitionService = func() (competitionCommandService, error) {
26
+ return cricinfo.NewCompetitionService(cricinfo.CompetitionServiceConfig{})
27
+ }
28
+
29
+ func newCompetitionsCommand(global *globalOptions) *cobra.Command {
30
+ opts := &competitionRuntimeOptions{}
31
+
32
+ cmd := &cobra.Command{
33
+ Use: "competitions",
34
+ Short: "Competition metadata including officials, broadcasts, tickets, odds, and aggregate views.",
35
+ Long: strings.Join([]string{
36
+ "Resolve a match/competition and drill into auxiliary competition metadata routes.",
37
+ "Empty-but-valid metadata collections are rendered as clean zero-result views.",
38
+ "",
39
+ "Next steps:",
40
+ " cricinfo competitions show <match>",
41
+ " cricinfo competitions officials <match>",
42
+ " cricinfo competitions broadcasts <match>",
43
+ " cricinfo competitions tickets <match>",
44
+ " cricinfo competitions odds <match>",
45
+ " cricinfo competitions metadata <match>",
46
+ }, "\n"),
47
+ Args: cobra.NoArgs,
48
+ RunE: func(cmd *cobra.Command, _ []string) error {
49
+ return cmd.Help()
50
+ },
51
+ }
52
+
53
+ cmd.PersistentFlags().StringVar(&opts.leagueID, "league", "", "Preferred league ID for match resolution context")
54
+
55
+ showCmd := &cobra.Command{
56
+ Use: "show <match>",
57
+ Short: "Show one competition summary",
58
+ Args: cobra.MinimumNArgs(1),
59
+ RunE: func(cmd *cobra.Command, args []string) error {
60
+ query := strings.TrimSpace(strings.Join(args, " "))
61
+ return runCompetitionCommand(cmd, global, func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error) {
62
+ return service.Show(ctx, query, cricinfo.CompetitionLookupOptions{LeagueID: opts.leagueID})
63
+ })
64
+ },
65
+ }
66
+
67
+ officialsCmd := &cobra.Command{
68
+ Use: "officials <match>",
69
+ Short: "Show competition officials entries",
70
+ Args: cobra.MinimumNArgs(1),
71
+ RunE: func(cmd *cobra.Command, args []string) error {
72
+ query := strings.TrimSpace(strings.Join(args, " "))
73
+ return runCompetitionCommand(cmd, global, func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error) {
74
+ return service.Officials(ctx, query, cricinfo.CompetitionLookupOptions{LeagueID: opts.leagueID})
75
+ })
76
+ },
77
+ }
78
+
79
+ broadcastsCmd := &cobra.Command{
80
+ Use: "broadcasts <match>",
81
+ Short: "Show competition broadcast entries",
82
+ Args: cobra.MinimumNArgs(1),
83
+ RunE: func(cmd *cobra.Command, args []string) error {
84
+ query := strings.TrimSpace(strings.Join(args, " "))
85
+ return runCompetitionCommand(cmd, global, func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error) {
86
+ return service.Broadcasts(ctx, query, cricinfo.CompetitionLookupOptions{LeagueID: opts.leagueID})
87
+ })
88
+ },
89
+ }
90
+
91
+ ticketsCmd := &cobra.Command{
92
+ Use: "tickets <match>",
93
+ Short: "Show competition ticket entries",
94
+ Args: cobra.MinimumNArgs(1),
95
+ RunE: func(cmd *cobra.Command, args []string) error {
96
+ query := strings.TrimSpace(strings.Join(args, " "))
97
+ return runCompetitionCommand(cmd, global, func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error) {
98
+ return service.Tickets(ctx, query, cricinfo.CompetitionLookupOptions{LeagueID: opts.leagueID})
99
+ })
100
+ },
101
+ }
102
+
103
+ oddsCmd := &cobra.Command{
104
+ Use: "odds <match>",
105
+ Short: "Show competition odds entries",
106
+ Args: cobra.MinimumNArgs(1),
107
+ RunE: func(cmd *cobra.Command, args []string) error {
108
+ query := strings.TrimSpace(strings.Join(args, " "))
109
+ return runCompetitionCommand(cmd, global, func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error) {
110
+ return service.Odds(ctx, query, cricinfo.CompetitionLookupOptions{LeagueID: opts.leagueID})
111
+ })
112
+ },
113
+ }
114
+
115
+ metadataCmd := &cobra.Command{
116
+ Use: "metadata <match>",
117
+ Short: "Show aggregated competition metadata across officials, broadcasts, tickets, and odds",
118
+ Args: cobra.MinimumNArgs(1),
119
+ RunE: func(cmd *cobra.Command, args []string) error {
120
+ query := strings.TrimSpace(strings.Join(args, " "))
121
+ return runCompetitionCommand(cmd, global, func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error) {
122
+ return service.Metadata(ctx, query, cricinfo.CompetitionLookupOptions{LeagueID: opts.leagueID})
123
+ })
124
+ },
125
+ }
126
+
127
+ cmd.AddCommand(showCmd, officialsCmd, broadcastsCmd, ticketsCmd, oddsCmd, metadataCmd)
128
+ return cmd
129
+ }
130
+
131
+ func runCompetitionCommand(
132
+ cmd *cobra.Command,
133
+ global *globalOptions,
134
+ fn func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error),
135
+ ) error {
136
+ service, err := newCompetitionService()
137
+ if err != nil {
138
+ return err
139
+ }
140
+ defer func() {
141
+ _ = service.Close()
142
+ }()
143
+
144
+ result, err := fn(cmd.Context(), service)
145
+ if err != nil {
146
+ return err
147
+ }
148
+
149
+ return cricinfo.Render(cmd.OutOrStdout(), result, cricinfo.RenderOptions{
150
+ Format: global.format,
151
+ Verbose: global.verbose,
152
+ AllFields: global.allFields,
153
+ })
154
+ }